diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 5d7871ac..6b0c5d3d 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2299,19 +2299,21 @@ struct S_RankUpdate_Ep3_B7 { struct S_UpdateMediaHeader_Ep3_B9 { // Valid values for the type field: - // 1: GVM file - // 2: Unknown; probably BML file - // 3: Unknown; probably BML file - // 4: Unknown; appears to be completely ignored - // Any other value: entire command is ignored - // For types 2 and 3, the game looks for various tokens in the decompressed - // data; specifically '****', 'GCAM', 'GJBM', 'GJTL', 'GLIM', 'GMDM', 'GSSM', - // 'NCAM', 'NJBM', 'NJCA', 'NLIM', 'NMDM', and 'NSSM'. Of these, 'GJTL', - // 'NMDM', and 'NSSM' are found in some of the game's existing BML files, but - // the others don't seem to be anywhere on the disc. 'NJBM' is found in - // psohistory_e.sfd, but not in any other files. + // 1: Texture set (GVM file) + // 2: Model + // 3: Animation + // 4: Delete all previous media updates + // Any other value: entire command is ignored + // A texture can be displayed without a model or animation. A model requires a + // texture (sent in a separate B9 command with the same location_flags), but + // does not require an animation - it will just stand still without one. An + // animation requires both a texture and model with the same location_flags. + // For models and animations, the game looks for various tokens in the + // decompressed data; specifically '****', 'GCAM', 'GJBM', 'GJTL', 'GLIM', + // 'GMDM', 'GSSM', 'NCAM', 'NJBM', 'NJCA', 'NLIM', 'NMDM', and 'NSSM'. le_uint32_t type = 0; - // which is a bit field specifying which positions to set. The bits are: + // location_flags is a bit field specifying where the banner or object should + // appear. The bits are: // 00000001: South above-counter banner (facing away from teleporters) // 00000002: West above-counter banner // 00000004: North above-counter banner (facing toward jukebox) @@ -2348,9 +2350,9 @@ struct S_UpdateMediaHeader_Ep3_B9 { // banners - if a banner is sent in these locations, it appears sideways and // halfway submerged in the floor, and has no collision. Furthermore, it seems // that up to 8 different banners or models may be set simultaneously (though - // each may appear in more than one position). If 8 B9 commands have been - // received, further B9 commands are ignored. - le_uint32_t which = 0x00000000; + // each may appear in more than one position). If 8 banners or objects already + // exist, further media sent via B9 is ignored. + le_uint32_t location_flags = 0x00000000; // This field specifies the size of the compressed data. The uncompressed size // is not sent anywhere in this command. le_uint16_t size = 0; @@ -2361,11 +2363,10 @@ struct S_UpdateMediaHeader_Ep3_B9 { // client ignores the command. } __packed_ws__(S_UpdateMediaHeader_Ep3_B9, 0x0C); -// B9 (C->S): Confirm received B9 (Episode 3) +// B9 (C->S): Confirm media update (Episode 3) // No arguments // This command is not valid on Episode 3 Trial Edition. -// The client sends this even if it ignores the contents of a B9 command (if -// the data size was too large, or if there were already 8 banners/models). +// The client sends this even if it ignores the contents of a B9 command. // BA: Meseta transaction (Episode 3) // This command is not valid on Episode 3 Trial Edition. diff --git a/src/GVMEncoder.cc b/src/GVMEncoder.cc index 4a7449c4..7cd4339c 100644 --- a/src/GVMEncoder.cc +++ b/src/GVMEncoder.cc @@ -11,13 +11,16 @@ using namespace std; struct GVMFileEntry { be_uint16_t file_num; pstring name; - parray unknown_a1; + uint8_t format_flags; // Same as in GVRHeader + GVRDataFormat data_format; // Same as in GVRHeader + be_uint16_t dimensions; // As powers of two in low nybbles (so e.g. 128x128 = 0x0055) + be_uint32_t global_index; } __packed_ws__(GVMFileEntry, 0x26); struct GVMFileHeader { - be_uint32_t magic; // 'GVMH' + be_uint32_t signature; // 'GVMH' le_uint32_t header_size; - be_uint16_t flags; + be_uint16_t flags; // Specifies which fields are present in GVMFileEntries; we always use 0xF (all fields present) be_uint16_t num_files; } __packed_ws__(GVMFileHeader, 0x0C); @@ -31,19 +34,24 @@ struct GVRHeader { be_uint16_t height; } __packed_ws__(GVRHeader, 0x10); -string encode_gvm(const Image& img, GVRDataFormat data_format) { - if (img.get_width() > 0xFFFF) { - throw runtime_error("image is too wide to be encoded as a GVR texture"); - } - if (img.get_height() > 0xFFFF) { - throw runtime_error("image is too tall to be encoded as a GVR texture"); - } - if (img.get_width() & 3) { - throw runtime_error("image width is not a multiple of 4"); - } - if (img.get_height() & 3) { - throw runtime_error("image height is not a multiple of 4"); +string encode_gvm(const Image& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index) { + int8_t dimensions_field = -2; + { + size_t h = img.get_height(); + size_t w = img.get_width(); + if ((h != w) || (w & (w - 1)) || (h & (h - 1))) { + throw runtime_error("image must be square and dimensions must be powers of 2"); + } + for (w >>= 1; w; w >>= 1, dimensions_field++) { + } + if (dimensions_field < 1) { + throw runtime_error("image is too small"); + } + if (dimensions_field > 0xF) { + throw runtime_error("image is too large"); + } } + size_t pixel_count = img.get_width() * img.get_height(); size_t pixel_bytes = 0; switch (data_format) { @@ -59,11 +67,14 @@ string encode_gvm(const Image& img, GVRDataFormat data_format) { } StringWriter w; - w.put({.magic = 0x47564D48, .header_size = 0x48, .flags = 0x010F, .num_files = 1}); + w.put({.signature = 0x47564D48, .header_size = 0x48, .flags = 0x000F, .num_files = 1}); GVMFileEntry file_entry; file_entry.file_num = 0; - file_entry.name.encode("img", 1); - file_entry.unknown_a1.clear(0); + file_entry.name.encode(internal_name, 1); + file_entry.data_format = data_format; + file_entry.format_flags = 0; + file_entry.dimensions = (dimensions_field << 4) | dimensions_field; + file_entry.global_index = global_index; w.put(file_entry); w.extend_to(0x50, 0x00); w.put({.magic = 0x47565254, diff --git a/src/GVMEncoder.hh b/src/GVMEncoder.hh index 10c8a527..8137c106 100644 --- a/src/GVMEncoder.hh +++ b/src/GVMEncoder.hh @@ -19,7 +19,7 @@ enum class GVRDataFormat : uint8_t { DXT1 = 0x0E, }; -std::string encode_gvm(const Image& img, GVRDataFormat data_format); +std::string encode_gvm(const Image& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index); constexpr uint16_t encode_rgb565(uint8_t r, uint8_t g, uint8_t b) { return ((r << 8) & 0xF800) | ((g << 3) & 0x07E0) | ((b >> 3) & 0x001F); diff --git a/src/Main.cc b/src/Main.cc index fb77a17a..8763ab92 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -790,7 +790,7 @@ Action a_encode_gvm( } else { img = Image(stdin); } - string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565); + string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0); write_output_data(args, encoded.data(), encoded.size(), "gvm"); }); diff --git a/src/ServerState.cc b/src/ServerState.cc index fbc8f835..539a442d 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -795,18 +795,25 @@ void ServerState::load_config_early() { if (!this->is_replay) { this->ep3_lobby_banners.clear(); + size_t banner_index = 0; for (const auto& it : this->config_json->get("Episode3LobbyBanners", JSON::list()).as_list()) { string path = "system/ep3/banners/" + it->at(2).as_string(); string compressed_gvm_data; string decompressed_gvm_data; - if (ends_with(path, ".gvm.prs")) { + string lower_path = tolower(path); + if (ends_with(lower_path, ".gvm.prs")) { compressed_gvm_data = load_file(path); - } else if (ends_with(path, ".gvm")) { + } else if (ends_with(lower_path, ".gvm")) { decompressed_gvm_data = load_file(path); - } else if (ends_with(path, ".bmp")) { + } else if (ends_with(lower_path, ".bmp")) { Image img(path); - decompressed_gvm_data = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565); + decompressed_gvm_data = encode_gvm( + img, + img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, + string_printf("bnr%zu", banner_index), + 0x80 | banner_index); + banner_index++; } else { throw runtime_error(string_printf("banner %s is in an unknown format", path.c_str())); }