Files
gregExtractor/Services/TemplateService.cs
T

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