1
0
mirror of https://github.com/ScrelliCopter/tmx2gba.git synced 2025-02-21 03:29:25 +11:00
Files
tmx2gba/src/tmx2gba.cpp
2024-03-21 10:14:17 +11:00

301 lines
8.1 KiB
C++

/* tmx2gba.cpp - Copyright (C) 2015-2024 a dinosaur (zlib, see COPYING.txt) */
#include "argparse.hpp"
#include "tmxreader.hpp"
#include "tmxlayer.hpp"
#include "tmxobject.hpp"
#include "headerwriter.hpp"
#include "swriter.hpp"
#include <iostream>
#include <map>
#include <algorithm>
static const char* versionStr = "tmx2gba version 0.3, (c) 2015-2022 a dinosaur";
struct Arguments
{
bool help = false, showVersion = false;
std::string inPath, outPath;
std::string layer, collisionlay, paletteLay;
std::string flagFile;
int offset = 0;
int palette = 0;
std::vector<std::string> objMappings;
};
using ArgParse::Option;
static const ArgParse::Options options =
{
Option::Optional('h', nullptr, "Display this help & command info"),
Option::Optional('v', nullptr, "Display version & quit"),
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")
};
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 'v': params.showVersion = 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 || params.showVersion)
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<std::string> 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)
{
options.ShowHelpUsage(argv[0], std::cout);
return 0;
}
if (p.showVersion)
{
std::cout << versionStr << std::endl;
return 0;
}
// Object mappings
std::map<std::string, uint32_t> 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;
std::ifstream fin(p.inPath);
if (!fin.is_open())
{
std::cerr << "Failed to open input file." << std::endl;
return 1;
}
tmx.Open(fin);
// Get layers
if (tmx.GetLayerCount() == 0)
{
std::cerr << "No layers found." << std::endl;
return 1;
}
const TmxLayer* layerGfx = p.layer.empty()
? tmx.GetLayer(0)
: tmx.GetLayer(p.layer);
const TmxLayer* layerCls = p.collisionlay.empty()
? nullptr
: tmx.GetLayer(p.collisionlay);
const TmxLayer* layerPal = p.paletteLay.empty()
? nullptr
: tmx.GetLayer(p.paletteLay);
if (layerGfx == nullptr)
{
std::cerr << "Input layer not found." << std::endl;
return 1;
}
// Get name from file
//TODO: properly sanitise
int slashPos = std::max(
static_cast<int>(p.outPath.find_last_of('/')),
static_cast<int>(p.outPath.find_last_of('\\')));
std::string name = p.outPath;
if (slashPos != -1)
name = name.substr(slashPos + 1);
// Open output files
SWriter outS; HeaderWriter outH;
if (!outS.Open(p.outPath + ".s", name))
{
std::cerr << "Failed to create output file \"" << p.outPath << ".s\".";
return 1;
}
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
const uint32_t* gfxTiles = layerGfx->GetData();
const uint32_t* palTiles = (layerPal == nullptr) ? nullptr : layerPal->GetData();
std::vector<uint16_t> charDat;
const size_t numTiles = static_cast<size_t>(layerGfx->GetWidth()) * static_cast<size_t>(layerGfx->GetHeight());
charDat.reserve(numTiles);
for (size_t i = 0; i < numTiles; ++i)
{
uint32_t read = (*gfxTiles++);
uint16_t tile = std::max(0, static_cast<int>(tmx.LidFromGid(read & ~TmxLayer::FLIP_MASK)) + p.offset);
uint8_t flags = 0x0;
// Get flipped!
flags |= (read & TmxLayer::FLIP_HORZ) ? 0x4 : 0x0;
flags |= (read & TmxLayer::FLIP_VERT) ? 0x8 : 0x0;
// Determine palette ID
uint32_t idx = 0;
if (palTiles != nullptr)
idx = tmx.LidFromGid((*palTiles++) & ~TmxLayer::FLIP_MASK);
if (idx == 0)
idx = p.palette + 1;
flags |= static_cast<uint8_t>(idx - 1) << 4;
charDat.push_back(tile | (static_cast<uint16_t>(flags) << 8));
}
// Write out charmap
outH.WriteSize(tmx.GetWidth(), tmx.GetHeight());
outH.WriteCharacterMap(charDat);
outS.WriteArray("Tiles", charDat);
// Convert collision map & write it out
if (layerCls != nullptr)
{
std::vector<uint8_t> collisionDat;
collisionDat.reserve(layerCls->GetWidth() * layerCls->GetHeight());
gfxTiles = layerCls->GetData();
for (int i = 0; i < layerCls->GetWidth() * layerCls->GetHeight(); ++i)
{
uint8_t ucTile = static_cast<uint8_t>(tmx.LidFromGid((*gfxTiles++) & ~TmxLayer::FLIP_MASK));
collisionDat.push_back(ucTile);
}
// Try to nicely append "_collision" to the output name
std::string path;
size_t extPos = p.outPath.find_last_of('.');
if (extPos != std::string::npos)
path = p.outPath.insert(extPos, "_collision");
else
path = p.outPath + "_collision";
// Write collision
outH.WriteCollision(collisionDat);
outS.WriteArray("Collision", collisionDat, 32);
}
if (!p.objMappings.empty())
{
std::vector<uint32_t> objDat;
for (size_t i = 0; i < tmx.GetObjectCount(); ++i)
{
auto obj = tmx.GetObject(i);
auto it = objMapping.find(obj->GetName());
if (it == objMapping.end())
continue;
float x, y;
obj->GetPos(x, y);
objDat.push_back(it->second);
objDat.push_back(static_cast<int>(x * 256.0f));
objDat.push_back(static_cast<int>(y * 256.0f));
}
// Write objects
outH.WriteObjects(objDat);
outS.WriteArray("Objdat", objDat);
}
return 0;
}