ltk_modpkg 0.3.0

League Toolkit mod package (.modpkg) reader/writer and utilities
Documentation
# MessagePack Metadata Cross-Language Compatibility Guide

## Overview

The `ModpkgMetadata` structure is encoded using MessagePack and stored as a special chunk at `_meta_/metadata.msgpack` within the modpkg file. This provides excellent cross-language compatibility and makes it easy to add new metadata fields in future versions.

## How Metadata is Stored

- **Location**: Metadata is stored as a regular chunk at path `_meta_/metadata.msgpack`
- **Layer**: No layer (NO_LAYER_INDEX)
- **Compression**: No compression
- **Format**: MessagePack with named fields (maps) and internally tagged enums

## Current Encoding Format

### Metadata Structure (Rust)
```rust
pub struct ModpkgMetadata {
    pub schema_version: u32, // Default: 1
    pub name: String,
    pub display_name: String,
    pub description: Option<String>,
    pub version: semver::Version,
    pub distributor: Option<DistributorInfo>,
    pub authors: Vec<ModpkgAuthor>,
    pub license: ModpkgLicense,
    pub layers: Vec<ModpkgLayerMetadata>,
}

pub struct DistributorInfo {
    pub site_id: String,
    pub site_name: String,
    pub site_url: String,
    pub mod_id: String,
}

pub struct ModpkgAuthor {
    pub name: String,
    pub role: Option<String>,
}

pub struct ModpkgLayerMetadata {
    pub name: String,
    pub priority: i32,
    pub description: Option<String>,
}

pub enum ModpkgLicense {
    None,
    Spdx { spdx_id: String },
    Custom { name: String, url: String },
}
```

### MessagePack Encoding Details

**Structs** are encoded as **MessagePack maps** (named fields):
- `ModpkgMetadata` → Map with keys: `{"schema_version": ..., "name": ..., "display_name": ..., "description": ..., "version": ..., "distributor": ..., "authors": ..., "license": ..., "layers": ...}`
- `DistributorInfo` → Map with keys: `{"site_id": ..., "site_name": ..., "site_url": ..., "mod_id": ...}`
- `ModpkgAuthor` → Map with keys: `{"name": ..., "role": ...}`
- Field names use `snake_case`

**Enums** use **internally tagged** format:
- `None``{"type": "none"}`
- `Spdx { spdx_id: "MIT" }``{"type": "spdx", "spdx_id": "MIT"}`
- `Custom { name: "X", url: "Y" }``{"type": "custom", "name": "X", "url": "Y"}`

**Option<T>** encodes as:
- `None` → MessagePack `nil`
- `Some(value)` → The value directly

**semver::Version** encodes as:
- A string (e.g., `"1.0.0"`)

This format is much more cross-language friendly than positional arrays!

## C# Implementation Example

### Using MessagePack-CSharp

```csharp
using MessagePack;
using System.Collections.Generic;

[MessagePackObject]
public class ModpkgMetadata
{
    [Key("schema_version")]
    public uint SchemaVersion { get; set; } = 1;

    [Key("name")]
    public string Name { get; set; }
    
    [Key("display_name")]
    public string DisplayName { get; set; }
    
    [Key("description")]
    public string? Description { get; set; }
    
    [Key("version")]
    public string Version { get; set; } // Semver string
    
    [Key("distributor")]
    public DistributorInfo? Distributor { get; set; }
    
    [Key("layers")]
    public List<ModpkgLayerMetadata>? Layers { get; set; }
}

[MessagePackObject]
public class DistributorInfo
{
    [Key("site_id")]
    public string SiteId { get; set; }
    
    [Key("site_name")]
    public string SiteName { get; set; }
    
    [Key("site_url")]
    public string SiteUrl { get; set; }
    
    [Key("mod_id")]
    public string ModId { get; set; }
}

[MessagePackObject]
public class ModpkgAuthor
{
    [Key("name")]
    public string Name { get; set; }
    
    [Key("role")]
    public string? Role { get; set; }
}

[MessagePackObject]
public class ModpkgLayerMetadata
{
    [Key("name")]
    public string Name { get; set; }
    
    [Key("priority")]
    public int Priority { get; set; }
    
    [Key("description")]
    public string? Description { get; set; }
}

// For enums, you need custom handling or use a union type
[Union(0, typeof(LicenseNone))]
[Union(1, typeof(LicenseSpdx))]
[Union(2, typeof(LicenseCustom))]
public interface ModpkgLicense { }

[MessagePackObject]
public class LicenseNone : ModpkgLicense
{
    // Matches "None" string encoding
}

[MessagePackObject]
public class LicenseSpdx : ModpkgLicense
{
    [Key("spdx_id")]
    public string SpdxId { get; set; }
}

[MessagePackObject]
public class LicenseCustom : ModpkgLicense
{
    [Key("name")]
    public string Name { get; set; }
    
    [Key("url")]
    public string Url { get; set; }
}

// Usage:
using (var stream = File.OpenRead("metadata.msgpack"))
{
    var options = MessagePackSerializerOptions.Standard.WithResolver(
        MessagePack.Resolvers.ContractlessStandardResolver.Instance);
    var metadata = MessagePackSerializer.Deserialize<ModpkgMetadata>(stream, options);
    Console.WriteLine($"Mod Name: {metadata.Name}");
}
```

## Python Implementation Example

```python
import msgpack
from typing import Optional, List
from dataclasses import dataclass

@dataclass
class ModpkgAuthor:
    name: str
    role: Optional[str]
    
    @staticmethod
    def from_msgpack(data):
        return ModpkgAuthor(name=data["name"], role=data.get("role"))

@dataclass
class ModpkgLayerMetadata:
    name: str
    priority: int
    description: Optional[str]
    
    @staticmethod
    def from_msgpack(data):
        return ModpkgLayerMetadata(
            name=data["name"],
            priority=data["priority"],
            description=data.get("description")
        )

@dataclass
class ModpkgLicense:
    pass

@dataclass
class LicenseNone(ModpkgLicense):
    pass

@dataclass
class LicenseSpdx(ModpkgLicense):
    spdx_id: str

@dataclass
class LicenseCustom(ModpkgLicense):
    name: str
    url: str

@dataclass
class DistributorInfo:
    site_id: str
    site_name: str
    site_url: str
    mod_id: str

@dataclass
class ModpkgMetadata:
    schema_version: int
    name: str
    display_name: str
    description: Optional[str]
    version: str # Semver string
    distributor: Optional[DistributorInfo]
    authors: List[ModpkgAuthor]
    license: ModpkgLicense
    layers: List[ModpkgLayerMetadata]
    
    @staticmethod
    def from_msgpack(data):
        return ModpkgMetadata(
            schema_version=data.get("schema_version", 1),
            name=data["name"],
            display_name=data["display_name"],
            description=data.get("description"),
            version=data["version"],
            # ... etc
            # For nested types, you'd instantiate them here using their from_msgpack methods
            distributor=DistributorInfo(
                site_id=data["distributor"]["site_id"],
                site_name=data["distributor"]["site_name"],
                site_url=data["distributor"]["site_url"],
                mod_id=data["distributor"]["mod_id"]
            ) if data.get("distributor") else None,
            authors=[ModpkgAuthor.from_msgpack(a) for a in data.get("authors", [])],
            license=LicenseNone(), # Placeholder for license logic
            layers=[ModpkgLayerMetadata.from_msgpack(l) for l in data.get("layers", [])]
        )

# Usage:
with open('metadata.msgpack', 'rb') as f:
    data = msgpack.unpackb(f.read(), raw=False)
    metadata = ModpkgMetadata.from_msgpack(data)
    print(f"Mod Name: {metadata.name}")
```