<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://manicpop.org/gewiki/index.php?action=history&amp;feed=atom&amp;title=Classic_bug%3A_Team_state_inconsistency</id>
	<title>Classic bug: Team state inconsistency - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://manicpop.org/gewiki/index.php?action=history&amp;feed=atom&amp;title=Classic_bug%3A_Team_state_inconsistency"/>
	<link rel="alternate" type="text/html" href="https://manicpop.org/gewiki/index.php?title=Classic_bug:_Team_state_inconsistency&amp;action=history"/>
	<updated>2026-10-20T16:14:14Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.2</generator>
	<entry>
		<id>https://manicpop.org/gewiki/index.php?title=Classic_bug:_Team_state_inconsistency&amp;diff=2028&amp;oldid=prev</id>
		<title>CodexBot: Link nightly cleanup to the midnight routine overview</title>
		<link rel="alternate" type="text/html" href="https://manicpop.org/gewiki/index.php?title=Classic_bug:_Team_state_inconsistency&amp;diff=2028&amp;oldid=prev"/>
		<updated>2026-10-17T04:44:56Z</updated>

		<summary type="html">&lt;p&gt;Link nightly cleanup to the midnight routine overview&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;{{Classic bug&lt;br /&gt;
|id=GE-BUG-014&lt;br /&gt;
|affected=Classic 3.2e and published classic source&lt;br /&gt;
|status=Verified in source; corrected in ge-next 0.3 and 0.3.3&lt;br /&gt;
|fixed=ge-next 0.3; online KICK edge fixed in 0.3.3&lt;br /&gt;
|source=GECMDS.C: cmd_team(); GEMAIN.C: midnight(), load_team_tab(), and update_team_tab()&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
Classic Galactic Empire represents a player&amp;#039;s team membership in several places that are not updated as one operation:&lt;br /&gt;
&lt;br /&gt;
* the active player&amp;#039;s in-memory &amp;lt;code&amp;gt;WARUSR.teamcode&amp;lt;/code&amp;gt;;&lt;br /&gt;
* the saved &amp;lt;code&amp;gt;teamcode&amp;lt;/code&amp;gt; in that player&amp;#039;s user record;&lt;br /&gt;
* each entry&amp;#039;s cached &amp;lt;code&amp;gt;teamcount&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;teamscore&amp;lt;/code&amp;gt; in the in-memory team table; and&lt;br /&gt;
* the flat team file written by &amp;lt;code&amp;gt;update_team_tab()&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Different TEAM subcommands update different subsets of those representations. Classic relies on [[Game execution flow#Midnight routine|midnight cleanup]] to recount members, recalculate scores, clear references to nonexistent teams, remove empty teams, and rewrite the team file. Until cleanup runs, commands can report mutually contradictory membership, member counts, and scores.&lt;br /&gt;
&lt;br /&gt;
== The four copies of team state ==&lt;br /&gt;
A team definition contains its numeric code, name, member count, team score, public password, and secret password. Classic loads those definitions from the team file into &amp;lt;code&amp;gt;teamtab[]&amp;lt;/code&amp;gt; when the module starts.&lt;br /&gt;
&lt;br /&gt;
Membership itself is also stored independently in every user&amp;#039;s &amp;lt;code&amp;gt;teamcode&amp;lt;/code&amp;gt;. An active user has a separate in-memory copy of that record in &amp;lt;code&amp;gt;waruptr&amp;lt;/code&amp;gt;. The team table&amp;#039;s member count and score are therefore derived values: they are only correct when they agree with all saved user records.&lt;br /&gt;
&lt;br /&gt;
Classic has no helper that commits a membership change across all of these locations. Each command edits fields directly, and its exact selection of writes differs from the other commands.&lt;br /&gt;
&lt;br /&gt;
== Command-specific inconsistencies ==&lt;br /&gt;
=== TEAM JOIN ===&lt;br /&gt;
Classic validates the requested team and password, then performs these operations:&lt;br /&gt;
&lt;br /&gt;
# It assigns the new team code to the active user&amp;#039;s in-memory &amp;lt;code&amp;gt;waruptr&amp;lt;/code&amp;gt;.&lt;br /&gt;
# It increments the destination team&amp;#039;s cached &amp;lt;code&amp;gt;teamcount&amp;lt;/code&amp;gt;.&lt;br /&gt;
# It rewrites the team file.&lt;br /&gt;
&lt;br /&gt;
It does not first check whether the player already belongs to a team. It also does not immediately write the changed user record with &amp;lt;code&amp;gt;geudb(GEUPDATE,...)&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
A player can consequently join Team Blue while already belonging to Team Red. The active session says Blue and Blue&amp;#039;s cached count increases, but Red&amp;#039;s count is not decreased. Until the user record is saved by some later path, a database-driven command such as [[Team command|&amp;lt;code&amp;gt;TEAM MEMBERS&amp;lt;/code&amp;gt;]] can still find the player under Red. A module interruption before a later user save can also discard the apparent move.&lt;br /&gt;
&lt;br /&gt;
Joining the same team repeatedly is similarly unsafe: every JOIN increments the cached count even though only one user record exists.&lt;br /&gt;
&lt;br /&gt;
=== TEAM START ===&lt;br /&gt;
Starting a team writes a new team-table entry with a count of one, assigns its code to the founder&amp;#039;s in-memory user record, and writes the team file. As with JOIN, classic does not immediately save the founder&amp;#039;s changed user record and does not reject a founder who already belongs to another team.&lt;br /&gt;
&lt;br /&gt;
The founder can therefore appear to lead the new team in the active session while the user database still associates that account with the old team. The new team&amp;#039;s saved aggregate says it has one member even when a database query cannot find that membership yet.&lt;br /&gt;
&lt;br /&gt;
=== TEAM UNJOIN ===&lt;br /&gt;
UNJOIN is more complete. It clears the active user&amp;#039;s team code, immediately updates the user database, decrements the cached member count, and rewrites the team file. Classic includes an underflow check that converts a wrapped count above 65000 back to zero.&lt;br /&gt;
&lt;br /&gt;
Even this path depends on the cached count having been correct before the command. If prior JOIN, START, or KICK operations have already inflated or failed to reduce it, subtracting one does not restore the true membership total. An empty team is also not removed immediately; that is deferred to cleanup.&lt;br /&gt;
&lt;br /&gt;
=== TEAM KICK ===&lt;br /&gt;
KICK loads the named user&amp;#039;s saved record, clears its team code, writes that user record, and sends mail to the removed player. It does not decrement the team&amp;#039;s cached member count and does not rewrite the team file for the membership change.&lt;br /&gt;
&lt;br /&gt;
If the removed player is currently active, KICK edits a temporary database record rather than that session&amp;#039;s &amp;lt;code&amp;gt;waruptr&amp;lt;/code&amp;gt;. The active copy can continue to identify the player as a member until it is reloaded or otherwise reconciled, and a later save of the stale active copy can conflict with the database update.&lt;br /&gt;
&lt;br /&gt;
The leader receives confirmation that the kick succeeded, but &amp;lt;code&amp;gt;TEAM SCORE&amp;lt;/code&amp;gt; can retain the old count and &amp;lt;code&amp;gt;TEAM MEMBERS&amp;lt;/code&amp;gt; can disagree with the active player&amp;#039;s own team-dependent behavior.&lt;br /&gt;
&lt;br /&gt;
=== TEAM SCORE ===&lt;br /&gt;
The classic TEAM SCORE command does not derive its output from current user records. It sorts and prints the cached &amp;lt;code&amp;gt;teamcount&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;teamscore&amp;lt;/code&amp;gt; values in &amp;lt;code&amp;gt;teamtab[]&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Member commands adjust the count inconsistently, while ordinary score changes do not update &amp;lt;code&amp;gt;teamscore&amp;lt;/code&amp;gt; at all. A player&amp;#039;s kills or planet activity can change that player&amp;#039;s score throughout the day without changing the displayed team score. The scoreboard is therefore a snapshot from the last midnight calculation, mixed with whatever count adjustments have occurred since then.&lt;br /&gt;
&lt;br /&gt;
== Worked example ==&lt;br /&gt;
Assume Alice and Bob belong to Red. Its cached member count is two. Blue has one member.&lt;br /&gt;
&lt;br /&gt;
# Alice runs &amp;lt;code&amp;gt;TEAM JOIN&amp;lt;/code&amp;gt; for Blue without first using UNJOIN.&lt;br /&gt;
# Alice&amp;#039;s active record now says Blue.&lt;br /&gt;
# Blue&amp;#039;s cached count rises from one to two.&lt;br /&gt;
# Red&amp;#039;s cached count remains two, so the two team counts now claim four memberships for only three users.&lt;br /&gt;
# Until Alice&amp;#039;s user record is written, &amp;lt;code&amp;gt;TEAM MEMBERS&amp;lt;/code&amp;gt; can still list her under Red because it searches saved user records rather than her active copy.&lt;br /&gt;
# Alice can see Blue as her team in commands that inspect &amp;lt;code&amp;gt;waruptr&amp;lt;/code&amp;gt;, producing two different answers during the same session.&lt;br /&gt;
&lt;br /&gt;
If Alice repeats the JOIN command, Blue&amp;#039;s cached count can rise again without creating another member. If a Red leader instead kicks Bob, Bob&amp;#039;s saved membership is cleared but Red&amp;#039;s cached count still remains unchanged. These are not display-only discrepancies; team capacity checks use the cached count and can reject or admit members based on a number that does not represent the saved roster.&lt;br /&gt;
&lt;br /&gt;
== What midnight cleanup does ==&lt;br /&gt;
Classic&amp;#039;s midnight routine attempts to rebuild consistency:&lt;br /&gt;
&lt;br /&gt;
# It zeros every team&amp;#039;s cached member count and score.&lt;br /&gt;
# It scans all user records and validates each positive &amp;lt;code&amp;gt;teamcode&amp;lt;/code&amp;gt; against the loaded team table.&lt;br /&gt;
# It clears a user&amp;#039;s saved team code when no matching team exists.&lt;br /&gt;
# It recounts valid memberships from the user database.&lt;br /&gt;
# It recomputes each user&amp;#039;s total score from planet score plus kill score.&lt;br /&gt;
# For each team member, it adds the configured team bonus and that user&amp;#039;s score divided by the team&amp;#039;s member count.&lt;br /&gt;
# It marks teams with no saved members for removal.&lt;br /&gt;
# It rewrites the team file with the reconstructed counts and scores.&lt;br /&gt;
&lt;br /&gt;
The effective classic team-score calculation is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
team score = sum(member score / number of members)&lt;br /&gt;
           + (number of members * team bonus)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The division is integer division and is performed separately for every member before the results are added. It can therefore lose more remainder points than dividing the sum of all member scores once. This rounding detail is secondary to the stale-state defect, but it matters when reproducing classic&amp;#039;s exact displayed total.&lt;br /&gt;
&lt;br /&gt;
This pass repairs many aggregate errors, but only according to the user records present when cleanup runs. If an earlier JOIN or START changed only the active copy and that change was never saved, cleanup treats the old saved membership as authoritative. Conversely, an online player whose stale active copy disagrees with a KICK can reintroduce bad state through a later save.&lt;br /&gt;
&lt;br /&gt;
Cleanup is therefore acting as delayed reconciliation for operations that should have been internally consistent when they completed. Players can spend most of a day seeing stale values before the next pass repairs them.&lt;br /&gt;
&lt;br /&gt;
== Gameplay consequences ==&lt;br /&gt;
The defects can cause:&lt;br /&gt;
&lt;br /&gt;
* TEAM SCORE to show yesterday&amp;#039;s score despite current kills and planet activity;&lt;br /&gt;
* TEAM MEMBERS, REP ACC, scans, team chat, and team-restricted planet access to disagree about a player&amp;#039;s affiliation;&lt;br /&gt;
* one player to inflate a team&amp;#039;s cached member count by joining repeatedly;&lt;br /&gt;
* a player to move directly between teams while the old team retains a phantom member;&lt;br /&gt;
* a kicked online player to retain stale team state in the active session;&lt;br /&gt;
* a team to appear full because its cached count is too high, or to accept too many members when it is too low;&lt;br /&gt;
* empty teams to remain available until cleanup; and&lt;br /&gt;
* cleanup or a restart to make apparently valid memberships or teams suddenly change.&lt;br /&gt;
&lt;br /&gt;
Because team affiliation controls more than the scoreboard, stale state can affect communication, friendly-fire decisions, planet access configured for a team, and the enforcement of the configured maximum team size.&lt;br /&gt;
&lt;br /&gt;
== Correction in ge-next ==&lt;br /&gt;
ge-next 0.3 changed the TEAM paths so membership operations update their relevant state immediately:&lt;br /&gt;
&lt;br /&gt;
* JOIN refuses players who already belong to a valid team and requires UNJOIN before changing teams.&lt;br /&gt;
* JOIN and START save the user&amp;#039;s new team code immediately instead of waiting for another user-record write.&lt;br /&gt;
* UNJOIN and KICK update the saved membership, cached member count, empty-team state, and team file as part of the command.&lt;br /&gt;
* Commands validate that a user&amp;#039;s saved team code still identifies a live team and clear stale assignments when encountered.&lt;br /&gt;
* An empty team is marked deleted immediately rather than remaining live until midnight.&lt;br /&gt;
* TEAM MEMBERS reads saved memberships after command paths have committed them.&lt;br /&gt;
* TEAM SCORE scans current user records when requested, builds temporary counts and score totals, applies the intended average-plus-bonus formula, and sorts that current result instead of trusting the last saved aggregate.&lt;br /&gt;
&lt;br /&gt;
TEAM SCORE sums current member scores first and divides the total once. This implements the intended average more accurately than classic&amp;#039;s sum of separately truncated quotients.&lt;br /&gt;
&lt;br /&gt;
Midnight cleanup remains as a defensive reconciliation pass. It is no longer the normal mechanism required to finish ordinary TEAM commands or make the scoreboard current.&lt;br /&gt;
&lt;br /&gt;
=== Online KICK edge fixed in 0.3.3 ===&lt;br /&gt;
Through ge-next 0.3.2, KICK updated the target&amp;#039;s saved user record, team count, and team file, but did not clear the target&amp;#039;s active &amp;lt;code&amp;gt;WARUSR.teamcode&amp;lt;/code&amp;gt; if that player was online. The active session could continue using team-dependent privileges and could later save its stale team code back over the corrected user record.&lt;br /&gt;
&lt;br /&gt;
ge-next 0.3.3 fixes this in both the 16-bit and 32-bit versions. After updating the saved record, KICK checks active game sessions for the kicked user and clears their in-memory team code if it still matches the team being left. The existing count and team-file updates then proceed. This closes the stale active-session path without changing an offline player or an active record that no longer identifies the kicked team.&lt;br /&gt;
&lt;br /&gt;
The separate problem of deleted numeric team codes later being reused, which can reconnect old planets to unrelated teams, is covered by [[Classic bug: Reused deleted team identifiers]].&lt;br /&gt;
&lt;br /&gt;
== Porting guidance ==&lt;br /&gt;
A port should choose one authoritative source for membership and treat member count and team score as derived data. If aggregates are cached, every membership transaction must update the user record and the cache together, with rollback or recovery if either write fails.&lt;br /&gt;
&lt;br /&gt;
Moving between teams should be one explicit operation or should require a completed leave before a join. Kicking an online player must update both persistent and active state. Score displays should be calculated from current member records or from aggregates maintained whenever the underlying scores change, not from a once-daily snapshot.&lt;br /&gt;
&lt;br /&gt;
A cleanup pass is still useful for repairing records after crashes or manual database changes, but normal correctness must not depend on waiting until midnight.&lt;br /&gt;
&lt;br /&gt;
[[Category:Classic bugs: Multiplayer state]]&lt;br /&gt;
[[Category:Classic bugs: Database]]&lt;br /&gt;
[[Category:Classic bugs: Teams]]&lt;br /&gt;
&lt;br /&gt;
{{GE navigation}}&lt;/div&gt;</summary>
		<author><name>CodexBot</name></author>
	</entry>
</feed>