Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions src/backends/lod114d/game/GameHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ uint32_t GetWeaponSwitch() {
}

GameType GetGameType() {
return static_cast<GameType>(*d2client::gbExpCharFlag);
return HasFlag(GetCharFlags(), CharFlag::Expansion) ? GameType::Expansion : GameType::Classic;
}

uint32_t GetMercReviveCost() {
Expand Down Expand Up @@ -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<Difficulty>(data->nMaxDiff) : Difficulty::Normal;
}

CharFlag GetCharFlags() {
auto* data = *d2launch::gpBnetData;
return data != nullptr ? static_cast<CharFlag>(data->nCharFlags) : CharFlag{};
}

std::optional<uint8_t> IsLadder() {
auto* data = *d2launch::gpBnetData;
return data != nullptr ? std::optional{data->nLadderFlag} : std::nullopt;
return data != nullptr ? data->nCharFlags : CharFlag{};
}

// === GameStructInfo Queries ===
Expand Down
16 changes: 11 additions & 5 deletions src/backends/lod114d/hooks/Intercepts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@

#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <format>
#include <span>
#include <string>
#include <system_error>
#include <utility>

#include "game/GameCallbacks.h"
#include "game/GameHelpers.h"
Expand Down Expand Up @@ -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; }
Expand All @@ -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<game::CharFlag>(UNLOCKED_PROGRESSION << PROGRESSION_SHIFT);
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/backends/lod114d/imports/extras/BnetData.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#pragma once

#include <array>
#include <cstddef>
#include <cstdint>

#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
Expand All @@ -27,8 +30,7 @@ struct BnetData {
std::array<char, 0x08> szRealmName; // 0xD5
std::array<uint8_t, 0x111> _8; // 0xDD
uint8_t nCharClass; // 0x1EE
uint8_t nCharFlags; // 0x1EF
uint8_t nMaxDiff; // 0x1F0
game::CharFlag nCharFlags; // 0x1EF word, SelectedCharBnetSingleTcpIp 0x434A5A
std::array<uint8_t, 0x1F> _9; // 0x1F1
uint8_t nCreatedGameDifficulty; // 0x210
void* _10; // 0x211
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/contract/ContractEnumNames.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions src/contract/game/Constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint16_t>::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,
Expand Down
2 changes: 0 additions & 2 deletions src/contract/game/GameHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ std::string GetAccountName();
std::string GetPlayerName();
std::string GetRealmName();
std::string GetRealmShort();
Difficulty GetMaxDiff();
CharFlag GetCharFlags();
std::optional<uint8_t> IsLadder();

// === GameStructInfo Queries ===
std::string GetGameName();
Expand Down
20 changes: 13 additions & 7 deletions src/runtime/api/classes/ClassRegistry.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "ClassRegistry.h"

#include <array>
#include <bit>
#include <utility>

#include "api/core/Convert.h"
Expand Down Expand Up @@ -182,11 +183,17 @@ v8::Local<v8::Object> CreateMeObject(v8::Isolate* isolate, v8::Local<v8::Context
info.GetReturnValue().Set(static_cast<uint32_t>(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<v8::Name>, const v8::PropertyCallbackInfo<v8::Value>& info) {
info.GetReturnValue().Set(static_cast<uint32_t>(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<uint32_t>(progression));
});

/// @description Name of the joined/created game. Empty string when not in a game.
Expand Down Expand Up @@ -245,12 +252,11 @@ v8::Local<v8::Object> CreateMeObject(v8::Isolate* isolate, v8::Local<v8::Context
info.GetReturnValue().Set(game::Unit::CursorItem().has_value());
});

/// @description Ladder status flag of the current realm/game; undefined when the status is unknown (out of game).
/// @type {number}
/// @description Whether the character is a ladder character: the Ladder bit of charflags.
/// @type {boolean}
JSUnit::InstanceProperty(
isolate, context, me, "ladder", +[](v8::Local<v8::Name>, const v8::PropertyCallbackInfo<v8::Value>& info) {
if (const auto ladder = game::IsLadder())
info.GetReturnValue().Set(convert::ToJS(info.GetIsolate(), static_cast<double>(ladder.value())));
info.GetReturnValue().Set(HasFlag(game::GetCharFlags(), game::CharFlag::Ladder));
});

/// @description Current network latency to the game server, in milliseconds.
Expand Down
4 changes: 1 addition & 3 deletions src/runtime/components/characterstate/CharacterState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
6 changes: 0 additions & 6 deletions tests/runtime/fakes/GameHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,9 @@ std::string GetRealmName() {
std::string GetRealmShort() {
return "";
}
Difficulty GetMaxDiff() {
return Difficulty::Normal;
}
CharFlag GetCharFlags() {
return CharFlag{};
}
std::optional<uint8_t> IsLadder() {
return {};
}

// === GameStructInfo Queries ===
std::string GetGameName() {
Expand Down
Loading