Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4f64fe2
move claim runtime services out of events
kaiserproger May 6, 2026
8562ef0
move citizen birth service out of events
kaiserproger May 6, 2026
5cb6ac2
add building type validator dispatcher
kaiserproger May 6, 2026
617bd30
Merge feature/eventspkg-002: move claim runtime services
kaiserproger May 6, 2026
0cf27ee
Merge feature/eventspkg-003: move citizen birth service
kaiserproger May 6, 2026
ef7b246
Merge feature/bldgvalidator-002: add building validator dispatcher
kaiserproger May 6, 2026
a235068
backlog: close eventspkg and validator tasks
kaiserproger May 6, 2026
d1657b7
move movement formation service out of events
kaiserproger May 6, 2026
afd2701
fix back-to-mount packet owner authority
kaiserproger May 6, 2026
165245e
add movement packet thread-safety gametest
kaiserproger May 6, 2026
264ec18
Merge feature/eventspkg-004: move movement formation service
kaiserproger May 6, 2026
f119b8e
Merge feature/packetauth-001: fix back-to-mount authority
kaiserproger May 6, 2026
742b5b1
Merge feature/testnetthread-001: add movement thread-safety test
kaiserproger May 6, 2026
4c57ff3
backlog: close eventspkg packetauth testnetthread
kaiserproger May 6, 2026
a776c39
fix clear upkeep packet owner authority
kaiserproger May 6, 2026
cadfb25
fix mount packet owner authority
kaiserproger May 6, 2026
e93abc3
backlog: split stale settlement treasury refactor
kaiserproger May 6, 2026
a1f2af9
Merge feature/packetauth-002: fix clear upkeep authority
kaiserproger May 6, 2026
0c6243a
Merge feature/packetauth-003: fix mount authority
kaiserproger May 6, 2026
bc8235e
backlog: close mount and upkeep auth tasks
kaiserproger May 6, 2026
b7e60e7
fix protect packet owner authority
kaiserproger May 6, 2026
5027223
fix group update ownership authority
kaiserproger May 6, 2026
11f3ba8
gate debug GUI recruit mutations
kaiserproger May 6, 2026
f3e7649
Merge feature/packetauth-004: fix protect authority
kaiserproger May 6, 2026
c9426d7
Merge feature/packetauth-005: fix group update authority
kaiserproger May 6, 2026
992f24e
Merge feature/packetauth-006: gate debug GUI authority
kaiserproger May 6, 2026
01494fd
backlog: close protect group debug auth tasks
kaiserproger May 6, 2026
abf7138
gate mount GUI recruit commands
kaiserproger May 7, 2026
7a20cb3
gate patrol waypoint packet authority
kaiserproger May 7, 2026
a03bf3e
backlog: split assassin count authority task
kaiserproger May 7, 2026
d68935b
Merge feature/packetauth-007: gate mount GUI authority
kaiserproger May 7, 2026
a92fe59
Merge feature/packetauth-009: gate patrol waypoint authority
kaiserproger May 7, 2026
8dced34
backlog: close mount GUI and patrol auth tasks
kaiserproger May 7, 2026
daa67be
gate patrol waypoint removal authority
kaiserproger May 7, 2026
256696b
gate patrol cycle packet authority
kaiserproger May 7, 2026
cc4d8be
Merge feature/packetauth-010: gate patrol waypoint removal
kaiserproger May 7, 2026
7058719
Merge feature/packetauth-011: gate patrol cycle authority
kaiserproger May 7, 2026
b1539d8
backlog: close patrol remove and cycle auth tasks
kaiserproger May 7, 2026
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
306 changes: 238 additions & 68 deletions docs/BANNERMOD_BACKLOG.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import com.talhanation.bannermod.BannerModDedicatedServerGameTestSupport;
import com.talhanation.bannermod.bootstrap.BannerModMain;
import com.talhanation.bannermod.network.compat.BannerModNetworkContext;
import com.talhanation.bannermod.network.messages.military.MessageMovement;
import com.talhanation.bannermod.network.messages.military.MessageUpkeepPos;
import com.talhanation.bannermod.network.throttle.PacketRateLimitConfig;
import com.talhanation.bannermod.network.throttle.PacketRateLimiter;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
Expand All @@ -22,10 +25,10 @@
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
import net.neoforged.neoforge.network.handling.IPayloadContext;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
Expand Down Expand Up @@ -67,6 +70,8 @@ public class NetworkThreadEnqueueGameTests {
private static final int DISPATCHES_PER_WORKER = 250;
private static final int TOTAL_DISPATCHES = WORKER_COUNT * DISPATCHES_PER_WORKER;
private static final UUID SENDER_UUID = UUID.fromString("00000000-0000-0000-0000-0000feed0001");
private static final UUID MOVEMENT_SENDER_UUID = UUID.fromString("00000000-0000-0000-0000-0000feed0002");
private static final UUID MOVEMENT_GROUP_UUID = UUID.fromString("00000000-0000-0000-0000-0000feed1002");

@PrefixGameTestTemplate(false)
@GameTest(template = "harness_empty", timeoutTicks = 1200)
Expand Down Expand Up @@ -148,6 +153,95 @@ public static void thousandConcurrentDispatchesAllRunOnMainThreadWithNoException
});
}

@PrefixGameTestTemplate(false)
@GameTest(template = "harness_empty", timeoutTicks = 1200)
public static void thousandConcurrentMovementPacketsDoNotTouchEntityCollectionsOffThread(GameTestHelper helper) {
ServerLevel level = helper.getLevel();
MinecraftServer server = level.getServer();
helper.assertTrue(server != null, "Gametest must run inside a real MinecraftServer context");

ServerPlayer sender = (ServerPlayer) BannerModDedicatedServerGameTestSupport
.createFakeServerPlayer(level, MOVEMENT_SENDER_UUID, "movement-enqueue-test-sender");

AtomicInteger completed = new AtomicInteger();
List<Throwable> errors = Collections.synchronizedList(new ArrayList<>());
List<String> runnerThreadNames = Collections.synchronizedList(new ArrayList<>());

IPayloadContext deferringContext = new MainThreadDeferringContext(
sender,
server,
completed,
errors,
runnerThreadNames
);
BannerModNetworkContext bannerCtx = new BannerModNetworkContext(deferringContext);

PacketRateLimiter limiter = PacketRateLimiter.shared();
limiter.setCooldownSource(packetClass -> packetClass == MessageMovement.class ? 0L : -1L);
limiter.clearState();

try {
Thread[] workers = new Thread[WORKER_COUNT];
for (int t = 0; t < WORKER_COUNT; t++) {
workers[t] = new Thread(() -> {
for (int i = 0; i < DISPATCHES_PER_WORKER; i++) {
try {
MessageMovement msg = new MessageMovement(
MOVEMENT_SENDER_UUID,
6,
MOVEMENT_GROUP_UUID,
0,
false
);
msg.executeServerSide(bannerCtx);
} catch (Throwable t1) {
errors.add(t1);
}
}
}, "movement-enqueue-test-worker-" + t);
workers[t].setDaemon(true);
workers[t].start();
}
for (Thread w : workers) {
try {
w.join(10_000L);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
helper.fail("Worker thread join interrupted");
}
helper.assertTrue(!w.isAlive(), "Worker thread did not finish dispatching within 10s");
}
} finally {
PacketRateLimitConfig.install();
limiter.clearState();
}

String mainThreadName = server.getRunningThread().getName();
helper.succeedWhen(() -> {
helper.assertTrue(
errors.isEmpty(),
"Expected zero movement packet exceptions; first was: "
+ (errors.isEmpty() ? "<none>" : errors.get(0).toString())
);
int done = completed.get();
helper.assertTrue(
done == TOTAL_DISPATCHES,
"Expected " + TOTAL_DISPATCHES + " movement packet bodies to complete; got " + done
);
List<String> snapshot;
synchronized (runnerThreadNames) {
snapshot = new ArrayList<>(runnerThreadNames);
}
boolean allOnMain = snapshot.stream().allMatch(name -> name.equals(mainThreadName));
helper.assertTrue(
allOnMain,
"Every movement body must run on the main server thread (" + mainThreadName
+ "); observed distinct threads: "
+ snapshot.stream().distinct().sorted().toList()
);
});
}

/**
* Test-only {@link IPayloadContext} whose {@code enqueueWork} defers to
* the server main-thread executor. Records each runnable's runner thread
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class GroupAssignmentAuthorityGameTests {
private static final UUID TRANSFER_GROUP_UUID = UUID.fromString("00000000-0000-0000-0000-000000000824");
private static final UUID SPOOFED_TRANSFER_GROUP_UUID = UUID.fromString("00000000-0000-0000-0000-000000000825");
private static final UUID TRUSTED_NEW_OWNER_UUID = UUID.fromString("00000000-0000-0000-0000-000000000826");
private static final UUID UPDATE_GROUP_UUID = UUID.fromString("00000000-0000-0000-0000-000000000827");
private static final UUID UPDATE_OWNER_UUID = UUID.fromString("00000000-0000-0000-0000-000000000828");
private static final UUID UPDATE_OUTSIDER_UUID = UUID.fromString("00000000-0000-0000-0000-000000000829");

@PrefixGameTestTemplate(false)
@GameTest(template = "harness_empty")
Expand Down Expand Up @@ -118,6 +121,31 @@ public static void groupTransferUpdatesGroupAndMembersFromTrustedPlayer(GameTest
helper.succeed();
}

@PrefixGameTestTemplate(false)
@GameTest(template = "harness_empty")
public static void groupUpdateRejectsNonOwnerAndKeepsExistingGroup(GameTestHelper helper) {
ServerLevel level = helper.getLevel();
ServerPlayer owner = createPlayer(helper, level, UPDATE_OWNER_UUID, "update-owner");
ServerPlayer outsider = createPlayer(helper, level, UPDATE_OUTSIDER_UUID, "update-outsider");

RecruitsGroup group = new RecruitsGroup("Owner Group", owner, 1);
group.setUUID(UPDATE_GROUP_UUID);
RecruitEvents.groupsManager().addOrUpdateGroup(level, owner, group);

RecruitsGroup spoofedUpdate = new RecruitsGroup("Spoofed Group", outsider, 9);
spoofedUpdate.setUUID(UPDATE_GROUP_UUID);
spoofedUpdate.removed = true;
RecruitEvents.groupsManager().addOrUpdateGroup(level, outsider, spoofedUpdate);

RecruitsGroup saved = RecruitEvents.groupsManager().getGroup(UPDATE_GROUP_UUID);
helper.assertTrue(saved != null, "Expected denied update to keep existing group");
helper.assertTrue(UPDATE_OWNER_UUID.equals(saved.getPlayerUUID()), "Expected denied update to keep owner UUID");
helper.assertTrue("update-owner".equals(saved.getPlayerName()), "Expected denied update to keep owner name");
helper.assertTrue("Owner Group".equals(saved.getName()), "Expected denied update to keep group name");
helper.assertFalse(saved.removed, "Expected denied update not to remove existing group");
helper.succeed();
}

private static ServerPlayer createPlayer(GameTestHelper helper, ServerLevel level, UUID playerId, String name) {
Player player = BannerModDedicatedServerGameTestSupport.createFakeServerPlayer(level, playerId, name);
BlockPos pos = helper.absolutePos(RecruitsBattleGameTestSupport.SquadAnchor.WEST.anchor());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.talhanation.bannermod.events;
package com.talhanation.bannermod.army.command.runtime;

import com.talhanation.bannermod.ai.military.controller.RecruitCommandStateTransitions;
import com.talhanation.bannermod.army.command.MovementCommandState;
Expand All @@ -7,6 +7,7 @@
import com.talhanation.bannermod.entity.military.AbstractRecruitEntity;
import com.talhanation.bannermod.entity.military.CaptainEntity;
import com.talhanation.bannermod.entity.military.RecruitIndex;
import com.talhanation.bannermod.events.RecruitEvents;
import com.talhanation.bannermod.persistence.military.RecruitsGroup;
import com.talhanation.bannermod.util.RuntimeProfilingCounters;
import com.talhanation.bannermod.util.FormationUtils;
Expand All @@ -29,23 +30,23 @@
import java.util.Objects;
import java.util.UUID;

final class MovementFormationCommandService {
public final class MovementFormationCommandService {

private static final String ACTIVE_GROUPS_KEY = "ActiveGroups";
private static final String FORMATION_KEY = "Formation";

private MovementFormationCommandService() {
}

static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruits, int movementState, int formation) {
public static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruits, int movementState, int formation) {
onMovementCommand(player, recruits, movementState, formation, false);
}

static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruits, int movementState, int formation, boolean tight) {
public static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruits, int movementState, int formation, boolean tight) {
onMovementCommand(player, recruits, movementState, formation, tight, null);
}

static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruits, int movementState, int formation, boolean tight, @Nullable Vec3 explicitTargetPos) {
public static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruits, int movementState, int formation, boolean tight, @Nullable Vec3 explicitTargetPos) {
if (formation != 0 && MovementCommandState.usesFormationTarget(movementState)) {
Vec3 targetPos = null;

Expand Down Expand Up @@ -137,11 +138,11 @@ static void onMovementCommand(Player player, List<AbstractRecruitEntity> recruit
}
}

static void applyFormation(int formation, List<AbstractRecruitEntity> recruits, Player player, Vec3 targetPos) {
public static void applyFormation(int formation, List<AbstractRecruitEntity> recruits, Player player, Vec3 targetPos) {
applyFormation(formation, recruits, player, targetPos, false);
}

static void applyFormation(int formation, List<AbstractRecruitEntity> recruits, Player player, Vec3 targetPos, boolean tight) {
public static void applyFormation(int formation, List<AbstractRecruitEntity> recruits, Player player, Vec3 targetPos, boolean tight) {
saveFormationCenter(player, targetPos);
double spacingMultiplier = tight ? 0.5 : 1.0;

Expand All @@ -158,7 +159,7 @@ static void applyFormation(int formation, List<AbstractRecruitEntity> recruits,
}
}

static void onFaceCommand(Player player, List<AbstractRecruitEntity> recruits, int formation, boolean tight) {
public static void onFaceCommand(Player player, List<AbstractRecruitEntity> recruits, int formation, boolean tight) {
if (recruits.isEmpty()) {
return;
}
Expand Down Expand Up @@ -186,7 +187,7 @@ static void onFaceCommand(Player player, List<AbstractRecruitEntity> recruits, i
}
}

static void onMovementCommandGUI(AbstractRecruitEntity recruit, int movementState) {
public static void onMovementCommandGUI(AbstractRecruitEntity recruit, int movementState) {
int state = recruit.getFollowState();

switch (movementState) {
Expand Down Expand Up @@ -230,7 +231,7 @@ static void onMovementCommandGUI(AbstractRecruitEntity recruit, int movementStat
recruit.forcedUpkeep = false;
}

static void checkPatrolLeaderState(AbstractRecruitEntity recruit) {
public static void checkPatrolLeaderState(AbstractRecruitEntity recruit) {
if (recruit instanceof AbstractLeaderEntity leader) {
AbstractLeaderEntity.State patrolState = AbstractLeaderEntity.State.fromIndex(leader.getPatrollingState());
AbstractLeaderEntity.State nextState = RecruitCommandStateTransitions.afterManualMovement(patrolState);
Expand All @@ -243,7 +244,7 @@ static void checkPatrolLeaderState(AbstractRecruitEntity recruit) {
}
}

static void onServerPlayerTick(ServerPlayer serverPlayer) {
public static void onServerPlayerTick(ServerPlayer serverPlayer) {
int formation = getSavedFormation(serverPlayer);
if (formation <= 0) {
return;
Expand Down Expand Up @@ -281,12 +282,12 @@ static void onServerPlayerTick(ServerPlayer serverPlayer) {
saveFormationPos(serverPlayer, new int[]{(int) targetPosition.x, (int) targetPosition.z});
}

static void initializePlayerCommandState(Player player) {
public static void initializePlayerCommandState(Player player) {
CompoundTag playerData = player.getPersistentData();
initializePlayerCommandState(playerData, (int) player.getX(), (int) player.getZ(), RecruitsServerConfig.MaxRecruitsForPlayer.get());
}

static void initializePlayerCommandState(CompoundTag playerData, int playerX, int playerZ, int maxRecruits) {
public static void initializePlayerCommandState(CompoundTag playerData, int playerX, int playerZ, int maxRecruits) {
CompoundTag data = playerData.getCompound(Player.PERSISTED_NBT_TAG);

if (!data.contains("MaxRecruits")) {
Expand All @@ -311,12 +312,12 @@ static void initializePlayerCommandState(CompoundTag playerData, int playerX, in
playerData.put(Player.PERSISTED_NBT_TAG, data);
}

static void copyPersistentCommandPreferences(Player original, Player clone) {
public static void copyPersistentCommandPreferences(Player original, Player clone) {
copyPersistentCommandPreferences(original.getPersistentData(), clone.getPersistentData());
initializePlayerCommandState(clone);
}

static void copyPersistentCommandPreferences(CompoundTag originalPlayerData, CompoundTag clonePlayerData) {
public static void copyPersistentCommandPreferences(CompoundTag originalPlayerData, CompoundTag clonePlayerData) {
CompoundTag originalData = originalPlayerData.getCompound(Player.PERSISTED_NBT_TAG);
CompoundTag cloneData = clonePlayerData.getCompound(Player.PERSISTED_NBT_TAG);

Expand All @@ -330,17 +331,17 @@ static void copyPersistentCommandPreferences(CompoundTag originalPlayerData, Com
clonePlayerData.put(Player.PERSISTED_NBT_TAG, cloneData);
}

static int getSavedFormation(Player player) {
public static int getSavedFormation(Player player) {
return getPersistedData(player).getInt(FORMATION_KEY);
}

static void saveFormation(Player player, int formation) {
public static void saveFormation(Player player, int formation) {
CompoundTag persisted = getPersistedData(player);
persisted.putInt(FORMATION_KEY, formation);
savePersistedData(player, persisted);
}

static void saveUUIDList(Player player, String key, Collection<UUID> uuids) {
public static void saveUUIDList(Player player, String key, Collection<UUID> uuids) {
CompoundTag persisted = getPersistedData(player);
ListTag list = new ListTag();

Expand All @@ -354,7 +355,7 @@ static void saveUUIDList(Player player, String key, Collection<UUID> uuids) {
savePersistedData(player, persisted);
}

static List<UUID> getSavedUUIDList(Player player, String key) {
public static List<UUID> getSavedUUIDList(Player player, String key) {
CompoundTag persisted = getPersistedData(player);
List<UUID> result = new ArrayList<>();
if (!persisted.contains(key, Tag.TAG_LIST)) {
Expand All @@ -372,17 +373,17 @@ static List<UUID> getSavedUUIDList(Player player, String key) {
return result;
}

static int[] getSavedFormationPos(Player player) {
public static int[] getSavedFormationPos(Player player) {
return getPersistedData(player).getIntArray("FormationPos");
}

static void saveFormationPos(Player player, int[] pos) {
public static void saveFormationPos(Player player, int[] pos) {
CompoundTag persisted = getPersistedData(player);
persisted.putIntArray("FormationPos", pos);
savePersistedData(player, persisted);
}

static void saveFormationCenter(Player player, Vec3 center) {
public static void saveFormationCenter(Player player, Vec3 center) {
CompoundTag persisted = getPersistedData(player);
persisted.putDouble("FormationCenterX", center.x);
persisted.putDouble("FormationCenterY", center.y);
Expand All @@ -391,7 +392,7 @@ static void saveFormationCenter(Player player, Vec3 center) {
}

@Nullable
static Vec3 getSavedFormationCenter(Player player) {
public static Vec3 getSavedFormationCenter(Player player) {
CompoundTag persisted = getPersistedData(player);
if (!persisted.contains("FormationCenterX")) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.talhanation.bannermod.events;
package com.talhanation.bannermod.citizen.runtime;

import com.talhanation.bannermod.config.WorkersServerConfig;
import com.talhanation.bannermod.entity.citizen.CitizenEntity;
import com.talhanation.bannermod.entity.citizen.CitizenIndex;
import com.talhanation.bannermod.events.ClaimEvents;
import com.talhanation.bannermod.events.WorkerSettlementClaimPolicy;
import com.talhanation.bannermod.persistence.military.RecruitsClaim;
import com.talhanation.bannermod.registry.citizen.ModCitizenEntityTypes;
import com.talhanation.bannermod.settlement.civilian.CitizenBirthRules;
Expand Down Expand Up @@ -30,20 +32,20 @@
* out of scope for this slice — a server restart resets the cooldown, which
* is acceptable for a 24000-tick (1 day) default.
*/
final class CitizenBirthService {
public final class CitizenBirthService {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Map<UUID, Long> LAST_BIRTH_GAME_TIME = new HashMap<>();
private static final Map<UUID, CitizenBirthRules.DenialReason> LAST_DENIAL_REASON = new HashMap<>();

private CitizenBirthService() {
}

static void resetRuntimeState() {
public static void resetRuntimeState() {
LAST_BIRTH_GAME_TIME.clear();
LAST_DENIAL_REASON.clear();
}

static void runCitizenBirthPass(ServerLevel level) {
public static void runCitizenBirthPass(ServerLevel level) {
if (level == null || ClaimEvents.claimManager() == null) {
return;
}
Expand Down
Loading
Loading