diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index 99ade90bc8..3f070ce02a 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -11,6 +11,7 @@ import com.unciv.UncivGame import com.unciv.UncivGameParameters import com.unciv.logic.GameSaver import com.unciv.ui.utils.Fonts +import com.unciv.utils.Log import java.io.File open class AndroidLauncher : AndroidApplication() { @@ -19,6 +20,7 @@ open class AndroidLauncher : AndroidApplication() { private var deepLinkedMultiplayerGame: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Log.backend = AndroidLogBackend() customSaveLocationHelper = CustomSaveLocationHelperAndroid(this) MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext) diff --git a/android/src/com/unciv/app/AndroidLogBackend.kt b/android/src/com/unciv/app/AndroidLogBackend.kt new file mode 100644 index 0000000000..5ff48517cf --- /dev/null +++ b/android/src/com/unciv/app/AndroidLogBackend.kt @@ -0,0 +1,35 @@ +package com.unciv.app + +import android.os.Build +import android.util.Log +import com.unciv.utils.LogBackend +import com.unciv.utils.Tag + +private const val TAG_MAX_LENGTH = 23 + +class AndroidLogBackend : LogBackend { + + override fun debug(tag: Tag, curThreadName: String, msg: String) { + Log.d(toAndroidTag(tag), "[$curThreadName] $msg") + } + + override fun error(tag: Tag, curThreadName: String, msg: String) { + Log.e(toAndroidTag(tag), "[$curThreadName] $msg") + } + + override fun isRelease(): Boolean { + return !BuildConfig.DEBUG + } +} + +private fun toAndroidTag(tag: Tag): String { + // This allows easy filtering of logcat by tag "Unciv" + val withUncivPrefix = if (tag.name.contains("unciv", true)) tag.name else "Unciv ${tag.name}" + + // Limit was removed in Nougat + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || tag.name.length <= TAG_MAX_LENGTH) { + withUncivPrefix + } else { + withUncivPrefix.substring(0, TAG_MAX_LENGTH) + } +} diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 4147cdb446..e210339f48 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -28,6 +28,7 @@ import com.unciv.ui.images.ImageGetter import com.unciv.ui.multiplayer.LoadDeepLinkScreen import com.unciv.ui.multiplayer.MultiplayerHelpers import com.unciv.ui.popup.Popup +import com.unciv.utils.debug import kotlinx.coroutines.runBlocking import java.util.* @@ -66,10 +67,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() { */ var simulateUntilTurnForDebug: Int = 0 - /** Console log battles - */ - val alertBattle = false - lateinit var worldScreen: WorldScreen private set fun getWorldScreenOrNull() = if (this::worldScreen.isInitialized) worldScreen else null @@ -124,10 +121,10 @@ class UncivGame(parameters: UncivGameParameters) : Game() { Gdx.graphics.isContinuousRendering = settings.continuousRendering launchCrashHandling("LoadJSON") { - RulesetCache.loadRulesets(printOutput = true) + RulesetCache.loadRulesets() translations.tryReadTranslationForCurrentLanguage() translations.loadPercentageCompleteOfLanguages() - TileSetCache.loadTileSetConfigs(printOutput = true) + TileSetCache.loadTileSetConfigs() if (settings.multiplayer.userId.isEmpty()) { // assign permanent user id settings.multiplayer.userId = UUID.randomUUID().toString() @@ -178,12 +175,18 @@ class UncivGame(parameters: UncivGameParameters) : Game() { */ fun resetToWorldScreen(newWorldScreen: WorldScreen? = null) { if (newWorldScreen != null) { + debug("Reset to new WorldScreen, gameId: %s, turn: %s, curCiv: %s", + newWorldScreen.gameInfo.gameId, newWorldScreen.gameInfo.turns, newWorldScreen.gameInfo.currentPlayer) val oldWorldScreen = getWorldScreenOrNull() worldScreen = newWorldScreen // setScreen disposes the current screen, but the old world screen is not the current screen, so need to dispose here if (screen != oldWorldScreen) { oldWorldScreen?.dispose() } + } else { + val oldWorldScreen = getWorldScreenOrNull()!! + debug("Reset to old WorldScreen, gameId: %s, turn: %s, curCiv: %s", + oldWorldScreen.gameInfo.gameId, oldWorldScreen.gameInfo.turns, oldWorldScreen.gameInfo.currentPlayer) } setScreen(worldScreen) worldScreen.shouldUpdate = true // This can set the screen to the policy picker or tech picker screen, so the input processor must come before @@ -264,7 +267,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { val threadList = Array(numThreads) { _ -> Thread() } Thread.enumerate(threadList) threadList.filter { it !== Thread.currentThread() && it.name != "DestroyJavaVM" }.forEach { - println(" Thread ${it.name} still running in UncivGame.dispose().") + debug("Thread %s still running in UncivGame.dispose().", it.name) } } diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 5b9c52551b..46ce47c0a7 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -2,6 +2,7 @@ package com.unciv.logic import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.utils.debug import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps import com.unciv.logic.BackwardCompatibility.migrateSeenImprovements @@ -209,7 +210,7 @@ class GameInfo { if (currentPlayerIndex == 0) { turns++ if (UncivGame.Current.simulateUntilTurnForDebug != 0) - println("Starting simulation of turn $turns") + debug("Starting simulation of turn %s", turns) } thisPlayer = civilizations[currentPlayerIndex] thisPlayer.startTurn() diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index e0abf4e104..2f75270a8f 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -13,6 +13,7 @@ import com.unciv.models.metadata.isMigrationNecessary import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.saves.Gzip +import com.unciv.utils.Log import kotlinx.coroutines.Job import java.io.File @@ -178,8 +179,7 @@ class GameSaver( // I'm not sure of the circumstances, // but some people were getting null settings, even though the file existed??? Very odd. // ...Json broken or otherwise unreadable is the only possible reason. - println("Error reading settings file: ${ex.localizedMessage}") - println(" cause: ${ex.cause}") + Log.error("Error reading settings file", ex) } } diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 9e442c40b6..917e1e2efa 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -2,6 +2,7 @@ package com.unciv.logic import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.utils.debug import com.unciv.logic.civilization.* import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap @@ -20,12 +21,11 @@ import kotlin.collections.HashSet object GameStarter { // temporary instrumentation while tuning/debugging - private const val consoleOutput = false private const val consoleTimings = false fun startNewGame(gameSetupInfo: GameSetupInfo): GameInfo { - if (consoleOutput || consoleTimings) - println("\nGameStarter run with parameters ${gameSetupInfo.gameParameters}, map ${gameSetupInfo.mapParameters}") + if (consoleTimings) + debug("\nGameStarter run with parameters %s, map %s", gameSetupInfo.gameParameters, gameSetupInfo.mapParameters) val gameInfo = GameInfo() lateinit var tileMap: TileMap @@ -133,7 +133,7 @@ object GameStarter { val startNanos = System.nanoTime() action() val delta = System.nanoTime() - startNanos - println("GameStarter.$text took ${delta/1000000L}.${(delta/10000L).rem(100)}ms") + debug("GameStarter.%s took %s.%sms", text, delta/1000000L, (delta/10000L).rem(100)) } private fun addPlayerIntros(gameInfo: GameInfo) { @@ -333,7 +333,7 @@ object GameStarter { var unit = unitParam // We want to change it and this is the easiest way to do so if (unit == Constants.eraSpecificUnit) unit = eraUnitReplacement if (unit == Constants.settler && Constants.settler !in ruleSet.units) { - val buildableSettlerLikeUnits = + val buildableSettlerLikeUnits = settlerLikeUnits.filter { it.value.isBuildable(civ) && it.value.isCivilian() @@ -352,7 +352,7 @@ object GameStarter { return civ.getEquivalentUnit(unit).name } - // City states should only spawn with one settler regardless of difficulty, but this may be disabled in mods + // City states should only spawn with one settler regardless of difficulty, but this may be disabled in mods if (civ.isCityState() && !ruleSet.modOptions.uniques.contains(ModOptionsConstants.allowCityStatesSpawnUnits)) { val startingSettlers = startingUnits.filter { settlerLikeUnits.contains(it) } diff --git a/core/src/com/unciv/logic/automation/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/WorkerAutomation.kt index 1133847d21..15a4157acb 100644 --- a/core/src/com/unciv/logic/automation/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/WorkerAutomation.kt @@ -14,11 +14,10 @@ import com.unciv.logic.map.TileInfo import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.utils.Log +import com.unciv.utils.debug private object WorkerAutomationConst { - /** Controls detailed logging of decisions to the console -Turn off for release builds! */ - const val consoleOutput = false - /** BFS max size is determined by the aerial distance of two cities to connect, padded with this */ // two tiles longer than the distance to the nearest connected city should be enough as the 'reach' of a BFS is increased by blocked tiles const val maxBfsReachPadding = 2 @@ -59,12 +58,12 @@ class WorkerAutomation( }.sortedBy { it.getCenterTile().aerialDistanceTo(civInfo.getCapital()!!.getCenterTile()) }.toList() - if (WorkerAutomationConst.consoleOutput) { - println("WorkerAutomation citiesThatNeedConnecting for ${civInfo.civName} turn $cachedForTurn:") + if (Log.shouldLog()) { + debug("WorkerAutomation citiesThatNeedConnecting for ${civInfo.civName} turn $cachedForTurn:") if (result.isEmpty()) - println("\tempty") + debug("\tempty") else result.forEach { - println("\t${it.name}") + debug("\t${it.name}") } } result @@ -76,12 +75,12 @@ class WorkerAutomation( .filter { it.isCapital() || it.cityStats.isConnectedToCapital(bestRoadAvailable) } .map { it.getCenterTile() } .toList() - if (WorkerAutomationConst.consoleOutput) { - println("WorkerAutomation tilesOfConnectedCities for ${civInfo.civName} turn $cachedForTurn:") + if (Log.shouldLog()) { + debug("WorkerAutomation tilesOfConnectedCities for ${civInfo.civName} turn $cachedForTurn:") if (result.isEmpty()) - println("\tempty") + debug("\tempty") else result.forEach { - println("\t$it") // ${it.getCity()?.name} included in TileInfo toString() + debug("\t$it") // ${it.getCity()?.name} included in TileInfo toString() } } result @@ -135,8 +134,7 @@ class WorkerAutomation( } if (tileToWork != currentTile) { - if (WorkerAutomationConst.consoleOutput) - println("WorkerAutomation: ${unit.label()} -> head towards $tileToWork") + debug("WorkerAutomation: %s -> head towards %s", unit.label(), tileToWork) val reachedTile = unit.movement.headTowards(tileToWork) if (reachedTile != currentTile) unit.doAction() // otherwise, we get a situation where the worker is automated, so it tries to move but doesn't, then tries to automate, then move, etc, forever. Stack overflow exception! return @@ -144,8 +142,7 @@ class WorkerAutomation( if (currentTile.improvementInProgress == null && currentTile.isLand && tileCanBeImproved(unit, currentTile)) { - if (WorkerAutomationConst.consoleOutput) - println("WorkerAutomation: ${unit.label()} -> start improving $currentTile") + debug("WorkerAutomation: ${unit.label()} -> start improving $currentTile") return currentTile.startWorkingOnImprovement(chooseImprovement(unit, currentTile)!!, civInfo, unit) } @@ -164,15 +161,13 @@ class WorkerAutomation( .firstOrNull { unit.movement.canReach(it.getCenterTile()) } //goto most undeveloped city if (mostUndevelopedCity != null && mostUndevelopedCity != currentTile.owningCity) { - if (WorkerAutomationConst.consoleOutput) - println("WorkerAutomation: ${unit.label()} -> head towards undeveloped city ${mostUndevelopedCity.name}") + debug("WorkerAutomation: %s -> head towards undeveloped city %s", unit.label(), mostUndevelopedCity.name) val reachedTile = unit.movement.headTowards(mostUndevelopedCity.getCenterTile()) if (reachedTile != currentTile) unit.doAction() // since we've moved, maybe we can do something here - automate return } - if (WorkerAutomationConst.consoleOutput) - println("WorkerAutomation: ${unit.label()} -> nothing to do") + debug("WorkerAutomation: %s -> nothing to do", unit.label()) unit.civInfo.addNotification("${unit.shortDisplayName()} has no work to do.", currentTile.position, unit.name, "OtherIcons/Sleep") // Idle CS units should wander so they don't obstruct players so much @@ -236,15 +231,14 @@ class WorkerAutomation( val improvement = bestRoadAvailable.improvement(ruleSet)!! tileToConstructRoadOn.startWorkingOnImprovement(improvement, civInfo, unit) } - if (WorkerAutomationConst.consoleOutput) - println("WorkerAutomation: ${unit.label()} -> connect city ${bfs.startingPoint.getCity()?.name} to ${cityTile.getCity()!!.name} on $tileToConstructRoadOn") + debug("WorkerAutomation: %s -> connect city %s to %s on %s", + unit.label(), bfs.startingPoint.getCity()?.name, cityTile.getCity()!!.name, tileToConstructRoadOn) return true } if (bfs.hasEnded()) break bfs.nextStep() } - if (WorkerAutomationConst.consoleOutput) - println("WorkerAutomation: ${unit.label()} -> connect city ${bfs.startingPoint.getCity()?.name} failed at BFS size ${bfs.size()}") + debug("WorkerAutomation: ${unit.label()} -> connect city ${bfs.startingPoint.getCity()?.name} failed at BFS size ${bfs.size()}") } return false diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 125cb5047b..ba737538ac 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -3,6 +3,7 @@ package com.unciv.logic.battle import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.utils.debug import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.* import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers @@ -75,10 +76,7 @@ object Battle { } fun attack(attacker: ICombatant, defender: ICombatant) { - if (UncivGame.Current.alertBattle) { - println(attacker.getCivInfo().civName + " " + attacker.getName() + " attacked " + - defender.getCivInfo().civName + " " + defender.getName()) - } + debug("%s %s attacked %s %s", attacker.getCivInfo().civName, attacker.getName(), defender.getCivInfo().civName, defender.getName()) val attackedTile = defender.getTile() if (attacker is MapUnitCombatant) { attacker.unit.attacksSinceTurnStart.add(Vector2(attackedTile.position)) diff --git a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt index 2b8e4a1a6c..9793f17f7f 100644 --- a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt @@ -2,6 +2,7 @@ import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.utils.debug import com.unciv.logic.battle.Battle import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.NotificationIcon @@ -32,7 +33,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ } private fun destroyBuildingsOnCapture() { - city.apply { + city.apply { // Possibly remove other buildings for (building in cityConstructions.getBuiltBuildings()) { when { @@ -49,7 +50,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ } } } - + private fun removeBuildingsOnMoveToCiv(oldCiv: CivilizationInfo) { city.apply { // Remove all buildings provided for free to this city @@ -60,7 +61,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ // Remove all buildings provided for free from here to other cities (e.g. CN Tower) for ((cityId, buildings) in cityConstructions.freeBuildingsProvidedFromThisCity) { val city = oldCiv.cities.firstOrNull { it.id == cityId } ?: continue - println("Removing buildings $buildings from city ${city.name}") + debug("Removing buildings %s from city %s", buildings, city.name) for (building in buildings) { city.cityConstructions.removeBuilding(building) } @@ -88,9 +89,9 @@ class CityInfoConquestFunctions(val city: CityInfo){ } } - /** Function for stuff that should happen on any capture, be it puppet, annex or liberate. + /** Function for stuff that should happen on any capture, be it puppet, annex or liberate. * Stuff that should happen any time a city is moved between civs, so also when trading, - * should go in `this.moveToCiv()`, which this function also calls. + * should go in `this.moveToCiv()`, which this function also calls. */ private fun conquerCity(conqueringCiv: CivilizationInfo, conqueredCiv: CivilizationInfo, receivingCiv: CivilizationInfo) { val goldPlundered = getGoldForCapturingCity(conqueringCiv) @@ -101,7 +102,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ val reconqueredCityWhileStillInResistance = previousOwner == conqueringCiv.civName && isInResistance() destroyBuildingsOnCapture() - + this@CityInfoConquestFunctions.moveToCiv(receivingCiv) Battle.destroyIfDefeated(conqueredCiv, conqueringCiv) @@ -272,12 +273,12 @@ class CityInfoConquestFunctions(val city: CityInfo){ // Stop WLTKD if it's still going resetWLTKD() - + // Remove their free buildings from this city and remove free buildings provided by the city from their cities removeBuildingsOnMoveToCiv(oldCiv) // Place palace for newCiv if this is the only city they have - // This needs to happen _before_ free buildings are added, as somtimes these should + // This needs to happen _before_ free buildings are added, as somtimes these should // only be placed in the capital, and then there needs to be a capital. if (newCivInfo.cities.size == 1) { newCivInfo.moveCapitalTo(this) @@ -308,4 +309,4 @@ class CityInfoConquestFunctions(val city: CityInfo){ } } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 51739d9e1d..09b82d1213 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -4,23 +4,32 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.HexMath import com.unciv.logic.civilization.CivilizationInfo -import com.unciv.logic.map.* +import com.unciv.logic.map.MapParameters +import com.unciv.logic.map.MapShape +import com.unciv.logic.map.MapType +import com.unciv.logic.map.Perlin +import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap import com.unciv.models.Counter import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.ruleset.unique.Unique -import kotlin.math.* import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.mapeditor.MapGeneratorSteps +import com.unciv.utils.Log +import com.unciv.utils.debug +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sign import kotlin.random.Random class MapGenerator(val ruleset: Ruleset) { companion object { - // temporary instrumentation while tuning/debugging - const val consoleOutput = false private const val consoleTimings = false } @@ -74,7 +83,7 @@ class MapGenerator(val ruleset: Ruleset) { return map } - if (consoleOutput || consoleTimings) println("\nMapGenerator run with parameters $mapParameters") + if (consoleTimings) debug("\nMapGenerator run with parameters %s", mapParameters) runAndMeasure("MapLandmassGenerator") { MapLandmassGenerator(ruleset, randomness).generateLand(map) } @@ -164,7 +173,7 @@ class MapGenerator(val ruleset: Ruleset) { val startNanos = System.nanoTime() action() val delta = System.nanoTime() - startNanos - println("MapGenerator.$text took ${delta/1000000L}.${(delta/10000L).rem(100)}ms") + debug("MapGenerator.%s took %s.%sms", text, delta/1000000L, (delta/10000L).rem(100)) } private fun convertTerrains(map: TileMap, ruleset: Ruleset) { @@ -312,19 +321,19 @@ class MapGenerator(val ruleset: Ruleset) { private fun raiseMountainsAndHills(tileMap: TileMap) { val mountain = ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInChains) }?.name val hill = ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInGroups) }?.name - val flat = ruleset.terrains.values.firstOrNull { - !it.impassable && it.type == TerrainType.Land && !it.hasUnique(UniqueType.RoughTerrain) + val flat = ruleset.terrains.values.firstOrNull { + !it.impassable && it.type == TerrainType.Land && !it.hasUnique(UniqueType.RoughTerrain) }?.name if (flat == null) { - println("Ruleset seems to contain no flat terrain - can't generate heightmap") + debug("Ruleset seems to contain no flat terrain - can't generate heightmap") return } - if (consoleOutput && mountain != null) - println("Mountain-like generation for $mountain") - if (consoleOutput && hill != null) - println("Hill-like generation for $hill") + if (mountain != null) + debug("Mountain-like generation for %s", mountain) + if (hill != null) + debug("Hill-like generation for %s", mountain) val elevationSeed = randomness.RNG.nextInt().toDouble() tileMap.setTransients(ruleset) @@ -478,7 +487,7 @@ class MapGenerator(val ruleset: Ruleset) { temperature < 0.8 -> if (humidity < 0.5) Constants.plains else Constants.grassland temperature <= 1.0 -> if (humidity < 0.7) Constants.desert else Constants.plains else -> { - println("applyHumidityAndTemperature: Invalid temperature $temperature") + debug("applyHumidityAndTemperature: Invalid temperature %s", temperature) firstLandTerrain.name } } @@ -492,7 +501,7 @@ class MapGenerator(val ruleset: Ruleset) { if (matchingTerrain != null) tile.baseTerrain = matchingTerrain.terrain.name else { tile.baseTerrain = firstLandTerrain.name - println("applyHumidityAndTemperature: No terrain found for temperature: $temperature, humidity: $humidity") + debug("applyHumidityAndTemperature: No terrain found for temperature: %s, humidity: %s", temperature, humidity) } tile.setTerrainTransients() } @@ -542,7 +551,7 @@ class MapGenerator(val ruleset: Ruleset) { ruleset.terrains.values.asSequence() .filter { it.type == TerrainType.Water } .map { it.name }.toSet() - val iceEquivalents: List = + val iceEquivalents: List = ruleset.terrains.values.asSequence() .filter { terrain -> terrain.type == TerrainType.TerrainFeature && @@ -645,8 +654,8 @@ class MapGenerationRandomness { } if (chosenTiles.size == number || distanceBetweenResources == 1) { // Either we got them all, or we're not going to get anything better - if (MapGenerator.consoleOutput && distanceBetweenResources < initialDistance) - println("chooseSpreadOutLocations: distance $distanceBetweenResources < initial $initialDistance") + if (Log.shouldLog() && distanceBetweenResources < initialDistance) + debug("chooseSpreadOutLocations: distance $distanceBetweenResources < initial $initialDistance") return chosenTiles } } diff --git a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt index 9432ea1e5d..496001e07e 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt @@ -1,6 +1,7 @@ package com.unciv.logic.map.mapgenerator import com.unciv.Constants +import com.unciv.utils.debug import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.models.ruleset.Ruleset @@ -49,8 +50,7 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration } } - if (MapGenerator.consoleOutput) - println("Natural Wonders for this game: $spawned") + debug("Natural Wonders for this game: %s", spawned) } private fun Unique.getIntParam(index: Int) = params[index].toInt() @@ -156,15 +156,13 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration clearTile(tile) } } - if (MapGenerator.consoleOutput) - println("Natural Wonder ${wonder.name} @${location.position}") + debug("Natural Wonder %s @%s", wonder.name, location.position) return true } } - if (MapGenerator.consoleOutput) - println("No suitable location for ${wonder.name}") + debug("No suitable location for %s", wonder.name) return false } diff --git a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt index 9ea1569fc8..0d68839737 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt @@ -2,6 +2,7 @@ package com.unciv.logic.map.mapgenerator import com.badlogic.gdx.math.Vector2 import com.unciv.Constants +import com.unciv.utils.debug import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.models.ruleset.Ruleset @@ -93,7 +94,7 @@ class RiverGenerator( } riverCoordinate = newCoordinate } - println("River reached max length!") + debug("River reached max length!") } /* diff --git a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt b/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt index 6093a26946..fb41be3604 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt @@ -1,7 +1,9 @@ package com.unciv.logic.multiplayer.storage +import com.unciv.utils.debug import com.unciv.json.json import com.unciv.ui.utils.UncivDateFormat.parseDate +import com.unciv.utils.Log import java.io.* import java.net.HttpURLConnection import java.net.URL @@ -41,10 +43,10 @@ object DropBox: FileStorage { return inputStream } catch (ex: Exception) { - println(ex.message) + debug("Dropbox exception", ex) val reader = BufferedReader(InputStreamReader(errorStream)) val responseString = reader.readText() - println(responseString) + debug("Response: %s", responseString) val error = json().fromJson(ErrorResponse::class.java, responseString) // Throw Exceptions based on the HTTP response from dropbox @@ -53,12 +55,11 @@ object DropBox: FileStorage { error.error_summary.startsWith("path/not_found/") -> throw FileNotFoundException() error.error_summary.startsWith("path/conflict/file") -> throw FileStorageConflictException() } - + return null } catch (error: Error) { - println(error.message) - val reader = BufferedReader(InputStreamReader(errorStream)) - println(reader.readText()) + Log.error("Dropbox error", error) + debug("Error stream: %s", { BufferedReader(InputStreamReader(errorStream)).readText() }) return null } } diff --git a/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt b/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt index c105d697cc..641ffd7eeb 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt @@ -2,6 +2,7 @@ package com.unciv.logic.multiplayer.storage import com.badlogic.gdx.Net import com.unciv.UncivGame +import com.unciv.utils.debug import java.io.BufferedReader import java.io.DataOutputStream import java.io.InputStreamReader @@ -48,11 +49,11 @@ object SimpleHttp { val text = BufferedReader(InputStreamReader(inputStream)).readText() action(true, text, responseCode) } catch (t: Throwable) { - println(t.message) + debug("Error during HTTP request", t) val errorMessageToReturn = if (errorStream != null) BufferedReader(InputStreamReader(errorStream)).readText() else t.message!! - println(errorMessageToReturn) + debug("Returning error message [%s]", errorMessageToReturn) action(false, errorMessageToReturn, if (errorStream != null) responseCode else null) } } diff --git a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt index 4dc4f664fd..8dfbea19aa 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt @@ -1,6 +1,7 @@ package com.unciv.logic.multiplayer.storage import com.badlogic.gdx.Net +import com.unciv.utils.debug import java.io.FileNotFoundException import java.lang.Exception @@ -9,7 +10,7 @@ class UncivServerFileStorage(val serverUrl: String, val timeout: Int = 30000) : SimpleHttp.sendRequest(Net.HttpMethods.PUT, fileUrl(fileName), data, timeout) { success, result, _ -> if (!success) { - println(result) + debug("Error from UncivServer during save: %s", result) throw Exception(result) } } @@ -20,7 +21,7 @@ class UncivServerFileStorage(val serverUrl: String, val timeout: Int = 30000) : SimpleHttp.sendGetRequest(fileUrl(fileName), timeout = timeout){ success, result, code -> if (!success) { - println(result) + debug("Error from UncivServer during load: %s", result) when (code) { 404 -> throw FileNotFoundException(result) else -> throw Exception(result) diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index cfa5d55b09..9619498688 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -34,6 +34,8 @@ import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.tr import com.unciv.ui.utils.colorFromRGB import com.unciv.ui.utils.getRelativeTextDistance +import com.unciv.utils.Log +import com.unciv.utils.debug import kotlin.collections.set object ModOptionsConstants { @@ -192,7 +194,7 @@ class Ruleset { fun allIHasUniques(): Sequence = allRulesetObjects() + sequenceOf(modOptions) - fun load(folderHandle: FileHandle, printOutput: Boolean) { + fun load(folderHandle: FileHandle) { val gameBasicsStartTime = System.currentTimeMillis() val modOptionsFile = folderHandle.child("ModOptions.json") @@ -342,8 +344,7 @@ class Ruleset { } } - val gameBasicsLoadTime = System.currentTimeMillis() - gameBasicsStartTime - if (printOutput) println("Loading ruleset - " + gameBasicsLoadTime + "ms") + debug("Loading ruleset - %sms", System.currentTimeMillis() - gameBasicsStartTime) } /** Building costs are unique in that they are dependant on info in the technology part. @@ -859,7 +860,7 @@ object RulesetCache : HashMap() { /** Returns error lines from loading the rulesets, so we can display the errors to users */ - fun loadRulesets(consoleMode: Boolean = false, printOutput: Boolean = false, noMods: Boolean = false) :List { + fun loadRulesets(consoleMode: Boolean = false, noMods: Boolean = false) :List { clear() for (ruleset in BaseRuleset.values()) { val fileName = "jsons/${ruleset.fullName}" @@ -867,7 +868,7 @@ object RulesetCache : HashMap() { if (consoleMode) FileHandle(fileName) else Gdx.files.internal(fileName) this[ruleset.fullName] = Ruleset().apply { - load(fileHandle, printOutput) + load(fileHandle) name = ruleset.fullName } } @@ -883,13 +884,16 @@ object RulesetCache : HashMap() { if (!modFolder.isDirectory) continue try { val modRuleset = Ruleset() - modRuleset.load(modFolder.child("jsons"), printOutput) + modRuleset.load(modFolder.child("jsons")) modRuleset.name = modFolder.name() modRuleset.folderLocation = modFolder this[modRuleset.name] = modRuleset - if (printOutput) { - println("Mod loaded successfully: " + modRuleset.name) - println(modRuleset.checkModLinks().getErrorText()) + debug("Mod loaded successfully: %s", modRuleset.name) + if (Log.shouldLog()) { + val modLinksErrors = modRuleset.checkModLinks() + if (modLinksErrors.any()) { + debug("checkModLinks errors: %s", modLinksErrors.getErrorText()) + } } } catch (ex: Exception) { errorLines += "Exception loading mod '${modFolder.name()}':" @@ -897,7 +901,7 @@ object RulesetCache : HashMap() { errorLines += " ${ex.cause?.localizedMessage}" } } - if (printOutput) for (line in errorLines) println(line) + if (Log.shouldLog()) for (line in errorLines) debug(line) return errorLines } diff --git a/core/src/com/unciv/models/tilesets/TileSetCache.kt b/core/src/com/unciv/models/tilesets/TileSetCache.kt index c73f22cb12..20f88afb58 100644 --- a/core/src/com/unciv/models/tilesets/TileSetCache.kt +++ b/core/src/com/unciv/models/tilesets/TileSetCache.kt @@ -7,6 +7,7 @@ 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 TileSetCache : HashMap() { private data class TileSetAndMod(val tileSet: String, val mod: String) @@ -36,7 +37,7 @@ object TileSetCache : HashMap() { } } - fun loadTileSetConfigs(consoleMode: Boolean = false, printOutput: Boolean = false){ + fun loadTileSetConfigs(consoleMode: Boolean = false){ allConfigs.clear() var tileSetName = "" @@ -51,21 +52,16 @@ object TileSetCache : HashMap() { val key = TileSetAndMod(tileSetName, "") assert(key !in allConfigs) allConfigs[key] = json().fromJsonFile(TileSetConfig::class.java, configFile) - if (printOutput) { - println("TileSetConfig loaded successfully: ${configFile.name()}") - println() - } + debug("TileSetConfig loaded successfully: %s", configFile.name()) } catch (ex: Exception){ - if (printOutput){ - println("Exception loading TileSetConfig '${configFile.path()}':") - println(" ${ex.localizedMessage}") - println(" (Source file ${ex.stackTrace[0].fileName} line ${ex.stackTrace[0].lineNumber})") - } + debug("Exception loading TileSetConfig '%s':", configFile.path()) + debug(" %s", ex.localizedMessage) + debug(" (Source file %s line %s)", ex.stackTrace[0].fileName, ex.stackTrace[0].lineNumber) } } //load mod TileSets - val modsHandles = + val modsHandles = if (consoleMode) FileHandle("mods").list().toList() else RulesetCache.values.mapNotNull { it.folderLocation } @@ -80,17 +76,12 @@ object TileSetCache : HashMap() { val key = TileSetAndMod(tileSetName, modName) assert(key !in allConfigs) allConfigs[key] = json().fromJsonFile(TileSetConfig::class.java, configFile) - if (printOutput) { - println("TileSetConfig loaded successfully: ${configFile.path()}") - println() - } + debug("TileSetConfig loaded successfully: %s", configFile.path()) } } catch (ex: Exception){ - if (printOutput) { - println("Exception loading TileSetConfig '${modFolder.name()}/jsons/TileSets/${tileSetName}':") - println(" ${ex.localizedMessage}") - println(" (Source file ${ex.stackTrace[0].fileName} line ${ex.stackTrace[0].lineNumber})") - } + debug("Exception loading TileSetConfig '%s/jsons/TileSets/%s':", modFolder.name(), tileSetName) + debug(" %s", ex.localizedMessage) + debug(" (Source file %s line %s)", ex.stackTrace[0].fileName, ex.stackTrace[0].lineNumber) } } diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index c27480aa16..48b0fa1574 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -16,6 +16,7 @@ import com.unciv.models.ruleset.unique.* import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.ruleset.unit.UnitType +import com.unciv.utils.debug import java.io.File import java.lang.reflect.Field import java.lang.reflect.Modifier @@ -143,7 +144,7 @@ object TranslationFileWriter { } val translationKey = line.split(" = ")[0].replace("\\n", "\n") - val hashMapKey = + val hashMapKey = if (translationKey == Translations.englishConditionalOrderingString) Translations.englishConditionalOrderingString else translationKey @@ -279,7 +280,7 @@ object TranslationFileWriter { .sortedBy { it.name() } // generatedStrings maintains order, so let's feed it a predictable one // One set per json file, secondary loop var. Could be nicer to isolate all per-file - // processing into another class, but then we'd have to pass uniqueIndexOfNewLine around. + // processing into another class, but then we'd have to pass uniqueIndexOfNewLine around. lateinit var resultStrings: MutableSet init { @@ -304,7 +305,7 @@ object TranslationFileWriter { } val displayName = if (jsonsFolder.name() != "jsons") jsonsFolder.name() else jsonsFolder.parent().name() // Show mod name - println("Translation writer took ${System.currentTimeMillis() - startMillis}ms for $displayName") + debug("Translation writer took %sms for %s", System.currentTimeMillis() - startMillis, displayName) } fun submitString(string: String) { @@ -390,7 +391,7 @@ object TranslationFileWriter { // Promotion names are not uniques but since we did the "[unitName] ability" // they need the "parameters" treatment too // Same for victory milestones - (field.name == "uniques" || field.name == "promotions" || field.name == "milestones") + (field.name == "uniques" || field.name == "promotions" || field.name == "milestones") && (fieldValue is java.util.AbstractCollection<*>) -> for (item in fieldValue) if (item is String) submitString(item, Unique(item)) else serializeElement(item!!) @@ -478,7 +479,7 @@ object TranslationFileWriter { } //endregion - //region Fastlane + //region Fastlane /** This writes translated short_description.txt and full_description.txt files into the Fastlane structure. * @param [translations] A [Translations] instance with all languages loaded. @@ -511,7 +512,7 @@ object TranslationFileWriter { File(path + File.separator + fileName).writeText(fileContent) } } - + // Original changelog entry, written by incrementVersionAndChangelog, is often changed manually for readability. // This updates the fastlane changelog entry to match the latest one in changelog.md private fun updateFastlaneChangelog() { diff --git a/core/src/com/unciv/models/translations/Translations.kt b/core/src/com/unciv/models/translations/Translations.kt index c102913eb0..633cd0c5df 100644 --- a/core/src/com/unciv/models/translations/Translations.kt +++ b/core/src/com/unciv/models/translations/Translations.kt @@ -6,6 +6,8 @@ import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.unique.Unique import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats +import com.unciv.utils.Log +import com.unciv.utils.debug import java.util.* import kotlin.collections.HashMap import kotlin.collections.LinkedHashSet @@ -30,7 +32,7 @@ import kotlin.collections.LinkedHashSet * @see String.tr for more explanations (below) */ class Translations : LinkedHashMap(){ - + var percentCompleteOfLanguages = HashMap() .apply { put("English",100) } // So even if we don't manage to load the percentages, we can still pass the language screen @@ -79,7 +81,7 @@ class Translations : LinkedHashMap(){ /** This reads all translations for a specific language, including _all_ installed mods. * Vanilla translations go into `this` instance, mod translations into [modsWithTranslations]. */ - private fun tryReadTranslationForLanguage(language: String, printOutput: Boolean) { + private fun tryReadTranslationForLanguage(language: String) { val translationStart = System.currentTimeMillis() val translationFileName = "jsons/translations/$language.properties" @@ -90,7 +92,7 @@ class Translations : LinkedHashMap(){ // which is super odd because everyone should support UTF-8 languageTranslations = TranslationFileReader.read(Gdx.files.internal(translationFileName)) } catch (ex: Exception) { - println("Exception reading translations for $language: ${ex.message}") + Log.error("Exception reading translations for $language", ex) return } @@ -106,15 +108,14 @@ class Translations : LinkedHashMap(){ try { translationsForMod.createTranslations(language, TranslationFileReader.read(modTranslationFile)) } catch (ex: Exception) { - println("Exception reading translations for ${modFolder.name()} $language: ${ex.message}") + Log.error("Exception reading translations for ${modFolder.name()} $language", ex) } } } createTranslations(language, languageTranslations) - val translationFilesTime = System.currentTimeMillis() - translationStart - if (printOutput) println("Loading translation file for $language - " + translationFilesTime + "ms") + debug("Loading translation file for %s - %sms", language, System.currentTimeMillis() - translationStart) } private fun createTranslations(language: String, languageTranslations: HashMap) { @@ -132,7 +133,7 @@ class Translations : LinkedHashMap(){ } fun tryReadTranslationForCurrentLanguage(){ - tryReadTranslationForLanguage(UncivGame.Current.settings.language, false) + tryReadTranslationForLanguage(UncivGame.Current.settings.language) } /** Get a list of supported languages for [readAllLanguagesTranslation] */ @@ -166,7 +167,7 @@ class Translations : LinkedHashMap(){ } /** Ensure _all_ languages are loaded, used by [TranslationFileWriter] and `TranslationTests` */ - fun readAllLanguagesTranslation(printOutput:Boolean=false) { + fun readAllLanguagesTranslation() { // Apparently you can't iterate over the files in a directory when running out of a .jar... // https://www.badlogicgames.com/forum/viewtopic.php?f=11&t=27250 // which means we need to list everything manually =/ @@ -174,11 +175,10 @@ class Translations : LinkedHashMap(){ val translationStart = System.currentTimeMillis() for (language in getLanguagesWithTranslationFile()) { - tryReadTranslationForLanguage(language, printOutput) + tryReadTranslationForLanguage(language) } - val translationFilesTime = System.currentTimeMillis() - translationStart - if(printOutput) println("Loading translation files - ${translationFilesTime}ms") + debug("Loading translation files - %sms", System.currentTimeMillis() - translationStart) } fun loadPercentageCompleteOfLanguages(){ @@ -186,14 +186,13 @@ class Translations : LinkedHashMap(){ percentCompleteOfLanguages = TranslationFileReader.readLanguagePercentages() - val translationFilesTime = System.currentTimeMillis() - startTime - println("Loading percent complete of languages - ${translationFilesTime}ms") + debug("Loading percent complete of languages - %sms", System.currentTimeMillis() - startTime) } - + fun getConditionalOrder(language: String): String { return getText(englishConditionalOrderingString, language, null) } - + fun placeConditionalsAfterUnique(language: String): Boolean { if (get(conditionalUniqueOrderString, language, null)?.get(language) == "before") return false @@ -207,15 +206,15 @@ class Translations : LinkedHashMap(){ val translation = getText("\" \"", language, null) return translation.substring(1, translation.length-1) } - + fun shouldCapitalize(language: String): Boolean { return get(shouldCapitalizeString, language, null)?.get(language)?.toBoolean() ?: true } - + companion object { // Whenever this string is changed, it should also be changed in the translation files! - // It is mostly used as the template for translating the order of conditionals - const val englishConditionalOrderingString = + // It is mostly used as the template for translating the order of conditionals + const val englishConditionalOrderingString = " " const val conditionalUniqueOrderString = "ConditionalsPlacement" const val shouldCapitalizeString = "StartWithCapitalLetter" diff --git a/core/src/com/unciv/ui/audio/MusicController.kt b/core/src/com/unciv/ui/audio/MusicController.kt index 74aac3cca1..5279edd61d 100644 --- a/core/src/com/unciv/ui/audio/MusicController.kt +++ b/core/src/com/unciv/ui/audio/MusicController.kt @@ -7,6 +7,8 @@ import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame import com.unciv.models.metadata.GameSettings import com.unciv.logic.multiplayer.storage.DropBox +import com.unciv.utils.Log +import com.unciv.utils.debug import java.util.* import kotlin.concurrent.thread import kotlin.concurrent.timer @@ -15,7 +17,7 @@ import kotlin.math.roundToInt /** * Play, choose, fade-in/out and generally manage music track playback. - * + * * Main methods: [chooseTrack], [pause], [resume], [setModList], [isPlaying], [gracefulShutdown] */ class MusicController { @@ -35,8 +37,6 @@ class MusicController { private const val musicHistorySize = 8 // number of names to keep to avoid playing the same in short succession private val fileExtensions = listOf("mp3", "ogg", "wav") // All Gdx formats - internal const val consoleLog = false - private fun getFile(path: String) = if (musicLocation == FileType.External && Gdx.files.isExternalStorageAvailable) Gdx.files.external(path) @@ -232,8 +232,7 @@ class MusicController { clearNext() clearCurrent() musicHistory.clear() - if (consoleLog) - println("MusicController shut down.") + debug("MusicController shut down.") } private fun audioExceptionHandler(ex: Throwable, music: Music) { @@ -247,12 +246,7 @@ class MusicController { if (music == next?.music) clearNext() if (music == current?.music) clearCurrent() - if (consoleLog) { - println("${ex.javaClass.simpleName} playing music: ${ex.message}") - if (ex.stackTrace != null) ex.printStackTrace() - } else { - println("Error playing music: ${ex.message ?: ""}") - } + Log.error("Error playing music", ex) // Since this is a rare emergency, go a simple way to reboot music later thread(isDaemon = true) { @@ -303,7 +297,7 @@ class MusicController { // Then just pick the first one. Not as wasteful as it looks - need to check all names anyway )).firstOrNull() // Note: shuffled().sortedWith(), ***not*** .sortedWith(.., Random) - // the latter worked with older JVM's, current ones *crash* you when a compare is not transitive. + // the latter worked with older JVM's, current ones *crash* you when a compare is not transitive. } private fun fireOnChange() { @@ -324,8 +318,7 @@ class MusicController { try { onTrackChangeListener?.invoke(trackLabel) } catch (ex: Throwable) { - if (consoleLog) - println("onTrackChange event invoke failed: ${ex.message}") + debug("onTrackChange event invoke failed", ex) onTrackChangeListener = null } } @@ -346,7 +339,7 @@ class MusicController { * Chooses and plays a music track using an adaptable approach - for details see the wiki. * Called without parameters it will choose a new ambient music track and start playing it with fade-in/out. * Will do nothing when no music files exist or the master volume is zero. - * + * * @param prefix file name prefix, meant to represent **Context** - in most cases a Civ name * @param suffix file name suffix, meant to represent **Mood** - e.g. Peace, War, Theme, Defeat, Ambient * (Ambient is the default when a track ends and exists so War Peace and the others are not chosen in that case) @@ -355,7 +348,7 @@ class MusicController { */ fun chooseTrack ( prefix: String = "", - suffix: String = "Ambient", + suffix: String = "Ambient", flags: EnumSet = EnumSet.noneOf(MusicTrackChooserFlags::class.java) ): Boolean { if (baseVolume == 0f) return false @@ -364,8 +357,7 @@ class MusicController { if (musicFile == null) { // MustMatch flags at work or Music folder empty - if (consoleLog) - println("No music found for prefix=$prefix, suffix=$suffix, flags=$flags") + debug("No music found for prefix=%s, suffix=%s, flags=%s", prefix, suffix, flags) return false } if (musicFile.path() == currentlyPlaying()) @@ -381,8 +373,7 @@ class MusicController { state = ControllerState.Silence // will retry after one silence period next = null }, onSuccess = { - if (consoleLog) - println("Music queued: ${musicFile.path()} for prefix=$prefix, suffix=$suffix, flags=$flags") + debug("Music queued: %s for prefix=%s, suffix=%s, flags=%s", musicFile.path(), prefix, suffix, flags) if (musicHistory.size >= musicHistorySize) musicHistory.removeFirst() musicHistory.addLast(musicFile.path()) @@ -407,7 +398,7 @@ class MusicController { startTimer() return true } - + /** Variant of [chooseTrack] that tries several moods ([suffixes]) until a match is chosen */ fun chooseTrack ( prefix: String = "", @@ -422,12 +413,11 @@ class MusicController { /** * Pause playback with fade-out - * + * * @param speedFactor accelerate (>1) or slow down (<1) the fade-out. Clamped to 1/1000..1000. */ fun pause(speedFactor: Float = 1f) { - if (consoleLog) - println("MusicTrackController.pause called") + debug("MusicTrackController.pause called") if ((state != ControllerState.Playing && state != ControllerState.PlaySingle) || current == null) return val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f) current!!.startFade(MusicTrackController.State.FadeOut, fadingStep) @@ -443,8 +433,7 @@ class MusicController { * @param speedFactor accelerate (>1) or slow down (<1) the fade-in. Clamped to 1/1000..1000. */ fun resume(speedFactor: Float = 1f) { - if (consoleLog) - println("MusicTrackController.resume called") + debug("MusicTrackController.resume called") if (state == ControllerState.Pause && current != null) { val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f) current!!.startFade(MusicTrackController.State.FadeIn, fadingStep) diff --git a/core/src/com/unciv/ui/audio/MusicTrackController.kt b/core/src/com/unciv/ui/audio/MusicTrackController.kt index 80d526dec3..e513d2b3a7 100644 --- a/core/src/com/unciv/ui/audio/MusicTrackController.kt +++ b/core/src/com/unciv/ui/audio/MusicTrackController.kt @@ -3,6 +3,8 @@ package com.unciv.ui.audio import com.badlogic.gdx.Gdx import com.badlogic.gdx.audio.Music import com.badlogic.gdx.files.FileHandle +import com.unciv.utils.Log +import com.unciv.utils.debug /** Wraps one Gdx Music instance and manages loading, playback, fading and cleanup */ internal class MusicTrackController(private var volume: Float) { @@ -54,8 +56,7 @@ internal class MusicTrackController(private var volume: Float) { clear() } else { state = State.Idle - if (MusicController.consoleLog) - println("Music loaded: $file") + debug("Music loaded %s", file) onSuccess?.invoke(this) } } catch (ex: Exception) { @@ -73,7 +74,7 @@ internal class MusicTrackController(private var volume: Float) { } /** Starts fadeIn or fadeOut. - * + * * Note this does _not_ set the current fade "percentage" to allow smoothly changing direction mid-fade * @param step Overrides current fade step only if >0 */ @@ -165,12 +166,7 @@ internal class MusicTrackController(private var volume: Float) { private fun audioExceptionHandler(ex: Throwable) { clear() - if (MusicController.consoleLog) { - println("${ex.javaClass.simpleName} playing music: ${ex.message}") - if (ex.stackTrace != null) ex.printStackTrace() - } else { - println("Error playing music: ${ex.message ?: ""}") - } + Log.error("Error playing music", ex) } //endregion diff --git a/core/src/com/unciv/ui/audio/Sounds.kt b/core/src/com/unciv/ui/audio/Sounds.kt index ace3cd76b7..7c2ab2d4a4 100644 --- a/core/src/com/unciv/ui/audio/Sounds.kt +++ b/core/src/com/unciv/ui/audio/Sounds.kt @@ -7,6 +7,7 @@ import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame import com.unciv.models.UncivSound import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.utils.debug import kotlinx.coroutines.delay import java.io.File @@ -43,8 +44,6 @@ import java.io.File * app lifetime - and we do dispose them when the app is disposed. */ object Sounds { - private const val debugMessages = false - @Suppress("EnumEntryName") private enum class SupportedExtensions { mp3, ogg, wav } // Per Gdx docs, no aac/m4a @@ -69,7 +68,7 @@ object Sounds { // Seems the mod list has changed - clear the cache clearCache() modListHash = newHash - if (debugMessages) println("Sound cache cleared") + debug("Sound cache cleared") } /** Release cached Sound resources */ @@ -135,12 +134,12 @@ object Sounds { @Suppress("LiftReturnOrAssignment") if (file == null || !file.exists()) { - if (debugMessages) println("Sound ${sound.value} not found!") + debug("Sound %s not found!", sound.value) // remember that the actual file is missing soundMap[sound] = null return null } else { - if (debugMessages) println("Sound ${sound.value} loaded from ${file.path()}") + debug("Sound %s loaded from %s", sound.value, file.path()) val newSound = Gdx.audio.newSound(file) // Store Sound for reuse soundMap[sound] = newSound diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt index 551393705e..9f318355b0 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt @@ -17,6 +17,7 @@ import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.addSeparator import com.unciv.ui.utils.onClick import com.unciv.ui.utils.toLabel +import com.unciv.utils.Log import kotlin.math.max /* Ideas: @@ -111,7 +112,7 @@ class FormattedLine ( val displayColor: Color by lazy { parseColor() } /** Returns true if this formatted line will not display anything */ - fun isEmpty(): Boolean = text.isEmpty() && extraImage.isEmpty() && + fun isEmpty(): Boolean = text.isEmpty() && extraImage.isEmpty() && !starred && icon.isEmpty() && link.isEmpty() && !separator /** Self-check to potentially support the mod checker @@ -187,9 +188,8 @@ class FormattedLine ( val result = HashMap() allObjectMapsSequence.filter { !it.first.hide } .flatMap { pair -> pair.second.keys.asSequence().map { key -> pair.first to key } } - .forEach { + .forEach { result[it.second] = it.first - //println(" ${it.second} is a ${it.first}") } result["Maya Long Count calendar cycle"] = CivilopediaCategories.Tutorial @@ -231,7 +231,7 @@ class FormattedLine ( /** * Renders the formatted line as a scene2d [Actor] (currently always a [Table]) * @param labelWidth Total width to render into, needed to support wrap on Labels. - * @param iconDisplay Flag to omit link or all images. + * @param iconDisplay Flag to omit link or all images. */ fun render(labelWidth: Float, iconDisplay: IconDisplay = IconDisplay.All): Actor { if (extraImage.isNotEmpty()) { @@ -250,7 +250,7 @@ class FormattedLine ( val height = width * image.height / image.width table.add(image).size(width, height) } catch (exception: Exception) { - println ("${exception.message}: ${exception.cause?.message}") + Log.error("Exception while rendering civilopedia text", exception) } return table } @@ -436,7 +436,7 @@ interface ICivilopediaText { * **or** [UncivGame.Current.worldScreen][UncivGame.worldScreen] being initialized, * this should be able to run from the main menu. * (And the info displayed should be about the **ruleset**, not the player situation) - * + * * Default implementation is empty - no need to call super in overrides. * * @param ruleset The current ruleset for the Civilopedia viewer diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt index 29ea0d7bbe..f0634b1539 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt @@ -14,6 +14,7 @@ import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.ImageGetter import com.unciv.ui.popup.ToastPopup import com.unciv.ui.utils.* +import com.unciv.utils.Log import java.io.PrintWriter import java.io.StringWriter import kotlin.concurrent.thread @@ -107,7 +108,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() { } init { - println(text) // Also print to system terminal. + Log.error(text) // Also print to system terminal. thread { throw exception } // this is so the GPC logs catch the exception stage.addActor(makeLayoutTable()) } diff --git a/core/src/com/unciv/ui/images/ImageGetter.kt b/core/src/com/unciv/ui/images/ImageGetter.kt index 57f97f5887..8ac67cbf4d 100644 --- a/core/src/com/unciv/ui/images/ImageGetter.kt +++ b/core/src/com/unciv/ui/images/ImageGetter.kt @@ -25,6 +25,7 @@ import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.stats.Stats import com.unciv.models.tilesets.TileSetCache import com.unciv.ui.utils.* +import com.unciv.utils.debug import kotlin.math.atan2 import kotlin.math.max import kotlin.math.min @@ -88,7 +89,7 @@ object ImageGetter { val extraAtlas = if (mod.isEmpty()) fileName else if (fileName == "game") mod else "$mod/$fileName" var tempAtlas = atlases[extraAtlas] // fetch if cached if (tempAtlas == null) { - println("Loading $extraAtlas = ${file.path()}") + debug("Loading %s = %s", extraAtlas, file.path()) tempAtlas = TextureAtlas(file) // load if not atlases[extraAtlas] = tempAtlas // cache the freshly loaded } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt index af5085bbc7..3b48f4bcdd 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt @@ -16,6 +16,7 @@ import com.unciv.ui.images.ImageGetter import com.unciv.ui.mapeditor.MapEditorOptionsTab.TileMatchFuzziness import com.unciv.ui.popup.ToastPopup import com.unciv.ui.utils.* +import com.unciv.utils.Log class MapEditorEditTab( private val editorScreen: MapEditorScreen, @@ -208,7 +209,7 @@ class MapEditorEditTab( val riverGenerator = RiverGenerator(editorScreen.tileMap, randomness, ruleset) riverGenerator.spawnRiver(riverStartTile!!, riverEndTile!!, resultingTiles) } catch (ex: Exception) { - println(ex.message) + Log.error("Exception while generating rivers", ex) ToastPopup("River generation failed!", editorScreen) } riverStartTile = null diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt index 13bb6ca897..cf37d481b3 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt @@ -16,6 +16,7 @@ import com.unciv.ui.newgamescreen.MapParametersTable import com.unciv.ui.popup.Popup import com.unciv.ui.popup.ToastPopup import com.unciv.ui.utils.* +import com.unciv.utils.Log import kotlin.concurrent.thread class MapEditorGenerateTab( @@ -116,7 +117,7 @@ class MapEditorGenerateTab( } } MapGeneratorSteps.Landmass -> { - // This step _could_ run on an existing tileMap, but that opens a loophole where you get hills on water - fixing that is more expensive than always recreating + // This step _could_ run on an existing tileMap, but that opens a loophole where you get hills on water - fixing that is more expensive than always recreating mapParameters.type = MapType.empty val generatedMap = generator!!.generateMap(mapParameters) mapParameters.type = editorScreen.newMapParameters.type @@ -136,7 +137,7 @@ class MapEditorGenerateTab( } } } catch (exception: Exception) { - println("Map generator exception: ${exception.message}") + Log.error("Exception while generating map", exception) Gdx.app.postRunnable { setButtonsEnabled(true) Gdx.input.inputProcessor = editorScreen.stage diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt index 7c28a146c0..f95807777c 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt @@ -13,6 +13,7 @@ import com.unciv.ui.popup.Popup import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.utils.* +import com.unciv.utils.Log import kotlin.concurrent.thread class MapEditorLoadTab( @@ -134,7 +135,7 @@ class MapEditorLoadTab( } catch (ex: Throwable) { needPopup = false popup?.close() - println("Error displaying map \"$chosenMap\": ${ex.message}") + Log.error("Error displaying map \"$chosenMap\"", ex) Gdx.input.inputProcessor = editorScreen.stage ToastPopup("Error loading map!", editorScreen) } @@ -143,7 +144,7 @@ class MapEditorLoadTab( needPopup = false Gdx.app.postRunnable { popup?.close() - println("Error loading map \"$chosenMap\": ${ex.message}") + Log.error("Error loading map \"$chosenMap\"", ex) ToastPopup("{Error loading map!}" + (if (ex is UncivShowableException) "\n{${ex.message}}" else ""), editorScreen) } @@ -151,4 +152,4 @@ class MapEditorLoadTab( } fun noMapsAvailable() = mapFiles.noMapsAvailable() -} \ No newline at end of file +} diff --git a/core/src/com/unciv/ui/options/ModCheckTab.kt b/core/src/com/unciv/ui/options/ModCheckTab.kt index 482dbb27bf..1ad448bec6 100644 --- a/core/src/com/unciv/ui/options/ModCheckTab.kt +++ b/core/src/com/unciv/ui/options/ModCheckTab.kt @@ -16,6 +16,8 @@ import com.unciv.ui.images.ImageGetter import com.unciv.ui.newgamescreen.TranslatedSelectBox import com.unciv.ui.popup.ToastPopup import com.unciv.ui.utils.* +import com.unciv.utils.Log +import com.unciv.utils.debug private const val MOD_CHECK_WITHOUT_BASE = "-none-" @@ -194,7 +196,7 @@ class ModCheckTab( deprecatedUnique.sourceObjectType!! ) for (error in modInvariantErrors) - println(error.text + " - " + error.errorSeverityToReport) + Log.error("ModInvariantError: %s - %s", error.text, error.errorSeverityToReport) if (modInvariantErrors.isNotEmpty()) continue // errors means no autoreplace if (mod.modOptions.isBaseRuleset) { @@ -206,12 +208,12 @@ class ModCheckTab( deprecatedUnique.sourceObjectType ) for (error in modSpecificErrors) - println(error.text + " - " + error.errorSeverityToReport) + Log.error("ModSpecificError: %s - %s", error.text, error.errorSeverityToReport) if (modSpecificErrors.isNotEmpty()) continue } deprecatedUniquesToReplacementText[deprecatedUnique.text] = uniqueReplacementText - println("Replace \"${deprecatedUnique.text}\" with \"$uniqueReplacementText\"") + debug("Replace \"%s\" with \"%s\"", deprecatedUnique.text, uniqueReplacementText) } return deprecatedUniquesToReplacementText } diff --git a/core/src/com/unciv/ui/pickerscreens/GitHub.kt b/core/src/com/unciv/ui/pickerscreens/GitHub.kt index 4746a874dd..3957872818 100644 --- a/core/src/com/unciv/ui/pickerscreens/GitHub.kt +++ b/core/src/com/unciv/ui/pickerscreens/GitHub.kt @@ -6,6 +6,8 @@ import com.unciv.json.fromJsonFile import com.unciv.json.json import com.unciv.logic.BackwardCompatibility.updateDeprecations import com.unciv.models.ruleset.ModOptions +import com.unciv.utils.Log +import com.unciv.utils.debug import java.io.* import java.net.HttpURLConnection import java.net.URL @@ -15,7 +17,7 @@ import java.util.zip.ZipFile /** * Utility managing Github access (except the link in WorldScreenCommunityPopup) - * + * * Singleton - RateLimit is shared app-wide and has local variables, and is not tested for thread safety. * Therefore, additional effort is required should [tryGetGithubReposWithTopic] ever be called non-sequentially. * [download] and [downloadAndExtract] should be thread-safe as they are self-contained. @@ -28,8 +30,8 @@ object Github { /** * Helper opens am url and accesses its input stream, logging errors to the console * @param url String representing a [URL] to download. - * @param action Optional callback that will be executed between opening the connection and - * accessing its data - passes the [connection][HttpURLConnection] and allows e.g. reading the response headers. + * @param action Optional callback that will be executed between opening the connection and + * accessing its data - passes the [connection][HttpURLConnection] and allows e.g. reading the response headers. * @return The [InputStream] if successful, `null` otherwise. */ fun download(url: String, action: (HttpURLConnection) -> Unit = {}): InputStream? { @@ -40,9 +42,9 @@ object Github { return try { inputStream } catch (ex: Exception) { - println(ex.message) + Log.error("Exception during GitHub download", ex) val reader = BufferedReader(InputStreamReader(errorStream)) - println(reader.readText()) + Log.error("Message from GitHub: %s", reader.readText()) null } } @@ -121,7 +123,7 @@ object Github { if (file().renameTo(dest.child(name()).file())) return else if (file().renameTo(dest.file())) return - } + } moveTo(dest) } @@ -204,7 +206,7 @@ object Github { val reset = getHeaderLong("X-RateLimit-Reset") if (limit != maxRequestsPerInterval) - println("GitHub API Limit reported via http ($limit) not equal assumed value ($maxRequestsPerInterval)") + debug("GitHub API Limit reported via http (%s) not equal assumed value (%s)", limit, maxRequestsPerInterval) account = maxRequestsPerInterval - remaining if (reset == 0L) return firstRequest = (reset + 1L) * 1000L - intervalInMilliSeconds @@ -234,7 +236,7 @@ object Github { RateLimit.notifyHttpResponse(it) retries++ // An extra retry so the 403 is ignored in the retry count } - } ?: continue + } ?: continue return json().fromJson(RepoSearch::class.java, inputStream.bufferedReader().readText()) } return null @@ -350,7 +352,7 @@ object Zip { // (with mild changes to fit the FileHandles) // https://stackoverflow.com/questions/981578/how-to-unzip-files-recursively-in-java - println("Extracting $zipFile to $unzipDestination") + debug("Extracting %s to %s", zipFile, unzipDestination) // establish buffer for writing file val data = ByteArray(bufferSize) @@ -389,7 +391,7 @@ object Zip { if (!entry.isDirectory) { streamCopy ( zip.getInputStream(entry), destFile) } - // The new file has a current last modification time + // The new file has a current last modification time // and not the one stored in the archive - we could: // 'destFile.file().setLastModified(entry.time)' // but later handling will throw these away anyway, diff --git a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt index 5b7926e71c..75f153f1cb 100644 --- a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt +++ b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt @@ -6,6 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.EventListener import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.InputListener import com.badlogic.gdx.scenes.scene2d.Stage +import com.unciv.utils.debug /* * For now, many combination keys cannot easily be expressed. @@ -181,8 +182,7 @@ class KeyPressDispatcher(val name: String? = null) : HashMap Boolean)? = null) { - if (consoleLog) - println("$this: install") + debug("%s: install", this) if (installStage != null) uninstall() listener = object : InputListener() { @@ -198,13 +198,11 @@ class KeyPressDispatcher(val name: String? = null) : HashMap` to overwrite [disableLogsFrom] completely + * (potentially copy/pasting the default class list from here and adjusting to your liking) + * 3. While the application is running, set a breakpoint somewhere and do a "Watch"/"Evaluate expression" with `Log.disableLogsFrom.add/remove("Something")` + */ +object Log { + + /** Add -DnoLog= to not log these classes. */ + private val disabledLogsFromProperty = System.getProperty("noLog")?.split(',')?.toMutableSet() ?: mutableSetOf() + + /** Log tags (= class names) **containing** these Strings will not be logged. */ + val disableLogsFrom = if (disabledLogsFromProperty.isEmpty()) { + "Battle,KeyPressDispatcher,Music,Sounds,Translations,WorkerAutomation" + .split(',').toMutableSet() + } else { + disabledLogsFromProperty + } + + var backend: LogBackend = DefaultLogBackend() + + fun shouldLog(tag: Tag = getTag()): Boolean { + return !backend.isRelease() && !isTagDisabled(tag) + } + + /** + * Logs the given message by [java.lang.String.format]ting them with an optional list of [params]. + * + * Only actually does something when logging is enabled. + * + * A tag will be added equal to the name of the calling class. + * + * The [params] can contain value-producing lambdas, which will be called and their value used as parameter for the message instead. + */ + fun debug(msg: String, vararg params: Any?) { + if (backend.isRelease()) return + debug(getTag(), msg, *params) + } + + /** + * Logs the given message by [java.lang.String.format]ting them with an optional list of [params]. + * + * Only actually does something when logging is enabled. + * + * The [params] can contain value-producing lambdas, which will be called and their value used as parameter for the message instead. + */ + fun debug(tag: Tag, msg: String, vararg params: Any?) { + if (!shouldLog(tag)) return + val formatArgs = replaceLambdasWithValues(params) + doLog(backend::debug, tag, msg, *formatArgs) + } + + /** + * Logs the given [throwable] by appending it to [msg] + * + * Only actually does something when logging is enabled. + * + * A tag will be added equal to the name of the calling class. + */ + fun debug(msg: String, throwable: Throwable) { + if (backend.isRelease()) return + debug(getTag(), msg, throwable) + } + /** + * Logs the given [throwable] by appending it to [msg] + * + * Only actually does something when logging is enabled. + */ + fun debug(tag: Tag, msg: String, throwable: Throwable) { + if (!shouldLog(tag)) return + doLog(backend::debug, tag, buildThrowableMessage(msg, throwable)) + } + + /** + * Logs the given error message by [java.lang.String.format]ting them with an optional list of [params]. + * + * Always logs, even in release builds. + * + * A tag will be added equal to the name of the calling class. + * + * The [params] can contain value-producing lambdas, which will be called and their value used as parameter for the message instead. + */ + fun error(msg: String, vararg params: Any?) { + error(getTag(), msg, *params) + } + + + /** + * Logs the given error message by [java.lang.String.format]ting them with an optional list of [params]. + * + * Always logs, even in release builds. + * + * The [params] can contain value-producing lambdas, which will be called and their value used as parameter for the message instead. + */ + fun error(tag: Tag, msg: String, vararg params: Any?) { + val formatArgs = replaceLambdasWithValues(params) + doLog(backend::error, tag, msg, *formatArgs) + } + + /** + * Logs the given [throwable] by appending it to [msg] + * + * Always logs, even in release builds. + * + * A tag will be added equal to the name of the calling class. + */ + fun error(msg: String, throwable: Throwable) { + error(getTag(), msg, throwable) + } + /** + * Logs the given [throwable] by appending it to [msg] + * + * Always logs, even in release builds. + */ + fun error(tag: Tag, msg: String, throwable: Throwable) { + doLog(backend::error, tag, buildThrowableMessage(msg, throwable)) + } +} + +class Tag(val name: String) + +interface LogBackend { + fun debug(tag: Tag, curThreadName: String, msg: String) + fun error(tag: Tag, curThreadName: String, msg: String) + + /** Do not log on release builds for performance reasons. */ + fun isRelease(): Boolean +} + +/** Only for tests, or temporary main() functions */ +open class DefaultLogBackend : LogBackend { + override fun debug(tag: Tag, curThreadName: String, msg: String) { + println("${Instant.now()} [${curThreadName}] [${tag.name}] $msg") + } + + override fun error(tag: Tag, curThreadName: String, msg: String) { + println("${Instant.now()} [${curThreadName}] [${tag.name}] [ERROR] $msg") + } + + override fun isRelease(): Boolean { + return false + } +} + +/** Shortcut for [Log.debug] */ +fun debug(msg: String, vararg params: Any?) { + Log.debug(msg, *params) +} + +/** Shortcut for [Log.debug] */ +fun debug(tag: Tag, msg: String, vararg params: Any?) { + Log.debug(tag, msg, *params) +} + +/** Shortcut for [Log.debug] */ +fun debug(msg: String, throwable: Throwable) { + Log.debug(msg, throwable) +} + +/** Shortcut for [Log.debug] */ +fun debug(tag: Tag, msg: String, throwable: Throwable) { + Log.debug(tag, msg, throwable) +} + +private fun doLog(logger: (Tag, String, String) -> Unit, tag: Tag, msg: String, vararg params: Any?) { + logger(tag, Thread.currentThread().name, msg.format(*params)) +} + +private fun isTagDisabled(tag: Tag): Boolean { + return Log.disableLogsFrom.any { it in tag.name } +} + +private fun buildThrowableMessage(msg: String, throwable: Throwable): String { + return "$msg | ${throwable.stackTraceToString()}" +} + +private fun replaceLambdasWithValues(params: Array): Array { + var out: Array? = null + for (i in 0 until params.size) { + val param = params[i] + if (param is Function0<*>) { + if (out == null) out = arrayOf(*params) + out[i] = param.invoke() + } + } + return out ?: params +} + + +private fun getTag(): Tag { + val firstOutsideStacktrace = Throwable().stackTrace.filter { "com.unciv.utils.Log" !in it.className }.first() + val simpleClassName = firstOutsideStacktrace.className.substringAfterLast('.') + return Tag(removeAnonymousSuffix(simpleClassName)) +} + +private val ANONYMOUS_CLASS_PATTERN = Pattern.compile("(\\$\\d+)+$") // all "$123" at the end of the class name +private fun removeAnonymousSuffix(tag: String): String { + val matcher = ANONYMOUS_CLASS_PATTERN.matcher(tag) + return if (matcher.find()) { + matcher.replaceAll("") + } else { + tag + } +} diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index b00a31ac54..7b6f1ee1ba 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -34,6 +34,7 @@ import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.tilegroups.WorldTileGroup import com.unciv.ui.utils.* +import com.unciv.utils.Log class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap: TileMap): ZoomableScrollPane() { @@ -226,8 +227,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap try { tileToMoveTo = selectedUnit.movement.getTileToMoveToThisTurn(targetTile) } catch (ex: Exception) { - println("Exception in getTileToMoveToThisTurn: ${ex.message}") - ex.printStackTrace() + Log.error("Exception in getTileToMoveToThisTurn", ex) return@launchCrashHandling } // can't move here @@ -251,8 +251,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap moveUnitToTargetTile(selectedUnits.subList(1, selectedUnits.size), targetTile) } else removeUnitActionOverlay() //we're done here } catch (ex: Exception) { - println("Exception in moveUnitToTargetTile: ${ex.message}") - ex.printStackTrace() + Log.error("Exception in moveUnitToTargetTile", ex) } } } diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 9fa17c53ba..766942a441 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -16,6 +16,7 @@ import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.MainMenuScreen import com.unciv.UncivGame +import com.unciv.utils.debug import com.unciv.logic.GameInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.ReligionState @@ -128,10 +129,6 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas private val events = EventBus.EventReceiver() - companion object { - /** Switch for console logging of next turn duration */ - private const val consoleLog = false - } init { topBar.setPosition(0f, stage.height - topBar.height) @@ -377,7 +374,11 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } try { + debug("loadLatestMultiplayerState current game: gameId: %s, turn: %s, curCiv: %s", + game.worldScreen.gameInfo.gameId, game.worldScreen.gameInfo.turns, game.worldScreen.gameInfo.currentPlayer) val latestGame = game.onlineMultiplayer.downloadGame(gameInfo.gameId) + debug("loadLatestMultiplayerState downloaded game: gameId: %s, turn: %s, curCiv: %s", + latestGame.gameId, latestGame.turns, latestGame.currentPlayer) if (viewingCiv.civName == latestGame.currentPlayer || viewingCiv.civName == Constants.spectator) { game.platformSpecificHelper?.notifyTurnStarted() } @@ -660,8 +661,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas // on a separate thread so the user can explore their world while we're passing the turn nextTurnUpdateJob = launchCrashHandling("NextTurn", runAsDaemon = false) { - if (consoleLog) - println("\nNext turn starting " + Date().formatDate()) + debug("Next turn starting") val startTime = System.currentTimeMillis() val originalGameInfo = gameInfo val gameInfoClone = originalGameInfo.clone() @@ -693,8 +693,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas return@launchCrashHandling this@WorldScreen.game.gameInfo = gameInfoClone - if (consoleLog) - println("Next turn took ${System.currentTimeMillis()-startTime}ms") + debug("Next turn took %sms", System.currentTimeMillis() - startTime) val shouldAutoSave = gameInfoClone.turns % game.settings.turnsBetweenAutosaves == 0 diff --git a/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt b/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt index 03b1f2eb3c..6efef61e4f 100644 --- a/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt @@ -3,6 +3,7 @@ package com.unciv.app.desktop import com.unciv.Constants import com.unciv.UncivGame import com.unciv.UncivGameParameters +import com.unciv.utils.Log import com.unciv.logic.GameStarter import com.unciv.logic.civilization.PlayerType import com.unciv.logic.map.MapParameters @@ -22,6 +23,7 @@ internal object ConsoleLauncher { @ExperimentalTime @JvmStatic fun main(arg: Array) { + Log.backend = DesktopLogBackend() val version = "0.1" val consoleParameters = UncivGameParameters( diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index ba7efaaa06..d7e996e3d7 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -9,8 +9,9 @@ import com.badlogic.gdx.graphics.glutils.HdpiMode import com.sun.jna.Native import com.unciv.UncivGame import com.unciv.UncivGameParameters +import com.unciv.utils.Log +import com.unciv.utils.debug import com.unciv.logic.GameSaver -import com.unciv.models.metadata.GameSettings import com.unciv.ui.utils.Fonts import java.util.* import kotlin.concurrent.timer @@ -20,6 +21,7 @@ internal object DesktopLauncher { @JvmStatic fun main(arg: Array) { + Log.backend = DesktopLogBackend() // Solves a rendering problem in specific GPUs and drivers. // For more info see https://github.com/yairm210/Unciv/pull/3202 and https://github.com/LWJGL/lwjgl/issues/119 System.setProperty("org.lwjgl.opengl.Display.allowSoftwareOpenGL", "true") @@ -85,7 +87,7 @@ internal object DesktopLauncher { } } catch (ex: Throwable) { // This needs to be a Throwable because if we can't find the discord_rpc library, we'll get a UnsatisfiedLinkError, which is NOT an exception. - println("Could not initialize Discord") + debug("Could not initialize Discord") } } diff --git a/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt b/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt new file mode 100644 index 0000000000..0f9c10db7c --- /dev/null +++ b/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt @@ -0,0 +1,16 @@ +package com.unciv.app.desktop + +import com.unciv.utils.DefaultLogBackend +import java.lang.management.ManagementFactory + +class DesktopLogBackend : DefaultLogBackend() { + + // -ea (enable assertions) or kotlin debugging property as marker for a debug run. + // Can easily be added to IntelliJ/Android Studio launch configuration template for all launches. + private val release = !ManagementFactory.getRuntimeMXBean().getInputArguments().contains("-ea") + && System.getProperty("kotlinx.coroutines.debug") == null + + override fun isRelease(): Boolean { + return release + } +} diff --git a/desktop/src/com/unciv/app/desktop/ImagePacker.kt b/desktop/src/com/unciv/app/desktop/ImagePacker.kt index 35f0726184..9200d5c8f8 100644 --- a/desktop/src/com/unciv/app/desktop/ImagePacker.kt +++ b/desktop/src/com/unciv/app/desktop/ImagePacker.kt @@ -3,15 +3,17 @@ package com.unciv.app.desktop import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.tools.texturepacker.TexturePacker import com.badlogic.gdx.utils.Json +import com.unciv.utils.Log +import com.unciv.utils.debug import java.io.File /** * Entry point: _ImagePacker.[packImages] ()_ - * + * * Re-packs our texture assets into atlas + png File pairs, which will be loaded by the game. * With the exception of the ExtraImages folder and the Font system these are the only * graphics used (The source Image folders are unused at run time except here). - * + * * [TexturePacker] documentation is [here](https://github.com/libgdx/libgdx/wiki/Texture-packer) */ internal object ImagePacker { @@ -68,14 +70,14 @@ internal object ImagePacker { try { packImagesPerMod(mod.path, mod.path, defaultSettings) } catch (ex: Throwable) { - println("Exception in ImagePacker: ${ex.message}") + Log.error("Exception in ImagePacker: %s", ex.message) } } } } val texturePackingTime = System.currentTimeMillis() - startTime - println("Packing textures - " + texturePackingTime + "ms") + debug("Packing textures - %sms", texturePackingTime) } // Scan multiple image folders and generate an atlas for each - if outdated diff --git a/desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt b/desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt index bb8a5a00d2..9ea2e05793 100644 --- a/desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt @@ -7,6 +7,7 @@ import com.sun.jna.Pointer import com.sun.jna.platform.win32.User32 import com.sun.jna.platform.win32.WinNT import com.sun.jna.platform.win32.WinUser +import com.unciv.utils.Log import org.lwjgl.glfw.GLFWNativeWin32 class MultiplayerTurnNotifierDesktop: Lwjgl3WindowAdapter() { @@ -18,7 +19,7 @@ class MultiplayerTurnNotifierDesktop: Lwjgl3WindowAdapter() { null } } catch (e: UnsatisfiedLinkError) { - println("Error while initializing turn notifier: " + e.message) + Log.error("Error while initializing turn notifier", e) null } } @@ -58,7 +59,7 @@ class MultiplayerTurnNotifierDesktop: Lwjgl3WindowAdapter() { user32.FlashWindowEx(flashwinfo) } catch (e: Throwable) { /** try to ignore even if we get an [Error], just log it */ - println("Error while notifying the user of their turn: " + e.message) + Log.error("Error while notifying the user of their turn", e) } } } diff --git a/tests/src/com/unciv/testing/BasicTests.kt b/tests/src/com/unciv/testing/BasicTests.kt index b8aabb7c2b..f7881af88c 100644 --- a/tests/src/com/unciv/testing/BasicTests.kt +++ b/tests/src/com/unciv/testing/BasicTests.kt @@ -18,6 +18,7 @@ import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText +import com.unciv.utils.debug import org.junit.Assert import org.junit.Before import org.junit.Test @@ -67,7 +68,7 @@ class BasicTests { var allObsoletingUnitsHaveUpgrades = true for (unit in units) { if (unit.obsoleteTech != null && unit.upgradesTo == null && unit.name !="Scout" ) { - println(unit.name + " obsoletes but has no upgrade") + debug(unit.name + " obsoletes but has no upgrade") allObsoletingUnitsHaveUpgrades = false } } @@ -93,7 +94,7 @@ class BasicTests { val ruleset = RulesetCache[baseRuleset.fullName]!! val modCheck = ruleset.checkModLinks() if (modCheck.isNotOK()) - println(modCheck.getErrorText(true)) + debug(modCheck.getErrorText(true)) Assert.assertFalse(modCheck.isNotOK()) } } @@ -106,7 +107,7 @@ class BasicTests { for (paramType in entry.value) { if (paramType == UniqueParameterType.Unknown) { val badParam = uniqueType.text.getPlaceholderParameters()[entry.index] - println("${uniqueType.name} param[${entry.index}] type \"$badParam\" is unknown") + debug("${uniqueType.name} param[${entry.index}] type \"$badParam\" is unknown") noUnknownParameters = false } } @@ -120,7 +121,7 @@ class BasicTests { var allOK = true for (uniqueType in UniqueType.values()) { if (uniqueType.targetTypes.isEmpty()) { - println("${uniqueType.name} has no targets.") + debug("${uniqueType.name} has no targets.") allOK = false } } @@ -134,7 +135,7 @@ class BasicTests { for (unit in units) { for (unique in unit.uniques) { if (!UniqueType.values().any { it.placeholderText == unique.getPlaceholderText() }) { - println("${unit.name}: $unique") + debug("${unit.name}: $unique") allOK = false } } @@ -149,7 +150,7 @@ class BasicTests { for (building in buildings) { for (unique in building.uniques) { if (!UniqueType.values().any { it.placeholderText == unique.getPlaceholderText() }) { - println("${building.name}: $unique") + debug("${building.name}: $unique") allOK = false } } @@ -164,7 +165,7 @@ class BasicTests { for (promotion in promotions) { for (unique in promotion.uniques) { if (!UniqueType.values().any { it.placeholderText == unique.getPlaceholderText() }) { - println("${promotion.name}: $unique") + debug("${promotion.name}: $unique") allOK = false } } @@ -182,7 +183,7 @@ class BasicTests { for (obj in objects) { for (unique in obj.uniques) { if (!UniqueType.values().any { it.placeholderText == unique.getPlaceholderText() }) { - println("${obj.name}: $unique") + debug("${obj.name}: $unique") allOK = false } } @@ -195,24 +196,24 @@ class BasicTests { var allOK = true for (uniqueType in UniqueType.values()) { val deprecationAnnotation = uniqueType.getDeprecationAnnotation() ?: continue - + val uniquesToCheck = deprecationAnnotation.replaceWith.expression.split("\", \"", Constants.uniqueOrDelimiter) - + for (uniqueText in uniquesToCheck) { val replacementTextUnique = Unique(uniqueText) if (replacementTextUnique.type == null) { - println("${uniqueType.name}'s deprecation text \"$uniqueText\" does not match any existing type!") + debug("${uniqueType.name}'s deprecation text \"$uniqueText\" does not match any existing type!") allOK = false } if (replacementTextUnique.type == uniqueType) { - println("${uniqueType.name}'s deprecation text references itself!") + debug("${uniqueType.name}'s deprecation text references itself!") allOK = false } for (conditional in replacementTextUnique.conditionals) { if (conditional.type == null) { - println("${uniqueType.name}'s deprecation text contains conditional \"${conditional.text}\" which does not match any existing type!") + debug("${uniqueType.name}'s deprecation text contains conditional \"${conditional.text}\" which does not match any existing type!") allOK = false } } @@ -222,7 +223,7 @@ class BasicTests { while (replacementUnique.getDeprecationAnnotation() != null) { if (iteration == 10) { allOK = false - println("${uniqueType.name}'s deprecation text never references an undeprecated unique!") + debug("${uniqueType.name}'s deprecation text never references an undeprecated unique!") break } iteration++ @@ -240,7 +241,7 @@ class BasicTests { Thread.sleep(5000) // makes timings a little more repeatable val startTime = System.nanoTime() statMathRunner(iterations = 1_000_000) - println("statMathStressTest took ${(System.nanoTime()-startTime)/1000}µs") + debug("statMathStressTest took ${(System.nanoTime()-startTime)/1000}µs") } @Test diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt index 2ce41d9fed..3d7ae83cf6 100644 --- a/tests/src/com/unciv/testing/SerializationTests.kt +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -16,6 +16,7 @@ import com.unciv.models.metadata.Player import com.unciv.models.ruleset.RulesetCache import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.utils.debug import org.junit.After import org.junit.Assert import org.junit.Before @@ -111,7 +112,7 @@ class SerializationTests { val pattern = """\{(\w+)\${'$'}delegate:\{class:kotlin.SynchronizedLazyImpl,""" val matches = Regex(pattern).findAll(json) matches.forEach { - println("Lazy missing `@delegate:Transient` annotation: " + it.groups[1]!!.value) + debug("Lazy missing `@delegate:Transient` annotation: " + it.groups[1]!!.value) } val result = matches.any() Assert.assertFalse("This test will only pass when no serializable lazy fields are found", result) diff --git a/tests/src/com/unciv/testing/TranslationTests.kt b/tests/src/com/unciv/testing/TranslationTests.kt index 35e1e57eac..d6386aeaaf 100644 --- a/tests/src/com/unciv/testing/TranslationTests.kt +++ b/tests/src/com/unciv/testing/TranslationTests.kt @@ -7,6 +7,7 @@ import com.unciv.models.metadata.GameSettings import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.* +import com.unciv.utils.debug import org.junit.Assert import org.junit.Before import org.junit.Test @@ -39,13 +40,13 @@ class TranslationTests { translations.size > 0) } - + // This test is incorrectly defined: it should read from the template.properties file and not fro the final translation files. // @Test // fun allUnitActionsHaveTranslation() { // val actions: MutableSet = HashSet() // for (action in UnitActionType.values()) { -// actions.add( +// actions.add( // when(action) { // UnitActionType.Upgrade -> "Upgrade to [unitType] ([goldCost] gold)" // UnitActionType.Create -> "Create [improvement]" @@ -95,7 +96,7 @@ class TranslationTests { for (placeholder in placeholders) { if (!output.contains(placeholder)) { allTranslationsHaveCorrectPlaceholders = false - println("Placeholder `$placeholder` not found in `$language` for entry `$translationEntry`") + debug("Placeholder `$placeholder` not found in `$language` for entry `$translationEntry`") } } } @@ -115,7 +116,7 @@ class TranslationTests { val keyFromEntry = translationEntry.replace(squareBraceRegex, "[]") if (key != keyFromEntry) { allPlaceholderKeysMatchEntry = false - println("Entry $translationEntry found under bad key $key") + debug("Entry $translationEntry found under bad key $key") break } } @@ -135,7 +136,7 @@ class TranslationTests { for (placeholder in placeholders) if (placeholders.count { it == placeholder } > 1) { noTwoPlaceholdersAreTheSame = false - println("Entry $translationEntry has the parameter $placeholder more than once") + debug("Entry $translationEntry has the parameter $placeholder more than once") break } } @@ -151,7 +152,7 @@ class TranslationTests { var failed = false for (line in templateLines) { if (line.endsWith(" =")) { - println("$line ends without a space at the end") + debug("$line ends without a space at the end") failed = true } } @@ -176,7 +177,7 @@ class TranslationTests { translationEntry.entry.tr() } catch (ex: Exception) { allWordsTranslatedCorrectly = false - println("Crashed when translating ${translationEntry.entry} to $language") + debug("Crashed when translating ${translationEntry.entry} to $language") } } } @@ -185,11 +186,11 @@ class TranslationTests { allWordsTranslatedCorrectly ) } - + @Test fun wordBoundaryTranslationIsFormattedCorrectly() { val translationEntry = translations["\" \""]!! - + var allTranslationsCheckedOut = true for ((language, translation) in translationEntry) { if (!translation.startsWith("\"") @@ -197,10 +198,10 @@ class TranslationTests { || translation.count { it == '\"' } != 2 ) { allTranslationsCheckedOut = false - println("Translation of the word boundary in $language was incorrectly formatted") + debug("Translation of the word boundary in $language was incorrectly formatted") } } - + Assert.assertTrue( "This test will only pass when the word boundrary translation succeeds", allTranslationsCheckedOut @@ -210,18 +211,18 @@ class TranslationTests { @Test fun translationParameterExtractionForNestedBracesWorks() { - Assert.assertEquals(listOf("New [York]"), + Assert.assertEquals(listOf("New [York]"), "The city of [New [York]]".getPlaceholderParametersIgnoringLowerLevelBraces()) // Closing braces without a matching opening brace - 'level 0' - are ignored Assert.assertEquals(listOf("New [York]"), "The city of [New [York]]]".getPlaceholderParametersIgnoringLowerLevelBraces()) - + // Opening braces without a matching closing brace mean that the term is never 'closed' // so there are no parameters Assert.assertEquals(listOf(), "The city of [[New [York]".getPlaceholderParametersIgnoringLowerLevelBraces()) - + // Supernesting val superNestedString = "The brother of [[my [best friend]] and [[America]'s greatest [Dad]]]" Assert.assertEquals(listOf("[my [best friend]] and [[America]'s greatest [Dad]]"), @@ -239,8 +240,8 @@ class TranslationTests { } addTranslation("The brother of [person]", "The sibling of [person]") Assert.assertEquals("The sibling of bob", "The brother of [bob]".tr()) - - + + addTranslation("[a] and [b]", "[a] and indeed [b]") addTranslation("my [whatever]", "mine own [whatever]") addTranslation("[place]'s greatest [job]", "the greatest [job] in [place]") @@ -248,11 +249,11 @@ class TranslationTests { addTranslation("best friend", "closest ally") addTranslation("America", "The old British colonies") - println("[Dad] and [my [best friend]]".getPlaceholderText()) + debug("[Dad] and [my [best friend]]".getPlaceholderText()) Assert.assertEquals(listOf("Dad","my [best friend]"), "[Dad] and [my [best friend]]".getPlaceholderParametersIgnoringLowerLevelBraces()) Assert.assertEquals("Father and indeed mine own closest ally", "[Dad] and [my [best friend]]".tr()) - + // Reminder: "The brother of [[my [best friend]] and [[America]'s greatest [Dad]]]" Assert.assertEquals("The sibling of mine own closest ally and indeed the greatest Father in The old British colonies", superNestedString.tr()) @@ -264,7 +265,7 @@ class TranslationTests { // val orderedConditionals = Translations.englishConditionalOrderingString // val orderedConditionalsSet = orderedConditionals.getConditionals().map { it.placeholderText } // val translationEntry = translations[orderedConditionals]!! -// +// // var allTranslationsCheckedOut = true // for ((language, translation) in translationEntry) { // val translationConditionals = translation.getConditionals().map { it.placeholderText } @@ -275,7 +276,7 @@ class TranslationTests { // println("Not all or double parameters found in the conditional ordering for $language") // } // } -// +// // Assert.assertTrue( // "This test will only pass when each of the conditionals exists exactly once in the translations for the conditional ordering", // allTranslationsCheckedOut