diff --git a/CLAUDE.md b/CLAUDE.md index f43fad4..5c1b3e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ the single source of truth for both the game and this web toy**. The web build c | `test/engine/` | Engine-only test harness (`make test-engine`) — compiles `Engine/**` alone into the same assembly as the tests, so it runs on a plain dev host where s&box cannot. The safety net for engine work. | | `sbox-library/Skafinity/skafinity.config.json` | The single shared **house-mix config** (peak balances / kit presence). Canonical here; the s&box plugin reads it at runtime and `make` copies it to `web/config.json`. Edit it to retune the baseline mix without a rebuild. | | `sbox-library/Skafinity/Code/SkafinityPlayer.cs` | The s&box playback driver (`SoundStream`, infinite `tag:n`, look-ahead, crossfade). Web equivalent is `web/app.js`; the s&box-only bits are not used on the web. | -| `sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor` (`.scss`) | Optional drop-in Razor `PanelComponent` — finds a `SkafinityPlayer` and exposes its knobs as in-game UI (seed/prev-next, genre, per-instrument vibe mixer, mute/volume, reroll, save). s&box-only; not in the web build. | +| `sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor` (`.scss`) | Optional drop-in Razor `PanelComponent` — finds a `SkafinityPlayer` and exposes its knobs as in-game UI (seed + this-song/this-station copy, prev-next, genre strip with a Random entry, mute/volume, new station, save, and the per-instrument vibe mixer behind a TINKER button). s&box-only; not in the web build. | | `sbox-library/Skafinity/Code/UI/SkafinityTheme.cs` | The panel's palette, derived at RUNTIME from one `Accent` colour so a consuming game can retint a *vendored* copy without editing it. Unset = neutral gray/black. | | `reference/*.cs` | The original Rotaliate-client copies, kept for context. **Read-only.** The `sbox-library` copies are what actually compile. | diff --git a/PLAN.md b/PLAN.md index 4ef239d..05c0f91 100644 --- a/PLAN.md +++ b/PLAN.md @@ -16,7 +16,7 @@ refactor proving it moved nothing). | Rank | Item | Notes | |---|---|---| -| 98 | Get s&box back to parity after the new seed format | The engine, `SkafinityPlayer.cs` and the Razor panel all compile and drive `tag:n[:genre][:vibe]` already — what has not followed is the panel's UI: the knob matrix is still open rather than behind a tinker button, the genre strip has no "Random" entry (the player has `RollGenre()` waiting for one), there is one copy button rather than this-song/this-station, and the reroll still says it rerolls the genre when it now rerolls the station. `RandomEverySong` is also still one switch over both genre and vibe, where the web splits them per seed part. Cannot be built or tested on the dev host, so it is a push-and-listen row. **And the save is broken**: `SaveCurrentToFile` passes `channels: 1` to `WavFromSamples` while `_curRaw` is the interleaved stereo buffer `SoundStream( _sr, MusicGen.Channels )` is fed, so every in-game save is stereo PCM under a mono header — half speed, channels interleaved. Output is always stereo (`MusicGen.Channels = 2`); pass it. | +| 55 | s&box has no shuffle, in the sense the web now means it | `shuffle` on the web is "every next song is a whole new station rather than the next song of this one" — `SeedCodec.RollTagFor` is in the engine for exactly that and only the web calls it. `SkafinityPlayer` walks song indices, so this is the positions-vs-indices rework the web did in `aeb9067`: the ledgers, the PCM cache, Prev/Next and the queue view all key on a timeline POSITION whose station is derived from the root. Push-and-listen, like everything s&box. | | 12 | Revisit the always-full-width vibe | A normalised vibe carries every instrument slot in every genre, so a genre using four voices still pays for the nine — 36 hex chars, whatever is playing. Whether that is worth compressing depends on how ugly the pasted result turns out to be in practice, which is not knowable until a few have been shared. Any scheme has to keep "incomplete is an error" and genre-independence, which is what rules out per-genre trimming. | | 45 | Some songs open with a cymbal wash nobody put there | Reported by ear as "a wash of cymbals that have nothing to do with what's being played from the beginning of tracks", and it survives every explanation tried so far. What is MEASURED: the render's first sample is near silence and reaches full peak in ~1 ms, so it is a real attack and not a truncated ring — the PCM does not begin mid-decay. Band-limited above 2.5 kHz the first two seconds are QUIETER than mid-song on all eight seeds checked, so there is no extra cymbal energy overall. But on 2 of those 8 the high band *decays* from sample 0 with tau ~330 ms and the first real hit is not until 360 ms, which is what is being heard. Two candidates were tested and BOTH are ruled out: the host fade-up (fixed separately, and the shape is identical with the old envelope removed) and CrashOnOne on the song's first bar (suppressed — the opening 340 ms is byte-identical with and without it, so that change is a correctness fix and not this). 330 ms matches KitNuance.OpenHatDurMin/Max (0.26-0.42 s) almost exactly, but both ska grooves put their Open cell on the "and of 4" rather than beat 1, so that does not explain it either. **The next step is an instrument, not more inference**: --render currently has no way to mute or solo the kit, so "is this the drums at all" cannot be answered in one measurement and every attempt so far has been a chain of guesses. Add that first. | | 40 | The cymbals are not tuned, hats included | The ride carries three stacked edits (`StrokeLevelRide` 0.45, `RingTau` knee 3500, `BowStrike` bump 4200) and the hats two (`KitNuance.OpenHatDurMin/Max` 0.26–0.42, `HatBalance` 0.288). Every one of them landed off a single listening note on a single seed, and none has been swept across the six genres or against a section that is dense rather than sparse. **The crash has deliberately not been touched through any of it**, so it is now the least-examined voice in the kit and quite possibly the loud one. Levers by reach: the three ride constants above, the open-hat band, then `HatBalance`/`RideBalance`/`CrashBalance` in `skafinity.config.json` (no rebuild). Judge the voice with `--cymbal` and the mix with the band + duty-cycle read (CLAUDE.md) — whole-mix RMS is what sent three rounds of this after the wrong voice. | diff --git a/sbox-library/Skafinity/Code/Engine/Wav.cs b/sbox-library/Skafinity/Code/Engine/Wav.cs index 0820e1e..05fe23f 100644 --- a/sbox-library/Skafinity/Code/Engine/Wav.cs +++ b/sbox-library/Skafinity/Code/Engine/Wav.cs @@ -12,10 +12,15 @@ static class Wav /// Clamp a −1..1 mix sample to signed 16-bit. public static short ToS16( float v ) => (short)(Math.Clamp( v, -1f, 1f ) * 32767f); - /// Wrap already-rendered 16-bit samples in a WAV. Mono or interleaved stereo per - /// . - public static byte[] FromSamples( short[] samples, int channels, int sampleRate ) + /// Wrap already-rendered 16-bit interleaved-stereo samples in a WAV. + /// The channel count is and is deliberately NOT a + /// parameter. Everything the engine renders and every buffer a host carries is interleaved + /// stereo, so a caller's channel count could only ever agree with that or be wrong — and wrong + /// is silent: stereo PCM under a mono header plays at half speed with the channels interleaved + /// into each other, which is a plausible-sounding artefact rather than an obvious failure. + public static byte[] FromSamples( short[] samples, int sampleRate ) { + const int channels = MusicGen.Channels; int dataSize = samples.Length * 2; int blockAlign = channels * 2; var bytes = new List( 44 + dataSize ); @@ -46,10 +51,9 @@ short[] ToShorts( float gain ) return s; } - /// Wrap already-rendered 16-bit samples in a WAV (for export). Mono or - /// interleaved stereo per . - public static byte[] WavFromSamples( short[] samples, int channels, int sampleRate ) - => Wav.FromSamples( samples, channels, sampleRate ); + /// Wrap already-rendered 16-bit interleaved-stereo samples in a WAV (for export). + public static byte[] WavFromSamples( short[] samples, int sampleRate ) + => Wav.FromSamples( samples, sampleRate ); - byte[] EncodeWav( float gain ) => Wav.FromSamples( ToShorts( gain ), Channels, _sr ); + byte[] EncodeWav( float gain ) => Wav.FromSamples( ToShorts( gain ), _sr ); } diff --git a/sbox-library/Skafinity/Code/SkafinityCommands.cs b/sbox-library/Skafinity/Code/SkafinityCommands.cs index a304342..4b71778 100644 --- a/sbox-library/Skafinity/Code/SkafinityCommands.cs +++ b/sbox-library/Skafinity/Code/SkafinityCommands.cs @@ -217,7 +217,8 @@ public static void SetGenre( int genre ) Log.Info( $"[Skafinity] genre {genre} = {VibeCodec.Genres[genre]} — {p.CurrentSeed}" ); } - /// Reroll the vibe: a new genre and every knob, keeping your per-instrument volumes. + /// Throw every knob somewhere new and pin it there, keeping your per-instrument + /// volumes and the genre. The way back out is skafinity_random vibe. [ConCmd( "skafinity_reroll" )] public static void Reroll() { @@ -225,7 +226,38 @@ public static void Reroll() if ( p == null ) return; p.RerollVibe(); - Log.Info( $"[Skafinity] rerolled — {p.CurrentSeed}" ); + Log.Info( $"[Skafinity] rerolled the vibe — {p.CurrentSeed}" ); + } + + /// A fresh random station at song 0. Anything pinned stays pinned. + [ConCmd( "skafinity_station" )] + public static void Station() + { + var p = Player(); + if ( p == null ) return; + + p.RerollStation(); + Log.Info( $"[Skafinity] new station — {p.StationSeed}" ); + } + + /// Hand a pinned seed part back to the station so every song rolls its own again: + /// skafinity_random genre, vibe, or both. + [ConCmd( "skafinity_random" )] + public static void Random( string part = "both" ) + { + var p = Player(); + if ( p == null ) return; + + switch ( (part ?? "").Trim().ToLowerInvariant() ) + { + case "genre": p.RollGenre(); break; + case "vibe": p.RollVibe(); break; + case "both" or "": p.RollGenre(); p.RollVibe(); break; + default: + Log.Warning( $"[Skafinity] '{part}' is not a seed part — genre, vibe or both." ); + return; + } + Log.Info( $"[Skafinity] rolling — {p.StationSeed}" ); } /// Write the playing song to a .wav under the s&box data folder. @@ -255,7 +287,8 @@ public static void Status() Log.Info( $" transport {( p.Enabled ? "on" : "MUTED" )}, vol {p.Volume:0.00}, " + $"{( p.IsPlaying ? "playing" : "not playing" )}" + $"{( p.IsBuffering ? ", BUFFERING" : p.IsGenerating ? ", generating ahead" : "" )}" ); - Log.Info( $" shuffle {( p.RandomEverySong ? "on — every song freezes a fresh vibe + genre" : "off" )}" ); + Log.Info( $" station {p.StationSeed} (genre {( p.GenrePinned ? "pinned" : "rolling" )}, " + + $"vibe {( p.VibePinned ? "pinned" : "rolling" )})" ); Log.Info( $" output {p.SampleRate} Hz, {p.RenderThreads} render thread(s)" ); // Zero here is the interesting case: the baseline mix is then the engine's compiled // defaults, not the file the web toy reads, and nothing else would ever say so. diff --git a/sbox-library/Skafinity/Code/SkafinityPlayer.cs b/sbox-library/Skafinity/Code/SkafinityPlayer.cs index af5d670..86afc47 100644 --- a/sbox-library/Skafinity/Code/SkafinityPlayer.cs +++ b/sbox-library/Skafinity/Code/SkafinityPlayer.cs @@ -38,10 +38,18 @@ public sealed class SkafinityPlayer : Component, Component.DontExecuteOnServer [Property, Group( "Music" )] public string MixerName { get; set; } = ""; /// Begin playing automatically in . Off = call yourself. [Property, Group( "Music" )] public bool AutoPlay { get; set; } = true; - /// Shuffle mode: re-randomise every knob (incl. genre) as each new song begins, so the - /// sequence keeps reinventing itself. Volumes are left alone (a local mix preference). Off = the - /// seed's vibe stays put. ON by default — endless variety out of the box. - [Property, Group( "Music" )] public bool RandomEverySong { get; set; } = true; + /// Roll a fresh GENRE for each new song, the way a seed with no genre part does. Off = + /// is pinned and every song plays it. ON by default. + /// The genre and the vibe get a switch each because the SEED gives them a part each: + /// tag:n[:genre][:vibe] pins either alone, so one switch over both could not express half + /// the seeds the engine parses (pin a genre and let vibes roll, or the reverse). These two ARE + /// that pinning — writes down whatever they leave rolling. + [Property, Group( "Music" )] public bool RandomGenreEverySong { get; set; } = true; + /// Roll a fresh VIBE (every knob but the per-instrument volumes, which are a local mix + /// preference) for each new song. Off = the live knobs / the override are + /// pinned and every song plays them. ON by default — endless variety out of the box. + /// + [Property, Group( "Music" )] public bool RandomVibeEverySong { get; set; } = true; // ── Seed ── /// Seed tag — any string (a name, a word). Empty falls back to "skafinity". @@ -201,9 +209,6 @@ public sealed class SkafinityPlayer : Component, Component.DontExecuteOnServer // instant within the window; outside it we regenerate from the ledger seed. readonly System.Collections.Generic.Dictionary _ledger = new(); readonly System.Collections.Generic.Dictionary _genreLedger = new(); - // Has someone CHOSEN this genre (the dropdown, a seed that wrote one down), as opposed to the - // station rolling it? Pinning is what a seed carries, so it has to outlive a StartSequence. - bool _genrePinned; readonly System.Collections.Generic.Dictionary _pcm = new(); // Per-song synthesis progress (0..1) for songs currently being generated; absent ⇒ not generating. readonly System.Collections.Generic.Dictionary _genProgress = new(); @@ -243,6 +248,18 @@ public sealed class SkafinityPlayer : Component, Component.DontExecuteOnServer { Tag = SeedTag, N = _curN, Genre = GenreForN( _curN ), Vibe = VibeForN( _curN ), } ); + + /// Shareable seed for the STATION: the seed exactly as it stands, so whatever this + /// player left rolling keeps rolling for whoever is handed it. The counterpart to + /// , and the reason there are two copy buttons — "share this" means one + /// of two different things, and neither can be recovered from the other. + public string StationSeed => SeedCodec.Format( new SeedCodec.Seed + { + Tag = SeedTag, + N = _curN, + Genre = RandomGenreEverySong ? SeedCodec.RolledGenre : Math.Clamp( Genre, 0, VibeCodec.GenreCount - 1 ), + Vibe = RandomVibeEverySong ? null : VibeForN( _curN ), + } ); /// True once a stream handle is live and audible. public bool IsPlaying => _handle != null; /// True while any synthesis is in flight (foreground seek or background look-ahead fill). @@ -423,8 +440,11 @@ float TargetVolume() return v; } - /// The config currently in effect (inspector knobs with any applied). - public MusicGen.Config EffectiveConfig() => BuildConfig(); + /// The config the PLAYING song was synthesised with — what a UI should draw its knobs + /// from. Deliberately the audible song rather than the live knobs: with the vibe left rolling, + /// those two are different configs and a mixer that shows the one you cannot hear is worse than + /// no mixer. + public MusicGen.Config EffectiveConfig() => ConfigForN( _curN ); MusicGen.Config BuildConfig() { @@ -447,9 +467,9 @@ MusicGen.Config BuildConfig() string VibeForN( int n ) { if ( _ledger.TryGetValue( n, out var v ) ) return v; - // Outside shuffle a song TRACKS the live knobs and is deliberately not cached, so a knob - // edit followed by a restart is always picked up. - if ( !RandomEverySong ) return VibeCodec.Encode( BuildKnobOnlyVibe() ); + // Pinned, a song TRACKS the live knobs and is deliberately not cached, so a knob edit + // followed by a restart is always picked up. + if ( !RandomVibeEverySong ) return VibeCodec.Encode( BuildKnobOnlyVibe() ); var rolled = SeedCodec.RollVibeFor( Tag, n ); _ledger[n] = rolled; return rolled; @@ -460,7 +480,7 @@ string VibeForN( int n ) int GenreForN( int n ) { if ( _genreLedger.TryGetValue( n, out var g ) ) return g; - if ( _genrePinned || !RandomEverySong ) return Math.Clamp( Genre, 0, VibeCodec.GenreCount - 1 ); + if ( !RandomGenreEverySong ) return Math.Clamp( Genre, 0, VibeCodec.GenreCount - 1 ); int rolled = SeedCodec.RollGenreFor( Tag, n ); _genreLedger[n] = rolled; return rolled; @@ -583,7 +603,7 @@ int ConfigHash() h.Add( KeysVol ); h.Add( KeysCutoff ); h.Add( KeysDrive ); h.Add( KeysChug ); h.Add( RhythmGtrVol ); h.Add( RhythmGtrCutoff ); h.Add( RhythmGtrDrive ); h.Add( RhythmGtrChug ); h.Add( LeadGtrVol ); h.Add( LeadGtrCutoff ); h.Add( LeadGtrDrive ); h.Add( LeadGtrBend ); - h.Add( Tag ); h.Add( Vibe ); h.Add( _genrePinned ); + h.Add( Tag ); h.Add( Vibe ); h.Add( RandomGenreEverySong ); h.Add( RandomVibeEverySong ); return h.ToHashCode(); } @@ -734,12 +754,9 @@ public void StartSequence() _pcm.Clear(); _genProgress.Clear(); _bufferingN = -1; - // Pin the current song to the explicit base vibe/genre (a pasted seed, a chosen genre, a - // reroll) so it is honoured even under shuffle — shuffle still rolls fresh from n+1 onward. - // No vibe ⇒ unpinned (shuffle rolls the current song too; non-shuffle tracks the live knobs). - if ( VibeCodec.IsVibe( Vibe ) ) _ledger[Math.Max( 0, _curN )] = Vibe; - if ( _genrePinned || !RandomEverySong ) - _genreLedger[Math.Max( 0, _curN )] = Math.Clamp( Genre, 0, VibeCodec.GenreCount - 1 ); + // Nothing is pinned into the ledger here any more. A pin is now per seed part and applies to + // EVERY song (RandomGenreEverySong / RandomVibeEverySong), which is what the seed means — so + // VibeForN/GenreForN already answer with it and a ledger entry could only shadow them. _handle?.Stop(); _handle = null; _stream = null; @@ -920,12 +937,12 @@ public bool PlaySeed( string seed, out string error ) if ( !SeedCodec.TryParse( seed, out var s, out error ) ) return false; Tag = (s.Tag ?? "").Trim().ToLowerInvariant(); _curN = Math.Max( 0, s.N ); - // A pinned part becomes the live value AND rides through StartSequence's pin below; an - // absent one goes back to rolling from this index on. + // A pinned part becomes the live value and stays pinned for every song; an absent one goes + // back to rolling per song, which is what the seed leaving it out means. Vibe = s.VibePinned ? s.Vibe : ""; - _genrePinned = s.GenrePinned; if ( s.GenrePinned ) Genre = s.Genre; - RandomEverySong = !s.GenrePinned || !s.VibePinned; + RandomGenreEverySong = !s.GenrePinned; + RandomVibeEverySong = !s.VibePinned; if ( PersistProgress ) SaveN( _curN ); StartSequence(); return true; @@ -957,7 +974,9 @@ public void SetTag( string tag ) /// a 0..1 fraction, store the re-encoded , and restart on a short debounce. public void SetVibe( int index, float norm ) { - var cfg = BuildConfig(); + // From the AUDIBLE song, not the live knobs: with the vibe rolling they are different + // configs, and moving one slider has to leave the other 35 where they were heard. + var cfg = ConfigForN( _curN ); var fields = VibeCodec.Fields( cfg.Genre ); if ( index < 0 || index >= fields.Count ) return; var f = fields[index]; @@ -970,7 +989,10 @@ public void SetVibe( int index, float norm ) } else { + // Dragging a knob PINS the vibe — otherwise the next song rolls the edit away and the + // slider is a control that does nothing past the crossfade. RollVibe is the way back out. Vibe = VibeCodec.Encode( cfg ); + RandomVibeEverySong = false; } _restartPending = true; _restartPendingSince = 0; @@ -984,30 +1006,67 @@ public void SetVibe( int index, float norm ) public void SetGenre( int genre ) { Vibe = CurrentVibe; + RandomVibeEverySong = false; Genre = Math.Clamp( genre, 0, VibeCodec.GenreCount - 1 ); - _genrePinned = true; + RandomGenreEverySong = false; StartSequence(); } - /// Hand the genre back to the station: from the current song on, each one rolls its own - /// again. The counterpart to , and the reason the dropdown needs a "Random" - /// entry — without one there is no way back out of a genre once one has been chosen. - public void RollGenre() + /// Hand the genre back to the station: every song rolls its own again. The counterpart + /// to , and the reason the genre strip needs a "Random" entry — without one + /// there is no way back out of a genre once one has been chosen. + public void RollGenre() => SetRandomGenreEverySong( true ); + + /// Hand the VIBE back to the station: every song rolls its own again. The way out of a + /// dragged knob — and it may well move nothing you can hear, because the song already playing + /// keeps the vibe it resolved to; what changes is what comes NEXT. + public void RollVibe() { - _genrePinned = false; + Vibe = ""; + SetRandomVibeEverySong( true ); + } + + /// Reroll the SEED: a fresh random station at song 0. Anything pinned stays pinned, + /// because a pin is a choice and this is a request for a different song, not a different + /// taste. + public void RerollStation() + { + Tag = RandomTag(); + _curN = 0; + if ( PersistProgress ) SaveN( _curN ); StartSequence(); } - /// Randomize the vibe knobs and restart on a short debounce. By default the - /// per-instrument volumes (and genre) are left alone so a reroll re-voices without upending - /// the mix; pass / for a - /// full shuffle. Pass = false to re-voice without yanking the - /// playhead — the caller is then responsible for letting the change take effect (e.g. by - /// clearing the look-ahead so upcoming songs regenerate with the new vibe). - public void RerollVibe( bool includeVolumes = false, bool includeGenre = true, bool restart = true ) + // Eight base-36 characters — a tag nobody has to read out, the same shape the web toy draws. + static string RandomTag() + { + var sb = new System.Text.StringBuilder( 8 ); + for ( int i = 0; i < 8; i++ ) + { + int q = System.Random.Shared.Next( 36 ); + sb.Append( q < 10 ? (char)('0' + q) : (char)('a' + q - 10) ); + } + return sb.ToString(); + } + + /// Throw every knob somewhere new and PIN it there — the die over the mixer. It always + /// moves every slider, because it draws a fresh vibe rather than handing the knobs back to the + /// station (that is , and a die that does nothing when nothing was pinned + /// is a die that looks broken). The per-instrument volumes and the GENRE are left alone by + /// default: a die on a mixer re-voices the band, it does not swap the band — pass + /// / for a full shuffle. Pass + /// = false to re-voice without yanking the playhead — the caller is + /// then responsible for letting the change take effect (e.g. by clearing the look-ahead so + /// upcoming songs regenerate with the new vibe). + public void RerollVibe( bool includeVolumes = false, bool includeGenre = false, bool restart = true ) { Vibe = VibeCodec.RollVibe( System.Random.Shared.NextSingle ); - if ( includeGenre ) Genre = VibeCodec.RollGenre( System.Random.Shared.NextSingle ); + RandomVibeEverySong = false; + if ( includeGenre ) + { + Genre = VibeCodec.RollGenre( System.Random.Shared.NextSingle ); + RandomGenreEverySong = false; + } if ( includeVolumes ) { // Volumes are not in the wire at all, so they are rolled separately and captured into @@ -1024,15 +1083,30 @@ public void RerollVibe( bool includeVolumes = false, bool includeGenre = true, b } } - /// Turn shuffle on/off and rebuild the forward timeline from the current song so the - /// change takes immediately: ON freezes a fresh rolled vibe+genre per upcoming n, OFF reverts - /// upcoming songs to the live knobs. History already played keeps whatever it was frozen as. - public void SetRandomEverySong( bool on ) + /// Let the GENRE roll per song again, or pin it to . Rebuilds the + /// forward timeline so the change takes immediately; history keeps what it was. + public void SetRandomGenreEverySong( bool on ) + { + if ( RandomGenreEverySong == on ) return; + RandomGenreEverySong = on; + ReresolveForward(); + } + + /// Let the VIBE roll per song again, or pin the live knobs. Rebuilds the forward + /// timeline so the change takes immediately; history keeps what it was. + public void SetRandomVibeEverySong( bool on ) + { + if ( RandomVibeEverySong == on ) return; + RandomVibeEverySong = on; + ReresolveForward(); + } + + // Drop the frozen line from the current song forward so upcoming songs re-resolve under whatever + // just changed; history (n < curN) keeps its frozen vibes so Prev still replays what you heard. + // Softer than StartSequence on purpose — the seed's TAG has not moved, so the songs behind you + // are still the same songs and their PCM is still worth having. + void ReresolveForward() { - if ( RandomEverySong == on ) return; - RandomEverySong = on; - // Drop the frozen line from the current song forward so upcoming songs re-resolve under the - // new mode; history (n < curN) keeps its frozen vibes so Prev still replays what you heard. var fwd = new System.Collections.Generic.List(); foreach ( var n in _ledger.Keys ) if ( n >= _curN ) fwd.Add( n ); foreach ( var n in fwd ) _ledger.Remove( n ); @@ -1045,7 +1119,13 @@ public void SetRandomEverySong( bool on ) SeekTo( _curN ); // regenerate the current song under the new mode, keeping history cached } - /// Write the playing song's raw loop (no fade) to a WAV under FileSystem.Data. + /// Is the genre written into the seed rather than rolled? What a UI's "hand it back to + /// the station" control reads, so it can be off when there is nothing to hand back. + public bool GenrePinned => !RandomGenreEverySong; + /// Is the vibe written into the seed rather than rolled? See . + public bool VibePinned => !RandomVibeEverySong; + + /// Write the playing song's raw loop (no fade) to a stereo WAV under FileSystem.Data. /// Returns the filename written, or null on failure. public string SaveCurrentToFile() { @@ -1054,7 +1134,9 @@ public string SaveCurrentToFile() var name = $"{tag}_{_curN}.wav"; try { - FileSystem.Data.WriteAllBytes( name, MusicGen.WavFromSamples( _curRaw, 1, _sr ) ); + // _curRaw is the interleaved stereo buffer the SoundStream is fed; Wav writes exactly + // that and no longer takes a channel count to disagree with it. + FileSystem.Data.WriteAllBytes( name, MusicGen.WavFromSamples( _curRaw, _sr ) ); return name; } catch ( Exception e ) @@ -1073,14 +1155,16 @@ public string SaveCurrentToFile() // Legacy pre-JSON progress file (just the song index) — still read as a fallback. string ProgressFile => $"skafinity_{(string.IsNullOrEmpty( SaveSlot ) ? "default" : SaveSlot)}.n"; + // A state file written before the genre/vibe switches split simply has neither field, so it + // loads with both rolling — which is the default a fresh install gets. Nothing to migrate. class SavedState { public string Tag { get; set; } = ""; public int N { get; set; } public string Vibe { get; set; } = ""; public int Genre { get; set; } - public bool GenrePinned { get; set; } - public bool RandomEverySong { get; set; } = true; + public bool RandomGenreEverySong { get; set; } = true; + public bool RandomVibeEverySong { get; set; } = true; public bool Enabled { get; set; } = true; public float Volume { get; set; } = 0.7f; } @@ -1092,8 +1176,8 @@ class SavedState int StateHash() { var h = new HashCode(); - h.Add( Tag ); h.Add( _curN ); h.Add( Vibe ); h.Add( Genre ); h.Add( _genrePinned ); - h.Add( RandomEverySong ); h.Add( Enabled ); h.Add( Volume ); + h.Add( Tag ); h.Add( _curN ); h.Add( Vibe ); h.Add( Genre ); + h.Add( RandomGenreEverySong ); h.Add( RandomVibeEverySong ); h.Add( Enabled ); h.Add( Volume ); return h.ToHashCode(); } @@ -1107,8 +1191,8 @@ void SaveState() N = _curN, Vibe = Vibe ?? "", Genre = Genre, - GenrePinned = _genrePinned, - RandomEverySong = RandomEverySong, + RandomGenreEverySong = RandomGenreEverySong, + RandomVibeEverySong = RandomVibeEverySong, Enabled = Enabled, Volume = Volume, } ) ); @@ -1129,8 +1213,8 @@ bool LoadState() _curN = Math.Max( 0, s.N ); Vibe = s.Vibe ?? ""; Genre = Math.Clamp( s.Genre, 0, VibeCodec.GenreCount - 1 ); - _genrePinned = s.GenrePinned; - RandomEverySong = s.RandomEverySong; + RandomGenreEverySong = s.RandomGenreEverySong; + RandomVibeEverySong = s.RandomVibeEverySong; Enabled = s.Enabled; Volume = Math.Clamp( s.Volume, 0f, 2f ); return true; diff --git a/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor b/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor index 144dbe9..e1682af 100644 --- a/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor +++ b/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor @@ -29,7 +29,8 @@ column (0 volume, then the four travelling columns). A new genre — or a new knob — is a pure engine change; there is no field table here. s&box has no slider widget, so each knob is a strip of tick cells (one per level the seed encodes); each change re-encodes the vibe and - restarts the player on a short debounce. + restarts the player on a short debounce. It sits behind the TINKER button, so the board opens + on the controls that decide what plays rather than on a wall of tick strips. Re-theming: the board derives its whole palette from one colour. Set SkafinityTheme.Accent from your game (e.g. to your own UI accent) and the board follows; leave it unset and it is @@ -49,10 +50,14 @@
-
NOW PLAYING
+
SEED — and what it leaves rolling
+ @* TWO copy buttons, because "share this" means one of two things and neither can be + recovered from the other: the song, with everything it left to chance written + down, or the station as it stands, which keeps rolling for whoever is handed it. *@
@Seed()
-
@_copyLabel
+
@_copySongLabel
+
@_copyStationLabel
@@ -138,24 +143,44 @@
+ @* The genre strip IS the seed's genre part, "Random" included: that entry takes the genre + out of the string so every song rolls its own again, and without it there is no way back + out of a genre once one has been chosen. It reads as selected when nothing is pinned. *@
GENRE
+
Random
@for ( int g = 0; g < VibeCodec.GenreCount; g++ ) { var gg = g; -
@VibeCodec.Genres[g]
} -
🎲 Reroll
-
🎲 Random every song: @( Player?.RandomEverySong == true ? "ON" : "OFF" )
+
+
+ + @* The controls that decide WHAT PLAYS, in a row of their own — they are not knob controls, + and living among the sliders made them look like part of the mixer AND hid them from + everyone who never opened it. *@ +
+
+
🎲 New station
Save .wav
+
@( _tinkering ? "Hide knobs" : "🎛 Tinker" )
+ @* The knobs live behind the tinker button. They are the deep end of the toy, and a wall of + tick strips is otherwise the first thing anybody meets. *@ + @if ( _tinkering ) + { +
VIBE — per-instrument mixer (tweak, then share the seed)
@@ -198,6 +223,20 @@
} + @* The only two buttons that act on the sliders, and they are not two dice. 🎲 always moves + every knob, because it draws a fresh vibe and PINS it. ↺ is the way back out — dragging a + knob pins the whole vibe, so without it one accidental drag turns an endless station into + one song forever — and it is off when there is nothing pinned rather than looking like a + die that did nothing. *@ +
+
+
🎲 Randomize
+
↺ Random each song
+
+
+ } + @if ( _msg != null ) {
@_msg
@@ -235,8 +274,12 @@ TextEntry _tagEntry; bool _tagInit; - string _copyLabel = "Copy"; + // Both copy buttons keep their own label so pressing one doesn't report "Copied!" on the other. + string _copySongLabel = "Copy song"; + string _copyStationLabel = "Copy station"; string _msg; + // The knob matrix is closed until asked for. Not a [Property] for the same reason IsOpen is not. + bool _tinkering; protected override void OnStart() { @@ -271,7 +314,12 @@ : e.Cached ? $"background-color:{SkafinityTheme.CellFillSoft};" : CellStyle; - string Seed() => Player?.CurrentSeed ?? "—"; + // The box shows the seed AS IT STANDS, so a station that is still rolling reads as one. The + // fully-resolved song seed is what the "copy song" button hands over. + string Seed() => Player?.StationSeed ?? "—"; + + bool GenreRolling => Player == null || !Player.GenrePinned; + bool VibeRolling => Player == null || !Player.VibePinned; /// Open/close the settings board. Convenience for hosts that want to bind a single /// action; you can also set directly. @@ -385,10 +433,19 @@ _msg = "Back to the default tag and vibe"; } - void CopySeed() + // This song, with everything it left to chance written down — whoever is handed it hears THIS, + // not whatever their own station rolls at that index. + void CopySong() { - try { Clipboard.SetText( Seed() ); _copyLabel = "Copied!"; } - catch { _copyLabel = "—"; } + try { Clipboard.SetText( Player?.CurrentSeed ?? "" ); _copySongLabel = "Copied!"; } + catch { _copySongLabel = "—"; } + } + + // The seed as it stands: whatever this player left rolling keeps rolling for them too. + void CopyStation() + { + try { Clipboard.SetText( Player?.StationSeed ?? "" ); _copyStationLabel = "Copied!"; } + catch { _copyStationLabel = "—"; } } void Save() @@ -397,18 +454,25 @@ _msg = string.IsNullOrEmpty( name ) ? "Couldn't save song" : $"Saved {name} to your s&box data folder"; } - // RerollVibe()'s defaults: the genre goes, the per-instrument volumes stay — a reroll should - // hand you a different song, not upend the mix you set. - void Reroll() { Player?.RerollVibe(); _msg = "Rerolled the genre and every knob but the volumes"; } + void ToggleTinker() { _tinkering = !_tinkering; } + + // A different SONG, not a different taste: a fresh station at song 0, with anything pinned left + // pinned. + void RerollStation() { Player?.RerollStation(); _msg = "New station"; } - void ToggleRandomEverySong() + // RerollVibe()'s defaults: the genre and the per-instrument volumes stay — the die over the + // mixer re-voices the band, it does not swap the band or upend the mix you set. + void RerollVibe() { Player?.RerollVibe(); _msg = "Threw every knob but the volumes, and pinned them"; } + + void RollVibe() { - if ( Player == null ) return; - bool on = !Player.RandomEverySong; - Player.SetRandomEverySong( on ); - _msg = on ? "Shuffle: every new song freezes a fresh vibe + genre (keeps your volumes)" : "Shuffle off"; + if ( Player == null || VibeRolling ) return; // nothing pinned — nothing to hand back + Player.RollVibe(); + _msg = "Every song rolls its own vibe again — what changes is the songs after this one"; } + void RollGenre() { Player?.RollGenre(); _msg = "Every song rolls its own genre again"; } + // ── Queue view ── // How many history / look-ahead entries to show either side of the current song. static int QueueBack => 3; @@ -438,8 +502,9 @@ var q = new HashCode(); q.Add( IsOpen ); q.Add( Player?.CurrentSeed ); q.Add( Player?.CurrentVibe ); q.Add( Player?.Enabled ?? true ); q.Add( Player?.Volume ?? 1f ); - q.Add( Player?.RandomEverySong ?? false ); q.Add( Player?.IsBuffering ?? false ); - q.Add( _msg ); q.Add( _copyLabel ); + q.Add( Player?.GenrePinned ?? false ); q.Add( Player?.VibePinned ?? false ); + q.Add( _tinkering ); q.Add( Player?.IsBuffering ?? false ); + q.Add( _msg ); q.Add( _copySongLabel ); q.Add( _copyStationLabel ); // The palette rides in inline style= values, so the board has to rebuild when the host // retints it — nothing else in this hash moves when only SkafinityTheme.Accent changes. q.Add( SkafinityTheme.Accent ); diff --git a/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor.scss b/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor.scss index 9f4c3aa..051ba20 100644 --- a/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor.scss +++ b/sbox-library/Skafinity/Code/UI/SkafinityMusicPanel.razor.scss @@ -96,6 +96,10 @@ skafinitymusicpanel { &.reroll { height: 28px; padding: 0 12px; font-size: 13px; margin-left: 12px; } &:hover { border-color: $hover; } &.toggle.on { border-color: $pick; } + // A control with nothing to act on (the vibe unpin, when nothing is pinned). Dimmed and + // inert rather than hidden — a button that comes and goes is harder to find than one that + // is visibly waiting for something. + &.off { opacity: 0.35; cursor: default; pointer-events: none; } } .cell { @@ -124,6 +128,9 @@ skafinitymusicpanel { .mhead .mlabel { font-size: 11px; letter-spacing: 2px; color: rgba(255,255,255,0.4); } .mcell { width: 0; flex-grow: 1; flex-direction: column; gap: 3px; } + // The two buttons under the matrix that act on the sliders. + .vibe-actions .cells { gap: 8px; } + .hint { font-size: 14px; letter-spacing: 1px; white-space: nowrap; } // ── Progress bar (queue entries + buffering banner) ── diff --git a/sbox-library/Skafinity/README.md b/sbox-library/Skafinity/README.md index e38a8be..41b2d5f 100644 --- a/sbox-library/Skafinity/README.md +++ b/sbox-library/Skafinity/README.md @@ -61,16 +61,23 @@ music.PrevSong(); // n-1 music.SetN( 100 ); // jump // Vibe knobs (the shareable subset of the config) -music.RerollVibe(); // randomise the vibe knobs, keep per-instrument volumes +music.RerollVibe(); // throw every knob somewhere new and PIN it (keeps volumes + genre) music.RerollVibe( includeVolumes: true, includeGenre: true ); // opt-in full shuffle (also rolls volumes + genre) music.SetVibe( 0, 0.5f ); // set field 0 of VibeCodec.Fields(genre) from a 0..1 fraction -music.SetGenre( 1 ); // switch genre (re-encodes the vibe so it sticks) -music.RandomEverySong = true; // re-roll the vibe each new song (keeps your volumes + genre) +music.SetGenre( 1 ); // pin a genre (re-encodes the vibe so it sticks) +music.RerollStation(); // a fresh random station at song 0; pins stay pinned + +// A pin is per SEED PART, the way the seed is: pin either alone, or neither. +music.RollGenre(); // hand the genre back to the station — every song rolls its own +music.RollVibe(); // ditto the vibe +music.RandomGenreEverySong = true; // the same two switches, as inspector properties +music.RandomVibeEverySong = true; string seed = music.CurrentSeed; // fully resolved — share this and they hear THIS song -var cfg = music.EffectiveConfig(); // the MusicGen.Config currently in effect +string stn = music.StationSeed; // as it stands — what it leaves rolling keeps rolling +var cfg = music.EffectiveConfig(); // the MusicGen.Config the playing song was built with -// Write the current loop to a WAV under FileSystem.Data +// Write the current loop to a stereo WAV under FileSystem.Data string file = music.SaveCurrentToFile(); ``` @@ -143,7 +150,7 @@ They're client-side, like the player itself. Delete the file if you don't want t | Group | What it does | |---|---| -| **Music** | Master `Enabled` / `Volume`, `LiveReload` (regenerate on knob change), `MixerName`, `AutoPlay`, `RandomEverySong` (shuffle) | +| **Music** | Master `Enabled` / `Volume`, `LiveReload` (regenerate on knob change), `MixerName`, `AutoPlay`, `RandomGenreEverySong` / `RandomVibeEverySong` (which seed parts keep rolling) | | **Seed** | `Tag`, `StartN`, `Vibe` override, `PersistProgress` + `SaveSlot` (resume across sessions) | | **Output** | `SampleRate` (32 kHz — below the engine's own default, since a game renders while it draws), `RenderThreads` (synthesis is split across worker threads) | | **Crossfade** | `Crossfade` window, `CrossfadeOverlap`, `AheadCount` (look-ahead depth), `PcmCacheRadius` | diff --git a/test/engine/Audition.cs b/test/engine/Audition.cs index a56b15e..1185d27 100644 --- a/test/engine/Audition.cs +++ b/test/engine/Audition.cs @@ -175,7 +175,7 @@ void One( string name, CymbalBands c ) pcm[i * 2 + 1] = (short)Math.Clamp( (int)MathF.Round( r[i] / peak * 32000f ), -32768, 32767 ); } string path = Path.Combine( dir, name + ".wav" ); - File.WriteAllBytes( path, MusicGen.WavFromSamples( pcm, 2, Rate ) ); + File.WriteAllBytes( path, MusicGen.WavFromSamples( pcm, Rate ) ); Console.WriteLine( $" {path}" ); } Console.WriteLine( "one hit per cymbal, dry and centred, peak-normalised per file:" ); @@ -206,7 +206,7 @@ static void Write( string wavPath, string txtPath, List L, List R, pcm[i * 2] = (short)Math.Clamp( (int)MathF.Round( L[i] * k * 32767f ), -32768, 32767 ); pcm[i * 2 + 1] = (short)Math.Clamp( (int)MathF.Round( R[i] * k * 32767f ), -32768, 32767 ); } - File.WriteAllBytes( wavPath, MusicGen.WavFromSamples( pcm, 2, Rate ) ); + File.WriteAllBytes( wavPath, MusicGen.WavFromSamples( pcm, Rate ) ); File.WriteAllText( txtPath, script ); Console.Write( script ); diff --git a/wasm/Exports.cs b/wasm/Exports.cs index 51b4041..2875627 100644 --- a/wasm/Exports.cs +++ b/wasm/Exports.cs @@ -62,7 +62,7 @@ internal static Span ChannelBytes( int channel ) internal static int GenerateWav( string seed, [JSMarshalAs>] double[] cfg ) { short[] s = MusicGen.GenerateSamples( seed, Cfg.From( cfg ), out int sr ); - _wav = MusicGen.WavFromSamples( s, MusicGen.Channels, sr ); + _wav = MusicGen.WavFromSamples( s, sr ); return _wav.Length; } diff --git a/web/.bundle-stamp b/web/.bundle-stamp index 1eb0105..f3655bb 100644 --- a/web/.bundle-stamp +++ b/web/.bundle-stamp @@ -1 +1 @@ -aot 40ab5935a72a5e6a71c8fbec60bc65daddf56665339dc2254abc5729a4482bd8 +aot 5cd308fcbeacf0315b06b4d0bbe4c3daad7ab343ac2b2cbe701798433a8241d8 diff --git a/web/_framework/Skafinity.Wasm.3os7pqq8i7.wasm b/web/_framework/Skafinity.Wasm.anx855lu34.wasm similarity index 92% rename from web/_framework/Skafinity.Wasm.3os7pqq8i7.wasm rename to web/_framework/Skafinity.Wasm.anx855lu34.wasm index 537bfc8..aacf136 100644 Binary files a/web/_framework/Skafinity.Wasm.3os7pqq8i7.wasm and b/web/_framework/Skafinity.Wasm.anx855lu34.wasm differ diff --git a/web/_framework/dotnet.js b/web/_framework/dotnet.js index 193565f..6394b5a 100644 --- a/web/_framework/dotnet.js +++ b/web/_framework/dotnet.js @@ -4,7 +4,7 @@ var e=!1;const t=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,6,64,25,11,11])),o=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),n=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),r=Symbol.for("wasm promise_control");function i(e,t){let o=null;const n=new Promise((function(n,r){o={isDone:!1,promise:null,resolve:t=>{o.isDone||(o.isDone=!0,n(t),e&&e())},reject:e=>{o.isDone||(o.isDone=!0,r(e),t&&t())}}}));o.promise=n;const i=n;return i[r]=o,{promise:i,promise_control:o}}function s(e){return e[r]}function a(e){e&&function(e){return void 0!==e[r]}(e)||Be(!1,"Promise is not controllable")}const l="__mono_message__",c=["debug","log","trace","warn","info","error"],d="MONO_WASM: ";let u,f,m,g,p,h;function w(e){g=e}function b(e){if(Pe.diagnosticTracing){const t="function"==typeof e?e():e;console.debug(d+t)}}function y(e,...t){console.info(d+e,...t)}function v(e,...t){console.info(e,...t)}function E(e,...t){console.warn(d+e,...t)}function _(e,...t){if(t&&t.length>0&&t[0]&&"object"==typeof t[0]){if(t[0].silent)return;if(t[0].toString)return void console.error(d+e,t[0].toString())}console.error(d+e,...t)}function x(e,t,o){return function(...n){try{let r=n[0];if(void 0===r)r="undefined";else if(null===r)r="null";else if("function"==typeof r)r=r.toString();else if("string"!=typeof r)try{r=JSON.stringify(r)}catch(e){r=r.toString()}t(o?JSON.stringify({method:e,payload:r,arguments:n.slice(1)}):[e+r,...n.slice(1)])}catch(e){m.error(`proxyConsole failed: ${e}`)}}}function j(e,t,o){f=t,g=e,m={...t};const n=`${o}/console`.replace("https://","wss://").replace("http://","ws://");u=new WebSocket(n),u.addEventListener("error",A),u.addEventListener("close",S),function(){for(const e of c)f[e]=x(`console.${e}`,T,!0)}()}function R(e){let t=30;const o=()=>{u?0==u.bufferedAmount||0==t?(e&&v(e),function(){for(const e of c)f[e]=x(`console.${e}`,m.log,!1)}(),u.removeEventListener("error",A),u.removeEventListener("close",S),u.close(1e3,e),u=void 0):(t--,globalThis.setTimeout(o,100)):e&&m&&m.log(e)};o()}function T(e){u&&u.readyState===WebSocket.OPEN?u.send(e):m.log(e)}function A(e){m.error(`[${g}] proxy console websocket error: ${e}`,e)}function S(e){m.debug(`[${g}] proxy console websocket closed: ${e}`,e)}function D(){Pe.preferredIcuAsset=O(Pe.config);let e="invariant"==Pe.config.globalizationMode;if(!e)if(Pe.preferredIcuAsset)Pe.diagnosticTracing&&b("ICU data archive(s) available, disabling invariant mode");else{if("custom"===Pe.config.globalizationMode||"all"===Pe.config.globalizationMode||"sharded"===Pe.config.globalizationMode){const e="invariant globalization mode is inactive and no ICU data archives are available";throw _(`ERROR: ${e}`),new Error(e)}Pe.diagnosticTracing&&b("ICU data archive(s) not available, using invariant globalization mode"),e=!0,Pe.preferredIcuAsset=null}const t="DOTNET_SYSTEM_GLOBALIZATION_INVARIANT",o=Pe.config.environmentVariables;if(void 0===o[t]&&e&&(o[t]="1"),void 0===o.TZ)try{const e=Intl.DateTimeFormat().resolvedOptions().timeZone||null;e&&(o.TZ=e)}catch(e){y("failed to detect timezone, will fallback to UTC")}}function O(e){var t;if((null===(t=e.resources)||void 0===t?void 0:t.icu)&&"invariant"!=e.globalizationMode){const t=e.applicationCulture||(ke?globalThis.navigator&&globalThis.navigator.languages&&globalThis.navigator.languages[0]:Intl.DateTimeFormat().resolvedOptions().locale),o=e.resources.icu;let n=null;if("custom"===e.globalizationMode){if(o.length>=1)return o[0].name}else t&&"all"!==e.globalizationMode?"sharded"===e.globalizationMode&&(n=function(e){const t=e.split("-")[0];return"en"===t||["fr","fr-FR","it","it-IT","de","de-DE","es","es-ES"].includes(e)?"icudt_EFIGS.dat":["zh","ko","ja"].includes(t)?"icudt_CJK.dat":"icudt_no_CJK.dat"}(t)):n="icudt.dat";if(n)for(let e=0;enull},url:e,arrayBuffer:()=>r,json:()=>JSON.parse(r),text:()=>{throw new Error("NotImplementedException")}}}if(o)return globalThis.fetch(e,t||{credentials:"same-origin"});if("function"==typeof read)return{ok:!0,url:e,headers:{length:0,get:()=>null},arrayBuffer:()=>new Uint8Array(read(e,"binary")),json:()=>JSON.parse(read(e,"utf8")),text:()=>read(e,"utf8")}}catch(t){return{ok:!1,url:e,status:500,headers:{length:0,get:()=>null},statusText:"ERR28: "+t,arrayBuffer:()=>{throw t},json:()=>{throw t},text:()=>{throw t}}}throw new Error("No fetch implementation available")}function I(e){return"string"!=typeof e&&Be(!1,"url must be a string"),!M(e)&&0!==e.indexOf("./")&&0!==e.indexOf("../")&&globalThis.URL&&globalThis.document&&globalThis.document.baseURI&&(e=new URL(e,globalThis.document.baseURI).toString()),e}const U=/^[a-zA-Z][a-zA-Z\d+\-.]*?:\/\//,P=/[a-zA-Z]:[\\/]/;function M(e){return Se||Ie?e.startsWith("/")||e.startsWith("\\")||-1!==e.indexOf("///")||P.test(e):U.test(e)}let L,N=0;const $=[],z=[],W=new Map,F={"js-module-threads":!0,"js-module-runtime":!0,"js-module-dotnet":!0,"js-module-native":!0,"js-module-diagnostics":!0},B={...F,"js-module-library-initializer":!0},V={...F,dotnetwasm:!0,heap:!0,manifest:!0},q={...B,manifest:!0},H={...B,dotnetwasm:!0},J={dotnetwasm:!0,symbols:!0},Z={...B,dotnetwasm:!0,symbols:!0},Q={symbols:!0};function G(e){return!("icu"==e.behavior&&e.name!=Pe.preferredIcuAsset)}function K(e,t,o){null!=t||(t=[]),Be(1==t.length,`Expect to have one ${o} asset in resources`);const n=t[0];return n.behavior=o,X(n),e.push(n),n}function X(e){V[e.behavior]&&W.set(e.behavior,e)}function Y(e){Be(V[e],`Unknown single asset behavior ${e}`);const t=W.get(e);if(t&&!t.resolvedUrl)if(t.resolvedUrl=Pe.locateFile(t.name),F[t.behavior]){const e=ge(t);e?("string"!=typeof e&&Be(!1,"loadBootResource response for 'dotnetjs' type should be a URL string"),t.resolvedUrl=e):t.resolvedUrl=ce(t.resolvedUrl,t.behavior)}else if("dotnetwasm"!==t.behavior)throw new Error(`Unknown single asset behavior ${e}`);return t}function ee(e){const t=Y(e);return Be(t,`Single asset for ${e} not found`),t}let te=!1;async function oe(){if(!te){te=!0,Pe.diagnosticTracing&&b("mono_download_assets");try{const e=[],t=[],o=(e,t)=>{!Z[e.behavior]&&G(e)&&Pe.expected_instantiated_assets_count++,!H[e.behavior]&&G(e)&&(Pe.expected_downloaded_assets_count++,t.push(se(e)))};for(const t of $)o(t,e);for(const e of z)o(e,t);Pe.allDownloadsQueued.promise_control.resolve(),Promise.all([...e,...t]).then((()=>{Pe.allDownloadsFinished.promise_control.resolve()})).catch((e=>{throw Pe.err("Error in mono_download_assets: "+e),Xe(1,e),e})),await Pe.runtimeModuleLoaded.promise;const n=async e=>{const t=await e;if(t.buffer){if(!Z[t.behavior]){t.buffer&&"object"==typeof t.buffer||Be(!1,"asset buffer must be array-like or buffer-like or promise of these"),"string"!=typeof t.resolvedUrl&&Be(!1,"resolvedUrl must be string");const e=t.resolvedUrl,o=await t.buffer,n=new Uint8Array(o);pe(t),await Ue.beforeOnRuntimeInitialized.promise,Ue.instantiate_asset(t,e,n)}}else J[t.behavior]?("symbols"===t.behavior&&(await Ue.instantiate_symbols_asset(t),pe(t)),J[t.behavior]&&++Pe.actual_downloaded_assets_count):(t.isOptional||Be(!1,"Expected asset to have the downloaded buffer"),!H[t.behavior]&&G(t)&&Pe.expected_downloaded_assets_count--,!Z[t.behavior]&&G(t)&&Pe.expected_instantiated_assets_count--)},r=[],i=[];for(const t of e)r.push(n(t));for(const e of t)i.push(n(e));Promise.all(r).then((()=>{Ce||Ue.coreAssetsInMemory.promise_control.resolve()})).catch((e=>{throw Pe.err("Error in mono_download_assets: "+e),Xe(1,e),e})),Promise.all(i).then((async()=>{Ce||(await Ue.coreAssetsInMemory.promise,Ue.allAssetsInMemory.promise_control.resolve())})).catch((e=>{throw Pe.err("Error in mono_download_assets: "+e),Xe(1,e),e}))}catch(e){throw Pe.err("Error in mono_download_assets: "+e),e}}}let ne=!1;function re(){if(ne)return;ne=!0;const e=Pe.config,t=[];if(e.assets)for(const t of e.assets)"object"!=typeof t&&Be(!1,`asset must be object, it was ${typeof t} : ${t}`),"string"!=typeof t.behavior&&Be(!1,"asset behavior must be known string"),"string"!=typeof t.name&&Be(!1,"asset name must be string"),t.resolvedUrl&&"string"!=typeof t.resolvedUrl&&Be(!1,"asset resolvedUrl could be string"),t.hash&&"string"!=typeof t.hash&&Be(!1,"asset resolvedUrl could be string"),t.pendingDownload&&"object"!=typeof t.pendingDownload&&Be(!1,"asset pendingDownload could be object"),t.isCore?$.push(t):z.push(t),X(t);else if(e.resources){const o=e.resources;o.wasmNative||Be(!1,"resources.wasmNative must be defined"),o.jsModuleNative||Be(!1,"resources.jsModuleNative must be defined"),o.jsModuleRuntime||Be(!1,"resources.jsModuleRuntime must be defined"),K(z,o.wasmNative,"dotnetwasm"),K(t,o.jsModuleNative,"js-module-native"),K(t,o.jsModuleRuntime,"js-module-runtime"),o.jsModuleDiagnostics&&K(t,o.jsModuleDiagnostics,"js-module-diagnostics");const n=(e,t,o)=>{const n=e;n.behavior=t,o?(n.isCore=!0,$.push(n)):z.push(n)};if(o.coreAssembly)for(let e=0;eglobalThis.setTimeout(e,100))),Pe.diagnosticTracing&&b(`Retrying download (2) '${e.name}' after delay`),await ae(e)}}}async function ae(e){for(;L;)await L.promise;try{++N,N==Pe.maxParallelDownloads&&(Pe.diagnosticTracing&&b("Throttling further parallel downloads"),L=i());const t=await async function(e){if(e.pendingDownload&&(e.pendingDownloadInternal=e.pendingDownload),e.pendingDownloadInternal&&e.pendingDownloadInternal.response)return e.pendingDownloadInternal.response;if(e.buffer){const t=await e.buffer;return e.resolvedUrl||(e.resolvedUrl="undefined://"+e.name),e.pendingDownloadInternal={url:e.resolvedUrl,name:e.name,response:Promise.resolve({ok:!0,arrayBuffer:()=>t,json:()=>JSON.parse(new TextDecoder("utf-8").decode(t)),text:()=>{throw new Error("NotImplementedException")},headers:{get:()=>{}}})},e.pendingDownloadInternal.response}const t=e.loadRemote&&Pe.config.remoteSources?Pe.config.remoteSources:[""];let o;for(let n of t){n=n.trim(),"./"===n&&(n="");const t=le(e,n);e.name===t?Pe.diagnosticTracing&&b(`Attempting to download '${t}'`):Pe.diagnosticTracing&&b(`Attempting to download '${t}' for ${e.name}`);try{e.resolvedUrl=t;const n=fe(e);if(e.pendingDownloadInternal=n,o=await n.response,!o||!o.ok)continue;return o}catch(e){o||(o={ok:!1,url:t,status:0,statusText:""+e});continue}}const n=e.isOptional||e.name.match(/\.pdb$/)&&Pe.config.ignorePdbLoadErrors;if(o||Be(!1,`Response undefined ${e.name}`),!n){const t=new Error(`download '${o.url}' for ${e.name} failed ${o.status} ${o.statusText}`);throw t.status=o.status,t}y(`optional download '${o.url}' for ${e.name} failed ${o.status} ${o.statusText}`)}(e);return t?(J[e.behavior]||(e.buffer=await t.arrayBuffer(),++Pe.actual_downloaded_assets_count),e):e}finally{if(--N,L&&N==Pe.maxParallelDownloads-1){Pe.diagnosticTracing&&b("Resuming more parallel downloads");const e=L;L=void 0,e.promise_control.resolve()}}}function le(e,t){let o;return null==t&&Be(!1,`sourcePrefix must be provided for ${e.name}`),e.resolvedUrl?o=e.resolvedUrl:(o=""===t?"assembly"===e.behavior||"pdb"===e.behavior?e.name:"resource"===e.behavior&&e.culture&&""!==e.culture?`${e.culture}/${e.name}`:e.name:t+e.name,o=ce(Pe.locateFile(o),e.behavior)),o&&"string"==typeof o||Be(!1,"attemptUrl need to be path or url string"),o}function ce(e,t){return Pe.modulesUniqueQuery&&q[t]&&(e+=Pe.modulesUniqueQuery),e}let de=0;const ue=new Set;function fe(e){try{e.resolvedUrl||Be(!1,"Request's resolvedUrl must be set");const t=function(e){let t=e.resolvedUrl;if(Pe.loadBootResource){const o=ge(e);if(o instanceof Promise)return o;"string"==typeof o&&(t=o)}const o={};return e.cache?o.cache=e.cache:Pe.config.disableNoCacheFetch||(o.cache="no-cache"),e.useCredentials?o.credentials="include":!Pe.config.disableIntegrityCheck&&e.hash&&(o.integrity=e.hash),Pe.fetch_like(t,o)}(e),o={name:e.name,url:e.resolvedUrl,response:t};return ue.add(e.name),o.response.then((()=>{"assembly"==e.behavior&&Pe.loadedAssemblies.push(e.name),de++,Pe.onDownloadResourceProgress&&Pe.onDownloadResourceProgress(de,ue.size)})),o}catch(t){const o={ok:!1,url:e.resolvedUrl,status:500,statusText:"ERR29: "+t,arrayBuffer:()=>{throw t},json:()=>{throw t}};return{name:e.name,url:e.resolvedUrl,response:Promise.resolve(o)}}}const me={resource:"assembly",assembly:"assembly",pdb:"pdb",icu:"globalization",vfs:"configuration",manifest:"manifest",dotnetwasm:"dotnetwasm","js-module-dotnet":"dotnetjs","js-module-native":"dotnetjs","js-module-runtime":"dotnetjs","js-module-threads":"dotnetjs"};function ge(e){var t;if(Pe.loadBootResource){const o=null!==(t=e.hash)&&void 0!==t?t:"",n=e.resolvedUrl,r=me[e.behavior];if(r){const t=Pe.loadBootResource(r,e.name,n,o,e.behavior);return"string"==typeof t?I(t):t}}}function pe(e){e.pendingDownloadInternal=null,e.pendingDownload=null,e.buffer=null,e.moduleExports=null}function he(e){let t=e.lastIndexOf("/");return t>=0&&t++,e.substring(t)}async function we(e){e&&await Promise.all((null!=e?e:[]).map((e=>async function(e){try{const t=e.name;if(!e.moduleExports){const o=ce(Pe.locateFile(t),"js-module-library-initializer");Pe.diagnosticTracing&&b(`Attempting to import '${o}' for ${e}`),e.moduleExports=await import(/*! webpackIgnore: true */o)}Pe.libraryInitializers.push({scriptName:t,exports:e.moduleExports})}catch(t){E(`Failed to import library initializer '${e}': ${t}`)}}(e))))}async function be(e,t){if(!Pe.libraryInitializers)return;const o=[];for(let n=0;nr.exports[e](...t))))}await Promise.all(o)}async function ye(e,t,o){try{await o()}catch(o){throw E(`Failed to invoke '${t}' on library initializer '${e}': ${o}`),Xe(1,o),o}}function ve(e,t){if(e===t)return e;const o={...t};return void 0!==o.assets&&o.assets!==e.assets&&(o.assets=[...e.assets||[],...o.assets||[]]),void 0!==o.resources&&(o.resources=_e(e.resources||{assembly:[],jsModuleNative:[],jsModuleRuntime:[],wasmNative:[]},o.resources)),void 0!==o.environmentVariables&&(o.environmentVariables={...e.environmentVariables||{},...o.environmentVariables||{}}),void 0!==o.runtimeOptions&&o.runtimeOptions!==e.runtimeOptions&&(o.runtimeOptions=[...e.runtimeOptions||[],...o.runtimeOptions||[]]),Object.assign(e,o)}function Ee(e,t){if(e===t)return e;const o={...t};return o.config&&(e.config||(e.config={}),o.config=ve(e.config,o.config)),Object.assign(e,o)}function _e(e,t){if(e===t)return e;const o={...t};return void 0!==o.coreAssembly&&(o.coreAssembly=[...e.coreAssembly||[],...o.coreAssembly||[]]),void 0!==o.assembly&&(o.assembly=[...e.assembly||[],...o.assembly||[]]),void 0!==o.lazyAssembly&&(o.lazyAssembly=[...e.lazyAssembly||[],...o.lazyAssembly||[]]),void 0!==o.corePdb&&(o.corePdb=[...e.corePdb||[],...o.corePdb||[]]),void 0!==o.pdb&&(o.pdb=[...e.pdb||[],...o.pdb||[]]),void 0!==o.jsModuleWorker&&(o.jsModuleWorker=[...e.jsModuleWorker||[],...o.jsModuleWorker||[]]),void 0!==o.jsModuleNative&&(o.jsModuleNative=[...e.jsModuleNative||[],...o.jsModuleNative||[]]),void 0!==o.jsModuleDiagnostics&&(o.jsModuleDiagnostics=[...e.jsModuleDiagnostics||[],...o.jsModuleDiagnostics||[]]),void 0!==o.jsModuleRuntime&&(o.jsModuleRuntime=[...e.jsModuleRuntime||[],...o.jsModuleRuntime||[]]),void 0!==o.wasmSymbols&&(o.wasmSymbols=[...e.wasmSymbols||[],...o.wasmSymbols||[]]),void 0!==o.wasmNative&&(o.wasmNative=[...e.wasmNative||[],...o.wasmNative||[]]),void 0!==o.icu&&(o.icu=[...e.icu||[],...o.icu||[]]),void 0!==o.satelliteResources&&(o.satelliteResources=function(e,t){if(e===t)return e;for(const o in t)e[o]=[...e[o]||[],...t[o]||[]];return e}(e.satelliteResources||{},o.satelliteResources||{})),void 0!==o.modulesAfterConfigLoaded&&(o.modulesAfterConfigLoaded=[...e.modulesAfterConfigLoaded||[],...o.modulesAfterConfigLoaded||[]]),void 0!==o.modulesAfterRuntimeReady&&(o.modulesAfterRuntimeReady=[...e.modulesAfterRuntimeReady||[],...o.modulesAfterRuntimeReady||[]]),void 0!==o.extensions&&(o.extensions={...e.extensions||{},...o.extensions||{}}),void 0!==o.vfs&&(o.vfs=[...e.vfs||[],...o.vfs||[]]),Object.assign(e,o)}function xe(){const e=Pe.config;if(e.environmentVariables=e.environmentVariables||{},e.runtimeOptions=e.runtimeOptions||[],e.resources=e.resources||{assembly:[],jsModuleNative:[],jsModuleWorker:[],jsModuleRuntime:[],wasmNative:[],vfs:[],satelliteResources:{}},e.assets){Pe.diagnosticTracing&&b("config.assets is deprecated, use config.resources instead");for(const t of e.assets){const o={};switch(t.behavior){case"assembly":o.assembly=[t];break;case"pdb":o.pdb=[t];break;case"resource":o.satelliteResources={},o.satelliteResources[t.culture]=[t];break;case"icu":o.icu=[t];break;case"symbols":o.wasmSymbols=[t];break;case"vfs":o.vfs=[t];break;case"dotnetwasm":o.wasmNative=[t];break;case"js-module-threads":o.jsModuleWorker=[t];break;case"js-module-runtime":o.jsModuleRuntime=[t];break;case"js-module-native":o.jsModuleNative=[t];break;case"js-module-diagnostics":o.jsModuleDiagnostics=[t];break;case"js-module-dotnet":break;default:throw new Error(`Unexpected behavior ${t.behavior} of asset ${t.name}`)}_e(e.resources,o)}}e.debugLevel,e.applicationEnvironment||(e.applicationEnvironment="Production"),e.applicationCulture&&(e.environmentVariables.LANG=`${e.applicationCulture}.UTF-8`),Ue.diagnosticTracing=Pe.diagnosticTracing=!!e.diagnosticTracing,Ue.waitForDebugger=e.waitForDebugger,Pe.maxParallelDownloads=e.maxParallelDownloads||Pe.maxParallelDownloads,Pe.enableDownloadRetry=void 0!==e.enableDownloadRetry?e.enableDownloadRetry:Pe.enableDownloadRetry}let je=!1;async function Re(e){var t;if(je)return void await Pe.afterConfigLoaded.promise;let o;try{if(e.configSrc||Pe.config&&0!==Object.keys(Pe.config).length&&(Pe.config.assets||Pe.config.resources)||(e.configSrc="dotnet.boot.js"),o=e.configSrc,je=!0,o&&(Pe.diagnosticTracing&&b("mono_wasm_load_config"),await async function(e){const t=e.configSrc,o=Pe.locateFile(t);let n=null;void 0!==Pe.loadBootResource&&(n=Pe.loadBootResource("manifest",t,o,"","manifest"));let r,i=null;if(n)if("string"==typeof n)n.includes(".json")?(i=await s(I(n)),r=await Ae(i)):r=(await import(I(n))).config;else{const e=await n;"function"==typeof e.json?(i=e,r=await Ae(i)):r=e.config}else o.includes(".json")?(i=await s(ce(o,"manifest")),r=await Ae(i)):r=(await import(ce(o,"manifest"))).config;function s(e){return Pe.fetch_like(e,{method:"GET",credentials:"include",cache:"no-cache"})}Pe.config.applicationEnvironment&&(r.applicationEnvironment=Pe.config.applicationEnvironment),ve(Pe.config,r)}(e)),xe(),await we(null===(t=Pe.config.resources)||void 0===t?void 0:t.modulesAfterConfigLoaded),await be("onRuntimeConfigLoaded",[Pe.config]),e.onConfigLoaded)try{await e.onConfigLoaded(Pe.config,Le),xe()}catch(e){throw _("onConfigLoaded() failed",e),e}xe(),Pe.afterConfigLoaded.promise_control.resolve(Pe.config)}catch(t){const n=`Failed to load config file ${o} ${t} ${null==t?void 0:t.stack}`;throw Pe.config=e.config=Object.assign(Pe.config,{message:n,error:t,isError:!0}),Xe(1,new Error(n)),t}}function Te(){return!!globalThis.navigator&&(Pe.isChromium||Pe.isFirefox)}async function Ae(e){const t=Pe.config,o=await e.json();t.applicationEnvironment||o.applicationEnvironment||(o.applicationEnvironment=e.headers.get("Blazor-Environment")||e.headers.get("DotNet-Environment")||void 0),o.environmentVariables||(o.environmentVariables={});const n=e.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES");n&&(o.environmentVariables.DOTNET_MODIFIABLE_ASSEMBLIES=n);const r=e.headers.get("ASPNETCORE-BROWSER-TOOLS");return r&&(o.environmentVariables.__ASPNETCORE_BROWSER_TOOLS=r),o}"function"!=typeof importScripts||globalThis.onmessage||(globalThis.dotnetSidecar=!0);const Se="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,De="function"==typeof importScripts,Oe=De&&"undefined"!=typeof dotnetSidecar,Ce=De&&!Oe,ke="object"==typeof window||De&&!Se,Ie=!ke&&!Se;let Ue={},Pe={},Me={},Le={},Ne={},$e=!1;const ze={},We={config:ze},Fe={mono:{},binding:{},internal:Ne,module:We,loaderHelpers:Pe,runtimeHelpers:Ue,diagnosticHelpers:Me,api:Le};function Be(e,t){if(e)return;const o="Assert failed: "+("function"==typeof t?t():t),n=new Error(o);_(o,n),Ue.nativeAbort(n)}function Ve(){return void 0!==Pe.exitCode}function qe(){return Ue.runtimeReady&&!Ve()}function He(){Ve()&&Be(!1,`.NET runtime already exited with ${Pe.exitCode} ${Pe.exitReason}. You can use runtime.runMain() which doesn't exit the runtime.`),Ue.runtimeReady||Be(!1,".NET runtime didn't start yet. Please call dotnet.create() first.")}function Je(){ke&&(globalThis.addEventListener("unhandledrejection",et),globalThis.addEventListener("error",tt))}let Ze,Qe;function Ge(e){Qe&&Qe(e),Xe(e,Pe.exitReason)}function Ke(e){Ze&&Ze(e||Pe.exitReason),Xe(1,e||Pe.exitReason)}function Xe(t,o){var n,r;const i=o&&"object"==typeof o;t=i&&"number"==typeof o.status?o.status:void 0===t?-1:t;const s=i&&"string"==typeof o.message?o.message:""+o;(o=i?o:Ue.ExitStatus?function(e,t){const o=new Ue.ExitStatus(e);return o.message=t,o.toString=()=>t,o}(t,s):new Error("Exit with code "+t+" "+s)).status=t,o.message||(o.message=s);const a=""+(o.stack||(new Error).stack);try{Object.defineProperty(o,"stack",{get:()=>a})}catch(e){}const l=!!o.silent;if(o.silent=!0,Ve())Pe.diagnosticTracing&&b("mono_exit called after exit");else{try{We.onAbort==Ke&&(We.onAbort=Ze),We.onExit==Ge&&(We.onExit=Qe),ke&&(globalThis.removeEventListener("unhandledrejection",et),globalThis.removeEventListener("error",tt)),Ue.runtimeReady?(Ue.jiterpreter_dump_stats&&Ue.jiterpreter_dump_stats(!1),0===t&&(null===(n=Pe.config)||void 0===n?void 0:n.interopCleanupOnExit)&&Ue.forceDisposeProxies(!0,!0),e&&0!==t&&(null===(r=Pe.config)||void 0===r||r.dumpThreadsOnNonZeroExit)):(Pe.diagnosticTracing&&b(`abort_startup, reason: ${o}`),function(e){Pe.allDownloadsQueued.promise_control.reject(e),Pe.allDownloadsFinished.promise_control.reject(e),Pe.afterConfigLoaded.promise_control.reject(e),Pe.wasmCompilePromise.promise_control.reject(e),Pe.runtimeModuleLoaded.promise_control.reject(e),Ue.dotnetReady&&(Ue.dotnetReady.promise_control.reject(e),Ue.afterInstantiateWasm.promise_control.reject(e),Ue.beforePreInit.promise_control.reject(e),Ue.afterPreInit.promise_control.reject(e),Ue.afterPreRun.promise_control.reject(e),Ue.beforeOnRuntimeInitialized.promise_control.reject(e),Ue.afterOnRuntimeInitialized.promise_control.reject(e),Ue.afterPostRun.promise_control.reject(e))}(o))}catch(e){E("mono_exit A failed",e)}try{l||(function(e,t){if(0!==e&&t){const e=Ue.ExitStatus&&t instanceof Ue.ExitStatus?b:_;"string"==typeof t?e(t):(void 0===t.stack&&(t.stack=(new Error).stack+""),t.message?e(Ue.stringify_as_error_with_stack?Ue.stringify_as_error_with_stack(t.message+"\n"+t.stack):t.message+"\n"+t.stack):e(JSON.stringify(t)))}!Ce&&Pe.config&&(Pe.config.logExitCode?Pe.config.forwardConsoleLogsToWS?R("WASM EXIT "+e):v("WASM EXIT "+e):Pe.config.forwardConsoleLogsToWS&&R())}(t,o),function(e){if(ke&&!Ce&&Pe.config&&Pe.config.appendElementOnExit&&document){const t=document.createElement("label");t.id="tests_done",0!==e&&(t.style.background="red"),t.innerHTML=""+e,document.body.appendChild(t)}}(t))}catch(e){E("mono_exit B failed",e)}Pe.exitCode=t,Pe.exitReason||(Pe.exitReason=o),!Ce&&Ue.runtimeReady&&We.runtimeKeepalivePop()}if(Pe.config&&Pe.config.asyncFlushOnExit&&0===t)throw(async()=>{try{await async function(){try{const e=await import(/*! webpackIgnore: true */"process"),t=e=>new Promise(((t,o)=>{e.on("error",o),e.end("","utf8",t)})),o=t(e.stderr),n=t(e.stdout);let r;const i=new Promise((e=>{r=setTimeout((()=>e("timeout")),1e3)}));await Promise.race([Promise.all([n,o]),i]),clearTimeout(r)}catch(e){_(`flushing std* streams failed: ${e}`)}}()}finally{Ye(t,o)}})(),o;Ye(t,o)}function Ye(e,t){if(Ue.runtimeReady&&Ue.nativeExit)try{Ue.nativeExit(e)}catch(e){!Ue.ExitStatus||e instanceof Ue.ExitStatus||E("set_exit_code_and_quit_now failed: "+e.toString())}if(0!==e||!ke)throw Se&&Ne.process?Ne.process.exit(e):Ue.quit&&Ue.quit(e,t),t}function et(e){ot(e,e.reason,"rejection")}function tt(e){ot(e,e.error,"error")}function ot(e,t,o){e.preventDefault();try{t||(t=new Error("Unhandled "+o)),void 0===t.stack&&(t.stack=(new Error).stack),t.stack=t.stack+"",t.silent||(_("Unhandled error:",t),Xe(1,t))}catch(e){}}!function(e){if($e)throw new Error("Loader module already loaded");$e=!0,Ue=e.runtimeHelpers,Pe=e.loaderHelpers,Me=e.diagnosticHelpers,Le=e.api,Ne=e.internal,Object.assign(Le,{INTERNAL:Ne,invokeLibraryInitializers:be}),Object.assign(e.module,{config:ve(ze,{environmentVariables:{}})});const r={mono_wasm_bindings_is_ready:!1,config:e.module.config,diagnosticTracing:!1,nativeAbort:e=>{throw e||new Error("abort")},nativeExit:e=>{throw new Error("exit:"+e)}},l={gitHash:"f7d90799ce4ef09a0bb257852a57248d2a8fb8dd",config:e.module.config,diagnosticTracing:!1,maxParallelDownloads:16,enableDownloadRetry:!0,_loaded_files:[],loadedFiles:[],loadedAssemblies:[],libraryInitializers:[],workerNextNumber:1,actual_downloaded_assets_count:0,actual_instantiated_assets_count:0,expected_downloaded_assets_count:0,expected_instantiated_assets_count:0,afterConfigLoaded:i(),allDownloadsQueued:i(),allDownloadsFinished:i(),wasmCompilePromise:i(),runtimeModuleLoaded:i(),loadingWorkers:i(),is_exited:Ve,is_runtime_running:qe,assert_runtime_running:He,mono_exit:Xe,createPromiseController:i,getPromiseController:s,assertIsControllablePromise:a,mono_download_assets:oe,resolve_single_asset_path:ee,setup_proxy_console:j,set_thread_prefix:w,installUnhandledErrorHandler:Je,retrieve_asset_download:ie,invokeLibraryInitializers:be,isDebuggingSupported:Te,exceptions:t,simd:n,relaxedSimd:o};Object.assign(Ue,r),Object.assign(Pe,l)}(Fe);let nt,rt,it,st=!1,at=!1;async function lt(e){if(!at){if(at=!0,ke&&Pe.config.forwardConsoleLogsToWS&&void 0!==globalThis.WebSocket&&j("main",globalThis.console,globalThis.location.origin),We||Be(!1,"Null moduleConfig"),Pe.config||Be(!1,"Null moduleConfig.config"),"function"==typeof e){const t=e(Fe.api);if(t.ready)throw new Error("Module.ready couldn't be redefined.");Object.assign(We,t),Ee(We,t)}else{if("object"!=typeof e)throw new Error("Can't use moduleFactory callback of createDotnetRuntime function.");Ee(We,e)}await async function(e){if(Se){const e=await import(/*! webpackIgnore: true */"process"),t=14;if(e.versions.node.split(".")[0]0&&(Pe.modulesUniqueQuery=t.substring(o)),Pe.scriptUrl=t.replace(/\\/g,"/").replace(/[?#].*/,""),Pe.scriptDirectory=(n=Pe.scriptUrl).slice(0,n.lastIndexOf("/"))+"/",Pe.locateFile=e=>"URL"in globalThis&&globalThis.URL!==C?new URL(e,Pe.scriptDirectory).toString():M(e)?e:Pe.scriptDirectory+e,Pe.fetch_like=k,Pe.out=console.log,Pe.err=console.error,Pe.onDownloadResourceProgress=e.onDownloadResourceProgress,ke&&globalThis.navigator){const e=globalThis.navigator,t=e.userAgentData&&e.userAgentData.brands;t&&t.length>0?Pe.isChromium=t.some((e=>"Google Chrome"===e.brand||"Microsoft Edge"===e.brand||"Chromium"===e.brand)):e.userAgent&&(Pe.isChromium=e.userAgent.includes("Chrome"),Pe.isFirefox=e.userAgent.includes("Firefox"))}Ne.require=Se?await import(/*! webpackIgnore: true */"module").then((e=>e.createRequire(/*! webpackIgnore: true */import.meta.url))):Promise.resolve((()=>{throw new Error("require not supported")})),void 0===globalThis.URL&&(globalThis.URL=C)}(We)}}async function ct(e){return await lt(e),Ze=We.onAbort,Qe=We.onExit,We.onAbort=Ke,We.onExit=Ge,We.ENVIRONMENT_IS_PTHREAD?async function(){(function(){const e=new MessageChannel,t=e.port1,o=e.port2;t.addEventListener("message",(e=>{var n,r;n=JSON.parse(e.data.config),r=JSON.parse(e.data.monoThreadInfo),st?Pe.diagnosticTracing&&b("mono config already received"):(ve(Pe.config,n),Ue.monoThreadInfo=r,xe(),Pe.diagnosticTracing&&b("mono config received"),st=!0,Pe.afterConfigLoaded.promise_control.resolve(Pe.config),ke&&n.forwardConsoleLogsToWS&&void 0!==globalThis.WebSocket&&Pe.setup_proxy_console("worker-idle",console,globalThis.location.origin)),t.close(),o.close()}),{once:!0}),t.start(),self.postMessage({[l]:{monoCmd:"preload",port:o}},[o])})(),await Pe.afterConfigLoaded.promise,function(){const e=Pe.config;e.assets||Be(!1,"config.assets must be defined");for(const t of e.assets)X(t),Q[t.behavior]&&z.push(t)}(),setTimeout((async()=>{try{await oe()}catch(e){Xe(1,e)}}),0);const e=dt(),t=await Promise.all(e);return await ut(t),We}():async function(){var e;await Re(We),re();const t=dt();(async function(){try{const e=ee("dotnetwasm");await se(e),e&&e.pendingDownloadInternal&&e.pendingDownloadInternal.response||Be(!1,"Can't load dotnet.native.wasm");const t=await e.pendingDownloadInternal.response,o=t.headers&&t.headers.get?t.headers.get("Content-Type"):void 0;let n;if("function"==typeof WebAssembly.compileStreaming&&"application/wasm"===o)n=await WebAssembly.compileStreaming(t);else{ke&&"application/wasm"!==o&&E('WebAssembly resource does not have the expected content type "application/wasm", so falling back to slower ArrayBuffer instantiation.');const e=await t.arrayBuffer();Pe.diagnosticTracing&&b("instantiate_wasm_module buffered"),n=Ie?await Promise.resolve(new WebAssembly.Module(e)):await WebAssembly.compile(e)}e.pendingDownloadInternal=null,e.pendingDownload=null,e.buffer=null,e.moduleExports=null,Pe.wasmCompilePromise.promise_control.resolve(n)}catch(e){Pe.wasmCompilePromise.promise_control.reject(e)}})(),setTimeout((async()=>{try{D(),await oe()}catch(e){Xe(1,e)}}),0);const o=await Promise.all(t);return await ut(o),await Ue.dotnetReady.promise,await we(null===(e=Pe.config.resources)||void 0===e?void 0:e.modulesAfterRuntimeReady),await be("onRuntimeReady",[Fe.api]),Le}()}function dt(){const e=ee("js-module-runtime"),t=ee("js-module-native");if(nt&&rt)return[nt,rt,it];"object"==typeof e.moduleExports?nt=e.moduleExports:(Pe.diagnosticTracing&&b(`Attempting to import '${e.resolvedUrl}' for ${e.name}`),nt=import(/*! webpackIgnore: true */e.resolvedUrl)),"object"==typeof t.moduleExports?rt=t.moduleExports:(Pe.diagnosticTracing&&b(`Attempting to import '${t.resolvedUrl}' for ${t.name}`),rt=import(/*! webpackIgnore: true */t.resolvedUrl));const o=Y("js-module-diagnostics");return o&&("object"==typeof o.moduleExports?it=o.moduleExports:(Pe.diagnosticTracing&&b(`Attempting to import '${o.resolvedUrl}' for ${o.name}`),it=import(/*! webpackIgnore: true */o.resolvedUrl))),[nt,rt,it]}async function ut(e){const{initializeExports:t,initializeReplacements:o,configureRuntimeStartup:n,configureEmscriptenStartup:r,configureWorkerStartup:i,setRuntimeGlobals:s,passEmscriptenInternals:a}=e[0],{default:l}=e[1],c=e[2];s(Fe),t(Fe),c&&c.setRuntimeGlobals(Fe),await n(We),Pe.runtimeModuleLoaded.promise_control.resolve(),l((e=>(Object.assign(We,{ready:e.ready,__dotnet_runtime:{initializeReplacements:o,configureEmscriptenStartup:r,configureWorkerStartup:i,passEmscriptenInternals:a}}),We))).catch((e=>{if(e.message&&e.message.toLowerCase().includes("out of memory"))throw new Error(".NET runtime has failed to start, because too much memory was requested. Please decrease the memory by adjusting EmccMaximumHeapSize. See also https://aka.ms/dotnet-wasm-features");throw e}))}const ft=new class{withModuleConfig(e){try{return Ee(We,e),this}catch(e){throw Xe(1,e),e}}withOnConfigLoaded(e){try{return Ee(We,{onConfigLoaded:e}),this}catch(e){throw Xe(1,e),e}}withConsoleForwarding(){try{return ve(ze,{forwardConsoleLogsToWS:!0}),this}catch(e){throw Xe(1,e),e}}withExitOnUnhandledError(){try{return ve(ze,{exitOnUnhandledError:!0}),Je(),this}catch(e){throw Xe(1,e),e}}withAsyncFlushOnExit(){try{return ve(ze,{asyncFlushOnExit:!0}),this}catch(e){throw Xe(1,e),e}}withExitCodeLogging(){try{return ve(ze,{logExitCode:!0}),this}catch(e){throw Xe(1,e),e}}withElementOnExit(){try{return ve(ze,{appendElementOnExit:!0}),this}catch(e){throw Xe(1,e),e}}withInteropCleanupOnExit(){try{return ve(ze,{interopCleanupOnExit:!0}),this}catch(e){throw Xe(1,e),e}}withDumpThreadsOnNonZeroExit(){try{return ve(ze,{dumpThreadsOnNonZeroExit:!0}),this}catch(e){throw Xe(1,e),e}}withWaitingForDebugger(e){try{return ve(ze,{waitForDebugger:e}),this}catch(e){throw Xe(1,e),e}}withInterpreterPgo(e,t){try{return ve(ze,{interpreterPgo:e,interpreterPgoSaveDelay:t}),ze.runtimeOptions?ze.runtimeOptions.push("--interp-pgo-recording"):ze.runtimeOptions=["--interp-pgo-recording"],this}catch(e){throw Xe(1,e),e}}withConfig(e){try{return ve(ze,e),this}catch(e){throw Xe(1,e),e}}withConfigSrc(e){try{return e&&"string"==typeof e||Be(!1,"must be file path or URL"),Ee(We,{configSrc:e}),this}catch(e){throw Xe(1,e),e}}withVirtualWorkingDirectory(e){try{return e&&"string"==typeof e||Be(!1,"must be directory path"),ve(ze,{virtualWorkingDirectory:e}),this}catch(e){throw Xe(1,e),e}}withEnvironmentVariable(e,t){try{const o={};return o[e]=t,ve(ze,{environmentVariables:o}),this}catch(e){throw Xe(1,e),e}}withEnvironmentVariables(e){try{return e&&"object"==typeof e||Be(!1,"must be dictionary object"),ve(ze,{environmentVariables:e}),this}catch(e){throw Xe(1,e),e}}withDiagnosticTracing(e){try{return"boolean"!=typeof e&&Be(!1,"must be boolean"),ve(ze,{diagnosticTracing:e}),this}catch(e){throw Xe(1,e),e}}withDebugging(e){try{return null!=e&&"number"==typeof e||Be(!1,"must be number"),ve(ze,{debugLevel:e}),this}catch(e){throw Xe(1,e),e}}withApplicationArguments(...e){try{return e&&Array.isArray(e)||Be(!1,"must be array of strings"),ve(ze,{applicationArguments:e}),this}catch(e){throw Xe(1,e),e}}withRuntimeOptions(e){try{return e&&Array.isArray(e)||Be(!1,"must be array of strings"),ze.runtimeOptions?ze.runtimeOptions.push(...e):ze.runtimeOptions=e,this}catch(e){throw Xe(1,e),e}}withMainAssembly(e){try{return ve(ze,{mainAssemblyName:e}),this}catch(e){throw Xe(1,e),e}}withApplicationArgumentsFromQuery(){try{if(!globalThis.window)throw new Error("Missing window to the query parameters from");if(void 0===globalThis.URLSearchParams)throw new Error("URLSearchParams is supported");const e=new URLSearchParams(globalThis.window.location.search).getAll("arg");return this.withApplicationArguments(...e)}catch(e){throw Xe(1,e),e}}withApplicationEnvironment(e){try{return ve(ze,{applicationEnvironment:e}),this}catch(e){throw Xe(1,e),e}}withApplicationCulture(e){try{return ve(ze,{applicationCulture:e}),this}catch(e){throw Xe(1,e),e}}withResourceLoader(e){try{return Pe.loadBootResource=e,this}catch(e){throw Xe(1,e),e}}async download(){try{await async function(){lt(We),await Re(We),re(),D(),oe(),await Pe.allDownloadsFinished.promise}()}catch(e){throw Xe(1,e),e}}async create(){try{return this.instance||(this.instance=await async function(){return await ct(We),Fe.api}()),this.instance}catch(e){throw Xe(1,e),e}}async run(){try{return We.config||Be(!1,"Null moduleConfig.config"),this.instance||await this.create(),this.instance.runMainAndExit()}catch(e){throw Xe(1,e),e}}},mt=Xe,gt=ct;Ie||"function"==typeof globalThis.URL||Be(!1,"This browser/engine doesn't support URL API. Please use a modern version. See also https://aka.ms/dotnet-wasm-features"),"function"!=typeof globalThis.BigInt64Array&&Be(!1,"This browser/engine doesn't support BigInt64Array API. Please use a modern version. See also https://aka.ms/dotnet-wasm-features"),ft.withConfig(/*json-start*/{ "mainAssemblyName": "Skafinity.Wasm", "resources": { - "hash": "sha256-lEa2TMyDOAYRShrkm/qZM7mN86RrFy6y0r0U6OObCwg=", + "hash": "sha256-q8R9A4oUlPQz0UiCTMwNtJK0UxgO4e9fJyKS2OQ45JI=", "jsModuleNative": [ { "name": "dotnet.native.4e2bmm9y7e.js" @@ -17,16 +17,16 @@ var e=!1;const t=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0, ], "wasmNative": [ { - "name": "dotnet.native.j23d7qb5x6.wasm", - "hash": "sha256-NbCYys8kYqixXjVFjOvDoOrVOByiO7CS/CYjBK/Q8OQ=", + "name": "dotnet.native.cb6ghqwbss.wasm", + "hash": "sha256-+JzO0UiwFWBD4BMN/Hv6LHGj/ibUlvHv+a43NGLpcFo=", "cache": "force-cache" } ], "coreAssembly": [ { "virtualPath": "Skafinity.Wasm.wasm", - "name": "Skafinity.Wasm.3os7pqq8i7.wasm", - "hash": "sha256-HXKtwZ5Lkmv1v63HAR60w+e8d36eFLOW2y1SN285Jw4=", + "name": "Skafinity.Wasm.anx855lu34.wasm", + "hash": "sha256-qnrp2RlPEWa+aI19EeVXBoG3yCw3dfbjurmfph8HncM=", "cache": "force-cache" }, { diff --git a/web/_framework/dotnet.native.j23d7qb5x6.wasm b/web/_framework/dotnet.native.cb6ghqwbss.wasm similarity index 99% rename from web/_framework/dotnet.native.j23d7qb5x6.wasm rename to web/_framework/dotnet.native.cb6ghqwbss.wasm index fceb395..36abc93 100755 Binary files a/web/_framework/dotnet.native.j23d7qb5x6.wasm and b/web/_framework/dotnet.native.cb6ghqwbss.wasm differ