]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Added Jukebox (#26736)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Wed, 17 Apr 2024 09:27:00 +0000 (19:27 +1000)
committerGitHub <noreply@github.com>
Wed, 17 Apr 2024 09:27:00 +0000 (19:27 +1000)
* Added Jukebox, along with music for jukebox

* Fixed Jukebox meta.json copyright

* Removed songs I couldn't find a license for.

* Renamed files to solve check failures from spaces

* Added missing attributions.yml

* Fixed lack of description in Jukebox

* Jukebox is now constructable.

* Change Jukebox menu to FancyWindow

* Moved Jukebox messages out of jukebox component

* Removed Jukebox OnValueChanged.

* JukeboxComp now uses AutoGenerateComponentState

* Removed state code, since it's auto generated

* Fixed various Jukebox code to match conventions.

* Updated Standard.yml to match changed song list.

* fixes

* Jukebox workin

* Fix

* Polishing

* Finalising

* Revert

* bad

* jukey

* Reviews

* name

* Update submodule to 218.2.0

---------

Co-authored-by: iNVERTED <alextjorgensen@gmail.com>
27 files changed:
Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Audio/Jukebox/JukeboxMenu.xaml [new file with mode: 0644]
Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs [new file with mode: 0644]
Content.Client/Audio/Jukebox/JukeboxSystem.cs [new file with mode: 0644]
Content.Server/Audio/Jukebox/JukeboxSystem.cs [new file with mode: 0644]
Content.Shared/Audio/Jukebox/JukeboxComponent.cs [new file with mode: 0644]
Content.Shared/Audio/Jukebox/JukeboxPrototype.cs [new file with mode: 0644]
Content.Shared/Audio/Jukebox/JukeboxUi.cs [new file with mode: 0644]
Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs [new file with mode: 0644]
Resources/Audio/Jukebox/attributions.yml [new file with mode: 0644]
Resources/Audio/Jukebox/constellations.ogg [new file with mode: 0644]
Resources/Audio/Jukebox/drifting.ogg [new file with mode: 0644]
Resources/Audio/Jukebox/flip-flap.ogg [new file with mode: 0644]
Resources/Audio/Jukebox/sector11.ogg [new file with mode: 0644]
Resources/Audio/Jukebox/starlight.ogg [new file with mode: 0644]
Resources/Audio/Jukebox/title3.ogg [new file with mode: 0644]
Resources/Locale/en-US/jukebox/jukebox-menu.ftl [new file with mode: 0644]
Resources/Prototypes/Catalog/Jukebox/Standard.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
Resources/Prototypes/Entities/Structures/Machines/jukebox.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Machines/lathe.yml
Resources/Prototypes/Recipes/Lathes/electronics.yml
Resources/Prototypes/Research/civilianservices.yml
Resources/Textures/Structures/Machines/jukebox.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Machines/jukebox.rsi/off.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/jukebox.rsi/on.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/jukebox.rsi/select.png [new file with mode: 0644]

diff --git a/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..072730d
--- /dev/null
@@ -0,0 +1,119 @@
+using Content.Shared.Audio.Jukebox;
+using Robust.Client.Audio;
+using Robust.Client.Player;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Audio.Jukebox;
+
+public sealed class JukeboxBoundUserInterface : BoundUserInterface
+{
+    [Dependency] private readonly IPlayerManager _player = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+    [ViewVariables]
+    private JukeboxMenu? _menu;
+
+    public JukeboxBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+        IoCManager.InjectDependencies(this);
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = new JukeboxMenu();
+        _menu.OnClose += Close;
+        _menu.OpenCentered();
+
+        _menu.OnPlayPressed += args =>
+        {
+            if (args)
+            {
+                SendMessage(new JukeboxPlayingMessage());
+            }
+            else
+            {
+                SendMessage(new JukeboxPauseMessage());
+            }
+        };
+
+        _menu.OnStopPressed += () =>
+        {
+            SendMessage(new JukeboxStopMessage());
+        };
+
+        _menu.OnSongSelected += SelectSong;
+
+        _menu.SetTime += SetTime;
+        PopulateMusic();
+        Reload();
+    }
+
+    /// <summary>
+    /// Reloads the attached menu if it exists.
+    /// </summary>
+    public void Reload()
+    {
+        if (_menu == null || !EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox))
+            return;
+
+        _menu.SetAudioStream(jukebox.AudioStream);
+
+        if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
+        {
+            var length = EntMan.System<AudioSystem>().GetAudioLength(songProto.Path.Path.ToString());
+            _menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);
+        }
+        else
+        {
+            _menu.SetSelectedSong(string.Empty, 0f);
+        }
+    }
+
+    public void PopulateMusic()
+    {
+        _menu?.Populate(_protoManager.EnumeratePrototypes<JukeboxPrototype>());
+    }
+
+    public void SelectSong(ProtoId<JukeboxPrototype> songid)
+    {
+        SendMessage(new JukeboxSelectedMessage(songid));
+    }
+
+    public void SetTime(float time)
+    {
+        var sentTime = time;
+
+        // You may be wondering, what the fuck is this
+        // Well we want to be able to predict the playback slider change, of which there are many ways to do it
+        // We can't just use SendPredictedMessage because it will reset every tick and audio updates every frame
+        // so it will go BRRRRT
+        // Using ping gets us close enough that it SHOULD, MOST OF THE TIME, fall within the 0.1 second tolerance
+        // that's still on engine so our playback position never gets corrected.
+        if (EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox) &&
+            EntMan.TryGetComponent(jukebox.AudioStream, out AudioComponent? audioComp))
+        {
+            audioComp.PlaybackPosition = time;
+        }
+
+        SendMessage(new JukeboxSetTimeMessage(sentTime));
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+
+        if (_menu == null)
+            return;
+
+        _menu.OnClose -= Close;
+        _menu.Dispose();
+        _menu = null;
+    }
+}
+
diff --git a/Content.Client/Audio/Jukebox/JukeboxMenu.xaml b/Content.Client/Audio/Jukebox/JukeboxMenu.xaml
new file mode 100644 (file)
index 0000000..e8d39a9
--- /dev/null
@@ -0,0 +1,18 @@
+<ui:FancyWindow xmlns="https://spacestation14.io" xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+                SetSize="400 500" Title="{Loc 'jukebox-menu-title'}">
+    <BoxContainer Margin="4 0" Orientation="Vertical">
+        <ItemList Name="MusicList" SelectMode="Button" Margin="3 3 3 3"
+        HorizontalExpand="True" VerticalExpand="True" SizeFlagsStretchRatio="8"/>
+        <BoxContainer Orientation="Vertical">
+            <Label Name="SongSelected" Text="{Loc 'jukebox-menu-selectedsong'}" />
+            <Label Name="SongName" Text="---" />
+            <Slider Name="PlaybackSlider" HorizontalExpand="True" />
+        </BoxContainer>
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True"
+        VerticalExpand="False" SizeFlagsStretchRatio="1">
+            <Button Name="PlayButton" Text="{Loc 'jukebox-menu-buttonplay'}" />
+            <Button Name="StopButton" Text="{Loc 'jukebox-menu-buttonstop'}" />
+            <Label Name="DurationLabel" Text="00:00 / 00:00" HorizontalAlignment="Right" HorizontalExpand="True"/>
+        </BoxContainer>
+    </BoxContainer>
+</ui:FancyWindow>
diff --git a/Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs b/Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs
new file mode 100644 (file)
index 0000000..e0904ee
--- /dev/null
@@ -0,0 +1,166 @@
+using Content.Shared.Audio.Jukebox;
+using Robust.Client.Audio;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Input;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
+
+namespace Content.Client.Audio.Jukebox;
+
+[GenerateTypedNameReferences]
+public sealed partial class JukeboxMenu : FancyWindow
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    private AudioSystem _audioSystem;
+
+    /// <summary>
+    /// Are we currently 'playing' or paused for the play / pause button.
+    /// </summary>
+    private bool _playState;
+
+    /// <summary>
+    /// True if playing, false if paused.
+    /// </summary>
+    public event Action<bool>? OnPlayPressed;
+    public event Action? OnStopPressed;
+    public event Action<ProtoId<JukeboxPrototype>>? OnSongSelected;
+    public event Action<float>? SetTime;
+
+    private EntityUid? _audio;
+
+    private float _lockTimer;
+
+    public JukeboxMenu()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+        _audioSystem = _entManager.System<AudioSystem>();
+
+        MusicList.OnItemSelected += args =>
+        {
+            var entry = MusicList[args.ItemIndex];
+
+            if (entry.Metadata is not string juke)
+                return;
+
+            OnSongSelected?.Invoke(juke);
+        };
+
+        PlayButton.OnPressed += args =>
+        {
+            OnPlayPressed?.Invoke(!_playState);
+        };
+
+        StopButton.OnPressed += args =>
+        {
+            OnStopPressed?.Invoke();
+        };
+        PlaybackSlider.OnReleased += PlaybackSliderKeyUp;
+
+        SetPlayPauseButton(_audioSystem.IsPlaying(_audio), force: true);
+    }
+
+    public JukeboxMenu(AudioSystem audioSystem)
+    {
+        _audioSystem = audioSystem;
+    }
+
+    public void SetAudioStream(EntityUid? audio)
+    {
+        _audio = audio;
+    }
+
+    private void PlaybackSliderKeyUp(Slider args)
+    {
+        SetTime?.Invoke(PlaybackSlider.Value);
+        _lockTimer = 0.5f;
+    }
+
+    /// <summary>
+    /// Re-populates the list of jukebox prototypes available.
+    /// </summary>
+    public void Populate(IEnumerable<JukeboxPrototype> jukeboxProtos)
+    {
+        MusicList.Clear();
+
+        foreach (var entry in jukeboxProtos)
+        {
+            MusicList.AddItem(entry.Name, metadata: entry.ID);
+        }
+    }
+
+    public void SetPlayPauseButton(bool playing, bool force = false)
+    {
+        if (_playState == playing && !force)
+            return;
+
+        _playState = playing;
+
+        if (playing)
+        {
+            PlayButton.Text = Loc.GetString("jukebox-menu-buttonpause");
+            return;
+        }
+
+        PlayButton.Text = Loc.GetString("jukebox-menu-buttonplay");
+    }
+
+    public void SetSelectedSong(string name, float length)
+    {
+        SetSelectedSongText(name);
+        PlaybackSlider.MaxValue = length;
+        PlaybackSlider.SetValueWithoutEvent(0);
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        if (_lockTimer > 0f)
+        {
+            _lockTimer -= args.DeltaSeconds;
+        }
+
+        PlaybackSlider.Disabled = _lockTimer > 0f;
+
+        if (_entManager.TryGetComponent(_audio, out AudioComponent? audio))
+        {
+            DurationLabel.Text = $@"{TimeSpan.FromSeconds(audio.PlaybackPosition):mm\:ss} / {_audioSystem.GetAudioLength(audio.FileName):mm\:ss}";
+        }
+        else
+        {
+            DurationLabel.Text = $"00:00 / 00:00";
+        }
+
+        if (PlaybackSlider.Grabbed)
+            return;
+
+        if (audio != null || _entManager.TryGetComponent(_audio, out audio))
+        {
+            PlaybackSlider.SetValueWithoutEvent(audio.PlaybackPosition);
+        }
+        else
+        {
+            PlaybackSlider.SetValueWithoutEvent(0f);
+        }
+
+        SetPlayPauseButton(_audioSystem.IsPlaying(_audio, audio));
+    }
+
+    public void SetSelectedSongText(string? text)
+    {
+        if (!string.IsNullOrEmpty(text))
+        {
+            SongName.Text = text;
+        }
+        else
+        {
+            SongName.Text = "---";
+        }
+    }
+}
diff --git a/Content.Client/Audio/Jukebox/JukeboxSystem.cs b/Content.Client/Audio/Jukebox/JukeboxSystem.cs
new file mode 100644 (file)
index 0000000..53bde82
--- /dev/null
@@ -0,0 +1,153 @@
+using Content.Shared.Audio.Jukebox;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Audio.Jukebox;
+
+
+public sealed class JukeboxSystem : SharedJukeboxSystem
+{
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+    [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<JukeboxComponent, AppearanceChangeEvent>(OnAppearanceChange);
+        SubscribeLocalEvent<JukeboxComponent, AnimationCompletedEvent>(OnAnimationCompleted);
+        SubscribeLocalEvent<JukeboxComponent, AfterAutoHandleStateEvent>(OnJukeboxAfterState);
+
+        _protoManager.PrototypesReloaded += OnProtoReload;
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+        _protoManager.PrototypesReloaded -= OnProtoReload;
+    }
+
+    private void OnProtoReload(PrototypesReloadedEventArgs obj)
+    {
+        if (!obj.WasModified<JukeboxPrototype>())
+            return;
+
+        var query = AllEntityQuery<JukeboxComponent, UserInterfaceComponent>();
+
+        while (query.MoveNext(out _, out var ui))
+        {
+            if (!ui.OpenInterfaces.TryGetValue(JukeboxUiKey.Key, out var baseBui) ||
+                baseBui is not JukeboxBoundUserInterface bui)
+            {
+                continue;
+            }
+
+            bui.PopulateMusic();
+        }
+    }
+
+    private void OnJukeboxAfterState(Entity<JukeboxComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (!TryComp(ent, out UserInterfaceComponent? ui))
+            return;
+
+        if (!ui.OpenInterfaces.TryGetValue(JukeboxUiKey.Key, out var baseBui) ||
+            baseBui is not JukeboxBoundUserInterface bui)
+        {
+            return;
+        }
+
+        bui.Reload();
+    }
+
+    private void OnAnimationCompleted(EntityUid uid, JukeboxComponent component, AnimationCompletedEvent args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        if (!TryComp<AppearanceComponent>(uid, out var appearance) ||
+            !_appearanceSystem.TryGetData<JukeboxVisualState>(uid, JukeboxVisuals.VisualState, out var visualState, appearance))
+        {
+            visualState = JukeboxVisualState.On;
+        }
+
+        UpdateAppearance(uid, visualState, component, sprite);
+    }
+
+    private void OnAppearanceChange(EntityUid uid, JukeboxComponent component, ref AppearanceChangeEvent args)
+    {
+        if (args.Sprite == null)
+            return;
+
+        if (!args.AppearanceData.TryGetValue(JukeboxVisuals.VisualState, out var visualStateObject) ||
+            visualStateObject is not JukeboxVisualState visualState)
+        {
+            visualState = JukeboxVisualState.On;
+        }
+
+        UpdateAppearance(uid, visualState, component, args.Sprite);
+    }
+
+    private void UpdateAppearance(EntityUid uid, JukeboxVisualState visualState, JukeboxComponent component, SpriteComponent sprite)
+    {
+        SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
+
+        switch (visualState)
+        {
+            case JukeboxVisualState.On:
+                SetLayerState(JukeboxVisualLayers.Base, component.OnState, sprite);
+                break;
+
+            case JukeboxVisualState.Off:
+                SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
+                break;
+
+            case JukeboxVisualState.Select:
+                PlayAnimation(uid, JukeboxVisualLayers.Base, component.SelectState, 1.0f, sprite);
+                break;
+        }
+    }
+
+    private void PlayAnimation(EntityUid uid, JukeboxVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
+    {
+        if (string.IsNullOrEmpty(state))
+            return;
+
+        if (!_animationPlayer.HasRunningAnimation(uid, state))
+        {
+            var animation = GetAnimation(layer, state, animationTime);
+            sprite.LayerSetVisible(layer, true);
+            _animationPlayer.Play(uid, animation, state);
+        }
+    }
+
+    private static Animation GetAnimation(JukeboxVisualLayers layer, string state, float animationTime)
+    {
+        return new Animation
+        {
+            Length = TimeSpan.FromSeconds(animationTime),
+            AnimationTracks =
+                {
+                    new AnimationTrackSpriteFlick
+                    {
+                        LayerKey = layer,
+                        KeyFrames =
+                        {
+                            new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
+                        }
+                    }
+                }
+        };
+    }
+
+    private void SetLayerState(JukeboxVisualLayers layer, string? state, SpriteComponent sprite)
+    {
+        if (string.IsNullOrEmpty(state))
+            return;
+
+        sprite.LayerSetVisible(layer, true);
+        sprite.LayerSetAutoAnimated(layer, true);
+        sprite.LayerSetState(layer, state);
+    }
+}
diff --git a/Content.Server/Audio/Jukebox/JukeboxSystem.cs b/Content.Server/Audio/Jukebox/JukeboxSystem.cs
new file mode 100644 (file)
index 0000000..bfb9b20
--- /dev/null
@@ -0,0 +1,152 @@
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Audio.Jukebox;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using JukeboxComponent = Content.Shared.Audio.Jukebox.JukeboxComponent;
+
+namespace Content.Server.Audio.Jukebox;
+
+
+public sealed class JukeboxSystem : SharedJukeboxSystem
+{
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+    [Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<JukeboxComponent, JukeboxSelectedMessage>(OnJukeboxSelected);
+        SubscribeLocalEvent<JukeboxComponent, JukeboxPlayingMessage>(OnJukeboxPlay);
+        SubscribeLocalEvent<JukeboxComponent, JukeboxPauseMessage>(OnJukeboxPause);
+        SubscribeLocalEvent<JukeboxComponent, JukeboxStopMessage>(OnJukeboxStop);
+        SubscribeLocalEvent<JukeboxComponent, JukeboxSetTimeMessage>(OnJukeboxSetTime);
+        SubscribeLocalEvent<JukeboxComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<JukeboxComponent, ComponentShutdown>(OnComponentShutdown);
+
+        SubscribeLocalEvent<JukeboxComponent, PowerChangedEvent>(OnPowerChanged);
+    }
+
+    private void OnComponentInit(EntityUid uid, JukeboxComponent component, ComponentInit args)
+    {
+        if (HasComp<ApcPowerReceiverComponent>(uid))
+        {
+            TryUpdateVisualState(uid, component);
+        }
+    }
+
+    private void OnJukeboxPlay(EntityUid uid, JukeboxComponent component, ref JukeboxPlayingMessage args)
+    {
+        if (Exists(component.AudioStream))
+        {
+            Audio.SetState(component.AudioStream, AudioState.Playing);
+        }
+        else
+        {
+            component.AudioStream = Audio.Stop(component.AudioStream);
+
+            if (string.IsNullOrEmpty(component.SelectedSongId) ||
+                !_protoManager.TryIndex(component.SelectedSongId, out var jukeboxProto))
+            {
+                return;
+            }
+
+            component.AudioStream = Audio.PlayPvs(jukeboxProto.Path, uid, AudioParams.Default.WithMaxDistance(10f))?.Entity;
+            Dirty(uid, component);
+        }
+    }
+
+    private void OnJukeboxPause(Entity<JukeboxComponent> ent, ref JukeboxPauseMessage args)
+    {
+        Audio.SetState(ent.Comp.AudioStream, AudioState.Paused);
+    }
+
+    private void OnJukeboxSetTime(EntityUid uid, JukeboxComponent component, JukeboxSetTimeMessage args)
+    {
+        var offset = (args.Session.Channel.Ping * 1.5f) / 1000f;
+        Audio.SetPlaybackPosition(component.AudioStream, args.SongTime + offset);
+    }
+
+    private void OnPowerChanged(Entity<JukeboxComponent> entity, ref PowerChangedEvent args)
+    {
+        TryUpdateVisualState(entity);
+
+        if (!this.IsPowered(entity.Owner, EntityManager))
+        {
+            Stop(entity);
+        }
+    }
+
+    private void OnJukeboxStop(Entity<JukeboxComponent> entity, ref JukeboxStopMessage args)
+    {
+        Stop(entity);
+    }
+
+    private void Stop(Entity<JukeboxComponent> entity)
+    {
+        Audio.SetState(entity.Comp.AudioStream, AudioState.Stopped);
+        Dirty(entity);
+    }
+
+    private void OnJukeboxSelected(EntityUid uid, JukeboxComponent component, JukeboxSelectedMessage args)
+    {
+        if (!Audio.IsPlaying(component.AudioStream))
+        {
+            component.SelectedSongId = args.SongId;
+            DirectSetVisualState(uid, JukeboxVisualState.Select);
+            component.Selecting = true;
+            component.AudioStream = Audio.Stop(component.AudioStream);
+        }
+
+        Dirty(uid, component);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<JukeboxComponent>();
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (comp.Selecting)
+            {
+                comp.SelectAccumulator += frameTime;
+                if (comp.SelectAccumulator >= 0.5f)
+                {
+                    comp.SelectAccumulator = 0f;
+                    comp.Selecting = false;
+
+                    TryUpdateVisualState(uid, comp);
+                }
+            }
+        }
+    }
+
+    private void OnComponentShutdown(EntityUid uid, JukeboxComponent component, ComponentShutdown args)
+    {
+        component.AudioStream = Audio.Stop(component.AudioStream);
+    }
+
+    private void DirectSetVisualState(EntityUid uid, JukeboxVisualState state)
+    {
+        _appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, state);
+    }
+
+    private void TryUpdateVisualState(EntityUid uid, JukeboxComponent? jukeboxComponent = null)
+    {
+        if (!Resolve(uid, ref jukeboxComponent))
+            return;
+
+        var finalState = JukeboxVisualState.On;
+
+        if (!this.IsPowered(uid, EntityManager))
+        {
+            finalState = JukeboxVisualState.Off;
+        }
+
+        _appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, finalState);
+    }
+}
diff --git a/Content.Shared/Audio/Jukebox/JukeboxComponent.cs b/Content.Shared/Audio/Jukebox/JukeboxComponent.cs
new file mode 100644 (file)
index 0000000..f9bb385
--- /dev/null
@@ -0,0 +1,80 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Audio.Jukebox;
+
+[NetworkedComponent, RegisterComponent, AutoGenerateComponentState(true)]
+[Access(typeof(SharedJukeboxSystem))]
+public sealed partial class JukeboxComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public ProtoId<JukeboxPrototype>? SelectedSongId;
+
+    [DataField, AutoNetworkedField]
+    public EntityUid? AudioStream;
+
+    /// <summary>
+    /// RSI state for the jukebox being on.
+    /// </summary>
+    [DataField]
+    public string? OnState;
+
+    /// <summary>
+    /// RSI state for the jukebox being on.
+    /// </summary>
+    [DataField]
+    public string? OffState;
+
+    /// <summary>
+    /// RSI state for the jukebox track being selected.
+    /// </summary>
+    [DataField]
+    public string? SelectState;
+
+    [ViewVariables]
+    public bool Selecting;
+
+    [ViewVariables]
+    public float SelectAccumulator;
+}
+
+[Serializable, NetSerializable]
+public sealed class JukeboxPlayingMessage : BoundUserInterfaceMessage;
+
+[Serializable, NetSerializable]
+public sealed class JukeboxPauseMessage : BoundUserInterfaceMessage;
+
+[Serializable, NetSerializable]
+public sealed class JukeboxStopMessage : BoundUserInterfaceMessage;
+
+[Serializable, NetSerializable]
+public sealed class JukeboxSelectedMessage(ProtoId<JukeboxPrototype> songId) : BoundUserInterfaceMessage
+{
+    public ProtoId<JukeboxPrototype> SongId { get; } = songId;
+}
+
+[Serializable, NetSerializable]
+public sealed class JukeboxSetTimeMessage(float songTime) : BoundUserInterfaceMessage
+{
+    public float SongTime { get; } = songTime;
+}
+
+[Serializable, NetSerializable]
+public enum JukeboxVisuals : byte
+{
+    VisualState
+}
+
+[Serializable, NetSerializable]
+public enum JukeboxVisualState : byte
+{
+    On,
+    Off,
+    Select,
+}
+
+public enum JukeboxVisualLayers : byte
+{
+    Base
+}
diff --git a/Content.Shared/Audio/Jukebox/JukeboxPrototype.cs b/Content.Shared/Audio/Jukebox/JukeboxPrototype.cs
new file mode 100644 (file)
index 0000000..256f22f
--- /dev/null
@@ -0,0 +1,23 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Audio.Jukebox;
+
+/// <summary>
+/// Soundtrack that's visible on the jukebox list.
+/// </summary>
+[Prototype]
+public sealed class JukeboxPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    /// <summary>
+    /// User friendly name to use in UI.
+    /// </summary>
+    [DataField(required: true)]
+    public string Name = string.Empty;
+
+    [DataField(required: true)]
+    public SoundPathSpecifier Path = default!;
+}
diff --git a/Content.Shared/Audio/Jukebox/JukeboxUi.cs b/Content.Shared/Audio/Jukebox/JukeboxUi.cs
new file mode 100644 (file)
index 0000000..bf1fc3d
--- /dev/null
@@ -0,0 +1,11 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Audio.Jukebox;
+
+
+[Serializable, NetSerializable]
+public enum JukeboxUiKey : byte
+{
+    Key,
+}
diff --git a/Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs b/Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs
new file mode 100644 (file)
index 0000000..1a8f9cb
--- /dev/null
@@ -0,0 +1,8 @@
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Shared.Audio.Jukebox;
+
+public abstract class SharedJukeboxSystem : EntitySystem
+{
+    [Dependency] protected readonly SharedAudioSystem Audio = default!;
+}
diff --git a/Resources/Audio/Jukebox/attributions.yml b/Resources/Audio/Jukebox/attributions.yml
new file mode 100644 (file)
index 0000000..8e48560
--- /dev/null
@@ -0,0 +1,22 @@
+- files: ["sector11.ogg"]
+  license: "CC-BY-NC-SA-3.0"
+  copyright: "-Sector11 by MashedByMachines. Converted to mono OGG."
+  source: "https://www.newgrounds.com/audio/listen/312622"
+
+- files: ["mod.flip-flap.ogg"]
+  license: "Custom"
+  copyright: "Flip Flap by X-ceed is licensed under a short but clear license (see flip-flap.txt in Audio/Lobby) and is free for non-commercial use. Converted to mono OGG."
+  source: "http://aminet.net/package/mods/xceed/Flipflap"
+
+- files: ["title3.ogg"]
+  license: "CC-BY-NC-SA-3.0"
+  copyright: "Title3 by Cuboos. It is a remix of the song 'Tintin on the Moon'. Converted to mono OGG."
+  source: "https://www.youtube.com/watch?v=YKVmXn-Gv0M"
+
+- files:
+  - "constellations.ogg"
+  - "drifting.ogg"
+  - "starlight.ogg"
+  license: "CC-BY-3.0"
+  copyright: "Constellations by Qwertyquerty. Converted to mono OGG."
+  source: "https://www.youtube.com/channel/UCPYbhBUGhH7n_G4HLK2YipQ"
diff --git a/Resources/Audio/Jukebox/constellations.ogg b/Resources/Audio/Jukebox/constellations.ogg
new file mode 100644 (file)
index 0000000..f177489
Binary files /dev/null and b/Resources/Audio/Jukebox/constellations.ogg differ
diff --git a/Resources/Audio/Jukebox/drifting.ogg b/Resources/Audio/Jukebox/drifting.ogg
new file mode 100644 (file)
index 0000000..321c098
Binary files /dev/null and b/Resources/Audio/Jukebox/drifting.ogg differ
diff --git a/Resources/Audio/Jukebox/flip-flap.ogg b/Resources/Audio/Jukebox/flip-flap.ogg
new file mode 100644 (file)
index 0000000..07c47c0
Binary files /dev/null and b/Resources/Audio/Jukebox/flip-flap.ogg differ
diff --git a/Resources/Audio/Jukebox/sector11.ogg b/Resources/Audio/Jukebox/sector11.ogg
new file mode 100644 (file)
index 0000000..f0ce993
Binary files /dev/null and b/Resources/Audio/Jukebox/sector11.ogg differ
diff --git a/Resources/Audio/Jukebox/starlight.ogg b/Resources/Audio/Jukebox/starlight.ogg
new file mode 100644 (file)
index 0000000..31e2341
Binary files /dev/null and b/Resources/Audio/Jukebox/starlight.ogg differ
diff --git a/Resources/Audio/Jukebox/title3.ogg b/Resources/Audio/Jukebox/title3.ogg
new file mode 100644 (file)
index 0000000..5cfe80b
Binary files /dev/null and b/Resources/Audio/Jukebox/title3.ogg differ
diff --git a/Resources/Locale/en-US/jukebox/jukebox-menu.ftl b/Resources/Locale/en-US/jukebox/jukebox-menu.ftl
new file mode 100644 (file)
index 0000000..d015976
--- /dev/null
@@ -0,0 +1,5 @@
+jukebox-menu-title = Jukebox
+jukebox-menu-selectedsong = Selected Song:
+jukebox-menu-buttonplay = Play
+jukebox-menu-buttonpause = Pause
+jukebox-menu-buttonstop = Stop
diff --git a/Resources/Prototypes/Catalog/Jukebox/Standard.yml b/Resources/Prototypes/Catalog/Jukebox/Standard.yml
new file mode 100644 (file)
index 0000000..e9d8687
--- /dev/null
@@ -0,0 +1,35 @@
+- type: jukebox
+  id: FlipFlap
+  name: X-CEED - Flip Flap
+  path:
+    path: /Audio/Jukebox/flip-flap.ogg
+
+- type: jukebox
+  id: Tintin
+  name: Jeroen Tel - Tintin on the Moon
+  path:
+    path: /Audio/Jukebox/title3.ogg
+
+- type: jukebox
+  id: Thunderdome
+  name: MashedByMachines - Sector 11
+  path:
+    path: /Audio/Jukebox/sector11.ogg
+
+- type: jukebox
+  id: Constellations
+  name: Qwertyquerty - Constellations
+  path:
+    path: /Audio/Jukebox/constellations.ogg
+
+- type: jukebox
+  id: Drifting
+  name: Qwertyquerty - Drifting
+  path:
+    path: /Audio/Jukebox/drifting.ogg
+
+- type: jukebox
+  id: starlight
+  name: Qwertyquerty - Starlight
+  path:
+    path: /Audio/Jukebox/starlight.ogg
index 7a9e60ac566d10512a873e0ce655918d1bf3427c..ebc8237cb2bf7a7d47c519dc342e4807f8b00f71 100644 (file)
       MatterBin: 1
       Manipulator: 3
     materialRequirements:
-      Glass: 1
\ No newline at end of file
+      Glass: 1
+
+- type: entity
+  parent: BaseMachineCircuitboard
+  id: JukeboxCircuitBoard
+  name: jukebox machine board
+  description: A machine printed circuit board for a jukebox.
+  components:
+  - type: MachineBoard
+    prototype: Jukebox
+    materialRequirements:
+      WoodPlank: 5
+      Steel: 2
+      Glass: 5
+      Cable: 2
diff --git a/Resources/Prototypes/Entities/Structures/Machines/jukebox.yml b/Resources/Prototypes/Entities/Structures/Machines/jukebox.yml
new file mode 100644 (file)
index 0000000..76b8ddd
--- /dev/null
@@ -0,0 +1,59 @@
+- type: entity
+  id: Jukebox
+  name: jukebox
+  parent: [ BaseMachinePowered, ConstructibleMachine ]
+  description: A machine capable of playing a wide variety of tunes. Enjoyment not guaranteed.
+  components:
+  - type: Sprite
+    sprite: Structures/Machines/jukebox.rsi
+    layers:
+    - state: "off"
+      map: ["enum.JukeboxVisualLayers.Base"]
+  - type: Transform
+    anchored: true
+  - type: Jukebox
+    onState: on
+    offState: off
+    selectState: select
+  - type: Machine
+    board: JukeboxCircuitBoard
+  - type: Appearance
+  - type: ApcPowerReceiver
+    powerLoad: 100
+  - type: ExtensionCableReceiver
+  - type: ActivatableUI
+    key: enum.JukeboxUiKey.Key
+  - type: ActivatableUIRequiresPower
+  - type: UserInterface
+    interfaces:
+      - key: enum.JukeboxUiKey.Key
+        type: JukeboxBoundUserInterface
+  - type: Damageable
+    damageContainer: Inorganic
+    damageModifierSet: Metallic
+  - type: Destructible
+    thresholds:
+    - trigger:
+        !type:DamageTrigger
+        damage: 75
+      behaviors:
+      - !type:DoActsBehavior
+        acts: ["Destruction"]
+      - !type:SpawnEntitiesBehavior
+        spawn:
+          SheetSteel1:
+            min: 1
+            max: 2
+  - type: Physics
+    bodyType: Static
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.25,-0.45,0.25,0.45"
+        mask:
+        - MachineMask
+        layer:
+        - MachineLayer
+        density: 200
index c32f2992f9455c924678bdc5e3ba695311cb3c7c..6f7e4cd6ab7ded377d50952a175e5261cb6cc3a2 100644 (file)
       - TelecomServerCircuitboard
       - MassMediaCircuitboard
       - ReagentGrinderIndustrialMachineCircuitboard
+      - JukeboxCircuitBoard
   - type: MaterialStorage
     whitelist:
       tags:
index c4417f868b56ab601488f287402d2a86b1471851..c9998c4c34b4d849e56b0eb909e62fdc9934ffa5 100644 (file)
      Steel: 100
      Glass: 900
      Gold: 100
+- type: latheRecipe
+  id: JukeboxCircuitBoard
+  result: JukeboxCircuitBoard
+  completetime: 4
+  materials:
+    Steel: 100
+    Glass: 900
index 79dac2751010dac221a96b55e74ff3c604401466..afb1f0ff50317f4f08f19c2c83215acd8b1fd44e 100644 (file)
@@ -70,6 +70,7 @@
     - BorgModuleClowning
     - DawInstrumentMachineCircuitboard
     - MassMediaCircuitboard
+    - JukeboxCircuitBoard
 
 - type: technology
   id: RoboticCleanliness
diff --git a/Resources/Textures/Structures/Machines/jukebox.rsi/meta.json b/Resources/Textures/Structures/Machines/jukebox.rsi/meta.json
new file mode 100644 (file)
index 0000000..f447b26
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "version": 1,
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from https://github.com/tgstation/tgstation at f349b842c84f500399bd5673e5e34a6bc45b001a, direct dmi link https://github.com/tgstation/tgstation/blob/f349b842c84f500399bd5673e5e34a6bc45b001a/icons/obj/stationobjs.dmi",
+  "states": [
+    {
+      "name": "on"
+    },
+    {
+      "name": "off"
+    },
+    {
+      "name": "select",
+         "delays": [
+        [
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+                 0.1,
+                 0.1,
+                 0.1
+        ]
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Structures/Machines/jukebox.rsi/off.png b/Resources/Textures/Structures/Machines/jukebox.rsi/off.png
new file mode 100644 (file)
index 0000000..f3c24b1
Binary files /dev/null and b/Resources/Textures/Structures/Machines/jukebox.rsi/off.png differ
diff --git a/Resources/Textures/Structures/Machines/jukebox.rsi/on.png b/Resources/Textures/Structures/Machines/jukebox.rsi/on.png
new file mode 100644 (file)
index 0000000..b397adc
Binary files /dev/null and b/Resources/Textures/Structures/Machines/jukebox.rsi/on.png differ
diff --git a/Resources/Textures/Structures/Machines/jukebox.rsi/select.png b/Resources/Textures/Structures/Machines/jukebox.rsi/select.png
new file mode 100644 (file)
index 0000000..0dd6d81
Binary files /dev/null and b/Resources/Textures/Structures/Machines/jukebox.rsi/select.png differ