implement Episode 3 lobby banners
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user