🔍 s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
🔗 Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=dogecoin.ssoundboard&take=20
Showing code results for query:
*
(118 total matches found)
Game
game
ColorHueControl
{
gap: 0.5rem;
flex-grow: 1;
pointer-events: all;
background: linear-gradient( to right, red, yellow, lime, cyan, blue, magenta, red );
border-radius: 4px;
padding: 2px;
height: 12px;
position: relative;
cursor: pointer;
margin: 10px 0;
border: 1px solid #333;
&:hover
{
border: 1px solid #08f;
}
&:active
{
border: 1px solid #fff;
}
.handle
{
top: -5px;
bottom: -5px;
aspect-ratio: 1;
border-radius: 100px;
border: 2px solid #444;
position: absolute;
background-color: white;
box-shadow: 2px 2px 16px #000a;
transform: translateX( -50% );
pointer-events: none;
}
}
Game
game
RootPanel
{
font-family: Roboto;
color: white;
}
IconPanel, i
{
font-family: Material Icons;
background-position: center;
background-size: contain;
}
.tooltip
{
background-color: rgba( black, 0.8 );
border: 1px solid rgba( #555, 0.1 );
padding: 10px;
border-radius: 4px;
color: white;
font-family: Inter;
font-weight: bold;
transition: opacity 0.09s ease;
opacity: 1;
margin: 0px;
&:intro, &:outro
{
opacity: 0;
}
}
.textentry
{
cursor: text;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow: hidden;
flex-direction: row;
position: relative;
padding: 10px;
&.disabled
{
cursor: default;
}
&.is-multiline
{
align-items: flex-start;
white-space: normal;
}
.content-label,
.placeholder
{
flex-grow: 1;
}
.placeholder
{
opacity: 0.2;
pointer-events: none;
}
.iconpanel
{
position: absolute;
right: 0;
padding-right: 10px;
pointer-events: none;
&.clearbutton
{
color: #fff;
pointer-events: all;
cursor: pointer;
opacity: 0.1;
&:hover
{
opacity: 0.5;
}
&:active
{
opacity: 1;
}
}
}
&:empty
{
.clearbutton
{
display: none;
}
}
&.has-icon
{
padding-right: 30px;
}
.content-label,
.prefix-label,
.suffix-label,
.placeholder
{
overflow: hidden;
}
.content-label
{
order: 1;
}
.suffix-label
{
order: 2;
}
.prefix-label,
.suffix-label
{
flex-shrink: 0;
opacity: 0.5;
}
&.invalid
{
box-shadow: 0px 0px 1px 2px #f8356b;
}
}
.popup-panel
{
background-color: #1b202c;
flex-direction: column;
color: #8e9ec2;
transition: all 0.1s ease-out;
position: absolute;
z-index: 2000;
min-height: 20px;
min-width: 10px;
overflow: scroll;
pointer-events: all;
box-shadow: 3px 3px 64px #000;
border-radius: 4px;
&.medium
{
max-width: 300px;
}
&.center-horizontal
{
transform-origin: top center;
}
&.below-center
{
transform: translateX( -50% );
transform-origin: 0% 0%;
}
&.above-left
{
transform-origin: 0% 100%;
}
&.above-center
{
transform: translateX( -50% );
transform-origin: 0% 100%;
}
&.left
{
transform-origin: 100% 50%;
transform: translate( 00% -50% );
}
&.right
{
transform-origin: 0 50%;
transform: translate( 0% -50% );
}
&:intro
{
transform: scale( 0 );
pointer-events: none;
opacity: 0;
}
&:outro
{
opacity: 0;
transition: all 0.1s ease-in;
margin-top: 50px;
pointer-events: none;
}
&.below-stretch
{
transform-origin: 0% 0%;
&:intro
{
transform: scaleY( 0.1 );
}
}
> .canvas
{
flex-direction: column;
}
.information
{
padding: 16px;
font-size: 14px;
opacity: 0.5;
}
button
{
padding: 2px 8px;
cursor: pointer;
font-family: Poppins;
font-weight: 600;
flex-shrink: 0;
&:hover
{
background-color: #08f;
color: #fff;
}
}
}
Game
game
@import "/ui/styles/_theme.scss";
.anchor-pad {
width: 130px;
height: 90px;
background-color: $bg-2;
border: 1px solid $line-strong;
padding: 6px;
position: relative;
.anchor-cell {
pointer-events: all;
background-color: rgba(255,255,255,0.04);
border: 1px solid $line-strong;
cursor: pointer;
position: absolute;
width: 34px;
height: 22px;
transition: background-color 0.1s ease, border-color 0.1s ease;
&:hover { background-color: rgba(255,255,255,0.1); border-color: $accent; }
&.active {
background-color: $accent;
border-color: $accent;
}
&.anchor-topleft { left: 6px; top: 6px; }
&.anchor-topright { right: 6px; top: 6px; }
&.anchor-bottomleft { left: 6px; bottom: 6px; }
&.anchor-bottomright { right: 6px; bottom: 6px; }
&.anchor-center { left: 50%; top: 50%; transform: translate(-50%, -50%); }
}
}
Game
game
@import "/ui/styles/_theme.scss";
.hint-box {
padding: 14px 16px;
background-color: rgba(98, 200, 255, 0.07);
border-left: 3px solid $info;
color: $text-dim;
font-size: $fs-3;
font-weight: 500;
letter-spacing: 0.2px;
code { color: $info; font-family: $font-mono; font-weight: 700; }
}
Game
game
@import "/ui/styles/_theme.scss";
.tile {
flex-direction: column;
width: 140px;
padding: 0;
background-color: $bg-2;
border: 1px solid $line-strong;
overflow: hidden;
cursor: pointer;
transition: border-color 0.1s ease;
.tile-swatch {
width: 100%;
height: 80px;
position: relative;
overflow: hidden;
background-color: #1a1f2e;
background-size: cover;
background-position: center;
}
.tile-thumb {
width: 100%;
height: 100%;
object-fit: contain;
}
.tile-placeholder {
width: 100%;
height: 100%;
}
.tile-vignette {
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
background-color: rgba(0,0,0,0.25);
pointer-events: none;
}
.tile-label {
height: 42px;
padding: 8px 10px;
flex-shrink: 0;
align-items: center;
justify-content: center;
font-size: $fs-1;
font-weight: 700;
color: $text-dim;
text-align: center;
font-family: $font-mono;
letter-spacing: 1px;
text-transform: uppercase;
border-top: 1px solid $line;
}
&:hover {
border-color: $accent;
.tile-label { color: $text; }
}
&.active {
border-color: $accent;
.tile-label { color: $text; background-color: $accent-soft; font-weight: 800; }
}
}
Game
game
@import "/ui/styles/_theme.scss";
@import "/ui/components/field/field.scss";
.v3-field {
.v3-row {
flex-direction: row;
gap: 6px;
align-items: stretch;
width: 100%;
}
}
Game
game
PackageFlairBar
{
position: absolute;
top: 6px;
left: 6px;
z-index: 2;
flex-direction: row;
gap: 5px;
pointer-events: none;
.flair
{
position: relative;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
pointer-events: all;
border-radius: 4px;
color: white;
border: 1px solid rgba( white, 0.1 );
outline: 1px solid #0006;
opacity: 0.7;
&:hover
{
opacity: 1;
}
icon
{
font-size: 15px;
}
}
}
Game
game
@import "/UI/Styles/_Theme.scss";
@import "/UI/Styles/_Fonts.scss";
@import "/UI/Components/Tile/Tile.scss";
@import "/UI/Components/Button/Button.scss";
SoundBoardPanel
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: all;
overflow: hidden;
.sb
{
width: 100%;
height: 100%;
flex-direction: column;
background-color: $bg-0;
overflow: hidden;
}
.sb-shell
{
width: 100%;
height: 100%;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
min-height: 0;
padding: 20px 24px 18px;
overflow: hidden;
}
.sb-topbar
{
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
height: 64px;
min-height: 64px;
max-height: 64px;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid $line;
flex-shrink: 0;
flex-wrap: nowrap;
overflow: hidden;
}
.sb-brand-block
{
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
flex-grow: 1;
}
.sb-status-led
{
width: 10px;
height: 10px;
border-radius: 50%;
background-color: $bg-2;
border: 1px solid $line;
flex-shrink: 0;
&.is-live
{
background-color: $success;
border-color: $success-line;
}
}
.sb-brand-copy
{
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.sb-eyebrow
{
font-size: $fs-1;
font-weight: 700;
letter-spacing: $ls-xwide;
color: $text-faint;
font-family: $font-mono;
}
.sb-title
{
font-size: $fs-5;
font-weight: 800;
color: $text;
font-family: $font-primary;
white-space: nowrap;
overflow: hidden;
}
.sb-voice-chip
{
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
width: 128px;
height: 52px;
min-width: 128px;
max-width: 128px;
padding: 8px 12px;
border-radius: 4px;
background-color: $bg-2;
border: 1px solid $line-strong;
flex-shrink: 0;
overflow: hidden;
}
.sb-voice-chip-head
{
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.sb-voice-chip-label
{
font-size: $fs-1;
font-weight: 700;
letter-spacing: $ls-wide;
color: $text-faint;
font-family: $font-mono;
}
.sb-voice-chip-value
{
font-size: $fs-4;
font-weight: 800;
line-height: 1;
color: $accent;
font-family: $font-mono;
}
.sb-voice-meter-stack
{
width: 100%;
height: 18px;
min-height: 18px;
max-height: 18px;
flex-direction: column;
justify-content: flex-end;
gap: 2px;
overflow: hidden;
}
.sb-voice-meter-tier
{
width: 100%;
height: 3px;
min-height: 3px;
max-height: 3px;
border-radius: 999px;
background-color: $bg-inset;
overflow: hidden;
}
.sb-voice-meter-fill
{
height: 100%;
border-radius: 999px;
transition: width 0.16s ease;
}
.sb-tabs-bar
{
display: flex;
flex-direction: row;
flex-shrink: 0;
align-items: center;
gap: 8px;
width: 100%;
height: 48px;
margin-bottom: 10px;
pointer-events: all;
}
.sb-search-bar
{
display: flex;
flex-direction: row;
flex-shrink: 0;
align-items: center;
gap: 10px;
width: 100%;
height: 40px;
margin-bottom: 12px;
padding: 0 12px;
border-radius: 8px;
background-color: $bg-2;
border: 1px solid $line-strong;
pointer-events: all;
}
.sb-search-icon
{
flex-shrink: 0;
font-size: 16px;
font-weight: 700;
color: $text-faint;
line-height: 1;
}
.sb-search-input
{
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
height: 36px;
min-height: 36px;
padding: 0 4px;
background-color: transparent;
border: 0;
color: $text;
font-size: $fs-2;
font-weight: 600;
font-family: $font-mono;
letter-spacing: 0.04em;
}
.sb-search-clear
{
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
background-color: $bg-inset;
border: 1px solid $line;
color: $text-dim;
font-size: 12px;
font-weight: 800;
cursor: pointer;
pointer-events: all;
&:hover
{
border-color: $accent;
color: $accent;
}
&:active
{
sound-in: "ui.button.press";
transform: scale(0.96);
}
}
.sb-search-count
{
flex-shrink: 0;
min-width: 36px;
text-align: right;
font-size: $fs-1;
font-weight: 700;
letter-spacing: $ls-wide;
color: $text-faint;
font-family: $font-mono;
white-space: nowrap;
}
.sb-empty
{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
min-height: 180px;
padding: 32px 16px;
pointer-events: none;
}
.sb-empty-title
{
font-size: $fs-4;
font-weight: 800;
color: $text-dim;
font-family: $font-mono;
letter-spacing: 0.08em;
}
.sb-empty-body
{
font-size: $fs-2;
font-weight: 500;
color: $text-faint;
font-family: $font-mono;
text-align: center;
}
.sb-tabs-arrow
{
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 36px;
height: 40px;
border-radius: 8px;
background-color: $bg-2;
border: 1px solid $line-strong;
color: $text;
font-size: 22px;
font-weight: 800;
cursor: pointer;
pointer-events: all;
z-index: 2;
&:hover
{
border-color: $accent;
color: $accent;
}
&:active
{
sound-in: "ui.button.press";
transform: scale(0.96);
}
&.is-disabled
{
opacity: 0.35;
cursor: default;
pointer-events: none;
}
}
.sb-tabs-scroll
{
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
height: 100%;
// Clip the track; scrolling is done via margin-left on .sb-tabs-track
// because Panel.ScrollSize often stays 0 for horizontal flex content.
overflow: hidden;
pointer-events: all;
}
.sb-tabs-track
{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
// Must grow with chips so content width > viewport (enables overflow).
width: auto;
flex-shrink: 0;
height: 100%;
padding: 2px 2px 4px;
// margin-left is driven from code for arrow / wheel scrolling.
}
.sb-tab-chip
{
display: flex;
flex-direction: row;
flex-shrink: 0;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 6px;
background-color: $bg-2;
border: 1px solid $line-strong;
cursor: pointer;
pointer-events: all;
white-space: nowrap;
&:hover
{
border-color: $accent;
}
&.is-active
{
background-color: $accent;
border-color: $accent;
color: $accent-ink;
.sb-tab-chip-label
{
color: $accent-ink;
}
}
&.is-search
{
background-color: #2a6df4;
border-color: #5b8dff;
color: #fff;
.sb-tab-chip-label
{
color: #fff;
}
}
&:active
{
sound-in: "ui.button.press";
transform: scale(0.97);
}
}
.sb-tab-chip-icon
{
font-size: 13px;
flex-shrink: 0;
}
.sb-tab-chip-label
{
font-size: $fs-2;
font-weight: 700;
font-family: $font-mono;
letter-spacing: 1.2px;
color: $text-dim;
white-space: nowrap;
flex-shrink: 0;
}
.sb-board
{
flex-grow: 1;
flex-shrink: 1;
min-height: 0;
margin-bottom: 14px;
border: 1px solid $line;
background-color: $bg-1;
flex-direction: column;
overflow: hidden;
pointer-events: all;
}
.sb-board-scroll
{
flex-grow: 1;
flex-shrink: 1;
min-height: 0;
flex-direction: column;
overflow-y: scroll;
overflow-x: hidden;
pointer-events: all;
}
.sb-pad-content
{
flex-direction: column;
flex-shrink: 0;
width: 100%;
pointer-events: all;
}
.tile-grid
{
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
width: 100%;
flex-shrink: 0;
gap: 12px;
padding: 8px;
pointer-events: all;
.tile
{
pointer-events: all;
flex-shrink: 0;
}
}
.sb-pad-grid
{
padding: 4px 4px 24px;
}
.tile
{
width: 112px;
&.is-loading
{
border-color: $line-strong;
.tile-swatch
{
opacity: 0.55;
}
.tile-label
{
color: $text-dim;
}
}
// Lobby co-op: pad lit by someone in the lobby (avatar + username).
&.is-remote
{
border-color: #5b8dff;
.tile-label
{
color: #e8f0ff;
background-color: rgba(42, 109, 244, 0.45);
font-weight: 800;
}
}
}
.tile-swatch
{
height: 64px;
position: relative;
}
.tile-remote-avatar
{
position: absolute;
top: 4px;
right: 4px;
width: 28px;
height: 28px;
border-radius: 50%;
background-size: cover;
background-position: center;
background-color: #0b1220;
border: 2px solid #ffffff;
z-index: 3;
pointer-events: none;
}
.tile-remote-name
{
position: absolute;
left: 3px;
right: 3px;
bottom: 3px;
padding: 2px 4px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.72);
color: #ffffff;
font-size: 9px;
font-weight: 800;
font-family: $font-mono;
letter-spacing: 0.04em;
text-align: center;
text-transform: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
z-index: 3;
pointer-events: none;
}
.sb-controls
{
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
flex-shrink: 0;
min-height: 56px;
}
// LOOP + CO-OP share one baseline.
.sb-toggle-row
{
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
gap: 10px;
}
.sb-toggle
{
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 100px;
height: 44px;
min-width: 100px;
min-height: 44px;
max-height: 44px;
padding: 0 12px;
border-radius: 10px;
font-size: 13px;
font-weight: 900;
letter-spacing: 0.16em;
color: #ffffff;
cursor: pointer;
pointer-events: all;
// s&box UI: use border shorthand only (border-style is invalid).
border: 3px solid #2a0000;
background-color: #c10f0f;
box-shadow: 0 3px 0 #3a0000, inset 0 1px 4px rgba(255, 255, 255, 0.2);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
&.is-on
{
border: 3px solid #0a3d14;
background-color: #1fbf3a;
box-shadow: 0 3px 0 #0f5a1c, 0 0 14px rgba(57, 255, 80, 0.35), inset 0 1px 4px rgba(255, 255, 255, 0.25);
}
&.is-off
{
border: 3px solid #2a0000;
background-color: #c10f0f;
box-shadow: 0 3px 0 #3a0000, inset 0 1px 4px rgba(255, 255, 255, 0.2);
}
// CO-OP on uses same green as LOOP (is-on); is-coop is optional marker.
&.is-on.is-coop
{
border: 3px solid #0a3d14;
background-color: #1fbf3a;
box-shadow: 0 3px 0 #0f5a1c, 0 0 14px rgba(57, 255, 80, 0.35), inset 0 1px 4px rgba(255, 255, 255, 0.25);
}
&:active
{
sound-in: "ui.button.press";
transform: translateY(2px);
}
}
.sb-now-playing
{
flex-grow: 1;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
padding: 10px 14px;
background-color: $bg-2;
border: 1px solid $line;
}
.sb-now-playing-dot
{
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $line-strong;
flex-shrink: 0;
&.is-live
{
background-color: $accent;
}
}
.sb-now-playing-text
{
font-size: $fs-2;
font-weight: 600;
color: $text-dim;
font-family: $font-mono;
white-space: nowrap;
overflow: hidden;
}
.sb-kill-btn
{
min-width: 148px;
flex-shrink: 0;
}
&.is-wide
{
.tile
{
width: 120px;
}
}
&.is-medium
{
.sb-shell
{
padding: 18px 20px 16px;
}
.sb-title
{
font-size: $fs-4;
}
.tile
{
width: 104px;
}
}
&.is-compact
{
.sb-shell
{
padding: 14px 14px 12px;
}
.sb-voice-chip
{
width: 116px;
min-width: 116px;
max-width: 116px;
}
.tile
{
width: 96px;
}
.tile-swatch
{
height: 56px;
}
}
&.is-tight
{
.sb-shell
{
padding: 12px 10px 10px;
}
.sb-search-bar
{
height: 36px;
padding: 0 10px;
}
.sb-search-count
{
min-width: 28px;
font-size: 10px;
}
.sb-topbar
{
height: 58px;
min-height: 58px;
max-height: 58px;
}
.sb-voice-chip
{
width: 108px;
min-width: 108px;
max-width: 108px;
height: 48px;
min-height: 48px;
max-height: 48px;
}
.sb-voice-chip-value
{
font-size: $fs-3;
}
.sb-title
{
font-size: $fs-3;
}
.sb-tabs-bar
{
height: 44px;
}
.sb-tab-chip
{
height: 32px;
padding: 0 10px;
}
.sb-tab-chip-label
{
font-size: 11px;
}
.tile
{
width: 88px;
}
.tile-swatch
{
height: 50px;
}
.sb-controls
{
flex-wrap: wrap;
}
.sb-now-playing
{
width: 100%;
order: 3;
}
.sb-kill-btn
{
width: 100%;
order: 2;
}
}
&.has-voices
{
.sb-topbar
{
border-bottom-color: $accent-line;
}
}
.sb-ach
{
position: absolute;
left: 24px;
bottom: 28px;
display: flex;
flex-direction: row;
align-items: center;
gap: 14px;
max-width: 420px;
padding: 14px 18px;
border-radius: 12px;
background-color: rgba(12, 8, 20, 0.94);
border: 2px solid rgba(255, 210, 80, 0.75);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55), 0 0 24px rgba(255, 180, 40, 0.22);
pointer-events: none;
z-index: 50;
}
.sb-ach-icon
{
font-size: 28px;
flex-shrink: 0;
}
.sb-ach-copy
{
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.sb-ach-kicker
{
font-size: 9px;
font-weight: 800;
letter-spacing: 0.2em;
color: #ffd24a;
}
.sb-ach-title
{
font-size: 15px;
font-weight: 900;
letter-spacing: 0.04em;
color: #fff;
}
.sb-ach-body
{
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
color: rgba(255, 255, 255, 0.72);
white-space: normal;
}
}
Game
game
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@attribute [StyleSheet]
@inherits Panel
<root class="modal-overlay @(IsOpen ? "is-open" : "") @ExtraClass">
@if ( IsOpen )
{
<div class="modal-scrim" @onclick=@HandleScrimClick></div>
<div class="modal-dialog" style=@DialogStyle>
@if ( Header is not null || !string.IsNullOrEmpty( Title ) || ShowClose )
{
<div class="modal-header">
@if ( Header is not null )
{
@Header
}
else
{
<div class="modal-title">@Title</div>
}
@if ( ShowClose )
{
<div class="modal-close" @onclick=@HandleClose>X</div>
}
</div>
}
@if ( Body is not null )
{
<div class="modal-body">@Body</div>
}
@if ( Footer is not null )
{
<div class="modal-footer">@Footer</div>
}
</div>
}
</root>
@code
{
public bool IsOpen { get; set; }
public string Title { get; set; }
public bool ShowClose { get; set; } = true;
public bool DismissOnScrim { get; set; } = true;
public string ExtraClass { get; set; }
public string DialogStyle { get; set; }
public Action OnClose { get; set; }
public RenderFragment Header { get; set; }
public RenderFragment Body { get; set; }
public RenderFragment Footer { get; set; }
void HandleClose()
{
try { OnClose?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Modal] OnClose threw: {e.Message}" ); }
}
void HandleScrimClick()
{
if ( !DismissOnScrim ) return;
HandleClose();
}
protected override int BuildHash() => HashCode.Combine( IsOpen, Title, ShowClose, ExtraClass, DialogStyle );
}
Game
game
namespace Sunless.Libraries.UI;
using System;
using Sandbox;
// Singleton toast state. Host <ToastHost /> once. Call:
//
// Toaster.Show( "#toast.saved" );
// Toaster.New().WithIcon( "save" ).WithCopy( path ).Show( "#toast.saved", path );
//
public static class Toaster
{
public static string Title { get; private set; }
public static string Message { get; private set; }
public static string Icon { get; private set; }
public static bool ShowCopy { get; private set; }
public static string CopyValue { get; private set; }
public static ToastTone Tone { get; private set; }
public static bool IsOpen => !string.IsNullOrEmpty( Message );
public static int Version { get; private set; }
public static Action OnShowSound { get; set; }
public static Action OnCopySound { get; set; }
public static ToastBuilder New() => new();
public static void Show( string message, string title = null )
=> New().Show( message, title );
public static void Dismiss()
{
if ( !IsOpen ) return;
Message = null;
Title = null;
Icon = null;
CopyValue = null;
ShowCopy = false;
Tone = ToastTone.Default;
Version++;
}
internal static void RaiseCopySound()
{
try { OnCopySound?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Toaster] copy sound threw: {e.Message}" ); }
}
internal static void Commit( ToastBuilder b )
{
Message = b._message;
Title = b._title;
Icon = b._icon;
ShowCopy = b._showCopy;
CopyValue = b._copyValue;
Tone = b._tone;
Version++;
try { (b._onShowSound ?? OnShowSound)?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Toaster] show sound threw: {e.Message}" ); }
}
}
public enum ToastTone { Default, Ok, Warn, Danger, Info }
public sealed class ToastBuilder
{
internal string _title;
internal string _message;
internal string _icon;
internal bool _showCopy;
internal string _copyValue;
internal ToastTone _tone = ToastTone.Default;
internal Action _onShowSound;
public ToastBuilder WithTitle( string title ) { _title = title; return this; }
public ToastBuilder WithIcon( string ligature ) { _icon = ligature; return this; }
public ToastBuilder WithCopy( string value = null ) { _showCopy = true; _copyValue = value; return this; }
public ToastBuilder WithTone( ToastTone tone ) { _tone = tone; return this; }
public ToastBuilder WithSound( Action sound ) { _onShowSound = sound; return this; }
public void Show( string message, string title = null )
{
_message = message;
if ( title is not null ) _title = title;
Toaster.Commit( this );
}
}
Game
game
@import "base/_splitcontainer.scss";
@import "base/_navigator.scss";
button
{
cursor: pointer;
}
IconPanel
{
font-family: Material Icons;
}
.is-half
{
width: 50%;
}
.is-third
{
width: 33%;
}
.is-quarter
{
width: 25%;
}
button.has-subtitle
{
position: relative;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: 40px; // icon space
.iconpanel
{
position: absolute;
left: 5px;
top: 0;
bottom: 0;
align-items: center;
}
.button-label
{
font-weight: bold;
}
.button-subtitle
{
font-size: 12px;
opacity: 0.5;
mix-blend-mode: lighten;
}
}
Game
game
ControlSheetRow
{
flex-direction: row;
flex-shrink: 0;
max-height: 32px;
border-radius: 4px;
&.hidden
{
display: none;
}
}
ControlSheetRow > .left
{
width: 150px;
flex-shrink: 0;
flex-grow: 0;
padding: 6px;
}
ControlSheetRow > .right
{
flex-grow: 1;
}
ControlSheetRow > .left > .title
{
width: 150px;
flex-shrink: 0;
flex-grow: 0;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
}
ControlSheetRow > .right > textentry,
ControlSheetRow > .right numberentry
{
background-color: #000a;
flex-grow: 1;
padding: 4px 8px;
border-radius: 2px;
}
Game
game
@import "/ui/styles/_theme.scss";
.float-dock {
pointer-events: all;
background-color: $bg-1;
border: 1px solid $line-strong;
border-left: 2px solid @import "/ui/styles/_theme.scss";
.float-dock {
pointer-events: all;
background-color: $bg-1;
border: 1px solid $line-strong;
border-left: 2px solid $accent;
padding: 10px 14px;
flex-direction: column;
gap: 8px;
&.float-dock--row { flex-direction: row; align-items: center; }
&.float-dock--quiet { border-left-color: $line-mid; }
&.float-dock--focus { border-left-color: $accent; }
&.float-dock--danger { border-left-color: $danger; }
}
_TMP_PRIMARY;
padding: 10px 14px;
flex-direction: column;
gap: 8px;
&.float-dock--row { flex-direction: row; align-items: center; }
&.float-dock--quiet { border-left-color: $line-mid; }
&.float-dock--focus { border-left-color: $accent; }
&.float-dock--danger { border-left-color: $danger; }
}
Game
game
@import "/ui/styles/_theme.scss";
.input-glyph {
flex-direction: row;
align-items: center;
justify-content: center;
img {
width: 32px;
height: 32px;
}
.input-glyph__fallback {
padding: 3px 8px;
background-color: rgba(255, 255, 255, 0.06);
border: 1px solid $line-strong;
color: $text;
font-family: $font-mono;
font-size: $fs-1;
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
}
}
Game
game
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@attribute [StyleSheet]
@inherits Panel
<root>
<div class="empty">
<div class="empty-icon">@Icon</div>
<div class="empty-title">@Title</div>
@if ( !string.IsNullOrEmpty( Sub ) )
{
<div class="empty-sub">@Sub</div>
}
</div>
</root>
@code
{
public string Icon { get; set; } = "-- --";
public string Title { get; set; } = "";
public string Sub { get; set; } = "";
protected override int BuildHash() => HashCode.Combine( Icon, Title, Sub );
}
Game
game
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@attribute [StyleSheet]
@inherits Panel
<root class="icon @SizeClass @ExtraClass">@Name</root>
@code
{
public string Name { get; set; } = "";
public IconSize Size { get; set; } = IconSize.Medium;
public string ExtraClass { get; set; }
string SizeClass => Size switch
{
IconSize.Small => "icon-sm",
IconSize.Large => "icon-lg",
IconSize.XLarge => "icon-xl",
_ => "icon-md",
};
protected override int BuildHash() => HashCode.Combine( Name, Size, ExtraClass );
}
Game
game
namespace Sunless.Libraries.UI;
using System;
using System.Collections.Generic;
public sealed class MenuLinkBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuLinkWidget _w;
internal MenuLinkBuilder( MenuScreenBuilder screen, string label, string icon )
{
_screen = screen;
_w = new MenuLinkWidget { Label = label, Icon = icon, Style = MenuLinkStyle.Row };
}
public MenuLinkBuilder WithStyle( MenuLinkStyle style ) { _w.Style = style; return this; }
public MenuLinkBuilder WithTone( MenuTone tone ) { _w.Tone = tone; return this; }
public MenuLinkBuilder WithHint( string hint ) { _w.Hint = hint; return this; }
public MenuLinkBuilder WhenEnabled( Func<bool> enabled ) { _w.Enabled = enabled; return this; }
public MenuLinkBuilder WithGoTo( string routeId ) { _w.RouteId = routeId; return this; }
public MenuLinkBuilder WithAction( Action invoke ) { _w.Invoke = invoke; return this; }
public MenuLinkBuilder Grows() { _w.Grow = true; return this; }
public MenuScreenBuilder EndLink()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuToggleBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuToggleWidget _w;
internal MenuToggleBuilder( MenuScreenBuilder screen, string label )
{
_screen = screen;
_w = new MenuToggleWidget { Label = label };
}
public MenuToggleBuilder WithHint( string hint ) { _w.Hint = hint; return this; }
public MenuToggleBuilder WithGetter( Func<bool> get ) { _w.Get = get; return this; }
public MenuToggleBuilder WithSetter( Action<bool> set ) { _w.Set = set; return this; }
public MenuToggleBuilder Bind( Func<bool> get, Action<bool> set )
{
_w.Get = get;
_w.Set = set;
return this;
}
public MenuScreenBuilder EndToggle()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuRangeBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuRangeWidget _w;
internal MenuRangeBuilder( MenuScreenBuilder screen, string label )
{
_screen = screen;
_w = new MenuRangeWidget { Label = label, Min = 0f, Max = 1f, Step = 0.01f, Format = "0.00" };
}
public MenuRangeBuilder WithHint( string hint ) { _w.Hint = hint; return this; }
public MenuRangeBuilder WithRange( float min, float max ) { _w.Min = min; _w.Max = max; return this; }
public MenuRangeBuilder WithStep( float step ) { _w.Step = step; return this; }
public MenuRangeBuilder WithFormat( string format ) { _w.Format = format; return this; }
public MenuRangeBuilder Bind( Func<float> get, Action<float> set )
{
_w.Get = get;
_w.Set = set;
return this;
}
public MenuScreenBuilder EndRange()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuSegmentedBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuSegmentedWidget _w;
internal MenuSegmentedBuilder( MenuScreenBuilder screen, string label, string[] choices )
{
_screen = screen;
_w = new MenuSegmentedWidget { Label = label, Choices = choices };
}
public MenuSegmentedBuilder WithHint( string hint ) { _w.Hint = hint; return this; }
public MenuSegmentedBuilder Bind( Func<int> get, Action<int> set )
{
_w.Get = get;
_w.Set = set;
return this;
}
public MenuScreenBuilder EndSegmented()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuRadioBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuRadioWidget _w;
readonly List<MenuRadioWidget.Option> _options = new();
internal MenuRadioBuilder( MenuScreenBuilder screen, string label )
{
_screen = screen;
_w = new MenuRadioWidget { Label = label };
}
public MenuRadioBuilder WithHint( string hint ) { _w.Hint = hint; return this; }
public MenuRadioBuilder WithOption( string label, string icon = null, string hint = null )
{
_options.Add( new MenuRadioWidget.Option { Label = label, Icon = icon, Hint = hint } );
return this;
}
public MenuRadioBuilder Bind( Func<int> get, Action<int> set )
{
_w.Get = get;
_w.Set = set;
return this;
}
public MenuScreenBuilder EndRadio()
{
_w.Options = _options;
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuInfoBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuInfoWidget _w;
internal MenuInfoBuilder( MenuScreenBuilder screen, string label )
{
_screen = screen;
_w = new MenuInfoWidget { Label = label };
}
public MenuInfoBuilder WithIcon( string icon ) { _w.Icon = icon; return this; }
public MenuInfoBuilder WithGetter( Func<string> get ) { _w.Get = get; return this; }
public MenuInfoBuilder WithValue( string value ) { _w.Get = () => value; return this; }
public MenuScreenBuilder EndInfo()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuTextFieldBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuTextFieldWidget _w;
internal MenuTextFieldBuilder( MenuScreenBuilder screen, string label )
{
_screen = screen;
_w = new MenuTextFieldWidget { Label = label };
}
public MenuTextFieldBuilder WithHint( string hint ) { _w.Hint = hint; return this; }
public MenuTextFieldBuilder Bind( Func<string> get, Action<string> set )
{
_w.Get = get;
_w.Set = set;
return this;
}
public MenuScreenBuilder EndTextField()
{
_screen.AddWidget( _w );
return _screen;
}
}
// === Composite widgets ===============================================
public sealed class MenuHeroCardBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuHeroCardWidget _w = new();
internal MenuHeroCardBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuHeroCardBuilder WithTitle( string title ) { _w.Title = title; return this; }
public MenuHeroCardBuilder WithSubtitle( string subtitle ) { _w.Subtitle = subtitle; return this; }
public MenuHeroCardBuilder WithDescription( string description ) { _w.Description = description; return this; }
public MenuHeroCardBuilder WithImage( string url ) { _w.Image = url; return this; }
public MenuHeroCardBuilder WithIcon( string icon ) { _w.Icon = icon; return this; }
public MenuScreenBuilder EndHeroCard()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuMapCardBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuMapCardWidget _w = new();
internal MenuMapCardBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuMapCardBuilder WithTitle( string title ) { _w.Title = title; return this; }
public MenuMapCardBuilder WithDescription( string description ) { _w.Description = description; return this; }
public MenuMapCardBuilder WithImage( string url ) { _w.Image = url; return this; }
public MenuMapCardBuilder WithChip( string label, string icon = null, MenuTone tone = MenuTone.Confirm )
{
_w.ChipLabel = label;
_w.ChipIcon = icon;
_w.ChipTone = tone;
return this;
}
public MenuMapCardBuilder WithAction( string label, string icon, Action invoke, string routeId = null )
{
_w.ActionLabel = label;
_w.ActionIcon = icon;
_w.ActionInvoke = invoke;
_w.ActionRouteId = routeId;
return this;
}
public MenuScreenBuilder EndMapCard()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuMapGridBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuMapGridWidget _w = new();
readonly List<MenuMapGridWidget.Card> _cards = new();
internal MenuMapGridBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuMapGridBuilder WithActive( Func<int> get ) { _w.Active = get; return this; }
public MenuMapGridBuilder OnPick( Action<int> set ) { _w.Pick = set; return this; }
public MenuMapCardCardBuilder WithCard( string title )
=> new( this, title );
internal void AddCard( MenuMapGridWidget.Card card ) => _cards.Add( card );
bool _closed;
public MenuScreenBuilder EndMapGrid()
{
if ( _closed ) return _screen;
_closed = true;
_w.Cards = _cards;
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuMapCardCardBuilder
{
readonly MenuMapGridBuilder _grid;
readonly MenuMapGridWidget.Card _c;
internal MenuMapCardCardBuilder( MenuMapGridBuilder grid, string title )
{
_grid = grid;
_c = new MenuMapGridWidget.Card { Title = title };
}
public MenuMapCardCardBuilder WithDescription( string text ) { _c.Description = text; return this; }
public MenuMapCardCardBuilder WithImage( string url ) { _c.Image = url; return this; }
public MenuMapCardCardBuilder WithBadge( string label, MenuTone tone = MenuTone.Confirm )
{
_c.BadgeLabel = label;
_c.BadgeTone = tone;
return this;
}
public MenuMapCardCardBuilder ComingSoon( bool v = true ) { _c.ComingSoon = v; return this; }
public MenuMapCardCardBuilder Locked( bool v = true ) { _c.Locked = v; return this; }
public MenuMapGridBuilder EndCard()
{
_grid.AddCard( _c );
return _grid;
}
}
public sealed class MenuTileGridBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuTileGridWidget _w = new();
readonly List<MenuTileGridWidget.Item> _items = new();
internal MenuTileGridBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuTileGridBuilder WithTile( string label, string color = null, string image = null )
{
_items.Add( new MenuTileGridWidget.Item { Label = label, Color = color, Image = image } );
return this;
}
public MenuTileGridBuilder WithActive( Func<int> get ) { _w.Active = get; return this; }
public MenuTileGridBuilder OnPick( Action<int> set ) { _w.Pick = set; return this; }
public MenuScreenBuilder EndTileGrid()
{
_w.Items = _items;
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuPlayerListBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuPlayerListWidget _w = new();
readonly List<MenuPlayerListWidget.Player> _players = new();
internal MenuPlayerListBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuPlayerListBuilder WithPlayer( string name, bool ready = false, bool host = false )
{
_players.Add( new MenuPlayerListWidget.Player { Name = name, Ready = ready, Host = host } );
return this;
}
public MenuScreenBuilder EndPlayerList()
{
_w.Players = _players;
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuKeyBindingsBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuKeyBindingsWidget _w = new();
readonly List<MenuKeyBindingsWidget.Binding> _bindings = new();
internal MenuKeyBindingsBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuKeyBindingsBuilder WithBinding( string action, params string[] keys )
{
_bindings.Add( new MenuKeyBindingsWidget.Binding { Action = action, Keys = keys } );
return this;
}
public MenuScreenBuilder EndKeyBindings()
{
_w.Bindings = _bindings;
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuPromptCardBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuPromptCardWidget _w = new() { Verb = "Press" };
internal MenuPromptCardBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuPromptCardBuilder WithVerb( string verb ) { _w.Verb = verb; return this; }
public MenuPromptCardBuilder WithPrompt( string prompt ) { _w.Prompt = prompt; return this; }
public MenuPromptCardBuilder WithGlyph( string ligature ) { _w.Glyph = ligature; return this; }
public MenuPromptCardBuilder WithCost( int cost ) { _w.Cost = cost; return this; }
public MenuPromptCardBuilder WhenDisabled( Func<bool> d ) { _w.Disabled = d; return this; }
public MenuPromptCardBuilder WithHoldProgress( Func<float> p ) { _w.Progress = p; return this; }
public MenuScreenBuilder EndPromptCard()
{
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuActionRowBuilder
{
readonly MenuScreenBuilder _screen;
readonly MenuActionRowWidget _w = new();
readonly List<MenuLinkWidget> _links = new();
internal MenuActionRowBuilder( MenuScreenBuilder screen ) { _screen = screen; }
public MenuActionRowLinkBuilder WithLink( string label, string icon = null )
=> new( this, label, icon );
internal void AddLink( MenuLinkWidget link ) => _links.Add( link );
public MenuScreenBuilder EndActionRow()
{
_w.Links = _links;
_screen.AddWidget( _w );
return _screen;
}
}
public sealed class MenuActionRowLinkBuilder
{
readonly MenuActionRowBuilder _row;
readonly MenuLinkWidget _w;
internal MenuActionRowLinkBuilder( MenuActionRowBuilder row, string label, string icon )
{
_row = row;
_w = new MenuLinkWidget { Label = label, Icon = icon, Style = MenuLinkStyle.Button };
}
public MenuActionRowLinkBuilder WithTone( MenuTone tone ) { _w.Tone = tone; return this; }
public MenuActionRowLinkBuilder WithGoTo( string routeId ) { _w.RouteId = routeId; return this; }
public MenuActionRowLinkBuilder WithAction( Action invoke ) { _w.Invoke = invoke; return this; }
public MenuActionRowLinkBuilder WhenEnabled( Func<bool> enabled ) { _w.Enabled = enabled; return this; }
public MenuActionRowLinkBuilder Grows() { _w.Grow = true; return this; }
public MenuActionRowBuilder EndLink()
{
_row.AddLink( _w );
return _row;
}
}
Game
game
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@inherits Panel
@attribute [StyleSheet]
<root class="@(IsDisabled ? "disabled" : "")">
<div class="prompt-frame">
<div class="prompt-vignette"></div>
<div class="prompt-content">
<div class="prompt-glyph">
@if ( !string.IsNullOrEmpty( Glyph ) )
{
<div class="kbd">@Glyph</div>
}
</div>
<div class="prompt-text">
<div class="prompt-verb">@EffectiveVerb</div>
<div class="prompt-label">@Prompt</div>
</div>
@if ( Cost > 0 )
{
<div class="prompt-cost">
<div class="prompt-cost-value">@Cost</div>
<div class="prompt-cost-unit">PTS</div>
</div>
}
</div>
<div class="prompt-rule">
<div class="prompt-rule-fill" @ref="ruleFill"></div>
</div>
</div>
</root>
@code
{
public string Verb { get; set; } = "Press";
public string Prompt { get; set; } = "";
public string Glyph { get; set; } = "E";
public int Cost { get; set; }
public bool IsDisabled { get; set; }
public Func<float> HoldProgressFn { get; set; }
Panel ruleFill { get; set; }
string EffectiveVerb => IsDisabled && Cost > 0 ? "Not enough points" : Verb;
public override void Tick()
{
if ( ruleFill is null ) return;
var p = HoldProgressFn?.Invoke() ?? 0f;
if ( p < 0f ) p = 0f;
else if ( p > 1f ) p = 1f;
ruleFill.Style.Opacity = p > 0f ? 1f : 0f;
ruleFill.Style.Width = Length.Percent( p * 100f );
}
protected override int BuildHash() => HashCode.Combine( Verb, Prompt, Glyph, Cost, IsDisabled );
}
Game
game
using System;
namespace SSoundboard;
/// <summary>
/// Network relay for co-op pad presses. Host owns one scene object; clients
/// call Rpc.Host then host rebroadcasts so every machine hears the chaos.
/// </summary>
public sealed class SoundBoardNet : Component
{
public static SoundBoardNet Instance { get; private set; }
protected override void OnEnabled()
{
Instance = this;
}
protected override void OnDisabled()
{
if ( Instance == this )
Instance = null;
}
protected override void OnDestroy()
{
if ( Instance == this )
Instance = null;
}
/// <summary>Host creates a networked singleton when a multiplayer session is live.</summary>
public static void EnsureHostRelay()
{
try
{
if ( !Networking.IsActive )
return;
if ( Instance.IsValid() )
return;
if ( !Networking.IsHost )
return;
var go = new GameObject( true, "SoundBoardNet" );
go.Components.Create<SoundBoardNet>();
go.NetworkSpawn();
Log.Info( "[S&Soundboard] CO-OP network relay spawned" );
}
catch ( Exception e )
{
Log.Warning( $"[S&Soundboard] CO-OP relay spawn failed: {e.Message}" );
}
}
/// <summary>Local player pressed a pad while CO-OP is on — share with the lobby.</summary>
public static void SharePadPress( SoundPad pad )
{
if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
return;
if ( !SoundBoardCoop.Enabled )
return;
try
{
if ( !Networking.IsActive )
return;
EnsureHostRelay();
var net = Instance;
if ( !net.IsValid() )
return;
// Calling Rpc on a non-networked object throws ("must be networked").
try
{
if ( net.GameObject is null || !net.GameObject.Network.Active )
return;
}
catch
{
return;
}
net.RequestPad(
pad.SoundId,
pad.Label ?? "",
pad.Color ?? "#ffffff",
pad.Volume <= 0f ? 1f : pad.Volume );
}
catch ( Exception e )
{
// Never let lobby sync break local button presses.
Log.Warning( $"[S&Soundboard] CO-OP share failed: {e.GetType().Name}: {e.Message}" );
}
}
[Rpc.Host]
void RequestPad( string soundId, string label, string color, float volume )
{
try
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return;
var caller = Rpc.Caller;
var name = ResolveDisplayName( caller );
var steamId = ResolveSteamId( caller );
// Host rebroadcasts to everyone (including the original presser).
ReceivePad( soundId, label ?? "", color ?? "#ffffff", volume, name, steamId );
}
catch ( Exception e )
{
Log.Warning( $"[S&Soundboard] CO-OP RequestPad failed: {e.GetType().Name}: {e.Message}" );
}
}
static string ResolveDisplayName( Connection caller )
{
if ( caller is null )
return "Player";
try
{
if ( !string.IsNullOrWhiteSpace( caller.DisplayName ) )
return caller.DisplayName;
}
catch { /* ignore */ }
try
{
if ( !string.IsNullOrWhiteSpace( caller.Name ) )
return caller.Name;
}
catch { /* ignore */ }
return "Player";
}
static long ResolveSteamId( Connection caller )
{
if ( caller is null )
return 0;
// Prefer string parse — SteamId / Friend.Id shapes vary; never throw on cast.
try
{
var id = ParseSteamId( caller.FriendInfo.Id );
if ( id != 0 )
return id;
}
catch { /* ignore */ }
try
{
var id = ParseSteamId( caller.SteamId );
if ( id != 0 )
return id;
}
catch { /* ignore */ }
try
{
return ParseSteamId( caller.OwnerSteamId );
}
catch
{
return 0;
}
}
static long ParseSteamId( object value )
{
if ( value is null )
return 0;
try
{
switch ( value )
{
case long l:
return l;
case ulong ul:
return unchecked( (long)ul );
case int i:
return i;
case uint ui:
return ui;
}
}
catch { /* fall through */ }
try
{
var s = value.ToString();
if ( string.IsNullOrWhiteSpace( s ) )
return 0;
// SteamId.ToString() is typically the numeric account id.
if ( long.TryParse( s, out var parsed ) )
return parsed;
}
catch { /* ignore */ }
return 0;
}
[Rpc.Broadcast( NetFlags.HostOnly | NetFlags.Reliable )]
void ReceivePad( string soundId, string label, string color, float volume, string playerName, long steamId )
{
SoundBoardCoop.OnNetworkPad( soundId, label, color, volume, playerName, steamId );
}
}
Game
game
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SSoundboard;
/// <summary>
/// On-demand sound loading. Prefer TryGet (already compiled); LoadAsync only on click.
/// </summary>
public static class SoundPreloadService
{
enum LoadState { Pending, Loading, Ready, Failed }
static readonly Dictionary<string, LoadState> _states = new( StringComparer.OrdinalIgnoreCase );
static readonly List<string> _priority = new();
static readonly Dictionary<string, SoundPad> _pendingPlays = new( StringComparer.OrdinalIgnoreCase );
static int _loaded;
static int _registered;
static int _activeLoads;
static bool _initialized;
static bool _startupComplete;
const int MaxConcurrentLoads = 2;
const int StartupWarmCap = 32;
public static event Action Changed;
public static int LoadedCount => _loaded;
public static bool IsInitialized => _initialized;
public static bool IsStartupComplete => _startupComplete;
/// <summary>Bumps when pending-play set changes — cheap UI rebuild signal.</summary>
public static int PendingRevision { get; private set; }
public static void Initialize()
{
if ( _initialized )
return;
_initialized = true;
var warmed = 0;
foreach ( var soundId in SoundBoardData.StartupSoundIds )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
continue;
if ( TryMarkReadyIfPresent( Normalize( soundId ) ) )
warmed++;
if ( warmed >= StartupWarmCap )
break;
}
_startupComplete = true;
Changed?.Invoke();
}
static void EnsureInitialized()
{
if ( !_initialized )
Initialize();
}
public static bool IsReady( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return false;
EnsureInitialized();
var key = Normalize( soundId );
if ( _states.TryGetValue( key, out var state ) )
{
if ( state == LoadState.Ready )
return true;
if ( state is LoadState.Failed or LoadState.Loading )
return false;
}
return TryMarkReadyIfPresent( key );
}
public static void BumpPriority( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return;
EnsureInitialized();
var key = Normalize( soundId );
if ( IsReady( key ) )
return;
if ( _states.TryGetValue( key, out var state ) )
{
if ( state is LoadState.Loading or LoadState.Ready )
return;
if ( state == LoadState.Failed )
_states[key] = LoadState.Pending;
}
else
{
_states[key] = LoadState.Pending;
_registered++;
}
_priority.Remove( key );
_priority.Insert( 0, key );
Changed?.Invoke();
}
public static void QueuePlayWhenReady( SoundPad pad )
{
if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
return;
var key = Normalize( pad.SoundId );
_pendingPlays[key] = pad;
PendingRevision++;
BumpPriority( key );
}
public static bool TryTakePendingPlay( string soundId, out SoundPad pad )
{
var removed = _pendingPlays.Remove( Normalize( soundId ), out pad );
if ( removed )
PendingRevision++;
return removed;
}
public static bool IsPendingPlay( string soundId )
{
return _pendingPlays.ContainsKey( Normalize( soundId ) );
}
public static void Tick()
{
EnsureInitialized();
TryPumpLoads();
}
static void TryPumpLoads()
{
while ( _activeLoads < MaxConcurrentLoads && TryDequeue( out var soundId ) )
{
if ( TryMarkReadyIfPresent( soundId ) )
{
SoundBoardManager.OnSoundReady( soundId );
continue;
}
_activeLoads++;
_states[soundId] = LoadState.Loading;
_ = LoadOneAsync( soundId );
}
}
static bool TryDequeue( out string soundId )
{
while ( _priority.Count > 0 )
{
var next = _priority[0];
_priority.RemoveAt( 0 );
if ( !_states.TryGetValue( next, out var st ) || st != LoadState.Pending )
continue;
soundId = next;
return true;
}
soundId = null;
return false;
}
static async Task LoadOneAsync( string soundId )
{
var path = ToSoundPath( soundId );
try
{
if ( ResourceLibrary.TryGet<SoundEvent>( path, out _ ) )
{
MarkReady( soundId );
SoundBoardManager.OnSoundReady( soundId );
return;
}
var loaded = await ResourceLibrary.LoadAsync<SoundEvent>( path );
if ( loaded is null )
{
MarkFailed( soundId );
return;
}
try { Sound.Preload( path ); }
catch { /* playable without preload */ }
MarkReady( soundId );
SoundBoardManager.OnSoundReady( soundId );
}
catch ( Exception e )
{
Log.Warning( $"[S&Soundboard] Sound load skipped: {path} ({e.GetType().Name})" );
MarkFailed( soundId );
}
finally
{
_activeLoads = Math.Max( 0, _activeLoads - 1 );
}
}
static bool TryMarkReadyIfPresent( string key )
{
try
{
if ( !ResourceLibrary.TryGet<SoundEvent>( ToSoundPath( key ), out _ ) )
return false;
}
catch
{
return false;
}
MarkReady( key );
return true;
}
static void MarkReady( string key )
{
var already = _states.TryGetValue( key, out var prev ) && prev == LoadState.Ready;
_states[key] = LoadState.Ready;
if ( !already )
_loaded++;
Changed?.Invoke();
}
static void MarkFailed( string key )
{
_states[key] = LoadState.Failed;
if ( _pendingPlays.Remove( key ) )
PendingRevision++;
Changed?.Invoke();
}
static string Normalize( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return soundId;
soundId = soundId.Replace( '\\', '/' ).Trim();
return soundId.EndsWith( ".sound", StringComparison.OrdinalIgnoreCase )
? soundId[..^6]
: soundId;
}
static string ToSoundPath( string soundId ) => $"{Normalize( soundId )}.sound";
}
Debug: View Raw JSON Response
{
"TotalCount": 118,
"Files": [
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/controls/color/colorhuecontrol.cs.scss",
"FileName": "colorhuecontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "ColorHueControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, red, yellow, lime, cyan, blue, magenta, red );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tmargin: 10px 0;\r\n\tborder: 1px solid #333;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\ttop: -5px;\r\n\t\tbottom: -5px;\r\n\t\taspect-ratio: 1;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% );\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "styles/rootpanel.scss",
"FileName": "rootpanel.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "\r\n\r\nRootPanel\r\n{\r\n\tfont-family: Roboto;\r\n\tcolor: white;\r\n}\r\n\r\nIconPanel, i\r\n{\r\n\tfont-family: Material Icons;\r\n\tbackground-position: center;\r\n\tbackground-size: contain;\r\n}\r\n\r\n.tooltip\r\n{\r\n\tbackground-color: rgba( black, 0.8 );\r\n\tborder: 1px solid rgba( #555, 0.1 );\r\n\tpadding: 10px;\r\n\tborder-radius: 4px;\r\n\tcolor: white;\r\n\tfont-family: Inter;\r\n\tfont-weight: bold;\r\n\ttransition: opacity 0.09s ease;\r\n\topacity: 1;\r\n\tmargin: 0px;\r\n\r\n\t&:intro, &:outro\r\n\t{\r\n\t\topacity: 0;\r\n\t}\r\n}\r\n\r\n.textentry\r\n{\r\n\tcursor: text;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n\tflex-direction: row;\r\n\tposition: relative;\r\n\tpadding: 10px;\r\n\r\n\t&.disabled\r\n\t{\r\n\t\tcursor: default;\r\n\t}\r\n\r\n\t&.is-multiline\r\n\t{\r\n\t\talign-items: flex-start;\r\n\t\twhite-space: normal;\r\n\t}\r\n\r\n\t.content-label,\r\n\t.placeholder\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t.placeholder\r\n\t{\r\n\t\topacity: 0.2;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t.iconpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tright: 0;\r\n\t\tpadding-right: 10px;\r\n\t\tpointer-events: none;\r\n\r\n\t\t&.clearbutton\r\n\t\t{\r\n\t\t\tcolor: #fff;\r\n\t\t\tpointer-events: all;\r\n\t\t\tcursor: pointer;\r\n\t\t\topacity: 0.1;\r\n\r\n\t\t\t&:hover\r\n\t\t\t{\r\n\t\t\t\topacity: 0.5;\r\n\t\t\t}\r\n\r\n\t\t\t&:active\r\n\t\t\t{\r\n\t\t\t\topacity: 1;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t&:empty\r\n\t{\r\n\t\t.clearbutton\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t&.has-icon\r\n\t{\r\n\t\tpadding-right: 30px;\r\n\t}\r\n\r\n\t.content-label,\r\n\t.prefix-label,\r\n\t.suffix-label,\r\n\t.placeholder\r\n\t{\r\n\t\toverflow: hidden;\r\n\t}\r\n\r\n\t.content-label\r\n\t{\r\n\t\torder: 1;\r\n\t}\r\n\r\n\t.suffix-label\r\n\t{\r\n\t\torder: 2;\r\n\t}\r\n\r\n\t.prefix-label,\r\n\t.suffix-label\r\n\t{\r\n\t\tflex-shrink: 0;\r\n\t\topacity: 0.5;\r\n\t}\r\n\r\n\t&.invalid\r\n\t{\r\n\t\tbox-shadow: 0px 0px 1px 2px #f8356b;\r\n\t}\r\n}\r\n\r\n.popup-panel\r\n{\r\n\tbackground-color: #1b202c;\r\n\tflex-direction: column;\r\n\tcolor: #8e9ec2;\r\n\ttransition: all 0.1s ease-out;\r\n\tposition: absolute;\r\n\tz-index: 2000;\r\n\tmin-height: 20px;\r\n\tmin-width: 10px;\r\n\toverflow: scroll;\r\n\tpointer-events: all;\r\n\tbox-shadow: 3px 3px 64px #000;\r\n\tborder-radius: 4px;\r\n\r\n\t&.medium\r\n\t{\r\n\t\tmax-width: 300px;\r\n\t}\r\n\r\n\t&.center-horizontal\r\n\t{\r\n\t\ttransform-origin: top center;\r\n\t}\r\n\r\n\t&.below-center\r\n\t{\r\n\t\ttransform: translateX( -50% );\r\n\t\ttransform-origin: 0% 0%;\r\n\t}\r\n\r\n\t&.above-left\r\n\t{\r\n\t\ttransform-origin: 0% 100%;\r\n\t}\r\n\r\n\t&.above-center\r\n\t{\r\n\t\ttransform: translateX( -50% );\r\n\t\ttransform-origin: 0% 100%;\r\n\t}\r\n\r\n\t&.left\r\n\t{\r\n\t\ttransform-origin: 100% 50%;\r\n\t\ttransform: translate( 00% -50% );\r\n\t}\r\n\r\n\t&.right\r\n\t{\r\n\t\ttransform-origin: 0 50%;\r\n\t\ttransform: translate( 0% -50% );\r\n\t}\r\n\r\n\t&:intro\r\n\t{\r\n\t\ttransform: scale( 0 );\r\n\t\tpointer-events: none;\r\n\t\topacity: 0;\r\n\t}\r\n\r\n\t&:outro\r\n\t{\r\n\t\topacity: 0;\r\n\t\ttransition: all 0.1s ease-in;\r\n\t\tmargin-top: 50px;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t&.below-stretch\r\n\t{\r\n\t\ttransform-origin: 0% 0%;\r\n\r\n\t\t&:intro\r\n\t\t{\r\n\t\t\ttransform: scaleY( 0.1 );\r\n\t\t}\r\n\t}\r\n\r\n\t> .canvas\r\n\t{\r\n\t\tflex-direction: column;\r\n\t}\r\n\r\n\t.information\r\n\t{\r\n\t\tpadding: 16px;\r\n\t\tfont-size: 14px;\r\n\t\topacity: 0.5;\r\n\t}\r\n\r\n\tbutton\r\n\t{\r\n\t\tpadding: 2px 8px;\r\n\t\tcursor: pointer;\r\n\t\tfont-family: Poppins;\r\n\t\tfont-weight: 600;\r\n\t\tflex-shrink: 0;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tbackground-color: #08f;\r\n\t\t\tcolor: #fff;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/anchorpad/anchorpad.razor.scss",
"FileName": "anchorpad.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/ui/styles/_theme.scss\";\n\n.anchor-pad {\n\twidth: 130px;\n\theight: 90px;\n\tbackground-color: $bg-2;\n\tborder: 1px solid $line-strong;\n\tpadding: 6px;\n\tposition: relative;\n\n\t.anchor-cell {\n\t\tpointer-events: all;\n\t\tbackground-color: rgba(255,255,255,0.04);\n\t\tborder: 1px solid $line-strong;\n\t\tcursor: pointer;\n\t\tposition: absolute;\n\t\twidth: 34px;\n\t\theight: 22px;\n\t\ttransition: background-color 0.1s ease, border-color 0.1s ease;\n\n\t\t&:hover { background-color: rgba(255,255,255,0.1); border-color: $accent; }\n\n\t\t&.active {\n\t\t\tbackground-color: $accent;\n\t\t\tborder-color: $accent;\n\t\t}\n\n\t\t&.anchor-topleft { left: 6px; top: 6px; }\n\t\t&.anchor-topright { right: 6px; top: 6px; }\n\t\t&.anchor-bottomleft { left: 6px; bottom: 6px; }\n\t\t&.anchor-bottomright { right: 6px; bottom: 6px; }\n\t\t&.anchor-center { left: 50%; top: 50%; transform: translate(-50%, -50%); }\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/hintbox/hintbox.scss",
"FileName": "hintbox.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/ui/styles/_theme.scss\";\n\n.hint-box {\n\tpadding: 14px 16px;\n\tbackground-color: rgba(98, 200, 255, 0.07);\n\tborder-left: 3px solid $info;\n\tcolor: $text-dim;\n\tfont-size: $fs-3;\n\tfont-weight: 500;\n\tletter-spacing: 0.2px;\n\tcode { color: $info; font-family: $font-mono; font-weight: 700; }\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/tile/tile.scss",
"FileName": "tile.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/ui/styles/_theme.scss\";\n\n.tile {\n\tflex-direction: column;\n\twidth: 140px;\n\tpadding: 0;\n\tbackground-color: $bg-2;\n\tborder: 1px solid $line-strong;\n\toverflow: hidden;\n\tcursor: pointer;\n\ttransition: border-color 0.1s ease;\n\n\t.tile-swatch {\n\t\twidth: 100%;\n\t\theight: 80px;\n\t\tposition: relative;\n\t\toverflow: hidden;\n\t\tbackground-color: #1a1f2e;\n\t\tbackground-size: cover;\n\t\tbackground-position: center;\n\t}\n\n\t.tile-thumb {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tobject-fit: contain;\n\t}\n\n\t.tile-placeholder {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.tile-vignette {\n\t\tposition: absolute;\n\t\tleft: 0; top: 0; right: 0; bottom: 0;\n\t\tbackground-color: rgba(0,0,0,0.25);\n\t\tpointer-events: none;\n\t}\n\n\t.tile-label {\n\t\theight: 42px;\n\t\tpadding: 8px 10px;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tfont-size: $fs-1;\n\t\tfont-weight: 700;\n\t\tcolor: $text-dim;\n\t\ttext-align: center;\n\t\tfont-family: $font-mono;\n\t\tletter-spacing: 1px;\n\t\ttext-transform: uppercase;\n\t\tborder-top: 1px solid $line;\n\t}\n\n\t&:hover {\n\t\tborder-color: $accent;\n\t\t.tile-label { color: $text; }\n\t}\n\t&.active {\n\t\tborder-color: $accent;\n\t\t.tile-label { color: $text; background-color: $accent-soft; font-weight: 800; }\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/vector3field/vector3field.razor.scss",
"FileName": "vector3field.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/ui/styles/_theme.scss\";\n@import \"/ui/components/field/field.scss\";\n\n.v3-field {\n\t.v3-row {\n\t\tflex-direction: row;\n\t\tgap: 6px;\n\t\talign-items: stretch;\n\t\twidth: 100%;\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/packageflairbar.razor.scss",
"FileName": "packageflairbar.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "PackageFlairBar\r\n{\r\n\tposition: absolute;\r\n\ttop: 6px;\r\n\tleft: 6px;\r\n\tz-index: 2;\r\n\tflex-direction: row;\r\n\tgap: 5px;\r\n\tpointer-events: none;\r\n\r\n\t.flair\r\n\t{\r\n\t\tposition: relative;\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t\twidth: 22px;\r\n\t\theight: 22px;\r\n\t\tpointer-events: all;\r\n\t\tborder-radius: 4px;\r\n\t\tcolor: white;\r\n\t\tborder: 1px solid rgba( white, 0.1 );\r\n\t\toutline: 1px solid #0006;\r\n\t\topacity: 0.7;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t}\r\n\r\n\t\ticon\r\n\t\t{\r\n\t\t\tfont-size: 15px;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "soundboardpanel.razor.scss",
"FileName": "soundboardpanel.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/UI/Styles/_Theme.scss\";\n@import \"/UI/Styles/_Fonts.scss\";\n@import \"/UI/Components/Tile/Tile.scss\";\n@import \"/UI/Components/Button/Button.scss\";\n\nSoundBoardPanel\n{\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tpointer-events: all;\n\toverflow: hidden;\n\n\t.sb\n\t{\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tflex-direction: column;\n\t\tbackground-color: $bg-0;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-shell\n\t{\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tflex-direction: column;\n\t\tflex-grow: 1;\n\t\tflex-shrink: 1;\n\t\tmin-height: 0;\n\t\tpadding: 20px 24px 18px;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-topbar\n\t{\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: space-between;\n\t\tgap: 16px;\n\t\theight: 64px;\n\t\tmin-height: 64px;\n\t\tmax-height: 64px;\n\t\tmargin-bottom: 14px;\n\t\tpadding-bottom: 14px;\n\t\tborder-bottom: 1px solid $line;\n\t\tflex-shrink: 0;\n\t\tflex-wrap: nowrap;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-brand-block\n\t{\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 12px;\n\t\tmin-width: 0;\n\t\tflex-grow: 1;\n\t}\n\n\t.sb-status-led\n\t{\n\t\twidth: 10px;\n\t\theight: 10px;\n\t\tborder-radius: 50%;\n\t\tbackground-color: $bg-2;\n\t\tborder: 1px solid $line;\n\t\tflex-shrink: 0;\n\n\t\t&.is-live\n\t\t{\n\t\t\tbackground-color: $success;\n\t\t\tborder-color: $success-line;\n\t\t}\n\t}\n\n\t.sb-brand-copy\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 2px;\n\t\tmin-width: 0;\n\t}\n\n\t.sb-eyebrow\n\t{\n\t\tfont-size: $fs-1;\n\t\tfont-weight: 700;\n\t\tletter-spacing: $ls-xwide;\n\t\tcolor: $text-faint;\n\t\tfont-family: $font-mono;\n\t}\n\n\t.sb-title\n\t{\n\t\tfont-size: $fs-5;\n\t\tfont-weight: 800;\n\t\tcolor: $text;\n\t\tfont-family: $font-primary;\n\t\twhite-space: nowrap;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-voice-chip\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\tgap: 6px;\n\t\twidth: 128px;\n\t\theight: 52px;\n\t\tmin-width: 128px;\n\t\tmax-width: 128px;\n\t\tpadding: 8px 12px;\n\t\tborder-radius: 4px;\n\t\tbackground-color: $bg-2;\n\t\tborder: 1px solid $line-strong;\n\t\tflex-shrink: 0;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-voice-chip-head\n\t{\n\t\tdisplay: flex;\n\t\talign-items: baseline;\n\t\tjustify-content: space-between;\n\t\tgap: 8px;\n\t}\n\n\t.sb-voice-chip-label\n\t{\n\t\tfont-size: $fs-1;\n\t\tfont-weight: 700;\n\t\tletter-spacing: $ls-wide;\n\t\tcolor: $text-faint;\n\t\tfont-family: $font-mono;\n\t}\n\n\t.sb-voice-chip-value\n\t{\n\t\tfont-size: $fs-4;\n\t\tfont-weight: 800;\n\t\tline-height: 1;\n\t\tcolor: $accent;\n\t\tfont-family: $font-mono;\n\t}\n\n\t.sb-voice-meter-stack\n\t{\n\t\twidth: 100%;\n\t\theight: 18px;\n\t\tmin-height: 18px;\n\t\tmax-height: 18px;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-end;\n\t\tgap: 2px;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-voice-meter-tier\n\t{\n\t\twidth: 100%;\n\t\theight: 3px;\n\t\tmin-height: 3px;\n\t\tmax-height: 3px;\n\t\tborder-radius: 999px;\n\t\tbackground-color: $bg-inset;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-voice-meter-fill\n\t{\n\t\theight: 100%;\n\t\tborder-radius: 999px;\n\t\ttransition: width 0.16s ease;\n\t}\n\n\t.sb-tabs-bar\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tgap: 8px;\n\t\twidth: 100%;\n\t\theight: 48px;\n\t\tmargin-bottom: 10px;\n\t\tpointer-events: all;\n\t}\n\n\t.sb-search-bar\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tgap: 10px;\n\t\twidth: 100%;\n\t\theight: 40px;\n\t\tmargin-bottom: 12px;\n\t\tpadding: 0 12px;\n\t\tborder-radius: 8px;\n\t\tbackground-color: $bg-2;\n\t\tborder: 1px solid $line-strong;\n\t\tpointer-events: all;\n\t}\n\n\t.sb-search-icon\n\t{\n\t\tflex-shrink: 0;\n\t\tfont-size: 16px;\n\t\tfont-weight: 700;\n\t\tcolor: $text-faint;\n\t\tline-height: 1;\n\t}\n\n\t.sb-search-input\n\t{\n\t\tflex-grow: 1;\n\t\tflex-shrink: 1;\n\t\tmin-width: 0;\n\t\theight: 36px;\n\t\tmin-height: 36px;\n\t\tpadding: 0 4px;\n\t\tbackground-color: transparent;\n\t\tborder: 0;\n\t\tcolor: $text;\n\t\tfont-size: $fs-2;\n\t\tfont-weight: 600;\n\t\tfont-family: $font-mono;\n\t\tletter-spacing: 0.04em;\n\t}\n\n\t.sb-search-clear\n\t{\n\t\tdisplay: flex;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 28px;\n\t\theight: 28px;\n\t\tborder-radius: 6px;\n\t\tbackground-color: $bg-inset;\n\t\tborder: 1px solid $line;\n\t\tcolor: $text-dim;\n\t\tfont-size: 12px;\n\t\tfont-weight: 800;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\n\t\t&:hover\n\t\t{\n\t\t\tborder-color: $accent;\n\t\t\tcolor: $accent;\n\t\t}\n\n\t\t&:active\n\t\t{\n\t\t\tsound-in: \"ui.button.press\";\n\t\t\ttransform: scale(0.96);\n\t\t}\n\t}\n\n\t.sb-search-count\n\t{\n\t\tflex-shrink: 0;\n\t\tmin-width: 36px;\n\t\ttext-align: right;\n\t\tfont-size: $fs-1;\n\t\tfont-weight: 700;\n\t\tletter-spacing: $ls-wide;\n\t\tcolor: $text-faint;\n\t\tfont-family: $font-mono;\n\t\twhite-space: nowrap;\n\t}\n\n\t.sb-empty\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 8px;\n\t\twidth: 100%;\n\t\tmin-height: 180px;\n\t\tpadding: 32px 16px;\n\t\tpointer-events: none;\n\t}\n\n\t.sb-empty-title\n\t{\n\t\tfont-size: $fs-4;\n\t\tfont-weight: 800;\n\t\tcolor: $text-dim;\n\t\tfont-family: $font-mono;\n\t\tletter-spacing: 0.08em;\n\t}\n\n\t.sb-empty-body\n\t{\n\t\tfont-size: $fs-2;\n\t\tfont-weight: 500;\n\t\tcolor: $text-faint;\n\t\tfont-family: $font-mono;\n\t\ttext-align: center;\n\t}\n\n\t.sb-tabs-arrow\n\t{\n\t\tdisplay: flex;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 36px;\n\t\theight: 40px;\n\t\tborder-radius: 8px;\n\t\tbackground-color: $bg-2;\n\t\tborder: 1px solid $line-strong;\n\t\tcolor: $text;\n\t\tfont-size: 22px;\n\t\tfont-weight: 800;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t\tz-index: 2;\n\n\t\t&:hover\n\t\t{\n\t\t\tborder-color: $accent;\n\t\t\tcolor: $accent;\n\t\t}\n\n\t\t&:active\n\t\t{\n\t\t\tsound-in: \"ui.button.press\";\n\t\t\ttransform: scale(0.96);\n\t\t}\n\n\t\t&.is-disabled\n\t\t{\n\t\t\topacity: 0.35;\n\t\t\tcursor: default;\n\t\t\tpointer-events: none;\n\t\t}\n\t}\n\n\t.sb-tabs-scroll\n\t{\n\t\tflex-grow: 1;\n\t\tflex-shrink: 1;\n\t\tmin-width: 0;\n\t\theight: 100%;\n\t\t// Clip the track; scrolling is done via margin-left on .sb-tabs-track\n\t\t// because Panel.ScrollSize often stays 0 for horizontal flex content.\n\t\toverflow: hidden;\n\t\tpointer-events: all;\n\t}\n\n\t.sb-tabs-track\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tflex-wrap: nowrap;\n\t\talign-items: center;\n\t\tgap: 6px;\n\t\t// Must grow with chips so content width > viewport (enables overflow).\n\t\twidth: auto;\n\t\tflex-shrink: 0;\n\t\theight: 100%;\n\t\tpadding: 2px 2px 4px;\n\t\t// margin-left is driven from code for arrow / wheel scrolling.\n\t}\n\n\t.sb-tab-chip\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 6px;\n\t\theight: 36px;\n\t\tpadding: 0 14px;\n\t\tborder-radius: 6px;\n\t\tbackground-color: $bg-2;\n\t\tborder: 1px solid $line-strong;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t\twhite-space: nowrap;\n\n\t\t&:hover\n\t\t{\n\t\t\tborder-color: $accent;\n\t\t}\n\n\t\t&.is-active\n\t\t{\n\t\t\tbackground-color: $accent;\n\t\t\tborder-color: $accent;\n\t\t\tcolor: $accent-ink;\n\n\t\t\t.sb-tab-chip-label\n\t\t\t{\n\t\t\t\tcolor: $accent-ink;\n\t\t\t}\n\t\t}\n\n\t\t&.is-search\n\t\t{\n\t\t\tbackground-color: #2a6df4;\n\t\t\tborder-color: #5b8dff;\n\t\t\tcolor: #fff;\n\n\t\t\t.sb-tab-chip-label\n\t\t\t{\n\t\t\t\tcolor: #fff;\n\t\t\t}\n\t\t}\n\n\t\t&:active\n\t\t{\n\t\t\tsound-in: \"ui.button.press\";\n\t\t\ttransform: scale(0.97);\n\t\t}\n\t}\n\n\t.sb-tab-chip-icon\n\t{\n\t\tfont-size: 13px;\n\t\tflex-shrink: 0;\n\t}\n\n\t.sb-tab-chip-label\n\t{\n\t\tfont-size: $fs-2;\n\t\tfont-weight: 700;\n\t\tfont-family: $font-mono;\n\t\tletter-spacing: 1.2px;\n\t\tcolor: $text-dim;\n\t\twhite-space: nowrap;\n\t\tflex-shrink: 0;\n\t}\n\n\t.sb-board\n\t{\n\t\tflex-grow: 1;\n\t\tflex-shrink: 1;\n\t\tmin-height: 0;\n\t\tmargin-bottom: 14px;\n\t\tborder: 1px solid $line;\n\t\tbackground-color: $bg-1;\n\t\tflex-direction: column;\n\t\toverflow: hidden;\n\t\tpointer-events: all;\n\t}\n\n\t.sb-board-scroll\n\t{\n\t\tflex-grow: 1;\n\t\tflex-shrink: 1;\n\t\tmin-height: 0;\n\t\tflex-direction: column;\n\t\toverflow-y: scroll;\n\t\toverflow-x: hidden;\n\t\tpointer-events: all;\n\t}\n\n\t.sb-pad-content\n\t{\n\t\tflex-direction: column;\n\t\tflex-shrink: 0;\n\t\twidth: 100%;\n\t\tpointer-events: all;\n\t}\n\n\t.tile-grid\n\t{\n\t\tflex-direction: row;\n\t\tflex-wrap: wrap;\n\t\talign-content: flex-start;\n\t\twidth: 100%;\n\t\tflex-shrink: 0;\n\t\tgap: 12px;\n\t\tpadding: 8px;\n\t\tpointer-events: all;\n\n\t\t.tile\n\t\t{\n\t\t\tpointer-events: all;\n\t\t\tflex-shrink: 0;\n\t\t}\n\t}\n\n\t.sb-pad-grid\n\t{\n\t\tpadding: 4px 4px 24px;\n\t}\n\n\t.tile\n\t{\n\t\twidth: 112px;\n\n\t\t&.is-loading\n\t\t{\n\t\t\tborder-color: $line-strong;\n\n\t\t\t.tile-swatch\n\t\t\t{\n\t\t\t\topacity: 0.55;\n\t\t\t}\n\n\t\t\t.tile-label\n\t\t\t{\n\t\t\t\tcolor: $text-dim;\n\t\t\t}\n\t\t}\n\n\t\t// Lobby co-op: pad lit by someone in the lobby (avatar + username).\n\t\t&.is-remote\n\t\t{\n\t\t\tborder-color: #5b8dff;\n\n\t\t\t.tile-label\n\t\t\t{\n\t\t\t\tcolor: #e8f0ff;\n\t\t\t\tbackground-color: rgba(42, 109, 244, 0.45);\n\t\t\t\tfont-weight: 800;\n\t\t\t}\n\t\t}\n\t}\n\n\t.tile-swatch\n\t{\n\t\theight: 64px;\n\t\tposition: relative;\n\t}\n\n\t.tile-remote-avatar\n\t{\n\t\tposition: absolute;\n\t\ttop: 4px;\n\t\tright: 4px;\n\t\twidth: 28px;\n\t\theight: 28px;\n\t\tborder-radius: 50%;\n\t\tbackground-size: cover;\n\t\tbackground-position: center;\n\t\tbackground-color: #0b1220;\n\t\tborder: 2px solid #ffffff;\n\t\tz-index: 3;\n\t\tpointer-events: none;\n\t}\n\n\t.tile-remote-name\n\t{\n\t\tposition: absolute;\n\t\tleft: 3px;\n\t\tright: 3px;\n\t\tbottom: 3px;\n\t\tpadding: 2px 4px;\n\t\tborder-radius: 4px;\n\t\tbackground-color: rgba(0, 0, 0, 0.72);\n\t\tcolor: #ffffff;\n\t\tfont-size: 9px;\n\t\tfont-weight: 800;\n\t\tfont-family: $font-mono;\n\t\tletter-spacing: 0.04em;\n\t\ttext-align: center;\n\t\ttext-transform: none;\n\t\twhite-space: nowrap;\n\t\toverflow: hidden;\n\t\ttext-overflow: ellipsis;\n\t\tz-index: 3;\n\t\tpointer-events: none;\n\t}\n\n\t.sb-controls\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tgap: 12px;\n\t\tflex-shrink: 0;\n\t\tmin-height: 56px;\n\t}\n\n\t// LOOP + CO-OP share one baseline.\n\t.sb-toggle-row\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tflex-shrink: 0;\n\t\tgap: 10px;\n\t}\n\n\t.sb-toggle\n\t{\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tflex-shrink: 0;\n\t\twidth: 100px;\n\t\theight: 44px;\n\t\tmin-width: 100px;\n\t\tmin-height: 44px;\n\t\tmax-height: 44px;\n\t\tpadding: 0 12px;\n\t\tborder-radius: 10px;\n\t\tfont-size: 13px;\n\t\tfont-weight: 900;\n\t\tletter-spacing: 0.16em;\n\t\tcolor: #ffffff;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t\t// s&box UI: use border shorthand only (border-style is invalid).\n\t\tborder: 3px solid #2a0000;\n\t\tbackground-color: #c10f0f;\n\t\tbox-shadow: 0 3px 0 #3a0000, inset 0 1px 4px rgba(255, 255, 255, 0.2);\n\t\ttext-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);\n\n\t\t&.is-on\n\t\t{\n\t\t\tborder: 3px solid #0a3d14;\n\t\t\tbackground-color: #1fbf3a;\n\t\t\tbox-shadow: 0 3px 0 #0f5a1c, 0 0 14px rgba(57, 255, 80, 0.35), inset 0 1px 4px rgba(255, 255, 255, 0.25);\n\t\t}\n\n\t\t&.is-off\n\t\t{\n\t\t\tborder: 3px solid #2a0000;\n\t\t\tbackground-color: #c10f0f;\n\t\t\tbox-shadow: 0 3px 0 #3a0000, inset 0 1px 4px rgba(255, 255, 255, 0.2);\n\t\t}\n\n\t\t// CO-OP on uses same green as LOOP (is-on); is-coop is optional marker.\n\t\t&.is-on.is-coop\n\t\t{\n\t\t\tborder: 3px solid #0a3d14;\n\t\t\tbackground-color: #1fbf3a;\n\t\t\tbox-shadow: 0 3px 0 #0f5a1c, 0 0 14px rgba(57, 255, 80, 0.35), inset 0 1px 4px rgba(255, 255, 255, 0.25);\n\t\t}\n\n\t\t&:active\n\t\t{\n\t\t\tsound-in: \"ui.button.press\";\n\t\t\ttransform: translateY(2px);\n\t\t}\n\t}\n\n\t.sb-now-playing\n\t{\n\t\tflex-grow: 1;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 10px;\n\t\tmin-width: 0;\n\t\tpadding: 10px 14px;\n\t\tbackground-color: $bg-2;\n\t\tborder: 1px solid $line;\n\t}\n\n\t.sb-now-playing-dot\n\t{\n\t\twidth: 8px;\n\t\theight: 8px;\n\t\tborder-radius: 50%;\n\t\tbackground-color: $line-strong;\n\t\tflex-shrink: 0;\n\n\t\t&.is-live\n\t\t{\n\t\t\tbackground-color: $accent;\n\t\t}\n\t}\n\n\t.sb-now-playing-text\n\t{\n\t\tfont-size: $fs-2;\n\t\tfont-weight: 600;\n\t\tcolor: $text-dim;\n\t\tfont-family: $font-mono;\n\t\twhite-space: nowrap;\n\t\toverflow: hidden;\n\t}\n\n\t.sb-kill-btn\n\t{\n\t\tmin-width: 148px;\n\t\tflex-shrink: 0;\n\t}\n\n\t&.is-wide\n\t{\n\t\t.tile\n\t\t{\n\t\t\twidth: 120px;\n\t\t}\n\t}\n\n\t&.is-medium\n\t{\n\t\t.sb-shell\n\t\t{\n\t\t\tpadding: 18px 20px 16px;\n\t\t}\n\n\t\t.sb-title\n\t\t{\n\t\t\tfont-size: $fs-4;\n\t\t}\n\n\t\t.tile\n\t\t{\n\t\t\twidth: 104px;\n\t\t}\n\t}\n\n\t&.is-compact\n\t{\n\t\t.sb-shell\n\t\t{\n\t\t\tpadding: 14px 14px 12px;\n\t\t}\n\n\t\t.sb-voice-chip\n\t\t{\n\t\t\twidth: 116px;\n\t\t\tmin-width: 116px;\n\t\t\tmax-width: 116px;\n\t\t}\n\n\t\t.tile\n\t\t{\n\t\t\twidth: 96px;\n\t\t}\n\n\t\t.tile-swatch\n\t\t{\n\t\t\theight: 56px;\n\t\t}\n\t}\n\n\t&.is-tight\n\t{\n\t\t.sb-shell\n\t\t{\n\t\t\tpadding: 12px 10px 10px;\n\t\t}\n\n\t\t.sb-search-bar\n\t\t{\n\t\t\theight: 36px;\n\t\t\tpadding: 0 10px;\n\t\t}\n\n\t\t.sb-search-count\n\t\t{\n\t\t\tmin-width: 28px;\n\t\t\tfont-size: 10px;\n\t\t}\n\n\t\t.sb-topbar\n\t\t{\n\t\t\theight: 58px;\n\t\t\tmin-height: 58px;\n\t\t\tmax-height: 58px;\n\t\t}\n\n\t\t.sb-voice-chip\n\t\t{\n\t\t\twidth: 108px;\n\t\t\tmin-width: 108px;\n\t\t\tmax-width: 108px;\n\t\t\theight: 48px;\n\t\t\tmin-height: 48px;\n\t\t\tmax-height: 48px;\n\t\t}\n\n\t\t.sb-voice-chip-value\n\t\t{\n\t\t\tfont-size: $fs-3;\n\t\t}\n\n\t\t.sb-title\n\t\t{\n\t\t\tfont-size: $fs-3;\n\t\t}\n\n\t\t.sb-tabs-bar\n\t\t{\n\t\t\theight: 44px;\n\t\t}\n\n\t\t.sb-tab-chip\n\t\t{\n\t\t\theight: 32px;\n\t\t\tpadding: 0 10px;\n\t\t}\n\n\t\t.sb-tab-chip-label\n\t\t{\n\t\t\tfont-size: 11px;\n\t\t}\n\n\t\t.tile\n\t\t{\n\t\t\twidth: 88px;\n\t\t}\n\n\t\t.tile-swatch\n\t\t{\n\t\t\theight: 50px;\n\t\t}\n\n\t\t.sb-controls\n\t\t{\n\t\t\tflex-wrap: wrap;\n\t\t}\n\n\t\t.sb-now-playing\n\t\t{\n\t\t\twidth: 100%;\n\t\t\torder: 3;\n\t\t}\n\n\t\t.sb-kill-btn\n\t\t{\n\t\t\twidth: 100%;\n\t\t\torder: 2;\n\t\t}\n\t}\n\n\t&.has-voices\n\t{\n\t\t.sb-topbar\n\t\t{\n\t\t\tborder-bottom-color: $accent-line;\n\t\t}\n\t}\n\n\t.sb-ach\n\t{\n\t\tposition: absolute;\n\t\tleft: 24px;\n\t\tbottom: 28px;\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tgap: 14px;\n\t\tmax-width: 420px;\n\t\tpadding: 14px 18px;\n\t\tborder-radius: 12px;\n\t\tbackground-color: rgba(12, 8, 20, 0.94);\n\t\tborder: 2px solid rgba(255, 210, 80, 0.75);\n\t\tbox-shadow: 0 12px 36px rgba(0, 0, 0, 0.55), 0 0 24px rgba(255, 180, 40, 0.22);\n\t\tpointer-events: none;\n\t\tz-index: 50;\n\t}\n\n\t.sb-ach-icon\n\t{\n\t\tfont-size: 28px;\n\t\tflex-shrink: 0;\n\t}\n\n\t.sb-ach-copy\n\t{\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 3px;\n\t\tmin-width: 0;\n\t}\n\n\t.sb-ach-kicker\n\t{\n\t\tfont-size: 9px;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 0.2em;\n\t\tcolor: #ffd24a;\n\t}\n\n\t.sb-ach-title\n\t{\n\t\tfont-size: 15px;\n\t\tfont-weight: 900;\n\t\tletter-spacing: 0.04em;\n\t\tcolor: #fff;\n\t}\n\n\t.sb-ach-body\n\t{\n\t\tfont-size: 11px;\n\t\tfont-weight: 500;\n\t\tletter-spacing: 0.02em;\n\t\tcolor: rgba(255, 255, 255, 0.72);\n\t\twhite-space: normal;\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "UI/Components/Modal/ModalPanel.razor",
"FileName": "ModalPanel.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@namespace Sunless.Libraries.UI\n@using Sandbox\n@using Sandbox.UI\n@attribute [StyleSheet]\n@inherits Panel\n\n<root class=\"modal-overlay @(IsOpen ? \"is-open\" : \"\") @ExtraClass\">\n\t@if ( IsOpen )\n\t{\n\t\t<div class=\"modal-scrim\" @onclick=@HandleScrimClick></div>\n\t\t<div class=\"modal-dialog\" style=@DialogStyle>\n\t\t\t@if ( Header is not null || !string.IsNullOrEmpty( Title ) || ShowClose )\n\t\t\t{\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t@if ( Header is not null )\n\t\t\t\t\t{\n\t\t\t\t\t\t@Header\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"modal-title\">@Title</div>\n\t\t\t\t\t}\n\t\t\t\t\t@if ( ShowClose )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"modal-close\" @onclick=@HandleClose>X</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t\t@if ( Body is not null )\n\t\t\t{\n\t\t\t\t<div class=\"modal-body\">@Body</div>\n\t\t\t}\n\n\t\t\t@if ( Footer is not null )\n\t\t\t{\n\t\t\t\t<div class=\"modal-footer\">@Footer</div>\n\t\t\t}\n\t\t</div>\n\t}\n</root>\n\n@code\n{\n\tpublic bool IsOpen { get; set; }\n\tpublic string Title { get; set; }\n\tpublic bool ShowClose { get; set; } = true;\n\tpublic bool DismissOnScrim { get; set; } = true;\n\tpublic string ExtraClass { get; set; }\n\tpublic string DialogStyle { get; set; }\n\tpublic Action OnClose { get; set; }\n\n\tpublic RenderFragment Header { get; set; }\n\tpublic RenderFragment Body { get; set; }\n\tpublic RenderFragment Footer { get; set; }\n\n\tvoid HandleClose()\n\t{\n\t\ttry { OnClose?.Invoke(); } catch ( Exception e ) { Log.Warning( $\"[Modal] OnClose threw: {e.Message}\" ); }\n\t}\n\n\tvoid HandleScrimClick()\n\t{\n\t\tif ( !DismissOnScrim ) return;\n\t\tHandleClose();\n\t}\n\n\tprotected override int BuildHash() => HashCode.Combine( IsOpen, Title, ShowClose, ExtraClass, DialogStyle );\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "UI/Components/Toast/Toast.cs",
"FileName": "Toast.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "namespace Sunless.Libraries.UI;\n\nusing System;\nusing Sandbox;\n\n// Singleton toast state. Host <ToastHost /> once. Call:\n//\n// Toaster.Show( \"#toast.saved\" );\n// Toaster.New().WithIcon( \"save\" ).WithCopy( path ).Show( \"#toast.saved\", path );\n//\npublic static class Toaster\n{\n\tpublic static string Title { get; private set; }\n\tpublic static string Message { get; private set; }\n\tpublic static string Icon { get; private set; }\n\tpublic static bool ShowCopy { get; private set; }\n\tpublic static string CopyValue { get; private set; }\n\tpublic static ToastTone Tone { get; private set; }\n\tpublic static bool IsOpen => !string.IsNullOrEmpty( Message );\n\tpublic static int Version { get; private set; }\n\n\tpublic static Action OnShowSound { get; set; }\n\tpublic static Action OnCopySound { get; set; }\n\n\tpublic static ToastBuilder New() => new();\n\n\tpublic static void Show( string message, string title = null )\n\t\t=> New().Show( message, title );\n\n\tpublic static void Dismiss()\n\t{\n\t\tif ( !IsOpen ) return;\n\t\tMessage = null;\n\t\tTitle = null;\n\t\tIcon = null;\n\t\tCopyValue = null;\n\t\tShowCopy = false;\n\t\tTone = ToastTone.Default;\n\t\tVersion++;\n\t}\n\n\tinternal static void RaiseCopySound()\n\t{\n\t\ttry { OnCopySound?.Invoke(); } catch ( Exception e ) { Log.Warning( $\"[Toaster] copy sound threw: {e.Message}\" ); }\n\t}\n\n\tinternal static void Commit( ToastBuilder b )\n\t{\n\t\tMessage = b._message;\n\t\tTitle = b._title;\n\t\tIcon = b._icon;\n\t\tShowCopy = b._showCopy;\n\t\tCopyValue = b._copyValue;\n\t\tTone = b._tone;\n\t\tVersion++;\n\t\ttry { (b._onShowSound ?? OnShowSound)?.Invoke(); } catch ( Exception e ) { Log.Warning( $\"[Toaster] show sound threw: {e.Message}\" ); }\n\t}\n}\n\npublic enum ToastTone { Default, Ok, Warn, Danger, Info }\n\npublic sealed class ToastBuilder\n{\n\tinternal string _title;\n\tinternal string _message;\n\tinternal string _icon;\n\tinternal bool _showCopy;\n\tinternal string _copyValue;\n\tinternal ToastTone _tone = ToastTone.Default;\n\tinternal Action _onShowSound;\n\n\tpublic ToastBuilder WithTitle( string title ) { _title = title; return this; }\n\tpublic ToastBuilder WithIcon( string ligature ) { _icon = ligature; return this; }\n\tpublic ToastBuilder WithCopy( string value = null ) { _showCopy = true; _copyValue = value; return this; }\n\tpublic ToastBuilder WithTone( ToastTone tone ) { _tone = tone; return this; }\n\tpublic ToastBuilder WithSound( Action sound ) { _onShowSound = sound; return this; }\n\n\tpublic void Show( string message, string title = null )\n\t{\n\t\t_message = message;\n\t\tif ( title is not null ) _title = title;\n\t\tToaster.Commit( this );\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "styles/base.scss",
"FileName": "base.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"base/_splitcontainer.scss\";\r\n@import \"base/_navigator.scss\";\r\n\r\nbutton\r\n{\r\n\tcursor: pointer;\r\n}\r\n\r\nIconPanel\r\n{\r\n\tfont-family: Material Icons;\r\n}\r\n\r\n.is-half\r\n{\r\n\twidth: 50%;\r\n}\r\n\r\n.is-third\r\n{\r\n\twidth: 33%;\r\n}\r\n\r\n.is-quarter\r\n{\r\n\twidth: 25%;\r\n}\r\n\r\nbutton.has-subtitle\r\n{\r\n\tposition: relative;\r\n\tflex-direction: column;\r\n\tjustify-content: flex-start;\r\n\talign-items: flex-start;\r\n\tpadding-left: 40px; // icon space\r\n\r\n\t.iconpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tleft: 5px;\r\n\t\ttop: 0;\r\n\t\tbottom: 0;\r\n\t\talign-items: center;\r\n\t}\r\n\r\n\t.button-label\r\n\t{\r\n\t\tfont-weight: bold;\r\n\t}\r\n\r\n\t.button-subtitle\r\n\t{\r\n\t\tfont-size: 12px;\r\n\t\topacity: 0.5;\r\n\t\tmix-blend-mode: lighten;\r\n\t}\r\n}"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/controlsheet/controlsheetrow.cs.scss",
"FileName": "controlsheetrow.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "\r\nControlSheetRow\r\n{\r\n\tflex-direction: row;\r\n\tflex-shrink: 0;\r\n\tmax-height: 32px;\r\n\tborder-radius: 4px;\r\n\r\n\t&.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n\r\nControlSheetRow > .left\r\n{\r\n\twidth: 150px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\tpadding: 6px;\r\n}\r\n\r\nControlSheetRow > .right\r\n{\r\n\tflex-grow: 1;\r\n}\r\n\r\nControlSheetRow > .left > .title\r\n{\r\n\twidth: 150px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\tfont-size: 13px;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n}\r\n\r\nControlSheetRow > .right > textentry,\r\nControlSheetRow > .right numberentry\r\n{\r\n\tbackground-color: #000a;\r\n\tflex-grow: 1;\r\n\tpadding: 4px 8px;\r\n\tborder-radius: 2px;\r\n}\r\n\r\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/floatdock/floatdock.scss",
"FileName": "floatdock.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/ui/styles/_theme.scss\";\n\n.float-dock {\n\tpointer-events: all;\n\tbackground-color: $bg-1;\n\tborder: 1px solid $line-strong;\n\tborder-left: 2px solid @import \"/ui/styles/_theme.scss\";\n\n.float-dock {\n\tpointer-events: all;\n\tbackground-color: $bg-1;\n\tborder: 1px solid $line-strong;\n\tborder-left: 2px solid $accent;\n\tpadding: 10px 14px;\n\tflex-direction: column;\n\tgap: 8px;\n\n\t&.float-dock--row { flex-direction: row; align-items: center; }\n\t&.float-dock--quiet { border-left-color: $line-mid; }\n\t&.float-dock--focus { border-left-color: $accent; }\n\t&.float-dock--danger { border-left-color: $danger; }\n}\n_TMP_PRIMARY;\n\tpadding: 10px 14px;\n\tflex-direction: column;\n\tgap: 8px;\n\n\t&.float-dock--row { flex-direction: row; align-items: center; }\n\t&.float-dock--quiet { border-left-color: $line-mid; }\n\t&.float-dock--focus { border-left-color: $accent; }\n\t&.float-dock--danger { border-left-color: $danger; }\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "ui/components/inputglyph/inputglyph.razor.scss",
"FileName": "inputglyph.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@import \"/ui/styles/_theme.scss\";\n\n.input-glyph {\n\tflex-direction: row;\n\talign-items: center;\n\tjustify-content: center;\n\n\timg {\n\t\twidth: 32px;\n\t\theight: 32px;\n\t}\n\n\t.input-glyph__fallback {\n\t\tpadding: 3px 8px;\n\t\tbackground-color: rgba(255, 255, 255, 0.06);\n\t\tborder: 1px solid $line-strong;\n\t\tcolor: $text;\n\t\tfont-family: $font-mono;\n\t\tfont-size: $fs-1;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 1px;\n\t\ttext-transform: uppercase;\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "UI/Components/EmptyState/EmptyState.razor",
"FileName": "EmptyState.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@namespace Sunless.Libraries.UI\n@using Sandbox\n@using Sandbox.UI\n@attribute [StyleSheet]\n@inherits Panel\n\n<root>\n\t<div class=\"empty\">\n\t\t<div class=\"empty-icon\">@Icon</div>\n\t\t<div class=\"empty-title\">@Title</div>\n\t\t@if ( !string.IsNullOrEmpty( Sub ) )\n\t\t{\n\t\t\t<div class=\"empty-sub\">@Sub</div>\n\t\t}\n\t</div>\n</root>\n\n@code\n{\n\tpublic string Icon { get; set; } = \"-- --\";\n\tpublic string Title { get; set; } = \"\";\n\tpublic string Sub { get; set; } = \"\";\n\n\tprotected override int BuildHash() => HashCode.Combine( Icon, Title, Sub );\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "UI/Components/Icon/Icon.razor",
"FileName": "Icon.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@namespace Sunless.Libraries.UI\n@using Sandbox\n@using Sandbox.UI\n@attribute [StyleSheet]\n@inherits Panel\n\n<root class=\"icon @SizeClass @ExtraClass\">@Name</root>\n\n@code\n{\n\tpublic string Name { get; set; } = \"\";\n\tpublic IconSize Size { get; set; } = IconSize.Medium;\n\tpublic string ExtraClass { get; set; }\n\n\tstring SizeClass => Size switch\n\t{\n\t\tIconSize.Small => \"icon-sm\",\n\t\tIconSize.Large => \"icon-lg\",\n\t\tIconSize.XLarge => \"icon-xl\",\n\t\t_ => \"icon-md\",\n\t};\n\n\tprotected override int BuildHash() => HashCode.Combine( Name, Size, ExtraClass );\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "UI/Components/Menu/MenuWidgetBuilders.cs",
"FileName": "MenuWidgetBuilders.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "namespace Sunless.Libraries.UI;\n\nusing System;\nusing System.Collections.Generic;\n\npublic sealed class MenuLinkBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuLinkWidget _w;\n\n\tinternal MenuLinkBuilder( MenuScreenBuilder screen, string label, string icon )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuLinkWidget { Label = label, Icon = icon, Style = MenuLinkStyle.Row };\n\t}\n\n\tpublic MenuLinkBuilder WithStyle( MenuLinkStyle style ) { _w.Style = style; return this; }\n\tpublic MenuLinkBuilder WithTone( MenuTone tone ) { _w.Tone = tone; return this; }\n\tpublic MenuLinkBuilder WithHint( string hint ) { _w.Hint = hint; return this; }\n\tpublic MenuLinkBuilder WhenEnabled( Func<bool> enabled ) { _w.Enabled = enabled; return this; }\n\tpublic MenuLinkBuilder WithGoTo( string routeId ) { _w.RouteId = routeId; return this; }\n\tpublic MenuLinkBuilder WithAction( Action invoke ) { _w.Invoke = invoke; return this; }\n\tpublic MenuLinkBuilder Grows() { _w.Grow = true; return this; }\n\n\tpublic MenuScreenBuilder EndLink()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuToggleBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuToggleWidget _w;\n\n\tinternal MenuToggleBuilder( MenuScreenBuilder screen, string label )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuToggleWidget { Label = label };\n\t}\n\n\tpublic MenuToggleBuilder WithHint( string hint ) { _w.Hint = hint; return this; }\n\tpublic MenuToggleBuilder WithGetter( Func<bool> get ) { _w.Get = get; return this; }\n\tpublic MenuToggleBuilder WithSetter( Action<bool> set ) { _w.Set = set; return this; }\n\n\tpublic MenuToggleBuilder Bind( Func<bool> get, Action<bool> set )\n\t{\n\t\t_w.Get = get;\n\t\t_w.Set = set;\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndToggle()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuRangeBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuRangeWidget _w;\n\n\tinternal MenuRangeBuilder( MenuScreenBuilder screen, string label )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuRangeWidget { Label = label, Min = 0f, Max = 1f, Step = 0.01f, Format = \"0.00\" };\n\t}\n\n\tpublic MenuRangeBuilder WithHint( string hint ) { _w.Hint = hint; return this; }\n\tpublic MenuRangeBuilder WithRange( float min, float max ) { _w.Min = min; _w.Max = max; return this; }\n\tpublic MenuRangeBuilder WithStep( float step ) { _w.Step = step; return this; }\n\tpublic MenuRangeBuilder WithFormat( string format ) { _w.Format = format; return this; }\n\n\tpublic MenuRangeBuilder Bind( Func<float> get, Action<float> set )\n\t{\n\t\t_w.Get = get;\n\t\t_w.Set = set;\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndRange()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuSegmentedBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuSegmentedWidget _w;\n\n\tinternal MenuSegmentedBuilder( MenuScreenBuilder screen, string label, string[] choices )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuSegmentedWidget { Label = label, Choices = choices };\n\t}\n\n\tpublic MenuSegmentedBuilder WithHint( string hint ) { _w.Hint = hint; return this; }\n\n\tpublic MenuSegmentedBuilder Bind( Func<int> get, Action<int> set )\n\t{\n\t\t_w.Get = get;\n\t\t_w.Set = set;\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndSegmented()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuRadioBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuRadioWidget _w;\n\treadonly List<MenuRadioWidget.Option> _options = new();\n\n\tinternal MenuRadioBuilder( MenuScreenBuilder screen, string label )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuRadioWidget { Label = label };\n\t}\n\n\tpublic MenuRadioBuilder WithHint( string hint ) { _w.Hint = hint; return this; }\n\n\tpublic MenuRadioBuilder WithOption( string label, string icon = null, string hint = null )\n\t{\n\t\t_options.Add( new MenuRadioWidget.Option { Label = label, Icon = icon, Hint = hint } );\n\t\treturn this;\n\t}\n\n\tpublic MenuRadioBuilder Bind( Func<int> get, Action<int> set )\n\t{\n\t\t_w.Get = get;\n\t\t_w.Set = set;\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndRadio()\n\t{\n\t\t_w.Options = _options;\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuInfoBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuInfoWidget _w;\n\n\tinternal MenuInfoBuilder( MenuScreenBuilder screen, string label )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuInfoWidget { Label = label };\n\t}\n\n\tpublic MenuInfoBuilder WithIcon( string icon ) { _w.Icon = icon; return this; }\n\tpublic MenuInfoBuilder WithGetter( Func<string> get ) { _w.Get = get; return this; }\n\tpublic MenuInfoBuilder WithValue( string value ) { _w.Get = () => value; return this; }\n\n\tpublic MenuScreenBuilder EndInfo()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuTextFieldBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuTextFieldWidget _w;\n\n\tinternal MenuTextFieldBuilder( MenuScreenBuilder screen, string label )\n\t{\n\t\t_screen = screen;\n\t\t_w = new MenuTextFieldWidget { Label = label };\n\t}\n\n\tpublic MenuTextFieldBuilder WithHint( string hint ) { _w.Hint = hint; return this; }\n\n\tpublic MenuTextFieldBuilder Bind( Func<string> get, Action<string> set )\n\t{\n\t\t_w.Get = get;\n\t\t_w.Set = set;\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndTextField()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\n// === Composite widgets ===============================================\n\npublic sealed class MenuHeroCardBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuHeroCardWidget _w = new();\n\n\tinternal MenuHeroCardBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuHeroCardBuilder WithTitle( string title ) { _w.Title = title; return this; }\n\tpublic MenuHeroCardBuilder WithSubtitle( string subtitle ) { _w.Subtitle = subtitle; return this; }\n\tpublic MenuHeroCardBuilder WithDescription( string description ) { _w.Description = description; return this; }\n\tpublic MenuHeroCardBuilder WithImage( string url ) { _w.Image = url; return this; }\n\tpublic MenuHeroCardBuilder WithIcon( string icon ) { _w.Icon = icon; return this; }\n\n\tpublic MenuScreenBuilder EndHeroCard()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuMapCardBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuMapCardWidget _w = new();\n\n\tinternal MenuMapCardBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuMapCardBuilder WithTitle( string title ) { _w.Title = title; return this; }\n\tpublic MenuMapCardBuilder WithDescription( string description ) { _w.Description = description; return this; }\n\tpublic MenuMapCardBuilder WithImage( string url ) { _w.Image = url; return this; }\n\n\tpublic MenuMapCardBuilder WithChip( string label, string icon = null, MenuTone tone = MenuTone.Confirm )\n\t{\n\t\t_w.ChipLabel = label;\n\t\t_w.ChipIcon = icon;\n\t\t_w.ChipTone = tone;\n\t\treturn this;\n\t}\n\n\tpublic MenuMapCardBuilder WithAction( string label, string icon, Action invoke, string routeId = null )\n\t{\n\t\t_w.ActionLabel = label;\n\t\t_w.ActionIcon = icon;\n\t\t_w.ActionInvoke = invoke;\n\t\t_w.ActionRouteId = routeId;\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndMapCard()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuMapGridBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuMapGridWidget _w = new();\n\treadonly List<MenuMapGridWidget.Card> _cards = new();\n\n\tinternal MenuMapGridBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuMapGridBuilder WithActive( Func<int> get ) { _w.Active = get; return this; }\n\tpublic MenuMapGridBuilder OnPick( Action<int> set ) { _w.Pick = set; return this; }\n\n\tpublic MenuMapCardCardBuilder WithCard( string title )\n\t\t=> new( this, title );\n\n\tinternal void AddCard( MenuMapGridWidget.Card card ) => _cards.Add( card );\n\n\tbool _closed;\n\n\tpublic MenuScreenBuilder EndMapGrid()\n\t{\n\t\tif ( _closed ) return _screen;\n\t\t_closed = true;\n\t\t_w.Cards = _cards;\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuMapCardCardBuilder\n{\n\treadonly MenuMapGridBuilder _grid;\n\treadonly MenuMapGridWidget.Card _c;\n\n\tinternal MenuMapCardCardBuilder( MenuMapGridBuilder grid, string title )\n\t{\n\t\t_grid = grid;\n\t\t_c = new MenuMapGridWidget.Card { Title = title };\n\t}\n\n\tpublic MenuMapCardCardBuilder WithDescription( string text ) { _c.Description = text; return this; }\n\tpublic MenuMapCardCardBuilder WithImage( string url ) { _c.Image = url; return this; }\n\tpublic MenuMapCardCardBuilder WithBadge( string label, MenuTone tone = MenuTone.Confirm )\n\t{\n\t\t_c.BadgeLabel = label;\n\t\t_c.BadgeTone = tone;\n\t\treturn this;\n\t}\n\n\tpublic MenuMapCardCardBuilder ComingSoon( bool v = true ) { _c.ComingSoon = v; return this; }\n\tpublic MenuMapCardCardBuilder Locked( bool v = true ) { _c.Locked = v; return this; }\n\n\tpublic MenuMapGridBuilder EndCard()\n\t{\n\t\t_grid.AddCard( _c );\n\t\treturn _grid;\n\t}\n}\n\npublic sealed class MenuTileGridBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuTileGridWidget _w = new();\n\treadonly List<MenuTileGridWidget.Item> _items = new();\n\n\tinternal MenuTileGridBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuTileGridBuilder WithTile( string label, string color = null, string image = null )\n\t{\n\t\t_items.Add( new MenuTileGridWidget.Item { Label = label, Color = color, Image = image } );\n\t\treturn this;\n\t}\n\n\tpublic MenuTileGridBuilder WithActive( Func<int> get ) { _w.Active = get; return this; }\n\tpublic MenuTileGridBuilder OnPick( Action<int> set ) { _w.Pick = set; return this; }\n\n\tpublic MenuScreenBuilder EndTileGrid()\n\t{\n\t\t_w.Items = _items;\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuPlayerListBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuPlayerListWidget _w = new();\n\treadonly List<MenuPlayerListWidget.Player> _players = new();\n\n\tinternal MenuPlayerListBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuPlayerListBuilder WithPlayer( string name, bool ready = false, bool host = false )\n\t{\n\t\t_players.Add( new MenuPlayerListWidget.Player { Name = name, Ready = ready, Host = host } );\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndPlayerList()\n\t{\n\t\t_w.Players = _players;\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuKeyBindingsBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuKeyBindingsWidget _w = new();\n\treadonly List<MenuKeyBindingsWidget.Binding> _bindings = new();\n\n\tinternal MenuKeyBindingsBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuKeyBindingsBuilder WithBinding( string action, params string[] keys )\n\t{\n\t\t_bindings.Add( new MenuKeyBindingsWidget.Binding { Action = action, Keys = keys } );\n\t\treturn this;\n\t}\n\n\tpublic MenuScreenBuilder EndKeyBindings()\n\t{\n\t\t_w.Bindings = _bindings;\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuPromptCardBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuPromptCardWidget _w = new() { Verb = \"Press\" };\n\n\tinternal MenuPromptCardBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuPromptCardBuilder WithVerb( string verb ) { _w.Verb = verb; return this; }\n\tpublic MenuPromptCardBuilder WithPrompt( string prompt ) { _w.Prompt = prompt; return this; }\n\tpublic MenuPromptCardBuilder WithGlyph( string ligature ) { _w.Glyph = ligature; return this; }\n\tpublic MenuPromptCardBuilder WithCost( int cost ) { _w.Cost = cost; return this; }\n\tpublic MenuPromptCardBuilder WhenDisabled( Func<bool> d ) { _w.Disabled = d; return this; }\n\tpublic MenuPromptCardBuilder WithHoldProgress( Func<float> p ) { _w.Progress = p; return this; }\n\n\tpublic MenuScreenBuilder EndPromptCard()\n\t{\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuActionRowBuilder\n{\n\treadonly MenuScreenBuilder _screen;\n\treadonly MenuActionRowWidget _w = new();\n\treadonly List<MenuLinkWidget> _links = new();\n\n\tinternal MenuActionRowBuilder( MenuScreenBuilder screen ) { _screen = screen; }\n\n\tpublic MenuActionRowLinkBuilder WithLink( string label, string icon = null )\n\t\t=> new( this, label, icon );\n\n\tinternal void AddLink( MenuLinkWidget link ) => _links.Add( link );\n\n\tpublic MenuScreenBuilder EndActionRow()\n\t{\n\t\t_w.Links = _links;\n\t\t_screen.AddWidget( _w );\n\t\treturn _screen;\n\t}\n}\n\npublic sealed class MenuActionRowLinkBuilder\n{\n\treadonly MenuActionRowBuilder _row;\n\treadonly MenuLinkWidget _w;\n\n\tinternal MenuActionRowLinkBuilder( MenuActionRowBuilder row, string label, string icon )\n\t{\n\t\t_row = row;\n\t\t_w = new MenuLinkWidget { Label = label, Icon = icon, Style = MenuLinkStyle.Button };\n\t}\n\n\tpublic MenuActionRowLinkBuilder WithTone( MenuTone tone ) { _w.Tone = tone; return this; }\n\tpublic MenuActionRowLinkBuilder WithGoTo( string routeId ) { _w.RouteId = routeId; return this; }\n\tpublic MenuActionRowLinkBuilder WithAction( Action invoke ) { _w.Invoke = invoke; return this; }\n\tpublic MenuActionRowLinkBuilder WhenEnabled( Func<bool> enabled ) { _w.Enabled = enabled; return this; }\n\tpublic MenuActionRowLinkBuilder Grows() { _w.Grow = true; return this; }\n\n\tpublic MenuActionRowBuilder EndLink()\n\t{\n\t\t_row.AddLink( _w );\n\t\treturn _row;\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "UI/Components/Menu/PromptCard.razor",
"FileName": "PromptCard.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "@namespace Sunless.Libraries.UI\n@using Sandbox\n@using Sandbox.UI\n@inherits Panel\n@attribute [StyleSheet]\n\n<root class=\"@(IsDisabled ? \"disabled\" : \"\")\">\n\t<div class=\"prompt-frame\">\n\t\t<div class=\"prompt-vignette\"></div>\n\n\t\t<div class=\"prompt-content\">\n\t\t\t<div class=\"prompt-glyph\">\n\t\t\t\t@if ( !string.IsNullOrEmpty( Glyph ) )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"kbd\">@Glyph</div>\n\t\t\t\t}\n\t\t\t</div>\n\n\t\t\t<div class=\"prompt-text\">\n\t\t\t\t<div class=\"prompt-verb\">@EffectiveVerb</div>\n\t\t\t\t<div class=\"prompt-label\">@Prompt</div>\n\t\t\t</div>\n\n\t\t\t@if ( Cost > 0 )\n\t\t\t{\n\t\t\t\t<div class=\"prompt-cost\">\n\t\t\t\t\t<div class=\"prompt-cost-value\">@Cost</div>\n\t\t\t\t\t<div class=\"prompt-cost-unit\">PTS</div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t<div class=\"prompt-rule\">\n\t\t\t<div class=\"prompt-rule-fill\" @ref=\"ruleFill\"></div>\n\t\t</div>\n\t</div>\n</root>\n\n@code\n{\n\tpublic string Verb { get; set; } = \"Press\";\n\tpublic string Prompt { get; set; } = \"\";\n\tpublic string Glyph { get; set; } = \"E\";\n\tpublic int Cost { get; set; }\n\tpublic bool IsDisabled { get; set; }\n\tpublic Func<float> HoldProgressFn { get; set; }\n\n\tPanel ruleFill { get; set; }\n\n\tstring EffectiveVerb => IsDisabled && Cost > 0 ? \"Not enough points\" : Verb;\n\n\tpublic override void Tick()\n\t{\n\t\tif ( ruleFill is null ) return;\n\n\t\tvar p = HoldProgressFn?.Invoke() ?? 0f;\n\t\tif ( p < 0f ) p = 0f;\n\t\telse if ( p > 1f ) p = 1f;\n\n\t\truleFill.Style.Opacity = p > 0f ? 1f : 0f;\n\t\truleFill.Style.Width = Length.Percent( p * 100f );\n\t}\n\n\tprotected override int BuildHash() => HashCode.Combine( Verb, Prompt, Glyph, Cost, IsDisabled );\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "SoundBoardNet.cs",
"FileName": "SoundBoardNet.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "using System;\n\nnamespace SSoundboard;\n\n/// <summary>\n/// Network relay for co-op pad presses. Host owns one scene object; clients\n/// call Rpc.Host then host rebroadcasts so every machine hears the chaos.\n/// </summary>\npublic sealed class SoundBoardNet : Component\n{\n\tpublic static SoundBoardNet Instance { get; private set; }\n\n\tprotected override void OnEnabled()\n\t{\n\t\tInstance = this;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( Instance == this )\n\t\t\tInstance = null;\n\t}\n\n\tprotected override void OnDestroy()\n\t{\n\t\tif ( Instance == this )\n\t\t\tInstance = null;\n\t}\n\n\t/// <summary>Host creates a networked singleton when a multiplayer session is live.</summary>\n\tpublic static void EnsureHostRelay()\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( !Networking.IsActive )\n\t\t\t\treturn;\n\n\t\t\tif ( Instance.IsValid() )\n\t\t\t\treturn;\n\n\t\t\tif ( !Networking.IsHost )\n\t\t\t\treturn;\n\n\t\t\tvar go = new GameObject( true, \"SoundBoardNet\" );\n\t\t\tgo.Components.Create<SoundBoardNet>();\n\t\t\tgo.NetworkSpawn();\n\t\t\tLog.Info( \"[S&Soundboard] CO-OP network relay spawned\" );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"[S&Soundboard] CO-OP relay spawn failed: {e.Message}\" );\n\t\t}\n\t}\n\n\t/// <summary>Local player pressed a pad while CO-OP is on \u2014 share with the lobby.</summary>\n\tpublic static void SharePadPress( SoundPad pad )\n\t{\n\t\tif ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )\n\t\t\treturn;\n\n\t\tif ( !SoundBoardCoop.Enabled )\n\t\t\treturn;\n\n\t\ttry\n\t\t{\n\t\t\tif ( !Networking.IsActive )\n\t\t\t\treturn;\n\n\t\t\tEnsureHostRelay();\n\n\t\t\tvar net = Instance;\n\t\t\tif ( !net.IsValid() )\n\t\t\t\treturn;\n\n\t\t\t// Calling Rpc on a non-networked object throws (\"must be networked\").\n\t\t\ttry\n\t\t\t{\n\t\t\t\tif ( net.GameObject is null || !net.GameObject.Network.Active )\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t\tcatch\n\t\t\t{\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tnet.RequestPad(\n\t\t\t\tpad.SoundId,\n\t\t\t\tpad.Label ?? \"\",\n\t\t\t\tpad.Color ?? \"#ffffff\",\n\t\t\t\tpad.Volume <= 0f ? 1f : pad.Volume );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\t// Never let lobby sync break local button presses.\n\t\t\tLog.Warning( $\"[S&Soundboard] CO-OP share failed: {e.GetType().Name}: {e.Message}\" );\n\t\t}\n\t}\n\n\t[Rpc.Host]\n\tvoid RequestPad( string soundId, string label, string color, float volume )\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( string.IsNullOrWhiteSpace( soundId ) )\n\t\t\t\treturn;\n\n\t\t\tvar caller = Rpc.Caller;\n\t\t\tvar name = ResolveDisplayName( caller );\n\t\t\tvar steamId = ResolveSteamId( caller );\n\n\t\t\t// Host rebroadcasts to everyone (including the original presser).\n\t\t\tReceivePad( soundId, label ?? \"\", color ?? \"#ffffff\", volume, name, steamId );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"[S&Soundboard] CO-OP RequestPad failed: {e.GetType().Name}: {e.Message}\" );\n\t\t}\n\t}\n\n\tstatic string ResolveDisplayName( Connection caller )\n\t{\n\t\tif ( caller is null )\n\t\t\treturn \"Player\";\n\n\t\ttry\n\t\t{\n\t\t\tif ( !string.IsNullOrWhiteSpace( caller.DisplayName ) )\n\t\t\t\treturn caller.DisplayName;\n\t\t}\n\t\tcatch { /* ignore */ }\n\n\t\ttry\n\t\t{\n\t\t\tif ( !string.IsNullOrWhiteSpace( caller.Name ) )\n\t\t\t\treturn caller.Name;\n\t\t}\n\t\tcatch { /* ignore */ }\n\n\t\treturn \"Player\";\n\t}\n\n\tstatic long ResolveSteamId( Connection caller )\n\t{\n\t\tif ( caller is null )\n\t\t\treturn 0;\n\n\t\t// Prefer string parse \u2014 SteamId / Friend.Id shapes vary; never throw on cast.\n\t\ttry\n\t\t{\n\t\t\tvar id = ParseSteamId( caller.FriendInfo.Id );\n\t\t\tif ( id != 0 )\n\t\t\t\treturn id;\n\t\t}\n\t\tcatch { /* ignore */ }\n\n\t\ttry\n\t\t{\n\t\t\tvar id = ParseSteamId( caller.SteamId );\n\t\t\tif ( id != 0 )\n\t\t\t\treturn id;\n\t\t}\n\t\tcatch { /* ignore */ }\n\n\t\ttry\n\t\t{\n\t\t\treturn ParseSteamId( caller.OwnerSteamId );\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\tstatic long ParseSteamId( object value )\n\t{\n\t\tif ( value is null )\n\t\t\treturn 0;\n\n\t\ttry\n\t\t{\n\t\t\tswitch ( value )\n\t\t\t{\n\t\t\t\tcase long l:\n\t\t\t\t\treturn l;\n\t\t\t\tcase ulong ul:\n\t\t\t\t\treturn unchecked( (long)ul );\n\t\t\t\tcase int i:\n\t\t\t\t\treturn i;\n\t\t\t\tcase uint ui:\n\t\t\t\t\treturn ui;\n\t\t\t}\n\t\t}\n\t\tcatch { /* fall through */ }\n\n\t\ttry\n\t\t{\n\t\t\tvar s = value.ToString();\n\t\t\tif ( string.IsNullOrWhiteSpace( s ) )\n\t\t\t\treturn 0;\n\t\t\t// SteamId.ToString() is typically the numeric account id.\n\t\t\tif ( long.TryParse( s, out var parsed ) )\n\t\t\t\treturn parsed;\n\t\t}\n\t\tcatch { /* ignore */ }\n\n\t\treturn 0;\n\t}\n\n\t[Rpc.Broadcast( NetFlags.HostOnly | NetFlags.Reliable )]\n\tvoid ReceivePad( string soundId, string label, string color, float volume, string playerName, long steamId )\n\t{\n\t\tSoundBoardCoop.OnNetworkPad( soundId, label, color, volume, playerName, steamId );\n\t}\n}\n"
},
{
"Ident": "dogecoin.ssoundboard",
"Path": "SoundPreloadService.cs",
"FileName": "SoundPreloadService.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 341579,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace SSoundboard;\n\n/// <summary>\n/// On-demand sound loading. Prefer TryGet (already compiled); LoadAsync only on click.\n/// </summary>\npublic static class SoundPreloadService\n{\n\tenum LoadState { Pending, Loading, Ready, Failed }\n\n\tstatic readonly Dictionary<string, LoadState> _states = new( StringComparer.OrdinalIgnoreCase );\n\tstatic readonly List<string> _priority = new();\n\tstatic readonly Dictionary<string, SoundPad> _pendingPlays = new( StringComparer.OrdinalIgnoreCase );\n\n\tstatic int _loaded;\n\tstatic int _registered;\n\tstatic int _activeLoads;\n\tstatic bool _initialized;\n\tstatic bool _startupComplete;\n\n\tconst int MaxConcurrentLoads = 2;\n\tconst int StartupWarmCap = 32;\n\n\tpublic static event Action Changed;\n\n\tpublic static int LoadedCount => _loaded;\n\tpublic static bool IsInitialized => _initialized;\n\tpublic static bool IsStartupComplete => _startupComplete;\n\t/// <summary>Bumps when pending-play set changes \u2014 cheap UI rebuild signal.</summary>\n\tpublic static int PendingRevision { get; private set; }\n\n\tpublic static void Initialize()\n\t{\n\t\tif ( _initialized )\n\t\t\treturn;\n\n\t\t_initialized = true;\n\n\t\tvar warmed = 0;\n\t\tforeach ( var soundId in SoundBoardData.StartupSoundIds )\n\t\t{\n\t\t\tif ( string.IsNullOrWhiteSpace( soundId ) )\n\t\t\t\tcontinue;\n\n\t\t\tif ( TryMarkReadyIfPresent( Normalize( soundId ) ) )\n\t\t\t\twarmed++;\n\n\t\t\tif ( warmed >= StartupWarmCap )\n\t\t\t\tbreak;\n\t\t}\n\n\t\t_startupComplete = true;\n\t\tChanged?.Invoke();\n\t}\n\n\tstatic void EnsureInitialized()\n\t{\n\t\tif ( !_initialized )\n\t\t\tInitialize();\n\t}\n\n\tpublic static bool IsReady( string soundId )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( soundId ) )\n\t\t\treturn false;\n\n\t\tEnsureInitialized();\n\t\tvar key = Normalize( soundId );\n\n\t\tif ( _states.TryGetValue( key, out var state ) )\n\t\t{\n\t\t\tif ( state == LoadState.Ready )\n\t\t\t\treturn true;\n\t\t\tif ( state is LoadState.Failed or LoadState.Loading )\n\t\t\t\treturn false;\n\t\t}\n\n\t\treturn TryMarkReadyIfPresent( key );\n\t}\n\n\tpublic static void BumpPriority( string soundId )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( soundId ) )\n\t\t\treturn;\n\n\t\tEnsureInitialized();\n\t\tvar key = Normalize( soundId );\n\n\t\tif ( IsReady( key ) )\n\t\t\treturn;\n\n\t\tif ( _states.TryGetValue( key, out var state ) )\n\t\t{\n\t\t\tif ( state is LoadState.Loading or LoadState.Ready )\n\t\t\t\treturn;\n\n\t\t\tif ( state == LoadState.Failed )\n\t\t\t\t_states[key] = LoadState.Pending;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_states[key] = LoadState.Pending;\n\t\t\t_registered++;\n\t\t}\n\n\t\t_priority.Remove( key );\n\t\t_priority.Insert( 0, key );\n\t\tChanged?.Invoke();\n\t}\n\n\tpublic static void QueuePlayWhenReady( SoundPad pad )\n\t{\n\t\tif ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )\n\t\t\treturn;\n\n\t\tvar key = Normalize( pad.SoundId );\n\t\t_pendingPlays[key] = pad;\n\t\tPendingRevision++;\n\t\tBumpPriority( key );\n\t}\n\n\tpublic static bool TryTakePendingPlay( string soundId, out SoundPad pad )\n\t{\n\t\tvar removed = _pendingPlays.Remove( Normalize( soundId ), out pad );\n\t\tif ( removed )\n\t\t\tPendingRevision++;\n\t\treturn removed;\n\t}\n\n\tpublic static bool IsPendingPlay( string soundId )\n\t{\n\t\treturn _pendingPlays.ContainsKey( Normalize( soundId ) );\n\t}\n\n\tpublic static void Tick()\n\t{\n\t\tEnsureInitialized();\n\t\tTryPumpLoads();\n\t}\n\n\tstatic void TryPumpLoads()\n\t{\n\t\twhile ( _activeLoads < MaxConcurrentLoads && TryDequeue( out var soundId ) )\n\t\t{\n\t\t\tif ( TryMarkReadyIfPresent( soundId ) )\n\t\t\t{\n\t\t\t\tSoundBoardManager.OnSoundReady( soundId );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t_activeLoads++;\n\t\t\t_states[soundId] = LoadState.Loading;\n\t\t\t_ = LoadOneAsync( soundId );\n\t\t}\n\t}\n\n\tstatic bool TryDequeue( out string soundId )\n\t{\n\t\twhile ( _priority.Count > 0 )\n\t\t{\n\t\t\tvar next = _priority[0];\n\t\t\t_priority.RemoveAt( 0 );\n\n\t\t\tif ( !_states.TryGetValue( next, out var st ) || st != LoadState.Pending )\n\t\t\t\tcontinue;\n\n\t\t\tsoundId = next;\n\t\t\treturn true;\n\t\t}\n\n\t\tsoundId = null;\n\t\treturn false;\n\t}\n\n\tstatic async Task LoadOneAsync( string soundId )\n\t{\n\t\tvar path = ToSoundPath( soundId );\n\n\t\ttry\n\t\t{\n\t\t\tif ( ResourceLibrary.TryGet<SoundEvent>( path, out _ ) )\n\t\t\t{\n\t\t\t\tMarkReady( soundId );\n\t\t\t\tSoundBoardManager.OnSoundReady( soundId );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar loaded = await ResourceLibrary.LoadAsync<SoundEvent>( path );\n\t\t\tif ( loaded is null )\n\t\t\t{\n\t\t\t\tMarkFailed( soundId );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry { Sound.Preload( path ); }\n\t\t\tcatch { /* playable without preload */ }\n\n\t\t\tMarkReady( soundId );\n\t\t\tSoundBoardManager.OnSoundReady( soundId );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"[S&Soundboard] Sound load skipped: {path} ({e.GetType().Name})\" );\n\t\t\tMarkFailed( soundId );\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\t_activeLoads = Math.Max( 0, _activeLoads - 1 );\n\t\t}\n\t}\n\n\tstatic bool TryMarkReadyIfPresent( string key )\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( !ResourceLibrary.TryGet<SoundEvent>( ToSoundPath( key ), out _ ) )\n\t\t\t\treturn false;\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tMarkReady( key );\n\t\treturn true;\n\t}\n\n\tstatic void MarkReady( string key )\n\t{\n\t\tvar already = _states.TryGetValue( key, out var prev ) && prev == LoadState.Ready;\n\t\t_states[key] = LoadState.Ready;\n\t\tif ( !already )\n\t\t\t_loaded++;\n\t\tChanged?.Invoke();\n\t}\n\n\tstatic void MarkFailed( string key )\n\t{\n\t\t_states[key] = LoadState.Failed;\n\t\tif ( _pendingPlays.Remove( key ) )\n\t\t\tPendingRevision++;\n\t\tChanged?.Invoke();\n\t}\n\n\tstatic string Normalize( string soundId )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( soundId ) )\n\t\t\treturn soundId;\n\n\t\tsoundId = soundId.Replace( '\\\\', '/' ).Trim();\n\t\treturn soundId.EndsWith( \".sound\", StringComparison.OrdinalIgnoreCase )\n\t\t\t? soundId[..^6]\n\t\t\t: soundId;\n\t}\n\n\tstatic string ToSoundPath( string soundId ) => $\"{Normalize( soundId )}.sound\";\n}\n"
}
]
}