mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-20 17:32:57 +07:00
Moddable UI skins (#7804)
* Added SkinStrings and SkinConfig TODO SkinCache * Deprecated all old ui background getters * Added SkinCache for SkinConfigs to take effect * Modable or moddable? idk ¯\_(ツ)_/¯ * Added separate alpha to config
This commit is contained in:
parent
a75a157dff
commit
d05b3d376b
@ -16,6 +16,7 @@ import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.multiplayer.OnlineMultiplayer
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.skins.SkinCache
|
||||
import com.unciv.models.tilesets.TileSetCache
|
||||
import com.unciv.models.translations.Translations
|
||||
import com.unciv.ui.LanguagePickerScreen
|
||||
@ -131,8 +132,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
settings.tileSet = Constants.defaultTileset
|
||||
}
|
||||
|
||||
BaseScreen.setSkin() // needs to come AFTER the Texture reset, since the buttons depend on it
|
||||
|
||||
Gdx.graphics.isContinuousRendering = settings.continuousRendering
|
||||
|
||||
Concurrency.run("LoadJSON") {
|
||||
@ -140,6 +139,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
translations.tryReadTranslationForCurrentLanguage()
|
||||
translations.loadPercentageCompleteOfLanguages()
|
||||
TileSetCache.loadTileSetConfigs()
|
||||
SkinCache.loadSkinConfigs()
|
||||
|
||||
if (settings.multiplayer.userId.isEmpty()) { // assign permanent user id
|
||||
settings.multiplayer.userId = UUID.randomUUID().toString()
|
||||
@ -152,6 +152,8 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
|
||||
// This stuff needs to run on the main thread because it needs the GL context
|
||||
launchOnGLThread {
|
||||
BaseScreen.setSkin() // needs to come AFTER the Texture reset, since the buttons depend on it and after loadSkinConfigs to be able to use the SkinConfig
|
||||
|
||||
musicController.chooseTrack(suffixes = listOf(MusicMood.Menu, MusicMood.Ambient),
|
||||
flags = EnumSet.of(MusicTrackChooserFlags.SuffixMustMatch))
|
||||
|
||||
|
90
core/src/com/unciv/models/skins/SkinCache.kt
Normal file
90
core/src/com/unciv/models/skins/SkinCache.kt
Normal file
@ -0,0 +1,90 @@
|
||||
package com.unciv.models.skins
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.json.fromJsonFile
|
||||
import com.unciv.json.json
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.utils.debug
|
||||
|
||||
object SkinCache : HashMap<String, SkinConfig>() {
|
||||
private data class SkinAndMod(val skin: String, val mod: String)
|
||||
private val allConfigs = HashMap<SkinAndMod, SkinConfig>()
|
||||
|
||||
/** Combine [SkinConfig]s for chosen mods.
|
||||
* Vanilla always active, even with a base ruleset mod active.
|
||||
* Permanent visual mods always included as long as UncivGame.Current is initialized.
|
||||
* Other active mods can be passed in parameter [ruleSetMods], if that is `null` and a game is in
|
||||
* progress, that game's mods are used instead.
|
||||
*/
|
||||
fun assembleSkinConfigs(ruleSetMods: Set<String>) {
|
||||
// Needs to be a list and not a set, so subsequent mods override the previous ones
|
||||
// Otherwise you rely on hash randomness to determine override order... not good
|
||||
val mods = mutableListOf("")
|
||||
if (UncivGame.isCurrentInitialized()) {
|
||||
mods.addAll(UncivGame.Current.settings.visualMods)
|
||||
}
|
||||
mods.addAll(ruleSetMods)
|
||||
clear()
|
||||
for (mod in mods.distinct()) {
|
||||
for (entry in allConfigs.entries.filter { it.key.mod == mod } ) { // Built-in skins all have empty strings as their `.mod`, so loop through all of them.
|
||||
val skin = entry.key.skin
|
||||
if (skin in this) this[skin]!!.updateConfig(entry.value)
|
||||
else this[skin] = entry.value.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSkinConfigs(consoleMode: Boolean = false){
|
||||
allConfigs.clear()
|
||||
var skinName = ""
|
||||
|
||||
//load internal Skins
|
||||
val fileHandles: Sequence<FileHandle> =
|
||||
if (consoleMode) FileHandle("jsons/Skins").list().asSequence()
|
||||
else ImageGetter.getAvailableSkins().map { Gdx.files.internal("jsons/Skins/$it.json")}.filter { it.exists() }
|
||||
|
||||
for (configFile in fileHandles){
|
||||
skinName = configFile.nameWithoutExtension().removeSuffix("Config")
|
||||
try {
|
||||
val key = SkinAndMod(skinName, "")
|
||||
assert(key !in allConfigs)
|
||||
allConfigs[key] = json().fromJsonFile(SkinConfig::class.java, configFile)
|
||||
debug("SkinConfig loaded successfully: %s", configFile.name())
|
||||
} catch (ex: Exception){
|
||||
debug("Exception loading SkinConfig '%s':", configFile.path())
|
||||
debug(" %s", ex.localizedMessage)
|
||||
debug(" (Source file %s line %s)", ex.stackTrace[0].fileName, ex.stackTrace[0].lineNumber)
|
||||
}
|
||||
}
|
||||
|
||||
//load mod Skins
|
||||
val modsHandles =
|
||||
if (consoleMode) FileHandle("mods").list().toList()
|
||||
else RulesetCache.values.mapNotNull { it.folderLocation }
|
||||
|
||||
for (modFolder in modsHandles) {
|
||||
val modName = modFolder.name()
|
||||
if (modName.startsWith('.')) continue
|
||||
if (!modFolder.isDirectory) continue
|
||||
|
||||
try {
|
||||
for (configFile in modFolder.child("jsons/Skins").list()){
|
||||
skinName = configFile.nameWithoutExtension().removeSuffix("Config")
|
||||
val key = SkinAndMod(skinName, modName)
|
||||
assert(key !in allConfigs)
|
||||
allConfigs[key] = json().fromJsonFile(SkinConfig::class.java, configFile)
|
||||
debug("Skin loaded successfully: %s", configFile.path())
|
||||
}
|
||||
} catch (ex: Exception){
|
||||
debug("Exception loading Skin '%s/jsons/Skins/%s':", modFolder.name(), skinName)
|
||||
debug(" %s", ex.localizedMessage)
|
||||
debug(" (Source file %s line %s)", ex.stackTrace[0].fileName, ex.stackTrace[0].lineNumber)
|
||||
}
|
||||
}
|
||||
|
||||
assembleSkinConfigs(hashSetOf()) // no game is loaded, this is just the initial game setup
|
||||
}
|
||||
}
|
36
core/src/com/unciv/models/skins/SkinConfig.kt
Normal file
36
core/src/com/unciv/models/skins/SkinConfig.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package com.unciv.models.skins
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
|
||||
class SkinElement {
|
||||
var image: String? = null
|
||||
var tint: Color? = null
|
||||
var alpha: Float? = null
|
||||
|
||||
fun clone(): SkinElement {
|
||||
val toReturn = SkinElement()
|
||||
toReturn.image = image
|
||||
toReturn.tint = tint?.cpy()
|
||||
toReturn.alpha = alpha
|
||||
return toReturn
|
||||
}
|
||||
}
|
||||
|
||||
class SkinConfig {
|
||||
var baseColor: Color = Color(0x004085bf)
|
||||
var skinVariants: HashMap<String, SkinElement> = HashMap()
|
||||
|
||||
fun clone(): SkinConfig {
|
||||
val toReturn = SkinConfig()
|
||||
toReturn.baseColor = baseColor.cpy()
|
||||
toReturn.skinVariants.putAll(skinVariants.map { Pair(it.key, it.value.clone()) })
|
||||
return toReturn
|
||||
}
|
||||
|
||||
fun updateConfig(other: SkinConfig) {
|
||||
baseColor = other.baseColor.cpy()
|
||||
for ((variantName, element) in other.skinVariants){
|
||||
skinVariants[variantName] = element.clone()
|
||||
}
|
||||
}
|
||||
}
|
47
core/src/com/unciv/models/skins/SkinStrings.kt
Normal file
47
core/src/com/unciv/models/skins/SkinStrings.kt
Normal file
@ -0,0 +1,47 @@
|
||||
package com.unciv.models.skins
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
|
||||
class SkinStrings(skin: String = UncivGame.Current.settings.skin) {
|
||||
private val skinLocation = "Skins/$skin/"
|
||||
val skinConfig = SkinCache[skin] ?: SkinConfig()
|
||||
|
||||
val roundedEdgeRectangle = skinLocation + "roundedEdgeRectangle"
|
||||
val rectangleWithOutline = skinLocation + "rectangleWithOutline"
|
||||
val selectBox = skinLocation + "select-box"
|
||||
val selectBoxPressed = skinLocation + "select-box-pressed"
|
||||
val checkbox = skinLocation + "checkbox"
|
||||
val checkboxPressed = skinLocation + "checkbox-pressed"
|
||||
|
||||
/**
|
||||
* Gets either a drawable which was defined inside skinConfig for the given path or the drawable
|
||||
* found at path itself or the default drawable to be applied as the background for an UI element.
|
||||
*
|
||||
* @param path The path of the UI background in UpperCamelCase. Should be the location of the
|
||||
* UI element inside the UI tree e.g. WorldScreen/TopBar/StatsTable.
|
||||
*
|
||||
* If the UI element is used in multiple Screens start the path with General
|
||||
* e.g. General/Tooltip
|
||||
*
|
||||
*
|
||||
* @param default The path to the background which should be used if path is not available.
|
||||
* Should be one of the predefined ones inside SkinStrings or null to get a
|
||||
* solid background.
|
||||
*/
|
||||
fun getUiBackground(path: String, default: String? = null, tintColor: Color? = null): NinePatchDrawable {
|
||||
val locationByName = skinLocation + path
|
||||
val locationByConfigVariant = skinLocation + skinConfig.skinVariants[path]?.image
|
||||
val tint = (skinConfig.skinVariants[path]?.tint ?: tintColor)?.apply {
|
||||
a = skinConfig.skinVariants[path]?.alpha ?: a
|
||||
}
|
||||
|
||||
return when {
|
||||
ImageGetter.ninePatchImageExists(locationByConfigVariant) -> ImageGetter.getNinePatch(locationByConfigVariant, tint)
|
||||
ImageGetter.ninePatchImageExists(locationByName) -> ImageGetter.getNinePatch(locationByName, tint)
|
||||
else -> ImageGetter.getNinePatch(default, tint)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import com.unciv.json.json
|
||||
import com.unciv.models.ruleset.Nation
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.models.skins.SkinCache
|
||||
import com.unciv.models.stats.Stats
|
||||
import com.unciv.models.tilesets.TileSetCache
|
||||
import com.unciv.ui.utils.*
|
||||
@ -76,6 +77,7 @@ object ImageGetter {
|
||||
}
|
||||
|
||||
TileSetCache.assembleTileSetConfigs(ruleset.mods)
|
||||
SkinCache.assembleSkinConfigs(ruleset.mods)
|
||||
}
|
||||
|
||||
/** Loads all atlas/texture files from a folder, as controlled by an Atlases.json */
|
||||
@ -181,10 +183,19 @@ object ImageGetter {
|
||||
return textureRegionDrawables[fileName] ?: textureRegionDrawables[whiteDotLocation]!!
|
||||
}
|
||||
|
||||
fun getNinePatch(fileName: String?): NinePatchDrawable {
|
||||
return ninePatchDrawables[fileName] ?: NinePatchDrawable(NinePatch(textureRegionDrawables[whiteDotLocation]!!.region))
|
||||
fun getNinePatch(fileName: String?, tintColor: Color? = null): NinePatchDrawable {
|
||||
val drawable = ninePatchDrawables[fileName] ?: NinePatchDrawable(NinePatch(textureRegionDrawables[whiteDotLocation]!!.region))
|
||||
|
||||
if (fileName == null || ninePatchDrawables[fileName] == null) {
|
||||
drawable.minHeight = 0f
|
||||
drawable.minWidth = 0f
|
||||
}
|
||||
if (tintColor == null)
|
||||
return drawable
|
||||
return drawable.tint(tintColor)
|
||||
}
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, BaseScreen.skinStrings.roundedEdgeRectangle, tintColor)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getRoundedEdgeRectangle(tintColor: Color? = null): NinePatchDrawable {
|
||||
val drawable = getNinePatch("Skins/${UncivGame.Current.settings.skin}/roundedEdgeRectangle")
|
||||
|
||||
@ -192,22 +203,27 @@ object ImageGetter {
|
||||
return drawable.tint(tintColor)
|
||||
}
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, BaseScreen.skinStrings.rectangleWithOutline)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getRectangleWithOutline(): NinePatchDrawable {
|
||||
return getNinePatch("Skins/${UncivGame.Current.settings.skin}/rectangleWithOutline")
|
||||
}
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, BaseScreen.skinStrings.selectBox)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getSelectBox(): NinePatchDrawable {
|
||||
return getNinePatch("Skins/${UncivGame.Current.settings.skin}/select-box")
|
||||
}
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, BaseScreen.skinStrings.selectBoxPressed)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getSelectBoxPressed(): NinePatchDrawable {
|
||||
return getNinePatch("Skins/${UncivGame.Current.settings.skin}/select-box-pressed")
|
||||
}
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, BaseScreen.skinStrings.checkbox)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getCheckBox(): NinePatchDrawable {
|
||||
return getNinePatch("Skins/${UncivGame.Current.settings.skin}/checkbox")
|
||||
}
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, BaseScreen.skinStrings.checkboxPressed)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getCheckBoxPressed(): NinePatchDrawable {
|
||||
return getNinePatch("Skins/${UncivGame.Current.settings.skin}/checkbox-pressed")
|
||||
}
|
||||
@ -215,6 +231,7 @@ object ImageGetter {
|
||||
fun imageExists(fileName: String) = textureRegionDrawables.containsKey(fileName)
|
||||
fun techIconExists(techName: String) = imageExists("TechIcons/$techName")
|
||||
fun unitIconExists(unitName: String) = imageExists("UnitIcons/$unitName")
|
||||
fun ninePatchImageExists(fileName: String) = ninePatchDrawables.containsKey(fileName)
|
||||
|
||||
fun getStatIcon(statName: String): Image {
|
||||
return getImage("StatIcons/$statName")
|
||||
@ -323,11 +340,13 @@ object ImageGetter {
|
||||
return getReligionImage(iconName).surroundWithCircle(size, color = Color.BLACK )
|
||||
}
|
||||
|
||||
@Deprecated("Use skin defined base color instead", ReplaceWith("BaseScreen.skinStrings.skinConfig.baseColor", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getBlue() = Color(0x004085bf)
|
||||
|
||||
fun getCircle() = getImage("OtherIcons/Circle")
|
||||
fun getTriangle() = getImage("OtherIcons/Triangle")
|
||||
|
||||
@Deprecated("Use SkinStrings.getUiBackground instead to make UI element moddable", ReplaceWith("BaseScreen.skinStrings.getUiBackground(path, tintColor=color)", "com.unciv.ui.utils.BaseScreen"))
|
||||
fun getBackground(color: Color): Drawable {
|
||||
val drawable = getDrawable("")
|
||||
drawable.minHeight = 0f
|
||||
|
@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Array
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.skins.SkinCache
|
||||
import com.unciv.models.tilesets.TileSetCache
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
@ -162,6 +163,7 @@ private fun addSkinSelectBox(table: Table, settings: GameSettings, selectBoxMinW
|
||||
skinSelectBox.onChange {
|
||||
settings.skin = skinSelectBox.selected
|
||||
// ImageGetter ruleset should be correct no matter what screen we're on
|
||||
SkinCache.assembleSkinConfigs(ImageGetter.ruleset.mods)
|
||||
onSkinChange()
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +138,11 @@ class OptionsPopup(
|
||||
private fun reloadWorldAndOptions() {
|
||||
Concurrency.run("Reload from options") {
|
||||
settings.save()
|
||||
withGLContext {
|
||||
// We have to run setSkin before the screen is rebuild else changing skins
|
||||
// would only load the new SkinConfig after the next rebuild
|
||||
BaseScreen.setSkin()
|
||||
}
|
||||
val screen = UncivGame.Current.screen
|
||||
if (screen is WorldScreen) {
|
||||
UncivGame.Current.reloadWorldscreen()
|
||||
@ -147,7 +152,6 @@ class OptionsPopup(
|
||||
}
|
||||
}
|
||||
withGLContext {
|
||||
BaseScreen.setSkin()
|
||||
UncivGame.Current.screen?.openOptionsPopup(tabs.activePage)
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.Drawable
|
||||
import com.badlogic.gdx.utils.viewport.ExtendViewport
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.models.skins.SkinStrings
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.activePopup
|
||||
@ -110,8 +111,10 @@ abstract class BaseScreen : Screen {
|
||||
var enableSceneDebug = false
|
||||
|
||||
lateinit var skin: Skin
|
||||
lateinit var skinStrings: SkinStrings
|
||||
fun setSkin() {
|
||||
Fonts.resetFont()
|
||||
skinStrings = SkinStrings()
|
||||
skin = Skin().apply {
|
||||
add("Nativefont", Fonts.font, BitmapFont::class.java)
|
||||
add("RoundedEdgeRectangle", ImageGetter.getRoundedEdgeRectangle(), Drawable::class.java)
|
||||
|
@ -17,6 +17,7 @@ import com.unciv.models.simulation.Simulation
|
||||
import com.unciv.models.tilesets.TileSetCache
|
||||
import com.unciv.models.metadata.GameSetupInfo
|
||||
import com.unciv.models.ruleset.Speed
|
||||
import com.unciv.models.skins.SkinCache
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
internal object ConsoleLauncher {
|
||||
@ -36,6 +37,7 @@ internal object ConsoleLauncher {
|
||||
|
||||
RulesetCache.loadRulesets(true)
|
||||
TileSetCache.loadTileSetConfigs(true)
|
||||
SkinCache.loadSkinConfigs(true)
|
||||
|
||||
val gameParameters = getGameParameters("China", "Greece")
|
||||
val mapParameters = getMapParameters()
|
||||
|
Loading…
Reference in New Issue
Block a user