Rebrand LegacyForge to Weave Loader

Rename across entire codebase:
- LegacyForge -> WeaveLoader (identifiers, namespaces, classes, DLLs)
- LegacyForgeRuntime -> WeaveLoaderRuntime (C++ project)
- LegacyForge.API/Core/Launcher -> WeaveLoader.API/Core/Launcher (C# projects)
- [LegacyForge] -> [WeaveLoader] (log prefixes)
- legacyforge -> weaveloader (config files, log files, backup suffixes)
- Display name "Weave Loader" in README, CONTRIBUTING, LICENSE
This commit is contained in:
Jacobwasbeast
2026-03-06 23:31:18 -06:00
parent f5805fc740
commit fa195fdc2e
70 changed files with 336 additions and 336 deletions

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WeaveLoader.Core")]

View File

@@ -0,0 +1,27 @@
namespace WeaveLoader.API.Assets;
/// <summary>
/// Asset registration for mods. Use for language strings and (future) texture paths.
/// Block and item display names are typically set via BlockProperties.Name() and ItemProperties.Name().
/// Use this registry for additional strings (e.g. GUI labels, messages).
/// </summary>
public static class AssetRegistry
{
/// <summary>
/// Register a display string for a custom description ID.
/// Use native_allocate_description_id() to get an ID, then wire it to your custom UI/entity.
/// </summary>
public static void RegisterString(int descriptionId, string displayName)
{
NativeInterop.native_register_string(descriptionId, displayName ?? "");
}
/// <summary>
/// Allocate a new description ID for custom strings.
/// IDs are in the mod range (10000+) and are looked up via the GetString hook.
/// </summary>
public static int AllocateDescriptionId()
{
return NativeInterop.native_allocate_description_id();
}
}

View File

@@ -0,0 +1,64 @@
namespace WeaveLoader.API.Block;
public enum MaterialType
{
Air = 0,
Stone = 1,
Wood = 2,
Cloth = 3,
Plant = 4,
Dirt = 5,
Sand = 6,
Glass = 7,
Water = 8,
Lava = 9,
Ice = 10,
Metal = 11,
Snow = 12,
Clay = 13,
Explosive = 14,
Web = 15
}
public enum SoundType
{
None = 0,
Stone = 1,
Wood = 2,
Gravel = 3,
Grass = 4,
Metal = 5,
Glass = 6,
Cloth = 7,
Sand = 8,
Snow = 9
}
/// <summary>
/// Fluent builder for defining block properties.
/// </summary>
public class BlockProperties
{
internal MaterialType MaterialValue = MaterialType.Stone;
internal float HardnessValue = 1.0f;
internal float ResistanceValue = 5.0f;
internal SoundType SoundValue = SoundType.Stone;
internal string IconValue = "stone";
internal float LightEmissionValue = 0.0f;
internal int LightBlockValue = 255;
internal CreativeTab CreativeTabValue = CreativeTab.None;
internal string? NameValue;
public BlockProperties Material(MaterialType material) { MaterialValue = material; return this; }
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>
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; }
}

View File

@@ -0,0 +1,58 @@
namespace WeaveLoader.API.Block;
/// <summary>
/// Represents a block that has been registered with the game engine.
/// </summary>
public class RegisteredBlock
{
/// <summary>The namespaced string ID (e.g. "mymod:ruby_ore").</summary>
public Identifier StringId { get; }
/// <summary>The numeric ID allocated by the engine.</summary>
public int NumericId { get; }
internal RegisteredBlock(Identifier id, int numericId)
{
StringId = id;
NumericId = numericId;
}
}
/// <summary>
/// Block registration via the WeaveLoader registry.
/// Accessed through <see cref="Registry.Block"/>.
/// </summary>
public static class BlockRegistry
{
/// <summary>
/// Register a new block with the game engine.
/// </summary>
/// <param name="id">Namespaced identifier (e.g. "mymod:ruby_ore").</param>
/// <param name="properties">Block properties built with <see cref="BlockProperties"/>.</param>
/// <returns>A handle to the registered block.</returns>
public static RegisteredBlock Register(Identifier id, BlockProperties properties)
{
int numericId = NativeInterop.native_register_block(
id.ToString(),
(int)properties.MaterialValue,
properties.HardnessValue,
properties.ResistanceValue,
(int)properties.SoundValue,
properties.IconValue,
properties.LightEmissionValue,
properties.LightBlockValue,
properties.NameValue ?? "");
if (numericId < 0)
throw new InvalidOperationException($"Failed to register block '{id}'. No free IDs or invalid parameters.");
if (properties.CreativeTabValue != CreativeTab.None)
{
NativeInterop.native_add_to_creative(numericId, 1, 0, (int)properties.CreativeTabValue);
Logger.Debug($"Block '{id}' added to creative tab {properties.CreativeTabValue}");
}
Logger.Debug($"Registered block '{id}' -> numeric ID {numericId}");
return new RegisteredBlock(id, numericId);
}
}

View File

@@ -0,0 +1,21 @@
namespace WeaveLoader.API;
/// <summary>
/// Creative inventory tabs matching the game's internal group indices.
/// Use with <see cref="Block.BlockProperties.CreativeTab"/> or
/// <see cref="Item.ItemProperties.CreativeTab"/> to make content
/// appear in the creative menu.
/// </summary>
public enum CreativeTab
{
None = -1,
BuildingBlocks = 0,
Decoration = 1,
Redstone = 2,
Transport = 3,
Materials = 4,
Food = 5,
ToolsAndWeapons = 6,
Brewing = 7,
Misc = 12
}

View File

@@ -0,0 +1,16 @@
namespace WeaveLoader.API.Entity;
/// <summary>
/// Fluent builder for defining entity properties.
/// </summary>
public class EntityDefinition
{
internal float WidthValue = 0.6f;
internal float HeightValue = 1.8f;
internal int TrackingRangeValue = 80;
public EntityDefinition Width(float width) { WidthValue = width; return this; }
public EntityDefinition Height(float height) { HeightValue = height; return this; }
public EntityDefinition TrackingRange(int range) { TrackingRangeValue = range; return this; }
public EntityDefinition Size(float width, float height) { WidthValue = width; HeightValue = height; return this; }
}

View File

@@ -0,0 +1,38 @@
namespace WeaveLoader.API.Entity;
/// <summary>
/// Represents an entity type that has been registered with the game engine.
/// </summary>
public class RegisteredEntity
{
public Identifier StringId { get; }
public int NumericId { get; }
internal RegisteredEntity(Identifier id, int numericId)
{
StringId = id;
NumericId = numericId;
}
}
/// <summary>
/// Entity registration via the WeaveLoader registry.
/// Accessed through <see cref="Registry.Entity"/>.
/// </summary>
public static class EntityRegistry
{
public static RegisteredEntity Register(Identifier id, EntityDefinition definition)
{
int numericId = NativeInterop.native_register_entity(
id.ToString(),
definition.WidthValue,
definition.HeightValue,
definition.TrackingRangeValue);
if (numericId < 0)
throw new InvalidOperationException($"Failed to register entity '{id}'.");
Logger.Debug($"Registered entity '{id}' -> numeric ID {numericId}");
return new RegisteredEntity(id, numericId);
}
}

View File

@@ -0,0 +1,60 @@
namespace WeaveLoader.API.Events;
public class TickEventArgs : EventArgs { }
public class BlockBreakEventArgs : EventArgs
{
public string BlockId { get; init; } = "";
public int X { get; init; }
public int Y { get; init; }
public int Z { get; init; }
public int PlayerId { get; init; }
}
public class BlockPlaceEventArgs : EventArgs
{
public string BlockId { get; init; } = "";
public int X { get; init; }
public int Y { get; init; }
public int Z { get; init; }
public int PlayerId { get; init; }
}
public class ChatEventArgs : EventArgs
{
public string Message { get; init; } = "";
public int PlayerId { get; init; }
}
public class EntitySpawnEventArgs : EventArgs
{
public string EntityId { get; init; } = "";
public float X { get; init; }
public float Y { get; init; }
public float Z { get; init; }
}
public class PlayerJoinEventArgs : EventArgs
{
public int PlayerId { get; init; }
public string PlayerName { get; init; } = "";
}
/// <summary>
/// Global game event subscriptions. Subscribe to these in your mod's OnInitialize().
/// Events are fired from the game's main thread via hooks in WeaveLoaderRuntime.
/// </summary>
public static class GameEvents
{
public static event EventHandler<BlockBreakEventArgs>? OnBlockBreak;
public static event EventHandler<BlockPlaceEventArgs>? OnBlockPlace;
public static event EventHandler<ChatEventArgs>? OnChat;
public static event EventHandler<EntitySpawnEventArgs>? OnEntitySpawn;
public static event EventHandler<PlayerJoinEventArgs>? OnPlayerJoin;
internal static void FireBlockBreak(BlockBreakEventArgs e) => OnBlockBreak?.Invoke(null, e);
internal static void FireBlockPlace(BlockPlaceEventArgs e) => OnBlockPlace?.Invoke(null, e);
internal static void FireChat(ChatEventArgs e) => OnChat?.Invoke(null, e);
internal static void FireEntitySpawn(EntitySpawnEventArgs e) => OnEntitySpawn?.Invoke(null, e);
internal static void FirePlayerJoin(PlayerJoinEventArgs e) => OnPlayerJoin?.Invoke(null, e);
}

39
WeaveLoader.API/IMod.cs Normal file
View File

@@ -0,0 +1,39 @@
namespace WeaveLoader.API;
/// <summary>
/// The main interface all WeaveLoader mods must implement.
/// Default interface methods allow mods to only override what they need.
/// </summary>
public interface IMod
{
/// <summary>
/// Called before vanilla registries are populated.
/// Use for very early setup that must happen before any game content loads.
/// </summary>
void OnPreInit() { }
/// <summary>
/// Called after vanilla registries are populated.
/// This is the main initialization point -- register your blocks, items,
/// entities, recipes, and event handlers here.
/// </summary>
void OnInitialize();
/// <summary>
/// Called after the game client has finished its own initialization.
/// Use for client-side setup like custom renderers or UI.
/// </summary>
void OnPostInitialize() { }
/// <summary>
/// Called once per game tick (20 times per second).
/// Use for ongoing mod logic.
/// </summary>
void OnTick() { }
/// <summary>
/// Called when the game is shutting down.
/// Use for cleanup and saving mod state.
/// </summary>
void OnShutdown() { }
}

View File

@@ -0,0 +1,39 @@
namespace WeaveLoader.API;
/// <summary>
/// A namespaced identifier in the form "namespace:path" (e.g. "minecraft:stone", "mymod:ruby_ore").
/// </summary>
public readonly record struct Identifier
{
public string Namespace { get; }
public string Path { get; }
public Identifier(string ns, string path)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ns);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
Namespace = ns;
Path = path;
}
public Identifier(string id)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
var colonIndex = id.IndexOf(':');
if (colonIndex < 0)
{
Namespace = "minecraft";
Path = id;
}
else
{
Namespace = id[..colonIndex];
Path = id[(colonIndex + 1)..];
}
}
public override string ToString() => $"{Namespace}:{Path}";
public static implicit operator Identifier(string id) => new(id);
}

View File

@@ -0,0 +1,26 @@
namespace WeaveLoader.API.Item;
/// <summary>
/// Fluent builder for defining item properties.
/// </summary>
public class ItemProperties
{
internal int MaxStackSizeValue = 64;
internal int MaxDamageValue = 0;
internal string IconValue = "";
internal CreativeTab CreativeTabValue = CreativeTab.None;
internal string? 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>
public ItemProperties Icon(string iconName) { IconValue = iconName; return this; }
/// <summary>
/// Set max damage for a tool/armor item. Setting this to a positive value
/// makes the item damageable with a durability bar.
/// </summary>
public ItemProperties MaxDamage(int damage) { MaxDamageValue = damage; MaxStackSizeValue = 1; 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; }
}

View File

@@ -0,0 +1,54 @@
namespace WeaveLoader.API.Item;
/// <summary>
/// Represents an item that has been registered with the game engine.
/// </summary>
public class RegisteredItem
{
/// <summary>The namespaced string ID (e.g. "mymod:ruby").</summary>
public Identifier StringId { get; }
/// <summary>The numeric ID allocated by the engine.</summary>
public int NumericId { get; }
internal RegisteredItem(Identifier id, int numericId)
{
StringId = id;
NumericId = numericId;
}
}
/// <summary>
/// Item registration via the WeaveLoader registry.
/// Accessed through <see cref="Registry.Item"/>.
/// </summary>
public static class ItemRegistry
{
/// <summary>
/// Register a new item with the game engine.
/// </summary>
/// <param name="id">Namespaced identifier (e.g. "mymod:ruby").</param>
/// <param name="properties">Item properties built with <see cref="ItemProperties"/>.</param>
/// <returns>A handle to the registered item.</returns>
public static RegisteredItem Register(Identifier id, ItemProperties properties)
{
int numericId = NativeInterop.native_register_item(
id.ToString(),
properties.MaxStackSizeValue,
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
if (numericId < 0)
throw new InvalidOperationException($"Failed to register item '{id}'. No free IDs or invalid parameters.");
if (properties.CreativeTabValue != CreativeTab.None)
{
NativeInterop.native_add_to_creative(numericId, 1, 0, (int)properties.CreativeTabValue);
Logger.Debug($"Item '{id}' added to creative tab {properties.CreativeTabValue}");
}
Logger.Debug($"Registered item '{id}' -> numeric ID {numericId}");
return new RegisteredItem(id, numericId);
}
}

36
WeaveLoader.API/Logger.cs Normal file
View File

@@ -0,0 +1,36 @@
namespace WeaveLoader.API;
public enum LogLevel
{
Debug = 0,
Info = 1,
Warning = 2,
Error = 3
}
/// <summary>
/// Logging facade that routes messages through the native runtime to the game's debug output.
/// </summary>
public static class Logger
{
private static Action<string, LogLevel>? LogHandler;
/// <summary>
/// Set the log handler that routes messages to the native runtime.
/// Called by WeaveLoader.Core during initialization.
/// </summary>
public static void SetLogHandler(Action<string, LogLevel> handler) => LogHandler = handler;
public static void Debug(string message) => Log(message, LogLevel.Debug);
public static void Info(string message) => Log(message, LogLevel.Info);
public static void Warning(string message) => Log(message, LogLevel.Warning);
public static void Error(string message) => Log(message, LogLevel.Error);
public static void Log(string message, LogLevel level = LogLevel.Info)
{
if (LogHandler != null)
LogHandler(message, level);
else
Console.WriteLine($"[WeaveLoader/{level}] {message}");
}
}

View File

@@ -0,0 +1,41 @@
namespace WeaveLoader.API;
/// <summary>
/// Marks a class as a WeaveLoader mod and provides metadata.
/// The class must also implement <see cref="IMod"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ModAttribute : Attribute
{
/// <summary>
/// The unique mod identifier (e.g. "examplemod"). Used as the default namespace
/// for content registered by this mod.
/// </summary>
public string Id { get; }
/// <summary>
/// Human-readable display name.
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Semantic version string (e.g. "1.0.0").
/// </summary>
public string Version { get; set; } = "1.0.0";
/// <summary>
/// Mod author(s).
/// </summary>
public string Author { get; set; } = "";
/// <summary>
/// Short description of the mod.
/// </summary>
public string Description { get; set; } = "";
public ModAttribute(string id)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
Id = id;
}
}

View File

@@ -0,0 +1,76 @@
using System.Runtime.InteropServices;
namespace WeaveLoader.API;
/// <summary>
/// Internal P/Invoke bindings to WeaveLoaderRuntime.dll native exports.
/// Mod authors should use the Registry and Logger classes instead of calling these directly.
/// </summary>
internal static class NativeInterop
{
private const string RuntimeDll = "WeaveLoaderRuntime";
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_block(
string namespacedId,
int materialId,
float hardness,
float resistance,
int soundType,
string iconName,
float lightEmission,
int lightBlock,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_item(
string namespacedId,
int maxStackSize,
int maxDamage,
string iconName,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_allocate_description_id();
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_register_string(int descriptionId, string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_entity(
string namespacedId,
float width,
float height,
int trackingRange);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_add_shaped_recipe(
string resultId,
int resultCount,
string pattern,
string ingredientIds);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_add_furnace_recipe(
string inputId,
string outputId,
float xp);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_log(string message, int level);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_get_block_id(string namespacedId);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_get_item_id(string namespacedId);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_get_entity_id(string namespacedId);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_subscribe_event(string eventName, IntPtr managedFnPtr);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern void native_add_to_creative(int numericId, int count, int auxValue, int groupIndex);
}

View File

@@ -0,0 +1,40 @@
namespace WeaveLoader.API.Recipe;
/// <summary>
/// Recipe registration via the WeaveLoader registry.
/// Accessed through <see cref="Registry.Recipe"/>.
/// </summary>
public static class RecipeRegistry
{
/// <summary>
/// Add a shaped crafting recipe.
/// </summary>
/// <param name="result">The output item identifier.</param>
/// <param name="resultCount">Number of items produced.</param>
/// <param name="pattern">Crafting grid pattern rows (e.g. "XXX", " | ", " | " for a pickaxe).</param>
/// <param name="keys">Character-to-ingredient mappings.</param>
public static void AddShaped(Identifier result, int resultCount, string[] pattern,
params (char key, Identifier ingredient)[] keys)
{
string patternStr = string.Join(";", pattern);
string ingredientStr = string.Join(";",
keys.Select(k => $"{k.key}={k.ingredient}"));
NativeInterop.native_add_shaped_recipe(
result.ToString(), resultCount, patternStr, ingredientStr);
Logger.Debug($"Added shaped recipe -> {resultCount}x {result}");
}
/// <summary>
/// Add a furnace/smelting recipe.
/// </summary>
/// <param name="input">The input item/block identifier.</param>
/// <param name="output">The output item identifier.</param>
/// <param name="xp">Experience granted per smelt.</param>
public static void AddFurnace(Identifier input, Identifier output, float xp)
{
NativeInterop.native_add_furnace_recipe(input.ToString(), output.ToString(), xp);
Logger.Debug($"Added furnace recipe: {input} -> {output} ({xp} xp)");
}
}

View File

@@ -0,0 +1,55 @@
using WeaveLoader.API.Block;
using WeaveLoader.API.Item;
using WeaveLoader.API.Entity;
using WeaveLoader.API.Recipe;
using WeaveLoader.API.Assets;
namespace WeaveLoader.API;
/// <summary>
/// Central access point for all WeaveLoader registries.
/// Use Registry.Block, Registry.Item, Registry.Entity, Registry.Recipe, or Registry.Assets.
/// </summary>
public static class Registry
{
/// <summary>Block registration. Call Register() with a namespaced ID and BlockProperties.</summary>
public static class Block
{
public static RegisteredBlock Register(Identifier id, BlockProperties properties)
=> BlockRegistry.Register(id, properties);
}
/// <summary>Item registration. Call Register() with a namespaced ID and ItemProperties.</summary>
public static class Item
{
public static RegisteredItem Register(Identifier id, ItemProperties properties)
=> ItemRegistry.Register(id, properties);
}
/// <summary>Entity registration. Call Register() with a namespaced ID and EntityDefinition.</summary>
public static class Entity
{
public static RegisteredEntity Register(Identifier id, EntityDefinition definition)
=> EntityRegistry.Register(id, definition);
}
/// <summary>Recipe registration for crafting and smelting.</summary>
public static class Recipe
{
public static void AddShaped(Identifier result, int count, string[] pattern,
params (char key, Identifier ingredient)[] keys)
=> RecipeRegistry.AddShaped(result, count, pattern, keys);
public static void AddFurnace(Identifier input, Identifier output, float xp)
=> RecipeRegistry.AddFurnace(input, output, xp);
}
/// <summary>Asset registration for language strings and (future) textures.</summary>
public static class Assets
{
public static void RegisterString(int descriptionId, string displayName)
=> AssetRegistry.RegisterString(descriptionId, displayName);
public static int AllocateDescriptionId()
=> AssetRegistry.AllocateDescriptionId();
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WeaveLoader.API</RootNamespace>
<AssemblyName>WeaveLoader.API</AssemblyName>
<Description>Mod API for WeaveLoader - Minecraft Legacy Edition mod loader</Description>
<Version>1.0.0</Version>
<OutputPath>..\build\mods\WeaveLoader.API</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
</Project>