Add nitrobanner tool

This tool was made from reverse-engineering the Nitro SDK's makebanner,
and is intended to replace that program.

It has been tested to match the Diamond/Pearl banners as well as the
HeartGold/SoulSilver banners.
This commit is contained in:
tgsm 2021-08-21 20:03:29 -04:00
parent 70f49f7dff
commit 352605c1b5
10 changed files with 478 additions and 2 deletions

View File

@ -90,6 +90,7 @@ KNARC = tools/knarc/knarc$(EXE)
MSGENC = tools/msgenc/msgenc$(EXE)
MWLDARM = tools/mwccarm/$(MWCCVERSION)/mwldarm.exe
MWASMARM = tools/mwccarm/$(MWCCVERSION)/mwasmarm.exe
NITROBANNER = tools/nitrobanner/nitrobanner$(EXE)
SCANINC = tools/scaninc/scaninc$(EXE)
AS = $(WINE) $(MWASMARM)
@ -119,7 +120,6 @@ JSONPROC = $(TOOLS_DIR)/jsonproc/jsonproc$(EXE)
O2NARC = $(TOOLS_DIR)/o2narc/o2narc$(EXE)
GFX = $(TOOLS_DIR)/nitrogfx/nitrogfx$(EXE)
MWASMARM_PATCHER = $(TOOLS_DIR)/mwasmarm_patcher/mwasmarm_patcher$(EXE) -q
MAKEBANNER = $(WINE) $(TOOLS_DIR)/bin/makebanner.exe
MAKEROM = $(WINE) $(TOOLS_DIR)/bin/makerom.exe
FIXROM = $(TOOLS_DIR)/fixrom/fixrom$(EXE)
NTRCOMP = $(WINE) $(TOOLS_DIR)/bin/ntrcomp.exe
@ -286,7 +286,7 @@ $(8BPP_COMP10_NOPAD_NCLR_PAL_FILES): GFX_FLAGS = -bitdepth 8 -nopad -comp 10
######################## Misc #######################
$(BNR): $(TARGET).bsf $(ICON_FILE:%.png=%.gbapal) $(ICON_FILE:%.png=%.4bpp)
$(MAKEBANNER) $< $@
$(NITROBANNER) $< $@
symbols.csv: arm9 arm7
(echo "Name,Location"; $(GREP) " *[0-9A-F]{8} [0-9A-F]{8} \S+ +\w+\t\(\w+\.o\)" arm9/$(BUILD_DIR)/arm9.elf.xMAP arm7/build/arm7.elf.xMAP | $(SED) 's/ *([0-9A-F]{8}) [0-9A-F]{8} \S+ +(\w+)\t\(\w+\.o\)/\2,\1/g' | cut -d: -f2) > $@

1
tools/nitrobanner/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
nitrobanner

21
tools/nitrobanner/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 tgsm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,30 @@
CXXFLAGS := -std=c++17 -O3 -Wall -Wextra -Wpedantic
ifeq ($(OS),Windows_NT)
LDFLAGS += -lstdc++fs
else
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
LDFLAGS += -lstdc++ -lc++ -lc /usr/local/opt/llvm@8/lib/libc++fs.a
else
LDFLAGS += -lstdc++fs
endif
endif
OBJS = \
banner.o \
crc16.o \
main.o
.PHONY: all clean
all: nitrobanner
%.o: %.cpp
$(CXX) -c -o $@ $< $(CXXFLAGS)
clean:
$(RM) nitrobanner nitrobanner.exe *.o
nitrobanner: $(OBJS)
$(CXX) -o $@ $^ $(LDFLAGS)

View File

@ -0,0 +1,276 @@
#include <cstdio>
#include <codecvt>
#include <exception>
#include <fstream>
#include <locale>
#include <string>
#include "banner.h"
#include "crc16.h"
template <typename T>
void SerializeData(std::ofstream& ostream, T data) {
for (std::size_t i = 0; i < sizeof(T); i++) {
ostream.put(static_cast<u8>((data >> (i * 8)) & 0xFF));
}
}
std::wstring GetTitleAndDeveloperFromSpecCommandArgument(std::wifstream& stream, const std::wstring& command_argument) {
std::wstring title_and_developer = command_argument;
constexpr int max_lines = 3;
int current_line = 1;
while (true) {
wchar_t first_char = stream.get();
stream.unget();
if (first_char != L' ') {
return title_and_developer;
}
if (current_line++ == max_lines) {
throw std::runtime_error("Title and developer can not be more than " + std::to_string(max_lines) + " lines");
}
std::wstring line;
std::getline(stream, line);
title_and_developer += '\n';
title_and_developer += line.substr(line.find_first_not_of(L' '), line.size());
}
}
SpecFileData ParseSpecFile(const filesystem::path& specfile_path) {
std::wifstream stream(specfile_path, std::ios::binary);
if (!stream.is_open()) {
throw std::runtime_error("could not open specfile " + specfile_path.string() + " for parsing");
}
// convert utf-16 to utf-32
stream.imbue(std::locale(stream.getloc(), new std::codecvt_utf16<wchar_t, 0x10ffff, std::little_endian>));
// first character of the file has to be U+FEFF.
if (stream.get() != 0xFEFF) {
throw std::runtime_error("specfile must be encoded as UTF-16");
}
SpecFileData specfile_data = {};
for (std::wstring line; std::getline(stream, line);) {
const std::size_t end_of_command_name = line.find(L':');
if (end_of_command_name == std::wstring::npos) {
continue;
}
const std::size_t beginning_of_command_name = line.find_first_not_of(' ');
const std::wstring command_name = line.substr(beginning_of_command_name, end_of_command_name - beginning_of_command_name);
const std::size_t beginning_of_command_argument = line.find_first_of(L": ") + 2;
const std::wstring command_argument = line.substr(beginning_of_command_argument, line.size());
if (command_name == L"Version") {
printf("warning: Version command is currently unsupported, defaulting to version 1\n");
specfile_data.version = 1;
} else if (command_name == L"ImageFile") {
specfile_data.icon_bitmap_filename = command_argument;
} else if (command_name == L"PlttFile") {
specfile_data.icon_palette_filename = command_argument;
} else if (CommandIsForTitleAndDeveloper(command_name)) {
const std::wstring& title_and_developer = GetTitleAndDeveloperFromSpecCommandArgument(stream, command_argument);
if (command_name == L"JP") {
for (wchar_t c : title_and_developer) {
specfile_data.japanese_title += char16_t(c);
}
} else if (command_name == L"EN") {
for (wchar_t c : title_and_developer) {
specfile_data.english_title += char16_t(c);
}
} else if (command_name == L"FR") {
for (wchar_t c : title_and_developer) {
specfile_data.french_title += char16_t(c);
}
} else if (command_name == L"GE") {
for (wchar_t c : title_and_developer) {
specfile_data.german_title += char16_t(c);
}
} else if (command_name == L"IT") {
for (wchar_t c : title_and_developer) {
specfile_data.italian_title += char16_t(c);
}
} else if (command_name == L"SP") {
for (wchar_t c : title_and_developer) {
specfile_data.spanish_title += char16_t(c);
}
}
} else {
printf("warning: unsupported command '%ls', ignoring...\n", command_name.data());
}
}
if (specfile_data.version == 0) {
// no banner version provided, assuming version 1
specfile_data.version = 1;
}
if (specfile_data.icon_bitmap_filename.empty()) {
throw std::runtime_error("missing required ImageFile command (filename of icon bitmap)");
}
if (specfile_data.icon_palette_filename.empty()) {
throw std::runtime_error("missing required PlttFile command (filename of icon palette)");
}
if (specfile_data.japanese_title.empty()) {
throw std::runtime_error("missing required JP command (Japanese title & developer)");
}
if (specfile_data.english_title.empty()) {
throw std::runtime_error("missing required EN command (English title & developer)");
}
if (specfile_data.french_title.empty()) {
throw std::runtime_error("missing required FR command (French title & developer)");
}
if (specfile_data.german_title.empty()) {
throw std::runtime_error("missing required GE command (German title & developer)");
}
if (specfile_data.italian_title.empty()) {
throw std::runtime_error("missing required IT command (Italian title & developer)");
}
if (specfile_data.spanish_title.empty()) {
throw std::runtime_error("missing required SP command (Spanish title & developer)");
}
return specfile_data;
}
IconBitmap GetIconBitmap(const filesystem::path& icon_bitmap_filename) {
if (!filesystem::is_regular_file(icon_bitmap_filename)) {
throw std::runtime_error("icon bitmap file '" + icon_bitmap_filename.string() + "' does not exist / is not a regular file");
}
if (filesystem::file_size(icon_bitmap_filename) != ICON_BITMAP_SIZE) {
throw std::runtime_error("icon bitmap is not 512 bytes");
}
std::ifstream stream(icon_bitmap_filename, std::ios::binary);
if (!stream.is_open()) {
throw std::runtime_error("could not open icon bitmap file '" + icon_bitmap_filename.string() + "' for reading");
}
IconBitmap bitmap = {};
stream.read(reinterpret_cast<char*>(bitmap.data()), ICON_BITMAP_SIZE);
return bitmap;
}
IconPalette GetIconPalette(const filesystem::path& icon_palette_filename) {
if (!filesystem::is_regular_file(icon_palette_filename)) {
throw std::runtime_error("icon palette file '" + icon_palette_filename.string() + "' does not exist / is not a regular file");
}
if (filesystem::file_size(icon_palette_filename) != ICON_PALETTE_SIZE) {
throw std::runtime_error("icon palette is not 32 bytes");
}
std::ifstream stream(icon_palette_filename, std::ios::binary);
if (!stream.is_open()) {
throw std::runtime_error("could not open icon palette file '" + icon_palette_filename.string() + "' for reading");
}
IconPalette palette = {};
stream.read(reinterpret_cast<char*>(palette.data()), ICON_PALETTE_SIZE);
return palette;
}
void OutputBanner(std::ofstream& ostream, const Banner& banner) {
SerializeData<u16>(ostream, banner.version);
SerializeData<u16>(ostream, banner.crc);
for ([[maybe_unused]] u16 i : banner.crc_padding) {
SerializeData<u16>(ostream, 0);
}
for ([[maybe_unused]] u8 i : banner.padding) {
SerializeData<u8>(ostream, 0);
}
for (u8 i : banner.bitmap) {
SerializeData<u8>(ostream, i);
}
for (u8 i : banner.palette) {
SerializeData<u8>(ostream, i);
}
for (char16_t c : banner.japanese_title) {
SerializeData<u16>(ostream, c);
}
for (char16_t c : banner.english_title) {
SerializeData<u16>(ostream, c);
}
for (char16_t c : banner.french_title) {
SerializeData<u16>(ostream, c);
}
for (char16_t c : banner.german_title) {
SerializeData<u16>(ostream, c);
}
for (char16_t c : banner.italian_title) {
SerializeData<u16>(ostream, c);
}
for (char16_t c : banner.spanish_title) {
SerializeData<u16>(ostream, c);
}
}
bool MakeBanner(const filesystem::path& specfile_path, const filesystem::path& outfile_path) {
std::ofstream ostream(outfile_path, std::ios::binary);
if (!ostream.is_open()) {
#ifdef _MSC_VER
printf("error: could not open %ls for writing\n", outfile_path.c_str());
#else
printf("error: could not open %s for writing\n", outfile_path.c_str());
#endif
return false;
}
SpecFileData specfile_data = {};
try {
specfile_data = ParseSpecFile(specfile_path);
} catch (std::runtime_error& e) {
printf("error: %s\n", e.what());
return false;
}
Banner banner = {};
banner.version = specfile_data.version;
banner.bitmap = GetIconBitmap(specfile_data.icon_bitmap_filename);
banner.palette = GetIconPalette(specfile_data.icon_palette_filename);
std::copy(specfile_data.japanese_title.begin(), specfile_data.japanese_title.end(), banner.japanese_title.data());
std::copy(specfile_data.english_title.begin(), specfile_data.english_title.end(), banner.english_title.data());
std::copy(specfile_data.french_title.begin(), specfile_data.french_title.end(), banner.french_title.data());
std::copy(specfile_data.german_title.begin(), specfile_data.german_title.end(), banner.german_title.data());
std::copy(specfile_data.italian_title.begin(), specfile_data.italian_title.end(), banner.italian_title.data());
std::copy(specfile_data.spanish_title.begin(), specfile_data.spanish_title.end(), banner.spanish_title.data());
// checksum the banner data, starting from the icon bitmap and ending at the end of the file.
banner.crc = CalculateCRC16FromBannerData(banner.bitmap.data());
// check against diamond's icon crc
// if (banner.crc != 0x048B) {
// printf("CRC did not match (expected 0x048B, got 0x%04X)\n", banner.crc);
// } else {
// printf("CRC matched\n");
// }
OutputBanner(ostream, banner);
return true;
}

View File

@ -0,0 +1,61 @@
#pragma once
#include <array>
#include <filesystem>
#include "types.h"
constexpr int TITLE_LENGTH = 0x100 / sizeof(u16); // 128 UTF-16 characters
constexpr int ICON_BITMAP_SIZE = 0x200; // 512 bytes
constexpr int ICON_PALETTE_SIZE = 0x20; // 32 bytes
struct SpecFileData {
u16 version;
std::wstring icon_bitmap_filename;
std::wstring icon_palette_filename;
std::u16string japanese_title;
std::u16string english_title;
std::u16string french_title;
std::u16string german_title;
std::u16string italian_title;
std::u16string spanish_title;
};
using IconBitmap = std::array<u8, ICON_BITMAP_SIZE>;
using IconPalette = std::array<u8, ICON_PALETTE_SIZE>;
struct Banner {
u16 version;
u16 crc;
const std::array<u16, 3> crc_padding {};
const std::array<u8, 0x16> padding {};
IconBitmap bitmap {};
IconPalette palette {};
std::array<u16, TITLE_LENGTH> japanese_title {};
std::array<u16, TITLE_LENGTH> english_title {};
std::array<u16, TITLE_LENGTH> french_title {};
std::array<u16, TITLE_LENGTH> german_title {};
std::array<u16, TITLE_LENGTH> italian_title {};
std::array<u16, TITLE_LENGTH> spanish_title {};
};
static_assert(sizeof(Banner) == 0x840, "Size of banner struct is wrong");
template <typename T>
void SerializeData(std::ofstream& ostream, T data);
void ProcessSpecFileCommand(SpecFileData& specfile_data, const std::wstring_view& command_name, const std::wstring_view& command_argument);
SpecFileData ParseSpecFile(const std::filesystem::path& specfile_path);
IconBitmap GetIconBitmap(const std::filesystem::path& icon_bitmap_filename);
IconPalette GetIconPalette(const std::filesystem::path& icon_palette_filename);
void OutputBanner(std::ofstream& ostream, const Banner& banner);
bool MakeBanner(const std::filesystem::path& specfile_path, const std::filesystem::path& outfile_path);
inline bool CommandIsForTitleAndDeveloper(const std::wstring& command_name) {
return command_name == L"JP" ||
command_name == L"EN" ||
command_name == L"FR" ||
command_name == L"GE" ||
command_name == L"IT" ||
command_name == L"SP";
}

View File

@ -0,0 +1,22 @@
#include <array>
#include "banner.h"
#include "crc16.h"
u16 CalculateCRC16FromBannerData(const u8* banner_data) {
const std::array<u16, 16> crc_table = {
0x0000, 0xCC01, 0xD801, 0x1400,
0xF001, 0x3C00, 0x2800, 0xE401,
0xA001, 0x6C00, 0x7800, 0xB401,
0x5000, 0x9C01, 0x8801, 0x4400,
};
u16 checksum = 0xFFFF;
const std::size_t data_size = sizeof(Banner) - offsetof(Banner, bitmap);
for (std::size_t i = 0; i < data_size; i++) {
u16 lookup_index = crc_table[banner_data[i] & 0xF] ^ (checksum >> 4) ^ crc_table[checksum & 0xF];
checksum = crc_table[banner_data[i] >> 4] ^ (lookup_index >> 4) ^ crc_table[lookup_index & 0xF];
}
return checksum;
}

View File

@ -0,0 +1,5 @@
#pragma once
#include "types.h"
u16 CalculateCRC16FromBannerData(const u8* banner_data);

View File

@ -0,0 +1,45 @@
#include <cstdio>
#include <cstring>
#include "banner.h"
#include "types.h"
#ifdef _MSC_VER
#define strcasecmp _stricmp
#endif
int main(int argc, char* argv[]) {
if (argc != 2 && argc != 3) {
printf("usage: %s <specfile> [outfile]\n", argv[0]);
return 1;
}
const filesystem::path specfile_path = argv[1];
if (!filesystem::is_regular_file(specfile_path)) {
printf("error: provided specfile does not exist / is not a regular file. (did you put the right path?)\n");
return 1;
}
if (specfile_path.extension() == ".bnr") {
printf("error: can't use a bnr file as a specfile\n");
return 1;
}
// If the user doesn't provide a path to an outfile, or if the provided outfile is
// identical to the provided specfile, use the specfile's name + the .bnr extension.
filesystem::path outfile_path;
if (argc == 2 || strcasecmp(argv[1], argv[2]) == 0) {
outfile_path = specfile_path.stem().string() + ".bnr";
} else {
outfile_path = argv[2];
}
// printf("debug: specfile: %s\n", specfile_path.c_str());
// printf("debug: outfile: %s\n", outfile_path.c_str());
if (!MakeBanner(specfile_path, outfile_path)) {
printf("error: failed to create banner file\n");
return 1;
}
return 0;
}

15
tools/nitrobanner/types.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#if (__GNUC__ <= 7) && !defined _MSC_VER
#include <experimental/filesystem>
namespace filesystem = std::experimental::filesystem;
#else
#include <filesystem>
namespace filesystem = std::filesystem;
#endif
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;