mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-05-24 06:34:34 +00:00
feat(api/runtime): java-style assets and localization sync
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using WeaveLoader.API;
|
||||
|
||||
namespace WeaveLoader.API.Block;
|
||||
|
||||
/// <summary>
|
||||
@@ -62,7 +64,7 @@ public class BlockProperties
|
||||
internal float LightEmissionValue = 0.0f;
|
||||
internal int LightBlockValue = 255;
|
||||
internal CreativeTab CreativeTabValue = CreativeTab.None;
|
||||
internal string? NameValue;
|
||||
internal Text? NameValue;
|
||||
internal int RequiredHarvestLevelValue = -1;
|
||||
internal ToolType RequiredToolValue = ToolType.None;
|
||||
internal bool AcceptsRedstonePowerValue;
|
||||
@@ -71,14 +73,19 @@ public class BlockProperties
|
||||
public BlockProperties Hardness(float hardness) { HardnessValue = hardness; return this; }
|
||||
public BlockProperties Resistance(float resistance) { ResistanceValue = resistance; return this; }
|
||||
public BlockProperties Sound(SoundType sound) { SoundValue = sound; return this; }
|
||||
/// <summary>Icon name in the terrain atlas. Use namespaced ID for mod textures (e.g. "examplemod:ruby_ore" from assets/blocks/ruby_ore.png), or vanilla names like "stone", "gold_ore".</summary>
|
||||
/// <summary>
|
||||
/// Icon name in the terrain atlas. Use Java-style names like "examplemod:block/ruby_ore"
|
||||
/// from assets/examplemod/textures/block/ruby_ore.png, or vanilla names like "stone", "gold_ore".
|
||||
/// </summary>
|
||||
public BlockProperties Icon(string iconName) { IconValue = iconName; return this; }
|
||||
public BlockProperties LightLevel(float level) { LightEmissionValue = level; return this; }
|
||||
public BlockProperties LightBlocking(int level) { LightBlockValue = level; return this; }
|
||||
public BlockProperties Indestructible() { HardnessValue = -1.0f; ResistanceValue = 6000000f; return this; }
|
||||
public BlockProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
|
||||
/// <summary>Display name shown in-game (e.g. "Ruby Ore"). Used for localization.</summary>
|
||||
public BlockProperties Name(string displayName) { NameValue = displayName; return this; }
|
||||
public BlockProperties Name(string displayName) { NameValue = Text.Literal(displayName); return this; }
|
||||
/// <summary>Localized display name using a language key (e.g. "block.examplemod.ruby_ore").</summary>
|
||||
public BlockProperties Name(Text text) { NameValue = text; return this; }
|
||||
/// <summary>Minimum harvest level required to properly mine this block (e.g. 3 for obsidian). -1 means no requirement.</summary>
|
||||
public BlockProperties RequiredHarvestLevel(int level) { RequiredHarvestLevelValue = level; return this; }
|
||||
/// <summary>Tool type required to harvest this block (e.g. Pickaxe for stone-like blocks).</summary>
|
||||
|
||||
@@ -59,7 +59,7 @@ public static class BlockRegistry
|
||||
properties.IconValue,
|
||||
properties.LightEmissionValue,
|
||||
properties.LightBlockValue,
|
||||
properties.NameValue ?? "",
|
||||
properties.NameValue?.Resolve() ?? "",
|
||||
properties.RequiredHarvestLevelValue,
|
||||
(int)properties.RequiredToolValue,
|
||||
properties.AcceptsRedstonePowerValue ? 1 : 0);
|
||||
@@ -98,7 +98,7 @@ public static class BlockRegistry
|
||||
properties.IconValue,
|
||||
properties.LightEmissionValue,
|
||||
properties.LightBlockValue,
|
||||
properties.NameValue ?? "",
|
||||
properties.NameValue?.Resolve() ?? "",
|
||||
properties.RequiredHarvestLevelValue,
|
||||
(int)properties.RequiredToolValue,
|
||||
properties.AcceptsRedstonePowerValue ? 1 : 0);
|
||||
@@ -144,7 +144,7 @@ public static class BlockRegistry
|
||||
properties.IconValue,
|
||||
properties.LightEmissionValue,
|
||||
properties.LightBlockValue,
|
||||
properties.NameValue ?? "",
|
||||
properties.NameValue?.Resolve() ?? "",
|
||||
properties.RequiredHarvestLevelValue,
|
||||
(int)properties.RequiredToolValue,
|
||||
properties.AcceptsRedstonePowerValue ? 1 : 0);
|
||||
@@ -191,7 +191,7 @@ public static class BlockRegistry
|
||||
properties.IconValue,
|
||||
properties.LightEmissionValue,
|
||||
properties.LightBlockValue,
|
||||
properties.NameValue ?? "",
|
||||
properties.NameValue?.Resolve() ?? "",
|
||||
properties.RequiredHarvestLevelValue,
|
||||
(int)properties.RequiredToolValue,
|
||||
properties.AcceptsRedstonePowerValue ? 1 : 0,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using WeaveLoader.API;
|
||||
|
||||
namespace WeaveLoader.API.Item;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,10 +12,13 @@ public class ItemProperties
|
||||
internal float AttackDamageValue = 0.0f;
|
||||
internal string IconValue = "";
|
||||
internal CreativeTab CreativeTabValue = CreativeTab.None;
|
||||
internal string? NameValue;
|
||||
internal Text? NameValue;
|
||||
|
||||
public ItemProperties MaxStackSize(int size) { MaxStackSizeValue = size; return this; }
|
||||
/// <summary>Icon name in the items atlas. Use namespaced ID for mod textures (e.g. "examplemod:ruby" from assets/items/ruby.png), or vanilla names like "diamond", "ingotIron".</summary>
|
||||
/// <summary>
|
||||
/// Icon name in the items atlas. Use Java-style names like "examplemod:item/ruby"
|
||||
/// from assets/examplemod/textures/item/ruby.png, or vanilla names like "diamond", "ingotIron".
|
||||
/// </summary>
|
||||
public ItemProperties Icon(string iconName) { IconValue = iconName; return this; }
|
||||
|
||||
/// <summary>
|
||||
@@ -25,5 +30,7 @@ public class ItemProperties
|
||||
public ItemProperties AttackDamage(float damage) { AttackDamageValue = damage; return this; }
|
||||
public ItemProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
|
||||
/// <summary>Display name shown in-game (e.g. "Ruby"). Used for localization.</summary>
|
||||
public ItemProperties Name(string displayName) { NameValue = displayName; return this; }
|
||||
public ItemProperties Name(string displayName) { NameValue = Text.Literal(displayName); return this; }
|
||||
/// <summary>Localized display name using a language key (e.g. "item.examplemod.ruby").</summary>
|
||||
public ItemProperties Name(Text text) { NameValue = text; return this; }
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public static class ItemRegistry
|
||||
(int)nativeTier,
|
||||
maxDamage,
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
properties.NameValue?.Resolve() ?? "");
|
||||
|
||||
if (numericId >= 0)
|
||||
ConfigureToolMaterial(id, numericId, ToolKind.Pickaxe, material, properties);
|
||||
@@ -129,7 +129,7 @@ public static class ItemRegistry
|
||||
(int)(material?.BaseTierValue ?? shovelItem.Tier),
|
||||
properties.MaxDamageValue,
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
properties.NameValue?.Resolve() ?? "");
|
||||
|
||||
if (numericId >= 0)
|
||||
ConfigureToolMaterial(id, numericId, ToolKind.Shovel, material, properties);
|
||||
@@ -142,7 +142,7 @@ public static class ItemRegistry
|
||||
(int)(material?.BaseTierValue ?? hoeItem.Tier),
|
||||
properties.MaxDamageValue,
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
properties.NameValue?.Resolve() ?? "");
|
||||
|
||||
if (numericId >= 0)
|
||||
ConfigureToolMaterial(id, numericId, ToolKind.Hoe, material, properties);
|
||||
@@ -155,7 +155,7 @@ public static class ItemRegistry
|
||||
(int)(material?.BaseTierValue ?? axeItem.Tier),
|
||||
properties.MaxDamageValue,
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
properties.NameValue?.Resolve() ?? "");
|
||||
|
||||
if (numericId >= 0)
|
||||
ConfigureToolMaterial(id, numericId, ToolKind.Axe, material, properties);
|
||||
@@ -168,7 +168,7 @@ public static class ItemRegistry
|
||||
(int)(material?.BaseTierValue ?? swordItem.Tier),
|
||||
properties.MaxDamageValue,
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
properties.NameValue?.Resolve() ?? "");
|
||||
|
||||
if (numericId >= 0)
|
||||
ConfigureToolMaterial(id, numericId, ToolKind.Sword, material, properties);
|
||||
@@ -180,7 +180,7 @@ public static class ItemRegistry
|
||||
properties.MaxStackSizeValue,
|
||||
properties.MaxDamageValue,
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
properties.NameValue?.Resolve() ?? "");
|
||||
}
|
||||
|
||||
if (numericId < 0)
|
||||
|
||||
@@ -145,6 +145,15 @@ internal static class NativeInterop
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
internal static extern void native_register_string(int descriptionId, string displayName);
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
|
||||
internal static extern int native_get_minecraft_language();
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
|
||||
internal static extern int native_get_minecraft_locale();
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
|
||||
internal static extern nint native_get_mods_path();
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
internal static extern int native_register_entity(
|
||||
string namespacedId,
|
||||
|
||||
286
WeaveLoader.API/Text.cs
Normal file
286
WeaveLoader.API/Text.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WeaveLoader.API;
|
||||
|
||||
/// <summary>
|
||||
/// Localized text value used for names, tooltips, and UI strings.
|
||||
/// </summary>
|
||||
public sealed class Text
|
||||
{
|
||||
public enum TextKind
|
||||
{
|
||||
Literal = 0,
|
||||
Translatable = 1
|
||||
}
|
||||
|
||||
public TextKind Kind { get; }
|
||||
public string Value { get; }
|
||||
|
||||
private Text(TextKind kind, string value)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||
Kind = kind;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static Text Literal(string value) => new(TextKind.Literal, value);
|
||||
public static Text Translatable(string key) => new(TextKind.Translatable, key);
|
||||
|
||||
internal string Resolve()
|
||||
{
|
||||
return Kind == TextKind.Literal ? Value : Localization.Resolve(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple language table loader for mod translations (Java-style .lang files).
|
||||
/// </summary>
|
||||
public static class Localization
|
||||
{
|
||||
private static readonly object s_lock = new();
|
||||
private static string s_locale = GetDefaultLocale();
|
||||
private static string? s_loadedLocale;
|
||||
private static Dictionary<string, string> s_entries = new(StringComparer.Ordinal);
|
||||
private static int s_lastGameLanguage = int.MinValue;
|
||||
|
||||
/// <summary>When true, follow the game's current language selection.</summary>
|
||||
public static bool UseGameLanguage { get; set; } = true;
|
||||
|
||||
/// <summary>Active locale used when resolving translatable keys (e.g. "en-GB").</summary>
|
||||
public static string Locale
|
||||
{
|
||||
get => s_locale;
|
||||
set
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||
lock (s_lock)
|
||||
{
|
||||
s_locale = value;
|
||||
s_loadedLocale = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reload language files for the current locale.</summary>
|
||||
public static void Reload()
|
||||
{
|
||||
lock (s_lock)
|
||||
{
|
||||
s_loadedLocale = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string Resolve(string key)
|
||||
{
|
||||
EnsureLoaded();
|
||||
return s_entries.TryGetValue(key, out var value) ? value : key;
|
||||
}
|
||||
|
||||
private static void EnsureLoaded()
|
||||
{
|
||||
lock (s_lock)
|
||||
{
|
||||
if (UseGameLanguage)
|
||||
{
|
||||
string? gameLocale = TryGetGameLocale();
|
||||
if (!string.IsNullOrWhiteSpace(gameLocale) && !string.Equals(gameLocale, s_locale, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
s_locale = gameLocale;
|
||||
s_loadedLocale = null;
|
||||
}
|
||||
}
|
||||
if (s_loadedLocale == s_locale)
|
||||
return;
|
||||
s_entries = LoadLocaleTable(s_locale);
|
||||
s_loadedLocale = s_locale;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDefaultLocale()
|
||||
{
|
||||
string? gameLocale = TryGetGameLocale();
|
||||
if (!string.IsNullOrWhiteSpace(gameLocale))
|
||||
return gameLocale!;
|
||||
var env = Environment.GetEnvironmentVariable("WEAVELOADER_LOCALE");
|
||||
if (!string.IsNullOrWhiteSpace(env))
|
||||
return env;
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
if (!string.IsNullOrWhiteSpace(culture.Name))
|
||||
return culture.Name.Replace('_', '-');
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
return "en-GB";
|
||||
}
|
||||
|
||||
private static string? TryGetGameLocale()
|
||||
{
|
||||
try
|
||||
{
|
||||
int lang = NativeInterop.native_get_minecraft_language();
|
||||
if (lang == s_lastGameLanguage)
|
||||
return null;
|
||||
s_lastGameLanguage = lang;
|
||||
return MapLanguageToLocale(lang);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? MapLanguageToLocale(int lang)
|
||||
{
|
||||
// 0 = default (system); return null so we fall back to system culture / env.
|
||||
return lang switch
|
||||
{
|
||||
1 => "en-GB",
|
||||
2 => "ja-JP",
|
||||
3 => "de-DE",
|
||||
4 => "fr-FR",
|
||||
5 => "es-ES",
|
||||
6 => "it-IT",
|
||||
7 => "ko-KR",
|
||||
8 => "zh-TW",
|
||||
9 => "pt-PT",
|
||||
10 => "pt-BR",
|
||||
11 => "ru-RU",
|
||||
12 => "nl-NL",
|
||||
13 => "fi-FI",
|
||||
14 => "sv-SE",
|
||||
15 => "da-DK",
|
||||
16 => "no-NO",
|
||||
17 => "pl-PL",
|
||||
18 => "tr-TR",
|
||||
19 => "es-MX",
|
||||
20 => "el-GR",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> LoadLocaleTable(string locale)
|
||||
{
|
||||
var entries = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var locales = BuildLocaleFallbacks(locale);
|
||||
foreach (var loc in locales)
|
||||
LoadLocaleFiles(entries, loc);
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<string> BuildLocaleFallbacks(string locale)
|
||||
{
|
||||
var list = new List<string>();
|
||||
void Add(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return;
|
||||
if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
|
||||
list.Add(value);
|
||||
}
|
||||
|
||||
Add(locale);
|
||||
Add(locale.Replace('_', '-'));
|
||||
Add(locale.Replace('-', '_'));
|
||||
Add("en-GB");
|
||||
Add("en-US");
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void LoadLocaleFiles(Dictionary<string, string> entries, string locale)
|
||||
{
|
||||
foreach (var modsPath in GetModsRoots())
|
||||
{
|
||||
foreach (var modDir in Directory.EnumerateDirectories(modsPath))
|
||||
{
|
||||
var assetsDir = Path.Combine(modDir, "assets");
|
||||
if (!Directory.Exists(assetsDir))
|
||||
continue;
|
||||
|
||||
foreach (var nsDir in Directory.EnumerateDirectories(assetsDir))
|
||||
{
|
||||
var langFile = Path.Combine(nsDir, "lang", $"{locale}.lang");
|
||||
if (!File.Exists(langFile))
|
||||
continue;
|
||||
ParseLangFile(langFile, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseLangFile(string path, Dictionary<string, string> entries)
|
||||
{
|
||||
foreach (var rawLine in File.ReadLines(path))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0 || line.StartsWith('#'))
|
||||
continue;
|
||||
|
||||
int idx = line.IndexOf('=');
|
||||
if (idx <= 0 || idx >= line.Length - 1)
|
||||
continue;
|
||||
|
||||
string key = line[..idx].Trim();
|
||||
string value = line[(idx + 1)..].Trim();
|
||||
if (key.Length == 0)
|
||||
continue;
|
||||
|
||||
entries[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> GetModsRoots()
|
||||
{
|
||||
var roots = new List<string>();
|
||||
void AddCandidate(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
string full;
|
||||
try
|
||||
{
|
||||
full = Path.GetFullPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Directory.Exists(full))
|
||||
return;
|
||||
if (!roots.Contains(full, StringComparer.OrdinalIgnoreCase))
|
||||
roots.Add(full);
|
||||
}
|
||||
|
||||
AddCandidate(GetNativeModsPath());
|
||||
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
AddCandidate(Path.Combine(baseDir, "mods"));
|
||||
AddCandidate(Path.Combine(baseDir, "..", "mods"));
|
||||
AddCandidate(Path.Combine(baseDir, "..", "..", "mods"));
|
||||
|
||||
var cwd = Directory.GetCurrentDirectory();
|
||||
AddCandidate(Path.Combine(cwd, "mods"));
|
||||
AddCandidate(Path.Combine(cwd, "..", "mods"));
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
private static string? GetNativeModsPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
var ptr = NativeInterop.native_get_mods_path();
|
||||
if (ptr == nint.Zero)
|
||||
return null;
|
||||
return Marshal.PtrToStringAnsi(ptr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,10 @@
|
||||
|
||||
<!-- Mod textures: use Target so they only copy when API builds to mods/, not when copied as dependency -->
|
||||
<Target Name="CopyModAssets" AfterTargets="Build">
|
||||
<MakeDir Directories="$(OutputPath)assets\blocks" />
|
||||
<MakeDir Directories="$(OutputPath)assets\items" />
|
||||
<Copy SourceFiles="mod_assets\blocks\missing_block.png" DestinationFolder="$(OutputPath)assets\blocks\" SkipUnchangedFiles="true" />
|
||||
<Copy SourceFiles="mod_assets\items\missing_item.png" DestinationFolder="$(OutputPath)assets\items\" SkipUnchangedFiles="true" />
|
||||
<MakeDir Directories="$(OutputPath)assets\weaveloader.api\textures\block" />
|
||||
<MakeDir Directories="$(OutputPath)assets\weaveloader.api\textures\item" />
|
||||
<Copy SourceFiles="mod_assets\weaveloader.api\textures\block\missing_block.png" DestinationFolder="$(OutputPath)assets\weaveloader.api\textures\block\" SkipUnchangedFiles="true" />
|
||||
<Copy SourceFiles="mod_assets\weaveloader.api\textures\item\missing_item.png" DestinationFolder="$(OutputPath)assets\weaveloader.api\textures\item\" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Placeholder textures for the missing block and missing item (unresolved mod content).
|
||||
Stored in `mod_assets/` to avoid conflict with the `Assets/` C# source folder (Windows case-insensitivity).
|
||||
Copied to `assets/blocks/` and `assets/items/` in the build output.
|
||||
Copied to `assets/weaveloader.api/textures/block/` and `assets/weaveloader.api/textures/item/` in the build output.
|
||||
|
||||
- **Blocks:** `mod_assets/blocks/missing_block.png` → icon `weaveloader.api:missing_block`
|
||||
- **Items:** `mod_assets/items/missing_item.png` → icon `weaveloader.api:missing_item`
|
||||
- **Blocks:** `mod_assets/weaveloader.api/textures/block/missing_block.png` → icon `weaveloader.api:block/missing_block`
|
||||
- **Items:** `mod_assets/weaveloader.api/textures/item/missing_item.png` → icon `weaveloader.api:item/missing_item`
|
||||
|
||||
These are used when a world contains blocks or items from mods that are no longer installed.
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Reference in New Issue
Block a user