Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/org/magiclib/Magic_modPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.thoughtworks.xstream.XStream;
import org.magiclib.util.memberMemory.MemberMemoryManager;
import org.lwjgl.util.vector.Vector2f;
import org.magiclib.achievements.MagicAchievementManager;
import org.magiclib.achievements.TestingAchievementSpec;
Expand Down Expand Up @@ -169,6 +170,7 @@ public void beforeGameSave() {

MagicAchievementManager.getInstance().beforeGameSave();
MagicPaintjobManager.beforeGameSave();
MemberMemoryManager.beforeGameSave();
}

@Override
Expand Down
20 changes: 20 additions & 0 deletions src/org/magiclib/util/memberMemory/MemberMemoryAccess.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.magiclib.util.memberMemory

import org.magiclib.util.memberMemory.MemberMemoryManager.getMemberMemoryStore

object MemberMemoryAccess {
const val SECTOR_MEMBER_MEMORY_KEY = "\$ML_MemberMemoryStore"

/**
* Member must be present in either an active fleet or in storage before game save, otherwise the memory related to it will be removed on game save as it is considered no longer existing.
*/
@JvmStatic
fun getMemberMemory(memberID: String): MutableMap<String, Any?> {
return getMemberMemoryStore().getMemberMemory(memberID)
}

@JvmStatic
fun unsetMemberMemory(memberID: String) {
getMemberMemoryStore().unsetMemberMemory(memberID)
}
}
13 changes: 13 additions & 0 deletions src/org/magiclib/util/memberMemory/MemberMemoryExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.magiclib.util.memberMemory

import com.fs.starfarer.api.fleet.FleetMemberAPI

object MemberMemoryExt {
/**
* Delegate for [MemberMemoryAccess.getMemberMemory]
*
* It is suggested to read the documentation on what this delegates to before using.
*/
fun FleetMemberAPI.getMemberMemory() =
MemberMemoryAccess.getMemberMemory(id)
}
141 changes: 141 additions & 0 deletions src/org/magiclib/util/memberMemory/MemberMemoryManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.magiclib.util.memberMemory

import com.fs.starfarer.api.Global
import com.fs.starfarer.api.campaign.CargoAPI
import com.fs.starfarer.api.campaign.econ.MarketAPI
import com.fs.starfarer.api.campaign.econ.SubmarketAPI
import org.magiclib.util.memberMemory.MemberMemoryAccess.SECTOR_MEMBER_MEMORY_KEY

internal object MemberMemoryManager {
fun getMemberMemoryStore(): MemberMemoryStore = getOrInitStore()

private fun getOrInitStore(): MemberMemoryStore {
val memory = Global.getSector().memoryWithoutUpdate

return memory?.get(SECTOR_MEMBER_MEMORY_KEY) as? MemberMemoryStore
?: MemberMemoryStore().also { memory?.set(SECTOR_MEMBER_MEMORY_KEY, it) }
}

@JvmStatic
fun beforeGameSave() {
val store = getOrInitStore()

if (!store.getMemberIDs().isEmpty()) {
// This shouldn't ever crash, but if it did, beforeGameSave would be a terrible place for it to happen. So prevent it anyway.
val mostMemberIDs = try {
getMostMemberIDs()
} catch (e: Exception) {
Global.getLogger(this.javaClass).error("Failed to get member IDs", e)
return
}

store.unsetMembersNotInSet(mostMemberIDs)
}
}

private fun getMostMemberIDs(): Set<String> {
val locations = Global.getSector().allLocations

val markets = locations
.flatMap { it.allEntities }
.mapNotNull { it.market }
.distinctBy { it.id } // A planet and the planet's station may link to the same market. This prevents that

val storages = markets
.flatMap { it.submarketsCopy }
.mapNotNull { it.cargo }
.distinctBy { it } // Mods such as Trails of Tooth and Claw share cargo. This avoids that causing mis-reporting duplicated members.

val fleetMembers = listOf(
locations.flatMap { it.fleets }.map { it.fleetData }, // Ships in active fleets.
storages.mapNotNull { it.mothballedShips }, // Ships in storage.
).flatten().flatMap { it.membersListCopy }

val ids = fleetMembers.map { it.id }

// While there shouldn't be duplicates in normal circumstances, let's check just in case. If any duplicates exist, it's usually fault of a mod.
if (ids.groupingBy { it }.eachCount().any { it.value > 1 }) {
duplicateLocationReporter() // Report where duplicates member IDs are into the log.
}

return ids.toSet()
}

private fun duplicateLocationReporter() {
val locations = Global.getSector().allLocations

val markets = locations
.flatMap { it.allEntities }
.mapNotNull { it.market }
.distinctBy { it.id }

data class StorageRef(
val market: MarketAPI,
val submarket: SubmarketAPI,
val cargo: CargoAPI
)

val storages = markets.flatMap { market ->
market.submarketsCopy.mapNotNull { sub ->
val cargo = sub.cargo ?: return@mapNotNull null
StorageRef(market, sub, cargo)
}
}.distinctBy { it.cargo }

// id -> list of human-readable locations
val idMap = mutableMapOf<String, MutableList<String>>()

// === ACTIVE FLEETS ===
for (loc in locations) {
for (fleet in loc.fleets) {
val fleetName = fleet.name

for (member in fleet.fleetData.membersListCopy) {
val id = member.id

idMap.getOrPut(id) { mutableListOf() }
.add("Fleet: $fleetName | Ship: ${member.shipName}")
}
}
}

// === STORAGE (MOTHBALLED SHIPS) ===
for (storage in storages) {
val sub = storage.submarket
val market = storage.market
val cargo = storage.cargo

val mothballed = cargo.mothballedShips ?: continue

val where = "Storage: ${sub.specId} @ ${market.name}"

for (member in mothballed.membersListCopy) {
val id = member.id

idMap.getOrPut(id) { mutableListOf() }
.add("$where | Ship: ${member.shipName}")
}
}

// === REPORT DUPLICATES ===
val sb = StringBuilder()
var duplicates = 0

for ((id, list) in idMap) {
if (list.size > 1) {
duplicates++

sb.appendLine("=== DUPLICATE ID: $id ===")

for (entry in list) {
sb.appendLine(" $entry")
}
sb.appendLine()
}
}

sb.appendLine("Duplicate Member IDs found: $duplicates")

Global.getLogger(this.javaClass).warn(sb.toString())
}
}
43 changes: 43 additions & 0 deletions src/org/magiclib/util/memberMemory/MemberMemoryStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.magiclib.util.memberMemory

class MemberMemoryStore {
// Member ID first, then the key
val memKeys: MutableMap<String, MutableMap<String, Any?>> = mutableMapOf()

fun set(memberID: String, key: String, value: Any?) {
val memberMemory = getMemberMemory(memberID)
memberMemory[key] = value
}

fun get(memberID: String, key: String): Any? {
return getMemberMemory(memberID)[key]
}

fun containsKey(memberID: String, key: String): Boolean {
return memKeys[memberID]?.containsKey(key) == true
}

fun unsetKey(memberID: String, key: String) {
memKeys[memberID]?.remove(key)
}

fun getMemberIDs(): Set<String> {
return memKeys.keys
}

fun unsetMemberMemory(memberID: String) {
memKeys.remove(memberID)
}

fun getMemberMemory(memberID: String): MutableMap<String, Any?> {
return memKeys[memberID] ?: run {
val newMemberMemory = mutableMapOf<String, Any?>()
memKeys[memberID] = newMemberMemory
newMemberMemory
}
}

fun unsetMembersNotInSet(memberIDs: Set<String>) {
memKeys.keys.retainAll(memberIDs)
}
}