Adds the server side of the chat command list: a view plugin which
sends one ChatCommandInfo per available command, and the handler for
the request of a client which supports it.
Both are limited to the extended protocol, so classic clients don't
receive messages they can't understand. The commands come from
GetAvailableChatCommandInfos, so a player only learns about the ones
he may execute, and deactivated ones stay out.
Executing a command needs nothing new: the client sends the composed
command line as an ordinary chat message, which the chat message
processor already routes to the command without broadcasting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSpK6jkyF8ZS5nYXyGwYxA
(cherry picked from commit f3a9cb132c295863942bb0f06f40cde231e57907)
Adds the two message definitions which let a client ask for the chat
commands of its player and receive them in a machine readable form:
- ChatCommandListRequest (C1, F5/00), sent by the client after it
entered the game world.
- ChatCommandInfo (C2, F5/01), one message per available command. It
carries the command, its localized name and description, the required
character status and one entry per parameter with its name, short
name, kind and accepted values.
F5 is unused in both directions and in the client, and F3 - the other
candidate - is character scoped, so it doesn't fit.
The generated code was produced with the same transformations the
project uses, so the diff only adds the new types and leaves the
existing ones untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSpK6jkyF8ZS5nYXyGwYxA
(cherry picked from commit 2cd46516f6136cfc7f96f4d3b7db4c8329559d3a)
Money has no owner - unlike a dropped item it can be picked up by anyone
standing on it. Deriving the drop's shares from the killer's party made a
stranger's pick up pay nothing and leave the money on the ground, destroying
zen that was previously taken and split by the picking party.
The recorded shares now apply only when the party picking the money up is the
one which earned it; otherwise there is no experience of theirs to follow and
the money is split equally between the picking party, as it was before and as
share-less money (e.g. from an item box) already is.
Covered by DroppedMoneyTest.StrangerPartySplitsPickedUpMoneyEquallyAsync and
EarningPartyReceivesMoneyByExperienceShareAsync.
(cherry picked from commit ae7cad81da6893cfbd1c28bb29811b7f0a10d540)
AwardExperienceAsync returned 0 for a character at the maximum level which has
not completed the master quest: it gains neither normal nor master experience.
Since the money of a kill is now derived from those returned values, such a
member got no zen at all in a party, while the same character still earns zen
when killing solo - AddExpAfterKillAsync returns the calculated amount there,
regardless of whether it could be applied.
The normal experience is now always calculated and returned, and only awarded
when the character can still gain it.
(cherry picked from commit 7cdb34b7c58f5924427b97ea17f26c3fe0f37719)
The money amount of a monster drop is computed as "gained experience + 7", but
in a party it was neither derived from, nor distributed like, that experience.
DistributeExperienceAfterKillAsync returned a party total without the game rate
and the personal experience rates, while AddExpAfterKillAsync returned a value
which had them applied. Both landed in the same variable in OnDeathAsync, so the
party number was orders of magnitude smaller. That mismatch was worked around by
recalculating the killer's solo experience for money purposes, which pinned the
party pool to a solo-sized amount: with the pool then split by member count, a
party of three received about a third of the solo zen each, while its experience
pool grows with the party size.
AwardExperienceAsync already computes the per member experience with all rates
applied, it just discarded it. It now returns that value, the distribution
returns the per member breakdown, and the money is split proportionally to it.
The workaround is gone, the pool follows the experience, and each member's zen
matches their own level, their own rates and their own master/normal branch. The
units lost to the integer division are handed to the shares which were cut the
most, one each, so no member is systematically favoured over many kills.
Two defects around the money rate are fixed on the way:
- MoneyAmountRate was applied for the killer when the drop was created and again
for the receiver when a party picked it up, so a rate of 3.0 paid a party 9x.
On the "money straight into the inventory" path for a solo killer it was not
applied at all. It is now applied exactly once, for the receiver.
- ClampMoneyOnPickup was only honoured for solo pick ups. A party member at
MaximumInventoryMoney silently lost their share, because the drop was consumed
as soon as any other member could take one. The clamp now runs on the shared
payout path, after the money rate, so it clamps the amount actually credited.
Money without a per player breakdown, such as the fixed amount of an item box,
keeps being split equally. Shares of players who are no longer eligible are
redistributed among the remaining ones instead of being lost.
(cherry picked from commit 53f25aca08e690255571f2bb6c796bb7c10934a2)
A character which has participated in a mini game with enabled ranking
statistics could not be deleted. The database rejected it with
Npgsql.PostgresException 23503: update or delete on table "Character"
violates foreign key constraint "FK_MiniGameRankingEntry_Character_CharacterId"
MiniGameRankingEntry is a member of neither the character nor the mini game
definition aggregate, so the model generator doesn't emit a delete behavior
for its references. The entity was registered without any additional
configuration, so Entity Framework fell back to no action for these optional
relationships, which the database translates into a restricting foreign key.
Both references are now configured with a cascading delete, matching the
behavior of the other references to a character. With that, all seven
relationships pointing to Character delete their dependents.
This affects both deletion paths: the one of the player
(DeleteCharacterAction) and the one of an administrator, who can remove a
character from the account in the admin panel.
The cascade on the mini game definition is deliberate and part of the same
defect: deleting a definition in the admin panel ran into the very same
restriction. It does mean that the ranking history of a mini game is dropped
together with its definition.
Reported in issue 796.
(cherry picked from commit a391bdedbbd3a36ccb37124125bd7ede4f851dca)
Two TvT (Heykel Savasi) event fixes found during mobile+PC client work:
- PeriodicTaskBasePlugIn: a forced start (GM /starths) no longer blocked while a
previous run's TaskDuration window is still open. When an event finished early
(team won / empty / before the 20-min duration) players are warped out but the
periodic state stayed Started with a future NextRunUtc, so the guard returned on
every tick and a re-start only worked after the full duration or a server
restart. Force-finish the stale run when _isStartForced so the same execution
falls through and prepares a fresh one; the NextRunUtc wait now applies to
scheduled runs only.
- HeykelSavasiTeamSelectHandlerPlugIn: IsEncryptionExpected => false. The AdaMu
client sends client-to-server packets unencrypted (like walk / animation /
talk-to-npc), so the server was dropping the team-select (JOIN) packet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Kills now credited from OnDeathAsync (the AfterKilledPlayerAsync/PK path is skipped when
AllowPlayerKilling is true, so event kills never reached the scoreboard).
- Respawn after death now stays on the mini-game's OWN map instance (older-client path fetched the
shared world map of the same number -> respawned player saw no statues/participants).
- On game end: announce the winner + run a 60s ceremony broadcast loop sending phase-3 HUD countdown
and the final scoreboard until the Lorencia teleport.
- Statue HP overridden to 100k at spawn (MaximumHealthOverride) for the test phase - no DB change.
- Guards made fully stationary by overriding their definition's IntelligenceTypeName to
NullMonsterIntelligence at runtime (no wander, no chase); no DB change.
- Statue/guard in-game names rebranded to 'TvT Statue' / 'TvT Guard' (runtime designation override).
- Bosses: 306/309/357/459 spawn once at (55,42)/(32,42)/(31,59)/(56,58) after 3 total statues broken.
- Guards/bosses spawn in a walkable BOX (retry) instead of a fixed point; several statue-guard
offsets landed on non-walkable terrain and silently failed, leaving statues unbreakable.
- Guard-alive gate uses the ACTUAL spawned count so a statue can always reach 0 -> breakable.
- HeykelSavasiStartPlugIn.OnStartedAsync force-disposes a lingering finished context so GM /starths
restarts the event immediately (no server restart).
- Statue-break + join messages translated to English.
A forced start (e.g. /starths) was blocked for the whole TaskDuration after a
run even though the event had already ended, so re-starting needed a server
restart. ForceStart now skips the 'previous event still running' guard;
scheduled auto-starts still respect it.
SendOpenedNotificationsAsync only looped per full minute, so a <1min entrance
(e.g. a 30s test window) sent no golden 'event opened' notification. Now sends
one up front when TotalMinutes==0. >=1min windows unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instead of a fixed lane, each statue (and its 4 guards) now spawns at a random
walkable coordinate anywhere on the map; breaking one reveals the next at a new
random spot, so players must search for it. Falls back to the fixed positions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- HeykelSavasiContext.IsItemAllowedToEquip disallows wings/capes during
Playing only (so entry is never rejected for wearing wings); auto-unequips
any equipped wing/cape into a free inventory slot in OnGameStartAsync.
- New S2C packet HeykelSavasiHudState (C1, code FB, length 11) carrying
phase, team, counts, statue progress and remaining seconds, plus its view
plugin/interface, broadcast once per second across registration/prep/
battle and one-shot on join/statue-break/game-end.
#1: block all player-vs-player damage while State != Playing (registration/
prep), keep same-team friendly-fire off during battle. Prevents pre-battle
kills (which also caused deaths to route to Lorencia instead of a base).
#3: remove a player from their team on OnObjectRemovedFromMapAsync (real
leave to another map), not on same-map death-respawn, so team counts stay
accurate and re-joining works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetTeamSpawnGate matched gates by exact X1/Y1 anchors, which threw
'Sequence contains no matching element' when the DB gate coords drifted a
tile from the code anchors. Now selects the two spawn gates by Y-order
(red=top, blue=bottom) so it works regardless of exact coordinates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>