load maps on all versions

This commit is contained in:
Martin Michelsen
2024-01-03 00:22:28 -08:00
parent 78e407a70f
commit df29a60a6e
9 changed files with 968 additions and 266 deletions
+5 -26
View File
@@ -52,19 +52,6 @@ void ItemCreator::set_random_state(uint32_t seed, uint32_t absolute_offset) {
}
}
void ItemCreator::set_box_destroyed(uint16_t entity_id) {
this->destroyed_boxes.emplace(entity_id);
}
void ItemCreator::set_monster_destroyed(uint16_t entity_id) {
this->destroyed_monsters.emplace(entity_id);
}
void ItemCreator::clear_destroyed_entities() {
this->destroyed_monsters.clear();
this->destroyed_boxes.clear();
}
bool ItemCreator::are_rare_drops_allowed() const {
// Note: The client has an additional check here, which appears to be a subtle
// anti-cheating measure. There is a flag on the client, initially zero, which
@@ -134,16 +121,12 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
}
}
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint16_t entity_id, uint8_t area) {
return this->destroyed_boxes.count(entity_id)
? DropResult()
: this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
return this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
}
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area) {
return this->destroyed_monsters.count(entity_id)
? DropResult()
: this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
return this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
}
ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
@@ -1680,11 +1663,7 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player
}
ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop(
uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
if (this->destroyed_boxes.count(entity_id)) {
return DropResult();
}
uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
DropResult res;
res.item = this->base_item_for_specialized_box(def0, def1, def2);
if (def_z == 0.0f) {
+3 -9
View File
@@ -29,19 +29,15 @@ public:
~ItemCreator() = default;
void set_random_state(uint32_t seed, uint32_t absolute_offset);
void clear_destroyed_entities();
struct DropResult {
ItemData item;
bool is_from_rare_table = false;
};
DropResult on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area);
DropResult on_box_item_drop(uint16_t entity_id, uint8_t area);
DropResult on_specialized_box_item_drop(uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
void set_monster_destroyed(uint16_t entity_id);
void set_box_destroyed(uint16_t entity_id);
DropResult on_monster_item_drop(uint32_t enemy_type, uint8_t area);
DropResult on_box_item_drop(uint8_t area);
DropResult on_specialized_box_item_drop(uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const;
@@ -82,8 +78,6 @@ private:
// Note: The original implementation uses 17 different random states for some
// reason. We forego that and use only one for simplicity.
PSOV2Encryption random_crypt;
std::unordered_set<uint16_t> destroyed_monsters;
std::unordered_set<uint16_t> destroyed_boxes;
inline bool is_v3() const {
return !is_v1_or_v2(this->version);
+48 -19
View File
@@ -262,16 +262,20 @@ void Lobby::create_item_creator() {
}
void Lobby::load_maps() {
auto s = this->require_server_state();
this->map = make_shared<Map>(this->lobby_id, this->random_crypt);
auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates)
? this->rare_enemy_rates
: Map::DEFAULT_RARE_ENEMIES;
auto s = this->require_server_state();
if (this->quest) {
auto leader_c = this->clients.at(this->leader_id);
if (!leader_c) {
throw logic_error("lobby leader is missing");
}
auto vq = this->quest->version(Version::BB_V4, leader_c->language());
auto vq = this->quest->version(this->base_version, leader_c->language());
auto dat_contents = prs_decompress(*vq->dat_contents);
this->map->clear();
this->map->add_enemies_and_objects_from_quest_data(
@@ -281,16 +285,48 @@ void Lobby::load_maps() {
dat_contents.data(),
dat_contents.size(),
this->random_seed,
this->rare_enemy_rates ? this->rare_enemy_rates : Map::NO_RARE_ENEMIES);
rare_rates);
} else if (this->mode != GameMode::CHALLENGE) {
// No quest loaded - load free-roam maps instead. Don't load free-roam maps
// in Challenge mode, since players can't go to Ragol without a quest loaded
} else { // No quest loaded
for (size_t floor = 0; floor < 0x10; floor++) {
this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32,
floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load());
auto get_file_data = [&](const string& filename) -> shared_ptr<const string> {
if (this->base_version == Version::BB_V4) {
try {
return s->load_bb_file(filename);
} catch (const exception& e) {
this->log.info("[Map/%zu:e] Failed to load %s from BB patch tree: %s", floor, filename.c_str(), e.what());
}
} else if (this->base_version == Version::PC_V2) {
try {
string path = "system/patch-pc/Media/PSO/" + filename;
auto ret = make_shared<string>(load_file(path));
this->log.info("[Map/%zu:e] Loaded %s from PC patch tree", floor, filename.c_str());
return ret;
} catch (const exception& e) {
this->log.info("[Map/%zu:e] Failed to load %s from PC patch tree: %s", floor, filename.c_str(), e.what());
}
}
try {
string path = string_printf("system/maps/%s/%s", file_path_token_for_version(this->base_version), filename.c_str());
auto ret = make_shared<string>(load_file(path));
this->log.info("[Map/%zu:e] Loaded %s from default maps", floor, filename.c_str());
return ret;
} catch (const exception& e) {
this->log.info("[Map/%zu:e] Failed to load %s from PC patch tree: %s", floor, filename.c_str(), e.what());
}
return nullptr;
};
auto enemy_filenames = map_filenames_for_variation(
this->base_version,
this->episode,
(this->mode == GameMode::SOLO),
this->mode,
floor,
this->variations[floor * 2],
this->variations[floor * 2 + 1],
@@ -300,8 +336,8 @@ void Lobby::load_maps() {
} else {
bool any_map_loaded = false;
for (const string& filename : enemy_filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
auto map_data = get_file_data(filename);
if (map_data) {
this->map->add_enemies_from_map_data(
this->episode,
this->difficulty,
@@ -309,11 +345,9 @@ void Lobby::load_maps() {
floor,
map_data->data(),
map_data->size(),
this->rare_enemy_rates);
rare_rates);
any_map_loaded = true;
break;
} catch (const exception& e) {
this->log.info("[Map/%zu:e] Failed to load %s: %s", floor, filename.c_str(), e.what());
}
}
if (!any_map_loaded) {
@@ -322,8 +356,9 @@ void Lobby::load_maps() {
}
auto object_filenames = map_filenames_for_variation(
this->base_version,
this->episode,
(this->mode == GameMode::SOLO),
this->mode,
floor,
this->variations[floor * 2],
this->variations[floor * 2 + 1],
@@ -333,13 +368,11 @@ void Lobby::load_maps() {
} else {
bool any_map_loaded = false;
for (const string& filename : object_filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
auto map_data = get_file_data(filename);
if (map_data) {
this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
any_map_loaded = true;
break;
} catch (const exception& e) {
this->log.info("[Map/%zu:o] Failed to load %s: %s", floor, filename.c_str(), e.what());
}
}
if (!any_map_loaded) {
@@ -361,10 +394,6 @@ void Lobby::load_maps() {
}
this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size());
if (this->item_creator) {
this->item_creator->clear_destroyed_entities();
}
}
void Lobby::create_ep3_server() {
+687 -68
View File
@@ -14,6 +14,561 @@ using namespace std;
static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f;
const char* Map::name_for_object_type(uint16_t type) {
switch (type) {
case 0x0000:
return "TObjPlayerSet";
case 0x0001:
return "TObjParticle";
case 0x0002:
return "TObjAreaWarpForest";
case 0x0003:
return "TObjMapWarpForest";
case 0x0004:
return "TObjLight";
case 0x0006:
return "TObjEnvSound";
case 0x0007:
return "TObjFogCollision";
case 0x0008:
return "TObjEvtCollision";
case 0x0009:
return "TObjCollision";
case 0x000A:
return "TOMineIcon01";
case 0x000B:
return "TOMineIcon02";
case 0x000C:
return "TOMineIcon03";
case 0x000D:
return "TOMineIcon04";
case 0x000E:
return "TObjRoomId";
case 0x000F:
return "TOSensorGeneral01";
case 0x0011:
return "TEF_LensFlare";
case 0x0012:
return "TObjQuestCol";
case 0x0013:
return "TOHealGeneral";
case 0x0014:
return "TObjMapCsn";
case 0x0015:
return "TObjQuestColA";
case 0x0016:
return "TObjItemLight";
case 0x0017:
return "TObjRaderCol";
case 0x0018:
return "TObjFogCollisionSwitch";
case 0x0019:
return "TObjWarpBossMulti(off)/TObjWarpBoss(on)";
case 0x001A:
return "TObjSinBoard";
case 0x001B:
return "TObjAreaWarpQuest";
case 0x001C:
return "TObjAreaWarpEnding";
case 0x001D:
return "__UNNAMED_001D__";
case 0x001E:
return "__UNNAMED_001E__";
case 0x001F:
return "TObjRaderHideCol";
case 0x0020:
return "TOSwitchItem";
case 0x0021:
return "TOSymbolchatColli";
case 0x0022:
return "TOKeyCol";
case 0x0023:
return "TOAttackableCol";
case 0x0024:
return "TOSwitchAttack";
case 0x0025:
return "TOSwitchTimer";
case 0x0026:
return "TOChatSensor";
case 0x0027:
return "TObjRaderIcon";
case 0x0028:
return "TObjEnvSoundEx";
case 0x0029:
return "TObjEnvSoundGlobal";
case 0x0040:
return "TShopGenerator";
case 0x0041:
return "TObjLuker";
case 0x0042:
return "TObjBgmCol";
case 0x0043:
return "TObjCityMainWarp";
case 0x0044:
return "TObjCityAreaWarp";
case 0x0045:
return "TObjCityMapWarp";
case 0x0046:
return "TObjCityDoor_Shop";
case 0x0047:
return "TObjCityDoor_Guild";
case 0x0048:
return "TObjCityDoor_Warp";
case 0x0049:
return "TObjCityDoor_Med";
case 0x004A:
return "__UNNAMED_004A__";
case 0x004B:
return "TObjCity_Season_EasterEgg";
case 0x004C:
return "TObjCity_Season_ValentineHeart";
case 0x004D:
return "TObjCity_Season_XmasTree";
case 0x004E:
return "TObjCity_Season_XmasWreath";
case 0x004F:
return "TObjCity_Season_HalloweenPumpkin";
case 0x0050:
return "TObjCity_Season_21_21";
case 0x0051:
return "TObjCity_Season_SonicAdv2";
case 0x0052:
return "TObjCity_Season_Board";
case 0x0053:
return "TObjCity_Season_FireWorkCtrl";
case 0x0054:
return "TObjCityDoor_Lobby";
case 0x0055:
return "TObjCityMainWarpChallenge";
case 0x0056:
return "TODoorLabo";
case 0x0057:
return "TObjTradeCollision";
case 0x0080:
return "TObjDoor";
case 0x0081:
return "TObjDoorKey";
case 0x0082:
return "TObjLazerFenceNorm";
case 0x0083:
return "TObjLazerFence4";
case 0x0084:
return "TLazerFenceSw";
case 0x0085:
return "TKomorebi";
case 0x0086:
return "TButterfly";
case 0x0087:
return "TMotorcycle";
case 0x0088:
return "TObjContainerItem";
case 0x0089:
return "TObjTank";
case 0x008B:
return "TObjComputer";
case 0x008C:
return "TObjContainerIdo";
case 0x008D:
return "TOCapsuleAncient01";
case 0x008E:
return "TOBarrierEnergy01";
case 0x008F:
return "TObjHashi";
case 0x0090:
return "TOKeyGenericSw";
case 0x0091:
return "TObjContainerEnemy";
case 0x0092:
return "TObjContainerBase";
case 0x0093:
return "TObjContainerAbeEnemy";
case 0x0095:
return "TObjContainerNoItem";
case 0x0096:
return "TObjLazerFenceExtra";
case 0x00C0:
return "TOKeyCave01";
case 0x00C1:
return "TODoorCave01";
case 0x00C2:
return "TODoorCave02";
case 0x00C3:
return "TOHangceilingCave01Key/TOHangceilingCave01Normal/TOHangceilingCave01KeyQuick";
case 0x00C4:
return "TOSignCave01";
case 0x00C5:
return "TOSignCave02";
case 0x00C6:
return "TOSignCave03";
case 0x00C7:
return "TOAirconCave01";
case 0x00C8:
return "TOAirconCave02";
case 0x00C9:
return "TORevlightCave01";
case 0x00CB:
return "TORainbowCave01";
case 0x00CC:
return "TOKurage";
case 0x00CD:
return "TODragonflyCave01";
case 0x00CE:
return "TODoorCave03";
case 0x00CF:
return "TOBind";
case 0x00D0:
return "TOCakeshopCave01";
case 0x00D1:
return "TORockCaveS01";
case 0x00D2:
return "TORockCaveM01";
case 0x00D3:
return "TORockCaveL01";
case 0x00D4:
return "TORockCaveS02";
case 0x00D5:
return "TORockCaveM02";
case 0x00D6:
return "TORockCaveL02";
case 0x00D7:
return "TORockCaveSS02";
case 0x00D8:
return "TORockCaveSM02";
case 0x00D9:
return "TORockCaveSL02";
case 0x00DA:
return "TORockCaveS03";
case 0x00DB:
return "TORockCaveM03";
case 0x00DC:
return "TORockCaveL03";
case 0x00DE:
return "TODummyKeyCave01";
case 0x00DF:
return "TORockCaveBL01";
case 0x00E0:
return "TORockCaveBL02";
case 0x00E1:
return "TORockCaveBL03";
case 0x0100:
return "TODoorMachine01";
case 0x0101:
return "TOKeyMachine01";
case 0x0102:
return "TODoorMachine02";
case 0x0103:
return "TOCapsuleMachine01";
case 0x0104:
return "TOComputerMachine01";
case 0x0105:
return "TOMonitorMachine01";
case 0x0106:
return "TODragonflyMachine01";
case 0x0107:
return "TOLightMachine01";
case 0x0108:
return "TOExplosiveMachine01";
case 0x0109:
return "TOExplosiveMachine02";
case 0x010A:
return "TOExplosiveMachine03";
case 0x010B:
return "TOSparkMachine01";
case 0x010C:
return "TOHangerMachine01";
case 0x0130:
return "TODoorVoShip";
case 0x0140:
return "TObjGoalWarpAncient";
case 0x0141:
return "TObjMapWarpAncient";
case 0x0142:
return "TOKeyAncient02";
case 0x0143:
return "TOKeyAncient03";
case 0x0144:
return "TODoorAncient01";
case 0x0145:
return "TODoorAncient03";
case 0x0146:
return "TODoorAncient04";
case 0x0147:
return "TODoorAncient05";
case 0x0148:
return "TODoorAncient06";
case 0x0149:
return "TODoorAncient07";
case 0x014A:
return "TODoorAncient08";
case 0x014B:
return "TODoorAncient09";
case 0x014C:
return "TOSensorAncient01";
case 0x014D:
return "TOKeyAncient01";
case 0x014E:
return "TOFenceAncient01";
case 0x014F:
return "TOFenceAncient02";
case 0x0150:
return "TOFenceAncient03";
case 0x0151:
return "TOFenceAncient04";
case 0x0152:
return "TContainerAncient01";
case 0x0153:
return "TOTrapAncient01";
case 0x0154:
return "TOTrapAncient02";
case 0x0155:
return "TOMonumentAncient01";
case 0x0156:
return "TOMonumentAncient02";
case 0x0159:
return "TOWreckAncient01";
case 0x015A:
return "TOWreckAncient02";
case 0x015B:
return "TOWreckAncient03";
case 0x015C:
return "TOWreckAncient04";
case 0x015D:
return "TOWreckAncient05";
case 0x015E:
return "TOWreckAncient06";
case 0x015F:
return "TOWreckAncient07";
case 0x0160:
return "TObjFogCollisionPoison/TObjWarpBoss03";
case 0x0161:
return "TOContainerAncientItemCommon";
case 0x0162:
return "TOContainerAncientItemRare";
case 0x0163:
return "TOContainerAncientEnemyCommon";
case 0x0164:
return "TOContainerAncientEnemyRare";
case 0x0165:
return "TOContainerAncientItemNone";
case 0x0166:
return "TOWreckAncientBrakable05";
case 0x0167:
return "TOTrapAncient02R";
case 0x0170:
return "TOBoss4Bird";
case 0x0171:
return "TOBoss4Tower";
case 0x0172:
return "TOBoss4Rock";
case 0x0180:
return "TObjInfoCol";
case 0x0181:
return "TObjWarpLobby";
case 0x0182:
return "TObjLobbyMain";
case 0x0183:
return "__TObjPathObj_subclass_0183__";
case 0x0184:
return "TObjButterflyLobby";
case 0x0185:
return "TObjRainbowLobby";
case 0x0186:
return "TObjKabochaLobby";
case 0x0187:
return "TObjStendGlassLobby";
case 0x0188:
return "TObjCurtainLobby";
case 0x0189:
return "TObjWeddingLobby";
case 0x018A:
return "TObjTreeLobby";
case 0x018B:
return "TObjSuisouLobby";
case 0x018C:
return "TObjParticleLobby";
case 0x0190:
return "TObjCamera";
case 0x0191:
return "TObjTuitate";
case 0x0192:
return "TObjDoaEx01";
case 0x0193:
return "TObjBigTuitate";
case 0x01A0:
return "TODoorVS2Door01";
case 0x01A1:
return "TOVS2Wreck01";
case 0x01A2:
return "TOVS2Wreck02";
case 0x01A3:
return "TOVS2Wreck03";
case 0x01A4:
return "TOVS2Wreck04";
case 0x01A5:
return "TOVS2Wreck05";
case 0x01A6:
return "TOVS2Wreck06";
case 0x01A7:
return "TOVS2Wall01";
case 0x01A8:
return "__UNNAMED_01A8__";
case 0x01A9:
return "TObjHashiVersus1";
case 0x01AA:
return "TObjHashiVersus2";
case 0x01AB:
return "TODoorFourLightRuins";
case 0x01C0:
return "TODoorFourLightSpace";
case 0x0200:
return "TObjContainerJung";
case 0x0201:
return "TObjWarpJung";
case 0x0202:
return "TObjDoorJung";
case 0x0203:
return "TObjContainerJungEx";
case 0x0204:
return "TODoorJungleMain";
case 0x0205:
return "TOKeyJungleMain";
case 0x0206:
return "TORockJungleS01";
case 0x0207:
return "TORockJungleM01";
case 0x0208:
return "TORockJungleL01";
case 0x0209:
return "TOGrassJungle";
case 0x020A:
return "TObjWarpJungMain";
case 0x020B:
return "TBGLightningCtrl";
case 0x020C:
return "__TObjPathObj_subclass_020C__";
case 0x020D:
return "__TObjPathObj_subclass_020D__";
case 0x020E:
return "TObjContainerJungEnemy";
case 0x020F:
return "TOTrapChainSawDamage";
case 0x0210:
return "TOTrapChainSawKey";
case 0x0211:
return "TOBiwaMushi";
case 0x0212:
return "__TObjPathObj_subclass_0212__";
case 0x0213:
return "TOJungleDesign";
case 0x0220:
return "TObjFish";
case 0x0221:
return "TODoorFourLightSeabed";
case 0x0222:
return "TODoorFourLightSeabedU";
case 0x0223:
return "TObjSeabedSuiso_CH";
case 0x0224:
return "TObjSeabedSuisoBrakable";
case 0x0225:
return "TOMekaFish00";
case 0x0226:
return "TOMekaFish01";
case 0x0227:
return "__TObjPathObj_subclass_0227__";
case 0x0228:
return "TOTrapSeabed01";
case 0x0229:
return "TOCapsuleLabo";
case 0x0240:
return "TObjParticle";
case 0x0280:
return "__TObjAreaWarpForest_subclass_0280__";
case 0x02A0:
return "TObjLiveCamera";
case 0x02B0:
return "TContainerAncient01R";
case 0x02B1:
return "TObjLaboDesignBase";
case 0x02B2:
return "TObjLaboDesignBase";
case 0x02B3:
return "TObjLaboDesignBase";
case 0x02B4:
return "TObjLaboDesignBase";
case 0x02B5:
return "TObjLaboDesignBase";
case 0x02B6:
return "TObjLaboDesignBase";
case 0x02B7:
return "TObjGbAdvance";
case 0x02B8:
return "TObjQuestColALock2";
case 0x02B9:
return "TObjMapForceWarp";
case 0x02BA:
return "TObjQuestCol2";
case 0x02BB:
return "TODoorLaboNormal";
case 0x02BC:
return "TObjAreaWarpEndingJung";
case 0x02BD:
return "TObjLaboMapWarp";
case 0x0300:
return "__UNKNOWN_0300__";
case 0x0301:
return "__UNKNOWN_0301__";
case 0x0302:
return "__UNKNOWN_0302__";
case 0x0303:
return "__UNKNOWN_0303__";
case 0x0340:
return "__UNKNOWN_0340__";
case 0x0341:
return "__UNKNOWN_0341__";
case 0x0380:
return "__UNKNOWN_0380__";
case 0x0381:
return "__UNKNOWN_0381__";
case 0x0382:
return "__UNKNOWN_0382__";
case 0x0383:
return "__UNKNOWN_0383__";
case 0x0385:
return "__UNKNOWN_0385__";
case 0x0386:
return "__UNKNOWN_0386__";
case 0x0387:
return "__UNKNOWN_0387__";
case 0x0388:
return "__UNKNOWN_0388__";
case 0x0389:
return "__UNKNOWN_0389__";
case 0x038A:
return "__UNKNOWN_038A__";
case 0x038B:
return "__UNKNOWN_038B__";
case 0x038C:
return "__UNKNOWN_038C__";
case 0x038D:
return "__UNKNOWN_038D__";
case 0x038E:
return "__UNKNOWN_038E__";
case 0x038F:
return "__UNKNOWN_038F__";
case 0x0390:
return "__UNKNOWN_0390__";
case 0x0391:
return "__UNKNOWN_0391__";
case 0x03C0:
return "__UNKNOWN_03C0__";
case 0x03C1:
return "__UNKNOWN_03C1__";
default:
return "__UNKNOWN__";
}
}
Map::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate)
: hildeblue(enemy_rate),
rappy(enemy_rate),
@@ -100,8 +655,9 @@ string Map::EnemyEntry::str() const {
this->unused.load());
}
Map::Enemy::Enemy(size_t source_index, uint8_t floor, EnemyType type)
Map::Enemy::Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type)
: source_index(source_index),
enemy_id(enemy_id),
type(type),
floor(floor),
state_flags(0),
@@ -109,19 +665,22 @@ Map::Enemy::Enemy(size_t source_index, uint8_t floor, EnemyType type)
}
string Map::Enemy::str() const {
return string_printf("[Map::Enemy source %zX %s floor=%02hhX flags=%02hhX last_hit_by_client_id=%hu]",
this->source_index, name_for_enum(this->type), this->floor, this->state_flags, this->last_hit_by_client_id);
return string_printf("[Map::Enemy E-%hX source %zX %s floor=%02hhX flags=%02hhX last_hit_by_client_id=%hu]",
this->enemy_id, this->source_index, name_for_enum(this->type), this->floor, this->state_flags, this->last_hit_by_client_id);
}
string Map::Object::str() const {
if (this->param1 <= 0.0f) {
return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (specialized: %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ") floor=%02hhX item_drop_checked=%s]",
this->source_index, this->base_type, this->section, this->param1, this->param4, this->param5, this->param6, this->floor, this->item_drop_checked ? "true" : "false");
} else {
return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (generic) p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]",
this->source_index, this->base_type, this->section, this->param1, this->param4, this->param5, this->param6,
this->floor, this->item_drop_checked ? "true" : "false");
}
return string_printf("[Map::Object source %zX %04hX(%s) @%04hX p1=%g p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]",
this->source_index,
this->base_type,
Map::name_for_object_type(this->base_type),
this->section,
this->param1,
this->param4,
this->param5,
this->param6,
this->floor,
this->item_drop_checked ? "true" : "false");
}
Map::Map(uint32_t lobby_id, std::shared_ptr<PSOLFGEncryption> random_crypt)
@@ -142,8 +701,11 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size
const auto* objects = reinterpret_cast<const ObjectEntry*>(data);
for (size_t z = 0; z < entry_count; z++) {
uint16_t object_id = this->objects.size();
this->objects.emplace_back(Object{
.source_index = z,
.object_id = object_id,
.floor = floor,
.base_type = objects[z].base_type,
.section = objects[z].section,
.param1 = objects[z].param1,
@@ -151,7 +713,6 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size
.param4 = objects[z].param4,
.param5 = objects[z].param5,
.param6 = objects[z].param6,
.floor = floor,
.item_drop_checked = false,
});
}
@@ -177,7 +738,8 @@ void Map::add_enemy(
const EnemyEntry& e,
std::shared_ptr<const RareEnemyRates> rare_rates) {
auto add = [&](EnemyType type) -> void {
this->enemies.emplace_back(index, floor, type);
uint16_t enemy_id = this->enemies.size();
this->enemies.emplace_back(enemy_id, index, floor, type);
};
EnemyType child_type = EnemyType::UNKNOWN;
@@ -956,6 +1518,24 @@ void Map::add_enemies_and_objects_from_quest_data(
}
}
const Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) const {
return const_cast<Map*>(this)->find_enemy(floor, type);
}
Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) {
if (enemies.empty()) {
throw out_of_range("no enemies defined");
}
// TODO: Linear search is bad here. Do something better, like binary search
// for the floor start and just linear search through the floor enemies.
for (auto& e : this->enemies) {
if (e.floor == floor && e.type == type) {
return e;
}
}
throw out_of_range("enemy not found");
}
string Map::disassemble_quest_data(const void* data, size_t size) {
auto all_floor_sections = Map::collect_quest_map_data_sections(data, size);
@@ -1089,12 +1669,12 @@ void SetDataTable::print(FILE* stream) const {
}
}
struct AreaMapFileIndex {
struct AreaMapFileInfo {
const char* name_token;
vector<uint32_t> variation1_values;
vector<uint32_t> variation2_values;
AreaMapFileIndex(
AreaMapFileInfo(
const char* name_token,
vector<uint32_t> variation1_values,
vector<uint32_t> variation2_values)
@@ -1103,8 +1683,27 @@ struct AreaMapFileIndex {
variation2_values(variation2_values) {}
};
static const vector<AreaMapFileInfo> map_file_info_dc_protos = {
{"city00", {}, {0}},
{"forest01", {}, {0, 1}},
{"forest02", {}, {0, 1}},
{"cave01", {0, 1}, {0, 1}},
{"cave02", {0, 1}, {0, 1}},
{"cave03", {0, 1}, {0, 1}},
{"machine01", {0}, {0, 1}},
{"machine02", {0}, {0, 1}},
{"ancient01", {0, 1}, {0, 1}},
{"ancient02", {0, 1}, {0, 1}},
{"ancient03", {0, 1}, {0, 1}},
{"boss01", {}, {}},
{"boss02", {}, {}},
{"boss03", {}, {}},
{"boss04", {}, {}},
{nullptr, {}, {}},
};
// These are indexed as [episode][is_solo][floor], where episode is 0-2
static const vector<vector<vector<AreaMapFileIndex>>> map_file_info = {
static const vector<vector<vector<AreaMapFileInfo>>> map_file_info = {
{
// Episode 1
{
@@ -1230,96 +1829,116 @@ static const vector<vector<vector<AreaMapFileIndex>>> map_file_info = {
},
};
const vector<vector<AreaMapFileIndex>>& map_file_info_for_episode(Episode ep) {
switch (ep) {
case Episode::EP1:
return map_file_info.at(0);
case Episode::EP2:
return map_file_info.at(1);
case Episode::EP4:
return map_file_info.at(2);
default:
throw invalid_argument("episode has no maps");
const AreaMapFileInfo& file_info_for_variation(
Version version, Episode episode, uint8_t area, bool is_solo) {
const vector<AreaMapFileInfo>* multi_index = nullptr;
const vector<AreaMapFileInfo>* solo_index = nullptr;
if (is_pre_v1(version)) {
multi_index = &map_file_info_dc_protos;
} else {
switch (episode) {
case Episode::EP1:
multi_index = &map_file_info.at(0).at(0);
solo_index = &map_file_info.at(0).at(1);
break;
case Episode::EP2:
multi_index = &map_file_info.at(1).at(0);
solo_index = &map_file_info.at(1).at(1);
break;
case Episode::EP3: {
static const AreaMapFileInfo blank_info = {nullptr, {}, {}};
return blank_info;
}
case Episode::EP4:
multi_index = &map_file_info.at(2).at(0);
solo_index = &map_file_info.at(2).at(1);
break;
default:
throw invalid_argument("episode has no maps");
}
}
if (is_solo && solo_index) {
const auto& ret = solo_index->at(area);
if (ret.name_token) {
return ret;
}
}
return multi_index->at(area);
}
void generate_variations(
parray<le_uint32_t, 0x20>& variations,
shared_ptr<PSOLFGEncryption> random_crypt,
Version version,
Episode episode,
bool is_solo) {
const auto& ep_index = map_file_info_for_episode(episode);
for (size_t z = 0; z < 0x10; z++) {
const AreaMapFileIndex* a = nullptr;
if (is_solo) {
a = &ep_index.at(true).at(z);
}
if (!a || !a->name_token) {
a = &ep_index.at(false).at(z);
}
if (!a->name_token) {
const auto& a = file_info_for_variation(version, episode, z, is_solo);
if (!a.name_token) {
variations[z * 2 + 0] = 0;
variations[z * 2 + 1] = 0;
} else {
variations[z * 2 + 0] = (a->variation1_values.size() < 2) ? 0 : (random_crypt->next() % a->variation1_values.size());
variations[z * 2 + 1] = (a->variation2_values.size() < 2) ? 0 : (random_crypt->next() % a->variation2_values.size());
variations[z * 2 + 0] = (a.variation1_values.size() < 2) ? 0 : (random_crypt->next() % a.variation1_values.size());
variations[z * 2 + 1] = (a.variation2_values.size() < 2) ? 0 : (random_crypt->next() % a.variation2_values.size());
}
}
}
void generate_variations_dc_nte(
parray<le_uint32_t, 0x20>& variations,
shared_ptr<PSOLFGEncryption> random_crypt) {
static const std::array<uint32_t, 0x20> maxes(
{1, 1, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1});
for (size_t z = 0; z < 0x20; z++) {
variations[z] = (maxes[z] < 2) ? 0 : (random_crypt->next() % maxes[z]);
}
}
vector<string> map_filenames_for_variation(
Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies) {
// Map filenames are like map_<name_token>[_VV][_VV][_off]<e|o>[_s].dat
// name_token comes from AreaMapFileIndex
Version version,
Episode episode,
GameMode mode,
uint8_t floor,
uint32_t var1,
uint32_t var2,
bool is_enemies) {
// Map filenames are like map_<name_token>_<VV>_<VV>(_off)?(e|o)(_s|_c1)?.dat
// name_token comes from AreaMapFileInfo
// _VV are the values from the variation<1|2>_values vector (in contrast to
// the values sent in the 64 command, which are INDEXES INTO THAT VECTOR)
// _off or _s are used for solo mode (try both - city uses _s whereas levels
// use _off apparently)
// _c1 is used for the city map in Challenge mode (which we don't load,
// since it contains only NPCs and not enemies)
// e|o specifies what kind of data: e = enemies, o = objects
const auto& ep_index = map_file_info_for_episode(episode);
const AreaMapFileIndex* a = nullptr;
if (is_solo) {
a = &ep_index.at(true).at(floor);
}
if (!a || !a->name_token) {
a = &ep_index.at(false).at(floor);
}
if (!a->name_token) {
const auto& a = file_info_for_variation(version, episode, floor, mode == GameMode::SOLO);
if (!a.name_token) {
return vector<string>();
}
string filename = "map_";
filename += a->name_token;
if (!a->variation1_values.empty()) {
filename += string_printf("_%02" PRIX32, a->variation1_values.at(var1));
filename += a.name_token;
if (!a.variation1_values.empty()) {
filename += string_printf("_%02" PRIX32, a.variation1_values.at(var1));
}
if (!a->variation2_values.empty()) {
filename += string_printf("_%02" PRIX32, a->variation2_values.at(var2));
if (!a.variation2_values.empty()) {
filename += string_printf("_%02" PRIX32, a.variation2_values.at(var2));
}
// Try both _off<e|o>.dat and <e|o>_s.dat suffixes first before falling back
// to non-solo version
vector<string> ret;
if (is_enemies) {
if (is_solo) {
if (mode == GameMode::SOLO) {
ret.emplace_back(filename + "_offe.dat");
ret.emplace_back(filename + "e_s.dat");
} else if (floor == 0) {
if (mode == GameMode::BATTLE) {
ret.emplace_back(filename + "e_d.dat");
} else if (mode == GameMode::CHALLENGE) {
ret.emplace_back(filename + "e_c1.dat");
}
}
ret.emplace_back(filename + "e.dat");
} else {
if (is_solo) {
if (mode == GameMode::SOLO) {
ret.emplace_back(filename + "_offo.dat");
ret.emplace_back(filename + "o_s.dat");
} else if (floor == 0) {
if (mode == GameMode::BATTLE) {
ret.emplace_back(filename + "o_d.dat");
} else if (mode == GameMode::CHALLENGE) {
ret.emplace_back(filename + "o_c1.dat");
}
}
ret.emplace_back(filename + "o.dat");
}
+11 -6
View File
@@ -15,6 +15,8 @@
#include "Text.hh"
struct Map {
static const char* name_for_object_type(uint16_t type);
struct SectionHeader {
enum class Type {
END = 0,
@@ -206,6 +208,8 @@ struct Map {
// TODO: Add more fields in here if we ever care about them. Currently we
// only care about boxes with fixed item drops.
size_t source_index;
uint16_t object_id;
uint8_t floor;
uint16_t base_type;
uint16_t section;
float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6
@@ -213,7 +217,6 @@ struct Map {
uint32_t param4;
uint32_t param5;
uint32_t param6;
uint8_t floor;
bool item_drop_checked;
std::string str() const;
@@ -228,12 +231,13 @@ struct Map {
ITEM_DROPPED = 0x10,
};
size_t source_index;
uint16_t enemy_id;
EnemyType type;
uint8_t floor;
uint8_t state_flags;
uint8_t last_hit_by_client_id;
Enemy(size_t source_index, uint8_t floor, EnemyType type);
Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type);
std::string str() const;
} __attribute__((packed));
@@ -305,6 +309,9 @@ struct Map {
uint32_t rare_seed,
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
Enemy& find_enemy(uint8_t floor, EnemyType type);
static std::string disassemble_quest_data(const void* data, size_t size);
PrefixedLogger log;
@@ -346,10 +353,8 @@ private:
void generate_variations(
parray<le_uint32_t, 0x20>& variations,
std::shared_ptr<PSOLFGEncryption> random,
Version version,
Episode episode,
bool is_solo);
void generate_variations_dc_nte(
parray<le_uint32_t, 0x20>& variations,
std::shared_ptr<PSOLFGEncryption> random);
std::vector<std::string> map_filenames_for_variation(
Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
Version version, Episode episode, GameMode mode, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
+13 -18
View File
@@ -2001,11 +2001,16 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
}
auto s = l->require_server_state();
// For Challenge quests, don't replace the map now - the leader will send an
// 02DF command to create overlays, which also replaces the map.
if ((l->base_version == Version::BB_V4) && l->map && (l->quest->challenge_template_index < 0)) {
// For BB Challenge quests, don't replace the map now - the leader will send
// an 02DF command to create overlays, which also replaces the map. (We do
// this because 02DF is also sent when a challenge is failed and retried,
// which reloads the map and recreates character overlays anyway.)
if ((l->base_version != Version::BB_V4) || (l->quest->challenge_template_index < 0)) {
l->load_maps();
}
// Delete all floor items
for (auto& m : l->floor_item_managers) {
m.clear();
}
@@ -2443,9 +2448,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
if (game->count_clients() == 1) {
// No one was in the game before, so the object and enemy state is lost;
// regenerate it as if the game was just created
if ((game->base_version == Version::BB_V4) && game->map) {
game->load_maps();
}
game->load_maps();
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
}
break;
@@ -4175,23 +4178,15 @@ shared_ptr<Lobby> create_game_generic(
bool is_solo = (game->mode == GameMode::SOLO);
// Generate the map variations
if (game->is_ep3()) {
game->variations.clear(0);
} else if ((c->version() == Version::DC_NTE) || (c->version() == Version::DC_V1_11_2000_PROTOTYPE)) {
generate_variations_dc_nte(game->variations, game->random_crypt);
} else {
generate_variations(game->variations, game->random_crypt, game->episode, is_solo);
}
if (game->mode == GameMode::CHALLENGE) {
game->rare_enemy_rates = s->rare_enemy_rates_challenge;
} else {
game->rare_enemy_rates = s->rare_enemy_rates_by_difficulty.at(game->difficulty);
}
if (game->base_version == Version::BB_V4) {
game->load_maps();
}
generate_variations(game->variations, game->random_crypt, game->base_version, game->episode, is_solo);
game->load_maps();
return game;
}
+164 -120
View File
@@ -2097,7 +2097,7 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
cmd.rt_index = in_cmd.rt_index;
cmd.x = in_cmd.x;
cmd.z = in_cmd.z;
cmd.ignore_def = 1;
cmd.ignore_def = in_cmd.ignore_def;
cmd.effective_area = in_cmd.effective_area;
} else {
const auto& in_cmd = check_size_t<G_StandardDropItemRequest_DC_6x60>(data, size);
@@ -2109,83 +2109,154 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
cmd.rt_index = in_cmd.rt_index;
cmd.x = in_cmd.x;
cmd.z = in_cmd.z;
cmd.ignore_def = 1;
cmd.ignore_def = in_cmd.ignore_def;
cmd.effective_area = in_cmd.floor;
}
auto generate_item = [&]() -> ItemCreator::DropResult {
if (cmd.rt_index == 0x30) {
if (l->map) {
auto& object = l->map->objects.at(cmd.entity_id);
Map::Object* map_object = nullptr;
Map::Enemy* map_enemy = nullptr;
bool ignore_def = (cmd.ignore_def != 0);
uint8_t effective_rt_index = 0xFF;
if (cmd.rt_index == 0x30) {
if (l->map) {
map_object = &l->map->objects.at(cmd.entity_id);
l->log.info("Drop check for K-%hX %c %s",
map_object->object_id, ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type));
if (cmd.floor != map_object->floor) {
l->log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, map_object->floor);
}
if (is_v1_or_v2(l->base_version) && (l->base_version != Version::GC_NTE)) {
// V1 and V2 don't have 6xA2, so we can't get ignore_def or the object
// parameters from the client on those versions
cmd.param3 = map_object->param3;
cmd.param4 = map_object->param4;
cmd.param5 = map_object->param5;
cmd.param6 = map_object->param6;
}
bool object_ignore_def = (map_object->param1 > 0.0);
if (ignore_def != object_ignore_def) {
l->log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)",
ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", map_object->param1);
}
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
send_text_message_printf(c, "$C5K-%hX %c %s",
map_object->object_id, ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type));
}
}
if (cmd.floor != object.floor) {
l->log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, object.floor);
}
bool object_ignore_def = (object.param1 > 0.0);
if (cmd.ignore_def != object_ignore_def) {
l->log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)",
cmd.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", object.param1);
} else {
if (l->map) {
map_enemy = &l->map->enemies.at(cmd.entity_id);
l->log.info("Drop check for E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type));
effective_rt_index = rare_table_index_for_enemy_type(map_enemy->type);
// rt_indexes in Episode 4 don't match those sent in the command; we just
// ignore what the client sends.
if ((l->episode != Episode::EP4) && (cmd.rt_index != effective_rt_index)) {
l->log.warning("rt_index %02hhX from command does not match entity\'s expected index %02" PRIX32,
cmd.rt_index, effective_rt_index);
if (!is_v4(l->base_version)) {
effective_rt_index = cmd.rt_index;
}
}
if (cmd.floor != map_enemy->floor) {
l->log.warning("Floor %02hhX from command does not match entity\'s expected floor %02hhX",
cmd.floor, map_enemy->floor);
}
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
send_text_message_printf(c, "$C5E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type));
}
}
}
if (cmd.ignore_def) {
bool should_drop = true;
if (map_object) {
if (map_object->item_drop_checked) {
l->log.info("Drop check has already occurred for K-%04hX; skipping it", map_object->object_id);
should_drop = false;
} else {
map_object->item_drop_checked = true;
}
}
if (map_enemy) {
if (map_enemy->state_flags & Map::Enemy::Flag::ITEM_DROPPED) {
l->log.info("Drop check has already occurred for E-%04hX; skipping it", map_enemy->enemy_id);
should_drop = false;
} else {
map_enemy->state_flags |= Map::Enemy::Flag::ITEM_DROPPED;
}
}
if (should_drop) {
auto generate_item = [&]() -> ItemCreator::DropResult {
if (cmd.rt_index == 0x30) {
if (ignore_def) {
l->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area);
return l->item_creator->on_box_item_drop(cmd.entity_id, cmd.effective_area);
return l->item_creator->on_box_item_drop(cmd.effective_area);
} else {
l->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")",
cmd.entity_id.load(), cmd.effective_area, object.param3, object.param4, object.param5, object.param6);
return l->item_creator->on_specialized_box_item_drop(cmd.entity_id, cmd.effective_area, object.param3, object.param4, object.param5, object.param6);
cmd.entity_id.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load());
return l->item_creator->on_specialized_box_item_drop(cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6);
}
} else if (cmd.ignore_def) {
l->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area);
return l->item_creator->on_box_item_drop(cmd.entity_id, cmd.effective_area);
} else {
l->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")",
cmd.entity_id.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load());
return l->item_creator->on_specialized_box_item_drop(
cmd.entity_id, cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6);
l->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area);
return l->item_creator->on_monster_item_drop(effective_rt_index, cmd.effective_area);
}
};
} else {
l->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area);
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
case Lobby::DropMode::CLIENT:
throw logic_error("unhandled simple drop mode");
case Lobby::DropMode::SERVER_SHARED:
case Lobby::DropMode::SERVER_DUPLICATE: {
// TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare
// items? Maybe by a factor of l->count_clients()?
auto res = generate_item();
if (res.item.empty()) {
l->log.info("No item was created");
} else {
string name = s->describe_item(l->base_version, res.item, false);
l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
if (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE) {
for (const auto& lc : l->clients) {
if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) {
res.item.id = l->generate_item_id(0xFF);
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
send_drop_item_to_channel(s, lc->channel, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
if (res.is_from_rare_table) {
send_rare_notification_if_needed(lc, res.item);
}
}
}
uint8_t effective_rt_index = cmd.rt_index;
if (l->map) {
const auto& enemy = l->map->enemies.at(cmd.entity_id);
effective_rt_index = rare_table_index_for_enemy_type(enemy.type);
// rt_indexes in Episode 4 don't match those sent in the command; we just
// ignore what the client sends.
if ((l->episode != Episode::EP4) && (cmd.rt_index != effective_rt_index)) {
l->log.warning("rt_index %02hhX from command does not match entity\'s expected index %02" PRIX32,
cmd.rt_index, effective_rt_index);
}
if (cmd.floor != enemy.floor) {
l->log.warning("Floor %02hhX from command does not match entity\'s expected floor %02hhX",
cmd.floor, enemy.floor);
} else {
res.item.id = l->generate_item_id(0xFF);
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients",
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load());
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F);
send_drop_item_to_lobby(l, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
if (res.is_from_rare_table) {
for (auto lc : l->clients) {
if (lc) {
send_rare_notification_if_needed(lc, res.item);
}
}
}
}
}
break;
}
return l->item_creator->on_monster_item_drop(cmd.entity_id, effective_rt_index, cmd.effective_area);
}
};
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
case Lobby::DropMode::CLIENT:
throw logic_error("unhandled simple drop mode");
case Lobby::DropMode::SERVER_SHARED:
case Lobby::DropMode::SERVER_DUPLICATE: {
// TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare
// items? Maybe by a factor of l->count_clients()?
auto res = generate_item();
if (res.item.empty()) {
l->log.info("No item was created");
} else {
string name = s->describe_item(l->base_version, res.item, false);
l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
if (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE) {
for (const auto& lc : l->clients) {
if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) {
case Lobby::DropMode::SERVER_PRIVATE: {
for (const auto& lc : l->clients) {
if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) {
auto res = generate_item();
if (res.item.empty()) {
l->log.info("No item was created for %s", lc->channel.name.c_str());
} else {
string name = s->describe_item(l->base_version, res.item, false);
l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
res.item.id = l->generate_item_id(0xFF);
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
@@ -2196,54 +2267,12 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
}
}
}
} else {
res.item.id = l->generate_item_id(0xFF);
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients",
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load());
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F);
send_drop_item_to_lobby(l, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
if (res.is_from_rare_table) {
for (auto lc : l->clients) {
if (lc) {
send_rare_notification_if_needed(lc, res.item);
}
}
}
}
break;
}
break;
default:
throw logic_error("invalid drop mode");
}
case Lobby::DropMode::SERVER_PRIVATE: {
for (const auto& lc : l->clients) {
if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) {
auto res = generate_item();
if (res.item.empty()) {
l->log.info("No item was created for %s", lc->channel.name.c_str());
} else {
string name = s->describe_item(l->base_version, res.item, false);
l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
res.item.id = l->generate_item_id(0xFF);
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
send_drop_item_to_channel(s, lc->channel, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
if (res.is_from_rare_table) {
send_rare_notification_if_needed(lc, res.item);
}
}
}
}
break;
}
default:
throw logic_error("invalid drop mode");
}
if (cmd.rt_index == 0x30) {
l->item_creator->set_box_destroyed(cmd.entity_id);
} else {
l->item_creator->set_monster_destroyed(cmd.entity_id);
}
}
@@ -2290,30 +2319,45 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
forward_subcommand(c, command, flag, data, size);
if (is_v3(c->version())) {
bool should_send_boss_drop_req = false;
if (is_v3(c->version()) && (l->drop_mode != Lobby::DropMode::DISABLED)) {
EnemyType boss_enemy_type = EnemyType::NONE;
bool is_ep2 = (l->episode == Episode::EP2);
if ((l->episode == Episode::EP1) && (c->floor == 0x0E)) {
// On Normal, Dark Falz does not have a third phase, so send the drop
// request after the end of the second phase. On all other difficulty
// levels, send it after the third phase.
if (((difficulty == 0) && (flag_index == 0x0035)) ||
((difficulty != 0) && (flag_index == 0x0037))) {
should_send_boss_drop_req = true;
if ((difficulty == 0) && (flag_index == 0x0035)) {
boss_enemy_type = EnemyType::DARK_FALZ_2;
} else if ((difficulty != 0) && (flag_index == 0x0037)) {
boss_enemy_type = EnemyType::DARK_FALZ_3;
}
} else if (is_ep2 && (flag_index == 0x0057) && (c->floor == 0x0D)) {
should_send_boss_drop_req = true;
boss_enemy_type = EnemyType::OLGA_FLOW_2;
}
if (should_send_boss_drop_req) {
auto c = l->clients.at(l->leader_id);
if (c) {
G_StandardDropItemRequest_PC_V3_BB_6x60 req = {
if (boss_enemy_type != EnemyType::NONE) {
l->log.info("Creating item from final boss (%s)", name_for_enum(boss_enemy_type));
uint16_t enemy_id = 0xFFFF;
if (l->map) {
try {
const auto& enemy = l->map->find_enemy(c->floor, boss_enemy_type);
enemy_id = enemy.enemy_id;
if (c->floor != enemy.floor) {
l->log.warning("Floor %02" PRIX32 " from client does not match entity\'s expected floor %02hhX", c->floor, enemy.floor);
}
l->log.info("Found enemy E-%hX on floor %" PRIX32, enemy_id, enemy.floor);
} catch (const out_of_range&) {
l->log.warning("Could not find enemy on floor %" PRIX32 "; unable to determine enemy type", c->floor);
}
}
if (boss_enemy_type != EnemyType::NONE) {
G_StandardDropItemRequest_PC_V3_BB_6x60 drop_req = {
{
{0x60, 0x06, 0x0000},
static_cast<uint8_t>(c->floor),
static_cast<uint8_t>(is_ep2 ? 0x4E : 0x2F),
0x0B4F,
rare_table_index_for_enemy_type(boss_enemy_type),
enemy_id == 0xFFFF ? 0x0B4F : enemy_id,
is_ep2 ? -9999.0f : 10160.58984375f,
0.0f,
2,
@@ -2321,7 +2365,7 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
},
0x01,
{}};
send_command_t(c, 0x62, l->leader_id, req);
on_entity_drop_item_request(c, 0x62, l->leader_id, &drop_req, sizeof(drop_req));
}
}
}
+35
View File
@@ -246,3 +246,38 @@ uint32_t default_specific_version_for_version(Version version, int64_t sub_versi
return 0x00000000;
}
}
const char* file_path_token_for_version(Version version) {
switch (version) {
case Version::PC_PATCH:
return "pc-patch";
case Version::BB_PATCH:
return "bb-patch";
case Version::DC_NTE:
return "dc-nte";
case Version::DC_V1_11_2000_PROTOTYPE:
return "dc-11-2000";
case Version::DC_V1:
return "dc-v1";
case Version::DC_V2:
return "dc-v2";
case Version::PC_NTE:
return "pc-nte";
case Version::PC_V2:
return "pc-v2";
case Version::GC_NTE:
return "gc-nte";
case Version::GC_V3:
return "gc-v3";
case Version::GC_EP3_NTE:
return "gc-ep3-nte";
case Version::GC_EP3:
return "gc-ep3";
case Version::XB_V3:
return "xb-v3";
case Version::BB_V4:
return "bb-v4";
default:
throw runtime_error("invalid game version");
}
}
+2
View File
@@ -136,3 +136,5 @@ template <>
const char* name_for_enum<ServerBehavior>(ServerBehavior behavior);
template <>
ServerBehavior enum_for_name<ServerBehavior>(const char* name);
const char* file_path_token_for_version(Version version);