Skip to content

Commit 7927173

Browse files
committed
Use implicit MaxLevel (based on highest level seen)
1 parent 1f72851 commit 7927173

2 files changed

Lines changed: 51 additions & 55 deletions

File tree

Packaging/resources/assets/txtdata/Readme.md

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,36 +76,25 @@ base 10 with no decimal or thousands separators. The first row of this file is
7676
used as a header and requires the following column names:
7777

7878
#### Level
79-
A numeric value used to set the order for remaining values, or the special
80-
`MaxLevel` value which is used to determine the maximum character level. The
81-
header line MUST be the first line in the file. The `MaxLevel` line MUST be
82-
present, it SHOULD be the second line but can appear later in the file. Levels
83-
SHOULD proceed in ascending order after that. If you leave any gaps then
84-
characters will not be able to advance past that level and experience caps
85-
will not apply.
86-
87-
For example you could set a maximum level of 36 by changing the MaxLevel value:
88-
```tsv
89-
Level Experience
90-
MaxLevel 36
91-
0 0
92-
1 2000
93-
...
94-
```
79+
A numeric value used to set the order for experience thresholds. The header
80+
line MUST be the first line in the file. Levels SHOULD proceed in ascending
81+
order after that. Levels up to 255 are supported, the highest value will be
82+
used as the maximum character level. If you leave any gaps then characters
83+
will not be able to advance past that level and experience caps will not apply.
84+
85+
If you're familiar with Diablo 2 text files you might expect to use a MaxLevel
86+
row to set character level limits, these lines are ignored and the largest
87+
Level value is used as described above.
9588

96-
The first row following the `MaxLevel` line SHOULD be `0 0` (as all characters
97-
start at level 1, we ignore these values and use the threshold for level 1 to
98-
determine when characters advance past level 1). There should also be at least
99-
as many rows as the highest value provided for `MaxLevel`. If you specify a
100-
`MaxLevel` of `60` but only provide experience values up to `50` then
101-
characters will level up to `51` but no further.
89+
The first row SHOULD be `0 0` (as all characters start at level 1, we ignore
90+
these values and use the threshold for level 1 to determine when characters
91+
advance past level 1).
10292

10393
#### Experience
10494
This column determines the experience points required for characters to
10595
advance past that level. For example a file like:
10696
```tsv
10797
Level Experience
108-
MaxLevel 5
10998
0 0
11099
1 2000
111100
2 4000
@@ -121,9 +110,9 @@ points and will not level up any further.
121110

122111
You should provide a value for every row up to (and including) the maximum
123112
level you intend players to be able to reach. If you have an empty cell for an
124-
experience value at a row earlier than the `MaxLevel` value then characters
125-
will not be able to advance past that level. They will continue to gain
126-
experience without a cap (up to the hard limit of `2^32-1`, 4,294,967,295).
113+
experience value at a given level then characters will not be able to advance
114+
past that level. They will continue to gain experience without a cap (up to the
115+
hard limit of `2^32-1`, 4,294,967,295).
127116

128117
[d2-excel-plus]: https://github.com/Cjreek/D2ExcelPlus
129118
[d2mods-info]: https://www.d2mods.info/forum/viewtopic.php?t=34455

Source/playerdat.cpp

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace devilution {
2626

2727
namespace {
2828

29-
struct ExperienceData {
29+
class ExperienceData {
3030
/** Specifies the experience point limit of each level. The given values are defaults used if the data file is missing. */
3131
std::vector<uint32_t> levelThresholds {
3232
0,
@@ -82,12 +82,32 @@ struct ExperienceData {
8282
1583495809
8383
};
8484

85-
static constexpr uint8_t DefaultMaxLevel = 50;
86-
uint8_t maxLevel = DefaultMaxLevel;
85+
public:
86+
uint8_t getMaxLevel() const
87+
{
88+
if (levelThresholds.empty())
89+
return 0;
90+
return static_cast<uint8_t>(std::min<size_t>(levelThresholds.size() - 1, std::numeric_limits<uint8_t>::max()));
91+
}
92+
93+
DVL_REINITIALIZES void clear()
94+
{
95+
levelThresholds.clear();
96+
}
8797

88-
[[nodiscard]] uint32_t getThresholdForLevel(unsigned level)
98+
[[nodiscard]] uint32_t getThresholdForLevel(unsigned level) const
8999
{
90-
return levelThresholds[std::min<size_t>({ level, maxLevel, levelThresholds.size() - 1 })];
100+
return levelThresholds[std::min<size_t>(level, getMaxLevel())];
101+
}
102+
103+
void setThresholdForLevel(unsigned level, uint32_t experience)
104+
{
105+
if (level >= levelThresholds.size()) {
106+
// To avoid ValidatePlayer() resetting players to 0 experience we need to use the maximum possible value here
107+
// As long as the file has no gaps it'll get initialised properly.
108+
levelThresholds.resize(static_cast<size_t>(level) + 1, std::numeric_limits<uint32_t>::max());
109+
}
110+
levelThresholds[level] = experience;
91111
}
92112
} ExperienceData;
93113

@@ -190,12 +210,11 @@ void ReloadExperienceData()
190210
return;
191211
}
192212

193-
ExperienceData.levelThresholds.clear();
194-
bool foundMaxLevelRecord = false;
213+
ExperienceData.clear();
195214
do {
196215
uint8_t level = 0;
197216
uint32_t experience = 0;
198-
bool isMaxLevelRecord = false;
217+
bool skipRecord = false;
199218
for (auto &column : columns) {
200219
result = DiscardMultipleFields(result.next, dataFile.end(), column.skipLength);
201220

@@ -208,11 +227,11 @@ void ReloadExperienceData()
208227
switch (column.type) {
209228
case ExperienceColumnDefinition::ColumnType::Level: {
210229
auto fromCharsResult = std::from_chars(result.next, dataFile.end(), level);
211-
if (fromCharsResult.ec == std::errc::invalid_argument && !foundMaxLevelRecord) {
230+
if (fromCharsResult.ec == std::errc::invalid_argument) {
212231
// not a signless numeric value, is this the MaxLevel line?
213232
result = GetNextField(fromCharsResult.ptr, dataFile.end());
214233
if (result.value == "MaxLevel") {
215-
isMaxLevelRecord = true;
234+
skipRecord = true;
216235
}
217236
// else it was an invalid value, TODO: let the player know the data file contains errors
218237
} else {
@@ -238,28 +257,16 @@ void ReloadExperienceData()
238257
default:
239258
result = DiscardField(result.next, dataFile.end());
240259
}
241-
}
242260

243-
if (isMaxLevelRecord) {
244-
ExperienceData.maxLevel = experience == 0 ? ExperienceData::DefaultMaxLevel
245-
: static_cast<uint8_t>(std::min<uint32_t>(experience, std::numeric_limits<uint8_t>::max()));
246-
foundMaxLevelRecord = true;
247-
} else {
248-
if (foundMaxLevelRecord && level > ExperienceData.maxLevel) {
249-
// ignore values we will never use. Should we notify the player?
250-
} else {
251-
if (level >= ExperienceData.levelThresholds.size()) {
252-
// To avoid ValidatePlayer() resetting players to 0 experience we need to use the maximum possible value here
253-
// As long as the file has no gaps it'll get initialised properly.
254-
ExperienceData.levelThresholds.resize(static_cast<size_t>(level) + 1, std::numeric_limits<uint32_t>::max());
255-
}
256-
ExperienceData.levelThresholds[level] = experience;
257-
}
261+
if (skipRecord)
262+
break;
258263
}
259264

260-
if (!result.endOfRecord()) {
265+
if (!skipRecord)
266+
ExperienceData.setThresholdForLevel(level, experience);
267+
268+
if (!result.endOfRecord())
261269
result = DiscardRemainingFields(result.next, dataFile.end());
262-
}
263270
} while (!result.endOfFile());
264271
}
265272

@@ -277,7 +284,7 @@ uint32_t GetNextExperienceThresholdForLevel(unsigned level)
277284

278285
uint8_t GetMaximumCharacterLevel()
279286
{
280-
return ExperienceData.maxLevel;
287+
return ExperienceData.getMaxLevel();
281288
}
282289

283290
const _sfx_id herosounds[enum_size<HeroClass>::value][enum_size<HeroSpeech>::value] = {

0 commit comments

Comments
 (0)