{"id":26094072,"url":"https://github.com/statismike/gd-props","last_synced_at":"2025-04-12T09:34:05.381Z","repository":{"id":202894972,"uuid":"707844510","full_name":"StatisMike/gd-props","owner":"StatisMike","description":null,"archived":false,"fork":false,"pushed_at":"2024-08-04T09:14:05.000Z","size":1309,"stargazers_count":3,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-26T04:33:37.797Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/StatisMike.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-20T19:41:38.000Z","updated_at":"2024-08-04T09:13:15.000Z","dependencies_parsed_at":null,"dependency_job_id":"62dcb0e7-62c0-4f89-814d-6d6e689dade3","html_url":"https://github.com/StatisMike/gd-props","commit_stats":null,"previous_names":["statismike/ronres-gdext","statismike/godot_io","statismike/gd-props"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StatisMike%2Fgd-props","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StatisMike%2Fgd-props/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StatisMike%2Fgd-props/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StatisMike%2Fgd-props/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/StatisMike","download_url":"https://codeload.github.com/StatisMike/gd-props/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248546496,"owners_count":21122330,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-09T12:49:43.790Z","updated_at":"2025-04-12T09:34:05.357Z","avatar_url":"https://github.com/StatisMike.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gd-props\n![Tests](https://github.com/StatisMike/gd-props/actions/workflows/tests.yaml/badge.svg)\n\n\u003e Resources are akin to the versatile props that set the scene for an interactive masterpiece on the stage of game world. \n\u003e Much like actors skillfully employ props to enrich their storytelling, game objects leverage resources to craft a compelling virtual \n\u003e narrative. The mastery lies in the thoughtful selection and optimization of these digital tools, guaranteeing a captivating \n\u003e performance as players step into the spotlight of the gaming world.\n\nCustom resources created with [godot-rust](https://github.com/godot-rust/gdext) can be very useful. However, as the game data becomes \ncomplex, the workflow available out of the box can be quite limiting. By default, Godot saves all resources into `.tres` and `.res` files, \npreserving only the state of fields marked with `#[export]`. This limitation confines the saving of state only to fields converted to \nGodot-compatible types.\n\n`gd-props` aims to address this issue by providing an alternative strategy for loading and saving resources, relying fully on `serde` \nserialization and deserialization. Resources can be saved in two formats:\n\n- `.gdron`: Based on the `Ron` format from the `ron` crate. Intended for human-readable output during development.\n- `.gdbin`: Based on the `MessagePack` format from the `rmp_serde` crate. Intended for faster serialization and deserialization \n- times, especially in exported games.\n\n\n## Limitations\n\n\u003e Currently `gd-props` supports only `Resource`s without the `Base\u003cResource\u003e` field.\n\nIf you include `gd-props` in your dependencies, you need use utilize [`godot`](https://github.com/godot-rust/gdext) crate with the feature\n`experimental-threads` enabled.\n\nThis necessity arises from Godot's utilization of registered `ResourceFormatLoader`s methods during `InitLevel::Editor` on an additional \nthread, which cannot be inhibited. The `godot` crate, by default, prohibits multi-threading (triggering a panic in debug builds), as it is \nstill not deemed safe by the maintainers.\n\nTo monitor the current status of the `experimental-threads` feature requirement, you can follow this \n[this issue](https://github.com/godot-rust/gdext/issues/597).\n\n## Current Features\n\nThe following features are currently available. More will be listed in the `In Development` section.\n\n- `GdProp` derive macro for custom `Resource`s, making them savable to `.gdron` and `.gdbin` formats.\n- `serde_gd` module containing submodules to be used with `serde`, making it easier to implement `Serialize` and `Deserialize` for your \n  custom resources.\n- `gd_props_plugin` macro, which handles:\n  - setting up `ResourceFormatSaver` and `ResourceFormatSaver` to handle `.gdron` and `.gdbin` formats.\n  - setting up `EditorPlugin` and `EditorExportPlugin` to handle export of `.gdron` and `.gdbin` formats.\n    - during export, all `.gdron` files are transformed into `.gdbin`, as the later is more compact and much faster to load. \n\n## In Development\n\n\u003e **This crate is not production-ready** ⚠️\n\u003e\n\u003e This crate is early in development, and its API may change. Contributions, discussions, and informed opinions are very welcome.\n\n## GdProp macro\nConsider a scenario where you have a resource with a structure similar to the one below. You might contemplate transforming a `HashMap` \ninto Godot's Dictionary, but this conversion could entail sacrificing some of its advantages. On the other hand, for structs like \n`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`.\n\n\n```rust\n#[derive(GodotClass, Serialize, Deserialize, GdProp)]\n#[class(base=Resource)]\npub struct Statistics {\n  /// Current character level - only available fully on Godot editor side. Rest can be accessed by other Rust GodotClasses.\n  #[var]\n  pub level: u32,\n  /// All stats\n  pub stats: HashMap\u003cGeneralStat, usize\u003e,\n  /// Experience currently gained by the character. Every 100 experience points grants a level up with the chance of increasing stats.\n  pub exp: usize,\n  /// Amount of bane needed to be applied to the character - the higher, the more *boons* it amassed.\n  pub bane: usize,\n  /// Modifiers from [StatModEffect]. Key is the number of turns left, while value is the stat modifiers.\n  pub effect_mods: HashMap\u003cusize, StatModifiers\u003e,\n  /// Modifiers from equipped items. Key is the index of the item.\n  pub item_mods: HashMap\u003cusize, StatModifiers\u003e,\n  /// Modifiers from character class\n  pub class_mods: StatModifiers,\n}\n```\n`GdProp` derive macro implements `GdProp` trait, and makes the Resource saveable with our `gd-props` straight to `.gdron` and `.gdbin` file.\n\nThe `.gdron` format is a slightly modified `Ron` file, distinguished by the inclusion of a header containing the struct identifier or \nresource class name. For a random object of the above structure, the resulting file might look like this:\n\n\n```\n(gd_class:\"Statistics\",uid:\"uid://bwgy4ec84b8xv\")\n(\n    level: 3,\n    stats: {\n        Mv: 0,\n        Lck: 7,\n        Def: 7,\n        Mag: 7,\n        Agi: 9,\n        HP: 28,\n        Res: 3,\n        Dex: 7,\n        Str: 7,\n    },\n    exp: 0,\n    bane: 4,\n    effect_mods: {},\n    item_mods: {},\n    class_mods: (\n        x: {},\n    ),\n)\n```\n\nThe header, in this case, contains `Statistics`, signifying the class name of the serialized struct. This format is designed for \nhuman-readable output during development, aiding in easy inspection and modification of the saved resources. Additionally,\nGodot's `uid` path is also preserved there.\n\nOn the other hand, the `.gdbin` format is based on the `MessagePack` format from the `rmp_serde` crate. It is intended for faster \nserialization and deserialization times, especially in exported games, and in other aspects is analogous to `.gdron`.\n\nBoth formats, whether human-readable or optimized for performance, offer the flexibility to choose the serialization strategy \nthat best suits your development and deployment needs.\n\nBoth file are recognizable by Godot editor, can be loaded through it and attached to some Godot class.\n\n## Bundled resources\nWhat 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: \n- `gd_props::serde_gd::gd_option` - for `Option\u003cGd\u003cT\u003e\u003e` fields,\n- `gd_props::serde_gd::gd` - for `Gd\u003cT\u003e` fields,\n- `gd_props::serde_gd::gd_array` - for `Array\u003cGd\u003cT\u003e\u003e` fields,\n- `gd_props::serde_gd::gd_hashmap` - for `HashMap\u003cK, Gd\u003cT\u003e` fields.\n\nThere are some requirements for this to work:\n- `T` needs to be User-defined `GodotClass` inheriting from `Resource`,\n- `T` needs to derive `Serialize` and `Deserialize`.\n  \n### Example\n```rust\n#[derive(GodotClass, Serialize, Deserialize, GdProp)]\n#[class(base=Resource)]\npub struct CharacterData {\n    #[export]\n    affiliation: CharacterAffiliation,\n    #[export]\n    #[serde(with=\"gd_props::serde_gd::gd_option\")]\n    statistics: Option\u003cGd\u003cStatistics\u003e\u003e,\n}\n```\nUpon saving, we receive file as below:\n```\n(gd_class:\"CharacterData\",uid:\"uid://dfa37uvpqlnhq\")\n(\n    affiliation: Player,\n    statistics: Some((\n        level: 3,\n        stats: {\n            Def: 7,\n            Dex: 7,\n            Lck: 7,\n            Mag: 7,\n            Res: 3,\n            Mv: 0,\n            HP: 28,\n            Agi: 9,\n            Str: 7,\n        },\n        exp: 0,\n        bane: 4,\n        effect_mods: {},\n        item_mods: {},\n        class_mods: (\n            x: {},\n        ),\n    )),\n)\n```\n\n## External Resources\nIf you desire to preserve a sub-resource as an External Resource, akin to regular resource saving in Godot, `gd-props` provides two additional modules:\n\n- `gd_props::serde_gd::ext_option` - designed for `Option\u003cGd\u003cT\u003e\u003e` fields,\n- `gd_props::serde_gd::ext` - designed for `Gd\u003cT\u003e` fields,\n- `gd_props::serde_gd::ext_array` - for `Array\u003cGd\u003cT\u003e\u003e` fields,\n- `gd_props::serde_gd::ext_hashmap` - for `HashMap\u003cK, Gd\u003cT\u003e` fields.\n\nTo enable this functionality, a few requirements must be met:\n\n- `T` needs to be a `Resource`.\n- `T` must be a standalone `Resource` and be savable to and loadable from a file.\n\nThis approach offers several advantages:\n\n- `T` doesn't necessarily need to be a User-defined `GodotClass`, making it compatible with built-in resources.\n- External Resource instances are reused whenever they are referenced, enhancing efficiency and reducing redundancy in the game data.\n\n### Example\n```rust\n#[derive(GodotClass, Serialize, Deserialize, GdProp)]\n#[class(base=Resource)]\npub struct CharacterData {\n    #[export]\n    affiliation: CharacterAffiliation,\n    // As `statistics` is User-defined Resource, so we could also use `gd_option` module to bundle the Resource.\n    #[export]\n    #[serde(with=\"gd_props::serde_gd::ext_option\")]\n    statistics: Option\u003cGd\u003cStatistics\u003e\u003e,\n    #[export]\n    #[serde(with=\"gd_props::serde_gd::ext_option\")]\n    nothing_is_here: Option\u003cGd\u003cResource\u003e\u003e,\n    #[export]\n    #[serde(with=\"gd_props::serde_gd::ext_option\")]\n    texture: Option\u003cGd\u003cCompressedTexture2D\u003e\u003e,\n}\n```\nUpon saving to `.gdron` format we receive file as below:\n```\n(gd_class:\"CharacterData\",uid:\"uid://dfa37uvpqlnhq\")\n(\n    affiliation: Player,\n    statistics: ExtResource((\n        gd_class: \"Statistics\",\n        uid: \"uid://dixv2uvh8waug\",\n        path: \"res://statistics.gdron\",\n    )),\n    nothing_is_here: None,\n    texture: ExtResource((\n        gd_class: \"CompressedTexture2D\",\n        uid: \"uid://ci3y6557pn0o\",\n        path: \"res://icon.svg\",\n    )),\n)\n```\n\n## GdProp tooling\n\nNow that we have Rust resources fully serializable to `.gdron` and `.gdprop`, the next step is to provide tools for saving and loading \nthem within the Godot engine. The default `ResourceSaver` and `ResourceLoader` are unaware of our `.gdron` and `.gdbin` files.\n\nAlso, by default the files won't be included into exported game, and actually we want to have a say in how they should be\nexported.\n\nTo automatically define all tool GodotClass needed to handle introduced formats, the `#[gd_props_plugin]` macro should be used.\nBelow example that creates all needed tools and register two `GdProps`-annotated resources to be recognized by them.\n\n```rust\nuse godot::prelude::*;\nuse gd_props::gd_props_plugin;\n\n// Macro creates four different GodotClasses and registers two resources implementing `GdProp`\n#[gd_props_plugin]\n#[register(CharacterData, Statistics)]\npub(crate) struct PropPlugin;\n \n// Plugin and Exporter are only available in-editor for exporting resources.\nassert_eq!(PropPlugin::INIT_LEVEL, InitLevel::Editor);\nassert_eq!(PropPluginExporter::INIT_LEVEL, InitLevel::Editor);\n\n// Loader and Saver are available in scenes for loading/saving registered resources.\nassert_eq!(PropPluginSaver::INIT_LEVEL, InitLevel::Scene);\nassert_eq!(PropPluginLoader::INIT_LEVEL, InitLevel::Scene);\n```\n\n### Custom Format Saving and Loading with `GdProp`\n\nAfter above, 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\nin `GdPropSaver` and `GdPropLoader` traits.\n\n```rust\n// lib.rs\nuse godot_io::traits::{GdPropLoader, GdPropSaver};\n\nstruct MyExtension;\n\n#[gdextension]\nunsafe impl ExtensionLibrary for MyExtension {\n    fn on_level_init(_level: InitLevel) {\n        if _level == InitLevel::Scene {\n            PropPluginLoader::register_loader();\n            PropPluginSaver::register_saver();\n        }\n    }\n    // And we need to unregister them when editor is closing!\n    fn on_level_deinit(deinit: InitLevel) {\n       if deinit == InitLevel::Scene {\n           PropPluginLoader::unregister_loader();\n           PropPluginSaver::unregister_saver();\n       }\n    }\n}\n```\n\n### Custom format export\n\nContrary to Loader and Saver, just a definition of `EditorPlugin` GodotClass is enough to handle the resources\non export and no extra steps are needed. Besides adding the custom format resources into the exported executable, \nall resources in `.gdron` format will be translated into `.gdbin`, as main reason for the former (being human-readible)\nisn't needed anymore, and the later is more concise and faster to load. \n\nAs comparison from `gd-rehearse` run shows, the difference is meaningiful, so currently there is no way\nto opt-out of the conversion.\n\nOn debug (both Godot and Rust) build, where resources with paths ending with `.gdron` are saved/loaded as `.gdron` files.\n\n```\n--------------------------------------------------------------------------------\n   Running Rust benchmarks\n--------------------------------------------------------------------------------\n                                              min       median\n   gdbin.rs:\n   -- serialize                  ...     27.260μs     29.396μs\n   -- deserialize                ...     73.402μs     73.855μs\n   -- gdbin_save                 ...    350.893μs    360.174μs\n   -- gdbin_load                 ...    228.172μs    230.172μs\n\n   gdron.rs:\n   -- serialize                  ...     37.871μs     40.088μs\n   -- deserialize                ...     82.897μs     83.356μs\n   -- gdron_save                 ...    492.388μs    502.603μs\n   -- gdron_load                 ...    979.330μs    988.216μs\n```\n\nOn release (both Godot and Rust) build, where resources with paths ending with `.gdron` are remapped to `.gdbin`, the \ntimes are similiar for both formats: slightly higher times on `gdron` are probably caused by Godot's remap system.\n\n\u003e Saving was omitted, as the `res://` path is unavailable while exported\n\n```\n--------------------------------------------------------------------------------\n   Running Rust benchmarks\n--------------------------------------------------------------------------------\n                                              min       median\n   gdbin.rs:\n   -- serialize                  ...      3.853μs      5.548μs\n   -- deserialize                ...      7.514μs      9.500μs\n   -- gdbin_load                 ...    120.190μs    177.116μs\n\n   gdron.rs:\n   -- serialize                  ...      5.105μs      6.051μs\n   -- deserialize                ...      7.113μs      8.318μs\n   -- gdron_load                 ...    140.451μs    205.902μs\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstatismike%2Fgd-props","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstatismike%2Fgd-props","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstatismike%2Fgd-props/lists"}