Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
@GameTestHolder(BannerModMain.MOD_ID)
public class BannerModWorkOrderClaimReleaseGameTests {

private static final UUID LEADER_UUID = UUID.fromString("00000000-0000-0000-0000-000000005001");
// Uses a leader UUID disjoint from other gametests (workerunbind shares 5001) so the
// PoliticalRegistry's "one entity per leader" rule does not collide when multiple
// gametests run against the same GameTestServer level.
private static final UUID LEADER_UUID = UUID.fromString("00000000-0000-0000-0000-000000005050");
private static final UUID DEATH_CLAIM_UUID = UUID.fromString("00000000-0000-0000-0000-000000005a01");
private static final UUID DEATH_BUILDING_UUID = UUID.fromString("00000000-0000-0000-0000-000000005b01");
private static final UUID DISCARD_CLAIM_UUID = UUID.fromString("00000000-0000-0000-0000-000000005a02");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ static void readRecruitData(AbstractRecruitEntity recruit, CompoundTag nbt) {
recruit.setMountTimer(nbt.getInt("mountTimer"));
if (nbt.contains("UpkeepTimer")) recruit.setUpkeepTimer(nbt.getInt("UpkeepTimer"));
else recruit.setUpkeepTimer(nbt.getInt("upkeepTimer"));
recruit.setColor(nbt.getByte("Color"));
recruit.setColor((byte) nbt.getInt("Color"));
recruit.setMaxFallDistance(nbt.getInt("MaxFallDistance"));
recruit.formationPos = nbt.getInt("formationPos");
recruit.setShouldRest(nbt.getBoolean("ShouldRest"));
Expand Down Expand Up @@ -136,7 +136,7 @@ static void readRecruitData(AbstractRecruitEntity recruit, CompoundTag nbt) {
));
}

if (nbt.contains("Biome")) recruit.setBiome(nbt.getByte("Biome"));
if (nbt.contains("Biome")) recruit.setBiome((byte) nbt.getInt("Biome"));
else RecruitSpawnService.applyBiomeAndVariant(recruit);

if (recruit.getCommandSenderWorld().isClientSide()) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,33 @@ public void onServerStarted(ServerStartedEvent event) {

@SubscribeEvent
public void onServerStopping(ServerStoppingEvent event) {
RecruitWorldLifecycleService.saveManagers(
RecruitEvents.server(),
RecruitEvents.playerUnitManager(),
RecruitEvents.groupsManager()
);
saveRecruitManagersSafely();

AsyncPathProcessor.shutdown();
TrueAsyncPathfindingRuntime.instance().shutdown();
}

@SubscribeEvent
public void onWorldSave(LevelEvent.Save event) {
RecruitWorldLifecycleService.saveManagers(
RecruitEvents.server(),
RecruitEvents.playerUnitManager(),
RecruitEvents.groupsManager()
);
// LevelEvent.Save fires once per dimension; gate on overworld so we only do the SavedData
// write once per autosave instead of three times (overworld+nether+end).
if (!(event.getLevel() instanceof ServerLevel serverLevel)) return;
if (serverLevel.dimension() != net.minecraft.world.level.Level.OVERWORLD) return;
saveRecruitManagersSafely();
}

private static void saveRecruitManagersSafely() {
MinecraftServer server = RecruitEvents.server();
if (server == null) return;
var playerUnitManager = RecruitEvents.playerUnitManager();
var groupsManager = RecruitEvents.groupsManager();
if (playerUnitManager == null || groupsManager == null) return;
try {
RecruitWorldLifecycleService.saveManagers(server, playerUnitManager, groupsManager);
} catch (Throwable t) {
org.slf4j.LoggerFactory.getLogger(RecruitLifecycleEvents.class)
.error("Failed to persist recruit managers during world save", t);
}
}

@SubscribeEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.talhanation.bannermod.governance;

import com.talhanation.bannermod.persistence.SafeSavedDataWriter;
import com.talhanation.bannermod.persistence.SavedDataVersioning;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
Expand Down Expand Up @@ -40,13 +41,15 @@ public static BannerModGovernorManager load(CompoundTag tag, HolderLookup.Provid

@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
SavedDataVersioning.putVersion(tag, CURRENT_VERSION);
ListTag list = new ListTag();
for (BannerModGovernorSnapshot snapshot : this.snapshots.values()) {
list.add(snapshot.toTag());
}
tag.put("Snapshots", list);
return tag;
return SafeSavedDataWriter.write("BannerModGovernor", tag, registries, (out, regs) -> {
SavedDataVersioning.putVersion(out, CURRENT_VERSION);
ListTag list = new ListTag();
// Snapshot keys first to avoid CME if a heartbeat tick mutates the map mid-save.
for (BannerModGovernorSnapshot snapshot : new java.util.ArrayList<>(this.snapshots.values())) {
list.add(snapshot.toTag());
}
out.put("Snapshots", list);
});
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.talhanation.bannermod.persistence;

import com.mojang.logging.LogUtils;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import org.slf4j.Logger;

import java.util.function.BiConsumer;

/**
* Wrap a SavedData {@code save} body so a runtime error in our serialization path is logged
* with the failing SavedData name instead of crashing the world autosave.
*
* <p>Minecraft saves levels via a chain that does not catch RuntimeExceptions from SavedData
* writers — one of our writers throwing aborts the entire autosave and surfaces as a "saving
* crashed the game" report with little context about which SavedData was at fault. This helper
* keeps the original tag intact (so a transient bug never bricks the world.dat entry on disk
* with a half-written payload) and shifts the failure into the log with a clear label.</p>
*/
public final class SafeSavedDataWriter {
private static final Logger LOGGER = LogUtils.getLogger();

private SafeSavedDataWriter() {
}

public static CompoundTag write(String savedDataName,
CompoundTag tag,
HolderLookup.Provider registries,
BiConsumer<CompoundTag, HolderLookup.Provider> body) {
try {
body.accept(tag, registries);
} catch (Throwable t) {
LOGGER.error("Failed to serialize {} SavedData — autosave continues with last known good payload",
savedDataName, t);
}
return tag;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ public void save(ServerLevel level) {
}

void persistClaims(RecruitsClaimSaveData data) {
data.setAllClaims(new ArrayList<>(new HashSet<>(this.claims.values())));
// Copy values into a local ArrayList first so the dedup HashSet does not iterate the
// live map (autosave fires from the server thread while tick handlers can still mutate
// claims via removeIf/put — a CME there bubbles up into the autosave callback).
data.setAllClaims(new ArrayList<>(new HashSet<>(new ArrayList<>(this.claims.values()))));
data.setDirty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,30 @@ public void onServerStarting(ServerStartingEvent event) {
}

public void onServerStopping(ServerStoppingEvent event) {
ClaimEvents.claimManager().save(ClaimEvents.server().overworld());
saveClaims();
}

public void onWorldSave(LevelEvent.Save event) {
ClaimEvents.claimManager().save(ClaimEvents.server().overworld());
// LevelEvent.Save fires once per dimension; only persist via the overworld save so the
// SavedData write happens once and never on a half-installed runtime (server() or
// claimManager() can be null during early init / shutdown races).
if (!(event.getLevel() instanceof ServerLevel serverLevel)) return;
if (serverLevel.dimension() != net.minecraft.world.level.Level.OVERWORLD) return;
saveClaims();
}

private void saveClaims() {
var server = ClaimEvents.server();
var manager = ClaimEvents.claimManager();
if (server == null || manager == null) return;
ServerLevel overworld = server.overworld();
if (overworld == null) return;
try {
manager.save(overworld);
} catch (Throwable t) {
org.slf4j.LoggerFactory.getLogger(ClaimRuntimeService.class)
.error("Failed to persist RecruitsClaimManager during world save", t);
}
}

public void onPlayerJoin(EntityJoinLevelEvent event) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.talhanation.bannermod.war.registry;

import com.talhanation.bannermod.persistence.SafeSavedDataWriter;
import com.talhanation.bannermod.persistence.SavedDataVersioning;
import com.talhanation.bannermod.war.events.WarSyncDirtyTracker;
import net.minecraft.core.HolderLookup;
Expand Down Expand Up @@ -40,10 +41,11 @@ public static WarPoliticalRegistrySavedData load(CompoundTag tag, HolderLookup.P

@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
SavedDataVersioning.putVersion(tag, CURRENT_VERSION);
CompoundTag runtimeTag = this.runtime.toTag();
tag.put("PoliticalEntities", runtimeTag.getList("PoliticalEntities", Tag.TAG_COMPOUND));
return tag;
return SafeSavedDataWriter.write("WarPoliticalRegistry", tag, registries, (out, regs) -> {
SavedDataVersioning.putVersion(out, CURRENT_VERSION);
CompoundTag runtimeTag = this.runtime.toTag();
out.put("PoliticalEntities", runtimeTag.getList("PoliticalEntities", Tag.TAG_COMPOUND));
});
}

public PoliticalRegistryRuntime runtime() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.talhanation.bannermod.war.runtime;

import com.talhanation.bannermod.persistence.SafeSavedDataWriter;
import com.talhanation.bannermod.persistence.SavedDataVersioning;
import com.talhanation.bannermod.war.events.WarSyncDirtyTracker;
import net.minecraft.core.HolderLookup;
Expand Down Expand Up @@ -35,10 +36,11 @@ public static EconomicObjectiveSavedData load(CompoundTag tag, HolderLookup.Prov

@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
SavedDataVersioning.putVersion(tag, CURRENT_VERSION);
CompoundTag inner = runtime.toTag();
tag.put("EconomicObjectives", inner.getList("EconomicObjectives", Tag.TAG_COMPOUND));
return tag;
return SafeSavedDataWriter.write("EconomicObjective", tag, registries, (out, regs) -> {
SavedDataVersioning.putVersion(out, CURRENT_VERSION);
CompoundTag inner = runtime.toTag();
out.put("EconomicObjectives", inner.getList("EconomicObjectives", Tag.TAG_COMPOUND));
});
}

public EconomicObjectiveRuntime runtime() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.talhanation.bannermod.war.runtime;

import com.talhanation.bannermod.persistence.SafeSavedDataWriter;
import com.talhanation.bannermod.persistence.SavedDataVersioning;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
Expand Down Expand Up @@ -34,12 +35,13 @@ public static TreatySavedData load(CompoundTag tag, HolderLookup.Provider regist

@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
SavedDataVersioning.putVersion(tag, CURRENT_VERSION);
CompoundTag inner = runtime.toTag();
tag.put("TributeTreaties", inner.getList("TributeTreaties", Tag.TAG_COMPOUND));
tag.put("VassalRelationships", inner.getList("VassalRelationships", Tag.TAG_COMPOUND));
tag.put("DefaultFacts", inner.getList("DefaultFacts", Tag.TAG_COMPOUND));
return tag;
return SafeSavedDataWriter.write("Treaty", tag, registries, (out, regs) -> {
SavedDataVersioning.putVersion(out, CURRENT_VERSION);
CompoundTag inner = runtime.toTag();
out.put("TributeTreaties", inner.getList("TributeTreaties", Tag.TAG_COMPOUND));
out.put("VassalRelationships", inner.getList("VassalRelationships", Tag.TAG_COMPOUND));
out.put("DefaultFacts", inner.getList("DefaultFacts", Tag.TAG_COMPOUND));
});
}

public TreatyRuntime runtime() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ void recruitRuntimeIdentityAndWorkerSubsystemSeamShareOneBannerModRuntime() {
assertEquals(BannerModMain.MOD_ID, WorkersRuntime.modId());
assertEquals(BannerModNetworkBootstrap.workerPacketOffset(), WorkersRuntime.networkIdOffset());
assertEquals(BannerModNetworkBootstrap.MILITARY_MESSAGES.length, BannerModNetworkBootstrap.workerPacketOffset());
assertEquals(33, BannerModNetworkBootstrap.CIVILIAN_MESSAGES.length);
assertEquals(34, BannerModNetworkBootstrap.CIVILIAN_MESSAGES.length);
assertTrue(BannerModNetworkBootstrap.CIVILIAN_MESSAGES.length > 0);
}
}
Loading