389 lines
12 KiB
C#
389 lines
12 KiB
C#
using gregExtractor.Models;
|
|
|
|
namespace gregExtractor.Services;
|
|
|
|
public sealed class TemplateService
|
|
{
|
|
private const string Header = "// UNITY 6: All Unity API calls must run on the Main Thread!\n// Use MelonCoroutines for asynchronous operations.\n// Generated by gregExtractor v1.0 — https://dcmods.com\n";
|
|
|
|
public async Task<TemplateGenerationResult> GenerateModAsync(
|
|
string modName,
|
|
string? category,
|
|
TemplateType type,
|
|
string outputRoot,
|
|
string hooksJsonPath,
|
|
IProgress<string>? progress,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(modName))
|
|
throw new ArgumentException("Mod name is required.", nameof(modName));
|
|
|
|
if (string.IsNullOrWhiteSpace(outputRoot))
|
|
throw new ArgumentException("Output path is required.", nameof(outputRoot));
|
|
|
|
return await Task.Run(() =>
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
string sanitizedName = SanitizeName(modName);
|
|
string modDir = Path.Combine(outputRoot, sanitizedName);
|
|
Directory.CreateDirectory(modDir);
|
|
|
|
var generatedFiles = new List<string>();
|
|
|
|
progress?.Report("Writing base project files...");
|
|
generatedFiles.Add(WriteFile(modDir, $"{sanitizedName}.csproj", BuildCsproj(sanitizedName)));
|
|
generatedFiles.Add(WriteFile(modDir, "MainPlugin.cs", BuildMainPlugin(sanitizedName, category)));
|
|
generatedFiles.Add(WriteFile(modDir, "mod.json", BuildModJson(sanitizedName, category, type)));
|
|
|
|
switch (type)
|
|
{
|
|
case TemplateType.HarmonyPatch:
|
|
GenerateHarmonyPatches(modDir, sanitizedName, category, hooksJsonPath, generatedFiles, progress, cancellationToken);
|
|
break;
|
|
|
|
case TemplateType.CustomServer:
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("Definitions", "ServerDefinition.json"), BuildSimpleDefinition("server", sanitizedName, category)));
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("Logic", $"{sanitizedName}ServerBehaviour.cs"), BuildCustomServerBehaviour(sanitizedName)));
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("Logic", $"{sanitizedName}ServerRegistration.cs"), BuildCustomServerRegistration(sanitizedName)));
|
|
break;
|
|
|
|
case TemplateType.CustomUI:
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("UI", $"{sanitizedName}UIManifest.json"), BuildSimpleDefinition("ui", sanitizedName, category)));
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("UI", $"{sanitizedName}Panel.cs"), BuildCustomUiPanel(sanitizedName)));
|
|
break;
|
|
|
|
case TemplateType.CustomWorld:
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("World", $"{sanitizedName}WorldDefinition.json"), BuildSimpleDefinition("world", sanitizedName, category)));
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("World", $"{sanitizedName}WorldLoader.cs"), BuildCustomWorldLoader(sanitizedName)));
|
|
break;
|
|
|
|
case TemplateType.CustomFurniture:
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("Furniture", $"{sanitizedName}FurnitureDefinition.json"), BuildSimpleDefinition("furniture", sanitizedName, category)));
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("Furniture", $"{sanitizedName}FurnitureBehaviour.cs"), BuildCustomFurnitureBehaviour(sanitizedName)));
|
|
break;
|
|
|
|
case TemplateType.CustomNPC:
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("NPC", $"{sanitizedName}NPCDefinition.json"), BuildSimpleDefinition("npc", sanitizedName, category)));
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("NPC", $"{sanitizedName}NPCBehaviour.cs"), BuildCustomNpcBehaviour(sanitizedName)));
|
|
break;
|
|
}
|
|
|
|
progress?.Report($"Mod scaffold generated in '{modDir}'.");
|
|
return new TemplateGenerationResult(modDir, generatedFiles);
|
|
}, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static void GenerateHarmonyPatches(
|
|
string modDir,
|
|
string modName,
|
|
string? category,
|
|
string hooksJsonPath,
|
|
List<string> generatedFiles,
|
|
IProgress<string>? progress,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (!File.Exists(hooksJsonPath))
|
|
throw new FileNotFoundException("game_hooks.json was not found. Run 'extract' first.", hooksJsonPath);
|
|
|
|
string json = File.ReadAllText(hooksJsonPath);
|
|
HookDefinition[] hooks = JsonSerializer.Deserialize<HookDefinition[]>(json, new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
}) ?? Array.Empty<HookDefinition>();
|
|
|
|
IEnumerable<HookDefinition> filtered = hooks;
|
|
if (!string.IsNullOrWhiteSpace(category))
|
|
filtered = filtered.Where(hook => string.Equals(hook.Group, category, StringComparison.OrdinalIgnoreCase));
|
|
|
|
foreach (HookDefinition hook in filtered)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
string groupName = string.IsNullOrWhiteSpace(hook.Group) ? "Misc" : hook.Group;
|
|
string patchDir = Path.Combine(modDir, "Patches", groupName);
|
|
string patchFile = $"{hook.ClassName}_{hook.MethodName}Patch.cs";
|
|
string patchContent = BuildHarmonyPatch(modName, hook, groupName);
|
|
generatedFiles.Add(WriteFile(modDir, Path.Combine("Patches", groupName, patchFile), patchContent));
|
|
}
|
|
|
|
progress?.Report($"Harmony patches generated: {generatedFiles.Count(file => file.Contains("Patches", StringComparison.OrdinalIgnoreCase))}");
|
|
}
|
|
|
|
private static string BuildCsproj(string modName)
|
|
{
|
|
return $$"""
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFramework>net6.0</TargetFramework>
|
|
<LangVersion>latest</LangVersion>
|
|
<Nullable>enable</Nullable>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<Reference Include="MelonLoader">
|
|
<HintPath>$(MODS_GAME_DIR)\MelonLoader\net6\MelonLoader.dll</HintPath>
|
|
<Private>false</Private>
|
|
</Reference>
|
|
<Reference Include="HarmonyLib">
|
|
<HintPath>$(MODS_GAME_DIR)\MelonLoader\net6\0Harmony.dll</HintPath>
|
|
<Private>false</Private>
|
|
</Reference>
|
|
<Reference Include="Assembly-CSharp">
|
|
<HintPath>$(MODS_GAME_DIR)\MelonLoader\Il2CppAssemblies\Assembly-CSharp.dll</HintPath>
|
|
<Private>false</Private>
|
|
</Reference>
|
|
</ItemGroup>
|
|
</Project>
|
|
""";
|
|
}
|
|
|
|
private static string BuildMainPlugin(string modName, string? category)
|
|
{
|
|
string safeCategory = string.IsNullOrWhiteSpace(category) ? "Misc" : category;
|
|
return $$"""
|
|
{{Header}}
|
|
using MelonLoader;
|
|
|
|
[assembly: MelonInfo(typeof({{modName}}.MainPlugin), "{{modName}}", "1.0.0", "YourName")]
|
|
[assembly: MelonGame("Waseku", "Data Center")]
|
|
|
|
namespace {{modName}};
|
|
|
|
public sealed class MainPlugin : MelonMod
|
|
{
|
|
public override void OnInitializeMelon()
|
|
{
|
|
LoggerInstance.Msg("{{modName}} initialized.");
|
|
|
|
// TODO: Enable gregCore registry calls after NuGet release.
|
|
// gregCore.Registry.Register("{{modName}}", "{{safeCategory}}");
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildHarmonyPatch(string modName, HookDefinition hook, string groupName)
|
|
{
|
|
string canonicalGroup = groupName.ToLowerInvariant();
|
|
string canonicalMethod = hook.MethodName;
|
|
string typeName = string.IsNullOrWhiteSpace(hook.Namespace) ? hook.ClassName : $"{hook.Namespace}.{hook.ClassName}";
|
|
|
|
return $$"""
|
|
{{Header}}
|
|
using HarmonyLib;
|
|
|
|
namespace {{modName}}.Patches.{{groupName}};
|
|
|
|
[HarmonyPatch(typeof({{typeName}}), nameof({{typeName}}.{{hook.MethodName}}))]
|
|
public static class {{hook.ClassName}}_{{hook.MethodName}}Patch
|
|
{
|
|
// Example Prefix:
|
|
// private static void Prefix()
|
|
// {
|
|
// }
|
|
|
|
private static void Postfix(object? __instance)
|
|
{
|
|
// TODO: Enable gregCore event bus when NuGet package is available.
|
|
// gregGameApi.Publish("greg.{{canonicalGroup}}.{{canonicalMethod}}", new { instance = __instance });
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildCustomServerBehaviour(string modName)
|
|
{
|
|
return $$"""
|
|
{{Header}}
|
|
namespace {{modName}}.Logic;
|
|
|
|
public sealed class {{modName}}ServerBehaviour
|
|
{
|
|
public void OnPowerOn()
|
|
{
|
|
}
|
|
|
|
public void OnPowerOff()
|
|
{
|
|
}
|
|
|
|
public void OnTick(float deltaTime)
|
|
{
|
|
}
|
|
|
|
public void OnBreak()
|
|
{
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildCustomServerRegistration(string modName)
|
|
{
|
|
return $$"""
|
|
{{Header}}
|
|
namespace {{modName}}.Logic;
|
|
|
|
public static class {{modName}}ServerRegistration
|
|
{
|
|
public static void Register()
|
|
{
|
|
// TODO: Enable gregCore registry call after NuGet release.
|
|
// gregCore.Registry.RegisterServer("{{modName}}");
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildCustomUiPanel(string modName)
|
|
{
|
|
return $$"""
|
|
{{Header}}
|
|
namespace {{modName}}.UI;
|
|
|
|
public sealed class {{modName}}Panel
|
|
{
|
|
public void Initialize()
|
|
{
|
|
}
|
|
|
|
public void Show()
|
|
{
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
}
|
|
|
|
public void BuildUI()
|
|
{
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildCustomWorldLoader(string modName)
|
|
{
|
|
return $$"""
|
|
{{Header}}
|
|
namespace {{modName}}.World;
|
|
|
|
public sealed class {{modName}}WorldLoader
|
|
{
|
|
public void Register()
|
|
{
|
|
// TODO: Enable gregCore world registration API.
|
|
}
|
|
|
|
public void OnSceneLoaded(int sceneIndex)
|
|
{
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildCustomFurnitureBehaviour(string modName)
|
|
{
|
|
return $$"""
|
|
{{Header}}
|
|
namespace {{modName}}.Furniture;
|
|
|
|
public sealed class {{modName}}FurnitureBehaviour
|
|
{
|
|
public void OnPlaced()
|
|
{
|
|
}
|
|
|
|
public void OnRemoved()
|
|
{
|
|
}
|
|
|
|
public void OnInteract()
|
|
{
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildCustomNpcBehaviour(string modName)
|
|
{
|
|
return $$"""
|
|
{{Header}}
|
|
namespace {{modName}}.NPC;
|
|
|
|
public sealed class {{modName}}NPCBehaviour
|
|
{
|
|
public void OnHired()
|
|
{
|
|
}
|
|
|
|
public void OnFired()
|
|
{
|
|
}
|
|
|
|
public void OnTaskAssigned(string taskId)
|
|
{
|
|
}
|
|
|
|
public void OnTaskCompleted(string taskId)
|
|
{
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
private static string BuildSimpleDefinition(string type, string modName, string? category)
|
|
{
|
|
return JsonSerializer.Serialize(
|
|
new
|
|
{
|
|
type,
|
|
id = modName,
|
|
name = modName,
|
|
category = string.IsNullOrWhiteSpace(category) ? "Misc" : category,
|
|
version = "1.0.0",
|
|
},
|
|
new JsonSerializerOptions { WriteIndented = true });
|
|
}
|
|
|
|
private static string BuildModJson(string modName, string? category, TemplateType type)
|
|
{
|
|
return JsonSerializer.Serialize(
|
|
new
|
|
{
|
|
id = modName,
|
|
name = modName,
|
|
version = "1.0.0",
|
|
author = "YourName",
|
|
description = $"Generated {type} mod scaffold.",
|
|
category = string.IsNullOrWhiteSpace(category) ? "Misc" : category,
|
|
gregCoreVersion = "0.0.0-local",
|
|
dependencies = new[] { "gregCore" },
|
|
},
|
|
new JsonSerializerOptions { WriteIndented = true });
|
|
}
|
|
|
|
private static string WriteFile(string baseDirectory, string relativePath, string content)
|
|
{
|
|
string fullPath = Path.Combine(baseDirectory, relativePath);
|
|
string? directory = Path.GetDirectoryName(fullPath);
|
|
if (!string.IsNullOrWhiteSpace(directory))
|
|
Directory.CreateDirectory(directory);
|
|
|
|
File.WriteAllText(fullPath, content);
|
|
return fullPath;
|
|
}
|
|
|
|
private static string SanitizeName(string value)
|
|
{
|
|
var builder = new StringBuilder(value.Length);
|
|
foreach (char character in value)
|
|
builder.Append(char.IsLetterOrDigit(character) || character is '_' or '.' ? character : '_');
|
|
|
|
return builder.ToString().Trim('_');
|
|
}
|
|
}
|
|
|
|
public sealed record TemplateGenerationResult(string ModDirectory, IReadOnlyList<string> GeneratedFiles);
|