A library for loading and creating skins for various rhythm games. It supports cross-platform usage including Web and Node.js environments via WebAssembly (WASM).
Add this to your Cargo.toml:
[dependencies]
rgskin = "0.0.3"Or run:
cargo add rgskinuse rgskin::prelude::*;
// importing a skin from a directory
let osu_skin = import::osu::skin_from_dir("path/to/skin").expect("Failed to import skin!", false);
let fluxis_skin = import::fluxis::skin_from_dir("path/to/skin").expect("Failed to import skin!", false);The second argument is for if you want to import ALL assets for a skin, it will import all textures but leave the unrequired unloaded, usually recommended when merging skins of the same type as you might need all assets; otherwise if false it will only import and load the required assets.
use rgskin::prelude::*;
// create a new texture store, this is where the actual textures will be stored
let mut textures = TextureStore::new();
// you can import textures from a directory like this:
textures = import::all_textures_from_dir("path/to/skin", None)?;
// or alternatively if you parse the skin config first, you can import only the textures you need like this:
let raw_str = import::osu::ini_str_from_dir("path/to/skin");
let skin_config = OsuSkinIni::from_str(&raw_str)?;
// since get_required_texture_paths returns a HashSet, we need to convert it to a Vec<&str> for the import function
let required_texture_paths_set = skin_config.get_required_texture_paths();
let required_texture_paths = required_texture_paths_set.iter().map(|s| s.as_str()).collect::<Vec<_>>();
textures = import::textures_from_dir("path/to/skin", &required_texture_paths)?;
// now you can create a skin from the config and textures
let osu_skin = OsuSkin::new(skin_config, Some(textures), None); // last parameter is the sound samples store, which you can import similarly to texturesAll skins are loaded in their original formats; Any textures go in TextureStore or samples go in SampleStore, etc. The config is also preserved so, this next part will talk about dealing with generic skins as creating skins for a specific game differs from one to another.
Additionally all skins can be converting into a generic version of it.
Examples:
OsuSkin::from_generic_mania(&generic);
OsuSkin.to_generic_mania(()); // yes, the extra parethesis is required.FluXisSkin::from_generic_mania(&generic);
FluXisSkin.to_generic_mania(fluxis_layout); // if you don't have a layout you can just pass None or ().Unlike skins from games not all textures are stored in TextureStore. Skin Elements can have their own textures that are shared pointers (Option<Arc<RwLock<Texture>>>). Meaning the texture can be shared anywhere either in a TextureStore or inside a Skin Element.
export::osu::skin_to_dir(&skin, "path/to/export/to")?;
export::fluxis::skin_to_dir(&skin, "path/to/export/to")?;export::osu::ini_to_dir(skin.skin_ini, "path/to/export/to"); // export::{game}
export::textures_to_dir(skin.textures, "path/to/export/to");
export::samples_to_dir(skin.samples, "path/to/export/to"); // if you have samplesFor Node.js:
npm install @r2o3/rgskin-nodejsFor web projects:
<script src="https://unpkg.com/@r2o3/rgskin-browser@latest/rgskin.js"></script>or
npm install @r2o3/rgskin-browserthen use as an ES module
// For ES modules
import * as rgskin from '@r2o3/rgskin'; // or if not in node modules use the path to rgskin.js
// or alternatively
const rgskin = await import('path/to/rgskin.js')
// For CommonJS
const rgskin = require('rgskin');you may need to do await rgskin.default() after importing if you've imported it in a script tag (with type="module") or you get an error like Uncaught TypeError: Cannot read properties of undefined (reading '__wbindgen_malloc')
As of now you can't parse/write using the original structures in JS/TS, will be supported in the near future.
For Node:
const OsuSkin = rgskin.osuSkinFromDir("path/to/skin");
const FluXisSkin = rgskin.fluXisSkinFromDir("path/to/skin");// create a new texture store, this is where the actual textures will be stored
let textures = new rgskin.TextureStore();
// you can import textures from a directory like this:
textures = rgskin.allTexturesFromDir("path/to/skin");
// you can parse configs like this:
let raw_str = rgskin.iniStrFromDir("path/to/skin")
let skin_config = rgskin.OsuSkinIni.fromStr(raw_str);
let osuSKin = new rgskin.OsuSkin(skin_config, textures, null) // last parameter is the sound samples store, which you can import similarly to texturesFor Browsers:
Unfortunately you can't automatically import everything using a single function. So you'll have to do a bit of work. Check FilesMap preparation
const OsuSkin = rgskin.osuSkinFromFiles(filesMap);
const FluXisSkin = rgskin.fluXisSkinFromFiles(filesMap);// create a new texture store, this is where the actual textures will be stored
let textures = new rgskin.TextureStore();
// you can import textures from files like this:
// filesMap is a Map object with relative path -> Uint8Array pairs
textures = rgskin.allTexturesFromFiles(filesMap);
// you can parse configs like this:
// assuming you have the skin.ini file in your filesMap
let raw_str = filesMap.get("skin.ini"); // get the Uint8Array
let decoder = new TextDecoder();
let ini_string = decoder.decode(raw_str);
let skin_config = rgskin.OsuSkinIni.fromStr(ini_string);
let osuSkin = new rgskin.OsuSkin(skin_config, textures, null); // last parameter is the sound samples store, which you can import similarly to texturesThe filesMap parameter is a Map object where:
- Keys are relative paths (strings)
- Values are file contents as
Uint8Array
Example: Reading files from an HTML file input
const filesMap = new Map();
// assuming you have an <input type="file" multiple webkitdirectory> element
fileInput.addEventListener('change', async (event) => {
const files = event.target.files;
for (const file of files) {
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const relativePath = file.webkitRelativePath || file.name;
filesMap.set(relativePath, uint8Array);
}
// now you can import the skin
const skin = rgskin.osuSkinFromFiles(filesMap);
});Check Rust's Creating Skins for more details
OsuSkin.fromGenericMania(&generic);
OsuSkin.toGenericMania();FluXisSkin.fromGenericMania(&generic);
FluXisSkin.toGenericMania(fluxis_layout); // if you don't have a layout you can just not pass anything or null.For Node:
rgskin.osuSkinToDir(skin, "path/to/export/to");
rgskin.fluXisSkinToDir(skin, "path/to/export/to");rgskin.iniToDir(skin.skin_ini, "path/to/export/to");
rgskin.texturesToDir(skin.textures, "path/to/export/to");
rgskin.samplesToDir(skin.samples, "path/to/export/to"); // if you have samplesFor Browsers:
// Returns a JavaScript Map object with relative path -> Uint8Array pairs
const filesMap = rgskin.osuSkinToFiles(skin);
const filesMap = rgskin.fluXisSkinToFiles(skin);const iniString = rgskin.iniToString(skin.skin_ini);
const texturesMap = rgskin.texturesToFiles(skin.textures);
const samplesMap = rgskin.samplesToFiles(skin.samples);
// combine them into a single Map if needed
const filesMap = new Map([
['skin.ini', new TextEncoder().encode(iniString)],
...texturesMap,
...samplesMap
]);Actually Exporting/Downloading the files will depend on your implementation.
Example using JSZip:
const filesMap = rgskin.osuSkinToFiles(skin);
const zip = new JSZip();
filesMap.forEach((data, path) => {
zip.file(path, data);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
const link = document.createElement('a');
link.href = URL.createObjectURL(zipBlob);
link.download = 'skin.zip';
link.click();Example Downloading each file indiviually:
const filesMap = rgskin.osuSkinToFiles(skin);
filesMap.forEach((data, path) => {
const blob = new Blob([data]);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = path;
link.click();
});cargo build- Install wasm-pack:
cargo install wasm-packImportant
It's really recommended to have wasm-opt installed and added to path for the wasm build.
- Build the package:
npm run build # debug build
npm run build-release # release build- This will build it for both node and browser and the output will be in
dist-webanddist-nodedirectory.
r2o3 uses the MIT License for all its sibiling projects. See LICENSE for more information