Skip to content

Commit

Permalink
Merge pull request #8 from StatisMike/crate_rename
Browse files Browse the repository at this point in the history
crate rename to gd-props
  • Loading branch information
StatisMike authored Dec 5, 2023
2 parents a29de45 + fc5410a commit 27168a9
Show file tree
Hide file tree
Showing 30 changed files with 314 additions and 549 deletions.
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[workspace]
resolver = "2"
members = [
"godot_io",
"godot_io_defs",
"godot_io_derive",
"gd-props",
"gd-props-defs",
"gd-props-macros",

# tests
"tests/rust"
Expand Down
165 changes: 105 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,75 @@
# godot_io
Creating custom Godot resources with [godot-rust](https://github.com/godot-rust/gdext) and using them in the Godot Editor is fun and useful. There are some drawbacks to the process out of the box, though.
# gd-props

Godot default `ResourceSaver` and `ResourceLoader` can only handle `exported` fields of your resources. These needs to be recognized by Godot editor - so Godot types. This can be cumbersome if you want to save some more complex state inside your resource.
> Resources are akin to the versatile props that set the scene for an interactive masterpiece on the stage of game world.
> Much like actors skillfully employ props to enrich their storytelling, game objects leverage resources to craft a compelling virtual
> narrative. The mastery lies in the thoughtful selection and optimization of these digital tools, guaranteeing a captivating
> performance as players step into the spotlight of the gaming world.
This crate is born from this frustration and its goal is to provide tools to save rust-created Resources straight to and from custom format.
Custom resources created with [godot-rust](https://github.com/godot-rust/gdext) can be very useful. However, as the game data becomes
complex, the workflow available out of the box can be quite limiting. By default, Godot saves all resources into `.tres` and `.res` files,
preserving only the state of fields marked with `#[export]`. This limitation confines the saving of state only to fields converted to
Godot-compatible types.

`gd-props` aims to address this issue by providing an alternative strategy for loading and saving resources, relying fully on `serde`
serialization and deserialization. Resources can be saved in two formats:

- `.gdron`: Based on the `Ron` format from the `ron` crate. Intended for human-readable output during development.
- `.gdbin`: Based on the `MessagePack` format from the `rmp_serde` crate. Intended for faster serialization and deserialization
- times, especially in exported games.

## Current Features

The following features are currently available. More will be listed in the `In Development` section.

- `GdProp` derive macro for custom `Resource`s, making them savable to `.gdron` and `.gdbin` formats.
- `GdPropSaver` and `GdPropLoader` macros for easily implementing `CustomFormatSaver` and `CustomFormatLoader` for `.gdron` and `.gdbin` formats.
- `serde_gd` module containing submodules to be used with `serde`, making it easier to implement `Serialize` and `Deserialize` for your
custom resources.

## In Development
> **This crate is not production ready** ⚠️

> **This crate is not production-ready** ⚠️
>
> This crate is early in development and its API can certainly change. Contributions, discussions and informed opinions are very welcome.
> This crate is early in development, and its API may change. Contributions, discussions, and informed opinions are very welcome.
Features that will certainly be expanded upon:

- Provide more submodules in the `serde_gd` module to support iterable `Gd<Resouce>` collections.
- Make the `gdron` and `gdbin` formats interchangeable for release mode with a custom `EditorExportPlugin`.
- Ensure everything works smoothly in compiled games, especially pointers to `.tres` resources after they are changed into `.res` format.

## GdProp macro
Consider a scenario where you have a resource with a structure similar to the one below. You might contemplate transforming a `HashMap`
into Godot's Dictionary, but this conversion could entail sacrificing some of its advantages. On the other hand, for structs like
`StatModifiers` that you don't intend to handle as a `Resource`, there is a risk of loss when saving the resource with Godot's `ResourceSaver`.

Features that will be certainly expanded upon:
- add support for more compact formats, like binary and binary compressed
- make the `gdron` format interchangeable with future binary/binary compressed (for release mode)
- make everything work smoothly in compiled game (especially pointers to `.tres` resources)

## GdRonResource macro
So, imagine that you have a Resouce with a structure similiar to one below. You could potentially transform `HashMap` into Godot's Dictionary, but you would also sacrifice some of its pros. Though other structs, like `StatModifiers` which you don't intend to handle like a `Resource` is sure to be lost if saving resource with Godot's resource saver.
```rust
#[derive(GodotClass, Serialize, Deserialize, GdRonResource)]
#[derive(GodotClass, Serialize, Deserialize, GdProp)]
#[class(base=Resource)]
pub struct Statistics {
/// Current character level
#[export]
/// Current character level - only available fully on Godot editor side. Rest can be accessed by other Rust GodotClasses.
#[var]
pub level: u32,
/// All stats
pub stats: HashMap<GeneralStat, usize>,
/// Experience currently gained by the character. Every 100 experience points grants a level up with the chance of increasing stats
/// Experience currently gained by the character. Every 100 experience points grants a level up with the chance of increasing stats.
pub exp: usize,
/// Amount of bane needed to be applied to the character - the higher, the more *boons* it amassed
/// Amount of bane needed to be applied to the character - the higher, the more *boons* it amassed.
pub bane: usize,
/// Modifiers from [StatModEffect]. Key is the number of turns left, while value is the stat modifiers
/// Modifiers from [StatModEffect]. Key is the number of turns left, while value is the stat modifiers.
pub effect_mods: HashMap<usize, StatModifiers>,
/// Modifiers from equipped items. Key is the index of the item
/// Modifiers from equipped items. Key is the index of the item.
pub item_mods: HashMap<usize, StatModifiers>,
/// Modifiers from character class
pub class_mods: StatModifiers,
}
```
I presume that you saw the `GdRonResource` derive macro there, though. It implements `GdRonResource` trait, and makes the Resource saveable with our custom saver straight to `.gdron` file.
`GdProp` derive macro implements `GdProp` trait, and makes the Resource saveable with our `gd-props` straight to `.gdron` and `.gdbin` file.

The `.gdron` format is a slightly modified `Ron` file, distinguished by the inclusion of a header containing the struct identifier or
resource class name. For a random object of the above structure, the resulting file might look like this:

`.gdron` is a very slightly modified `ron` file - it's only change is an inclusion of a header containing the struct identifier (or resource type identifier in Godot terms). For a random object of above structure it would look like that:

```
(gd_class:"Statistics",uid:"uid://bwgy4ec84b8xv")
Expand All @@ -66,26 +95,37 @@ I presume that you saw the `GdRonResource` derive macro there, though. It implem
),
)
```
File is recognizable by Godot editor, could be loaded through it and attached to a node or other Resource.

The header, in this case, contains `Statistics`, signifying the class name of the serialized struct. This format is designed for
human-readable output during development, aiding in easy inspection and modification of the saved resources. Additionally,
Godot's `uid` path is also preserved there.

On the other hand, the `.gdbin` format is based on the `MessagePack` format from the `rmp_serde` crate. It is intended for faster
serialization and deserialization times, especially in exported games, and in other aspects is analogous to `.gdron`.

Both formats, whether human-readable or optimized for performance, offer the flexibility to choose the serialization strategy
that best suits your development and deployment needs.

Both file are recognizable by Godot editor, can be loaded through it and attached to some Godot class.

## Bundled resources
What if we have a Resource which contains another resource, which we would want to save as a bundled resource? There are two modules that handle this case:
- `godot_io::serde_gd::gd_option` - for `Option<Gd<T>>` fields
- `godot_io::serde_gd::gd` - for `Gd<T>` fields.
- `gd_props::serde_gd::gd_option` - for `Option<Gd<T>>` fields,
- `gd_props::serde_gd::gd` - for `Gd<T>` fields.

There are some requirements for this to work:
- `T` needs to be User-defined `GodotClass` inheriting from `Resource`
- `T` needs to derive `Serialize` and `Deserialize`
- `T` needs to be User-defined `GodotClass` inheriting from `Resource`,
- `T` needs to derive `Serialize` and `Deserialize`.

### Example
```rust
#[derive(GodotClass, Serialize, Deserialize, GdRonResource)]
#[derive(GodotClass, Serialize, Deserialize, GdProp)]
#[class(base=Resource)]
pub struct CharacterData {
#[export]
affiliation: CharacterAffiliation,
#[export]
#[serde(with="godot_io::serde_gd::gd_option")]
#[serde(with="gd_props::serde_gd::gd_option")]
statistics: Option<Gd<Statistics>>,
}
```
Expand Down Expand Up @@ -119,39 +159,41 @@ Upon saving, we receive file as below:
```

## External Resources
All right, but what if we would like to preserve the sub resource as an External Resource, just in a way that regular resource saving in Godot works? It is possible with two additional modules:
If you desire to preserve a sub-resource as an External Resource, akin to regular resource saving in Godot, `gd-props` provides two additional modules:

- `godot_io::serde_gd::ext_option` - for `Option<Gd<T>>` fields
- `godot_io::serde_gd::ext` - for `Gd<T>` fields.
- `gd_props::serde_gd::ext_option` - designed for `Option<Gd<T>>` fields.
- `gd_props::serde_gd::ext` - designed for `Gd<T>` fields.

There are some requirements for this to work:
- `T` needs to be a `Resource`
- `T` needs to be a standalone `Resource` (needs to be saved to a file and loadable from it)
To enable this functionality, a few requirements must be met:

- `T` needs to be a `Resource`.
- `T` must be a standalone `Resource` and be savable to and loadable from a file.

This approach has numerous benefits:
- `T` doesn't need to be a User-defined `GodotClass` (so built-in resources will work)
- External Resource instance will be reused whenever it is referenced
This approach offers several advantages:

- `T` doesn't necessarily need to be a User-defined `GodotClass`, making it compatible with built-in resources.
- External Resource instances are reused whenever they are referenced, enhancing efficiency and reducing redundancy in the game data.

### Example
```rust
#[derive(GodotClass, Serialize, Deserialize, GdRonResource)]
#[derive(GodotClass, Serialize, Deserialize, GdProp)]
#[class(base=Resource)]
pub struct CharacterData {
#[export]
affiliation: CharacterAffiliation,
// As `statistics` is User-defined Resource, we could also use `gd_option` module
// As `statistics` is User-defined Resource, so we could also use `gd_option` module to bundle the Resource.
#[export]
#[serde(with="godot_io::serde_gd::ext_option")]
#[serde(with="gd_props::serde_gd::ext_option")]
statistics: Option<Gd<Statistics>>,
#[export]
#[serde(with="godot_io::serde_gd::ext_option")]
#[serde(with="gd_props::serde_gd::ext_option")]
nothing_is_here: Option<Gd<Resource>>,
#[export]
#[serde(with="godot_io::serde_gd::ext_option")]
#[serde(with="gd_props::serde_gd::ext_option")]
texture: Option<Gd<CompressedTexture2D>>,
}
```
Upon saving, we receive file as below:
Upon saving to `.gdron` format we receive file as below:
```
(gd_class:"CharacterData",uid:"uid://dfa37uvpqlnhq")
(
Expand All @@ -170,50 +212,53 @@ Upon saving, we receive file as below:
)
```

## GdRonSaver and GdRonLoader macros
As we now have rust Resources fully Serializable to `.gdron`, we now need a tools for saving and loading them - default `ResourceSaver` and `ResourceLoader` don't know and won't recognize our `.gdron` files.
## Custom Format Saving and Loading with `GdProp`

Now that we have Rust resources fully serializable to `.gdron` and `.gdprop`, the next step is to provide tools for saving and loading
them within the Godot engine. The default `ResourceSaver` and `ResourceLoader` are unaware of our `.gdron` files.

`godot_io` comes with `GdRonSaver` and `GdRonLoader` derive macro, that can be used to easily create `CustomFormatSaver` and `CustomFormatLoader` and register our Resources to them! Their syntax is very similiar:
`gd-props` introduces two powerful derive macros, `GdPropSaver` and `GdPropLoader`, designed to simplify the creation of `CustomFormatSaver`
and `CustomFormatLoader` for formats it introduces. These macros enable the seamless registration of our resources to the Godot engine,
ensuring compatibility with the `.gdron` and `.gdbin` formats.

The syntax for both macros is quite similar:
```rust
// The derive itself
#[derive(GdRonSaver)]
#[derive(GdPropSaver)]
// Attribute to register the GdRonResources to be handled by given Saver/Loader
#[register(CharacterData, Statistics)]
// Multiple `register` macro attributes could be provided, all identifiers contained within will be registered
#[register(AnotherGdResource)]
#[register(AnotherGodotResource)]
```
Full example - defining both Saver and Loader:
```rust
#[derive(GodotClass, GdRonSaver)]
#[derive(GodotClass, GdPropSaver)]
#[class(base=ResourceFormatSaver, init, tool)]
#[register(CharacterData, Statistics)]
pub struct CustomRonSaver {}

#[godot_api]
impl CustomRonSaver {}

#[derive(GodotClass, GdRonLoader)]
#[derive(GodotClass, GdPropLoader)]
#[class(base=ResourceFormatLoader, init, tool)]
#[register(CharacterData)]
#[register(Statistics)]
pub struct CustomRonLoader {}

#[godot_api]
impl CustomRonLoader {}
pub struct CustomPropLoader {}
```
All that is left for Godot Editor to use our new `ResourceFormatSaver` and `ResourceFormatLoader` is to register them upon loading out `gdextension`:
All that is left for Godot Editor to use our new `ResourceFormatSaver` and `ResourceFormatLoader` is to register them upon loading out
`gdextension` to Godot's `ResourceSaver` and `ResourceLoader`, respectively. It can be achieved with provided associated methods
in `GdPropSaver` and `GdPropLoader` traits.
```rust
// lib.rs
use godot_io::traits::{GdRonLoader, GdRonSaver};
use godot_io::traits::{GdPropLoader, GdPropSaver};

struct MyExtension;

#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {
fn on_level_init(_level: InitLevel) {
if _level == InitLevel::Scene {
CustomRonLoader::register_loader();
CustomRonSaver::register_saver();
CustomPropLoader::register_loader();
CustomPropSaver::register_saver();
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion doc_index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv=refresh content=0;url=godot_io/index.html />
<meta http-equiv=refresh content=0;url=gd_props/index.html />
</head>
</html>
4 changes: 2 additions & 2 deletions godot_io_defs/Cargo.toml → gd-props-defs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "godot_io_defs"
name = "gd-props-defs"
version = "0.1.0"
edition = "2021"

Expand All @@ -12,4 +12,4 @@ godot = { git = "https://github.com/godot-rust/gdext", branch = "master" }
rmp-serde = "1.1.2"

[dev-dependencies]
godot_io = { path = "../godot_io" }
gd-props = { path = "../gd-props" }
24 changes: 24 additions & 0 deletions gd-props-defs/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use core::fmt;

use ron::error::SpannedError;

#[derive(Debug, Clone)]
pub enum GdPropError {
OpenFileRead,
OpenFileWrite,
HeaderDeserialize(SpannedError),
HeaderSerialize,
}

impl fmt::Display for GdPropError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GdPropError::OpenFileRead => write!(f, "Can't open file for reading"),
GdPropError::OpenFileWrite => write!(f, "Can't open file for writing"),
GdPropError::HeaderDeserialize(spanned) => {
write!(f, "Can't deserialize header: {}", spanned)
}
GdPropError::HeaderSerialize => write!(f, "Can't serialize header"),
}
}
}
Loading

0 comments on commit 27168a9

Please sign in to comment.