implement Episode 3 lobby banners

This commit is contained in:
Martin Michelsen
2023-09-07 22:34:07 -07:00
parent bd6102a894
commit cfa4e3b8b0
8 changed files with 236 additions and 0 deletions
+1
View File
@@ -65,6 +65,7 @@ add_executable(newserv
src/FileContentsCache.cc
src/FunctionCompiler.cc
src/GSLArchive.cc
src/GVMEncoder.cc
src/IPFrameInfo.cc
src/IPStackSimulator.cc
src/ItemCreator.cc
+118
View File
@@ -0,0 +1,118 @@
#include "GVMEncoder.hh"
#include <phosg/Encoding.hh>
#include <phosg/Image.hh>
#include <phosg/Strings.hh>
#include "Text.hh"
using namespace std;
static uint16_t encode_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r << 8) & 0xF800) | ((g << 3) & 0x07E0) | ((b >> 3) & 0x001F);
}
static uint16_t encode_rgb5a3(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
if ((a & 0xE0) == 0xE0) {
return 0x8000 | ((r << 7) & 0x7C00) | ((g << 2) & 0x03E0) | ((b >> 3) & 0x001F);
} else {
return ((a << 7) & 0x7000) | ((r << 4) & 0x0F00) | (g & 0x00F0) | ((b >> 4) & 0x000F);
}
}
static uint32_t encode_argb8888(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
return (a << 24) | (r << 16) | (g << 8) | b;
}
struct GVMFileEntry {
be_uint16_t file_num;
parray<char, 28> name;
parray<be_uint32_t, 2> unknown_a1;
} __attribute__((packed));
struct GVMFileHeader {
be_uint32_t magic; // 'GVMH'
le_uint32_t header_size;
be_uint16_t flags;
be_uint16_t num_files;
} __attribute__((packed));
struct GVRHeader {
be_uint32_t magic; // 'GVRT'
le_uint32_t data_size;
be_uint16_t unknown;
uint8_t format_flags; // High 4 bits are pixel format, low 4 are data flags
GVRDataFormat data_format;
be_uint16_t width;
be_uint16_t height;
} __attribute__((packed));
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");
}
size_t pixel_count = img.get_width() * img.get_height();
size_t pixel_bytes = 0;
switch (data_format) {
case GVRDataFormat::RGB565:
case GVRDataFormat::RGB5A3:
pixel_bytes = pixel_count * 2;
break;
case GVRDataFormat::ARGB8888:
pixel_bytes = pixel_count * 2;
break;
default:
throw invalid_argument("cannot encode pixel format");
}
StringWriter w;
w.put<GVMFileHeader>({.magic = 0x47564D48, .header_size = 0x48, .flags = 0x010F, .num_files = 1});
GVMFileEntry file_entry;
file_entry.file_num = 0;
file_entry.name = "img";
file_entry.unknown_a1.clear(0);
w.put(file_entry);
w.extend_to(0x50, 0x00);
w.put<GVRHeader>({.magic = 0x47565254,
.data_size = pixel_bytes + 8,
.unknown = 0,
.format_flags = 0,
.data_format = data_format,
.width = img.get_width(),
.height = img.get_height()});
for (size_t y = 0; y < img.get_height(); y += 4) {
for (size_t x = 0; x < img.get_width(); x += 4) {
for (size_t yy = 0; yy < 4; yy++) {
for (size_t xx = 0; xx < 4; xx++) {
uint64_t a, r, g, b;
img.read_pixel(x + xx, y + yy, &r, &g, &b, &a);
switch (data_format) {
case GVRDataFormat::RGB565:
w.put_u16b(encode_rgb565(r, g, b));
break;
case GVRDataFormat::RGB5A3:
w.put_u16b(encode_rgb5a3(r, g, b, a));
break;
case GVRDataFormat::ARGB8888:
w.put_u32b(encode_argb8888(r, g, b, a));
break;
default:
throw logic_error("cannot encode pixel format");
}
}
}
}
}
return std::move(w.str());
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <phosg/Encoding.hh>
#include <phosg/Image.hh>
#include <phosg/Strings.hh>
#include "Text.hh"
using namespace std;
enum class GVRDataFormat : uint8_t {
INTENSITY_4 = 0x00,
INTENSITY_8 = 0x01,
INTENSITY_A4 = 0x02,
INTENSITY_A8 = 0x03,
RGB565 = 0x04,
RGB5A3 = 0x05,
ARGB8888 = 0x06,
INDEXED_4 = 0x08,
INDEXED_8 = 0x09,
DXT1 = 0x0E,
};
string encode_gvm(const Image& img, GVRDataFormat data_format);
+20
View File
@@ -18,6 +18,7 @@
#include "Compression.hh"
#include "DNSServer.hh"
#include "GSLArchive.hh"
#include "GVMEncoder.hh"
#include "IPStackSimulator.hh"
#include "Loggers.hh"
#include "NetworkAddresses.hh"
@@ -274,6 +275,7 @@ enum class Behavior {
ENCRYPT_GCI_SAVE,
DECRYPT_GCI_SAVE,
DECODE_GCI_SNAPSHOT,
ENCODE_GVM,
FIND_DECRYPTION_SEED,
SALVAGE_GCI,
DECODE_QUEST_FILE,
@@ -316,6 +318,7 @@ static bool behavior_takes_input_filename(Behavior b) {
(b == Behavior::DECRYPT_CHALLENGE_DATA) ||
(b == Behavior::DECRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_GCI_SNAPSHOT) ||
(b == Behavior::ENCODE_GVM) ||
(b == Behavior::SALVAGE_GCI) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_QUEST_FILE) ||
@@ -350,6 +353,7 @@ static bool behavior_takes_output_filename(Behavior b) {
(b == Behavior::DECRYPT_GCI_SAVE) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_GCI_SNAPSHOT) ||
(b == Behavior::ENCODE_GVM) ||
(b == Behavior::ENCODE_QST) ||
(b == Behavior::DISASSEMBLE_QUEST_SCRIPT) ||
(b == Behavior::CONVERT_ITEMRT_REL_TO_JSON) ||
@@ -528,6 +532,8 @@ int main(int argc, char** argv) {
behavior = Behavior::ENCRYPT_GCI_SAVE;
} else if (!strcmp(argv[x], "decode-gci-snapshot")) {
behavior = Behavior::DECODE_GCI_SNAPSHOT;
} else if (!strcmp(argv[x], "encode-gvm")) {
behavior = Behavior::ENCODE_GVM;
} else if (!strcmp(argv[x], "find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "salvage-gci")) {
@@ -649,6 +655,8 @@ int main(int argc, char** argv) {
}
} else if (behavior == Behavior::DECODE_GCI_SNAPSHOT) {
filename += ".bmp";
} else if (behavior == Behavior::ENCODE_GVM) {
filename += ".gvm";
} else if (behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) {
filename += ".txt";
} else if (behavior == Behavior::CONVERT_ITEMRT_REL_TO_JSON) {
@@ -1011,6 +1019,18 @@ int main(int argc, char** argv) {
break;
}
case Behavior::ENCODE_GVM: {
Image img;
if (input_filename && strcmp(input_filename, "-")) {
img = Image(input_filename);
} else {
img = Image(stdin);
}
string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565);
write_output_data(encoded.data(), encoded.size());
break;
}
case Behavior::SALVAGE_GCI: {
uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF;
if (system_filename) {
+3
View File
@@ -2481,6 +2481,9 @@ static void on_61_98(shared_ptr<ServerState> s, shared_ptr<Client> c,
// login sequence.
if ((c->flags & Client::Flag::IS_EPISODE_3) && !(c->flags & Client::Flag::HAS_EP3_CARD_DEFS)) {
send_ep3_card_list_update(s, c);
for (const auto& banner : s->ep3_lobby_banners) {
send_ep3_media_update(c, banner.type, banner.which, banner.data);
}
auto team = c->ep3_tournament_team.lock();
auto tourn = team ? team->tournament.lock() : nullptr;
if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) {
+19
View File
@@ -3,10 +3,12 @@
#include <string.h>
#include <memory>
#include <phosg/Image.hh>
#include <phosg/Network.hh>
#include "Compression.hh"
#include "FileContentsCache.hh"
#include "GVMEncoder.hh"
#include "IPStackSimulator.hh"
#include "Loggers.hh"
#include "NetworkAddresses.hh"
@@ -650,6 +652,23 @@ void ServerState::parse_config(const JSON& json) {
.card_name = it.first});
}
for (const auto& it : json.get("Episode3LobbyBanners", JSON::list()).as_list()) {
Image img("system/ep3/banners/" + it->at(2).as_string());
string gvm = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565);
if (gvm.size() > 0x37000) {
throw runtime_error(string_printf("banner %s is too large (0x%zX bytes; maximum size is 0x37000 bytes)", it->at(2).as_string().c_str(), gvm.size()));
}
string compressed = prs_compress_optimal(gvm.data(), gvm.size());
if (compressed.size() > 0x3800) {
throw runtime_error(string_printf("banner %s cannot be compressed small enough (0x%zX bytes; maximum size is 0x3800 bytes compressed)", it->at(2).as_string().c_str(), compressed.size()));
}
config_log.info("Loaded Episode 3 lobby banner %s (0x%zX -> 0x%zX bytes)", it->at(2).as_string().c_str(), gvm.size(), compressed.size());
this->ep3_lobby_banners.emplace_back(
Ep3LobbyBannerEntry{.type = static_cast<uint32_t>(it->at(0).as_int()),
.which = static_cast<uint32_t>(it->at(1).as_int()),
.data = std::move(compressed)});
}
{
auto parse_ep3_ex_result_cmd = [&](const JSON& src) -> shared_ptr<G_SetEXResultValues_GC_Ep3_6xB4x4B> {
shared_ptr<G_SetEXResultValues_GC_Ep3_6xB4x4B> ret(new G_SetEXResultValues_GC_Ep3_6xB4x4B());
+6
View File
@@ -103,6 +103,12 @@ struct ServerState {
std::string card_name;
};
std::vector<CardAuctionPoolEntry> ep3_card_auction_pool;
struct Ep3LobbyBannerEntry {
uint32_t type = 1;
uint32_t which; // See B9 documentation in CommandFormats.hh
std::string data;
};
std::vector<Ep3LobbyBannerEntry> ep3_lobby_banners;
std::shared_ptr<LicenseManager> license_manager;
+45
View File
@@ -382,6 +382,51 @@
"Thread +": [800, 8],
},
// Episode 3 lobby banners. Currently only images are supported.
// Banners are specified as lists of [type, which, filename]. Files are
// expected to be BMP or PPM files in the system/ep3/banners directory. type
// should be 1 for image files. which is a bitmask specifying where in the
// lobby the banner 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)
// 00000008: East above-counter banner
// 00000010: Banner above west (left) teleporter
// 00000020: Banner above east (right) teleporter
// 00000040: Banner at south end of lobby (opposite the jukebox)
// 00000080: Immediately left of 00000040
// 00000100: Immediately right of 00000040
// 00000200: Same as 00000080, but further left and at a slight inward angle
// 00000400: Same as 00000100, but further right and at a slight inward angle
// 00000800: Banner at north end of lobby, above the jukebox
// 00001000: Immediately right of 00000800
// 00002000: Immediately left of 00000800
// 00004000: Same as 00001000, but further right and at a slight inward angle
// 00008000: Same as 00002000, but further left and at a slight inward angle
// 00010000: Banners at west AND east ends of lobby, next to battle tables
// 00020000: Immediately left of 00001000 (2 banners)
// 00040000: Immediately right of 00001000 (2 banners)
// 00080000: Banners on southwest AND southeast ends of the lobby
// 00100000: Banners on south-southwest AND south-southeast ends of the lobby
// 00200000: Floor banners in front of the counter (4 banners)
// 00400000: Banners on both small walls in front of the battle tables
// 00800000: On southern platform
// 01000000: In front of jukebox
// 02000000: In western battle table corner (next to 4-player tables)
// 04000000: In eastern battle table corner (next to 2-player tables)
// 08000000: In southeastern battle table corner (next to 2-player tables)
// 10000000: In southwestern battle table corner (next to 4-player tables)
// 20000000: Just north-northwest of the counter
// 40000000: In front of the small wall in front of the 2-player battle tables
// 80000000: Inside the lobby counter, facing southeast
// So, to make the image system/ep3/banners/test-image.bmp appear in the lobby
// above both the left and right teleporters, you would set
// Episode3LobbyBanners like so:
// "Episode3LobbyBanners": [
// [1, 0x00000030, "test-image.bmp"],
// ],
"Episode3LobbyBanners": [],
// Quest category configuration. See README.md for information on how quest
// files should be named. This list specifies the quest category names and
// descriptions. (We don't use a map here because the category order