diff options
author | Akira Akashi <rubenru09@aol.com> | 2021-08-22 12:31:00 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-22 12:31:00 +0100 |
commit | 28b66f4e9efc2dbde3005f3e5bcc44b93ae47796 (patch) | |
tree | 6078bb064621ff03fbcb8ddec952eb05730c8e8c | |
parent | 97c8e77b2f951b6f0c43933576f21b833e7d5876 (diff) | |
parent | d08722a7381c4c05a40ee59bb6de556616e1dfc2 (diff) |
Merge branch 'master' into master
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | tools/nitrobanner/.gitignore | 1 | ||||
-rw-r--r-- | tools/nitrobanner/LICENSE | 21 | ||||
-rw-r--r-- | tools/nitrobanner/Makefile | 30 | ||||
-rw-r--r-- | tools/nitrobanner/banner.cpp | 276 | ||||
-rw-r--r-- | tools/nitrobanner/banner.h | 61 | ||||
-rw-r--r-- | tools/nitrobanner/crc16.cpp | 22 | ||||
-rw-r--r-- | tools/nitrobanner/crc16.h | 5 | ||||
-rw-r--r-- | tools/nitrobanner/main.cpp | 45 | ||||
-rw-r--r-- | tools/nitrobanner/types.h | 15 |
10 files changed, 478 insertions, 2 deletions
@@ -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) > $@ diff --git a/tools/nitrobanner/.gitignore b/tools/nitrobanner/.gitignore new file mode 100644 index 00000000..e8eff425 --- /dev/null +++ b/tools/nitrobanner/.gitignore @@ -0,0 +1 @@ +nitrobanner diff --git a/tools/nitrobanner/LICENSE b/tools/nitrobanner/LICENSE new file mode 100644 index 00000000..193cec26 --- /dev/null +++ b/tools/nitrobanner/LICENSE @@ -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. diff --git a/tools/nitrobanner/Makefile b/tools/nitrobanner/Makefile new file mode 100644 index 00000000..322525bd --- /dev/null +++ b/tools/nitrobanner/Makefile @@ -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) diff --git a/tools/nitrobanner/banner.cpp b/tools/nitrobanner/banner.cpp new file mode 100644 index 00000000..ef73ccf9 --- /dev/null +++ b/tools/nitrobanner/banner.cpp @@ -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; +} diff --git a/tools/nitrobanner/banner.h b/tools/nitrobanner/banner.h new file mode 100644 index 00000000..70684172 --- /dev/null +++ b/tools/nitrobanner/banner.h @@ -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"; +} diff --git a/tools/nitrobanner/crc16.cpp b/tools/nitrobanner/crc16.cpp new file mode 100644 index 00000000..bac64b57 --- /dev/null +++ b/tools/nitrobanner/crc16.cpp @@ -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; +} diff --git a/tools/nitrobanner/crc16.h b/tools/nitrobanner/crc16.h new file mode 100644 index 00000000..29811cf9 --- /dev/null +++ b/tools/nitrobanner/crc16.h @@ -0,0 +1,5 @@ +#pragma once + +#include "types.h" + +u16 CalculateCRC16FromBannerData(const u8* banner_data); diff --git a/tools/nitrobanner/main.cpp b/tools/nitrobanner/main.cpp new file mode 100644 index 00000000..031d9218 --- /dev/null +++ b/tools/nitrobanner/main.cpp @@ -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; +} diff --git a/tools/nitrobanner/types.h b/tools/nitrobanner/types.h new file mode 100644 index 00000000..5776e62f --- /dev/null +++ b/tools/nitrobanner/types.h @@ -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; |