From f590d8d5611bf588612dead9e95c972273b5507c Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Thu, 11 Apr 2024 22:33:10 +0200 Subject: [PATCH] Two extension features for custom maps (#11443) * Fix apparent bug removing starting locations * Remove StartingLocation legacy support (when they were TileImprovements) * Starting locations get a "usage" controlling new game "select players" * Maps get a description map creators can use to pass info to their users * Fix bad fix from last PR preventing then map preview of the initial selection to show in some cases * Wrap all those setProgrammaticChangeEvents calls * Fix None/Normal discrepancy --- .../jsons/translations/template.properties | 3 + core/src/com/unciv/logic/map/TileMap.kt | 80 ++++++++++--------- .../mapeditorscreen/MapEditorScreen.kt | 6 ++ .../tabs/MapEditorEditSubTabs.kt | 25 +++++- .../mapeditorscreen/tabs/MapEditorSaveTab.kt | 5 +- .../mapeditorscreen/tabs/MapEditorViewTab.kt | 16 ++-- .../newgamescreen/MapFileSelectTable.kt | 74 ++++++++++++----- 7 files changed, 141 insertions(+), 68 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 7a0765221f..60adaf7e8d 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -544,6 +544,8 @@ An overlay image is incompatible with world wrap and was deactivated. = Choose a Wesnoth map file = That map is invalid! = ("[code]" does not conform to TerrainCodesWML) = +Use for new game "Select players" button: = +Enter a description for the users of this map = ## Map/Tool names My new map = @@ -571,6 +573,7 @@ Spawn river from/to = Bottom left river = Bottom right river = Bottom river = +Player = # Multiplayer diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index c35952106b..43a0fbf4fc 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -29,18 +29,6 @@ import kotlin.math.abs * @param initialCapacity Passed to constructor of [tileList] */ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization { - companion object { - /** Legacy way to store starting locations - now this is used only in [translateStartingLocationsFromMap] */ - const val startingLocationPrefix = "StartingLocation " - - /** - * To be backwards compatible, a json without a startingLocations element will be recognized by an entry with this marker - * New saved maps will never have this marker and will always have a serialized startingLocations list even if empty. - * New saved maps will also never have "StartingLocation" improvements, these are converted on load in [setTransients]. - */ - private const val legacyMarker = " Legacy " - } - //region Fields, Serialized var mapParameters = MapParameters() @@ -51,8 +39,29 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization { * @param position [Vector2] of the location * @param nation Name of the nation */ - private data class StartingLocation(val position: Vector2 = Vector2.Zero, val nation: String = "") : IsPartOfGameInfoSerialization - private val startingLocations = arrayListOf(StartingLocation(Vector2.Zero, legacyMarker)) + data class StartingLocation( + val position: Vector2 = Vector2.Zero, + val nation: String = "", + val usage: Usage = Usage.default // default for maps saved pior to this feature + ) : IsPartOfGameInfoSerialization { + /** How a starting location may be used when the map is loaded for a new game */ + enum class Usage(val label: String) { + /** Starting location only */ + Normal("None"), + /** Use for "Select players from starting locations" */ + Player("Player"), + /** Use as first Human player */ + Human("Human") + ; + companion object { + val default get() = Player + } + } + } + val startingLocations = arrayListOf() + + /** Optional freeform text a mod map creator can set for their "customers" */ + var description = "" //endregion //region Fields, Transient @@ -179,6 +188,8 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization { toReturn.startingLocations.clear() toReturn.startingLocations.ensureCapacity(startingLocations.size) toReturn.startingLocations.addAll(startingLocations) + + toReturn.description = description toReturn.tileUniqueMapCache = tileUniqueMapCache return toReturn @@ -635,45 +646,33 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization { } /** - * Initialize startingLocations transients, including legacy support (maps saved with placeholder improvements) + * Initialize startingLocations transients */ fun setStartingLocationsTransients() { - if (startingLocations.size == 1 && startingLocations[0].nation == legacyMarker) - return translateStartingLocationsFromMap() startingLocationsByNation.clear() for ((position, nationName) in startingLocations) { startingLocationsByNation.addToMapOfSets(nationName, get(position)) } } - /** - * Scan and remove placeholder improvements from map and build startingLocations from them - */ - private fun translateStartingLocationsFromMap() { - startingLocations.clear() - tileList.asSequence() - .filter { it.improvement?.startsWith(startingLocationPrefix) == true } - .map { it to StartingLocation(it.position, it.improvement!!.removePrefix(startingLocationPrefix)) } - .sortedBy { it.second.nation } // vanity, or to make diffs between un-gzipped map files easier - .forEach { (tile, startingLocation) -> - tile.removeImprovement() - startingLocations.add(startingLocation) - } - setStartingLocationsTransients() - } - /** Adds a starting position, maintaining the transients + * + * Note: Will not replace an existing StartingLocation to update its [usage] * @return true if the starting position was not already stored as per [Collection]'s add */ - fun addStartingLocation(nationName: String, tile: Tile): Boolean { + fun addStartingLocation( + nationName: String, + tile: Tile, + usage: StartingLocation.Usage = StartingLocation.Usage.Player + ): Boolean { if (startingLocationsByNation.contains(nationName, tile)) return false - startingLocations.add(StartingLocation(tile.position, nationName)) + startingLocations.add(StartingLocation(tile.position, nationName, usage)) return startingLocationsByNation.addToMapOfSets(nationName, tile) } /** Removes a starting position, maintaining the transients * @return true if the starting position was removed as per [Collection]'s remove */ fun removeStartingLocation(nationName: String, tile: Tile): Boolean { - if (startingLocationsByNation.contains(nationName, tile)) return false + if (!startingLocationsByNation.contains(nationName, tile)) return false startingLocations.remove(StartingLocation(tile.position, nationName)) return startingLocationsByNation[nationName]!!.remove(tile) // we do not clean up an empty startingLocationsByNation[nationName] set - not worth it @@ -755,6 +754,13 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization { class Preview { val mapParameters = MapParameters() private val startingLocations = arrayListOf() - fun getDeclaredNations() = startingLocations.asSequence().map { it.nation }.distinct() + fun getDeclaredNations() = startingLocations.asSequence() + .filter { it.usage != StartingLocation.Usage.Normal } + .map { it.nation } + .distinct() + fun getNationsForHumanPlayer() = startingLocations.asSequence() + .filter { it.usage == StartingLocation.Usage.Human } + .map { it.nation } + .distinct() } } diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/MapEditorScreen.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/MapEditorScreen.kt index f12ad7cfb1..c41c2898ca 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/MapEditorScreen.kt @@ -21,9 +21,11 @@ import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache +import com.unciv.ui.components.UncivTextField import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyShortcutDispatcherVeto import com.unciv.ui.components.input.KeyboardPanningListener +import com.unciv.ui.components.input.onChange import com.unciv.ui.components.tilegroups.TileGroup import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageWithCustomSize @@ -83,6 +85,7 @@ class MapEditorScreen(map: TileMap? = null) : BaseScreen(), RecreateOnResize { val tabs: MapEditorMainTabs var tileClickHandler: ((tile: Tile)->Unit)? = null private var zoomController: ZoomButtonPair? = null + val descriptionTextField = UncivTextField.create("Enter a description for the users of this map") private val highlightedTileGroups = mutableListOf() @@ -98,10 +101,12 @@ class MapEditorScreen(map: TileMap? = null) : BaseScreen(), RecreateOnResize { } else { ruleset = map.ruleset ?: RulesetCache.getComplexRuleset(map.mapParameters) tileMap = map + descriptionTextField.text = map.description } mapHolder = newMapHolder() // will set up ImageGetter and translations, and all dirty flags isDirty = false + descriptionTextField.onChange { isDirty = true } tabs = MapEditorMainTabs(this) MapEditorToolsDrawer(tabs, stage, mapHolder) @@ -212,6 +217,7 @@ class MapEditorScreen(map: TileMap? = null) : BaseScreen(), RecreateOnResize { clearOverlayImages() mapHolder.remove() tileMap = map + descriptionTextField.text = map.description ruleset = newRuleset ?: RulesetCache.getComplexRuleset(map.mapParameters) mapHolder = newMapHolder() isDirty = false diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt index 43f4eb3b59..b1112619c2 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt @@ -2,9 +2,12 @@ package com.unciv.ui.screens.mapeditorscreen.tabs import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Group +import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup +import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.map.TileMap import com.unciv.logic.map.tile.RoadStatus import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.Ruleset @@ -269,6 +272,7 @@ class MapEditorEditStartsTab( private val ruleset: Ruleset ): Table(BaseScreen.skin), IMapEditorEditSubTabs { private val collator = UncivGame.Current.settings.getCollatorFromLocale() + private val usageOptionGroup = ButtonGroup() init { top() @@ -287,6 +291,8 @@ class MapEditorEditStartsTab( } } }).padBottom(0f).row() + addUsage() + // Create the nation list with the spectator nation included, and shown/interpreted as "Any Civ" starting location. // We use Nation/Spectator because it hasn't been used yet and we need an icon within the Nation. add( @@ -297,10 +303,13 @@ class MapEditorEditStartsTab( UncivGame.Current.musicController.chooseTrack(it, MusicMood.Theme, MusicTrackChooserFlags.setSpecific) val icon = "Nation/$it" val pediaLink = if (it == Constants.spectator) "" else icon + val isMajorCiv = ruleset.nations[it]?.isMajorCiv ?: false + val selectedUsage = if (isMajorCiv) TileMap.StartingLocation.Usage.values()[usageOptionGroup.checkedIndex] + else TileMap.StartingLocation.Usage.Normal editTab.setBrush(BrushHandlerType.Direct, it.spectatorToAnyCiv(), icon, pediaLink) { tile -> // toggle the starting location here, note this allows // both multiple locations per nation and multiple nations per tile - if (!tile.tileMap.addStartingLocation(it, tile)) + if (!tile.tileMap.addStartingLocation(it, tile, selectedUsage)) tile.tileMap.removeStartingLocation(it, tile) } } @@ -318,6 +327,20 @@ class MapEditorEditStartsTab( FormattedLine("[${it.name.spectatorToAnyCiv()}] starting location", link = it.name, icon = "Nation/${it.name}", size = 24) }.asIterable() + private fun addUsage() { + val table = Table() + table.defaults().pad(5f) + table.add("Use for new game \"Select players\" button:".toLabel()).colspan(3).row() + val defaultUsage = TileMap.StartingLocation.Usage.default + for (usage in TileMap.StartingLocation.Usage.values()) { + val checkBox = CheckBox(usage.label.tr(), skin) + table.add(checkBox) + usageOptionGroup.add(checkBox) + checkBox.isChecked = usage == defaultUsage + } + add(table).row() + } + override fun isDisabled() = allowedNations().none() } diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorSaveTab.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorSaveTab.kt index d63b73b2dc..ce03cf3730 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorSaveTab.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorSaveTab.kt @@ -9,8 +9,6 @@ import com.unciv.logic.files.MapSaver import com.unciv.logic.map.MapGeneratedMainType import com.unciv.logic.map.TileMap import com.unciv.models.translations.tr -import com.unciv.ui.components.widgets.AutoScrollPane -import com.unciv.ui.components.widgets.TabbedPager import com.unciv.ui.components.UncivTextField import com.unciv.ui.components.extensions.isEnabled import com.unciv.ui.components.extensions.toTextButton @@ -19,6 +17,8 @@ import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onChange import com.unciv.ui.components.input.onClick +import com.unciv.ui.components.widgets.AutoScrollPane +import com.unciv.ui.components.widgets.TabbedPager import com.unciv.ui.popups.ConfirmPopup import com.unciv.ui.popups.Popup import com.unciv.ui.popups.ToastPopup @@ -89,6 +89,7 @@ class MapEditorSaveTab( if (mapNameTextField.text.isBlank()) return editorScreen.tileMap.mapParameters.name = mapNameTextField.text editorScreen.tileMap.mapParameters.type = MapGeneratedMainType.custom + editorScreen.tileMap.description = editorScreen.descriptionTextField.text setSaveButton(false) editorScreen.startBackgroundJob("MapSaver", false) { saverThread() } } diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorViewTab.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorViewTab.kt index 8163f668ec..6a7d2f3014 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorViewTab.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorViewTab.kt @@ -101,6 +101,8 @@ class MapEditorViewTab( val statsLabel = WrappableLabel(statsText, labelWidth) add(statsLabel.apply { wrap = true }).row() + add(editorScreen.descriptionTextField).growX().row() + // Map editor must not touch tileMap.naturalWonders as it is a by lazy immutable list, // and we wouldn't be able to fix it when the natural wonders change if (editorScreen.naturalWondersNeedRefresh) { @@ -191,8 +193,7 @@ class MapEditorViewTab( lines += FormattedLine(stats.toString()) } - val nations = tile.tileMap.getTileStartingLocations(tile) - .joinToString { it.name.tr() } + val nations = tile.tileMap.getTileStartingLocationSummary(tile) if (nations.isNotEmpty()) { lines += FormattedLine() lines += FormattedLine("Starting location(s): [$nations]") @@ -257,11 +258,12 @@ class MapEditorViewTab( tileClickHandler(tile) } - private fun TileMap.getTileStartingLocations(tile: Tile?) = - startingLocationsByNation.asSequence() - .filter { tile == null || tile in it.value } - .mapNotNull { ruleset!!.nations[it.key] } - .sortedWith(compareBy{ it.isCityState }.thenBy(collator) { it.name.tr(hideIcons = true) }) + private fun TileMap.getTileStartingLocationSummary(tile: Tile) = + startingLocations.asSequence() + .filter { it.position == tile.position } + .mapNotNull { if (it.nation in ruleset!!.nations) ruleset!!.nations[it.nation]!! to it.usage else null } + .sortedWith(compareBy>{ it.first.isCityState }.thenBy(collator) { it.first.name.tr(hideIcons = true) }) + .joinToString { "{${it.first.name}} ({${it.second.label}})".tr() } private fun TileMap.getStartingLocationSummary() = startingLocationsByNation.asSequence() diff --git a/core/src/com/unciv/ui/screens/newgamescreen/MapFileSelectTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/MapFileSelectTable.kt index 5186a1eac3..73ded176f6 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/MapFileSelectTable.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/MapFileSelectTable.kt @@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.scenes.scene2d.ui.Cell import com.badlogic.gdx.scenes.scene2d.ui.Container +import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.SelectBox import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.UncivGame @@ -39,6 +40,12 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive +private inline fun SelectBox.withoutChangeEvents(block: SelectBox.() -> Unit) { + selection.setProgrammaticChangeEvents(false) + block() + selection.setProgrammaticChangeEvents(true) +} + class MapFileSelectTable( private val newGameScreen: NewGameScreen, private val mapParameters: MapParameters @@ -49,9 +56,12 @@ class MapFileSelectTable( private val useNationsFromMapButton = "Select players from starting locations".toTextButton(AnimatedMenuPopup.SmallButtonStyle()) private val useNationsButtonCell: Cell private var mapNations = emptyList() + private var mapHumanPick: String? = null private val miniMapWrapper = Container() private var mapPreviewJob: Job? = null private var preselectedName = mapParameters.name + private val descriptionLabel = "".toLabel() + private val descriptionLabelCell: Cell