add further learnings about Ep3 B9 command

This commit is contained in:
Martin Michelsen
2024-04-15 22:53:14 -07:00
parent d6edf1b24d
commit 1870273f89
5 changed files with 61 additions and 42 deletions
+19 -18
View File
@@ -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.
+29 -18
View File
@@ -11,13 +11,16 @@ using namespace std;
struct GVMFileEntry {
be_uint16_t file_num;
pstring<TextEncoding::ASCII, 0x1C> name;
parray<be_uint32_t, 2> 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<GVMFileHeader>({.magic = 0x47564D48, .header_size = 0x48, .flags = 0x010F, .num_files = 1});
w.put<GVMFileHeader>({.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<GVRHeader>({.magic = 0x47565254,
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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");
});
+11 -4
View File
@@ -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()));
}