The Thick Third - #138
Merged
Merged
The Thick Third#138
Conversation
…kill Pvp: two players are enemies when they are on opposite sides of a wargame they are both in, read from the WargameData sides as the client's Actor.GetWargameParticipantStatus does. The only wargame so far is the Clan Feud. - TargetCategory per recipient: HOSTILE to an enemy, FRIENDLY otherwise, in CreatePlayerEntityData and again for everyone around a player whenever ClanFeuds sends their WargameData (feud start and end, joining or leaving a clan at feud). The client's hostile targeting, right-click Attack, radar and overhead go by it. - Weapons (missiles, melee, constant fire including the propellant cone) and direct-damage abilities (single target, cone, radius around source or target) land on enemy players. Effect abilities, splash, cone weapons' other victims, crit effects, pulls and lightning's arc and storm still take creatures only. A friendly-effect ability at an enemy player is refused, as the client refuses it. - shared/gameconstants.py: PVP_DAMAGE_MODIFIER 0.5 on a player's damage to a player; PVP_HEALING_MODIFIER 0.5 on a player's heal on a player who dealt or took PvP damage within CombatRegen.CombatTimeoutMs; PVP_EFFECT_DURATION_MODIFIER -0.5 on a player's stun on a player (weapon and ability stuns and knockbacks go through PlayerCrowdControl). PVP_CRITICAL_DAMAGE_MODIFIER was already applied by CriticalHits. - Defeat: a player brought to zero by an enemy player gets full health and armour back, the kill is counted for the feud (ClanFeuds.Kill, scoreboard to both clans), and PvP Safety (gameeffectdata PVP_SAFETY 386, PVP_SAFETY_DURATION 60 s) goes on them. There is no player death yet. - PvP Safety stops PvP damage both ways; the hit lands as Immune. PvpTests: enemies across a feud only; HOSTILE introduction and retargeting on feud start and end; weapon hit halved on an enemy and nothing on anyone else; Safety immune both ways; defeat restores, counts and makes safe; ability damage and in-fight healing halved; a radius ability catches enemies and spares friends; duration and damage scaling.
…rfeit
Duels implements the Duel Wargame of client/wargame.py:
- ChallengeUserToWargameByName(targetName, timeMins, maxKills), sent by
/duel and the radial menu's Invite to Duel: ChallengingToWargameDuel to
the challenger, ChallengedToWargameDuel to the one challenged.
- WargameChallengeResponse(accepted, pmMsg): a decline is
WargameChallengeRefused to both (PM_WARGAME_YOU_REFUSED, PM_WARGAME_REFUSED);
an accept starts the duel - WargameStarted with the enemy's account id,
SetWargameMaxKills, DisplayWargameTimer - and each duelist's WargameData
{wargameId: side} goes to everyone around them, so Pvp makes them enemies.
- WargameChallengeRevoked(): RevokeWargameChallenge to both.
- A defeat between the two (Pvp.Defeat) is a duel kill: WargameScoreboard to
both, and WargameVictory / WargameDefeat once the kills asked for are made.
- SurrenderWargame(): the duel goes to the other side
(PM_WARGAME_NOT_IN_SQUAD_OR_DUEL when there is none).
- Leaving the map or the world (ManifestationManager.RemovePlayerCharacter)
takes back an open challenge and forfeits a duel: RemoveFromWargame to the
leaver, PM_WARGAME_PLAYER_LEFT and WargameVictory to the other.
- The map channel worker lapses challenges (PM_WARGAME_CHALLENGE_TIMED_OUT)
and ends duels whose time is up: more kills win, equal is WargameTied.
Refusals are the client's messages: no such player, oneself, another map,
a duel or challenge already open on either side, an ignore, the same squad,
a squad on one side, an open squad invitation. Two squads is a Squad
Wargame and is refused as a squad on the other side until those exist.
PartyManager refuses a squad invitation while either side is in a duel or
has a challenge open (PM_PARTY_INVITE_FAILED_*_IN_WARGAME / _WG_CHALLENGE).
Ours: a challenge lapses after 60 s; a duel is for 1 kill and 10 minutes
unless asked otherwise (at most 99 kills, 60 minutes); duel wargame ids start
at 1,000,000,000, clear of the feuds'. Nothing is saved.
Wargames.DataOf merges feud and duel WargameData for the entity
introduction, Pvp.AreEnemies and Wargames.Show (which ClanFeuds now uses).
DuelTests: the accepted challenge, a defeat ending it, a two-kill duel, the
refusals, decline, revoke and lapse, surrender, forfeit on leaving, time up,
and the packets as the client sends them.
Debuffs, areas, summons and weapon extras that were creature-only now treat an enemy player (and a creature an enemy owns) as a target. - Pvp.Controller resolves a player-owned creature (MasterEntityId) to its master. IsPvp, Shielded, Defeats, RecordEngagement and Defeat use it, so a turret, crab mine, trap, rift or minion hit counts as its master's: PVP_DAMAGE_MODIFIER, PvP Safety, and the kill credited to the master's side. - An enemy's creature is sent HOSTILE (Pvp.CategoryFor, on creation and on feud/duel retarget), is attackable (AbilityManager.IsHostile, MissileManager), and fights its master's enemies and their creatures (Pvp.SummonMayFight in BehaviorManager.MayFight and the aggro scan). - AbilityManager.VictimsWithin / VictimsInCone (creatures plus enemy players) replace HostilesWithin for Self Destruct, Scatterbombs, Fire Support (air strike, napalm, ion strike), corpse explosion, trap strike, crab mine blast, Reality Ripper, Scourge-style tick radius, Feedback P3+, Controlled Fission, lightning arc and storm, Reconstruct, launcher splash, shotgun cone and crit arc. FoesWithin (what a summon seeks) skips enemies under PvP Safety. - Single-target debuffs take any attackable actor: Decay, Target Painting, Polarity Field, Controlled Fission, Explosive Nanites, Disease, Called Shot, Feedback. A Called Shot aim on a player lands from OnPlayerDamaged. - Weapon and crit extras on an enemy player: crit side-effects (ice stun, sonic knockback, burn, armour suppression, arc, ranged weakening), net gun root (PlayerCrowdControl.Root), Vortex pull (PlayerCrowdControl.Pull), Shredder Ammo, constant-fire crits and polarity charge. Ranged damage modifiers apply to the shooter. - GameEffectManager.Attach shortens a timed debuff from an enemy player or their creature by PVP_EFFECT_DURATION_MODIFIER; stuns are scaled where made (AbilityManager.StunVictim, PlayerCrowdControl). - PlayerCrowdControl.HoldInPlace blocks a player's movement only once the rooting effect has attached, and unblocks on detach. - Feedback counts a player's own combat actions (OnPlayerActed). Tests: PvpSkillTests.
Mind Control, Traitor, Hack and Tactical Evasion work on enemy players across a wargame, and the radar perception modifiers are sent. - Mind Control on an enemy player (AbilityManager.PvpControl): P1 is a stun for DURATION scaled by PVP_EFFECT_DURATION_MODIFIER; P2-P5 put MIND_CONTROL_EFFECT on them with GameEffect.NoAttack (no weapon fire, melee or attack ability), P4-P5 also NoAssist (no ability aimed at another player, no heal or buff on anyone else, SquadWithin returns only themselves). Immune to reapplication for DURATION after it ends. - Traitor on an enemy player: GameEffect.Restrains. Pvp.Restrained makes their hits on the caster, the caster's side, or a creature of theirs land as Immune (Pvp.Shielded, MissileManager creature path); Pvp.OnHit removes it when the caster damages them. - Hack on an enemy player's machine: HACKED_EFFECT as a server stun that breaks when the machine takes damage (GameEffect.BreaksOnDamage), or when its master takes damage or makes a combat action (ReleaseHackedPets). Turrets hold fire while stunned. Construction bots carry MECHANICAL (Creature.ExtraFlags, CreatureManager.CreatureFlagsOf) so the client's HackAction accepts them. - Mind Control and Traitor on an enemy player's creature are refused as immune instead of turning it. - Tactical Evasion P1/P3 mag flash also blinds enemy players in radius and clears their target server-side. - Spotter: while one is out its owner's ToPerceiveModifier is 1 + 15 m x Spotter pumps owned / 72 m, sent to their client when it changes and with their own entity data. Stealth Armor sends ToBePerceivedModifier (DetectionRangePercent / 100) to onlookers when it changes and with the entity data. Both packets now write a double. - RequestPerformAbility: hostile effect abilities accept an enemy player target (IsAttackable); single-target debuffs were refused before. - GameEffectManager.Attach refuses a debuff Pvp.Shielded stops (PvP Safety, Traitor restraint) and a buff on someone else from a NoAssist player. - PlayerCrowdControl: Stun, Knockback and Pull report whether the effect attached; Knockback uses HoldInPlace and does not move a player whose hold was refused. Tests: PvpControlTests.
Players die at zero health and come back at a hospital or from a
medic's revive. GameMaster accounts stand back up unless .allowdeath.
- PlayerDeath.AtZero, called from ActorManager.Damage,
MissileManager.DoDamageToPlayer and FallDamage.Apply:
- GameMaster accounts (GmLevel.GameMaster and up) without
.allowdeath, and players with no client, stand back up at full.
- Duels keep Pvp.Defeat; clan feuds count the kill (Pvp.CountKill)
and kill.
- Death: State Dead, health 0, auto-fire, constant fire and pending
abilities stopped, Threat.Forget, effects detached except skill
passives and REZ_SICKNESS, ActorKilled to onlookers, PlayerDead to
the player with the hospitals they may use. DeathBlow is now set
for players in missile hit data; falls send a death blow.
- Dead players: moves refused, weapon fire refused, SelectWaypoint
refused; DeadOnArrival in entity data for onlookers.
- ReviveMe(graveyardId) / BuryMe: respawn at the chosen hospital, or the
nearest available, or in place on a map with none; full health and
armour, Revived broadcast, PvP Safety after a PvP death. Leaving the
game dead (RemovePlayerCharacter) respawns at a hospital first.
- Penalties from DEATH_PENALTY_MIN_LEVEL 5 on coming back: REZ_SICKNESS
(196) -20% Body/Mind/Spirit per stack (max 3), 120 s per death up to
360 s (GameEffect.PrimaryAttributesPercent, Stacks); REZ_SICKNESS_NO_HEAL
(195) 30 s (BlocksHealing); EQUIPMENT_DAMAGE_PER_DEATH 10% on equipped
items and the weapon drawer (Durability.WearForDeath).
- Hospitals (teleporter type 5): gained within 15 m (Hospitals.Worker in
the map-trigger tick), saved as character_teleporter type 5,
GraveyardGained sent and the map marker updated. Offered on death:
gained ones plus free ones (MapMarkerManager.IsSafeZone, or a " Base"
hospital that is not a control point), else the nearest on the map.
HospitalGraveyards maps teleporter ids to graveyardlanguage ids by
name (142 entries), others get an unused generic id per map.
- Revives: Cure P3/P5 (Resuscitate) and the healing disc at Healing 3
on a dead player send ReviveRequestInfo; RequestRevive within
REVIVE_REQUEST_DURATION (120 s) revives in place with the offer's
health (ATTRIBUTE_MAX_CHANGE % for Cure, the disc's heal);
RefuseRevive drops it. Enemies cannot offer. RequestPerformAbility
accepts a dead player target for Cure P3/P5.
- .allowdeath [on|off] (GameMaster): toggles Manifestation.AllowDeath.
- Packets: PlayerDead, DeadOnArrival, GraveyardGained,
ReviveRequestInfo; client ReviveMe, BuryMe, RequestRevive,
RefuseRevive with handlers.
Tests: PlayerDeathTests; PvpTests and PvpSkillTests updated for feud
deaths.
Passwords were stored as one round of SHA256("{salt}:{password}"), with a
random per-account salt. They are now PBKDF2-HMAC-SHA256 over the
per-account salt, with an optional server-wide pepper applied first as
HMAC-SHA256(pepper, password).
Stored format: pbkdf2-sha256$<iterations>$<p|->$<64 hex>. The iteration
count and whether a pepper was used are recorded per hash, so either
setting can change without breaking existing accounts.
- New appsettings.json section PasswordHashConfig { Pepper, Iterations }.
Iterations 0 means 600,000; values under 100,000 are raised to 100,000.
Settings are read on every hash, so a config reload applies to the next
login.
- Legacy SHA-256 hashes still verify. On a successful login, a hash in the
legacy format, with a different iteration count, or without the pepper
now configured is rewritten with the current settings.
- A peppered hash does not verify when no pepper, or a different pepper,
is configured. The auth server logs the hashing settings at startup and
on change, and logs an error when a pepper that was set is changed or
removed.
- Unknown user names cost the same PBKDF2 work as a wrong password.
- account.password widened from varchar(64) to varchar(255): MySQL
migration Widen_password_hash; SQLite migration is empty as SQLite does
not enforce the length.
- The create command no longer logs the new account's password, and
LoginPacket.ToString no longer includes the password.
- Tests for PasswordHasher.
Corpse abilities accept a dead enemy player's body, and a Hominis Machina can get up once from where it died. - IsUsableCorpse(target, by): a dead player is a usable body when they are an enemy of the user across a wargame and nothing holds the body. The request gate passes the performer; IsCorpseInUse, BurningCorpse and Plant hold an Actor. - Reanimation / Reanimation Wave: an enemy body rises as "Clone of <name>" (Create Clone's body classes, appearance copy and weapon attack, at the risen level, with the enemy's max health and armour) under REANIMATED, following the caster (RaiseClone; Raise and the shared Rise split out). The wave also takes dead enemy players in its radius. The enemy is sent to their nearest hospital (PlayerDeath.ForceToHospital). - Hortimonculus: grows from an enemy body at its position, with the enemy's max health and their weapon's damage type as the resist type (DamageTypeOfBody); the plant then drops the body and the enemy is sent to their nearest hospital. - Cadaver Immolation: may burn an enemy body. When the player gets up (hospital, revive or self revive) the bomb goes off immediately where the body lay (OnCorpseRising, called from PlayerDeath.Revive before the move). Immolate is static; creature bodies keep the despawn path. - Polymorph P5 (Hominis Machina) drawer carries POLY_SELF_RES 417/1 (abilities.selfres). A morphed player with it unspent keeps the morph through death; the ability is accepted and resolved while dead (request and PerformRecovery gates, CanResolve) and revives in place on full health once (PlayerDeath.SelfRevive, SpendSelfRevive). ReviveMe to a hospital ends the morph. Tests: PvpCorpseTests.
A challenge between two players who each lead a squad of their own is a
Squad Wargame (SquadWargames); Duels hands it over from
ChallengeUserToWargameByName.
- Challenge: only squad leaders challenge and are challenged. Each
squad is its members in the world on the challenger's map. Every
member gets ChallengingToWargameSquad (627) or
ChallengedToWargameSquad (636) with the other squad's info
(userId, name, classId, level, isAfk), timeMins and maxKills.
- Answer: the challenged leader accepts or declines with
WargameChallengeResponse, and the challenging leader revokes with
WargameChallengeRevoked; members' answers are ignored. A refusal is
WargameChallengeRefused to both squads; a revoke or lapse is
RevokeWargameChallenge.
- Start: members still in the squad and on the map get
WargameStarted with the other squad's account ids, plus
SetWargameMaxKills, DisplayWargameTimer and WargameData
{wargameId: side}.
- Scoring: each kill counts for the killer's squad and sends
WargameScoreboard with the victim and killer to everyone. A loser
is defeated, not killed (Pvp.InSquadWargame in PlayerDeath.AtZero).
- End: the first side to reach maxKills wins (default 10, duel
limits), or the side with more kills when time runs out (equal is
a tie). A leader's /surrender gives it to the other side; anyone
else gets PM_WARGAME_NOT_ABLE_TO_SURRENDER.
- Leaving: a member who leaves the squad (leave, kick, feud
separation, disband), the map or the world gets RemoveFromWargame,
and everyone else gets PM_WARGAME_PLAYER_LEFT. A side left empty
loses; a challenge with an empty side is revoked.
- PartyManager: invitations and join-request merges are refused while
either side is in a duel or squad wargame or has a challenge open
(Wargames.IsWargaming / HasChallenge).
- Duels: wargame ids are shared with duels (NextWargameId). The
same-map, ignore and squad-invitation refusals are shared
(SameMapRefusal, InviteRefusal).
ClanFeuds.DefaultDuration is 7 days, up from 1 hour. .feud length still sets it from 1 minute up to a week.
BranchMigrationsAreConsolidatedByDatabase required the auth databases to have no migrations dated 202609 or later at all. The auth contexts have nothing consolidated, so any later auth migration failed it - 20261001000000_Widen_password_hash (password hashing) does. Char and world already allow migrations after their consolidated ones; auth now does the same, with ordering left to MigrationIdsAreUniqueAndOrdered.
Logging out and back in healed a character to full and removed Rez Trauma and the no-healing after a revive. Closing the client in a fight removed the character on the next tick and every creature dropped it. Relog state (RelogVitals): - New character columns current_health, current_armor, current_power (-1 = not saved, loads on full), rez_trauma_stacks, rez_trauma_ends_at and no_heal_ends_at (Unix ms). Migration Add_character_vitals for MySQL and SQLite, snapshots and Designer files updated. - RemovePlayer on logout saves them before the effects are cleared, after a dead player has been revived at a hospital. Penalties set aside on a loading screen (EffectCarry) are saved too. - Login: health, armour and power are set back after UpdateStatsValues, clamped to the current maximum (health at least 1); the penalties are re-attached after AssignPlayer for their remaining wall-clock time, capped at 3 stacks / 360 s and 30 s. Adrenaline still starts at 0. - EffectCarry.Carries also carries Rez Trauma and the no-healing, so a map link or dropship no longer removes them. Combat logout (CombatLogout): - Client.Close sets Manifestation.LingerUntil for an in-world player who is in combat: max(LogoutDelayMs, combat expiry), at most CombatTimeoutMs from the drop. Not for a logout already under way, a dead player, or a loading screen. - The map worker skips a lingering player until then. Threat.CanFight and BehaviorManager.ScanCell use Manifestation.IsGone (disconnected and not lingering), so creatures keep fighting the body; map links, regions, triggers and missions still treat it as gone. - CleanupDisconnected leaves a player flagged for removal and still on a map list to that map's worker, which now calls it after RemovePlayer. Before, a connection closed during a tick after the worker had run was cleaned up without RemovePlayer, skipping the position, cooldown and vitals saves. Tests: RelogStateTests (11).
The console's exit stopped the server with no warning and saved nobody: every player came back where they last logged in. Any other stop (Ctrl+C, a service stop, docker stop) did the same. Characters were only written on leaving the world, so a crash lost everything since login. Shutdown: - exit [minutes] [reason] warns everyone in the world in chat at once, then at 60/30/15/10 minutes, each minute from 5, and 30 and 10 seconds (ShutdownSchedule). exit cancel calls it off and says so. Minutes may be fractional; bad input prints the usage instead of throwing. - In the last 60 seconds, new connections to the world port are closed on accept. - At the end, on the loop thread (EvacuateAll): every connection is closed, and every flagged character is taken out with RemovePlayer (position, time played, health, death penalties, cooldowns; a dead player revived at a hospital first) with no time budget (MapChannelManager.RemoveAllFlaggedPlayers) and no combat linger. Then Shutdown and StopApplication. - GameHost.StopAsync asks the loop to do the same and waits up to 8 s (EvacuateForHost), so a host stop also saves everyone. - Shutdown runs once (later calls do nothing) and only stops a running loop. OnAccept tolerates the listener already being closed. Autosave (AutoSave): - GameConfig.AutoSaveMinutes (default 5, 0 = off; in appsettings.json). Every 5 s each map saves its players whose time has come: position, run/crouch, time played, RelogVitals and ActionReuse. The first save is staggered across the interval by entity id; at most 10 per map per pass. Players loading, transferring, leaving, disconnected or dead are skipped. A failed save is logged and retried next interval. - The worker's removal pass is now RemoveFlaggedPlayers(map, budgeted). Tests: ShutdownAndAutoSaveTests (10).
Running feuds and open challenges are saved to the character database
and loaded back when the server starts. Duels and squad wargames are
still not saved.
- Schema: migration 20261101000000_Add_clan_feuds (MySQL and SQLite)
adds two tables. clan_feud holds id, challenger_clan_id,
target_clan_id, ends_at (Unix ms, UTC), challenger_kills and
target_kills. clan_feud_challenge holds wargame_id,
challenger_clan_id and target_clan_id. Both keys are server-assigned
wargame ids (ValueGeneratedNever).
- Repository: ClanFeudRepository (ICharUnitOfWork.ClanFeuds) gets,
upserts and deletes rows, saving on each call.
- ClanFeuds writes through an IStore:
- a challenge is saved when made, and deleted when answered,
revoked, made moot by a feud starting, or dropped because a clan
disbanded;
- a feud is saved when it starts and on every kill, and deleted
when it ends.
Store failures are logged and the feud carries on in memory.
- ClanFeuds.Load(store), called from Server after ClansInit:
- each feud's clock is rebuilt from ends_at, so the clock keeps
running while the server is down, and a feud that ran out
meanwhile ends on the first worker tick, decided on its kept
score;
- rows naming a missing clan, the same clan twice, or a duplicate
pair are dropped and deleted;
- new wargame ids continue after the highest loaded id.
- A loaded challenge is sent to the challenged clan's leader once,
as ClanWargameInviteReceived, the first time they enter the world,
because their client lost the indicator in the restart.
- Without a store (tests), nothing is saved.
Message of the day:
- New appsettings.json section MessageOfTheDay { Text, ShowEveryLogin,
Translations (client language id -> text) }. Default text is the old
hard-coded line; an empty Text sends nothing.
- Sent once per connection on entering the world. LoginOk also runs
after every dropship ride and used to resend it each time.
- By default sent as SendMOTD (language dictionary; the client shows it
only when it differs from the last one it showed). ShowEveryLogin
sends PreviewMOTD, shown every time.
- A change to the file is pushed to everyone in the world on the main
loop. Console "motd" prints it; ".motd" (Observer) previews it.
GM tools (Moderation), in game at GameMaster level and on the console:
- .announce / announce <message>: "[Announcement] ..." system message
to everyone in the world or loading into it.
- .kick / kick <familyName> [reason]: tells the player, then closes
every connection of that account after 1 s. Client.SkipCombatLinger
stops a kicked player lingering in combat.
- .mute / mute <familyName> <minutes> [reason] (1 min to 1 year) and
.unmute / unmute <familyName>: stored on the account (new column
account.muted_until, Unix ms; migration Add_account_muted_until for
MySQL and SQLite), so it survives relogs and restarts and works on
offline accounts. Uses the client's PM_GM_* silence messages.
- A silenced player cannot use say, shout, emote, squad, clan, clan
leader, channel chat or whisper/reply, except whispers to a game
master. Dot commands and petitions still work. They are reminded on
entering the world and told when the silence runs out (per-map
check every second).
- In game, a GM cannot kick or silence an account of their own level
or higher; the console can.
- Console commands run their work on the main loop (Server.RunOnLoop).
Tests: MotdAndModerationTests (9).
docs/gm-commands.md: - New Moderation section: .announce, .kick, .mute and .unmute, the level rule (only accounts below the GM's level; the console can act on anyone), and how a silence works (stored on the account; which chat it blocks; whispers to GMs, petitions and dot commands still allowed; reminder on entering the world and notice when it ends). - .motd under Information and diagnostics. - Game server console: motd, announce, kick, mute, unmute, and exit with a countdown, reason and cancel; host stops also save players; the message of the day is set in appsettings.json. - Sources list and the development commit the page was checked against.
PvP Safety no longer stops what its holder does. Pvp.Shielded blocks a player's (or their creature's) hit or debuff only when the target holds Safety; the attacker's own Safety no longer counts. Attacking an enemy player now ends the attacker's Safety. The new Pvp.Attack(mapChannel, source, target) runs where an attack is resolved: - ActorManager.Damage, for direct hits only; - MissileManager.MissileTrigger; - GameEffectManager.Attach, for debuffs; - Mind Control, Traitor and the Tactical Evasion flash. When the target is an enemy player, Pvp.Attack takes the attacker's Safety off (Pvp.EndSafety, which detaches PVP_SAFETY so the client says "PvP damage enabled") before checking the target's Safety. An attack on a safe enemy therefore still ends the attacker's Safety. These leave Safety on: - a damage-over-time tick (isPeriodic); - buffs; - hits on non-enemies. Tests: PvpTests covers the holder staying immune, a safe attacker's hit landing and ending Safety, an attack on a safe enemy, a periodic tick versus a new debuff, and actions on non-enemies.
A mission status refresh (deadline started, satisfied or ended; an indicator revealed; a timed mission failed) sent MissionStatusInfo with a dictionary holding only the mission that changed. The client's Recv_MissionStatusInfo replaces its whole mission log with that dictionary and untracks every mission not in it, so all other missions disappeared from the journal and the tracker until the next login sent the full snapshot. - MissionProtocolAdapter.PublishMissionStatus now sends BuildStatusSnapshot(player), the same snapshot a login sends, when the mission named is one the client is shown. - New overload taking several mission ids sends one snapshot for all of them; the indicator/deadline refresh loop in MissionPublicationPlans uses it instead of one packet per mission. Tests: MissionLifecycleTests.AStatusRefreshForOneMissionCarriesTheWholeJournal; two BootcampProtocolTests that asserted a one-mission dictionary now assert the whole journal.
A kill in a clan feud now gives the killer prestige (PvpPrestige), using the values in the client's shared/gameconstants.py. Duels and squad wargames give none, as their wargame flags say. - Generated: 30 + 2 per level the victim is above the killer, clamped to 10-50. The same killer generates from the same victim at most once per 600 s; records are dropped after 900 s. - Stolen: 1% + 0.1% per level the victim is above the killer, clamped to 0-2%, of the victim's prestige, rounded down. Taken on every kill. - Kill credit: the killer may be at most 4 levels above the victim and at most 8 below. A kill outside that range is not counted for the feud, sends no scoreboard and gives no prestige; killer and victim get PM_WARGAME_FEUD_NO_KILL_CREDIT_LEVELS. ClanFeuds.Kill now returns whether the kill was counted. - Both changes go through CharacterManager.UpdateCharacter (CharacterUpdate.Prestige), which persists them and sends UpdateCredits with the delta. The stolen amount is taken from the victim first and refunded if the killer's gain cannot be saved. - Messages: the killer gets PM_PRESTIGE_POINTS_RECEIVED_PVPKILL (amount, playerName, amountGenerated, amountStolen) and the victim gets PM_PRESTIGE_POINTS_REMOVED_PVPDEATH when something was stolen, both under the PrestigeGainLose filter. - Feud victory: ClanFeuds.End calls PvpPrestige.FeudWon for each winning-clan member online. PvpPrestige.FeudVictoryPrestige is 0, so nothing is given yet. The formulas, the direction of the level range and the per-pair interval are interpretations of the constant names. Wagered-item bonuses are not implemented.
…s put back in service The client's ALTERNATE_MESH game effect (170, gameeffects/altmesh.py) swaps a creature's model for a second one: SwapMeshDeath swaps to it and, at level 1, plays OBJECT_ABILITY_EXPLOSION_LARGE; SwapMeshRevive swaps back. Both are called through CallGameEffectMethod. Nothing sent either. AlternateMesh: - The effect is attached with every creature that has a wreck as it is given to a client (CreateCreatureOnClient): args (origMeshId, altMeshId, collisionRole, alreadySwapped), unannounced, source the creature itself. Announced on attach, the client marks it announced without visuals and SwapMeshDeath's own announce - the explosion - returns at once. - Which classes have one: creature flags ALT_MESH (43) and ALT_MESH_DELAYED_3500 (103) in creature_class_flag, and a class -> wreck model table (the class's model with "_destroyed" on its name): the AFS Turret, AFS Light Turret, Brann Turret, Bane Mortar and Bane Light Mortar at once (43); the Predator and the Ravager 3.5 s after they die (103). - HandleCreatureKill sends SwapMeshDeath (or times it; the behaviour loop's dead branch sends it when due). CreatureSupport.Revive sends SwapMeshRevive before Revived. A client arriving later is given alreadySwapped. - Kept on the creature (AlternateMeshEffectId/Swapped/SwapAt), not among its GameEffects, which are cleared on death. - An emplacement's wreck stays on its mount: a turret from an automatic pool of emplacements is not deleted when its corpse time is up (SpawnPool.Wrecks), and when the pool's respawn comes round the wreck is revived in place at full health and armour instead of a new creature being spawned. A wreck removed from the world lets the pool spawn as before. A walker's wreck is an ordinary corpse. Add_wreck_creatures (SQLite and MySQL, SQL inserts, no model change): - creature_class_flag: 43 on 4064, 11302, 23902, 7482, 10509; 103 on 3902, 30080. - Creature rows for the four classes that had none, level 42, no spawn pool: 590001 Bane Mortar (7482), 590002 Bane Light Mortar (10509), 590003 Brann Turret (23902), 590004 Ravager (30080), with actions 71001-71004, stats and weapon appearances. MissileTrigger: WEAPON_GROUNDTARGET (411), the Bane Mortar's attack, is resolved as a weapon attack instead of through the unsupported-action default. AlternateMeshTests: flags and rows in the migrated database, wreck models against the destroyed prop classes, attach args, death swap, delayed swap, wreck kept past corpse time, in-place revive by the pool, revive by CreatureSupport.Revive, late arrival, wreck removed, creature with no wreck.
The prestige window's wager slot now works (InventoryManager.Wager). The client sends WagerItem(slot) and RemoveWageredItem(); the server had the opcodes and message ids but no handlers. The slot - One item: a character_inventory row of type WagerInventory, slot 0. Loaded with the character, shown on every arrival (ResetWagerInventory, AddWagerItem), released with the rest of the inventory, and counted by HoldsTemplate. - WagerItem(slot) swaps the wager slot with a backpack slot; None or an empty slot takes the wagered item back, to that slot or the first free one of its tab. RemoveWageredItem() takes it back. - Wagerable: equipment in the equipment tab, not a stack, tradable, not bound, not mission-bound, not broken, of Uncommon, Rare or Epic quality (+20%, +35%, +50%), with a level requirement within 4 of the character's level. Not while in combat or dead. Each refusal is the client's own message (PM_WAGER_ITEM_*). - Entering combat with an item that gives its bonus locks it (ManifestationManager.EnterCombat). The lock is kept in character.wager_locked and shown with game effect LOCK_WAGERED_ITEM_EFFECT (10000087), attached again on every arrival and revive. A locked item can only be exchanged for one of a higher quality, or the same quality and the same or a higher level; the replacement is locked in its place. - A wagered item cannot be taken back while Pvp.IsEngaged. - A level change that leaves the item's range sends WageredItemOutLeveled, ends the bonus and frees the item. - GameEffect.OwnerOnly: an effect sent to its holder's client alone. The lock's client class shows the padlock on any client that receives the effect. The bonus - PvpPrestige.FeudKill adds the killer's wager percent to the prestige the kill generates, rounded down, and reports it as generated. It is not applied to stolen prestige. Forfeits - When a clan feud ends with a winner (expiry with more kills, surrender, .feud end <id> <clan>), every item wagered by a member of the losing clan, online or not, moves to the lowest free unlocked slot of the winning clan's lockbox, with a lockbox history entry. - With the lockbox full it goes to the inbox of the winning clan's character of the challenge: who made it if the challenger won, who accepted it if the target won. If that character is no longer a member, or the feud had no challenge, it goes to the winning clan's highest-ranking member. With no inbox room the item stays with its owner. - A tie, a cancelled feud and a disbanded clan forfeit nothing. - clan_feud and clan_feud_challenge record challenger_character_id, and clan_feud target_character_id. Migration 20261103000000_Add_wagering (SQLite and MySQL): character.wager_locked, clan_feud.challenger_character_id, clan_feud.target_character_id, clan_feud_challenge.challenger_character_id. Interpretations, where the client gives only names or help text: the lock lasting until the item is replaced or out-levelled, "needs repair" meaning broken, the bonus applying to generated prestige only, and all of the forfeit rules.
A member with a wagered item could leave a clan, or be kicked from it, before a feud ended and keep the item: only the losing clan's members at the end of the feud forfeited theirs. - ClanFeuds.MemberRemoved(characterId, clanId, client): called by ClanManager when a member row is deleted by LeaveClan or KickPlayerFromClan, whether or not the character is online. If the character has an item wagered (InventoryManager.HoldsWager: the loaded player's slot, else the character_inventory row), they are recorded in Feud.Departed for every feud the clan is in, with the clan they left. A leaver who is online is told. - ClanFeuds.End passes the characters who left the losing clan to the forfeit, and InventoryManager.ForfeitWagers takes their wagered items with the members'. What is forfeit is what is in the slot when the feud ends. A leaver's message names the clan they left. - The records are kept in clan_feud_stake (feud_id, character_id, clan_id), read back by ClanFeuds.Load and deleted with their feud (ClanFeudRepository.DeleteFeud), however the feud ends. - Nothing is recorded for a leaver with nothing wagered, or when the clan is in no feud. A logout is not a departure: it still goes through MemberLeft only. A disbanded clan's feuds are still cancelled, which forfeits nothing. Migration 20261104000000_Add_clan_feud_stakes (SQLite and MySQL): table clan_feud_stake.
…from anywhere on the map The client draws a mission marker on the map, and a radar pip or rim arrow, for an NPC whose conversation status is MissionComplete or Reward (mapwindow.py HandleUpdateOverheadIndicator, radarwindow.py _UpdateWidgets), while one of its missions is tracked. It draws them from the NPC's entity, and a client only holds the creatures of the 5 x 5 cells around its player (64 m), so the marker appeared only once the player was already there. Objective indicators cannot carry it: they are not drawn for a completed objective, and an extra objective entry would show in the mission log. MissionContacts gives the receiver of a mission the player can hand in to their client wherever it stands on their map: - Sync gives each such NPC the client does not hold (CreateCreatureOnClient, which sends its conversation status) and destroys the ones given from afar that are no longer one. Run from RefreshNpcConversationStatuses after mission progress, and every 3 s per client from CellManager.DoWork. - UpdateVisibility and CellUpdateLocation do not destroy a receiver that leaves the player's cells; it is recorded in Client.FarContacts instead. - RemoveCreatureFromWorld destroys it on the clients holding it from afar. - DetachClient clears Client.FarContacts. - RefreshNpcConversationStatuses also refreshes the status of the NPCs in Client.FarContacts. MissionApplication.TurnInReceivers: the receiver creature ids of the player's missions that are Active and completeable, or Success with a reward, with an NPC completion channel and the turn-in requirement met - the test ClassifyNpcConversation makes for the MissionComplete status. MissionContactsTests: given out of range, not given for a mission in progress, given on RefreshNpcConversationStatuses, kept when walking away, destroyed when the mission is gone or the NPC is dead or removed, handed back to the cells in range, periodic check, cleared on leaving the map.
ClanFeuds.ClanDisbanded ended the disbanding clan's feuds as cancelled, and a cancelled feud forfeits nothing: a clan about to lose kept every wagered item by disbanding first. - Each feud of a disbanding clan now ends as won by the other clan, whatever the score (End, Outcome.Won): the result, the messages and the forfeit of wagered items are those of any lost feud, including the items of members who left during the feud. - ClanManager.DisbandClan already calls ClanDisbanded before the member rows are deleted, so the forfeit reads the members from the database as usual. - A clan in several feuds loses them oldest first (lowest wargame id). Its members' wagered items go to the winner of the first; the later feuds end as lost with nothing left to take. - Challenges to or from the clan are dropped as before.
control_point and control_point_link (world database) hold the 41 control points of the open zones and what belongs to each: the spawn pools of the Bane's garrison and of the AFS's, its hospital and waypoint, and the creatures that are bosses of its garrison. control_point_state (character database) holds who has each point that has changed hands, and is read when the server starts. ControlPoints places each point's object on its map's own channel, in place of the one point DynamicObjectManager hard-coded, and runs it from the map channel worker: - The holder's garrison pools run and the other side's are suspended (SpawnPool.Suspended). Pools of mode 1 spawn while the Bane hold the point they are linked to; one linked to no point stays dormant. - A Bane point's object is out of service while any of its garrison is alive or on the way. With all of it dead, a 10 s interruptible use gives the point to the AFS if the garrison is still down when the use ends. - A Bane garrison pool that has been killed does not respawn on its own timer: the whole garrison returns once all of it has been down for the shortest of its pools' respawn times. - An AFS point whose garrison pools are all dead at once goes to the Bane. - When a point changes hands the losing side's living creatures are removed, corpses are left, and the new holder's pools spawn on the next pass. - A Bane point's hospital is not offered to the dead or gained, and its waypoint is contested: not listed, not selectable, not gained. Entries a character already has are kept. If the Bane hold every hospital on a map the nearest is still offered. - A kill of a Bane garrison creature gives the credited player 30 prestige, 100 for a boss (a kind 5 link, or creature flag 28 or 29), with ReceivedCreatureKillPrestige. - The point's map marker is sent as (FACTION_OWNED, True/False), isFriendly of its hospital and waypoint markers follows the owner, and PM_CONTROLPOINT_CLAIMING and PM_CONTROLPOINT_OWNED go to everyone on the map. Six points start with the Bane: Bane Comm Center, Bane Guard Station, Bane Assertion Camp, Southeast Bane, Southwest Bane and Bane Conscription Garrison. The rest start with the AFS. A private copy of a map has no control points of its own: the cloned objects are out of service. An object of the control point type that is not one of these points keeps its previous use behaviour. .cp lists the points, gives one to a side, goes to one, or moves its object to where the game master stands. Migrations: 20261105000000_Add_control_points (world) and 20261105000000_Add_control_point_state (character).
A map can run in several shared copies. appsettings.json's MapInstances names the maps by context id, each with Capacity (32), MaxCopies (8) and IdleCloseSeconds (300). Edmund Range (2374) has an entry without one in the file; MaxCopies 1 turns a map back into one channel. - The first copy is the map's own channel, instance 1, and is never closed. The others are made from it the way a private instance is (spawn pools, objects, triggers and links cloned), belong to nobody, and are ticked, searched and cleaned up with the private instances. MapChannel.IsSharedInstance marks them; IsCopy is either kind. World control points are out of service in any copy. - A copy opens when somebody walks into a map link to the map and every copy is full. A player sent to a copy counts towards it from then, not from when their client has loaded. - While the map has one copy the link leads straight in. With more, the client's instance picker is shown (ChooseInstanceList, 685): one row a copy, the fullest with room first, the full ones last, each with its number and the population word for how full it is. SelectInstance (687) moves the player to the copy picked; SelectInstanceCancel (688) drops it. A pick is refused if it was not offered, comes later than 120 s, from another map or more than 12 m from where the picker opened, or from a dead or logging-out player; a copy that filled or closed meanwhile gets the picker shown again. - With every copy full and MaxCopies reached the player is told so and stays where they are. - A shared copy closes once it has had nobody in it or on the way for IdleCloseSeconds. - A summon brings the player into the copy the summoner stands in. A login and the game master travel commands go to the map's own channel. - .instance lists the copies of the current map; .instance open, pick, go <number> and close <number> open one, show the picker, move to one and close an empty one. World migration 20261106000000_Add_edmund_range_door adds two map_link rows, 9001 and 9002: a door in the CELLAR Arena at (9.27, 40.50, 136.06), where the client draws its Edmund Range link, to (-62.5, 364.2, -412.0) in Edmund Range, and the way back from (-62.5, 364.2, -421.5). The two places in Edmund Range are taken from its navmesh and have not been walked. Data/MapTemplates maps a map context id to the client's map template id, which the picker's rows are named by.
Requires "Shared map instances, the instance picker, and a door to Edmund Range": a match belongs to a map channel, the map's own and each shared copy. Battlegrounds runs the match on map 2374. - Teams. A map link of kind 2 (Red) or 3 (Blue) in the staging area puts the player who walks into it on that team and in its base; kind 4, in a base, takes a player who stands in it for 5 s off the team and back to the staging area. A team that has more players than the other cannot be joined (Battleground.MaxImbalance, 0). JoinedTeam, LeftTeam, AddTeamMember, RemoveTeamMember and SetNumberOfTeams are sent. - Phases. Waiting until each team has MinPlayersPerTeam (1), a preparation of PrepSeconds (60), then running for at most MaxMinutes (20). A team wins by holding every control point once MinMinutes (10) have passed, or the most at the end, then by kills; level on both, nobody wins. A team with nobody left loses. After a match the points reset, everyone is put back in their base and the next one waits or prepares. - The teams are enemies only while the match runs: the match is a wargame in WargameData, Red the true side. - Bases. Until the match runs a team's players are kept within BaseRadius (45 m) of their hospital; a player in the other team's base is put back in their own; a PvP hit into or out of a player's own base lands as Immune. A player with no team is kept to the staging area. Game masters are exempt. - Death. A player on a team goes back to the team's hospital or that of a control point the team holds, with no Rez Trauma, healing block or durability wear. No hospital on the map is gained by walking up to it. - Control points. Whiskey, Charlie and Echo are the ownable control point class (10000071): states uncontrolled and team controlled, the holder shown with SetOwnerId (884). A point is nobody's at the start with its Simulated Bane standing; with all of them dead a player of a team that does not hold it captures it with a use of CaptureSeconds (10). The Bane return when the field is reset. Markers are sent as TEAM_OWNED. The points' waypoints are closed. - Desertion. Leaving a team while the match runs - the way out, another map, a logout, a disconnect - bars the other team until a match on the first has been finished. Kept in memory. - Scoreboard. ScoreBoardActive, ScoreBoardGameScore (clock and point owners by the client's control point ids 12, 10, 11) and ScoreBoardIndividualUpdate (kills, deaths, damage, healing, captures, prestige); WonBattleground and LostBattleground at the end. - Prestige. A kill generates what a feud kill does and steals none (PvpPrestige.TeamKill). A capture gives CapturePrestige (50) once a point a player a match. A win gives WinPrestige (200) and a loss LossPrestige (50) when the match reached its minimum time. - Entry. A map link into the map is refused below MinLevel (45). - Squads. A squad with players of both teams cannot be formed, and joining a team takes a player out of one that would have them. - .bg shows the match; .bg team, start, end and capture set a team, begin and end a match and give a point to a team. ControlPoints loads the points of a battleground map and leaves them out of its own rules: not placed, not indexed by pool or teleporter, not given to a faction. ".cp <id> here" moves them on every channel. World migration 20261107000000_Add_edmund_range_match, data only: creatures 595001-595004 (Simulated Thrax Rifleman, Grunt, Technician and Caretaker, copies of 531029-531032 with their stats and appearance), spawn pools 595101-595106, control points 101-103 with their links, and map links 9003-9006. The teleporters, the ways back, where a returning player arrives and the pools are placed from the map's static objects and navmesh and have not been walked. Not done: force fields at the base exits, the teams' north and south bases and their teleporters, the two depots, the control point waypoints, and the staging area's five-state sign.
A player who leaves a match while it is running (logout, lost connection, another map, the way out of the base) is kept to the copy of the map they left for Battleground.LeaverLockoutMinutes (15; 0 for none). Leaving in the staging area, during the preparation or between matches starts nothing. - Battlegrounds: a lockout per character (map, instance, until) is recorded with the desertion; LockoutOf, LockoutFor, BarredFrom, Forgive. Join refuses a team of any other copy. A player arriving on another copy is told. The time runs from the leaving and is not shortened by going back; a later desertion replaces it. Kept in memory, as desertions are. - MapChannelManager.EnterMap: a locked-out player is sent to the copy they left with no picker, and is told when it is full or has closed; no copy is opened for them. SelectInstance refuses any other copy. - SummonManager.MoveTo: not into another copy. - Game masters are not held to it; .bg team ignores it. - .bg shows the caller's lockout; .bg forgive [family name] clears a desertion and its lockout. Tests: BattlegroundTests, SharedMapInstanceTests.
A character entering the world on a map that runs in copies was put on the map's own channel, whichever copy it had left: on Edmund Range that is another match. - MapChannelManager.RememberCopy notes the copy a player is taken out of (ManifestationManager.RemovePlayerCharacter); kept in memory. - MapChannelManager.PlaceLogin, called from CharacterManager between loading the character and telling the client where to: back into the copy left if it still stands, has room and no battleground lockout shuts it to them. Otherwise, and after a restart, out of the map: to where its first map link out leads, or with none to the trigger of the first link into it. A map with neither is entered on its own channel as before, with an error logged. - A character placed outside is told why once it has arrived (Client.ArrivalNotice, MapChannelManager.ShowArrivalNotice). - Maps of one copy are not affected. Tests: SharedMapInstanceTests.
The client has no claiming state for a control point. USE_CPOINT_STATE_FACTION_A_CLAIMING (180) and FACTION_B_CLAIMING (177) are in no augmentation's state list; CONTROLPOINT has 176, 179 and 181, and ForceState or Use to any other state is ignored. A claim is shown by the interruptible use: LockToActor and UseInterruptible attach usabledata's specialFX (class, state, state) - arch_eloh_controlpoint_contested for class 3814 - in place of the owner's state effect until the lock is released. TryLockForUse already sent both. Changes for a control point of the world (ControlPoints) or of a match (Battlegrounds): - The claimant is no longer sent Use with the object's current state. That ran the state-to-itself transition, which attaches the same specialFX key untracked and re-attaches the owner's state effect the interruptible use had removed. - Everyone else in range is sent PerformWindup(UseObject, 7, objectId) on the claimant, so their clients play the use's windup (animation family 1264, FX family 1490). - An interrupted claim sends ActionInterrupt to the others and a silent UserActionFailed to the claimant, and no PerformRecovery. - ControlPoints.SetOwner sends one ForceState carrying the capture time and no UsableInfo. A player who dies with a use pending releases its lock (UseInterrupted, LockToActor(0)) before the action is dropped; the object stayed locked to them before. An object of the control point type that is neither kind of point is sent what it was.
Two TCP listeners of the game server's own (Rasa.Api), set under
ApiConfig in appsettings.json and applied again on a config reload.
Both are off when the section is missing.
- ApiConfig.Rest: HTTP on its own port (8104), GET/HEAD /<endpoint>,
JSON. Endpoints are ApiEndpoint classes registered in ApiHost:
/healthcheck {game_server_status, app_server_status}, 200 or 503
/serverstatus {uptimehours, currentconnections, peakconnections,
maxconnections}
Public (API-wide, or per endpoint) turns the key requirement off;
otherwise the X-API-Key header or a Bearer token must match the
endpoint's ApiKey or the global ApiKey. Per endpoint: Enabled,
Public, ApiKey. AllowedIps: addresses and CIDR ranges, empty for all.
Written on TcpListener rather than HttpListener, which needs a URL
reservation or an administrator on Windows.
- ApiConfig.StatusPort: a TCP port (8105) that answers the first bytes
of a connection with both objects above as one line of JSON, and
closes. AllowedIps as above; no key.
- ServerStatus: written by the world loop (a beat every tick, the
player count once a second), read by the listeners without a lock.
game_server_status is healthy while the server is ready, listening
and its loop ticked within ApiConfig.LoopStallSeconds (15);
app_server_status while the auth link is connected and logged in
(Server.AuthLinkUp).
- Connections are limited to 5 s each and 64 at once; refusals are
logged at most every 30 s.
Tests: StatusApiTests.
The client draws a squad member's and a team member's map marker from the member's entity (mapwindow.py Update: GetEntity for each party and team widget, hidden when there is none) and the radar's pip and rim arrow the same way (radarwindow.py _UpdateWidgets, kRADARTYPE_PARTY and kRADARTYPE_TEAM). A client is given the players of the 5 x 5 cells around its own (51 to 77 m) and loses them beyond, so a squad mate further off had no marker and no arrow. The squad window reads a member's health and armour from the same entity and draws the row disabled without it (partystatuswindow.py). FarAllies keeps a player's allies - the members of their squad (Player.PartyId) and, on a battleground, of their team in the channel's match - on their client wherever they stand on the same map channel: - Sync(client): gives each ally out of the client's cells that it does not hold (CellIntroducePlayersToClient), takes away the ones it was given that are no longer allies, present or visible to it, and lets go of the ones its cells have taken over. Worker runs it for every client of a map once a second, from CellManager.DoWork. - CellManager.UpdateVisibility: an ally leaving the cells is not destroyed on either client (KeptBy, KeepingFor), and one entering them is not created again (NotHeldBy, NotHolding); AddToWorld uses the same two filters. - What a player's cells are sent about them goes to the clients holding them from afar as well: CellManager.CellCallMethod(mapChannel, actor, packet), Client.CellCallMethod for the player's own entity, Client.CellIgnoreSelfCallMethod, Client.CellMoveObject and ManifestationManager.ShowTitle. - CellManager.DetachClient: FarAllies.Forget destroys the leaving player on every client that held them from afar, and what the leaving client held on it. - Detection.Hide: a cloaked player is taken off the clients that held them from afar and are not in their squad (FarAllies.Hidden); Sync does not give a player hidden from the client. - Client.FarAllies, Client.NextAllySync, Manifestation.FarWatchers. Not relayed to a far holder: the few broadcasts that walk the cell lists themselves (GetClientsInCells), and creatures' own packets. Tests: FarAlliesTests (7), BattlegroundTests .TeamMatesAcrossTheFieldStayOnEachOthersClients.
The client puts MISSION_USABLE_INDICATOR on a usable object that is in service and mission activated - the fifth argument of UsableInfo (client/augmentations/usable.py Recv_UsableInfo, _SetEnabled). Every UsableInfo sent DynamicObject.ActivateMission, which nothing sets, so no object had the effect. An object is mission activated for a player while a use of it would move one of their missions on: its entity class is the subject of an InteractionUsed trigger on a transition of an objective they have Incomplete, in an Active mission. - MissionApplication.WantedInteractions(player): those classes, each with its mission. - MissionObjects.ActivationFor(client, object): the mission id, or 0. An object with a MissionConversation has none (it has an NPC's status and no UsableInfo); nor has one outside the player's map instance. DynamicObject.ActivateMission, when set, is still sent as it is. - MissionObjects.InfoFor builds an object's UsableInfo for one client. Used where the object is created on a client (CreateDynamicObjectOnClient), where a scenario puts it in or out of service (SetScenarioInteractionEnabled), after a finished use (FootlockerRecovery; it sent 0) and where a reward container is attached for its owner (LootDispenserManager). The first three were one packet to every client in range; they are one per client now. - MissionObjects.Refresh(client), from MissionApplication.RefreshNpcConversationStatuses, which follows every mission change: an object in the player's cells that has become activated is sent UsableInfo with the mission. One that no longer is is sent UsableInfo out of service with no activation and then SetUsable(true) if it is in service: the client detaches the effect only when the object goes out of service. - Client.MissionObjects: the objects a client has been told are activated. The content has four such triggers, all in Bootcamp: mission 1992 objective 1 (the equipment crate, class 29877, which the tests use), 1995 objectives 1 and 3 and 2005 objective 1. Tests: MissionObjectSparkleTests (3).
NPCConversationStatus is now sent with CONVO_STATUS_UNAVAILABLE (1) for an NPC who gives a mission the player cannot take yet. overheadwindow.py draws OVERHEAD_MISSION_UNAVAILABLE for it; the status was defined and never sent. A mission counts when it passes every test of an offer except its prerequisites, and everything unmet comes with playing on (MissionApplication.IsNotYetAvailable): - MissionPrerequisiteKind.PlayerLevelAtLeast, or a LevelRequirement - MissionPrerequisiteKind.MissionCompleted (or a MissionStateRequirement that is not Accepted) with required state none, Success or Completed, the required mission being operational content - a Cooldown or Daily repeat policy that does not allow it at this time A required Failed state, MissionAccepted, PlayerFlagValue, MapRequirement and CustomRequirement keep a mission out whatever else it asks: Bootcamp's retry (2005, wants 1995 failed) is not waited for. MissionConversationState carries the ids (NotYetAvailable); they are no topic of the conversation. NpcManager.UpdateConversationStatus sends the status last, after the mission statuses, Train, Vending, Auctioneer and Clan and before None: npc.py keeps its vendor package, clan master and auctioneer flags from the status. The mission ids go as the data; the client reads none for it. npc.py offers CONVERSE on any status but None, so such an NPC can be talked to. OpenConversation answers a conversation with nothing in it with CONVO_TYPE_ENDCONVERSATION instead of an empty dictionary (the client logs "Unknown conversation type received from server" for one), and when a mission waits sends player message 939, "'%(missionId)s' is not available to you now.", with missionId as a number. A level change refreshes the conversation statuses of the NPCs in view (GainExperience, PublishExperience, and the GM command's LowerLevel); nothing did, so a level prerequisite met by a level left the giver's status as it was. With the content as it is: Major McAllister offers 1990 while 1992 waits, Corporal DeSimone shows 1994 unavailable until 1992 is completed, and Captain Youngblood, who is on the map only as a scene actor, 1995 until 1994 is. Tests: MissionNotYetAvailableTests (4), MissionLifecycleTests.OnlyAMissionAheadOfThePlayerIsNotYetAvailable (4 kinds), MissionLifecycleTests.GainingTheLevelAMissionWaitedForTurnsItsGiverToAvailable. MissionProtocolTests.CompleteMissionRequestClaimsRewardsWithoutAnotherAcceptStep expected an empty conversation dictionary from a receiver with nothing left; it now expects the single EndConversation entry.
…nstances The action (487) is now refused on a battleground's map for everyone, not only a player with no team, and in an instance: a squad instance or a private instance channel, a map SquadInstancePolicies.IsSquadMap names, or one of SquadInstancePolicies.DefaultMaps whether or not squad instances are enabled. A further public copy of an open map is not an instance. The no-time-limit rule for levels 2 and 3 on such maps is removed: the duration is always the action's. The object has 5000 hit points (PersonalWaypoints.MaxHealth) and is removed at 0. Who may damage it: a player who is an enemy of its owner across a wargame (Pvp.AreEnemies), a creature whose master is one, and a creature with no master whose category seeks FRIENDLY. The amount lands as it is, with no crit, cover or resistance. Damage by a player or a player's creature ends that player's PvP Safety. - MissileManager.MissileLaunch accepts it as a target for those, before the practice-target branch; MissileTrigger puts the damage on it and writes what it took into the hit. - ConstantFire.Pulse lands a non-cone pulse on it. - ManifestationManager.TryMeleeAttack applies the swing's range to it. - Abilities are not changed: they resolve actors only. Creatures: BehaviorManager.ScanCell considers it with players and creatures, nearest first; MayFight and Threat.CanFight accept it for a creature under no mind control; CreatureThink takes its position as the target's. A target that has been removed is dropped by the existing entity-type checks. Each client is told, on meeting it and when it changes (squad joined or left, wargame begun or ended), by PersonalWaypoints.Introduce and Sync: - owner, or squad at level 3: enabled; DamageInfo canBeDamaged false; - enemy: not enabled; DamageInfo canBeDamaged true; category left HOSTILE; - anyone else: not enabled; TargetCategory FRIENDLY. The client's Wormhole starts with _targetCategory False, which compares equal to HOSTILE (0), and is mouse-targetable only while its category is HOSTILE. UpdateHitPoints goes to everyone around on each hit. On every removal (destroyed, expired, replaced, owner gone) FX package 28072 vfx_ability_wormhole_death is played at its position for 3 s with EmitterManager.PlayTemporary.
The Polymorph pump tooltips list "Revive (Machine Only)" for the Thrax
Technician and "Revive (Biological Only)" for the Bane Caretaker, and
neither was in the morph's drawer. Both are now, at their player-facing
level 5: CR_TECHNICIAN_REVIVE 400 (5 m) and CR_CARETAKER_REVIVE 242
(40 m), both TARGET_FRIENDLY and canTargetDead in the client.
- AbilityManager.MorphRevive resolves them (MorphSupportModules), with
HEAL_AMOUNT at the performer's level as the health given back:
- Resuscitate on a dead player who is no enemy of the performer and
whom they may help: PlayerDeath.OfferRevive, as Cure's Resuscitate
does. The recovery names them with no healing, since
CaretakerReviveAbility announces nothing for a player with none.
- Either on a dead FRIENDLY creature with a spawn pool, dead less
than CreatureSupport.ReviveWindowMs, not scripted, not crit-killed,
not claimed: Jumpstart the MECHANICAL and MACHINA ones, Resuscitate
the rest (CreatureSupport.IsRevivableBy, CreatureSupport.Revive).
- A killed summon (no master, no spawn pool) is not revived.
- RequestPerformAbility lets these two be aimed at a dead target that
IsMorphRevivable allows, refuses any other target with
PM_TARGET_INVALID, and refuses no target or the performer themselves.
Tests: MorphReviveTests.
The client puts a mission marker on an NPC whose conversation status is MissionComplete, Reward, ObjectivComplete or ObjectivChoice (mapwindow.py HandleUpdateOverheadIndicator; radarwindow.py), from the NPC's entity. MissionContacts gave a client the receiver of a mission ready to hand in from beyond its cells; the NPC an objective is talked through with was given only by its cells, so its marker appeared once the player was already near it. MissionContacts now holds both: - MissionApplication.ObjectiveContacts(player): the NPC packages with an ObjectiveCompletion or ObjectiveChoice topic of an active mission whose objective is Incomplete and whose requirement is met. The test is the one ClassifyNpcConversation makes, moved to OpenTopics and used by both. - A contact is a live, interactable NPC whose creature row is in TurnInReceivers or whose NPC package is in ObjectiveContacts. Sync, Keep (both overloads) and Worker use it; nothing else about them changed. - MissionContacts.Relay: a move of an NPC that went to its cells (CellManager.CellMoveObject) also goes to the clients holding it from afar and out of its cells. Before, a client was not told of a far NPC moving. Tests: MissionContactsTests, five added (given from afar with status ObjectivComplete, no longer held once the objective is talked through, kept when walked away from, the two sets name exactly the NPCs with a marked status, a far-held NPC's move is relayed), and AMissionStillInProgressGivesNothing, which expected nothing at all to be given for a mission in progress, is now AMissionStillInProgressDoesNotGiveItsReceiver.
A new table of the character database, chat_log, takes a row for every line of chat a player sends: when (UTC), the kind of chat, the account, its level, the character and both names, the map, instance and position, the group it was said to (squad, clan or channel id), a whisper's recipient, to whom in words, how many players other than the sender were sent it, the line (512 characters at most), and the result - 1 delivered, 2 the sender is silenced, 3 a whisper to nobody in the game, 4 a whisper to someone ignoring the sender, 5 the sender is not in that squad, clan, rank or channel, 6 too long. - ChatAudit (new): Record; IStore and ServerStore over the character unit of work, loaded in Server's constructor next to GmAudit. One insert a line, made after the line is passed on. A store that cannot be written is logged as an error and the line is said all the same. - CommunicatorManager: RadialChat (1 say), Shout (2), Emote (3), Whisper and Reply (4), PartyChat (5), ClanChat (6), ClanLeadersChat (7) and ChannelChat (8) record the line where they pass it on and at each point they refuse it. The checks and their order are unchanged. A line that begins with a dot is a command and is not recorded here; an empty line is not recorded. - ChatChannel.Team: which team a team channel is, so its lines are recorded as "Team 1" or "Team 2". - IChatLogRepository (Add, GetRecent, GetByAccount - said by the account or whispered to it) as ICharUnitOfWork.ChatLogs. Schema: migration 20261114000000_Add_chat_log for SqliteChar and MySqlChar. docs/gm-commands.md: a Chat log section. Tests: ChatAuditTests (11).
A control point of the open world that a player in a clan takes from the Bane is held by that clan for the AFS. A player in no clan takes it for the AFS, as before. Holding - The point stays an AFS point: their garrison, hospital and waypoint, and the AFS colour on the map. - While a clan holds it the point's object is the client's clan control point class (29329) in its clan controlled state, named with name override 338, "Central Dispatch Unit - Control Point". The object is replaced when a clan takes or loses the point; AFS and Bane points keep the class of their row. - The clan class is sent ClanAssociation ahead of UsableInfo, the using clan with UseInterruptible and the owning clan with Use. Its client methods take those arguments and fail without them. Losing - The Bane take it back, as they take any AFS point. - A member of a clan at feud with the holder uses the point: ClanCaptureSeconds of an interruptible use, offered to those players only. - The clan disbands. - The weekly reset, on the squad instances' day and time: every clan's point goes back to the AFS. A reset missed while the server was down is applied on start-up to the points held from before it. Pay and lockbox - Each held point pays ClanPrestige prestige into the clan's lockbox every ClanPrestigeMinutes, by the wall clock, with a deposit line in the lockbox history. Time more than two intervals behind is paid once. - A point may have a clan lockbox: a footlocker row linked to the point with control_point_link kind 6. It is on the map only while a clan holds the point and opens for that clan's members only. Settings: appsettings.json ControlPoints (ClanOwnership, ClanPrestige, ClanPrestigeMinutes, ClanCaptureSeconds, ClanWeeklyReset). A file without the section has the defaults. GM: .cp lists the holding clan; .cp <id> clan <name or id> gives a point to a clan; .cp <id> lockbox places or moves the lockbox and .cp <id> lockbox remove takes it away. docs/gm-commands.md is updated. Database: character migration 20261115000000_Add_control_point_clan adds clan_id and clan_paid_at to control_point_state. No world schema change.
GainExperience counted the clone credits to save with
Array.IndexOf(CloneCreditLevels, level): level is an int, the table a
byte[]. That call cannot infer a type argument, so it binds to
IndexOf(Array, object) and compares a boxed int with boxed bytes: -1 for
every level. The count was always zero and UpdateCharacterCloneCredits was
never called. The publish loop below it asks with player.Level, a byte, and
did match, so the player got the credit in memory and a CloneCredits packet
while the row kept the old count.
From then on character.CloneCredits != player.CloneCredits. Every later
GainExperience was refused ("Durable progression changed before experience
could be awarded") and so was every mission reward ("Runtime progression no
longer matches durable character state"), until the character was loaded
again - which took the row's count and dropped the credit.
Reached by any GainExperience that crosses level 5, 15 or 30: creature
kills, .givexp, and .setlevel upward. Mission rewards (PlanExperience)
counted correctly and are unchanged.
- IsCloneCreditLevel(int) compares the level with each entry of the table.
GainExperience's count and both publish loops (GainExperience,
PublishExperience) ask through it; no Array.IndexOf on the table is left.
Tests, in ProgressionPersistenceTests:
- KillExperienceSavesTheCloneCreditsItHandsOut: level 5, 15, 30, 1 to 15 in
one award, and a level that pays nothing. The row, the player and the
CloneCredits packets agree, and the next award is accepted and saved.
- TheCloneCreditLevelsAreTheOnesTheTableLists.
No schema change. A credit dropped before this change is not restored.
AssignPlayer sent no CloneCredits. The client's manifestation is made again on every map with __cloneCredits = None (augmentations/manifestation.py), and it was told a count only at the moment a credit was gained: level 5, 15 or 30, or a clone credit item. After any login or map change: - the attributes window read "None" for the clone credits (attributeswindow.py, _UpdateCloneCreditsDisplay); - the trainer's Clone button, enabled on cloneCredits > 0, stayed disabled (tierselect.py), and with it the logout to the pods that it starts (exitgame.OnRequestLogoutForCloning). AssignPlayer now sends CloneCreditsPacket(player.CloneCredits) with the rest of the character, a count of 0 included. Recv_CloneCredits posts its "clone credit added" message and tutorial only when the count goes up from one the manifestation already held, so the first count on a map adds neither. Tests: CloneCreditsOnArrivalTests. No schema change.
RequestCloneCharacterToSlot took the source from client.AccountEntry. That entry is loaded at login and reloaded on character select, create, delete, clone and rename - not when a character comes back from the world to the pods: StartCharacterSelection sends the pods from a fresh query and leaves the entry as it was. Coming back from the world is the client's own route to a clone: the trainer's Clone button is a logout to character selection (exitgame.OnRequestLogoutForCloning). For that player the entry was the character as it stood when it was selected: - a clone credit earned since was not in it. The pod showed the credit and the clone was refused with NotEnoughCloneCredits, until the character was selected again or the account logged in again; - with an older credit in hand, the clone was made with the level, experience, class, attributes and position of the moment of selection, and the source's credits were written as that moment's count less one, over any earned since. The source is now read with Characters.GetByAccountId(account, slot) in the unit of work that makes the clone. Tests, in CharacterStartingExperienceCreationTests: - CloningReadsTheSourceFromItsRowNotFromTheAccountLoadedEarlier - CloningWithNoCloneCreditInTheRowIsRefusedWhateverTheAccountLoadedEarlierSays No schema change.
An NPC with more than one topic - two missions, a mission and a shop - opens the client's topic list, which is headed by the NPC's greeting (convoDataDict[CONVO_TYPE_GREETING]). The server sent none, so the client printed its own error text there: "ERROR: 7: No greeting". A conversation the client counts more than one topic in is now sent with a greeting. The count is the client's own (npc.py Recv_Converse): missions to complete, reward, give and remind of, objectives to complete, choose and talk about, vendor packages, and one for an auctioneer. A conversation of one topic goes straight to that topic and is sent as before. The client has 1,699 greeting lines and nothing that ties a line to an NPC, so every NPC is given npcgreetinglanguage 93, "Greetings.", a line the client carries under fourteen ids. No database migration.
…tages are left out SpawnPoolManager.ValidatePools reported eight Wilderness pools once the Wilderness missions' pools (upstream's 630001-630199) were in the world database. None was a camp on safe ground or in an AFS turret's scan. - A turret, to the check, was any pool of emplacements, whichever side. The missions' four Bane mortars (630076-630079, Emplacement_Bane_Turret_Standard, hostile) were turrets and hostile pools at once: each was reported 0 m from the turret of its own pool, and the Bane squad of 580007 as standing in the scan of the mortar beside it. A turret is now a pool of friendly emplacements. A Bane emplacement's pool is a hostile pool like any other, checked against the AFS turrets and the safe ground. - A pool a mission's public encounter is staged on (PublicEncounterBinding.SpawnId, PublicActorLeaseService.StagesSpawn) is on neither side of the check: not safe ground when it is friendly, not a camp when it is hostile. Sgt. Pierre (630070, mission 666) is a captive held in the Bane post of 580005 and 580012; Thrax Overseer Skeev (630130, mission 701) is one creature its scene stands 6 m from Officer Burke (171). - ValidatePools gathers the hospitals and waypoint pads, logs and records; the lines are PoolsTooClose's. SpawnPoolManager.IsStaged is replaceable for tests. Over upstream/development's world data (2119 pools, 999 creatures, 307 hospitals and waypoints, 6 public encounters) PoolsTooClose returns no line; without IsStaged it returns the three of 630070 and 630130. Tests: SpawnPoolCheckTests.
…zero health A Critical Death window opens on a stunned creature with 1 to 8 percent of its health left. Both ways out of it - CritDeathManager.WindowClosed, when nobody finishes it, and Finished, when the finishing move has played - call CreatureManager.HandleCreatureKill with that health still on the creature. HandleCreatureKill set State = Dead and did not touch health; every other caller (MissileManager, ActorManager, CreatureBombs, Reanimation) brings the health to zero before it calls. BehaviorManager.CreatureThink knew a corpse by Health.Current <= 0 alone, so for these bodies: - LootDispenserManager.AdvanceCorpseLifetime never ran: the body, and its loot, never left the map; - the Dying return no longer applied (the state was Dead), so the creature went on to the wander and fight code: it took a nearby player as its target while lying dead, and the damage paths refuse a dead creature. Changes: - HandleCreatureKill zeroes the creature's health and armour, and their refresh, where it sets State = Dead (ZeroVitals), for every caller. When the health was above zero it sends UpdateHealth before the StateChange, as the damage paths do. - CreatureThink treats State == Dead as a corpse as well as Health <= 0. Tests: CritDeathCorpseTests - a creature left to die in its window, one finished in it (RequestCritDeathFinish through to Finished), and a creature marked dead with health left on it. No schema change.
InfiniteRasa#112, InfiniteRasa#114 Merges upstream development 64eb4e8 into 427958a (merge base 39b5a40). Sixteen files conflicted. Mission 1449, Wilderness Targets of Opportunity, is defined on both sides: revision targets_1 here (TargetsOfOpportunitySeed) and wilderness_1_16_5 upstream (WildernessAliaOpeningV1). Game does not start with a mission enabled under two revisions. The upstream mission runs, with the fork's kill rules on its kill objectives 3, 4, 5 and 7: - a kill counts by creature flag (74, 77, 62, 68) in place of one creature row each (87, 85, 3, 88); - on the battlefield and its instances, shared with the squad within TargetsOfOpportunitySeed.SquadRadius; - with titles 365, 366, 363 and 369. - TargetsOfOpportunitySeed: Wilderness leaves Zones (14 remain) and becomes TargetsOfOpportunitySeed.Wilderness; a new database never gets the targets_1 rows of 1449. - WildernessTargetsKillRules and 20261116000000_Wilderness_targets_kill_rules (World, SQLite and MySQL): delete the targets_1 rows of 1449, change the four kill triggers, rewrite the mission's scene binding with titles, credit and map requirements added. Down restores the upstream rows. - 20261116000000_Wilderness_targets_kill_rules (Char): delete assignments and offers of 1449 held under targets_1. Titles are not touched. Conflicts: - AbilityManager: upstream's action.Completed guard, then the fork's interrupted / dead-unless-self-revive check. - CreatureManager.HandleCreatureKill: the fork's RecordKill and KillEvents; outside a scene, upstream's Scenes.PublicActorDied runs before kill credit. - MissileManager: upstream's earlier damage-immunity check and HasCombatAuthorization return; the fork's Pvp.Restrained block and GameEffectManager.ApplyRangedDamage. - MissionContentValidator: upstream's object packages and typed scenarios plus the fork's creature flag ids in the reference set. - MissionProgressRuleAuthoring: a multi-trigger rule is a kill counter (fork), a scoped scene set (upstream) or a distinct set. Upstream's TryBuildCreatureCounterSet is dropped; the kill counter covers it. - MissionApplication: upstream's SourceCreatureId filter and DialogueRequirement inside the fork's OpenTopics; ObjectiveContacts records source creatures as speakers. - MissionPublicationPlans, MissionInfo, Mission, MissionObjectiveDefinition, MissionProgressRule, MissionRequirements: both sides' members kept (CategoryId and ClientCategoryId; aggregation fields and TitleId; AssignmentItems and MapContextId). - PersistenceIntegrationTests, BootcampWorldContentTests, MissionMigrationTests, MissionSharingTests: upstream's revision-filtered assertions, with the fourteen battlefield missions expected. Changes outside the conflicts: - MissionRuntime.SelectCandidates: upstream's batch summing for a counter with several subjects applies to CreatureKilled only. A kill is reported once per creature flag of its class and was counted once per flag. - MissionContacts.Wanted: holds the speaker creatures of source-creature dialogue topics. - MissionRequirementFactsAdapter: MapContextId passed by name; upstream added AssignmentItems before it. - MissionApplication.HasProgressCandidate: passes the progress list to SelectHistoryAggregates. - KillTitlesTests: the worked example is Divide (1582); 14 missions and 59 kill objectives in the seed, 63 with the Wilderness four. - WildernessTargetsOfOpportunityTests: kills reported by creature flag. - WildernessTargetsKillRulesTests: new. Upstream tests held to the fork's state: - WildernessMigrationTests, WildernessCoverageTests: the Wilderness migrations are followed by the fork's (Add_control_points to Wilderness_targets_kill_rules), and 20261104000000_Add_wreck_creatures shares their date. Enabled definitions are 69 plus the fourteen battlefields; the last applied migration is the latest one; each Wilderness target model is compared with the last Wilderness one, which the snapshot must contain; evidence rows leave out revision targets_1. - WildernessSmugglerBranchTests, WildernessRanjaGorgeTests: the refused medpack request names a level no pack item performs. Those rewards are two medpacks of different levels, and AbilityManager.PackItemPerforming carries out a request for the other one's level with the other medpack.
… fire damages them Cache of the Day's fuel containers (mission 665, class 9260, 100 hit points) took damage with their bar staying full, and took none from a chaingun. The bar: SceneApplication.PublishObjectDamage sent DamageInfo for every hit. usable.py's Recv_DamageInfo stores the hit points and posts no event; overheadwindow.py redraws a usable's bar on USABLE_HITPOINT_CHANGE, which only Recv_UpdateHitPoints posts. It now sends UpdateHitPoints with what is left, and DamageInfo (canBeDamaged false) only once the object is destroyed or disabled - after the update, since an UpdateHitPoints of a figure the client already holds posts nothing. Constant fire: ConstantFire.Pulse resolved its target with EntityManager.GetActor, so an object (DynamicObjectType.PracticeDummy, with or without MissionDestruction) was never hit by the machine guns, the leech gun, the polarity gun or the propellant gun. PulseAtObject adds the object the shooter targets to the tick and passes the pulse's damage to PracticeTargetManager.RecordHit, as MissileManager.MissileTrigger does for a missile: no critical hit, range falloff or resistance. Within MissileManager.MaxTargetDistance (now internal), or the cone's range for the propellant gun. Tests: WildernessLandingZoneTests .DamagedFuelContainerTellsItsHitPointsWithUpdateHitPoints and .ConstantFireWeaponDamagesAndDestroysAFuelContainer.
…or name The client's map and radar name a mission marker by the indicator id sent with it (missionobjectiveindicatorlanguage; mapwindow.py and radarwindow.py, OnMissionMarkerHighlighted), and with None in its place by the objective's own name. The Wilderness missions' mission_indicator rows are keyed 1, 2, 3.. within each mission, and MissionInfo sent the key as that id: Mortar By Numbers' four mortars read "Possible Food Crate Location", "Warnet Valley", "Calla Ferns" and "AFS Hydro-electric Plant", and the first indicator of every Wilderness mission "Bane Base". All 97 Wilderness rows were named this way. Bootcamp's 12 rows are keyed by the client's ids (430..439) and were right. - MissionIndicator.UnnamedFrom (1000000000): an indicator id from there up has no client name. ClientNameId is the id below it and null from it on, and MissionInfo.Write writes that, None for null. - 20261111000000_Wilderness_indicators_without_client_names (Sqlite and MySql, data only; WildernessUnnamedIndicatorsV1): the rows of revision wilderness_1_16_5 get UnnamedFrom added to indicator_id. No mission_action of the revision refers to an indicator. Down subtracts it. Tests: MissionTrackerTests (None on the wire for an unnamed indicator; the id range), WildernessLandingZoneTests (Mortar By Numbers' MissionGained), WildernessMigrationTests (all 97 rows unnamed and the 12 Bootcamp rows named; back to WildernessEvidenceCapacity and forward; AssertMergedWorld and the pending-migration count no longer take WildernessEvidenceCapacity to be the last World migration), BootcampGearingUpInteractionTests (the crate's indicator keeps 433).
The greeting an NPC is sent with was the same for every NPC ("Greetings.",
npcgreetinglanguage 93). It is now world data: table npc_greeting holds a
greeting id by creature row, and an NPC with no row says the default.
- An NPC's own line heads its topic list in place of the default.
- An NPC with a line of its own and nothing else to talk about has the
client's greeting status (CONVO_STATUS_GREETING, 11), which offers the
converse action, and answers with the greeting alone; the client shows it
in its conversation window. An NPC with no line of its own is unchanged:
status None, not conversable. A mission, shop, trainer, auctioneer or
clan registrar status still comes first.
- 82 NPCs are seeded with the line the client's own text ties to them
(NpcGreetingSeed): the speaker names themselves, the line is part of that
NPC's mission dialogue, the speaker says where they stand, or says what
they do. The client has no table linking a line to an NPC.
- GM command .greeting: shows the targeted NPC's line, sets it
(.greeting <id>), clears it (.greeting clear), or shows any line on the
game master's own client (.greeting show <id>). A greeting id the client
does not have is refused. docs/gm-commands.md is updated.
Database: world migration 20261111000000_Add_npc_greetings creates
npc_greeting (id, greeting_id) and inserts the 82 rows.
… no greetings CreatureManager.CreatureInit reads every NPC's greeting from npc_greeting, the table of 20261111000000_Add_npc_greetings. WildernessRuntimeTestHarness can stop its World at an earlier migration (targetWorldMigration), where the table does not exist, and starts Game on it. Ten tests failed there with "SQLite Error 1: 'no such table: npc_greeting'": WildernessMigrationTests: FirstHubMigrationsEnableExactlyBootcampAndTheFourOpeningMissions AliaBranchDataDownRemovesOnlyItsOwnDependencyGraph WaveBProvidersActivateInOrderWithoutChangingTheWaveABoundary EvidenceCapacityPreservesFreshW1UpgradesAndEveryAuthoredNoteAcrossRollback ForwardCapacityMigrationPreservesAnAlreadyAppliedLateSchema GeneratedSqliteUpgradeAndRollbackScriptsPreserveEvidenceThroughTableRebuilds WildernessRewardReconstructionTests: ForwardRewardCorrectionChangesOnlyTheEightOwnedTemplateRows RewardCorrectionDownRestoresExactKeysWithoutRewritingOtherMedpacks WildernessProgressionAcceptanceTests: WaveAProviderBoundaryEnablesExactlyItsNativeOutdoorScopeAndBootcamp ForwardWorldUpgradePreservesTheActiveTargetsOfOpportunityAttemptAndCharacterState The harness's World unit of work now gives its creature repository as CreaturesOfThisWorld: GetNpcGreetings returns no rows while sqlite_master has no npc_greeting table, and the table's rows once it has. Nothing outside the test project changes; a server's World is migrated to the end and always has the table. NpcGreetingTests has the case: a World stopped at WildernessAliaOpening starts with no greetings and Outpost Commander Rogers says the default; migrated to the end it has the 82 seed rows, and a harness on a migrated World loads Rogers with 1620.
WildernessRuntimeTestHarness read adv_foreas_concordia_wilderness.nav (15 MB) for every harness, and each one stayed reachable after Dispose: about 50 MB of managed memory a test. After 14 harnesses the test host held 724 MB after a full collection, and a run of 136 Wilderness tests in one process reached 5 GB and threw OutOfMemoryException. The file is read once (WildernessNavMesh) and each harness has its own NavMeshQuery over the same mesh; a NavMeshQuery is not thread safe and is not shared. After 14 harnesses the host holds 74 MB.
…ld falls Pierre (pool 630070) stands in the Bane cache of Cache of the Day as an ordinary FRIENDLY creature with no attack: 7 m from the Atropos Linker of pool 580012 and inside the post of pool 580005. Hostile creatures seek any friendly creature in their scan, so the garrison fought her as soon as she spawned, with nobody on the mission. - Hold_captive_pierre (20261117000000, MySqlWorld and SqliteWorld; data only) makes mission 666's public encounter a ManualCombat one: one update of its mission_scene_binding row (WildernessHeldCaptiveV1), nothing else of the scene changed, and Down writes the seeded binding back. WildernessLandingZoneV1.EscapeVelocityScene(held) builds both documents. A ManualCombat actor has the scripted combat gate from its spawn (PublicActorLeaseService.BindCombatGate): CanParticipateInCombat is false, so it is not sought (BehaviorManager's scan, IsHostileTarget, MayFight) and takes no damage (IsInvulnerable, DamageImmunity) - with nobody on the mission, and with its owner still at the forcefield. - SceneWorldAdapter: a RunRouteIntent with ResumeAfterCombat authorizes the actor's combat under the route's operation key (PublicActorLeaseService.AuthorizeCombat) before the route starts, and revokes it if the route does not start. An actor with no gate is unaffected. Both of Pierre's routes resume after combat, so she is in combat from the forcefield's fall; her death on the way fails the escort as before, and a lease that resets (BeginReset) holds her again. - HandleCreatureKill does nothing to an invulnerable creature, so a captive Pierre can no longer be killed: the scene's failure on her death while captive is no longer reachable that way. Tests: - WildernessEscapeVelocityTests: the Linker of 580012 fights her on the data before the migration and leaves her alone after it; PierreCannotBeKilledWhileStillCaptive replaces PierreDeathWhileStillCaptiveFailsTheAttemptAndReleasesHerReplacement; she is in combat on the escort route and her replacement is held again. - WildernessMigrationTests: the migration changes nothing but the encounter's manualCombat, and rolls back to the seeded scene. docs/wilderness-missions.md records the reconstruction.
The client sends ('LeaveClan', (member.characterId, member.clanId)) for its
own line of the roster (client/clan.py SendLeaveClan, from /clanleave and the
clan window). That characterId is the value the server sent the line with,
which ClanMemberData.Write writes with WriteULong: a Python long, and in a
member's own line their entity id. LeaveClanPacket read it with ReadUInt,
which throws InvalidDataException ("Expected 0x1_. Got: 2F") on a long, and a
throw out of the decode closes the connection. No member could leave a clan;
each attempt disconnected them and the membership stayed.
LeaveClanPacket.CharacterId is a ulong, read as a long or an int by the type
that arrives. ClanManager.LeaveClan is unchanged: it never used that id, and
the one who leaves is the one who sent the request.
ClanLeaveTests: the member's own line goes out with a long and the same two
values decode as LeaveClan; longs of any size and an int are read; a member
sending it as the client does is out of the clan, with PlayerLeftClan to them
under their entity id and to the others under their character id; another
member's id in the request does not make that member leave.
…the same things twice Three things an abandoned mission left behind, reported on Gearing Up for Battle (1992) and Capture the Flag (1994); the client lets both be abandoned (gameconstants NON_ABANDONABLE_MISSIONS is 1990, 2010, 2011). - TryAbandon published MissionDiscarded and nothing else. The giver's NPCConversationStatus stayed as the client last had it, so the mission could not be taken again until the NPC left the player's sight and came back, or the player logged in again. TryAbandon (both ways out) and TryClear now end with RefreshNpcConversationStatuses, as acceptance and reward do. - The equipment crate's rows are claimed as character_mission_scenario_step rows, which go with the assignment's row. In the same session the crate kept its emptied dispenser, and a second attempt's crate objective could not be completed; after a login the crate was rebuilt full and handed the six items out again. Archive already keeps an ended assignment's steps as "legacy:" receipts under its assignment id. AttachRewardLoot and the claim now count a row as taken when the current attempt or an abandoned or failed one since the last success took it (MissionRuntimeRepository.UnfinishedAttempts, HadStepInUnfinishedAttempt). A container with nothing left while its objective is open stays usable, and using it records the interaction that taking the last row records (LootDispenserManager.FinishEmptyRewardLoot, from DynamicObjectManager.FootlockerRecovery). - A scene's GrantRewardIntent is receipted per run at generation 0, and every assignment's scene is a new run: DeSimone's promotion (reward 59, 43000 experience) was paid on every attempt. SceneApplication.Commit skips the grant when the scene of an abandoned or failed attempt since the last success holds the receipt (GrantedInUnfinishedAttempt); the new run still records its own. No schema or data change: the evidence is rows already written. Tests: BootcampAbandonedAttemptTests, 8 (the giver's status after abandonment; an emptied crate and a part-emptied one on a retry, in the same session and after a reconnect, through a third attempt; the promotion over three attempts, the same two ways; UnfinishedAttempts and the two lookups against history rows on either side of a success).
…s them Three requests of the auction house window threw out of their decode, which closes the connection: - RequestCreateAuction (g_auctioneerId, itemId, price, duration) and RequestCancelAuction (g_auctioneerId, itemId): the item is the selected row's, int(itemWidget.GetID()) - a Python int. Both readers took it with ReadULong: "Expected 0x2_. Got: 1F". - RequestTakeItemFromInboxInventory (auctioneerId, entityId, destSlot): Receive and a right-click send an int id and None for the slot, Receive All a long id and None, a drag onto another tab's slot a long id and None. The reader took a long and an int: "Expected 0x2_. Got: 1F", "Expected 0x1_. Got: 00". Only a drag onto a slot of the item's own tab decoded. Nobody could list an item, cancel a listing, or collect with Receive, Receive All or a right-click. - PythonReader.ReadId reads an id as a long or an int by the type that arrives. The three packets read the auctioneer and the item with it. - RequestTakeItemFromInboxInventoryPacket.DestSlot is a uint?: null for None. - InventoryManager.RequestTakeItemFromInboxInventory: the item goes to the slot asked for when that is a free slot of the item's own tab, and otherwise to the first free slot of that tab (InboxDestination). With the tab full it stays in the inbox and the player is sent PmInventoryFull. An occupied slot, or one outside the item's tab, was refused or used as given; the client sends neither. AuctionHouseRequestTests: the payloads of each button decode; an item is listed and the listing cancelled from decoded requests; an inbox item taken with no slot goes to the first free slot of its tab, for an int and a long id; Receive All's three requests take three items; a full tab leaves the item in the inbox with the message; a free slot of the tab is used as asked; an occupied slot, another tab's and one past the pack are treated as no slot; an item that is not in the inbox is not taken.
This was referenced Oct 5, 2026
EllimistArcade
added a commit
to EllimistArcade/Rasa.NET
that referenced
this pull request
Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rounding out the last of the game functions, its all refinement from here lads!
Game Features:
Skill Related:
Security:
General Enhancements: