diff --git a/android/assets/modss/myFirstMod/Images/UnitIcons/Maori Warrior.png b/android/assets/modss/myFirstMod/Images/UnitIcons/Maori Warrior.png new file mode 100644 index 0000000000..5b9139b57b Binary files /dev/null and b/android/assets/modss/myFirstMod/Images/UnitIcons/Maori Warrior.png differ diff --git a/android/assets/modss/myFirstMod/game.atlas b/android/assets/modss/myFirstMod/game.atlas new file mode 100644 index 0000000000..edc265c261 --- /dev/null +++ b/android/assets/modss/myFirstMod/game.atlas @@ -0,0 +1,13 @@ + +game.png +size: 128,128 +format: RGBA8888 +filter: MipMapLinearLinear,MipMapLinearLinear +repeat: none +UnitIcons/Maori Warrior + rotate: false + xy: 2, 2 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 diff --git a/android/assets/modss/myFirstMod/game.png b/android/assets/modss/myFirstMod/game.png new file mode 100644 index 0000000000..170be4d1e8 Binary files /dev/null and b/android/assets/modss/myFirstMod/game.png differ diff --git a/android/assets/mods/myFirstMod/jsons/Units.json b/android/assets/modss/myFirstMod/jsons/Units.json similarity index 100% rename from android/assets/mods/myFirstMod/jsons/Units.json rename to android/assets/modss/myFirstMod/jsons/Units.json diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 21bebfab65..06f6f33473 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -13,7 +13,7 @@ import com.unciv.logic.GameStarter import com.unciv.logic.map.MapParameters import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameSettings -import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.TranslationFileReader import com.unciv.models.translations.Translations import com.unciv.ui.LanguagePickerScreen @@ -37,7 +37,7 @@ class UncivGame( /** For when you need to test something in an advanced game and don't have time to faff around */ val superchargedForDebug = false - var rewriteTranslationFiles = false + var rewriteTranslationFiles = true lateinit var worldScreen: WorldScreen @@ -46,7 +46,6 @@ class UncivGame( val musicLocation = "music/thatched-villagers.mp3" var isInitialized = false - lateinit var ruleset:Ruleset val translations = Translations() @@ -68,7 +67,7 @@ class UncivGame( screen = LoadingScreen() thread(name="LoadJSON") { - ruleset = Ruleset(true) + RulesetCache.loadRulesets() if (rewriteTranslationFiles) { // Yes, also when running from the Jar. Sue me. translations.readAllLanguagesTranslation() @@ -122,7 +121,8 @@ class UncivGame( fun loadGame(gameInfo:GameInfo){ this.gameInfo = gameInfo - + ImageGetter.ruleset = gameInfo.ruleSet + ImageGetter.refreshAltas() worldScreen = WorldScreen(gameInfo.getPlayerToViewAs()) setWorldScreen() } diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index a846b2be14..516fa852b6 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -10,8 +10,10 @@ import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.PlayerType import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap -import com.unciv.models.ruleset.Difficulty import com.unciv.models.metadata.GameParameters +import com.unciv.models.ruleset.Difficulty +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache import java.util.* class GameInfo { @@ -20,7 +22,7 @@ class GameInfo { /** This is used in multiplayer games, where I may have a saved game state on my phone * that is inconsistent with the saved game on the cloud */ @Transient var isUpToDate=false - @Transient var ruleSet = UncivGame.Current.ruleset + @Transient lateinit var ruleSet:Ruleset var civilizations = mutableListOf() var difficulty="Chieftain" // difficulty is game-wide, think what would happen if 2 human players could play on different difficulties? @@ -203,7 +205,7 @@ class GameInfo { // will be done here, and not in CivInfo.setTransients or CityInfo fun setTransients() { tileMap.gameInfo = this - + ruleSet = RulesetCache.getComplexRuleset(gameParameters.mods) // Renames as of version 3.1.8, because of translation conflicts with the property "Range" and the difficulty "Immortal" // Needs to be BEFORE tileMap.setTransients, because the units' setTransients is called from there diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index b41fdbcf56..8076b12c96 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -2,11 +2,11 @@ package com.unciv.logic import com.badlogic.gdx.math.Vector2 import com.unciv.Constants -import com.unciv.UncivGame import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.* -import com.unciv.models.ruleset.Ruleset import com.unciv.models.metadata.GameParameters +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache import java.util.* import kotlin.collections.ArrayList import kotlin.math.max @@ -17,20 +17,20 @@ class GameStarter{ val gameInfo = GameInfo() gameInfo.gameParameters = newGameParameters - val gameBasics = UncivGame.Current.ruleset + val ruleset = RulesetCache.getComplexRuleset(newGameParameters.mods) if(mapParameters.name!="") gameInfo.tileMap = MapSaver().loadMap(mapParameters.name) - else gameInfo.tileMap = MapGenerator().generateMap(mapParameters, gameBasics) + else gameInfo.tileMap = MapGenerator().generateMap(mapParameters, ruleset) gameInfo.tileMap.mapParameters = mapParameters - gameInfo.tileMap.setTransients(gameBasics) + gameInfo.tileMap.setTransients(ruleset) gameInfo.tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map gameInfo.difficulty = newGameParameters.difficulty - addCivilizations(newGameParameters, gameInfo, gameBasics) // this is before the setTransients so gameInfo doesn't yet have the gameBasics + addCivilizations(newGameParameters, gameInfo, ruleset) // this is before the setTransients so gameInfo doesn't yet have the gameBasics gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameinfo set @@ -41,7 +41,7 @@ class GameStarter{ for (tech in gameInfo.getDifficulty().aiFreeTechs) civInfo.tech.addTechnology(tech) - for (tech in gameBasics.technologies.values + for (tech in ruleset.technologies.values .filter { it.era() < newGameParameters.startingEra }) if (!civInfo.tech.isResearched(tech.name)) civInfo.tech.addTechnology(tech.name) diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index db0f274acc..69e1217e3b 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -20,4 +20,5 @@ class GameParameters { // Default values are the default new game var startingEra = TechEra.Ancient var isOnlineMultiplayer = false + var mods = HashSet() } \ No newline at end of file diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index d86ef88554..e2263be7f7 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -13,12 +13,11 @@ import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.stats.INamed import kotlin.collections.set -class Ruleset(load: Boolean = true) { +class Ruleset() { private val jsonParser = JsonParser() var name = "" - val mods = LinkedHashSet() val buildings = LinkedHashMap() val terrains = LinkedHashMap() val tileResources = LinkedHashMap() @@ -29,19 +28,14 @@ class Ruleset(load: Boolean = true) { val nations = LinkedHashMap() val policyBranches = LinkedHashMap() val difficulties = LinkedHashMap() + val mods = HashSet() fun clone(): Ruleset { - val newRuleset = Ruleset(false) + val newRuleset = Ruleset() newRuleset.add(this) return newRuleset } - init { - if (load) { - load(Gdx.files.internal("jsons")) - } - } - private fun createHashmap(items: Array): LinkedHashMap { val hashMap = LinkedHashMap() for (item in items) @@ -63,7 +57,7 @@ class Ruleset(load: Boolean = true) { units.putAll(ruleset.units) } - fun clearExceptModNames() { + fun clear() { buildings.clear() difficulties.clear() nations.clear() @@ -75,6 +69,7 @@ class Ruleset(load: Boolean = true) { tileResources.clear() unitPromotions.clear() units.clear() + mods.clear() } fun load(folderHandle :FileHandle ) { @@ -146,3 +141,34 @@ class Ruleset(load: Boolean = true) { } } +/** Loading mods is expensive, so let's only do it once and + * save all of the loaded rulesets somewhere for later use + * */ +object RulesetCache :HashMap(){ + fun loadRulesets(){ + this[""] = Ruleset().apply { load(Gdx.files.internal("jsons")) } + + for(modFolder in Gdx.files.local("mods").list()){ + try{ + val modRuleset = Ruleset() + modRuleset.load(modFolder.child("jsons")) + modRuleset.name = modFolder.name() + this[modRuleset.name] = modRuleset + } + catch (ex:Exception){} + } + } + + fun getBaseRuleset() = this[""]!! + + fun getComplexRuleset(mods:Collection): Ruleset { + val newRuleset = Ruleset() + newRuleset.add(getBaseRuleset()) + for(mod in mods) + if(containsKey(mod)) { + newRuleset.add(this[mod]!!) + newRuleset.mods+=mod + } + return newRuleset + } +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index da24f25525..2a22a9ab49 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -6,16 +6,16 @@ import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.InputListener import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.scenes.scene2d.ui.TextButton -import com.unciv.UncivGame import com.unciv.logic.MapSaver import com.unciv.logic.map.TileMap +import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr import com.unciv.ui.utils.CameraStageBaseScreen import com.unciv.ui.utils.onClick import com.unciv.ui.utils.setFontSize class MapEditorScreen(): CameraStageBaseScreen() { - val ruleset = UncivGame.Current.ruleset + val ruleset = RulesetCache.getBaseRuleset() var mapName = "My first map" var tileMap = TileMap() diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index f2430fcfde..fc978e4a0b 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -10,12 +10,12 @@ import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver import com.unciv.logic.GameStarter import com.unciv.logic.civilization.PlayerType +import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.utils.disable import com.unciv.ui.utils.enable import com.unciv.ui.utils.onClick -import com.unciv.ui.worldscreen.WorldScreen import com.unciv.ui.worldscreen.optionstable.OnlineMultiplayer import com.unciv.ui.worldscreen.optionstable.PopupTable import java.util.* @@ -25,7 +25,7 @@ class NewGameScreen: PickerScreen(){ val newGameParameters= UncivGame.Current.gameInfo.gameParameters val mapParameters = UncivGame.Current.gameInfo.tileMap.mapParameters - val ruleSet = UncivGame.Current.ruleset.clone() + val ruleSet = RulesetCache.getComplexRuleset(newGameParameters.mods) init { setDefaultCloseAction() @@ -105,9 +105,7 @@ class NewGameScreen: PickerScreen(){ override fun render(delta: Float) { if(newGame!=null){ - game.gameInfo=newGame!! - game.worldScreen = WorldScreen(newGame!!.currentPlayerCiv) - game.setWorldScreen() + game.loadGame(newGame!!) } super.render(delta) } diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt index b4ba87195e..f1cfdfd68b 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt @@ -1,6 +1,5 @@ package com.unciv.ui.newgamescreen -import com.badlogic.gdx.Gdx import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.SelectBox @@ -9,7 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener import com.badlogic.gdx.utils.Array import com.unciv.logic.MapSaver import com.unciv.models.metadata.GameSpeed -import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.VictoryType import com.unciv.models.ruleset.tech.TechEra import com.unciv.models.translations.tr @@ -20,7 +19,6 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay : Table(CameraStageBaseScreen.skin) { val newGameParameters = newGameScreen.newGameParameters val mapParameters = newGameScreen.mapParameters - val baseRuleset = newGameScreen.ruleSet.clone() val ruleset = newGameScreen.ruleSet init { @@ -221,46 +219,22 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay fun addModCheckboxes() { - - val modFolders = Gdx.files.local("mods") - if(!modFolders.exists()) return - val loadableMods = ArrayList() - - for (modFolder in modFolders.list()) { - if (modFolder.list().any { it.name() == "jsons" }) { - val modRuleset = Ruleset(false) - - try { - modRuleset.load(modFolder.child("jsons")) - modRuleset.name = modFolder.nameWithoutExtension() - loadableMods.add(modRuleset) - - } catch (ex: Exception) { - print(ex.message) - } - } - } + val modRulesets = RulesetCache.filter { it.key!="" }.values + if(modRulesets.isEmpty()) return fun reloadMods(){ - ruleset.clearExceptModNames() - ruleset.add(baseRuleset) - for(modName in ruleset.mods){ - val correspondingMod = loadableMods.first { it.name==modName } - ruleset.add(correspondingMod) - } + ruleset.clear() + ruleset.add(RulesetCache.getComplexRuleset(newGameParameters.mods)) } - if(loadableMods.isEmpty()) return - - add("{Mods}:".tr()).colspan(2).row() val modCheckboxTable = Table().apply { defaults().pad(10f) } - for(mod in loadableMods){ + for(mod in modRulesets){ val checkBox = CheckBox(mod.name,CameraStageBaseScreen.skin) checkBox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { - if(checkBox.isChecked) ruleset.mods.add(mod.name) - else ruleset.mods.remove(mod.name) + if(checkBox.isChecked) newGameParameters.mods.add(mod.name) + else newGameParameters.mods.remove(mod.name) reloadMods() updatePlayerPickerTable() } diff --git a/core/src/com/unciv/ui/utils/ImageGetter.kt b/core/src/com/unciv/ui/utils/ImageGetter.kt index 7f2dcc5671..346dc9bfd2 100644 --- a/core/src/com/unciv/ui/utils/ImageGetter.kt +++ b/core/src/com/unciv/ui/utils/ImageGetter.kt @@ -1,5 +1,6 @@ package com.unciv.ui.utils +import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.TextureAtlas @@ -11,8 +12,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.utils.Drawable import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable import com.badlogic.gdx.utils.Align -import com.unciv.UncivGame import com.unciv.models.ruleset.Nation +import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType object ImageGetter { @@ -24,6 +25,7 @@ object ImageGetter { // So, we now use TexturePacker in the DesktopLauncher class to pack all the different images into single images, // and the atlas is what tells us what was packed where. var atlas = TextureAtlas("game.atlas") + var ruleset = Ruleset() // We then shove all the drawables into a hashmap, because the atlas specifically tells us // that the search on it is inefficient @@ -33,14 +35,26 @@ object ImageGetter { setTextureRegionDrawables() } - fun getRuleSet() = UncivGame.Current.ruleset fun setTextureRegionDrawables(){ textureRegionDrawables.clear() + // These are the srawables from the base game for(region in atlas.regions){ val drawable =TextureRegionDrawable(region) textureRegionDrawables[region.name] = drawable } + + // These are from the mods + for(mod in ruleset.mods){ + val modAltasFile = Gdx.files.local("mods/$mod/game.atlas") + if (modAltasFile.exists()) { + val modAtlas = TextureAtlas(modAltasFile) + for (region in modAtlas.regions) { + val drawable = TextureRegionDrawable(region) + textureRegionDrawables[region.name] = drawable + } + } + } } fun refreshAltas() { @@ -112,13 +126,13 @@ object ImageGetter { return getImage("OtherIcons/Stop") if(improvementName.startsWith("StartingLocation ")){ val nationName = improvementName.removePrefix("StartingLocation ") - val nation = getRuleSet().nations[nationName]!! + val nation = ruleset.nations[nationName]!! return getNationIndicator(nation,size) } val iconGroup = getImage("ImprovementIcons/$improvementName").surroundWithCircle(size) - val improvement = getRuleSet().tileImprovements[improvementName]!! + val improvement = ruleset.tileImprovements[improvementName]!! when { improvement.food>0 -> iconGroup.circle.color= foodCircleColor improvement.production>0 -> iconGroup.circle.color= productionCircleColor @@ -131,8 +145,8 @@ object ImageGetter { } fun getConstructionImage(construction: String): Image { - if(getRuleSet().buildings.containsKey(construction)) return getImage("BuildingIcons/$construction") - if(getRuleSet().units.containsKey(construction)) return getUnitIcon(construction) + if(ruleset.buildings.containsKey(construction)) return getImage("BuildingIcons/$construction") + if(ruleset.units.containsKey(construction)) return getUnitIcon(construction) if(construction=="Nothing") return getImage("OtherIcons/Stop") return getStatIcon(construction) } @@ -180,7 +194,7 @@ object ImageGetter { fun getResourceImage(resourceName: String, size:Float): Actor { val iconGroup = getImage("ResourceIcons/$resourceName").surroundWithCircle(size) - val resource = getRuleSet().tileResources[resourceName]!! + val resource = ruleset.tileResources[resourceName]!! when { resource.food>0 -> iconGroup.circle.color= foodCircleColor resource.production>0 -> iconGroup.circle.color= productionCircleColor @@ -204,7 +218,7 @@ object ImageGetter { fun getTechIconGroup(techName: String, circleSize: Float): Group { var techIconColor = Color.WHITE - when (getRuleSet().technologies[techName]!!.era().name) { + when (ruleset.technologies[techName]!!.era().name) { "Ancient" -> techIconColor = colorFromRGB(255, 87, 35) "Classical" -> techIconColor = colorFromRGB(233, 31, 99) "Medieval" -> techIconColor = colorFromRGB(157, 39, 176) diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index 3bd99f6100..89f14d0a22 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -53,6 +53,14 @@ internal object DesktopLauncher { settings.filterMin = Texture.TextureFilter.MipMapLinearLinear TexturePacker.process(settings, "../Images", ".", "game") + // pack for mods as well + val modDirectory = File("../assets/mods") + if(modDirectory.exists()) { + for (mod in modDirectory.listFiles()!!){ + TexturePacker.process(settings, mod.path + "/Images", mod.path, "game") + } + } + val texturePackingTime = System.currentTimeMillis() - startTime println("Packing textures - "+texturePackingTime+"ms") }