diff --git a/build.gradle.kts b/build.gradle.kts index 7a666617ce..185c0ba5a7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,9 @@ allprojects { apply(plugin = "io.github.yairm210.purity-plugin") configure{ wellKnownPureFunctions = setOf( + "java.util.regex.Pattern.matcher", + "java.util.regex.Matcher.find", + "java.util.regex.Matcher.replaceAll", ) wellKnownReadonlyFunctions = setOf( "com.badlogic.gdx.math.Vector2.len", @@ -61,6 +64,9 @@ allprojects { "com.badlogic.gdx.files.FileHandle.isDirectory", "com.badlogic.gdx.files.FileHandle.isFile", "com.badlogic.gdx.files.FileHandle.name", + + "kotlin.Throwable.getStackTrace", + "java.lang.StackTraceElement.getClassName", ) wellKnownPureClasses = setOf( ) diff --git a/core/src/com/unciv/logic/map/AStar.kt b/core/src/com/unciv/logic/map/AStar.kt index 1e3c0cd83f..7ca62078ba 100644 --- a/core/src/com/unciv/logic/map/AStar.kt +++ b/core/src/com/unciv/logic/map/AStar.kt @@ -1,6 +1,7 @@ package com.unciv.logic.map import com.unciv.logic.map.tile.Tile +import yairm210.purity.annotations.InternalState import java.util.PriorityQueue @@ -37,6 +38,7 @@ data class TilePriority(val tile: Tile, val priority: Float) * val path = aStarSearch.findPath(goalTile) * ``` */ +@InternalState class AStar( val startingPoint: Tile, private val predicate : (Tile) -> Boolean, diff --git a/core/src/com/unciv/logic/map/MapPathing.kt b/core/src/com/unciv/logic/map/MapPathing.kt index 180bb33b08..47812dd6ae 100644 --- a/core/src/com/unciv/logic/map/MapPathing.kt +++ b/core/src/com/unciv/logic/map/MapPathing.kt @@ -15,6 +15,7 @@ object MapPathing { * Otherwise, we set every tile to have equal value since building a road on any of them makes the original movement cost irrelevant. */ @Suppress("UNUSED_PARAMETER") // While `from` is unused, this function should stay close to the signatures expected by the AStar and getPath `heuristic` parameter. + @Readonly private fun roadPreferredMovementCost(unit: MapUnit, from: Tile, to: Tile): Float{ // hasRoadConnection accounts for civs that treat jungle/forest as roads // Ignore road over river penalties. @@ -24,6 +25,7 @@ object MapPathing { return 1f } + @Readonly fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean { val roadImprovement = tile.ruleset.roadImprovement val railRoadImprovement = tile.ruleset.railroadImprovement @@ -49,6 +51,7 @@ object MapPathing { * @param endTile The destination tile of the path. * @return A sequence of tiles representing the path from startTile to endTile, or null if no valid path is found. */ + @Readonly fun getRoadPath(unit: MapUnit, startTile: Tile, endTile: Tile): List?{ return getPath(unit, startTile, @@ -73,6 +76,7 @@ object MapPathing { * It takes a MapUnit, a 'from' Tile, and a 'to' Tile, returning a Float value representing the heuristic cost estimate. * @return A list of tiles representing the path from the startTile to the endTile. Returns null if no valid path is found. */ + @Readonly private fun getPath(unit: MapUnit, startTile: Tile, endTile: Tile, @@ -104,7 +108,7 @@ object MapPathing { * Gets the connection to the end tile. This does not take into account tile movement costs. * Takes in a civilization instead of a specific unit. */ - @Readonly @Suppress("purity") // todo continue + @Readonly fun getConnection(civ: Civilization, startTile: Tile, endTile: Tile, diff --git a/core/src/com/unciv/utils/Log.kt b/core/src/com/unciv/utils/Log.kt index 6bb0e67e9c..460de2c835 100644 --- a/core/src/com/unciv/utils/Log.kt +++ b/core/src/com/unciv/utils/Log.kt @@ -1,5 +1,8 @@ package com.unciv.utils +import yairm210.purity.annotations.Immutable +import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly import java.time.Instant import java.util.regex.Pattern @@ -21,10 +24,10 @@ object Log { * Log tags (= class names) **containing** these Strings will not be logged. * You _can_ disable the default exclusions with an empty `-DnoLog=` argument. */ - val disableLogsFrom = ( + @Immutable val disableLogsFrom = ( System.getProperty("noLog") ?: "Battle,Music,Sounds,Translations,WorkerAutomation,assignRegions,RoadBetweenCitiesAutomation" - ).split(',').filterNot { it.isEmpty() }.toMutableSet() + ).split(',').filterNot { it.isEmpty() }.toSet() /** * Add -DonlyLog= to only log specific classes. @@ -32,13 +35,15 @@ object Log { * [disableLogsFrom] will still be respected if this is set. * Note you cannot disable all logging with `-DonlyLog=`, use `-DonlyLog=~~~` instead. */ + @Immutable val enableLogsFrom = ( System.getProperty("onlyLog") ?: "" - ).split(',').filterNot { it.isEmpty() }.toMutableSet() + ).split(',').filterNot { it.isEmpty() }.toSet() var backend: LogBackend = DefaultLogBackend() + @Readonly fun shouldLog(tag: Tag = getTag()): Boolean { return !backend.isRelease() && !isTagDisabled(tag) } @@ -54,6 +59,7 @@ object Log { * * The [params] can contain value-producing lambdas, which will be called and their value used as parameter for the message instead. */ + @Pure @Suppress("purity") // log considered pure everywhere fun debug(msg: String, vararg params: Any?) { if (backend.isRelease()) return debug(getTag(), msg, *params) @@ -159,7 +165,7 @@ interface LogBackend { fun error(tag: Tag, curThreadName: String, msg: String) /** Do not log on release builds for performance reasons. */ - fun isRelease(): Boolean + @Readonly fun isRelease(): Boolean /** Get string information about operation system */ fun getSystemInfo(): String @@ -209,6 +215,7 @@ private fun doLog(logger: (Tag, String, String) -> Unit, tag: Tag, msg: String, logger(tag, Thread.currentThread().name, formattedMessage) } +@Pure private fun isTagDisabled(tag: Tag): Boolean { return Log.disableLogsFrom.any { it in tag.name } || (Log.enableLogsFrom.isNotEmpty() && Log.enableLogsFrom.none { it in tag.name }) @@ -231,14 +238,16 @@ private fun replaceLambdasWithValues(params: Array): Array { } +@Readonly private fun getTag(): Tag { @Suppress("ThrowingExceptionsWithoutMessageOrCause") - val firstOutsideStacktrace = Throwable().stackTrace.filter { "com.unciv.utils.Log" !in it.className }.first() + val firstOutsideStacktrace = Throwable().stackTrace.first { "com.unciv.utils.Log" !in it.className } 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 +@Pure private fun removeAnonymousSuffix(tag: String): String { val matcher = ANONYMOUS_CLASS_PATTERN.matcher(tag) return if (matcher.find()) {