diff --git a/src/backends/lod114d/game/GameHelpers.cpp b/src/backends/lod114d/game/GameHelpers.cpp index 0756754..b3f599d 100644 --- a/src/backends/lod114d/game/GameHelpers.cpp +++ b/src/backends/lod114d/game/GameHelpers.cpp @@ -311,7 +311,7 @@ uint32_t GetWeaponSwitch() { } GameType GetGameType() { - return static_cast(*d2client::gbExpCharFlag); + return HasFlag(GetCharFlags(), CharFlag::Expansion) ? GameType::Expansion : GameType::Classic; } uint32_t GetMercReviveCost() { @@ -348,19 +348,9 @@ std::string GetRealmShort() { return data != nullptr ? ToString(data->szRealmName2) : std::string{}; } -Difficulty GetMaxDiff() { - auto* data = *d2launch::gpBnetData; - return data != nullptr ? static_cast(data->nMaxDiff) : Difficulty::Normal; -} - CharFlag GetCharFlags() { auto* data = *d2launch::gpBnetData; - return data != nullptr ? static_cast(data->nCharFlags) : CharFlag{}; -} - -std::optional IsLadder() { - auto* data = *d2launch::gpBnetData; - return data != nullptr ? std::optional{data->nLadderFlag} : std::nullopt; + return data != nullptr ? data->nCharFlags : CharFlag{}; } // === GameStructInfo Queries === diff --git a/src/backends/lod114d/hooks/Intercepts.cpp b/src/backends/lod114d/hooks/Intercepts.cpp index 76d7eee..51b8e3e 100644 --- a/src/backends/lod114d/hooks/Intercepts.cpp +++ b/src/backends/lod114d/hooks/Intercepts.cpp @@ -58,6 +58,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,7 @@ #include #include #include +#include #include "game/GameCallbacks.h" #include "game/GameHelpers.h" @@ -700,10 +702,11 @@ extern "C" D2UnitStrc* OnGetSelectedUnit() { } // P9: CongratsScreen. Original CALL/5 at 0x44EBEF -> CongratsScreen_I. -// Bumps `nMaxDiff` to 10 (any value high enough to enable the next -// difficulty selector) after the Baal credits roll on any non-Hell -// difficulty, so Nightmare unlocks after Normal-Baal and Hell unlocks -// after Nightmare-Baal. +// Bumps the character flags' progression (CharFlag::ProgressionMask, the acts +// completed across all difficulties; the byte the reference calls nMaxDiff) to +// 10 - any value high enough to enable the next difficulty selector - after +// the Baal credits roll on any non-Hell difficulty, so Nightmare unlocks after +// Normal-Baal and Hell unlocks after Nightmare-Baal. // // Reference d2bs (D2Handlers.cpp:436) fired this only on Nightmare: // if (D2CLIENT_GetDifficulty() == 1) { ... nMaxDiff = 10; } @@ -717,7 +720,10 @@ extern "C" void SetMaxDiff() { if (game::GetDifficulty() < game::Difficulty::Hell && *imports::d2client::gbExpCharFlag != 0) { auto* pData = *imports::d2launch::gpBnetData; if (pData != nullptr) { - pData->nMaxDiff = 10; + constexpr uint16_t UNLOCKED_PROGRESSION = 10; + constexpr auto PROGRESSION_SHIFT = std::countr_zero(std::to_underlying(game::CharFlag::ProgressionMask)); + pData->nCharFlags = (pData->nCharFlags & ~game::CharFlag::ProgressionMask) | + static_cast(UNLOCKED_PROGRESSION << PROGRESSION_SHIFT); } } } diff --git a/src/backends/lod114d/imports/extras/BnetData.h b/src/backends/lod114d/imports/extras/BnetData.h index 93f9f50..e80965d 100644 --- a/src/backends/lod114d/imports/extras/BnetData.h +++ b/src/backends/lod114d/imports/extras/BnetData.h @@ -1,8 +1,11 @@ #pragma once #include +#include #include +#include "game/Constants.h" + // Battle.net account state - d2bs-internal struct. D2MOO does not model BNCS. // Field layout reverse-engineered against 1.14d; size verified by static_assert. // NOLINTBEGIN(readability-identifier-naming) - struct fields match binary layout @@ -27,8 +30,7 @@ struct BnetData { std::array szRealmName; // 0xD5 std::array _8; // 0xDD uint8_t nCharClass; // 0x1EE - uint8_t nCharFlags; // 0x1EF - uint8_t nMaxDiff; // 0x1F0 + game::CharFlag nCharFlags; // 0x1EF word, SelectedCharBnetSingleTcpIp 0x434A5A std::array _9; // 0x1F1 uint8_t nCreatedGameDifficulty; // 0x210 void* _10; // 0x211 @@ -47,6 +49,7 @@ struct BnetData { }; static_assert(sizeof(BnetData) == 0x3C6, "BnetData must be 0x3C6 bytes"); +static_assert(offsetof(BnetData, nCharFlags) == 0x1EF); #pragma pack(pop) diff --git a/src/contract/ContractEnumNames.h b/src/contract/ContractEnumNames.h index b8a5d8a..ac032b3 100644 --- a/src/contract/ContractEnumNames.h +++ b/src/contract/ContractEnumNames.h @@ -46,7 +46,7 @@ enum class MessageLevel : uint8_t; namespace d2bs::game { -enum class CharFlag : uint32_t; +enum class CharFlag : uint16_t; enum class OutOfGameLocation : uint32_t; enum class ClickButton : uint8_t; enum class Hand : uint8_t; diff --git a/src/contract/game/Constants.h b/src/contract/game/Constants.h index 77c16a7..57618a9 100644 --- a/src/contract/game/Constants.h +++ b/src/contract/game/Constants.h @@ -29,11 +29,11 @@ constexpr uint32_t MAX_WAYPOINT_ID = 40; // RosterUnit::wPartyId sentinel for "not in a party". constexpr uint16_t NO_PARTY_ID = std::numeric_limits::max(); -// Character flags (me.charflags; 1.14d's BnetData::nCharFlags). Values follow -// D2MOO's CLIENTSAVEFLAG_*, which D2R keeps; the names of the bits D2MOO leaves -// unnamed are D2R's. +// Character flags (me.charflags; 1.14d's BnetData::nCharFlags, a 16-bit word). +// Values follow D2MOO's CLIENTSAVEFLAG_*; the bits D2MOO leaves unnamed are +// named after later game versions. /// @flags -enum class CharFlag : uint32_t { +enum class CharFlag : uint16_t { Newbie = 0x0001, Error = 0x0002, // D2MOO: set at character creation for realm characters Hardcore = 0x0004, diff --git a/src/contract/game/GameHelpers.h b/src/contract/game/GameHelpers.h index 90e210c..4b0fe0f 100644 --- a/src/contract/game/GameHelpers.h +++ b/src/contract/game/GameHelpers.h @@ -52,9 +52,7 @@ std::string GetAccountName(); std::string GetPlayerName(); std::string GetRealmName(); std::string GetRealmShort(); -Difficulty GetMaxDiff(); CharFlag GetCharFlags(); -std::optional IsLadder(); // === GameStructInfo Queries === std::string GetGameName(); diff --git a/src/runtime/api/classes/ClassRegistry.cpp b/src/runtime/api/classes/ClassRegistry.cpp index 4ce8d69..dc447f2 100644 --- a/src/runtime/api/classes/ClassRegistry.cpp +++ b/src/runtime/api/classes/ClassRegistry.cpp @@ -1,6 +1,7 @@ #include "ClassRegistry.h" #include +#include #include #include "api/core/Convert.h" @@ -182,11 +183,17 @@ v8::Local CreateMeObject(v8::Isolate* isolate, v8::Local(game::GetDifficulty())); }); - /// @description Highest difficulty unlocked for this character. - /// @type {Difficulty} + /// @description The character's progression: the number of acts completed across all difficulties, the + /// ProgressionMask bits of charflags (bits 8-12) shifted down - not a difficulty. A difficulty takes 5 acts in + /// an expansion game and 4 in classic, so the highest difficulty unlocked is min(maxdiff / acts, 2) + /// (0 Normal, 1 Nightmare, 2 Hell): an expansion character that has finished Nightmare reads 10. + /// @type {number} JSUnit::InstanceProperty( isolate, context, me, "maxdiff", +[](v8::Local, const v8::PropertyCallbackInfo& info) { - info.GetReturnValue().Set(static_cast(game::GetMaxDiff())); + constexpr auto PROGRESSION_MASK = std::to_underlying(game::CharFlag::ProgressionMask); + const auto progression = + (std::to_underlying(game::GetCharFlags()) & PROGRESSION_MASK) >> std::countr_zero(PROGRESSION_MASK); + info.GetReturnValue().Set(static_cast(progression)); }); /// @description Name of the joined/created game. Empty string when not in a game. @@ -245,12 +252,11 @@ v8::Local CreateMeObject(v8::Isolate* isolate, v8::Local, const v8::PropertyCallbackInfo& info) { - if (const auto ladder = game::IsLadder()) - info.GetReturnValue().Set(convert::ToJS(info.GetIsolate(), static_cast(ladder.value()))); + info.GetReturnValue().Set(HasFlag(game::GetCharFlags(), game::CharFlag::Ladder)); }); /// @description Current network latency to the game server, in milliseconds. diff --git a/src/runtime/components/characterstate/CharacterState.cpp b/src/runtime/components/characterstate/CharacterState.cpp index 2478a39..bbfb4e3 100644 --- a/src/runtime/components/characterstate/CharacterState.cpp +++ b/src/runtime/components/characterstate/CharacterState.cpp @@ -120,9 +120,7 @@ json BuildIdentity() { identity["charFlags"] = std::to_underlying(game::GetCharFlags()); // The game's own type, read past the compatibility flags like the rest of the capture. identity["gameType"] = std::to_underlying(game::GetGameType()); - // hardcore/expansion are derivable from charFlags; ladder is a separate BnetData - // flag, so it stays here. - identity["ladder"] = game::IsLadder().value_or(0) != 0; + identity["ladder"] = HasFlag(game::GetCharFlags(), game::CharFlag::Ladder); return identity; } diff --git a/tests/runtime/fakes/GameHelpers.cpp b/tests/runtime/fakes/GameHelpers.cpp index daef4be..8a100cb 100644 --- a/tests/runtime/fakes/GameHelpers.cpp +++ b/tests/runtime/fakes/GameHelpers.cpp @@ -74,15 +74,9 @@ std::string GetRealmName() { std::string GetRealmShort() { return ""; } -Difficulty GetMaxDiff() { - return Difficulty::Normal; -} CharFlag GetCharFlags() { return CharFlag{}; } -std::optional IsLadder() { - return {}; -} // === GameStructInfo Queries === std::string GetGameName() {