add configurable min levels for non-BB; closes #666

This commit is contained in:
Martin Michelsen
2025-07-11 17:57:39 -07:00
parent 118512ebb2
commit 60291993b6
11 changed files with 66 additions and 49 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ struct GVRHeader {
be_uint16_t height;
} __packed_ws__(GVRHeader, 0x10);
string encode_gvm(const phosg::ImageRGBA8888& img, GVRDataFormat data_format, const string& internal_name, uint32_t global_index) {
string encode_gvm(const phosg::ImageRGBA8888N& img, GVRDataFormat data_format, const string& internal_name, uint32_t global_index) {
int8_t dimensions_field = -2;
{
size_t h = img.get_height();
+1 -1
View File
@@ -20,7 +20,7 @@ enum class GVRDataFormat : uint8_t {
};
std::string encode_gvm(
const phosg::ImageRGBA8888& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index);
const phosg::ImageRGBA8888N& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index);
phosg::ImageRGB888 decode_fon(const std::string& data, size_t width);
std::string encode_fon(const phosg::ImageRGB888& img);
+1 -1
View File
@@ -118,7 +118,7 @@ struct ItemData {
// sending where needed.
// Related note: PSO V2 has an annoyingly complicated format for mags that
// doesn't match the above table. We decode this upon receipt and encode it
// imemdiately before sending when interacting with V2 clients; see the
// immediately before sending when interacting with V2 clients; see the
// implementation of decode_for_version() for details.
union {
+4 -4
View File
@@ -1183,7 +1183,7 @@ Action a_encode_gvm(
} else {
data = phosg::read_all(stdin);
}
auto img = phosg::ImageRGBA8888::from_file_data(data);
auto img = phosg::ImageRGBA8888N::from_file_data(data);
// If the image has any transparent pixels at all, use RGB5A3
string encoded = encode_gvm(
img, has_any_transparent_pixels(img) ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0);
@@ -2595,17 +2595,17 @@ Action a_generate_ep3_cards_html(
phosg::parallel_range<uint32_t>([&](uint32_t index, size_t) -> bool {
auto& info = this->card_infos[index];
if (!info.large_filename.empty()) {
auto img = phosg::ImageRGBA8888::from_file_data(phosg::load_file(info.large_filename));
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.large_filename));
img.resize(512, 399);
info.large_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
}
if (!info.medium_filename.empty()) {
auto img = phosg::ImageRGBA8888::from_file_data(phosg::load_file(info.medium_filename));
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.medium_filename));
img.resize(184, 144);
info.medium_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
}
if (!info.small_filename.empty()) {
auto img = phosg::ImageRGBA8888::from_file_data(phosg::load_file(info.small_filename));
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.small_filename));
img.resize(58, 43);
info.small_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
}
+2 -2
View File
@@ -830,8 +830,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// regsA[0] = floor
// regsA[1] = section
// regsA[2] = group
{0x8A, "unhide_obj", nullptr, {{REG_SET_FIXED, 3}}, F_V0_V4},
{0x8B, "unhide_ene", nullptr, {{REG_SET_FIXED, 3}}, F_V0_V4},
{0x8A, "construct_delayed_object", "unhide_obj", {{REG_SET_FIXED, 3}}, F_V0_V4},
{0x8B, "construct_delayed_enemy", "unhide_ene", {{REG_SET_FIXED, 3}}, F_V0_V4},
// Starts a new thread when the player is close enough to the given point.
// The collision is created on the current floor; the thread is created
+43 -35
View File
@@ -400,23 +400,20 @@ shared_ptr<const QuestIndex> ServerState::quest_index(Version version) const {
}
size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const {
// A player's actual level is their displayed level - 1, so the minimums for
// Episode 1 (for example) are actually 1, 20, 40, 80.
const auto& min_levels = is_v4(version)
? this->min_levels_v4
: is_v3(version)
? this->min_levels_v3
: this->min_levels_v1_v2;
switch (episode) {
case Episode::EP1: {
const auto& min_levels = (version == Version::BB_V4) ? this->min_levels_v4[0] : DEFAULT_MIN_LEVELS_V3;
return min_levels.at(difficulty);
}
case Episode::EP2: {
const auto& min_levels = (version == Version::BB_V4) ? this->min_levels_v4[1] : DEFAULT_MIN_LEVELS_V3;
return min_levels.at(difficulty);
}
case Episode::EP1:
return min_levels[0].at(difficulty);
case Episode::EP2:
return min_levels[1].at(difficulty);
case Episode::EP3:
return 0;
case Episode::EP4: {
const auto& min_levels = (version == Version::BB_V4) ? this->min_levels_v4[2] : DEFAULT_MIN_LEVELS_V3;
return min_levels.at(difficulty);
}
case Episode::EP4:
return min_levels[2].at(difficulty);
default:
throw runtime_error("invalid episode");
}
@@ -966,7 +963,7 @@ void ServerState::load_config_early() {
} else if (lower_path.ends_with(".gvm")) {
decompressed_gvm_data = phosg::load_file(path);
} else if (lower_path.ends_with(".bmp")) {
auto img = phosg::ImageRGBA8888::from_file_data(phosg::load_file(path));
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(path));
decompressed_gvm_data = encode_gvm(
img,
has_any_transparent_pixels(img) ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565,
@@ -1273,31 +1270,42 @@ void ServerState::load_config_early() {
this->rare_enemy_rates_challenge = MapState::DEFAULT_RARE_ENEMIES;
}
this->min_levels_v1_v2[0] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v1_v2[1] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v1_v2[2] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v3[0] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v3[1] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v3[2] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_V4_EP1;
this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_V4_EP2;
this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_V4_EP4;
try {
for (const auto& ep_it : this->config_json->get_dict("BBMinimumLevels")) {
array<size_t, 4> levels({0, 0, 0, 0});
for (size_t z = 0; z < 4; z++) {
levels[z] = ep_it.second->get_int(z) - 1;
}
switch (episode_for_token_name(ep_it.first)) {
case Episode::EP1:
this->min_levels_v4[0] = levels;
break;
case Episode::EP2:
this->min_levels_v4[1] = levels;
break;
case Episode::EP4:
this->min_levels_v4[2] = levels;
break;
default:
throw runtime_error("unknown episode");
auto populate_min_levels = [&](std::array<std::array<size_t, 4>, 3>& dest, const char* key_name) -> void {
try {
for (const auto& ep_it : this->config_json->get_dict(key_name)) {
array<size_t, 4> levels({0, 0, 0, 0});
for (size_t z = 0; z < 4; z++) {
levels[z] = ep_it.second->get_int(z) - 1;
}
switch (episode_for_token_name(ep_it.first)) {
case Episode::EP1:
dest[0] = levels;
break;
case Episode::EP2:
dest[1] = levels;
break;
case Episode::EP4:
dest[2] = levels;
break;
default:
throw runtime_error("unknown episode");
}
}
} catch (const out_of_range&) {
}
} catch (const out_of_range&) {
}
};
populate_min_levels(this->min_levels_v1_v2, "V1V2MinimumLevels");
populate_min_levels(this->min_levels_v3, "V3MinimumLevels");
populate_min_levels(this->min_levels_v4, "BBMinimumLevels");
this->bb_required_patches.clear();
try {
+2
View File
@@ -218,6 +218,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const SetDataTableBase> bb_solo_set_data_table_ep1_ult;
std::array<std::shared_ptr<const MapState::RareEnemyRates>, 4> rare_enemy_rates_by_difficulty;
std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates_challenge;
std::array<std::array<size_t, 4>, 3> min_levels_v1_v2; // Indexed as [episode][difficulty]
std::array<std::array<size_t, 4>, 3> min_levels_v3; // Indexed as [episode][difficulty]
std::array<std::array<size_t, 4>, 3> min_levels_v4; // Indexed as [episode][difficulty]
std::unordered_set<std::string> bb_required_patches;
std::unordered_set<std::string> auto_patches;
+1 -1
View File
@@ -805,7 +805,7 @@ char char_for_challenge_rank(uint8_t rank) {
return "BAS"[rank];
}
const array<size_t, 4> DEFAULT_MIN_LEVELS_V3({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109});
+1 -1
View File
@@ -106,7 +106,7 @@ uint32_t class_flags_for_class(uint8_t char_class);
char char_for_challenge_rank(uint8_t rank);
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V3;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V123;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4;
+2 -2
View File
@@ -98,7 +98,7 @@ void TeamIndex::Team::save_config() const {
}
void TeamIndex::Team::load_flag() {
auto img = phosg::ImageRGBA8888::from_file_data(phosg::load_file(this->flag_filename()));
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(this->flag_filename()));
if (img.get_width() != 32 || img.get_height() != 32) {
throw runtime_error("incorrect flag image dimensions");
}
@@ -114,7 +114,7 @@ void TeamIndex::Team::save_flag() const {
if (!this->flag_data) {
return;
}
phosg::ImageRGBA8888 img(32, 32);
phosg::ImageRGBA8888N img(32, 32);
for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; x++) {
img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x)));
+8 -1
View File
@@ -1195,8 +1195,15 @@
// "RareEnemyRates-Ultimate": {...},
// "RareEnemyRates-Challenge": {...},
// You can override the minimum character levels required to make BB games in
// You can override the minimum character levels required to make games in
// each episode and difficulty level here.
"V1V2MinimumLevels": {
"Episode1": [1, 20, 40, 80],
},
"V3MinimumLevels": {
"Episode1": [1, 20, 40, 80],
"Episode2": [1, 20, 40, 80],
},
"BBMinimumLevels": {
"Episode1": [1, 20, 50, 90],
"Episode2": [1, 30, 60, 100],