rgskin

Crates.iorgskin
lib.rsrgskin
version0.0.1
created_at2026-01-07 02:30:05.253658+00
updated_at2026-01-07 02:30:05.253658+00
descriptionA library for converting rhythm game skins.
homepage
repositoryhttps://github.com/r2o3/rgskin
max_upload_size
id2027316
size332,674
menvae (menvae)

documentation

README

rgskin

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).

Table of Contents

Rust Usage

Installation

Add this to your Cargo.toml:

[dependencies]
rgskin = "0.0.1"

Or run:

cargo add rgskin

API Reference

Importing/Loading Skins

Recommended way of Loading a skin
use rgskin::prelude::*;

// importing a skin from a directory
let osu_skin = import::osu::skin_from_dir("path/to/skin").expect("Failed to import skin!");
let fluxis_skin = import::fluxis::skin_from_dir("path/to/skin").expect("Failed to import skin!");
Manually loading a skin
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")?;

// 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 textures

Creating Skins

All 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();
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.

Exporting Skins

Recommened way of exporting a skin
export::osu::skin_to_dir(&skin, "path/to/export/to")?;
export::fluxis::skin_to_dir(&skin, "path/to/export/to")?;
Manually exporting a skin
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 samples

JavaScript/TypeScript Usage

Installation

For Node.js:

npm install @r2o3/rgskin-nodejs

For web projects:

<script src="https://unpkg.com/@r2o3/rgskin-browser@latest/rgskin.js"></script>

or

npm install @r2o3/rgskin-browser

then use as an ES module

API Reference

Initialization

// 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.

Importing/Loading Skins

For Node:

Recommended way of Loading a skin
const OsuSkin = rgskin.osuSkinFromDir("path/to/skin");
const FluXisSkin = rgskin.fluXisSkinFromDir("path/to/skin");
Manually loading a 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 textures

For 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

Recommended way of Loading a skin
const OsuSkin = rgskin.osuSkinFromFiles(filesMap);
const FluXisSkin = rgskin.fluXisSkinFromFiles(filesMap);
Manually loading a 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 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 textures
Preparing the filesMap

The filesMap parameter is a JavaScript Map object where:

  • Keys are filenames (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);
});

Creating Skins

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.

Exporting Skins

For Node:

Recommended way of exporting a skin
rgskin.osuSkinToDir(skin, "path/to/export/to");
rgskin.fluXisSkinToDir(skin, "path/to/export/to");
Manually exporting a skin
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 samples

For Browsers:

Recommended way of exporting a skin
// Returns a JavaScript Map object with relative path -> Uint8Array pairs
const filesMap = rgskin.osuSkinToFiles(skin);
const filesMap = rgskin.fluXisSkinToFiles(skin);
Manually exporting a 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();
});

Building

Rust Library

cargo build

WASM Bindings

  1. Install wasm-pack:
cargo install wasm-pack

[!IMPORTANT]
It's really recommended to have wasm-opt installed and added to path for the wasm build.

  1. Build the package:
npm run build # debug build
npm run build-release # release build
  1. This will build it for both node and browser and the output will be in dist-web and dist-node directory.

License

r2o3 uses the MIT License for all its sibiling projects. See LICENSE for more information

Commit count: 63

cargo fmt