File size: 11,171 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
using System.Diagnostics;
using System.Text.Json;
using Repoverse.Core;
using Repoverse.Core.Generation;
using Repoverse.Core.Ingestion;
using Repoverse.Core.Simulation;
using Repoverse.Core.Voxels;

// repoverse <command> [options]. Each command reads/writes plain JSON so stages can be run,
// inspected and diffed independently: snapshot → manifest → chunks/meshes.

var cmd = args.FirstOrDefault();
var opts = ParseOptions(args.Skip(1).ToArray());
try
{
    return cmd switch
    {
        "ingest-local" => IngestLocal(opts),
        "ingest-github" => await IngestGitHub(opts),
        "generate" => Generate(opts),
        "stats" => await Stats(opts),
        "export-mesh" => ExportMesh(opts),
        "diff" => Diff(opts),
        _ => Usage(),
    };
}
catch (Exception e) when (e is IOException or InvalidDataException or FormatException or ArgumentException or InvalidOperationException or JsonException)
{
    Console.Error.WriteLine($"error: {GitHubIngest.Redact(e.Message)}");
    return 1;
}

static int Usage()
{
    Console.Error.WriteLine("""
        usage:
          repoverse ingest-local  <git-dir> [--owner NAME] [--subprojects] --out snapshot.json
          repoverse ingest-github <owner> [--limit N] [--forks] [--cache DIR] --out snapshot.json   (token: $GITHUB_TOKEN)
          repoverse generate      --snapshot snapshot.json --out manifest.json [--seed N] [--overrides FILE] [--previous manifest.json]
          repoverse stats         --manifest manifest.json
          repoverse export-mesh   --manifest manifest.json --out world-mesh.json
          repoverse diff          --before snapshot.json --after snapshot.json
        """);
    return 2;
}

static int IngestLocal(Dictionary<string, string> o)
{
    var dir = Required(o, "_0");
    var owner = o.GetValueOrDefault("owner", "local");
    var repos = LocalGitIngest.Ingest(dir, owner, o.ContainsKey("subprojects"));
    var snap = new Snapshot { TakenAt = DateTimeOffset.UtcNow, Source = $"git:{Path.GetFullPath(dir)}", Repositories = repos };
    SnapshotStore.Save(Required(o, "out"), snap);
    Console.WriteLine($"[GITHUB] ingested {repos.Count} repositories from {dir}");
    return 0;
}

static async Task<int> IngestGitHub(Dictionary<string, string> o)
{
    var owner = Required(o, "_0");
    var outPath = Required(o, "out");
    var cache = o.GetValueOrDefault("cache", Path.Combine(Path.GetTempPath(), "repoverse-cache"));
    var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
    var ingest = new GitHubIngest(new HttpClient(), cache, token);
    var result = await ingest.IngestOwnerAsync(owner, o.ContainsKey("forks"), o.TryGetValue("limit", out var l) ? int.Parse(l) : null);

    var merged = SnapshotStore.Merge(SnapshotStore.Load(outPath), result, $"github:{owner}", DateTimeOffset.UtcNow);
    if (merged.Snapshot.Repositories.Count > 0) SnapshotStore.Save(outPath, merged.Snapshot);
    foreach (var f in result.Failures) Console.Error.WriteLine($"[GITHUB] failed {f.Repo}: {f.Reason}");
    Console.WriteLine($"[GITHUB] {result.Repositories.Count} fresh, {merged.Stale.Count} stale, {result.Failures.Count} failed; " +
                      $"{ingest.RequestsSent} requests ({ingest.NotModified} not modified){(result.RateLimited ? "; RATE LIMITED" : "")}");
    return merged.Snapshot.Repositories.Count > 0 ? 0 : 1;
}

static int Generate(Dictionary<string, string> o)
{
    var snap = SnapshotStore.Load(Required(o, "snapshot")) ?? throw new IOException("snapshot not found");
    var seed = new Seed(o.TryGetValue("seed", out var s) ? ulong.Parse(s) : 0x5AA4C177UL);
    var overrides = o.TryGetValue("overrides", out var ov) ? Overrides.Load(ov) : null;
    var previous = o.TryGetValue("previous", out var pv) && File.Exists(pv) ? ReadManifest(pv) : null;

    var sw = Stopwatch.StartNew();
    var manifest = ManifestBuilder.Build(snap.Repositories, seed, overrides, previous);
    File.WriteAllText(Required(o, "out"), JsonSerializer.Serialize(manifest, Json.Options));
    Console.WriteLine($"[MANIFEST] {manifest.Buildings.Count} buildings, {manifest.Dependencies.Count} dependency edges in {sw.ElapsedMilliseconds} ms (seed {manifest.WorldSeed})");
    Console.WriteLine($"{"repository",-44} {"category",-22} {"district",-18} {"archetype",-13} {"floors",6} {"rooms",6}");
    foreach (var b in manifest.Buildings)
        Console.WriteLine($"{b.Repo.Key,-44} {b.Category.Value,-22} {b.District.Value,-18} {b.Archetype.Value,-13} {b.Floors,6} {b.Interior.Rooms.Count(BuildingRasterizer.IsWorkRoom),6}   ← {b.Category.Why}");
    return 0;
}

static async Task<int> Stats(Dictionary<string, string> o)
{
    var manifest = ReadManifest(Required(o, "manifest"));
    var gen = new WorldGenerator(manifest);
    var chunks = ChunksForBuildings(manifest).ToList();

    var sw = Stopwatch.StartNew();
    var generated = chunks.AsParallel().Select(c => gen.Generate(c)).ToList();
    var genTime = sw.Elapsed;
    var byCoord = generated.ToDictionary(c => c.Coord);
    Voxel? Sample(int x, int y, int z) => byCoord.TryGetValue(ChunkCoord.FromVoxel(x, y, z), out var ch)
        ? ch[x - ch.Coord.OriginX, y - ch.Coord.OriginY, z - ch.Coord.OriginZ] : null;

    sw.Restart();
    var meshes = generated.AsParallel().Select(c => GreedyMesher.Build(c, Sample)).ToList();
    var meshTime = sw.Elapsed;
    int faces = meshes.Sum(m => m.VisibleFaces), quads = meshes.Sum(m => m.Quads.Count);
    int objects = manifest.Buildings.Sum(b => BuildingRasterizer.Objects(b).Count());

    Console.WriteLine($"[VOXEL] {generated.Count} chunks, {generated.Sum(c => (long)c.NonAirCount):N0} solid voxels, {generated.Count * 64} KiB voxel memory");
    Console.WriteLine($"[VOXEL] generate {genTime.TotalMilliseconds:F0} ms total, {genTime.TotalMilliseconds / generated.Count:F2} ms/chunk (parallel)");
    Console.WriteLine($"[VOXEL] mesh {meshTime.TotalMilliseconds:F0} ms; {faces:N0} visible faces → {quads:N0} quads ({quads * 2:N0} tris), {100.0 * quads / Math.Max(1, faces):F1}% of naive");
    Console.WriteLine($"[VOXEL] draw groups: max {meshes.Max(m => m.MaterialGroups)} per chunk, {meshes.Sum(m => m.MaterialGroups)} total");
    Console.WriteLine($"[WORLDGEN] {objects} interactive objects (entities, not voxels)");

    var streamer = new ChunkStreamer(gen, new WorldEdits(), radius: 4);
    var b0 = manifest.Buildings[0];
    sw.Restart();
    await streamer.SettleAsync(b0.Footprint.X, b0.Footprint.Z);
    Console.WriteLine($"[STREAMING] radius 4 around {b0.Repo}: {streamer.Loaded.Count} chunks resident in {sw.ElapsedMilliseconds} ms");
    return 0;
}

static int ExportMesh(Dictionary<string, string> o)
{
    var manifest = ReadManifest(Required(o, "manifest"));
    var gen = new WorldGenerator(manifest);
    var chunks = ChunksForBuildings(manifest).AsParallel().Select(c => gen.Generate(c)).ToDictionary(c => c.Coord);
    Voxel? Sample(int x, int y, int z) => chunks.TryGetValue(ChunkCoord.FromVoxel(x, y, z), out var ch)
        ? ch[x - ch.Coord.OriginX, y - ch.Coord.OriginY, z - ch.Coord.OriginZ] : null;

    // Flat arrays per mesh key keep the file small and map 1:1 onto GPU buffers.
    var groups = new Dictionary<ushort, (List<float> Pos, List<int> Normal)>();
    foreach (var mesh in chunks.Values.AsParallel().Select(c => GreedyMesher.Build(c, Sample)).ToList())
        foreach (var q in mesh.Quads)
        {
            if (!groups.TryGetValue(q.MeshKey, out var g)) groups[q.MeshKey] = g = ([], []);
            foreach (var (x, y, z) in GreedyMesher.Corners(mesh.Coord, q)) { g.Pos.Add(x); g.Pos.Add(y); g.Pos.Add(z); }
            g.Normal.Add((q.Axis + 1) * (q.Positive ? 1 : -1));
        }

    var export = new
    {
        manifest.WorldSeed,
        Groups = groups.OrderBy(kv => kv.Key).Select(kv => new
        {
            Block = ((BlockType)(kv.Key & 0xFF)).ToString(),
            Material = ((Repoverse.Core.Model.MaterialFamily)(kv.Key >> 8 & 0xF)).ToString(),
            Positions = kv.Value.Pos,
            Normals = kv.Value.Normal,
        }),
        Buildings = manifest.Buildings.Select(b => new
        {
            Repo = b.Repo.Key, b.Description, Category = b.Category.Value.ToString(), CategoryWhy = b.Category.Why.ToString(),
            District = b.District.Value.ToString(), Archetype = b.Archetype.Value.ToString(), b.Floors,
            Activity = b.Activity.Value.ToString(), Construction = b.Construction.Value.ToString(),
            b.Footprint.X, b.Footprint.Z, b.Footprint.Width, b.Footprint.Depth, b.Height,
            Rooms = b.Interior.Rooms.Where(BuildingRasterizer.IsWorkRoom).Select(r => new { r.Name, Purpose = r.Purpose.ToString(), r.Floor, r.FileCount }),
        }),
        Objects = manifest.Buildings.SelectMany(BuildingRasterizer.Objects)
            .Select(w => new { w.X, w.Y, w.Z, Kind = w.Kind.ToString(), Uri = new RepoverseUri { Repo = w.Repo, Path = w.SourcePath }.ToString() }),
        Dependencies = manifest.Dependencies.Select(d => new[] { d.From.Key, d.To.Key }),
    };
    File.WriteAllText(Required(o, "out"), JsonSerializer.Serialize(export, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
    Console.WriteLine($"[VOXEL] exported {groups.Values.Sum(g => g.Normal.Count):N0} quads in {groups.Count} material groups");
    return 0;
}

static int Diff(Dictionary<string, string> o)
{
    var before = SnapshotStore.Load(Required(o, "before")) ?? throw new IOException("before snapshot not found");
    var after = SnapshotStore.Load(Required(o, "after")) ?? throw new IOException("after snapshot not found");
    foreach (var e in SnapshotDiff.Diff(before.Repositories, after.Repositories)) Console.WriteLine(e);
    return 0;
}

static IEnumerable<ChunkCoord> ChunksForBuildings(WorldManifest m)
{
    var set = new HashSet<ChunkCoord>();
    foreach (var b in m.Buildings)
    {
        var lo = ChunkCoord.FromVoxel(b.Footprint.X - WorldConstants.RoadWidth, 0, b.Footprint.Z - WorldConstants.RoadWidth);
        var hi = ChunkCoord.FromVoxel(b.Footprint.MaxX + WorldConstants.RoadWidth, WorldConstants.GroundLevel + b.Height, b.Footprint.MaxZ + WorldConstants.RoadWidth);
        for (int x = lo.X; x <= hi.X; x++) for (int y = 0; y <= hi.Y; y++) for (int z = lo.Z; z <= hi.Z; z++)
            set.Add(new ChunkCoord(x, y, z));
    }
    return set.OrderBy(c => c.X).ThenBy(c => c.Z).ThenBy(c => c.Y);
}

static WorldManifest ReadManifest(string path) =>
    JsonSerializer.Deserialize<WorldManifest>(File.ReadAllText(path), Json.Options) ?? throw new InvalidDataException($"{path} is empty");

static string Required(Dictionary<string, string> o, string key) =>
    o.TryGetValue(key, out var v) ? v : throw new ArgumentException(key.StartsWith('_') ? "missing positional argument" : $"missing --{key}");

static Dictionary<string, string> ParseOptions(string[] a)
{
    var o = new Dictionary<string, string>();
    int pos = 0;
    for (int i = 0; i < a.Length; i++)
    {
        if (!a[i].StartsWith("--")) { o[$"_{pos++}"] = a[i]; continue; }
        var key = a[i][2..];
        o[key] = i + 1 < a.Length && !a[i + 1].StartsWith("--") ? a[++i] : "true";
    }
    return o;
}