using AcDream.Core.Chat; namespace AcDream.Core.Tests.Chat; /// /// Tests for . The retail client showed /// these as plain-language strings; we mirror that via templated lookup. /// Filed after the 2026-04-25 live launch where the user saw cryptic /// "WeenieError 0x051B" in chat for what was actually a friendly login /// notification. /// public sealed class WeenieErrorMessagesTests { // ── known codes — informational, parameterised ─────────────────── [Fact] public void Format_YouHaveEnteredChannel_SubstitutesParam() { // 0x051B = WeenieErrorWithString.YouHaveEnteredThe_Channel. // Template "You have entered the _ channel." with `_` placeholder. Assert.Equal( "You have entered the General channel.", WeenieErrorMessages.Format(0x051B, "General")); } [Fact] public void Format_YouHaveEnteredChannel_WorksForEachChannelName() { Assert.Equal("You have entered the Trade channel.", WeenieErrorMessages.Format(0x051B, "Trade")); Assert.Equal("You have entered the LFG channel.", WeenieErrorMessages.Format(0x051B, "LFG")); Assert.Equal("You have entered the Roleplay channel.",WeenieErrorMessages.Format(0x051B, "Roleplay")); } [Fact] public void Format_YouHaveLeftChannel_SubstitutesParam() { Assert.Equal( "You have left the General channel.", WeenieErrorMessages.Format(0x051C, "General")); } // ── known codes — informational, no parameter ──────────────────── [Fact] public void Format_0x051D_FallsBackToHex_NoRetailCaseExists() { // Campaign CH slice CH2: the pre-CH2 "Turbine Chat is enabled." // text for 0x051D was an ACE-derived guess, never decomp-confirmed. // The full HandleFailureEvent port found NO case for 0x51D anywhere // in the switch (only 0x51C has one — case 0x51c: at raw line // 383115-383118 of acclient_2013_pseudo_c.txt) — retail's own // client simply has no display text for this id. Falling back to // the generic form is now the retail-faithful answer, not a gap. Assert.Equal( "WeenieError 0x051D", WeenieErrorMessages.Format(0x051D, param: null)); } // ── known codes — error-level ──────────────────────────────────── [Fact] public void Format_CharacterNotAvailable_NoParam() { // 0x052B fired by the server when a Tell target lookup fails // (e.g. the user typed "/t je, hello" → server got "je," → no // character). Should read like the retail message. Assert.Equal( "That person is not available now.", WeenieErrorMessages.Format(0x052B, param: null)); } [Fact] public void Format_TradeComplete() { Assert.Equal("Trade Complete!", WeenieErrorMessages.Format(0x0529, null)); } [Fact] public void Format_ThatIsNotAValidCommand() { // 0x0026 fires on /-prefixed text that ACE's command parser // can't resolve. Filed after a 2026-04-25 trace where /help // produced cryptic "WeenieError 0x0026" lines. Assert.Equal( "That is not a valid command.", WeenieErrorMessages.Format(0x0026, null)); } [Fact] public void Format_YouAreNotInAllegiance() { Assert.Equal( "You are not in an allegiance!", WeenieErrorMessages.Format(0x0414, null)); } [Fact] public void Format_YouDoNotBelongToAFellowship() { Assert.Equal( "You do not belong to a Fellowship.", WeenieErrorMessages.Format(0x050F, null)); } [Theory] [InlineData(0x0036u, "Action cancelled!")] [InlineData(0x003Du, "You charged too far!")] [InlineData(0x004Au, "Ack! You killed yourself!")] [InlineData(0x0550u, "Out of Range!")] public void Format_CombatMovementErrors(uint code, string expected) => Assert.Equal(expected, WeenieErrorMessages.Format(code, null)); // ── PK status codes ─────────────────────────────────────────────── [Fact] public void Format_YouAreNonPKAgain_ExactRetailText() { // 0x0504 = WeenieError.YouAreNonPKAgain. Filed after the user saw // "WeenieError 0x0504" on login following a PK Lite status // reversion. Retail: ClientCommunicationSystem::HandleFailureEvent // @0x00571990 case 0x504 @0x005745cd; string recovered byte-exact // from data_7d32c0 in the PDB-paired binary (the pseudo-C dump // truncates at the declared 0x5f-wchar16 array bound, mid-sentence // at "...protection of the Lig"). Assert.Equal( "You are enveloped in a feeling of warmth as you are brought back into the protection of the Light. You are once again a Non-Player Killer.", WeenieErrorMessages.Format(0x0504, null)); } [Fact] public void Format_YoureTooCloseToYourSanctuary() { // 0x0505, case 0x505 @0x00574c65, data_7d2640. Assert.Equal( "You're too close to your sanctuary!", WeenieErrorMessages.Format(0x0505, null)); } [Fact] public void Format_CannotChangePKStatusWhileRecovering() { // 0x04EC, case 0x4ec @0x0057446f, data_7d3820. Assert.Equal( "You cannot modify your player killer status while you are recovering from a PK death.", WeenieErrorMessages.Format(0x04EC, null)); } [Fact] public void Format_AdvocatesCannotChangePKStatus() { // 0x04ED, case 0x4ed @0x005744a1, data_7d37b0. Assert.Equal( "Advocates may not change their player killer status!", WeenieErrorMessages.Format(0x04ED, null)); } [Fact] public void Format_LevelTooLowToChangePKStatus_NowResolvedByCH2() { // 0x04EE (LevelTooLowToChangePKStatusWithObject) sits right next to // the PK-status codes above; the pre-CH2 test asserted the // fallback because only a curated ~60-entry subset was ported then. // Campaign CH slice CH2's full 338-row HandleFailureEvent port // (Appendix A row 0x4EE, Type 0x00) resolves it for real. Assert.Equal( "Your level is too low to change your player killer status with this object.", WeenieErrorMessages.Format(0x04EE, null)); } // ── unknown codes — graceful fallback preserves debug info ─────── [Fact] public void Format_UnknownCode_NoParam_FallsBackToHexForm() { Assert.Equal("WeenieError 0xABCD", WeenieErrorMessages.Format(0xABCD, null)); } [Fact] public void Format_UnknownCode_WithParam_FallsBackToColonForm() { Assert.Equal( "WeenieError 0xDEAD: Mana Stone", WeenieErrorMessages.Format(0xDEAD, "Mana Stone")); } [Fact] public void Format_UnknownCode_EmptyParam_StaysAsHexOnly() { // Empty string param shouldn't add a stray colon. Assert.Equal("WeenieError 0xCAFE", WeenieErrorMessages.Format(0xCAFE, "")); } // ── parameterised templates with non-trivial params ────────────── [Fact] public void Format_HearListAdded_SubstitutesParam() { Assert.Equal( "Caith has been added to the list of people you can hear.", WeenieErrorMessages.Format(0x0521, "Caith")); } [Fact] public void Format_0x004F_FallsBackToHex_NoRetailCaseExists() { // Campaign CH slice CH2: the pre-CH2 "You fail to affect _ because // they cannot be harmed!" text for 0x004F was an ACE-derived guess. // Direct decomp verification (grepping every "case 0x4f:" in // ClientCommunicationSystem::HandleFailureEvent's whole body) found // NONE — only 0x4E, 0x50, 0x51, 0x52, 0x53, 0x54 have cases; 0x4F is // skipped entirely, same as the many other gaps in that switch's // sparse jump table. Retail has no display text for this id. Assert.Equal( "WeenieError 0x004F: Drudge", WeenieErrorMessages.Format(0x004F, "Drudge")); } [Fact] public void Format_HealingTargetAlreadyFull_SubstitutesParam() { Assert.Equal( "+Acdream is already at full health!", WeenieErrorMessages.Format(0x04FF, "+Acdream")); } // ── Campaign CH slice CH2: the full HandleFailureEvent table port ──── /// /// Pins the table's size: 338 rows (Appendix A's 339 minus the one /// deliberately-excluded 0x4F8, see the class doc comment on /// ). A change to this number without /// a matching research/commit citation is a red flag, not a routine /// edit. /// [Fact] public void Resolve_FullTable_HasExactly338Rows() { int count = 0; for (uint id = 0; id <= 0x600u; id++) { var (text, _) = WeenieErrorMessages.Resolve(id, null); if (!text.StartsWith("WeenieError 0x", StringComparison.Ordinal)) count++; } Assert.Equal(338, count); } [Fact] public void Resolve_0x4F8_IsDeliberatelyExcluded_FallsBackToHex() { // See the class doc comment: 0x4F8's case body is a tangled // multi-operator+ decompiler artifact that could not be resolved // with confidence — excluded rather than guessed. var (text, type) = WeenieErrorMessages.Resolve(0x4F8, "Someone"); Assert.Equal("WeenieError 0x04F8: Someone", text); Assert.Equal(RetailLogTextType.Default, type); } // ── spot pins across all three retail routing destinations ────────── [Theory] // ClientLocal (0x1A) — the SpewBox destination. [InlineData(0x017u, "You failed to go to non-combat mode.", RetailLogTextType.ClientLocal)] [InlineData(0x02Au, "You are too encumbered to carry that!", RetailLogTextType.ClientLocal)] [InlineData(0x04EBu, "You can't do that while in the air!", RetailLogTextType.ClientLocal)] [InlineData(0x550u, "Out of Range!", RetailLogTextType.ClientLocal)] // Magic (0x07) — the light-blue spell/portal-failure channel. [InlineData(0x402u, "Your spell fizzled.", RetailLogTextType.Magic)] [InlineData(0x49Bu, "You fail to link with the lifestone!", RetailLogTextType.Magic)] [InlineData(0x593u, "Olthoi characters can only use Lifestone and PK Arena recalls!", RetailLogTextType.Magic)] // Default (0x00) — the ordinary broadcast/green channel. [InlineData(0x4A, "Ack! You killed yourself!", RetailLogTextType.Default)] [InlineData(0x50Cu, "%s is now a closed fellowship.", RetailLogTextType.Default)] [InlineData(0x55Fu, "Only Player Killer characters may use this command!", RetailLogTextType.Default)] public void Resolve_SpotPins_TextAndTypeMatchAppendixA(uint id, string expectedTemplate, RetailLogTextType expectedType) { var (text, type) = WeenieErrorMessages.Resolve(id, param: null); Assert.Equal(expectedTemplate, text); Assert.Equal(expectedType, type); } [Fact] public void Resolve_JumpFamily_SharesClientTextRefusalsConstantsVerbatim() { // HandleFailureEvent's 0x24/0x48/0x49 cases reuse the SAME string // globals as the local jump-refusal sites — assert byte-identity, // not just similar wording. Assert.Equal(ClientTextRefusals.CantJumpInAir, WeenieErrorMessages.Resolve(0x024u, null).Text); Assert.Equal(ClientTextRefusals.CantJumpPosition, WeenieErrorMessages.Resolve(0x048u, null).Text); Assert.Equal(ClientTextRefusals.CantJumpLoad, WeenieErrorMessages.Resolve(0x049u, null).Text); Assert.Equal(RetailLogTextType.ClientLocal, WeenieErrorMessages.Resolve(0x024u, null).Type); Assert.Equal(RetailLogTextType.ClientLocal, WeenieErrorMessages.Resolve(0x048u, null).Type); Assert.Equal(RetailLogTextType.ClientLocal, WeenieErrorMessages.Resolve(0x049u, null).Type); } [Fact] public void Resolve_0x4F4_PreservesRetailDollarSTypo() { // Retail's own literal is "...because $s cannot affect anyone!" — // a genuine retail typo (should have been %s). Only the FIRST %s // substitutes; the literal "$s" must NOT be replaced. var (text, type) = WeenieErrorMessages.Resolve(0x4F4u, "A drudge"); Assert.Equal("A drudge fails to affect you because $s cannot affect anyone!", text); Assert.Equal(RetailLogTextType.Magic, type); } }