[nostalgia] Add models and serialization to Developer Handbook

This commit is contained in:
Gary Talent 2021-05-03 21:51:56 -04:00
parent a415e86ae5
commit d49576451c

View File

@ -127,9 +127,10 @@ question.
Pointers are generally preferred to references. References should be used for Pointers are generally preferred to references. References should be used for
optimizing the passing in of parameters and for returning from accessor optimizing the passing in of parameters and for returning from accessor
operators (e.g. ```T &Vector::operator[](size_t)```). As parameters, references operators (e.g. ```T &Vector::operator[](size_t)```). As parameters, references
should always be const. A non-const reference is generally used because the parameter should always be const. A non-const reference is generally used because the
value is changed in the function, but it will look like it was passed in by value parameter value is changed in the function, but it will look like it was passed
where it is called, making that code less readable. in by value where it is called and thus not subject to change. The reference
operator makes it clear that the value can and likely will change.
### Error Handling ### Error Handling
@ -140,12 +141,13 @@ disabled. Exceptions cause the compiler to generate a great deal of extra code
that inflates the size of the binary. The binary size bloat is often cited as that inflates the size of the binary. The binary size bloat is often cited as
one of the main reasons why many embedded developers prefer C to C++. one of the main reasons why many embedded developers prefer C to C++.
Instead throwing exceptions, all engine code must return error codes. Nostalgia Instead of throwing exceptions, all engine code must return error codes.
and Ox both use ```ox::Error``` to report errors. ```ox::Error``` is a struct Nostalgia and Ox both use ```ox::Error``` to report errors. ```ox::Error``` is
that has overloaded operators to behave like an integer error code, plus some a struct that has overloaded operators to behave like an integer error code,
extra fields to enhance debuggability. If instantiated through the ```OxError(x)``` plus some extra fields to enhance debuggability. If instantiated through the
macro, it will also include the file and line of the error. The ```OxError(x)``` ```OxError(x)``` macro, it will also include the file and line of the error.
macro should only be used for the initial instantiation of an ```ox::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<T>```. In addition to ```ox::Error``` there is also the template ```ox::Result<T>```.
```ox::Result``` simply wraps the type T value in a struct that also includes ```ox::Result``` simply wraps the type T value in a struct that also includes
@ -271,11 +273,14 @@ ox::Result<int> f2() {
* ```oxRequireT``` - oxRequire Throw * ```oxRequireT``` - oxRequire Throw
* ```oxRequireMT``` - oxRequire Mutable Throw * ```oxRequireMT``` - oxRequire Mutable Throw
### Logging ### Logging and Output
Ox provides for logging and debug prints via the ```oxTrace```, ```oxDebug```, and ```oxError``` macros. Ox provides for logging and debug prints via the ```oxTrace```, ```oxDebug```, and ```oxError``` macros.
Each of these also provides a format variation. 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. 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. They also print with the channel that they are on, along with file and line.
@ -284,7 +289,7 @@ Where trace statements are intended to be written with thoughtfulness,
debug statements are intended to be quick and temporary insertions. debug statements are intended to be quick and temporary insertions.
Debug statements trigger compilation failures if OX_NODEBUG is enabled when CMake is run, 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. as it is on Jenkins builds, so ```oxDebug``` statements should never be checked in.
This makes oxDebug preferable to other from of logging, as temporary prints should This makes oxDebug preferable to other forms of logging, as temporary prints should
never be checked in anyway. never be checked in anyway.
```oxError``` always prints. ```oxError``` always prints.
@ -325,3 +330,245 @@ nostalgia::studio::Project, which should go through ox::FileSystem.
ox::FileSystem abstracts away differences between conventional storage devices ox::FileSystem abstracts away differences between conventional storage devices
and ROM. and ROM.
### 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.
The type must provide its number of fields in a static constexper integer named Fields.
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 witht 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 Fields = 1;
static constexpr auto TypeVersion = 1;
ox::Vector<Color16> colors;
};
struct NostalgiaGraphic {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
static constexpr auto Fields = 6;
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<uint8_t> pixels;
};
template<typename T>
constexpr ox::Error model(T *io, NostalgiaPalette *pal) {
io->template setTypeInfo<NostalgiaPalette>();
// it is also possible to provide the type name and number of fields as function arguments
//io->setTypeInfo("net.drinkingtea.nostalgia.core.NostalgiaPalette", 1);
oxReturnError(io->field("colors", &pal->colors));
return OxError(0);
}
template<typename T>
constexpr ox::Error model(T *io, NostalgiaGraphic *ng) {
io->template setTypeInfo<NostalgiaGraphic>();
oxReturnError(io->field("bpp", &ng->bpp));
oxReturnError(io->field("rows", &ng->rows));
oxReturnError(io->field("columns", &ng->columns));
oxReturnError(io->field("defaultPalette", &ng->defaultPalette));
oxReturnError(io->field("pal", &ng->pal));
oxReturnError(io->field("pixels", &ng->pixels));
return OxError(0);
}
```
The model system also provides for unions:
```cpp
#include <ox/model/types.hpp>
class FileAddress {
template<typename T>
friend Error model(T*, FileAddress*);
public:
static constexpr auto TypeName = "net.drinkingtea.ox.FileAddress";
static constexpr auto Fields = 2;
union Data {
static constexpr auto TypeName = "net.drinkingtea.ox.FileAddress.Data";
static constexpr auto Fields = 3;
char *path;
const char *constPath;
uint64_t inode;
};
FileAddressType m_type = FileAddressType::None;
Data m_data;
};
template<typename T>
constexpr Error model(T *io, FileAddress::Data *obj) {
io->template setTypeInfo<FileAddress::Data>();
oxReturnError(io->field("path", SerStr(&obj->path)));
oxReturnError(io->field("constPath", SerStr(&obj->path)));
oxReturnError(io->field("inode", &obj->inode));
return OxError(0);
}
template<typename T>
constexpr Error model(T *io, FileAddress *fa) {
io->template setTypeInfo<FileAddress>();
oxReturnError(io->field("type", bit_cast<int8_t*>(&fa->m_type)));
oxReturnError(io->field("data", UnionView(&fa->m_data, static_cast<int>(fa->m_type))));
return OxError(0);
}
```
### 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.
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.
Except when the data is exported for loading on the GBA, Claw is always used as
a wrapper around the bare formats.
These formats do not currently support ```float```s.
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
#### Metal Claw Example
##### Read
```cpp
#include <ox/mc/read.hpp>
ox::Result<NostalgiaPalette> loadPalette1(const Buffer &buff) noexcept {
return ox::readMC<NostalgiaPalette>(buff);
}
ox::Result<NostalgiaPalette> loadPalette2(const Buffer &buff) noexcept {
return ox::readMC<NostalgiaPalette>(buff.data(), buff.size());
}
ox::Result<NostalgiaPalette> loadPalette3(const Buffer &buff) noexcept {
NostalgiaPalette pal;
oxReturnError(ox::readMC(buff.data(), buff.size(), &pal));
return pal;
}
```
##### Write
```cpp
#include <ox/mc/write.hpp>
ox::Result<ox::Buffer> writeSpritePalette1(NostalgiaPalette *pal) noexcept {
ox::Buffer buffer(ox::units::MB);
oxReturnError(ox::writeMC(buffer.data(), buffer.size(), pal));
return ox::move(buffer);
}
ox::Result<ox::Buffer> writeSpritePalette2(NostalgiaPalette *pal) noexcept {
return ox::writeMC(pal);
}
```
#### Organic Claw Example
##### Read
```cpp
#include <ox/oc/read.hpp>
ox::Result<NostalgiaPalette> loadPalette1(const Buffer &buff) noexcept {
return ox::readOC<NostalgiaPalette>(buff);
}
ox::Result<NostalgiaPalette> loadPalette2(const Buffer &buff) noexcept {
return ox::readOC<NostalgiaPalette>(buff.data(), buff.size());
}
ox::Result<NostalgiaPalette> loadPalette3(const Buffer &buff) noexcept {
NostalgiaPalette pal;
oxReturnError(ox::readOC(buff.data(), buff.size(), &pal));
return pal;
}
```
##### Write
```cpp
#include <ox/oc/write.hpp>
ox::Result<ox::Buffer> writeSpritePalette1(NostalgiaPalette *pal) noexcept {
ox::Buffer buffer(ox::units::MB);
oxReturnError(ox::writeOC(buffer.data(), buffer.size(), pal));
return ox::move(buffer);
}
ox::Result<ox::Buffer> writeSpritePalette2(NostalgiaPalette *pal) noexcept {
return ox::writeOC(pal);
}
```
#### Claw Example
##### Read
```cpp
#include <ox/claw/read.hpp>
ox::Result<NostalgiaPalette> loadPalette1(const Buffer &buff) noexcept {
return ox::readClaw<NostalgiaPalette>(buff);
}
ox::Result<NostalgiaPalette> loadPalette2(const Buffer &buff) noexcept {
return ox::readClaw<NostalgiaPalette>(buff.data(), buff.size());
}
ox::Result<NostalgiaPalette> loadPalette3(const Buffer &buff) noexcept {
NostalgiaPalette pal;
oxReturnError(ox::readClaw(buff.data(), buff.size(), &pal));
return pal;
}
```
##### Write
```cpp
#include <ox/claw/write.hpp>
ox::Result<ox::Buffer> writeSpritePalette(NostalgiaPalette *pal) noexcept {
return ox::writeClaw(&pal);
}
```