mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2024-11-25 23:25:40 +00:00
Merge branch 'master' of https://github.com/trzy/Supermodel
This commit is contained in:
commit
33d65e097f
|
@ -5,6 +5,7 @@
|
||||||
#include "Util/ByteSwap.h"
|
#include "Util/ByteSwap.h"
|
||||||
#include "Util/Format.h"
|
#include "Util/Format.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
@ -161,18 +162,68 @@ GameLoader::Region::ptr_t GameLoader::Region::Create(const GameLoader &loader, c
|
||||||
{
|
{
|
||||||
if (GameLoader::MissingAttrib(loader, region_node, "name") | MissingAttrib(loader, region_node, "stride") | GameLoader::MissingAttrib(loader, region_node, "chunk_size")) // no || to easier detect errors
|
if (GameLoader::MissingAttrib(loader, region_node, "name") | MissingAttrib(loader, region_node, "stride") | GameLoader::MissingAttrib(loader, region_node, "chunk_size")) // no || to easier detect errors
|
||||||
return ptr_t();
|
return ptr_t();
|
||||||
|
|
||||||
|
if (region_node["byte_swap"].Exists() && region_node["byte_layout"].Exists())
|
||||||
|
{
|
||||||
|
ErrorLog("%s: '%s' region has both 'byte_swap' and 'byte_layout' attributes. Use one or the other.", loader.m_xml_filename.c_str(), region_node["name"].Value<std::string>().c_str());
|
||||||
|
return ptr_t();
|
||||||
|
}
|
||||||
|
|
||||||
ptr_t region = std::make_shared<Region>();
|
ptr_t region = std::make_shared<Region>();
|
||||||
|
|
||||||
region->region_name = region_node["name"].Value<std::string>();
|
region->region_name = region_node["name"].Value<std::string>();
|
||||||
|
|
||||||
region->stride = region_node["stride"].ValueAs<size_t>();
|
region->stride = region_node["stride"].ValueAs<size_t>();
|
||||||
|
if (region->stride == 0)
|
||||||
|
{
|
||||||
|
ErrorLog("%s: '%s' stride length must be greater than 0.", loader.m_xml_filename.c_str(), region->region_name.c_str());
|
||||||
|
return ptr_t();
|
||||||
|
}
|
||||||
|
|
||||||
region->chunk_size = region_node["chunk_size"].ValueAs<size_t>();
|
region->chunk_size = region_node["chunk_size"].ValueAs<size_t>();
|
||||||
region->byte_swap = region_node["byte_swap"].ValueAsDefault<bool>(false);
|
if (region->chunk_size == 0)
|
||||||
|
{
|
||||||
|
ErrorLog("%s: '%s' chunk size must be greater than 0.", loader.m_xml_filename.c_str(), region->region_name.c_str());
|
||||||
|
return ptr_t();
|
||||||
|
}
|
||||||
|
|
||||||
region->required = region_node["required"].ValueAsDefault<bool>(true);
|
region->required = region_node["required"].ValueAsDefault<bool>(true);
|
||||||
|
|
||||||
|
// Byte layout. If byte_swap was specified, construct the byte swapped layout string based on
|
||||||
|
// stride size. If byte_swap is set to false, empty layout string is fine.
|
||||||
|
if (region_node["byte_swap"].Exists())
|
||||||
|
{
|
||||||
|
if (region_node["byte_swap"].ValueAs<bool>())
|
||||||
|
{
|
||||||
|
// Special case: if chunk size and stride are both 1, change them both to 2 so we can allow byte
|
||||||
|
// swapping (these values are used for singular ROMs that don't need to be merged; technically,
|
||||||
|
// the stride and chunk size should be 2 since they are 16-bit ROMs).
|
||||||
|
if (region->stride == 1 && region->chunk_size == 1)
|
||||||
|
{
|
||||||
|
region->stride = 2;
|
||||||
|
region->chunk_size = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string byte_layout;
|
||||||
|
for (size_t i = 0; i < region->stride; i++)
|
||||||
|
{
|
||||||
|
byte_layout += '0' + (i ^ 1);
|
||||||
|
}
|
||||||
|
region->byte_layout = byte_layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
region->byte_layout = region_node["byte_layout"].ValueAsDefault<std::string>(std::string());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return region;
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameLoader::Region::AttribsMatch(const ptr_t &other) const
|
bool GameLoader::Region::AttribsMatch(const ptr_t &other) const
|
||||||
{
|
{
|
||||||
return stride == other->stride && chunk_size == other->chunk_size && byte_swap == other->byte_swap;
|
return stride == other->stride && chunk_size == other->chunk_size && byte_layout == other->byte_layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameLoader::Region::FindFileIndexByOffset(size_t *idx, uint32_t offset) const
|
bool GameLoader::Region::FindFileIndexByOffset(size_t *idx, uint32_t offset) const
|
||||||
|
@ -451,7 +502,7 @@ void GameLoader::LogROMDefinition(const std::string &game_name, const RegionsByN
|
||||||
InfoLog("%s:", game_name.c_str());
|
InfoLog("%s:", game_name.c_str());
|
||||||
for (auto &v2: regions_by_name)
|
for (auto &v2: regions_by_name)
|
||||||
{
|
{
|
||||||
InfoLog(" %s: stride=%zu, chunk size=%zu, byte swap=%d", v2.first.c_str(), v2.second->stride, v2.second->chunk_size, v2.second->byte_swap ? 1 : 0);
|
InfoLog(" %s: stride=%zu, chunk size=%zu, byte layout=%s", v2.first.c_str(), v2.second->stride, v2.second->chunk_size, v2.second->byte_layout.c_str());
|
||||||
for (auto &file: v2.second->files)
|
for (auto &file: v2.second->files)
|
||||||
{
|
{
|
||||||
InfoLog(" %s, crc32=0x%08x, offset=0x%08x", file->filename.c_str(), file->crc32, file->offset);
|
InfoLog(" %s, crc32=0x%08x, offset=0x%08x", file->filename.c_str(), file->crc32, file->offset);
|
||||||
|
@ -727,19 +778,78 @@ bool GameLoader::ComputeRegionSize(uint32_t *region_size, const GameLoader::Regi
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to preserve the absolute offsets in order for byte swapping to work
|
static bool ApplyLayout(ROM *rom, const std::string &byte_layout, size_t stride, const std::string ®ion_name)
|
||||||
// properly when chunk size is 1
|
|
||||||
static inline void CopyBytes(uint8_t *dest_base, uint32_t dest_offset, const uint8_t *src_base, uint32_t src_offset, uint32_t size, uint32_t byte_swap)
|
|
||||||
{
|
{
|
||||||
for (uint32_t i = 0; i < size; i++)
|
// Empty layout means do nothing
|
||||||
|
if (byte_layout.size() == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Validate that the layout string includes the same number of bytes as the region stride. The
|
||||||
|
// stride is block size that the ROM files all contribute to. We also verify that each byte is
|
||||||
|
// used once and only once.
|
||||||
|
if (byte_layout.size() != stride)
|
||||||
{
|
{
|
||||||
dest_base[(dest_offset + i) ^ byte_swap] = src_base[src_offset + i];
|
ErrorLog("Byte layout of '%s' region does not match the stride length (%d bytes but should be %d bytes).", region_name.c_str(), byte_layout.size(), stride);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stride > 8)
|
||||||
|
{
|
||||||
|
ErrorLog("Region '%s' has stride larger than 8 (%d), which is currently unsupported.", region_name.c_str(), stride);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<size_t> byte_offsets;
|
||||||
|
for (char c: byte_layout)
|
||||||
|
{
|
||||||
|
if (isdigit(c))
|
||||||
|
{
|
||||||
|
byte_offsets.push_back(c - '0');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorLog("Byte layout of '%s' region contains non-numeric characters. Use single-digit byte indices only.", region_name.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all byte indices 0..N-1 are present
|
||||||
|
std::vector<size_t> sorted(byte_offsets);
|
||||||
|
std::sort(sorted.begin(), sorted.end()); // ascending order
|
||||||
|
size_t expected_offset = 0;
|
||||||
|
for (size_t offset: sorted)
|
||||||
|
{
|
||||||
|
if (offset != expected_offset)
|
||||||
|
{
|
||||||
|
ErrorLog("Byte layout of '%s' region must specify all byte offsets exactly once.", region_name.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
expected_offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Okay, all good. Now we can reshuffle the region memory according to layout.
|
||||||
|
uint8_t *buffer = new uint8_t[stride];
|
||||||
|
uint8_t *dest = rom->data.get();
|
||||||
|
for (size_t dest_offset = 0; (dest_offset + stride) <= rom->size; dest_offset += stride)
|
||||||
|
{
|
||||||
|
// Copy current region bytes to temporary buffer. The layout offsets refer to this original layout.
|
||||||
|
memcpy(buffer, dest + dest_offset, stride);
|
||||||
|
|
||||||
|
// Place the bytes back into the ROM region in the layout order specified.
|
||||||
|
for (size_t i = 0; i < stride; i++)
|
||||||
|
{
|
||||||
|
dest[dest_offset + i] = buffer[byte_offsets[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete [] buffer;
|
||||||
|
|
||||||
|
return false; // no error
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameLoader::LoadRegion(ROM *rom, const GameLoader::Region::ptr_t ®ion, const ZipArchive &zip) const
|
bool GameLoader::LoadRegion(ROM *rom, const GameLoader::Region::ptr_t ®ion, const ZipArchive &zip) const
|
||||||
{
|
{
|
||||||
bool error = false;
|
bool error = false;
|
||||||
|
|
||||||
for (auto &file: region->files)
|
for (auto &file: region->files)
|
||||||
{
|
{
|
||||||
std::shared_ptr<uint8_t> tmp;
|
std::shared_ptr<uint8_t> tmp;
|
||||||
|
@ -752,8 +862,6 @@ bool GameLoader::LoadRegion(ROM *rom, const GameLoader::Region::ptr_t ®ion, c
|
||||||
if (region->chunk_size == region->stride)
|
if (region->chunk_size == region->stride)
|
||||||
{
|
{
|
||||||
memcpy(dest + file->offset, src, file_size);
|
memcpy(dest + file->offset, src, file_size);
|
||||||
if (region->byte_swap)
|
|
||||||
Util::FlipEndian16(dest + file->offset, file_size);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -762,16 +870,21 @@ bool GameLoader::LoadRegion(ROM *rom, const GameLoader::Region::ptr_t ®ion, c
|
||||||
uint32_t src_offset = 0;
|
uint32_t src_offset = 0;
|
||||||
uint32_t chunk_size = (uint32_t)region->chunk_size; // cache these as pointer dereferencing cripples performance in a tight loop
|
uint32_t chunk_size = (uint32_t)region->chunk_size; // cache these as pointer dereferencing cripples performance in a tight loop
|
||||||
uint32_t stride = (uint32_t)region->stride;
|
uint32_t stride = (uint32_t)region->stride;
|
||||||
uint32_t byte_swap = region->byte_swap;
|
|
||||||
for (uint32_t i = 0; i < num_chunks; i++)
|
for (uint32_t i = 0; i < num_chunks; i++)
|
||||||
{
|
{
|
||||||
CopyBytes(dest, dest_offset, src, src_offset, chunk_size, byte_swap);
|
memcpy(dest + dest_offset, src + src_offset, chunk_size);
|
||||||
dest_offset += stride;
|
dest_offset += stride;
|
||||||
src_offset += chunk_size;
|
src_offset += chunk_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!error)
|
||||||
|
{
|
||||||
|
error = ApplyLayout(rom, region->byte_layout, region->stride, region->region_name);
|
||||||
|
}
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ private:
|
||||||
std::string region_name;
|
std::string region_name;
|
||||||
size_t stride;
|
size_t stride;
|
||||||
size_t chunk_size;
|
size_t chunk_size;
|
||||||
bool byte_swap;
|
std::string byte_layout;
|
||||||
bool required;
|
bool required;
|
||||||
std::vector<File::ptr_t> files;
|
std::vector<File::ptr_t> files;
|
||||||
static ptr_t Create(const GameLoader &loader, const Util::Config::Node ®ion_node);
|
static ptr_t Create(const GameLoader &loader, const Util::Config::Node ®ion_node);
|
||||||
|
|
|
@ -324,8 +324,8 @@ private:
|
||||||
|
|
||||||
// M68K CPU
|
// M68K CPU
|
||||||
M68KCtx M68K;
|
M68KCtx M68K;
|
||||||
static const int k_framePeriod = 11000000/60;
|
static constexpr int k_framePeriod = 11000000/60;
|
||||||
static const int k_timerPeriod = 11000000/1000; // 1KHz timer
|
static constexpr int k_timerPeriod = 11000000/1000; // 1KHz timer
|
||||||
int m_cyclesElapsedThisFrame;
|
int m_cyclesElapsedThisFrame;
|
||||||
int m_nextTimerInterruptCycles;
|
int m_nextTimerInterruptCycles;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/**
|
/**
|
||||||
** Supermodel
|
** Supermodel
|
||||||
** A Sega Model 3 Arcade Emulator.
|
** A Sega Model 3 Arcade Emulator.
|
||||||
** Copyright 2011-2016 Bart Trzynadlowski, Nik Henson
|
** Copyright 2011-2016 Bart Trzynadlowski, Nik Henson
|
||||||
**
|
**
|
||||||
** This file is part of Supermodel.
|
** This file is part of Supermodel.
|
||||||
**
|
**
|
||||||
** Supermodel is free software: you can redistribute it and/or modify it under
|
** Supermodel is free software: you can redistribute it and/or modify it under
|
||||||
** the terms of the GNU General Public License as published by the Free
|
** the terms of the GNU General Public License as published by the Free
|
||||||
** Software Foundation, either version 3 of the License, or (at your option)
|
** Software Foundation, either version 3 of the License, or (at your option)
|
||||||
** any later version.
|
** any later version.
|
||||||
**
|
**
|
||||||
|
@ -18,10 +18,10 @@
|
||||||
** You should have received a copy of the GNU General Public License along
|
** You should have received a copy of the GNU General Public License along
|
||||||
** with Supermodel. If not, see <http://www.gnu.org/licenses/>.
|
** with Supermodel. If not, see <http://www.gnu.org/licenses/>.
|
||||||
**/
|
**/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Model3GraphicsState.h
|
* Model3GraphicsState.h
|
||||||
*
|
*
|
||||||
* Minimalistic implementation of IEmulator designed to load and view graphics
|
* Minimalistic implementation of IEmulator designed to load and view graphics
|
||||||
* state.
|
* state.
|
||||||
*/
|
*/
|
||||||
|
@ -52,7 +52,7 @@ public:
|
||||||
void SaveState(CBlockFile *SaveState) override
|
void SaveState(CBlockFile *SaveState) override
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadState(CBlockFile *SaveState) override
|
void LoadState(CBlockFile *SaveState) override
|
||||||
{
|
{
|
||||||
m_real3D.LoadState(SaveState);
|
m_real3D.LoadState(SaveState);
|
||||||
|
@ -116,7 +116,7 @@ public:
|
||||||
else
|
else
|
||||||
rom_set.get_rom("vrom").CopyTo(m_vrom.get(), 64*0x100000);
|
rom_set.get_rom("vrom").CopyTo(m_vrom.get(), 64*0x100000);
|
||||||
int stepping = ((m_game.stepping[0] - '0') << 4) | (m_game.stepping[2] - '0');
|
int stepping = ((m_game.stepping[0] - '0') << 4) | (m_game.stepping[2] - '0');
|
||||||
m_real3D.SetStepping(stepping, false);
|
m_real3D.SetStepping(stepping);
|
||||||
return OKAY;
|
return OKAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,23 @@
|
||||||
*
|
*
|
||||||
* Main program driver for the SDL port.
|
* Main program driver for the SDL port.
|
||||||
*
|
*
|
||||||
|
* Bugs and Issues to Address
|
||||||
|
* --------------------------
|
||||||
|
* - -gfx-state crashes when ENABLE_DEBUGGER is also enabled:
|
||||||
|
*
|
||||||
|
* #0 0x00007ff6586392a0 in CSoundBoard::GetDSB (this=0x1128) at Src/Model3/SoundBoard.cpp:552
|
||||||
|
* #1 0x00007ff6587072cf in Debugger::CSupermodelDebugger::CreateDSBCPUDebug (model3=0x0) at Src/Debugger/SupermodelDebugger.cpp:269
|
||||||
|
* #2 0x00007ff6587076e9 in Debugger::CSupermodelDebugger::AddCPUs (this=0x185da75df40) at Src/Debugger/SupermodelDebugger.cpp:361
|
||||||
|
* #3 0x00007ff6586f91a6 in Debugger::CDebugger::Attach (this=0x185da75df40) at Src/Debugger/Debugger.cpp:263
|
||||||
|
* #4 0x00007ff658705312 in Debugger::CConsoleDebugger::Attach (this=0x185da75df40) at Src/Debugger/ConsoleDebugger.cpp:2707
|
||||||
|
* #5 0x00007ff65862c6fc in Supermodel (game=..., rom_set=0x6cc8dff0e0, Model3=0x185da7598d0, Inputs=0x185da752590, Outputs=0x0, Debugger=std::shared_ptr<Debugger::CDebugger> (use count 3, weak count 0) = {...})
|
||||||
|
* at Src/OSD/SDL/Main.cpp:978
|
||||||
|
* #6 0x00007ff658634ba3 in SDL_main (argc=3, argv=0x185da4e68a0) at Src/OSD/SDL/Main.cpp:2056
|
||||||
|
* #7 0x00007ff658720141 in main_getcmdline ()
|
||||||
|
* #8 0x00007ff6585b13b1 in __tmainCRTStartup () at C:/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:321
|
||||||
|
* #9 0x00007ff6585b14e6 in mainCRTStartup () at C:/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:202
|
||||||
|
* - Need to address all the stale TO-DOs below and clear them out :)
|
||||||
|
*
|
||||||
* To Do Before Next Release
|
* To Do Before Next Release
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* - Thoroughly test config system (do overrides work as expected? XInput
|
* - Thoroughly test config system (do overrides work as expected? XInput
|
||||||
|
@ -547,6 +564,7 @@ void Screenshot()
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
static std::string s_gfxStatePath;
|
static std::string s_gfxStatePath;
|
||||||
|
static const std::string k_gfxAnalysisPath = "GraphicsAnalysis/";
|
||||||
|
|
||||||
static std::string GetFileBaseName(const std::string &file)
|
static std::string GetFileBaseName(const std::string &file)
|
||||||
{
|
{
|
||||||
|
@ -603,7 +621,7 @@ static void TestPolygonHeaderBits(IEmulator *Emu)
|
||||||
if ((unknownPolyBits[idx] & mask))
|
if ((unknownPolyBits[idx] & mask))
|
||||||
{
|
{
|
||||||
Emu->RenderFrame();
|
Emu->RenderFrame();
|
||||||
std::string file = Util::Format() << s_analysisPath << GetFileBaseName(s_gfxStatePath) << "." << "poly" << "." << idx << "_" << Util::Hex(mask) << ".bmp";
|
std::string file = Util::Format() << k_gfxAnalysisPath << GetFileBaseName(s_gfxStatePath) << "." << "poly" << "." << idx << "_" << Util::Hex(mask) << ".bmp";
|
||||||
SaveFrameBuffer(file);
|
SaveFrameBuffer(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -619,7 +637,7 @@ static void TestPolygonHeaderBits(IEmulator *Emu)
|
||||||
if ((unknownCullingNodeBits[idx] & mask))
|
if ((unknownCullingNodeBits[idx] & mask))
|
||||||
{
|
{
|
||||||
Emu->RenderFrame();
|
Emu->RenderFrame();
|
||||||
std::string file = Util::Format() << s_analysisPath << GetFileBaseName(s_gfxStatePath) << "." << "culling" << "." << idx << "_" << Util::Hex(mask) << ".bmp";
|
std::string file = Util::Format() << k_gfxAnalysisPath << GetFileBaseName(s_gfxStatePath) << "." << "culling" << "." << idx << "_" << Util::Hex(mask) << ".bmp";
|
||||||
SaveFrameBuffer(file);
|
SaveFrameBuffer(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -628,7 +646,7 @@ static void TestPolygonHeaderBits(IEmulator *Emu)
|
||||||
glReadBuffer(readBuffer);
|
glReadBuffer(readBuffer);
|
||||||
|
|
||||||
// Generate the HTML GUI
|
// Generate the HTML GUI
|
||||||
std::string file = Util::Format() << s_analysisPath << "_" << GetFileBaseName(s_gfxStatePath) << ".html";
|
std::string file = Util::Format() << k_gfxAnalysisPath << "_" << GetFileBaseName(s_gfxStatePath) << ".html";
|
||||||
std::ofstream fs(file);
|
std::ofstream fs(file);
|
||||||
if (!fs.good())
|
if (!fs.good())
|
||||||
ErrorLog("Unable to open '%s' for writing.", file.c_str());
|
ErrorLog("Unable to open '%s' for writing.", file.c_str());
|
||||||
|
@ -1603,8 +1621,13 @@ static void Help(void)
|
||||||
#ifdef SUPERMODEL_DEBUGGER
|
#ifdef SUPERMODEL_DEBUGGER
|
||||||
puts(" -disable-debugger Completely disable debugger functionality");
|
puts(" -disable-debugger Completely disable debugger functionality");
|
||||||
puts(" -enter-debugger Enter debugger at start of emulation");
|
puts(" -enter-debugger Enter debugger at start of emulation");
|
||||||
puts("");
|
|
||||||
#endif // SUPERMODEL_DEBUGGER
|
#endif // SUPERMODEL_DEBUGGER
|
||||||
|
#ifdef DEBUG
|
||||||
|
puts(" -gfx-state=<file> Produce graphics analysis for save state (works only");
|
||||||
|
puts(" with the legacy 3D engine and requires a");
|
||||||
|
puts(" GraphicsAnalysis directory to exist)");
|
||||||
|
#endif
|
||||||
|
puts("");
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParsedCommandLine
|
struct ParsedCommandLine
|
||||||
|
|
Loading…
Reference in a new issue