Map editor update - concurrency, resource amounts, file double-click (#9461)

This commit is contained in:
SomeTroglodyte
2023-05-27 20:44:23 +02:00
committed by GitHub
parent 1c124c9ddf
commit ef193babee
11 changed files with 141 additions and 53 deletions

View File

@ -501,6 +501,7 @@ Map copy and paste =
Position: [param] =
Starting location(s): [param] =
Continent: [param] ([amount] tiles) =
Resource abundance =
Change map to fit selected ruleset? =
Area: [amount] tiles, [amount2] continents/islands =
Area: [amount] tiles, [amount2]% water, [amount3] continents/islands =

View File

@ -8,9 +8,11 @@ import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.HexMath
import com.unciv.logic.map.MapParameters // Kdoc only
import com.unciv.logic.map.MapResources
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.mapunit.UnitMovement // Kdoc only
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.Terrain
@ -676,7 +678,7 @@ open class Tile : IsPartOfGameInfoSerialization {
/**
* @returns whether units of [civInfo] can pass through this tile, considering only civ-wide filters.
* Use [UnitMovementAlgorithms.canPassThrough] to check whether a specific unit can pass through a tile.
* Use [UnitMovement.canPassThrough] to check whether a specific unit can pass through a tile.
*/
fun canCivPassThrough(civInfo: Civilization): Boolean {
val tileOwner = getOwner()
@ -785,7 +787,10 @@ open class Tile : IsPartOfGameInfoSerialization {
fun setTileResource(newResource: TileResource, majorDeposit: Boolean? = null, rng: Random = Random.Default) {
resource = newResource.name
if (newResource.resourceType != ResourceType.Strategic) return
if (newResource.resourceType != ResourceType.Strategic) {
resourceAmount = 0
return
}
for (unique in newResource.getMatchingUniques(UniqueType.ResourceAmountOnTiles, StateForConditionals(tile = this))) {
if (matchesTerrainFilter(unique.params[0])) {

View File

@ -83,6 +83,7 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
private var hexOutlineIcon: Actor? = null
private var resourceName: String? = null
private var resourceAmount: Int = -1
private var resourceIcon: Actor? = null
private var workedIcon: Actor? = null
@ -168,15 +169,16 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
}
// If resource has changed (e.g. tech researched) - force new icon next time it's needed
if (resourceName != tile().resource) {
if (resourceName != tile().resource || resourceAmount != tile().resourceAmount) {
resourceName = tile().resource
resourceAmount = tile().resourceAmount
resourceIcon?.remove()
resourceIcon = null
}
// Get a fresh Icon if and only if necessary
if (resourceName != null && effectiveVisible && resourceIcon == null) {
val icon = ImageGetter.getResourcePortrait(resourceName!!, 20f, tile().resourceAmount)
val icon = ImageGetter.getResourcePortrait(resourceName!!, 20f, resourceAmount)
icon.center(tileGroup)
icon.x -= 22 // left
icon.y += 10 // top

View File

@ -12,13 +12,15 @@ import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.onDoubleClick
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toLabel
class MapEditorFilesTable(
initWidth: Float,
private val includeMods: Boolean = false,
private val onSelect: (FileHandle) -> Unit
private val onSelect: (FileHandle) -> Unit,
private val onDoubleClick: () -> Unit
): Table(BaseScreen.skin) {
private var selectedIndex = -1
@ -41,7 +43,7 @@ class MapEditorFilesTable(
onSelect(sortedFiles[row].file)
}
fun moveSelection(delta: Int) {
private fun moveSelection(delta: Int) {
selectedIndex = when {
selectedIndex + delta in sortedFiles.indices ->
selectedIndex + delta
@ -92,6 +94,10 @@ class MapEditorFilesTable(
mapButton.onClick {
markSelection(mapButton, index)
}
mapButton.onDoubleClick {
markSelection(mapButton, index)
onDoubleClick()
}
add(mapButton).row()
}
layout()

View File

@ -22,12 +22,15 @@ import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardPanningListener
import com.unciv.ui.screens.basescreen.RecreateOnResize
import com.unciv.ui.screens.worldscreen.ZoomButtonPair
import com.unciv.utils.Concurrency
import com.unciv.utils.Dispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
//todo normalize properly
//todo Remove "Area: [amount] tiles, [amount2] continents/islands = " after 2022-07-01
//todo Direct Strategic Resource abundance control
//todo functional Tab for Units (empty Tab is prepared but commented out in MapEditorEditTab.AllEditSubTabs)
//todo copy/paste tile areas? (As tool tab, brush sized, floodfill forbidden, tab displays copied area)
//todo Synergy with Civilopedia for drawing loose tiles / terrain icons
@ -73,6 +76,9 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
private val highlightedTileGroups = mutableListOf<TileGroup>()
// Control of background jobs - make them cancel on context changes like exit editor or resize screen
private val jobs = ArrayDeque<Job>(3)
init {
if (map == null) {
ruleset = RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!!
@ -189,6 +195,7 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
"Do you want to leave without saving the recent changes?",
"Leave"
) {
cancelJobs()
game.popScreen()
}
}
@ -216,5 +223,32 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
highlightTile(tile, color)
}
override fun recreate(): BaseScreen = MapEditorScreen(tileMap)
override fun recreate(): BaseScreen {
cancelJobs()
return MapEditorScreen(tileMap)
}
override fun dispose() {
cancelJobs()
super.dispose()
}
fun startBackgroundJob(
name: String,
isDaemon: Boolean = true,
block: suspend CoroutineScope.() -> Unit
) {
val scope = CoroutineScope(if (isDaemon) Dispatcher.DAEMON else Dispatcher.NON_DAEMON)
val newJob = Concurrency.run(name, scope, block)
jobs.add(newJob)
newJob.invokeOnCompletion {
jobs.remove(newJob)
}
}
private fun cancelJobs() {
for (job in jobs)
job.cancel()
jobs.clear()
}
}

View File

@ -156,6 +156,7 @@ class MapEditorEditResourcesTab(
add(eraser.render(0f).apply { onClick {
editTab.setBrush("Remove resource", eraserIcon, true) { tile ->
tile.resource = null
tile.resourceAmount = 0
}
} }).padBottom(0f).row()
add(
@ -165,7 +166,10 @@ class MapEditorEditResourcesTab(
) { resourceName ->
val resource = ruleset.tileResources[resourceName]!!
editTab.setBrush(resourceName, resource.makeLink()) {
it.setTileResource(resource, rng = editTab.randomness.RNG)
if (it.resource == resourceName && resource.resourceType == ResourceType.Strategic)
it.resourceAmount = (it.resourceAmount + 1).coerceAtMost(42)
else
it.setTileResource(resource, rng = editTab.randomness.RNG)
}
}).padTop(0f).row()
}

View File

@ -11,26 +11,19 @@ import com.unciv.logic.map.mapgenerator.RiverGenerator
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.translations.tr
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditFeaturesTab
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditImprovementsTab
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditResourcesTab
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditRiversTab
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditStartsTab
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditTerrainTab
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorEditWondersTab
import com.unciv.ui.screens.mapeditorscreen.MapEditorScreen
import com.unciv.ui.screens.mapeditorscreen.TileInfoNormalizer
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorOptionsTab.TileMatchFuzziness
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.UncivSlider
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.mapeditorscreen.MapEditorScreen
import com.unciv.ui.screens.mapeditorscreen.TileInfoNormalizer
import com.unciv.ui.screens.mapeditorscreen.tabs.MapEditorOptionsTab.TileMatchFuzziness
import com.unciv.utils.Log
class MapEditorEditTab(
@ -190,7 +183,7 @@ class MapEditorEditTab(
editorScreen.tileClickHandler = null
}
fun tileClickHandler(tile: Tile) {
private fun tileClickHandler(tile: Tile) {
if (brushSize < -1 || brushSize > 5 || brushHandlerType == BrushHandlerType.None) return
if (editorScreen.mapHolder.isPanning || editorScreen.mapHolder.isZooming()) return
editorScreen.hideSelection()

View File

@ -29,8 +29,8 @@ import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.utils.Concurrency
import com.unciv.utils.Log
import kotlin.concurrent.thread
class MapEditorGenerateTab(
private val editorScreen: MapEditorScreen
@ -73,7 +73,7 @@ class MapEditorGenerateTab(
val mapParameters = editorScreen.newMapParameters.clone() // this clone is very important here
val message = mapParameters.mapSize.fixUndesiredSizes(mapParameters.worldWrap)
if (message != null) {
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
ToastPopup( message, editorScreen, 4000 )
newTab.mapParametersTable.run { mapParameters.mapSize.also {
customMapSizeRadius.text = it.radius.toString()
@ -113,7 +113,7 @@ class MapEditorGenerateTab(
}
// Map generation can take a while and we don't want ANRs
thread(name = "MapGenerator", isDaemon = true) {
editorScreen.startBackgroundJob("MapEditor.MapGenerator") {
try {
val (newRuleset, generator) = if (step > MapGeneratorSteps.Landmass) null to null
else {
@ -124,7 +124,7 @@ class MapEditorGenerateTab(
MapGeneratorSteps.All -> {
val generatedMap = generator!!.generateMap(mapParameters)
val savedScale = editorScreen.mapHolder.scaleX
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
freshMapCompleted(generatedMap, mapParameters, newRuleset!!, selectPage = 0)
editorScreen.mapHolder.zoom(savedScale)
}
@ -136,7 +136,7 @@ class MapEditorGenerateTab(
mapParameters.type = editorScreen.newMapParameters.type
generator.generateSingleStep(generatedMap, step)
val savedScale = editorScreen.mapHolder.scaleX
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
freshMapCompleted(generatedMap, mapParameters, newRuleset!!, selectPage = 1)
editorScreen.mapHolder.zoom(savedScale)
}
@ -144,14 +144,14 @@ class MapEditorGenerateTab(
else -> {
editorScreen.tileMap.mapParameters.seed = mapParameters.seed
MapGenerator(editorScreen.ruleset).generateSingleStep(editorScreen.tileMap, step)
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
stepCompleted(step)
}
}
}
} catch (exception: Exception) {
Log.error("Exception while generating map", exception)
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
setButtonsEnabled(true)
Gdx.input.inputProcessor = editorScreen.stage
Popup(editorScreen).apply {

View File

@ -22,8 +22,10 @@ import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.utils.Concurrency
import com.unciv.utils.Log
import kotlin.concurrent.thread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
class MapEditorLoadTab(
private val editorScreen: MapEditorScreen,
@ -32,7 +34,9 @@ class MapEditorLoadTab(
private val mapFiles = MapEditorFilesTable(
initWidth = editorScreen.getToolsWidth() - 20f,
includeMods = true,
this::selectFile)
this::selectFile,
this::loadHandler
)
private val loadButton = "Load map".toTextButton()
private val deleteButton = "Delete map".toTextButton()
@ -53,7 +57,7 @@ class MapEditorLoadTab(
val fileTableHeight = editorScreen.stage.height - headerHeight - buttonTable.height - 2f
val scrollPane = AutoScrollPane(mapFiles, skin)
scrollPane.setOverscroll(false, true)
add(scrollPane).height(fileTableHeight).width(editorScreen.getToolsWidth() - 20f).row()
add(scrollPane).size(editorScreen.getToolsWidth() - 20f, fileTableHeight).padTop(10f).row()
add(buttonTable).row()
}
@ -63,7 +67,7 @@ class MapEditorLoadTab(
"Do you want to load another map without saving the recent changes?",
"Load map"
) {
thread(name = "MapLoader", isDaemon = true, block = this::loaderThread)
editorScreen.startBackgroundJob("MapLoader") { loaderThread() }
}
}
@ -89,18 +93,18 @@ class MapEditorLoadTab(
pager.setScrollDisabled(false)
}
fun selectFile(file: FileHandle?) {
private fun selectFile(file: FileHandle?) {
chosenMap = file
loadButton.isEnabled = (file != null)
deleteButton.isEnabled = (file != null)
deleteButton.color = if (file != null) Color.SCARLET else Color.BROWN
}
fun loaderThread() {
private fun CoroutineScope.loaderThread() {
var popup: Popup? = null
var needPopup = true // loadMap can fail faster than postRunnable runs
Gdx.app.postRunnable {
if (!needPopup) return@postRunnable
Concurrency.runOnGLThread {
if (!needPopup) return@runOnGLThread
popup = Popup(editorScreen).apply {
addGoodSizedLabel(Constants.loading)
open()
@ -108,6 +112,7 @@ class MapEditorLoadTab(
}
try {
val map = MapSaver.loadMap(chosenMap!!)
if (!isActive) return
val missingMods = map.mapParameters.mods.filter { it !in RulesetCache }.toMutableList()
// [TEMPORARY] conversion of old maps with a base ruleset contained in the mods
@ -117,12 +122,12 @@ class MapEditorLoadTab(
if (map.mapParameters.baseRuleset !in RulesetCache) missingMods += map.mapParameters.baseRuleset
if (missingMods.isNotEmpty()) {
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
needPopup = false
popup?.close()
ToastPopup("Missing mods: [${missingMods.joinToString()}]", editorScreen)
}
} else Gdx.app.postRunnable {
} else Concurrency.runOnGLThread {
Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up.
try {
// For deprecated maps, set the base ruleset field if it's still saved in the mods field
@ -155,7 +160,7 @@ class MapEditorLoadTab(
}
} catch (ex: Throwable) {
needPopup = false
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
popup?.close()
Log.error("Error loading map \"$chosenMap\"", ex)

View File

@ -1,6 +1,5 @@
package com.unciv.ui.screens.mapeditorscreen.tabs
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
@ -25,8 +24,10 @@ import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.onChange
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.utils.Concurrency
import com.unciv.utils.Log
import kotlin.concurrent.thread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
class MapEditorSaveTab(
private val editorScreen: MapEditorScreen,
@ -35,7 +36,9 @@ class MapEditorSaveTab(
private val mapFiles = MapEditorFilesTable(
initWidth = editorScreen.getToolsWidth() - 40f,
includeMods = false,
this::selectFile)
this::selectFile,
this::saveHandler
)
private val saveButton = "Save map".toTextButton()
private val deleteButton = "Delete map".toTextButton()
@ -76,11 +79,17 @@ class MapEditorSaveTab(
add(buttonTable).row()
}
private fun setSaveButton(enabled: Boolean) {
saveButton.isEnabled = enabled
saveButton.setText((if (enabled) "Save map" else "Working...").tr())
}
private fun saveHandler() {
if (mapNameTextField.text.isBlank()) return
editorScreen.tileMap.mapParameters.name = mapNameTextField.text
editorScreen.tileMap.mapParameters.type = MapGeneratedMainType.custom
thread(name = "MapSaver", block = this::saverThread)
setSaveButton(false)
editorScreen.startBackgroundJob("MapSaver", false) { saverThread() }
}
private fun deleteHandler() {
@ -106,7 +115,7 @@ class MapEditorSaveTab(
stage.keyboardFocus = null
}
fun selectFile(file: FileHandle?) {
private fun selectFile(file: FileHandle?) {
chosenMap = file
mapNameTextField.text = file?.name() ?: editorScreen.tileMap.mapParameters.name
if (mapNameTextField.text.isBlank()) mapNameTextField.text = "My new map".tr()
@ -117,22 +126,26 @@ class MapEditorSaveTab(
deleteButton.color = if (file != null) Color.SCARLET else Color.BROWN
}
private fun saverThread() {
private fun CoroutineScope.saverThread() {
try {
val mapToSave = editorScreen.getMapCloneForSave()
if (!isActive) return
mapToSave.assignContinents(TileMap.AssignContinentsMode.Reassign)
if (!isActive) return
MapSaver.saveMap(mapNameTextField.text, mapToSave)
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
ToastPopup("Map saved successfully!", editorScreen)
}
editorScreen.isDirty = false
setSaveButton(true)
} catch (ex: Exception) {
Log.error("Failed to save map", ex)
Gdx.app.postRunnable {
Concurrency.runOnGLThread {
val cantLoadGamePopup = Popup(editorScreen)
cantLoadGamePopup.addGoodSizedLabel("It looks like your map can't be saved!").row()
cantLoadGamePopup.addCloseButton()
cantLoadGamePopup.open(force = true)
setSaveButton(true)
}
}
}

View File

@ -3,6 +3,7 @@ package com.unciv.ui.screens.mapeditorscreen.tabs
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.civilization.Civilization
@ -12,15 +13,18 @@ import com.unciv.logic.map.tile.TileDescription
import com.unciv.models.Counter
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.components.ExpanderTab
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.UncivSlider
import com.unciv.ui.components.WrappableLabel
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.basescreen.BaseScreen
@ -43,6 +47,8 @@ class MapEditorViewTab(
init {
top()
// Note on width: max expander content width + 2 * expander.defaultPad + 2 * the following horizontal pad
// should not exceed editorScreen.getToolsWidth() or the page will scroll horizontally!
defaults().pad(5f, 20f)
update()
}
@ -72,7 +78,8 @@ class MapEditorViewTab(
val headerText = tileMap.mapParameters.name.ifEmpty { "New map" }
add(ExpanderTab(
headerText,
startsOutOpened = false
startsOutOpened = false,
defaultPad = 0f // See note in init
) {
val mapParameterText = tileMap.mapParameters.toString()
.replace("\"${tileMap.mapParameters.name}\" ", "")
@ -196,7 +203,7 @@ class MapEditorViewTab(
lines += FormattedLine("Continent: [$continent] ([${tile.tileMap.continentSizes[continent]}] tiles)", link = "continent")
}
tileDataCell?.setActor(MarkupRenderer.render(lines, labelWidth) {
val renderedInfo = MarkupRenderer.render(lines, labelWidth) {
if (it == "continent") {
// Visualize the continent this tile is on
editorScreen.hideSelection()
@ -209,7 +216,25 @@ class MapEditorViewTab(
// This needs CivilopediaScreen to be able to work without a GameInfo!
UncivGame.Current.pushScreen(CivilopediaScreen(tile.ruleset, link = it))
}
})
}
if (tile.resource != null && (tile.resourceAmount > 0 || tile.tileResource.resourceType == ResourceType.Strategic)) {
renderedInfo.addSeparator(Color.GRAY)
renderedInfo.add(Table().apply {
add("Resource abundance".toLabel(alignment = Align.left)).left().growX()
val slider = UncivSlider(0f, 42f, 1f,
initial = tile.resourceAmount.toFloat()
) {
tile.resourceAmount = it.toInt()
editorScreen.updateTile(tile)
editorScreen.isDirty = true
}
slider.setSnapToValues(floatArrayOf(0f,1f,2f,3f,4f,5f,6f,7f,8f,9f,10f,12f,15f,20f,30f,40f), 5f)
add(slider).right().minWidth(80f).fillX().padTop(15f)
}).fillX()
}
tileDataCell?.setActor(renderedInfo)
editorScreen.hideSelection()
editorScreen.highlightTile(tile, Color.CORAL)