// tmx2gba.cpp - main entry point // SPDX-License-Identifier: Zlib // SPDX-FileCopyrightText: (c) 2015-2024 a dinosaur #include constexpr std::string_view copyrightStr("(c) 2015-2024 a dinosaur"); #include "argparse.hpp" #include "tmxreader.hpp" #include "convert.hpp" #include "headerwriter.hpp" #include "swriter.hpp" #include "strtools.hpp" #include "config.h" #include #include #include struct Arguments { std::string inPath, outPath; std::string layer, collisionlay, paletteLay; std::string flagFile; int offset = 0; int palette = 0; std::vector objMappings; bool help = false; }; using ArgParse::Option; static const ArgParse::Options options = { Option::Optional('h', {}, "Display help & command info"), Option::Optional('l', "name", "Name of layer to use (default first layer in TMX)"), Option::Optional('y', "name", "Layer for palette mappings"), Option::Optional('c', "name", "Output a separate 8bit collision map of the specified layer"), Option::Optional('r', "offset", "Offset tile indices (default 0)"), Option::Optional('p', "0-15", "Select which palette to use for 4-bit tilesets"), Option::Optional('m', "name;id", "Map an object name to an ID, will enable object exports"), Option::Required('i', "inpath", "Path to input TMX file"), Option::Required('o', "outpath", "Path to output files"), Option::Optional('f', "file", "Specify a file to use for flags, will override any options" " specified on the command line") }; static bool ParseArgs(int argc, char** argv, Arguments& params) { auto parser = ArgParse::ArgParser(argv[0], options, [&](int opt, const std::string_view arg) -> ArgParse::ParseCtrl { using ArgParse::ParseCtrl; try { switch (opt) { case 'h': params.help = true; return ParseCtrl::QUIT_EARLY; case 'l': params.layer = arg; return ParseCtrl::CONTINUE; case 'c': params.collisionlay = arg; return ParseCtrl::CONTINUE; case 'y': params.paletteLay = arg; return ParseCtrl::CONTINUE; case 'r': params.offset = std::stoi(std::string(arg)); return ParseCtrl::CONTINUE; case 'p': params.palette = std::stoi(std::string(arg)); return ParseCtrl::CONTINUE; case 'm': params.objMappings.emplace_back(arg); return ParseCtrl::CONTINUE; case 'i': params.inPath = arg; return ParseCtrl::CONTINUE; case 'o': params.outPath = arg; return ParseCtrl::CONTINUE; case 'f': params.flagFile = arg; return ParseCtrl::CONTINUE; default: return ParseCtrl::QUIT_ERR_UNKNOWN; } } catch (std::invalid_argument const&) { return ParseCtrl::QUIT_ERR_INVALID; } catch (std::out_of_range const&) { return ParseCtrl::QUIT_ERR_RANGE; } }); if (!parser.Parse(std::span(argv + 1, argc - 1))) { return false; } if (params.help) { return true; } if (!params.flagFile.empty()) { std::ifstream paramFile(params.flagFile); if (!paramFile.is_open()) { std::cerr << "Failed to open param file." << std::endl; return false; } std::vector tokens; if (!ArgParse::ReadParamFile(tokens, paramFile)) { std::cerr << "Failed to read param file: Unterminated quote string." << std::endl; return false; } if (!parser.Parse(tokens)) return false; } // Check my paranoia if (params.inPath.empty()) { parser.DisplayError("No input file specified."); return false; } if (params.outPath.empty()) { parser.DisplayError("No output file specified."); return false; } if (params.palette < 0 || params.palette > 15) { parser.DisplayError("Invalid palette index."); return false; } return true; } int main(int argc, char** argv) { Arguments p; if (!ParseArgs(argc, argv, p)) { return 1; } if (p.help) { std::cout << "tmx2gba v" << TMX2GBA_VERSION << ", " << copyrightStr << std::endl; options.ShowHelpUsage(argv[0], std::cout); return 0; } // Object mappings std::map objMapping; if (!p.objMappings.empty()) { for (const auto& objToken : p.objMappings) { auto splitter = objToken.find_last_of(';'); if (splitter == std::string::npos) { std::cerr << "Malformed mapping (missing a splitter)." << std::endl; return 1; } try { std::string name = objToken.substr(0, splitter); int id = std::stoi(objToken.substr(splitter + 1)); objMapping[name] = id; } catch (std::exception&) { std::cerr << "Malformed mapping, make sure id is numeric." << std::endl; } } } // Open & read input file TmxReader tmx; switch (tmx.Open(p.inPath, p.layer, p.paletteLay, p.collisionlay, objMapping)) { case TmxReader::Error::LOAD_FAILED: std::cerr << "Failed to open input file." << std::endl; return 1; case TmxReader::Error::NO_LAYERS: std::cerr << "No suitable tile layer found." << std::endl; return 1; case TmxReader::Error::GRAPHICS_NOTFOUND: std::cerr << "No graphics layer \"" << p.layer << "\" found." << std::endl; return 1; case TmxReader::Error::PALETTE_NOTFOUND: std::cerr << "No palette layer \"" << p.paletteLay << "\" found." << std::endl; return 1; case TmxReader::Error::COLLISION_NOTFOUND: std::cerr << "No collision layer \"" << p.collisionlay << "\" found." << std::endl; return 1; case TmxReader::Error::OK: break; } // Get name from file std::string name = SanitiseLabel(std::filesystem::path(p.outPath).stem().string()); // Open output files SWriter outS; if (!outS.Open(p.outPath + ".s", name)) { std::cerr << "Failed to create output file \"" << p.outPath << ".s\"."; return 1; } HeaderWriter outH; if (!outH.Open(p.outPath + ".h", name)) { std::cerr << "Failed to create output file \"" << p.outPath << ".h\"."; return 1; } // Convert to GBA-friendly charmap data { std::vector charDat; if (!convert::ConvertCharmap(charDat, p.offset, p.palette, tmx)) return 1; // Write out charmap outH.WriteSize(tmx.GetSize().width, tmx.GetSize().height); outH.WriteCharacterMap(charDat); outS.WriteArray("Tiles", charDat); } // Convert collision map & write out if (tmx.HasCollisionTiles()) { std::vector collisionDat; if (!convert::ConvertCollision(collisionDat, tmx)) { return 1; } outH.WriteCollision(collisionDat); outS.WriteArray("Collision", collisionDat, 32); } if (tmx.HasObjects()) { std::vector objDat; if (!convert::ConvertObjects(objDat, tmx)) { return 1; } outH.WriteObjects(objDat); outS.WriteArray("Objdat", objDat); } return 0; }