Prevent duplicate map entry on repeated F3 12 packet
Guards ClientReadyAfterMapChangeAsync against re-entry when CurrentMap is
already set. A repeated client-ready packet previously re-ran the handler,
adding the player and its summon to the area of interest a second time.
GameMap.AddAsync partially dedupes (no id regeneration, no double player
count), but calls AddObjectAsync unconditionally, and Bucket<T> neither
dedupes on add nor removes more than the first occurrence on remove, so
the duplicate entry outlived the player on the map.
Includes ClientReadyAfterMapChangeTests covering both halves of the
invariant: the player is added exactly once across two calls, and no entry
survives removal afterwards.
Periodic events (invasions, Blood Castle, Devil Square, Chaos Castle,
Happy Hour) match their timetable against DateTime.UtcNow, so the times
of day are effectively interpreted as UTC. There was no concept of a
server time zone, which forces admins to enter UTC times and makes the
schedule drift with daylight saving time.
This adds a SystemConfiguration.TimeZoneId (nullable string), resolved
once at startup and used by the scheduler to interpret the timetable.
When empty or unresolvable it falls back to UTC, so existing servers
keep their current behavior without any action.
- DataModel: SystemConfiguration.TimeZoneId + resources and an EF
migration for the new nullable column.
- GameLogic: IGameContext.ServerTimeZone; PeriodicTaskConfiguration
.IsItTimeToStart now interprets the timetable in that zone.
- Startup: resolve TimeZoneId (IANA or Windows id via ICU) with a UTC
fallback and a warning, and flow it into each game server context.
- Admin Panel: the System page gets a "Server time zone" field which
suggests the available system time zones (normalized to IANA) and
validates the entered id, while still allowing a free-text value.
Every Player subclass logged under the same MUnique.OpenMU.GameLogic.Player
category, because the logger was created with CreateLogger<Player>()
regardless of the runtime type. Bots are Players, so their output was
indistinguishable from a real player's and no log configuration could
silence one without the other.
Measured with 500 bots on a Season 6 server: 592 log lines per minute
right after startup and 293 in the steady state, and every single one of
them under MUnique.OpenMU.GameLogic.Player. BotNavigator alone accounted
for 74.6% of the volume, its hunting-ground line for 58.7%.
RuntimeCategoryLogger resolves the category from the owner's runtime type.
BotPlayer installs it for itself in its constructor, so bots now log under
MUnique.OpenMU.GameLogic.Bots.BotPlayer while real players - connected and
their offline MU Helper alike - keep the base Player logger and its
unchanged MUnique.OpenMU.GameLogic.Player category. Serilog's prefix-based
MinimumLevel.Override can now silence the bots on their own
(...GameLogic.Bots=Warning) or raise just them to Debug for a closer look.
On top of that, the 33 bot log entries which fire on a recurring per-bot
basis move from Information to Debug, so the default configuration stays
quiet: walking to a hunting ground, shopping trips, party churn, equipment
and progression milestones. The eight operator-facing events keep their
Information level - start, stop, purge, reset, account generation and the
population summary - as do all warnings and errors.
The same 500 bots now produce one log line per five minutes.
The comment implied CurrentMap is always null when this handler runs. The
IRespawnAfterDeathPlugIn branch of RespawnAtAsync is the exception: it assigns
CurrentMap and adds the player itself, so a trailing packet is redundant there
as well rather than merely duplicated.
A duplicate F3 12 is client behaviour the server cannot otherwise observe, so
it is now logged as a warning, matching how suspicious client input is handled
elsewhere in this file.
The tests cover both halves of the invariant: the player is only added to the
area of interest once, and no ghost entry survives removal from the map. Both
fail without the guard.
A repeated 'client ready after map change' (F3 12) packet re-runs
ClientReadyAfterMapChangeAsync, which adds the player and its summon to the
current map a second time.
During a regular map change CurrentMap is set to null and only reassigned in
this handler, so a non-null CurrentMap on entry means the handler already
completed for the current map change. Return early in that case.
Moves four cohesive subsystems into components which are owned by the
player, following the pattern of Walker, MagicEffectsList and
ObserverToWorldViewAdapter:
- PlayerMovement owns the walker, the move lock, the speed calculation
and the validation of the walk requests of the client.
- PlayerPersistence owns the persistence lock, its reentrancy tracking
and the progress save, including the documented lock invariants.
- PlayerSummon owns the summoned monster and its map handling.
- PlayerStorages owns inventory, shop, vault, temporary and backup
storage, their creation and the restore of the temporary storage.
The public members of the player stay as they are and delegate, so no
call site changes. TemporaryStorage has no setter anymore, it was
private before and is only written by the storages component now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5grV5oveZNhhjK1mazM2B
The declared ChangingMap state was never used and unreachable, so a warp
raised no state change at all: ClientReadyAfterMapChangeAsync advances to
EnteredWorld, which is not a possible transition of EnteredWorld itself,
so TryAdvanceToAsync just returned false.
WarpToAsync and RespawnAtAsync now advance to ChangingMap while the
client loads the map, which makes map changes observable through
IPlayerStateChangedPlugIn and cancelable through
IPlayerStateChangingPlugIn. As a consequence, the state guards of the
player actions reject actions while a map is loading.
The logout back to the character selection advances to Authenticated,
which was in no in-game state's possible transitions - it failed
silently and left the player in EnteredWorld without a selected
character. It only worked because the character list request accepts
EnteredWorld too; from an opened NPC dialog or from the dead state, the
player could not get back to the character selection at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5grV5oveZNhhjK1mazM2B
First step of the refactoring plan: move logic which does not need any
private state of the player into extension classes of the same
namespace, so all call sites stay unchanged, and move the three nested
classes into their own files.
Moved to extension classes:
- messages and localization (PlayerMessageExtensions)
- money and vault money (PlayerMoneyExtensions)
- item requirements, item destruction and invalid item logging
(PlayerItemExtensions)
- self defense queries, next to the state they read (SelfDefenseExtensions)
- the magic effect power-up creation (MagicEffectPowerUpExtensions)
- the invisibility effect (PlayerInvisibilityExtensions)
Moved to their own files: PlayerAppearanceData (formerly the nested
AppearanceDataAdapter), GameMasterMagicEffectDefinition and
TemporaryItemStorage.
TryAddMoney loses its virtual modifier - no type overrides it.
No behavior change: Player.cs shrinks by 486 lines and the only
additions to it are the two renamed type references.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5grV5oveZNhhjK1mazM2B
Admins may deactivate any extracted plugin, so no "not deactivatable"
marker is introduced and the plan extracts more generously. Existing
plugin interfaces get async signatures instead of async sibling points;
IAttackableMovedPlugIn stays synchronous because it is raised from
property setters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5grV5oveZNhhjK1mazM2B
Instead of adding new plugin points for entering the world and changing
the map, the plan now uses the existing IPlayerStateChangedPlugIn and
IPlayerStateChangingPlugIn. Adds the state machine repair needed for
that: the declared ChangingMap state is currently unreachable, so warps
raise no state change at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5grV5oveZNhhjK1mazM2B
Documents how the responsibilities currently bundled in Player can be
extracted: new plugin points for game rules, component objects for
stateful subsystems, extension methods for pure helpers, plus the
infrastructure gaps (plugin ordering, per-player plugin state) that need
to be closed first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5grV5oveZNhhjK1mazM2B
Replace public monster-number constants with static read-only properties and update the corresponding switch matching. Remove the redundant namespace import while preserving the thread-safe Castle Siege player tracker.
Make Guardian Statue regeneration atomic, restore closed gates during mid-battle startup, and truncate movement at blocked terrain. Optimize Castle Siege player queries and broadcasts, configure structure repair costs, harden overlapping gate terrain restoration, centralize upgrade lookups, and add regression coverage.
Replace public NPC number constants with static read-only properties and update the dependent matching logic.
Make Crown and switch timer callbacks task-returning and document the intentional no-op path for unspawned non-life upgrades.
Resolve alliance registrations directly through persistent alliance-master identities, including when the master has no online members.
Preserve customized Sign of Lord settings, tolerate configurations without the default item, document persistence-before-consumption semantics, and extend regression coverage.
Rebase the registration flow on the merged Castle Siege packet definitions and address the review findings.
Preserve Sign of Lord items until mark persistence succeeds, expose the complete client result codes, replace guild-table scans with direct guild identity lookup, and make Sign of Lord registration configurable for new and existing databases. Add packet safeguards, plug-in metadata, documentation, migrations, and regression coverage.
Add client-verified Castle Siege packet enums, clarify tax value semantics, and normalize packet naming.
Mark unverified result semantics accordingly, regenerate packet APIs and documentation, and extend conformance tests for packed MuMain layouts and enum values.
Add MuMain-compatible Castle Siege server-to-client packet definitions, correct gate-operation semantics, and define the Alliance removal guild name as a fixed eight-byte field. Regenerate packet structs, extensions, tests, and documentation, and add conformance tests covering sizes, offsets, endianness, and string widths.
Preserve persisted NPC states when the runtime snapshot is incomplete, update NPC rows in place, and separate loading from default-state creation. Add regression coverage for missing data, in-place updates, stale NPC removal, and incomplete snapshots.