Refactor: Split options into multiple files (#6857)

* Refactor: Move OptionsPopup to own package

* Refactor: Split OptionsPopup into multiple classes

# Conflicts:
#	core/src/com/unciv/ui/options/OptionsPopup.kt
This commit is contained in:
Timo T 2022-05-21 20:57:06 +02:00 committed by GitHub
parent a128ea0d59
commit 81379078fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1187 additions and 1016 deletions

View File

@ -7,7 +7,7 @@ import com.unciv.ui.utils.enable
import com.unciv.ui.utils.onClick
import com.unciv.ui.utils.LanguageTable
import com.unciv.ui.utils.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.worldscreen.mainmenu.OptionsPopup
import com.unciv.ui.options.OptionsPopup
/** A [PickerScreen] to select a language, used once on the initial run after a fresh install.
* After that, [OptionsPopup] provides the functionality.

View File

@ -0,0 +1,19 @@
package com.unciv.ui.options
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.civilopedia.MarkupRenderer
import com.unciv.ui.utils.BaseScreen
fun aboutTab(screen: BaseScreen): Table {
val version = screen.game.version
val versionAnchor = version.replace(".", "")
val lines = sequence {
yield(FormattedLine(extraImage = "banner", imageSize = 240f, centered = true))
yield(FormattedLine())
yield(FormattedLine("{Version}: $version", link = "https://github.com/yairm210/Unciv/blob/master/changelog.md#$versionAnchor"))
yield(FormattedLine("See online Readme", link = "https://github.com/yairm210/Unciv/blob/master/README.md#unciv---foss-civ-v-for-androiddesktop"))
yield(FormattedLine("Visit repository", link = "https://github.com/yairm210/Unciv"))
}
return MarkupRenderer.render(lines.toList()).pad(20f)
}

View File

@ -0,0 +1,179 @@
package com.unciv.ui.options
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Array
import com.unciv.models.metadata.GameSettings
import com.unciv.models.translations.TranslationFileWriter
import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.popup.YesNoPopup
import com.unciv.ui.utils.*
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import java.util.*
fun advancedTab(
optionsPopup: OptionsPopup,
onFontChange: () -> Unit
) = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(5f)
val settings = optionsPopup.settings
addAutosaveTurnsSelectBox(this, settings)
optionsPopup.addCheckbox(
this, "{Show experimental world wrap for maps}\n{HIGHLY EXPERIMENTAL - YOU HAVE BEEN WARNED!}",
settings.showExperimentalWorldWrap
) {
settings.showExperimentalWorldWrap = it
}
addMaxZoomSlider(this, settings)
val screen = optionsPopup.screen
if (screen.game.platformSpecificHelper != null) {
optionsPopup.addCheckbox(this, "Enable portrait orientation", settings.allowAndroidPortrait) {
settings.allowAndroidPortrait = it
// Note the following might close the options screen indirectly and delayed
screen.game.platformSpecificHelper.allowPortrait(it)
}
}
addFontFamilySelect(this, settings, optionsPopup.selectBoxMinWidth, onFontChange)
addTranslationGeneration(this, optionsPopup)
addSetUserId(this, settings, screen)
}
private fun addAutosaveTurnsSelectBox(table: Table, settings: GameSettings) {
table.add("Turns between autosaves".toLabel()).left().fillX()
val autosaveTurnsSelectBox = SelectBox<Int>(table.skin)
val autosaveTurnsArray = Array<Int>()
autosaveTurnsArray.addAll(1, 2, 5, 10)
autosaveTurnsSelectBox.items = autosaveTurnsArray
autosaveTurnsSelectBox.selected = settings.turnsBetweenAutosaves
table.add(autosaveTurnsSelectBox).pad(10f).row()
autosaveTurnsSelectBox.onChange {
settings.turnsBetweenAutosaves = autosaveTurnsSelectBox.selected
settings.save()
}
}
private
fun addFontFamilySelect(table: Table, settings: GameSettings, selectBoxMinWidth: Float, onFontChange: () -> Unit) {
table.add("Font family".toLabel()).left().fillX()
val selectCell = table.add()
table.row()
fun loadFontSelect(fonts: Array<FontFamilyData>, selectCell: Cell<Actor>) {
if (fonts.isEmpty) return
val fontSelectBox = SelectBox<FontFamilyData>(table.skin)
fontSelectBox.items = fonts
// `FontFamilyData` implements kotlin equality contract such that _only_ the invariantName field is compared.
// The Gdx SelectBox should honor that - but it doesn't, as it is a _kotlin_ thing to implement
// `==` by calling `equals`, and there's precompiled _Java_ `==` in the widget code.
// `setSelected` first calls a `contains` which can switch between using `==` and `equals` (set to `equals`)
// but just one step later (where it re-checks whether the new selection is equal to the old one)
// it does a hard `==`. Also, setSelection copies its argument to the selection var, it doesn't pull a match from `items`.
// Therefore, _selecting_ an item in a `SelectBox` by an instance of `FontFamilyData` where only the `invariantName` is valid won't work properly.
//
// This is why it's _not_ `fontSelectBox.selected = FontFamilyData(settings.fontFamily)`
val fontToSelect = settings.fontFamily
fontSelectBox.selected = fonts.firstOrNull { it.invariantName == fontToSelect } // will default to first entry if `null` is passed
selectCell.setActor(fontSelectBox).minWidth(selectBoxMinWidth).pad(10f)
fontSelectBox.onChange {
settings.fontFamily = fontSelectBox.selected.invariantName
Fonts.resetFont(settings.fontFamily)
onFontChange()
}
}
launchCrashHandling("Add Font Select") {
// This is a heavy operation and causes ANRs
val fonts = Array<FontFamilyData>().apply {
add(FontFamilyData.default)
for (font in Fonts.getAvailableFontFamilyNames())
add(font)
}
postCrashHandlingRunnable { loadFontSelect(fonts, selectCell) }
}
}
private fun addMaxZoomSlider(table: Table, settings: GameSettings) {
table.add("Max zoom out".tr()).left().fillX()
val maxZoomSlider = UncivSlider(
2f, 6f, 1f,
initial = settings.maxWorldZoomOut
) {
settings.maxWorldZoomOut = it
settings.save()
}
table.add(maxZoomSlider).pad(5f).row()
}
private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) {
if (Gdx.app.type != Application.ApplicationType.Desktop) return
val generateTranslationsButton = "Generate translation files".toTextButton()
val generateAction: () -> Unit = {
optionsPopup.tabs.selectPage("Advanced")
generateTranslationsButton.setText("Working...".tr())
launchCrashHandling("WriteTranslations") {
val result = TranslationFileWriter.writeNewTranslationFiles()
postCrashHandlingRunnable {
// notify about completion
generateTranslationsButton.setText(result.tr())
generateTranslationsButton.disable()
}
}
}
generateTranslationsButton.onClick(generateAction)
optionsPopup.keyPressDispatcher[Input.Keys.F12] = generateAction
generateTranslationsButton.addTooltip("F12", 18f)
table.add(generateTranslationsButton).colspan(2).row()
}
private fun addSetUserId(table: Table, settings: GameSettings, screen: BaseScreen) {
val idSetLabel = "".toLabel()
val takeUserIdFromClipboardButton = "Take user ID from clipboard".toTextButton()
.onClick {
try {
val clipboardContents = Gdx.app.clipboard.contents.trim()
UUID.fromString(clipboardContents)
YesNoPopup(
"Doing this will reset your current user ID to the clipboard contents - are you sure?",
{
settings.userId = clipboardContents
settings.save()
idSetLabel.setFontColor(Color.WHITE).setText("ID successfully set!".tr())
},
screen
).open(true)
idSetLabel.isVisible = true
} catch (ex: Exception) {
idSetLabel.isVisible = true
idSetLabel.setFontColor(Color.RED).setText("Invalid ID!".tr())
}
}
table.add(takeUserIdFromClipboardButton).pad(5f).colspan(2).row()
table.add(idSetLabel).colspan(2).row()
}

View File

@ -0,0 +1,101 @@
package com.unciv.ui.options
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.UncivGame
import com.unciv.logic.GameSaver
import com.unciv.logic.MapSaver
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.ui.utils.*
fun debugTab() = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(5f)
val game = UncivGame.Current
val simulateButton = "Simulate until turn:".toTextButton()
val simulateTextField = TextField(game.simulateUntilTurnForDebug.toString(), BaseScreen.skin)
val invalidInputLabel = "This is not a valid integer!".toLabel().also { it.isVisible = false }
simulateButton.onClick {
val simulateUntilTurns = simulateTextField.text.toIntOrNull()
if (simulateUntilTurns == null) {
invalidInputLabel.isVisible = true
return@onClick
}
game.simulateUntilTurnForDebug = simulateUntilTurns
invalidInputLabel.isVisible = false
game.worldScreen.nextTurn()
}
add(simulateButton)
add(simulateTextField).row()
add(invalidInputLabel).colspan(2).row()
add("Supercharged".toCheckBox(game.superchargedForDebug) {
game.superchargedForDebug = it
}).colspan(2).row()
add("View entire map".toCheckBox(game.viewEntireMapForDebug) {
game.viewEntireMapForDebug = it
}).colspan(2).row()
if (game.isGameInfoInitialized()) {
add("God mode (current game)".toCheckBox(game.gameInfo.gameParameters.godMode) {
game.gameInfo.gameParameters.godMode = it
}).colspan(2).row()
}
add("Save games compressed".toCheckBox(GameSaver.saveZipped) {
GameSaver.saveZipped = it
}).colspan(2).row()
add("Save maps compressed".toCheckBox(MapSaver.saveZipped) {
MapSaver.saveZipped = it
}).colspan(2).row()
add("Gdx Scene2D debug".toCheckBox(BaseScreen.enableSceneDebug) {
BaseScreen.enableSceneDebug = it
}).colspan(2).row()
add("Allow untyped Uniques in mod checker".toCheckBox(RulesetCache.modCheckerAllowUntypedUniques) {
RulesetCache.modCheckerAllowUntypedUniques = it
}).colspan(2).row()
add(Table().apply {
add("Unique misspelling threshold".toLabel()).left().fillX()
add(
UncivSlider(0f, 0.5f, 0.05f, initial = RulesetCache.uniqueMisspellingThreshold.toFloat()) {
RulesetCache.uniqueMisspellingThreshold = it.toDouble()
}
).minWidth(120f).pad(5f)
}).colspan(2).row()
val unlockTechsButton = "Unlock all techs".toTextButton()
unlockTechsButton.onClick {
if (!game.isGameInfoInitialized())
return@onClick
for (tech in game.gameInfo.ruleSet.technologies.keys) {
if (tech !in game.gameInfo.getCurrentPlayerCivilization().tech.techsResearched) {
game.gameInfo.getCurrentPlayerCivilization().tech.addTechnology(tech)
game.gameInfo.getCurrentPlayerCivilization().popupAlerts.removeLastOrNull()
}
}
game.gameInfo.getCurrentPlayerCivilization().updateSightAndResources()
game.worldScreen.shouldUpdate = true
}
add(unlockTechsButton).colspan(2).row()
val giveResourcesButton = "Get all strategic resources".toTextButton()
giveResourcesButton.onClick {
if (!game.isGameInfoInitialized())
return@onClick
val ownedTiles = game.gameInfo.tileMap.values.asSequence().filter { it.getOwner() == game.gameInfo.getCurrentPlayerCivilization() }
val resourceTypes = game.gameInfo.ruleSet.tileResources.values.asSequence().filter { it.resourceType == ResourceType.Strategic }
for ((tile, resource) in ownedTiles zip resourceTypes) {
tile.resource = resource.name
tile.resourceAmount = 999
// Debug option, so if it crashes on this that's relatively fine
// If this becomes a problem, check if such an improvement exists and otherwise plop down a great improvement or so
tile.improvement = resource.getImprovements().first()
}
game.gameInfo.getCurrentPlayerCivilization().updateSightAndResources()
game.worldScreen.shouldUpdate = true
}
add(giveResourcesButton).colspan(2).row()
}

View File

@ -0,0 +1,122 @@
package com.unciv.ui.options
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Array
import com.unciv.models.metadata.GameSettings
import com.unciv.models.tilesets.TileSetCache
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.WorldScreen
private val resolutionArray = com.badlogic.gdx.utils.Array(arrayOf("750x500", "900x600", "1050x700", "1200x800", "1500x1000"))
fun displayTab(
optionsPopup: OptionsPopup,
onResolutionChange: () -> Unit,
onTilesetChange: () -> Unit
) = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(2.5f)
val settings = optionsPopup.settings
optionsPopup.addCheckbox(this, "Show unit movement arrows", settings.showUnitMovements, true) { settings.showUnitMovements = it }
optionsPopup.addCheckbox(this, "Show tile yields", settings.showTileYields, true) { settings.showTileYields = it } // JN
optionsPopup.addCheckbox(this, "Show worked tiles", settings.showWorkedTiles, true) { settings.showWorkedTiles = it }
optionsPopup.addCheckbox(this, "Show resources and improvements", settings.showResourcesAndImprovements, true) {
settings.showResourcesAndImprovements = it
}
optionsPopup.addCheckbox(this, "Show tutorials", settings.showTutorials, true) { settings.showTutorials = it }
optionsPopup.addCheckbox(this, "Show pixel units", settings.showPixelUnits, true) { settings.showPixelUnits = it }
optionsPopup.addCheckbox(this, "Show pixel improvements", settings.showPixelImprovements, true) { settings.showPixelImprovements = it }
optionsPopup.addCheckbox(this, "Experimental Demographics scoreboard", settings.useDemographics, true) { settings.useDemographics = it }
addMinimapSizeSlider(this, settings, optionsPopup.screen, optionsPopup.selectBoxMinWidth)
addResolutionSelectBox(this, settings, optionsPopup.selectBoxMinWidth, onResolutionChange)
addTileSetSelectBox(this, settings, optionsPopup.selectBoxMinWidth, onTilesetChange)
optionsPopup.addCheckbox(this, "Continuous rendering", settings.continuousRendering) {
settings.continuousRendering = it
Gdx.graphics.isContinuousRendering = it
}
val continuousRenderingDescription = "When disabled, saves battery life but certain animations will be suspended"
val continuousRenderingLabel = WrappableLabel(
continuousRenderingDescription,
optionsPopup.tabs.prefWidth, Color.ORANGE.brighten(0.7f), 14
)
continuousRenderingLabel.wrap = true
add(continuousRenderingLabel).colspan(2).padTop(10f).row()
}
private fun addMinimapSizeSlider(table: Table, settings: GameSettings, screen: BaseScreen, selectBoxMinWidth: Float) {
table.add("Show minimap".toLabel()).left().fillX()
// The meaning of the values needs a formula to be synchronized between here and
// [Minimap.init]. It goes off-10%-11%..29%-30%-35%-40%-45%-50% - and the percentages
// correspond roughly to the minimap's proportion relative to screen dimensions.
val offTranslated = "off".tr() // translate only once and cache in closure
val getTipText: (Float) -> String = {
when (it) {
0f -> offTranslated
in 0.99f..21.01f -> "%.0f".format(it + 9) + "%"
else -> "%.0f".format(it * 5 - 75) + "%"
}
}
val minimapSlider = UncivSlider(
0f, 25f, 1f,
initial = if (settings.showMinimap) settings.minimapSize.toFloat() else 0f,
getTipText = getTipText
) {
val size = it.toInt()
if (size == 0) settings.showMinimap = false
else {
settings.showMinimap = true
settings.minimapSize = size
}
settings.save()
if (screen is WorldScreen)
screen.shouldUpdate = true
}
table.add(minimapSlider).minWidth(selectBoxMinWidth).pad(10f).row()
}
private fun addResolutionSelectBox(table: Table, settings: GameSettings, selectBoxMinWidth: Float, onResolutionChange: () -> Unit) {
table.add("Resolution".toLabel()).left().fillX()
val resolutionSelectBox = SelectBox<String>(table.skin)
resolutionSelectBox.items = resolutionArray
resolutionSelectBox.selected = settings.resolution
table.add(resolutionSelectBox).minWidth(selectBoxMinWidth).pad(10f).row()
resolutionSelectBox.onChange {
settings.resolution = resolutionSelectBox.selected
onResolutionChange()
}
}
private fun addTileSetSelectBox(table: Table, settings: GameSettings, selectBoxMinWidth: Float, onTilesetChange: () -> Unit) {
table.add("Tileset".toLabel()).left().fillX()
val tileSetSelectBox = SelectBox<String>(table.skin)
val tileSetArray = Array<String>()
val tileSets = ImageGetter.getAvailableTilesets()
for (tileset in tileSets) tileSetArray.add(tileset)
tileSetSelectBox.items = tileSetArray
tileSetSelectBox.selected = settings.tileSet
table.add(tileSetSelectBox).minWidth(selectBoxMinWidth).pad(10f).row()
tileSetSelectBox.onChange {
settings.tileSet = tileSetSelectBox.selected
// ImageGetter ruleset should be correct no matter what screen we're on
TileSetCache.assembleTileSetConfigs(ImageGetter.ruleset.mods)
onTilesetChange()
}
}

View File

@ -0,0 +1,36 @@
package com.unciv.ui.options
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.civilization.PlayerType
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.worldscreen.WorldScreen
fun gameplayTab(
optionsPopup: OptionsPopup
) = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(5f)
val settings = optionsPopup.settings
val screen = optionsPopup.screen
optionsPopup.addCheckbox(this, "Check for idle units", settings.checkForDueUnits, true) { settings.checkForDueUnits = it }
optionsPopup.addCheckbox(this, "Move units with a single tap", settings.singleTapMove) { settings.singleTapMove = it }
optionsPopup.addCheckbox(this, "Auto-assign city production", settings.autoAssignCityProduction, true) {
settings.autoAssignCityProduction = it
if (it && screen is WorldScreen &&
screen.viewingCiv.isCurrentPlayer() && screen.viewingCiv.playerType == PlayerType.Human
) {
screen.gameInfo.currentPlayerCiv.cities.forEach { city ->
city.cityConstructions.chooseNextConstruction()
}
}
}
optionsPopup.addCheckbox(this, "Auto-build roads", settings.autoBuildingRoads) { settings.autoBuildingRoads = it }
optionsPopup.addCheckbox(
this,
"Automated workers replace improvements",
settings.automatedWorkersReplaceImprovements
) { settings.automatedWorkersReplaceImprovements = it }
optionsPopup.addCheckbox(this, "Order trade offers by amount", settings.orderTradeOffersByAmount) { settings.orderTradeOffersByAmount = it }
}

View File

@ -0,0 +1,37 @@
package com.unciv.ui.options
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.utils.onClick
fun languageTab(
optionsPopup: OptionsPopup,
onLanguageSelected: () -> Unit
): Table = Table(BaseScreen.skin).apply {
val settings = optionsPopup.settings
val languageTables = this.addLanguageTables(optionsPopup.tabs.prefWidth * 0.9f - 10f)
var chosenLanguage = settings.language
fun selectLanguage() {
settings.language = chosenLanguage
settings.updateLocaleFromLanguage()
optionsPopup.screen.game.translations.tryReadTranslationForCurrentLanguage()
onLanguageSelected()
}
fun updateSelection() {
languageTables.forEach { it.update(chosenLanguage) }
if (chosenLanguage != settings.language)
selectLanguage()
}
updateSelection()
languageTables.forEach {
it.onClick {
chosenLanguage = it.language
updateSelection()
}
}
}

View File

@ -0,0 +1,248 @@
package com.unciv.ui.options
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.newgamescreen.TranslatedSelectBox
import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.utils.*
private const val MOD_CHECK_WITHOUT_BASE = "-none-"
class ModCheckTab(
val screen: BaseScreen
) : Table(), TabbedPager.IPageExtensions {
private val fixedContent = Table()
// marker for automatic first run on selecting the tab
private var modCheckFirstRun = true
private var modCheckBaseSelect: TranslatedSelectBox? = null
private val modCheckResultTable = Table()
init {
defaults().pad(10f).align(Align.top)
fixedContent.defaults().pad(10f).align(Align.top)
val reloadModsButton = "Reload mods".toTextButton().onClick(::runAction)
fixedContent.add(reloadModsButton).row()
val labeledBaseSelect = Table().apply {
add("Check extension mods based on:".toLabel()).padRight(10f)
val baseMods = listOf(MOD_CHECK_WITHOUT_BASE) + RulesetCache.getSortedBaseRulesets()
modCheckBaseSelect = TranslatedSelectBox(baseMods, MOD_CHECK_WITHOUT_BASE, BaseScreen.skin).apply {
selectedIndex = 0
onChange { runAction() }
}
add(modCheckBaseSelect)
}
fixedContent.add(labeledBaseSelect).row()
add(modCheckResultTable)
}
private fun runAction() {
if (modCheckFirstRun) runModChecker()
else runModChecker(modCheckBaseSelect!!.selected.value)
}
override fun getFixedContent() = fixedContent
override fun activated(index: Int, caption: String, pager: TabbedPager) {
runAction()
}
fun runModChecker(base: String = MOD_CHECK_WITHOUT_BASE) {
modCheckFirstRun = false
if (modCheckBaseSelect == null) return
modCheckResultTable.clear()
val rulesetErrors = RulesetCache.loadRulesets()
if (rulesetErrors.isNotEmpty()) {
val errorTable = Table().apply { defaults().pad(2f) }
for (rulesetError in rulesetErrors)
errorTable.add(rulesetError.toLabel()).width(stage.width / 2).row()
modCheckResultTable.add(errorTable)
}
modCheckResultTable.add("Checking mods for errors...".toLabel()).row()
modCheckBaseSelect!!.isDisabled = true
launchCrashHandling("ModChecker") {
for (mod in RulesetCache.values.sortedBy { it.name }) {
if (base != MOD_CHECK_WITHOUT_BASE && mod.modOptions.isBaseRuleset) continue
val modLinks =
if (base == MOD_CHECK_WITHOUT_BASE) mod.checkModLinks(forOptionsPopup = true)
else RulesetCache.checkCombinedModLinks(linkedSetOf(mod.name), base, forOptionsPopup = true)
modLinks.sortByDescending { it.errorSeverityToReport }
val noProblem = !modLinks.isNotOK()
if (modLinks.isNotEmpty()) modLinks += Ruleset.RulesetError("", Ruleset.RulesetErrorSeverity.OK)
if (noProblem) modLinks += Ruleset.RulesetError("No problems found.".tr(), Ruleset.RulesetErrorSeverity.OK)
postCrashHandlingRunnable {
// When the options popup is already closed before this postRunnable is run,
// Don't add the labels, as otherwise the game will crash
if (stage == null) return@postCrashHandlingRunnable
// Don't just render text, since that will make all the conditionals in the mod replacement messages move to the end, which makes it unreadable
// Don't use .toLabel() either, since that activates translations as well, which is what we're trying to avoid,
// Instead, some manual work needs to be put in.
val iconColor = modLinks.getFinalSeverity().color
val iconName = when (iconColor) {
Color.RED -> "OtherIcons/Stop"
Color.YELLOW -> "OtherIcons/ExclamationMark"
else -> "OtherIcons/Checkmark"
}
val icon = ImageGetter.getImage(iconName)
.apply { color = Color.BLACK }
.surroundWithCircle(30f, color = iconColor)
val expanderTab = ExpanderTab(mod.name, icon = icon, startsOutOpened = false) {
it.defaults().align(Align.left)
if (!noProblem && mod.folderLocation != null) {
val replaceableUniques = getDeprecatedReplaceableUniques(mod)
if (replaceableUniques.isNotEmpty())
it.add("Autoupdate mod uniques".toTextButton()
.onClick { autoUpdateUniques(screen, mod, replaceableUniques) }).pad(10f).row()
}
for (line in modLinks) {
val label = Label(line.text, BaseScreen.skin)
.apply { color = line.errorSeverityToReport.color }
label.wrap = true
it.add(label).width(stage.width / 2).row()
}
if (!noProblem)
it.add("Copy to clipboard".toTextButton().onClick {
Gdx.app.clipboard.contents = modLinks
.joinToString("\n") { line -> line.text }
}).row()
}
val loadingLabel = modCheckResultTable.children.last()
modCheckResultTable.removeActor(loadingLabel)
modCheckResultTable.add(expanderTab).row()
modCheckResultTable.add(loadingLabel).row()
}
}
// done with all mods!
postCrashHandlingRunnable {
modCheckResultTable.removeActor(modCheckResultTable.children.last())
modCheckBaseSelect!!.isDisabled = false
}
}
}
private fun getDeprecatedReplaceableUniques(mod: Ruleset): HashMap<String, String> {
val objectsToCheck = sequenceOf(
mod.beliefs,
mod.buildings,
mod.nations,
mod.policies,
mod.technologies,
mod.terrains,
mod.tileImprovements,
mod.unitPromotions,
mod.unitTypes,
mod.units,
)
val allDeprecatedUniques = HashSet<String>()
val deprecatedUniquesToReplacementText = HashMap<String, String>()
val deprecatedUniques = objectsToCheck
.flatMap { it.values }
.flatMap { it.uniqueObjects }
.filter { it.getDeprecationAnnotation() != null }
for (deprecatedUnique in deprecatedUniques) {
if (allDeprecatedUniques.contains(deprecatedUnique.text)) continue
allDeprecatedUniques.add(deprecatedUnique.text)
// note that this replacement does not contain conditionals attached to the original!
var uniqueReplacementText = deprecatedUnique.getReplacementText(mod)
while (Unique(uniqueReplacementText).getDeprecationAnnotation() != null)
uniqueReplacementText = Unique(uniqueReplacementText).getReplacementText(mod)
for (conditional in deprecatedUnique.conditionals)
uniqueReplacementText += " <${conditional.text}>"
val replacementUnique = Unique(uniqueReplacementText)
val modInvariantErrors = mod.checkUnique(
replacementUnique,
false,
"",
UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant,
deprecatedUnique.sourceObjectType!!
)
for (error in modInvariantErrors)
println(error.text + " - " + error.errorSeverityToReport)
if (modInvariantErrors.isNotEmpty()) continue // errors means no autoreplace
if (mod.modOptions.isBaseRuleset) {
val modSpecificErrors = mod.checkUnique(
replacementUnique,
false,
"",
UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant,
deprecatedUnique.sourceObjectType
)
for (error in modSpecificErrors)
println(error.text + " - " + error.errorSeverityToReport)
if (modSpecificErrors.isNotEmpty()) continue
}
deprecatedUniquesToReplacementText[deprecatedUnique.text] = uniqueReplacementText
println("Replace \"${deprecatedUnique.text}\" with \"$uniqueReplacementText\"")
}
return deprecatedUniquesToReplacementText
}
private fun autoUpdateUniques(screen: BaseScreen, mod: Ruleset, replaceableUniques: HashMap<String, String>) {
val filesToReplace = listOf(
"Beliefs.json",
"Buildings.json",
"Nations.json",
"Policies.json",
"Techs.json",
"Terrains.json",
"TileImprovements.json",
"UnitPromotions.json",
"UnitTypes.json",
"Units.json",
)
val jsonFolder = mod.folderLocation!!.child("jsons")
for (fileName in filesToReplace) {
val file = jsonFolder.child(fileName)
if (!file.exists() || file.isDirectory) continue
var newFileText = file.readString()
for ((original, replacement) in replaceableUniques) {
newFileText = newFileText.replace("\"$original\"", "\"$replacement\"")
}
file.writeString(newFileText, false)
}
val toastText = "Uniques updated!"
ToastPopup(toastText, screen).open(true)
runModChecker()
}
}

View File

@ -0,0 +1,152 @@
package com.unciv.ui.options
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.utils.Array
import com.unciv.Constants
import com.unciv.logic.multiplayer.storage.SimpleHttp
import com.unciv.models.metadata.GameSettings
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.popup.Popup
import com.unciv.ui.utils.*
fun multiplayerTab(
optionsPopup: OptionsPopup
): Table = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(5f)
val settings = optionsPopup.settings
// at the moment the notification service only exists on Android
if (Gdx.app.type == Application.ApplicationType.Android) {
optionsPopup.addCheckbox(
this, "Enable out-of-game turn notifications",
settings.multiplayerTurnCheckerEnabled
) {
settings.multiplayerTurnCheckerEnabled = it
settings.save()
}
if (settings.multiplayerTurnCheckerEnabled) {
addMultiplayerTurnCheckerDelayBox(this, settings)
optionsPopup.addCheckbox(
this, "Show persistent notification for turn notifier service",
settings.multiplayerTurnCheckerPersistentNotificationEnabled
)
{ settings.multiplayerTurnCheckerPersistentNotificationEnabled = it }
}
}
val connectionToServerButton = "Check connection to server".toTextButton()
val textToShowForMultiplayerAddress =
if (settings.multiplayerServer != Constants.dropboxMultiplayerServer) settings.multiplayerServer
else "https://..."
val multiplayerServerTextField = TextField(textToShowForMultiplayerAddress, BaseScreen.skin)
multiplayerServerTextField.setTextFieldFilter { _, c -> c !in " \r\n\t\\" }
multiplayerServerTextField.programmaticChangeEvents = true
val serverIpTable = Table()
serverIpTable.add("Server address".toLabel().onClick {
multiplayerServerTextField.text = Gdx.app.clipboard.contents
}).row()
multiplayerServerTextField.onChange {
connectionToServerButton.isEnabled = multiplayerServerTextField.text != Constants.dropboxMultiplayerServer
if (connectionToServerButton.isEnabled) {
fixTextFieldUrlOnType(multiplayerServerTextField)
// we can't trim on 'fixTextFieldUrlOnType' for reasons
settings.multiplayerServer = multiplayerServerTextField.text.trimEnd('/')
} else {
settings.multiplayerServer = multiplayerServerTextField.text
}
settings.save()
}
val screen = optionsPopup.screen
serverIpTable.add(multiplayerServerTextField).minWidth(screen.stage.width / 2).growX()
add(serverIpTable).fillX().row()
add("Reset to Dropbox".toTextButton().onClick {
multiplayerServerTextField.text = Constants.dropboxMultiplayerServer
}).row()
add(connectionToServerButton.onClick {
val popup = Popup(screen).apply {
addGoodSizedLabel("Awaiting response...").row()
}
popup.open(true)
successfullyConnectedToServer(settings) { success, _, _ ->
popup.addGoodSizedLabel(if (success) "Success!" else "Failed!").row()
popup.addCloseButton()
}
}).row()
}
private fun successfullyConnectedToServer(settings: GameSettings, action: (Boolean, String, Int?) -> Unit) {
launchCrashHandling("TestIsAlive") {
SimpleHttp.sendGetRequest("${settings.multiplayerServer}/isalive") {
success, result, code ->
postCrashHandlingRunnable {
action(success, result, code)
}
}
}
}
private fun fixTextFieldUrlOnType(TextField: TextField) {
var text: String = TextField.text
var cursor: Int = minOf(TextField.cursorPosition, text.length)
// if text is 'http:' or 'https:' auto append '//'
if (Regex("^https?:$").containsMatchIn(text)) {
TextField.appendText("//")
return
}
val textBeforeCursor: String = text.substring(0, cursor)
// replace multiple slash with a single one
val multipleSlashes = Regex("/{2,}")
text = multipleSlashes.replace(text, "/")
// calculate updated cursor
cursor = multipleSlashes.replace(textBeforeCursor, "/").length
// operations above makes 'https://' -> 'https:/'
// fix that if available and update cursor
val i: Int = text.indexOf(":/")
if (i > -1) {
text = text.replaceRange(i..i + 1, "://")
if (cursor > i + 1) ++cursor
}
// update TextField
if (text != TextField.text) {
TextField.text = text
TextField.cursorPosition = cursor
}
}
private fun addMultiplayerTurnCheckerDelayBox(table: Table, settings: GameSettings) {
table.add("Time between turn checks out-of-game (in minutes)".toLabel()).left().fillX()
val checkDelaySelectBox = SelectBox<Int>(table.skin)
val possibleDelaysArray = Array<Int>()
possibleDelaysArray.addAll(1, 2, 5, 15)
checkDelaySelectBox.items = possibleDelaysArray
checkDelaySelectBox.selected = settings.multiplayerTurnCheckerDelayInMinutes
table.add(checkDelaySelectBox).pad(10f).row()
checkDelaySelectBox.onChange {
settings.multiplayerTurnCheckerDelayInMinutes = checkDelaySelectBox.selected
settings.save()
}
}

View File

@ -0,0 +1,141 @@
package com.unciv.ui.options
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.MainMenuScreen
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.Popup
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.TabbedPager
import com.unciv.ui.utils.center
import com.unciv.ui.utils.toCheckBox
import com.unciv.ui.worldscreen.WorldScreen
/**
* The Options (Settings) Popup
* @param screen The caller - note if this is a [WorldScreen] or [MainMenuScreen] they will be rebuilt when major options change.
*/
//region Fields
class OptionsPopup(
screen: BaseScreen,
private val selectPage: Int = defaultPage,
private val onClose: () -> Unit = {}
) : Popup(screen) {
val settings = screen.game.settings
val tabs: TabbedPager
val selectBoxMinWidth: Float
//endregion
companion object {
const val defaultPage = 2 // Gameplay
}
init {
settings.addCompletedTutorialTask("Open the options table")
innerTable.pad(0f)
val tabMaxWidth: Float
val tabMinWidth: Float
val tabMaxHeight: Float
screen.run {
selectBoxMinWidth = if (stage.width < 600f) 200f else 240f
tabMaxWidth = if (isPortrait()) stage.width - 10f else 0.8f * stage.width
tabMinWidth = 0.6f * stage.width
tabMaxHeight = (if (isPortrait()) 0.7f else 0.8f) * stage.height
}
tabs = TabbedPager(
tabMinWidth, tabMaxWidth, 0f, tabMaxHeight,
headerFontSize = 21, backgroundColor = Color.CLEAR, keyPressDispatcher = this.keyPressDispatcher, capacity = 8
)
add(tabs).pad(0f).grow().row()
tabs.addPage(
"About",
aboutTab(screen),
ImageGetter.getExternalImage("Icon.png"), 24f
)
tabs.addPage(
"Display",
displayTab(this, ::reloadWorldAndOptions, ::reloadWorldAndOptions),
ImageGetter.getImage("UnitPromotionIcons/Scouting"), 24f
)
tabs.addPage(
"Gameplay",
gameplayTab(this),
ImageGetter.getImage("OtherIcons/Options"), 24f
)
tabs.addPage(
"Language",
languageTab(this, ::reloadWorldAndOptions),
ImageGetter.getImage("FlagIcons/${settings.language}"), 24f
)
tabs.addPage(
"Sound",
soundTab(this),
ImageGetter.getImage("OtherIcons/Speaker"), 24f
)
tabs.addPage(
"Multiplayer",
multiplayerTab(this),
ImageGetter.getImage("OtherIcons/Multiplayer"), 24f
)
tabs.addPage(
"Advanced",
advancedTab(this, ::reloadWorldAndOptions),
ImageGetter.getImage("OtherIcons/Settings"), 24f
)
if (RulesetCache.size > BaseRuleset.values().size) {
val content = ModCheckTab(screen)
tabs.addPage("Locate mod errors", content, ImageGetter.getImage("OtherIcons/Mods"), 24f)
}
if (Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT) && (Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT) || Gdx.input.isKeyPressed(Input.Keys.ALT_RIGHT))) {
tabs.addPage("Debug", debugTab(), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f, secret = true)
}
tabs.bindArrowKeys() // If we're sharing WorldScreen's dispatcher that's OK since it does revertToCheckPoint on update
addCloseButton {
screen.game.musicController.onChange(null)
screen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait)
onClose()
}.padBottom(10f)
pack() // Needed to show the background.
center(screen.stage)
}
override fun setVisible(visible: Boolean) {
super.setVisible(visible)
if (!visible) return
tabs.askForPassword(secretHashCode = 2747985)
if (tabs.activePage < 0) tabs.selectPage(selectPage)
}
/** Reload this Popup after major changes (resolution, tileset, language, font) */
private fun reloadWorldAndOptions() {
settings.save()
if (screen is WorldScreen) {
screen.game.worldScreen = WorldScreen(screen.gameInfo, screen.viewingCiv)
screen.game.setWorldScreen()
} else if (screen is MainMenuScreen) {
screen.game.setScreen(MainMenuScreen())
}
(screen.game.screen as BaseScreen).openOptionsPopup(tabs.activePage)
}
fun addCheckbox(table: Table, text: String, initialState: Boolean, updateWorld: Boolean = false, action: ((Boolean) -> Unit)) {
val checkbox = text.toCheckBox(initialState) {
action(it)
settings.save()
if (updateWorld && screen is WorldScreen)
screen.shouldUpdate = true
}
table.add(checkbox).colspan(2).left().row()
}
}

View File

@ -0,0 +1,150 @@
package com.unciv.ui.options
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.models.UncivSound
import com.unciv.models.metadata.GameSettings
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.utils.*
import kotlin.math.floor
fun soundTab(
optionsPopup: OptionsPopup
): Table = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(5f)
val settings = optionsPopup.settings
val screen = optionsPopup.screen
addSoundEffectsVolumeSlider(this, settings)
if (screen.game.musicController.isMusicAvailable()) {
addMusicVolumeSlider(this, settings, screen)
addMusicPauseSlider(this, settings, screen)
addMusicCurrentlyPlaying(this, screen)
}
if (!screen.game.musicController.isDefaultFileAvailable())
addDownloadMusic(this, optionsPopup)
}
private fun addDownloadMusic(table: Table, optionsPopup: OptionsPopup) {
val downloadMusicButton = "Download music".toTextButton()
table.add(downloadMusicButton).colspan(2).row()
val errorTable = Table()
table.add(errorTable).colspan(2).row()
downloadMusicButton.onClick {
downloadMusicButton.disable()
errorTable.clear()
errorTable.add("Downloading...".toLabel())
// So the whole game doesn't get stuck while downloading the file
launchCrashHandling("MusicDownload") {
try {
val screen = optionsPopup.screen
screen.game.musicController.downloadDefaultFile()
postCrashHandlingRunnable {
optionsPopup.tabs.replacePage("Sound", soundTab(optionsPopup))
screen.game.musicController.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
}
} catch (ex: Exception) {
postCrashHandlingRunnable {
errorTable.clear()
errorTable.add("Could not download music!".toLabel(Color.RED))
}
}
}
}
}
private fun addSoundEffectsVolumeSlider(table: Table, settings: GameSettings) {
table.add("Sound effects volume".tr()).left().fillX()
val soundEffectsVolumeSlider = UncivSlider(
0f, 1.0f, 0.05f,
initial = settings.soundEffectsVolume,
getTipText = UncivSlider::formatPercent
) {
settings.soundEffectsVolume = it
settings.save()
}
table.add(soundEffectsVolumeSlider).pad(5f).row()
}
private fun addMusicVolumeSlider(table: Table, settings: GameSettings, screen: BaseScreen) {
table.add("Music volume".tr()).left().fillX()
val musicVolumeSlider = UncivSlider(
0f, 1.0f, 0.05f,
initial = settings.musicVolume,
sound = UncivSound.Silent,
getTipText = UncivSlider::formatPercent
) {
settings.musicVolume = it
settings.save()
val music = screen.game.musicController
music.setVolume(it)
if (!music.isPlaying())
music.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
}
table.add(musicVolumeSlider).pad(5f).row()
}
private fun addMusicPauseSlider(table: Table, settings: GameSettings, screen: BaseScreen) {
val music = screen.game.musicController
// map to/from 0-1-2..10-12-14..30-35-40..60-75-90-105-120
fun posToLength(pos: Float): Float = when (pos) {
in 0f..10f -> pos
in 11f..20f -> pos * 2f - 10f
in 21f..26f -> pos * 5f - 70f
else -> pos * 15f - 330f
}
fun lengthToPos(length: Float): Float = floor(
when (length) {
in 0f..10f -> length
in 11f..30f -> (length + 10f) / 2f
in 31f..60f -> (length + 10f) / 5f
else -> (length + 330f) / 15f
}
)
val getTipText: (Float) -> String = {
"%.0f".format(posToLength(it))
}
table.add("Pause between tracks".tr()).left().fillX()
val pauseLengthSlider = UncivSlider(
0f, 30f, 1f,
initial = lengthToPos(music.silenceLength),
sound = UncivSound.Silent,
getTipText = getTipText
) {
music.silenceLength = posToLength(it)
settings.pauseBetweenTracks = music.silenceLength.toInt()
}
table.add(pauseLengthSlider).pad(5f).row()
}
private fun addMusicCurrentlyPlaying(table: Table, screen: BaseScreen) {
val label = WrappableLabel("", table.width - 10f, Color(-0x2f5001), 16)
label.wrap = true
table.add(label).padTop(20f).colspan(2).fillX().row()
screen.game.musicController.onChange {
postCrashHandlingRunnable {
label.setText("Currently playing: [$it]".tr())
}
}
label.onClick(UncivSound.Silent) {
screen.game.musicController.chooseTrack(flags = MusicTrackChooserFlags.none)
}
}

View File

@ -16,7 +16,7 @@ import com.unciv.models.Tutorial
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.hasOpenPopups
import com.unciv.ui.tutorials.TutorialController
import com.unciv.ui.worldscreen.mainmenu.OptionsPopup
import com.unciv.ui.options.OptionsPopup
abstract class BaseScreen : Screen {

File diff suppressed because it is too large Load Diff