diff --git a/deps/ox/ox-docs.md b/deps/ox/ox-docs.md new file mode 100644 index 00000000..ffb6c360 --- /dev/null +++ b/deps/ox/ox-docs.md @@ -0,0 +1,472 @@ + +# Ox Docs + +## Systems + +### Error Handling + +The GBA build has exceptions disabled. +Instead of throwing exceptions, all engine code must return ```ox::Error```s. +For the sake of consistency, try to stick to ```ox::Error``` in non-engine code +as well, but non-engine code is free to use exceptions when they make sense. +Nostalgia and Ox both use ```ox::Error``` to report errors. ```ox::Error``` is +a struct that has overloaded operators to behave like an integer error code, +plus some extra fields to enhance debuggability. +If instantiated through the ```OxError(x)``` macro, it will also include the +file and line of the error. +The ```OxError(x)``` macro should only be used for the initial instantiation of +an ```ox::Error```. + +In addition to ```ox::Error``` there is also the template ```ox::Result```. +```ox::Result``` simply wraps the type T value in a struct that also includes +error information, which allows the returning of a value and an error without +resorting to output parameters. + +If a function returns an ```ox::Error``` or ```ox::Result``` it should be +declared as ```noexcept``` and all exceptions should be translated to an +```ox::Error```. + +```ox::Result``` can be used as follows: + +```cpp +ox::Result foo(int i) noexcept { + if (i < 10) { + return i + 1; // implicitly calls ox::Result::Result(T) + } + return OxError(1); // implicitly calls ox::Result::Result(ox::Error) +} + +int caller1() { + auto v = foo(argc); + if (v.error) { + return 1; + } + std::cout << v.value << '\n'; + return 0; +} + +int caller2() { + // it is also possible to capture the value and error in their own variables + auto [val, err] = foo(argc); + if (err) { + return 1; + } + std::cout << val << '\n'; + return 0; +} + +ox::Error caller3(int &i) { + return foo(i).moveTo(i); +} + +ox::Error caller4(int &i) { + return foo(i).copyTo(i); +} + +int caller5(int i) { + return foo(i).unwrap(); // unwrap will kill the program if there is an error +} + +int caller6(int i) { + return foo(i).unwrapThrow(); // unwrap will throw if there is an error +} + +int caller7(int i) { + return foo(i).or_value(0); // will return 0 if foo returned an error +} + +ox::Result caller8(int i) { + return foo(i).to(); // will convert the result of foo to uint64_t +} +``` + +Lastly, there are a few macros available to help in passing ```ox::Error```s +back up the call stack, ```oxReturnError```, ```oxThrowError```, and +```oxRequire```. + +```oxReturnError``` is by far the more helpful of the two. +```oxReturnError``` will return an ```ox::Error``` if it is not 0 and +```oxThrowError``` will throw an ```ox::Error``` if it is not 0. +Because exceptions are disabled for GBA builds and thus cannot be used in the +engine, ```oxThrowError``` is only really useful at the boundary between +engine libraries and Nostalgia Studio. + +Since ```ox::Error``` is always nodiscard, you must do something with them. +In rare cases, you may not have anything you can do with them or you may know +the code will never fail in that particular instance. +This should be used sparingly. + + +```cpp +void studioCode() { + auto [val, err] = foo(1); + oxThrowError(err); + doStuff(val); +} + +ox::Error engineCode() noexcept { + auto [val, err] = foo(1); + oxReturnError(err); + doStuff(val); + return {}; +} + +void anyCode() { + auto [val, err] = foo(1); + std::ignore = err; + doStuff(val); +} +``` + +Both macros will also take the ```ox::Result``` directly: + +```cpp +void studioCode() { + auto valerr = foo(1); + oxThrowError(valerr); + doStuff(valerr.value); +} + +ox::Error engineCode() noexcept { + auto valerr = foo(1); + oxReturnError(valerr); + doStuff(valerr.value); + return {}; +} +``` + +Ox also has the ```oxRequire``` macro, which will initialize a value if there is no error, and return if there is. +It aims to somewhat emulate the ```?``` operator in Rust and Swift. + +Rust ```?``` operator: +```rust +fn f() -> Result { + // do stuff +} + +fn f2() -> Result { + let i = f()?; + Ok(i + 4) +} +``` + +```oxRequire```: +```cpp +ox::Result f() noexcept { + // do stuff +} + +ox::Result f2() noexcept { + oxRequire(i, f()); // const auto [out, oxConcat(oxRequire_err_, __LINE__)] = x; oxReturnError(oxConcat(oxRequire_err_, __LINE__)) + return i + 4; +} +``` +```oxRequire``` is not quite as versatile, but it should still cleanup a lot of otherwise less ideal code. + +```oxRequire``` also has variants for throwing the error and for making to value non-const: + +* ```oxRequireM``` - oxRequire Mutable +* ```oxRequireT``` - oxRequire Throw +* ```oxRequireMT``` - oxRequire Mutable Throw + +The throw variants of ```oxRequire``` are generally legacy code. +```ox::Result::unwrapThrow``` is generally preferred now. + +### Logging and Output + +Ox provides for logging and debug prints via the ```oxTrace```, ```oxDebug```, and ```oxError``` macros. +Each of these also provides a format variation. + +Ox also provide ```oxOut``` and ```oxErr``` for printing to stdout and stderr. +These are intended for permanent messages and always go to stdout and stderr. + +Tracing functions do not go to stdout unless the OXTRACE environment variable is set. +They also print with the channel that they are on, along with file and line. + +Debug statements go to stdout and go to the logger on the "debug" channel. +Where trace statements are intended to be written with thoughtfulness, +debug statements are intended to be quick and temporary insertions. +Debug statements trigger compilation failures if OX_NODEBUG is enabled when CMake is run, +as it is on Jenkins builds, so ```oxDebug``` statements should never be checked in. +This makes ```oxDebug``` preferable to other forms of logging, as temporary prints should +never be checked in. + +```oxError``` always prints. +It includes file and line, and is prefixed with a red "ERROR:". +It should generally be used conservatively. +It shuld be used only when there is an error that is not technically fatal, but +the user almost certainly wants to know about it. + +```oxTrace``` and ```oxTracef```: +```cpp +void f(int x, int y) { // x = 9, y = 4 + oxTrace("nostalgia.core.sdl.gfx") << "f:" << x << y; // Output: "f: 9 4" + oxTracef("nostalgia.core.sdl.gfx", "f: {}, {}", x, y); // Output: "f: 9, 4" +} +``` + +```oxDebug``` and ```oxDebugf```: +```cpp +void f(int x, int y) { // x = 9, y = 4 + oxDebug() << "f:" << x << y; // Output: "f: 9 4" + oxDebugf("f: {}, {}", x, y); // Output: "f: 9, 4" +} +``` + +```oxError``` and ```oxErrorf```: +```cpp +void f(int x, int y) { // x = 9, y = 4 + oxError() << "f:" << x << y; // Output: "ERROR: (:): f: 9 4" + oxErrorf("f: {}, {}", x, y); // Output: "ERROR: (:): f: 9, 4" +} +``` + +### Model System + +Ox has a model system that provides a sort of manual reflection mechanism. + +Models require a model function for the type that you want to model. +It is also good to provide a type name and type version number, though that is not required. + +The model function takes an instance of the type it is modelling and a template +parameter type. +The template parameter type must implement the API used in the models, but it +can do anything with the data provided to it. + +Here is an example from the Nostalgia/Core package: + +```cpp +struct NostalgiaPalette { + static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaPalette"; + static constexpr auto TypeVersion = 1; + ox::Vector colors; +}; + +struct NostalgiaGraphic { + static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic"; + static constexpr auto TypeVersion = 1; + int8_t bpp = 0; + // rows and columns are really only used by TileSheetEditor + int rows = 1; + int columns = 1; + ox::FileAddress defaultPalette; + NostalgiaPalette pal; + ox::Vector pixels; +}; + +template +constexpr ox::Error model(T *h, ox::CommonPtrWith auto *pal) noexcept { + h->template setTypeInfo(); + // it is also possible to provide the type name and type version as function arguments + //h->setTypeInfo("net.drinkingtea.nostalgia.core.NostalgiaPalette", 1); + oxReturnError(h->field("colors", &pal->colors)); + return {}; +} + +template +constexpr ox::Error model(T *h, ox::CommonPtrWith auto *ng) noexcept { + h->template setTypeInfo(); + oxReturnError(h->field("bpp", &ng->bpp)); + oxReturnError(h->field("rows", &ng->rows)); + oxReturnError(h->field("columns", &ng->columns)); + oxReturnError(h->field("defaultPalette", &ng->defaultPalette)); + oxReturnError(h->field("pal", &ng->pal)); + oxReturnError(h->field("pixels", &ng->pixels)); + return {}; +} +``` + +The model system also provides for unions: + +```cpp + +#include + +class FileAddress { + + template + friend constexpr Error model(T*, ox::CommonPtrWith auto*) noexcept; + + public: + static constexpr auto TypeName = "net.drinkingtea.ox.FileAddress"; + + union Data { + static constexpr auto TypeName = "net.drinkingtea.ox.FileAddress.Data"; + char *path; + const char *constPath; + uint64_t inode; + }; + + protected: + FileAddressType m_type = FileAddressType::None; + Data m_data; + +}; + +template +constexpr Error model(T *h, ox::CommonPtrWith auto *obj) noexcept { + h->template setTypeInfo(); + oxReturnError(h->fieldCString("path", &obj->path)); + oxReturnError(h->fieldCString("constPath", &obj->path)); + oxReturnError(h->field("inode", &obj->inode)); + return {}; +} + +template +constexpr Error model(T *io, ox::CommonPtrWith auto *fa) noexcept { + io->template setTypeInfo(); + // cannot read from object in Reflect operation + if constexpr(ox_strcmp(T::opType(), OpType::Reflect) == 0) { + int8_t type = 0; + oxReturnError(io->field("type", &type)); + oxReturnError(io->field("data", UnionView(&fa->m_data, 0))); + } else { + auto type = static_cast(fa->m_type); + oxReturnError(io->field("type", &type)); + fa->m_type = static_cast(type); + oxReturnError(io->field("data", UnionView(&fa->m_data, static_cast(fa->m_type)))); + } + return {}; +} + +``` + +There are also macros in `````` for simplifying the declaration of models: + +```cpp +oxModelBegin(NostalgiaGraphic) + oxModelField(bpp) + oxModelField(rows) + oxModelField(columns) + oxModelField(defaultPalette) + oxModelField(pal) + oxModelField(pixels) +oxModelEnd() +``` + +### Serialization + +Using the model system, Ox provides for serialization. +Ox has MetalClaw and OrganicClaw as its serialization format options. +MetalClaw is a custom binary format designed for minimal size. +OrganicClaw is a wrapper around JsonCpp, chosen because it technically +implements a superset of JSON. +OrganicClaw requires support for 64 bit integers, whereas normal JSON +technically does not. + +These formats do not currently support floats. + +There is also a wrapper format called Claw that provides a header at the +beginning of the file and can dynamically switch between the two depending on +what the header says is present. +The Claw header also includes information about the type and type version of +the data. + +Claw header: ```M1;net.drinkingtea.nostalgia.core.NostalgiaPalette;1;``` + +That reads: + +* Format is Metal Claw, version 1 +* Type ID is net.drinkingtea.nostalgia.core.NostalgiaPalette +* Type version is 1 + +Except when the data is exported for loading on the GBA, Claw is always used as +a wrapper around the bare formats. + +#### Metal Claw Example + +##### Read + +```cpp +#include + +ox::Result loadPalette1(ox::BufferView const&buff) noexcept { + return ox::readMC(buff); +} + +ox::Result loadPalette2(ox::BufferView const&buff) noexcept { + NostalgiaPalette pal; + oxReturnError(ox::readMC(buff, pal)); + return pal; +} +``` + +##### Write + +```cpp +#include + +ox::Result writeSpritePalette1(NostalgiaPalette const&pal) noexcept { + ox::Buffer buffer(ox::units::MB); + std::size_t sz = 0; + oxReturnError(ox::writeMC(buffer.data(), buffer.size(), pal, &sz)); + buffer.resize(sz); + return buffer; +} + +ox::Result writeSpritePalette2(NostalgiaPalette const&pal) noexcept { + return ox::writeMC(pal); +} +``` + +#### Organic Claw Example + +##### Read + +```cpp +#include + +ox::Result loadPalette1(ox::BufferView const&buff) noexcept { + return ox::readOC(buff); +} + +ox::Result loadPalette2(ox::BufferView const&buff) noexcept { + NostalgiaPalette pal; + oxReturnError(ox::readOC(buff, &pal)); + return pal; +} +``` + +##### Write + +```cpp +#include + +ox::Result writeSpritePalette1(NostalgiaPalette const&pal) noexcept { + ox::Buffer buffer(ox::units::MB); + oxReturnError(ox::writeOC(buffer.data(), buffer.size(), pal)); + return buffer; +} + +ox::Result writeSpritePalette2(NostalgiaPalette const&pal) noexcept { + return ox::writeOC(pal); +} +``` + +#### Claw Example + +##### Read + +```cpp +#include + +ox::Result loadPalette1(ox::BufferView const&buff) noexcept { + return ox::readClaw(buff); +} + +ox::Result loadPalette2(ox::BufferView const&buff) noexcept { + NostalgiaPalette pal; + oxReturnError(ox::readClaw(buff, pal)); + return pal; +} +``` + +##### Write + +```cpp +#include + +ox::Result writeSpritePalette(NostalgiaPalette const&pal) noexcept { + return ox::writeClaw(pal); +} +```