Skip to content

Commit 798c6c2

Browse files
committed
feat(songs): add portable htksong packages
1 parent ef6d27e commit 798c6c2

11 files changed

Lines changed: 664 additions & 11 deletions

File tree

docs/development/chart-creator.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,31 @@ The exporter creates a new, never-overwritten folder under
2828
- `song.json`;
2929
- `notes.json`.
3030

31+
It also creates one portable `<song-id>.htksong` file alongside the folder.
32+
The package is a ZIP-compatible, chart-only container with exactly three
33+
entries:
34+
35+
- `htksong-version` (`1`);
36+
- `song.json`;
37+
- `notes.json`.
38+
39+
To transfer a take, copy only the `.htksong` file into
40+
`Documents/HTKSongs` on the other computer and refresh the Song Library. The
41+
game validates and atomically imports it. Existing song folders are never
42+
overwritten.
43+
3144
The publish is atomic and both documents are parsed by the production loaders
32-
before the folder becomes visible. The manifest declares chart availability but
45+
before the folder or package becomes visible. The manifest declares chart availability but
3346
keeps audio as `missing`. Chart Creator never copies, embeds, downloads, or
3447
redistributes the source audio. To play or share the take, the user must add an
3548
audio file they are entitled to use and update the local binding explicitly.
3649

50+
Import is fail-closed. Unknown/archive entries, audio declarations, symbolic
51+
links, duplicate names, unsupported versions, malformed JSON, invalid charts,
52+
oversized data and path traversal are rejected before extraction. Version 1 is
53+
intentionally chart-only; adding optional distributable audio requires a future
54+
explicit package version and rights-aware UX.
55+
3756
The exported title is marked `Recorded Take` and the difficulty hint says that
3857
the performance must be reviewed before sharing. This is a captured performance,
3958
not a claim of authoritative transcription.

docs/development/song-library.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,19 @@ machine-specific path into bundled `song.json`.
9494
The refresh button performs an explicit rescan. Discovery does not watch the
9595
filesystem continuously, allocate every frame, or access folders outside the
9696
two configured roots.
97+
98+
## Portable `.htksong` chart packages
99+
100+
Chart Creator writes a portable chart-only package next to each recorded take.
101+
Copy a `.htksong` file directly into `~/Documents/HTKSongs` and select
102+
**Refresh library**. Before normal folder discovery, the game validates the
103+
container and atomically installs its `song.json` and `notes.json` into a folder
104+
named after the validated song ID. The package remains in place so it can be
105+
copied to another computer; subsequent refreshes are idempotent.
106+
107+
Package schema version 1 contains no audio and rejects any audio declaration or
108+
extra archive entry. Imports are bounded to 5 MiB, reject links, duplicate or
109+
case-colliding names, malformed ZIP/JSON/chart data, unsupported versions and
110+
path traversal, and never replace an existing song folder. An imported chart is
111+
therefore visible but unavailable until the player explicitly supplies audio
112+
they are entitled to use through the ordinary local song binding.

src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/ChartCreator.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,17 +263,24 @@ public ProjectedHit(double timeSeconds, DrumPad pad, int recordingIndex)
263263

264264
public sealed class ChartCreatorExportResult
265265
{
266-
internal ChartCreatorExportResult(string songId, string folderPath, string chartPath, string manifestPath)
266+
internal ChartCreatorExportResult(
267+
string songId,
268+
string folderPath,
269+
string chartPath,
270+
string manifestPath,
271+
string packagePath)
267272
{
268273
SongId = songId;
269274
FolderPath = folderPath;
270275
ChartPath = chartPath;
271276
ManifestPath = manifestPath;
277+
PackagePath = packagePath;
272278
}
273279
public string SongId { get; }
274280
public string FolderPath { get; }
275281
public string ChartPath { get; }
276282
public string ManifestPath { get; }
283+
public string PackagePath { get; }
277284
}
278285

279286
public sealed class ChartCreatorExporter
@@ -300,8 +307,10 @@ public ChartCreatorExportResult ExportChartOnly(
300307
string baseId = $"{prefix}-take-{createdAtUtc:yyyyMMdd-HHmmss}";
301308
string songId = UniqueSongId(root, baseId);
302309
string destination = Path.Combine(root, songId);
310+
string packagePath = Path.Combine(root, songId + HtkSongPackageService.Extension);
303311
string temporary = Path.Combine(root, $".hitthekit-chart-{Guid.NewGuid():N}");
304312
Directory.CreateDirectory(temporary);
313+
bool destinationPublished = false;
305314
try
306315
{
307316
string chartJson = ChartCreatorJson.Serialize(draft, metadata.Difficulty, metadata.Bpm, quantization);
@@ -314,15 +323,19 @@ public ChartCreatorExportResult ExportChartOnly(
314323
new ChartLoader().Load(chartJson, metadata.Difficulty);
315324
new SongLibraryDiscovery().Parse(File.ReadAllText(manifestPath), temporary, SongLibraryOrigin.UserFolder);
316325
Directory.Move(temporary, destination);
326+
destinationPublished = true;
327+
new HtkSongPackageService().CreateChartOnlyPackage(destination, packagePath);
317328
return new ChartCreatorExportResult(
318329
songId,
319330
destination,
320331
Path.Combine(destination, "notes.json"),
321-
Path.Combine(destination, "song.json"));
332+
Path.Combine(destination, "song.json"),
333+
packagePath);
322334
}
323335
catch
324336
{
325337
if (Directory.Exists(temporary)) Directory.Delete(temporary, true);
338+
if (destinationPublished && Directory.Exists(destination)) Directory.Delete(destination, true);
326339
throw;
327340
}
328341
}
@@ -331,7 +344,9 @@ private static string UniqueSongId(string root, string baseId)
331344
{
332345
string value = baseId;
333346
int suffix = 2;
334-
while (Directory.Exists(Path.Combine(root, value))) value = $"{baseId}-{suffix++}";
347+
while (Directory.Exists(Path.Combine(root, value)) ||
348+
File.Exists(Path.Combine(root, value + HtkSongPackageService.Extension)))
349+
value = $"{baseId}-{suffix++}";
335350
ChartCreatorMetadata.ValidateIdentifier(value, nameof(baseId));
336351
return value;
337352
}

src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayHighwayController.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Globalization;
4+
using System.IO;
45
using HitTheKit.Core;
56
using HitTheKit.Unity.Audio;
67
using HitTheKit.Unity.Charts;
@@ -124,6 +125,7 @@ public sealed class GameplayHighwayController : MonoBehaviour
124125
public double CurrentAttemptPracticeSeconds => practiceTimer.CurrentAttemptSeconds;
125126
public int RecordedChartHitCount => chartRecording?.HitCount ?? chartDraft?.Hits.Count ?? 0;
126127
public string LastChartExportPath { get; private set; }
128+
public string LastChartPackagePath { get; private set; }
127129

128130
private void Awake()
129131
{
@@ -508,6 +510,7 @@ public void RestartRun()
508510
resultRecorded = false;
509511
chartDraft = null;
510512
LastChartExportPath = null;
513+
LastChartPackagePath = null;
511514
chartRecording?.Restart();
512515
scoreTracker.Reset();
513516
keyboardCalibration.Reset();
@@ -643,8 +646,10 @@ public ChartCreatorExportResult SaveChartRecording(ChartQuantization quantizatio
643646
SongLibraryRuntime.UserRoot,
644647
DateTimeOffset.UtcNow);
645648
LastChartExportPath = result.FolderPath;
649+
LastChartPackagePath = result.PackagePath;
646650
if (chartCreatorStatusLabel != null)
647-
chartCreatorStatusLabel.text = $"SALVATO · {result.SongId} · AGGIUNGI SOLO AUDIO AUTORIZZATO";
651+
chartCreatorStatusLabel.text =
652+
$"SALVATO · {Path.GetFileName(result.PackagePath)} · COPIA IL PACCHETTO SU UN ALTRO MAC";
648653
return result;
649654
}
650655

0 commit comments

Comments
 (0)