Added Jukebox (#26736)
* 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>
This commit is contained in:
119
Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
Normal file
119
Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
18
Content.Client/Audio/Jukebox/JukeboxMenu.xaml
Normal file
18
Content.Client/Audio/Jukebox/JukeboxMenu.xaml
Normal file
@@ -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>
|
||||||
166
Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs
Normal file
166
Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs
Normal file
@@ -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 = "---";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Content.Client/Audio/Jukebox/JukeboxSystem.cs
Normal file
153
Content.Client/Audio/Jukebox/JukeboxSystem.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Content.Server/Audio/Jukebox/JukeboxSystem.cs
Normal file
152
Content.Server/Audio/Jukebox/JukeboxSystem.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Content.Shared/Audio/Jukebox/JukeboxComponent.cs
Normal file
80
Content.Shared/Audio/Jukebox/JukeboxComponent.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
23
Content.Shared/Audio/Jukebox/JukeboxPrototype.cs
Normal file
23
Content.Shared/Audio/Jukebox/JukeboxPrototype.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
11
Content.Shared/Audio/Jukebox/JukeboxUi.cs
Normal file
11
Content.Shared/Audio/Jukebox/JukeboxUi.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Audio.Jukebox;
|
||||||
|
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum JukeboxUiKey : byte
|
||||||
|
{
|
||||||
|
Key,
|
||||||
|
}
|
||||||
8
Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs
Normal file
8
Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
22
Resources/Audio/Jukebox/attributions.yml
Normal file
22
Resources/Audio/Jukebox/attributions.yml
Normal file
@@ -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"
|
||||||
BIN
Resources/Audio/Jukebox/constellations.ogg
Normal file
BIN
Resources/Audio/Jukebox/constellations.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Jukebox/drifting.ogg
Normal file
BIN
Resources/Audio/Jukebox/drifting.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Jukebox/flip-flap.ogg
Normal file
BIN
Resources/Audio/Jukebox/flip-flap.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Jukebox/sector11.ogg
Normal file
BIN
Resources/Audio/Jukebox/sector11.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Jukebox/starlight.ogg
Normal file
BIN
Resources/Audio/Jukebox/starlight.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Jukebox/title3.ogg
Normal file
BIN
Resources/Audio/Jukebox/title3.ogg
Normal file
Binary file not shown.
5
Resources/Locale/en-US/jukebox/jukebox-menu.ftl
Normal file
5
Resources/Locale/en-US/jukebox/jukebox-menu.ftl
Normal file
@@ -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
|
||||||
35
Resources/Prototypes/Catalog/Jukebox/Standard.yml
Normal file
35
Resources/Prototypes/Catalog/Jukebox/Standard.yml
Normal file
@@ -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
|
||||||
@@ -1369,4 +1369,18 @@
|
|||||||
MatterBin: 1
|
MatterBin: 1
|
||||||
Manipulator: 3
|
Manipulator: 3
|
||||||
materialRequirements:
|
materialRequirements:
|
||||||
Glass: 1
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -446,6 +446,7 @@
|
|||||||
- TelecomServerCircuitboard
|
- TelecomServerCircuitboard
|
||||||
- MassMediaCircuitboard
|
- MassMediaCircuitboard
|
||||||
- ReagentGrinderIndustrialMachineCircuitboard
|
- ReagentGrinderIndustrialMachineCircuitboard
|
||||||
|
- JukeboxCircuitBoard
|
||||||
- type: MaterialStorage
|
- type: MaterialStorage
|
||||||
whitelist:
|
whitelist:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -965,3 +965,11 @@
|
|||||||
Steel: 100
|
Steel: 100
|
||||||
Glass: 900
|
Glass: 900
|
||||||
Gold: 100
|
Gold: 100
|
||||||
|
|
||||||
|
- type: latheRecipe
|
||||||
|
id: JukeboxCircuitBoard
|
||||||
|
result: JukeboxCircuitBoard
|
||||||
|
completetime: 4
|
||||||
|
materials:
|
||||||
|
Steel: 100
|
||||||
|
Glass: 900
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
- BorgModuleClowning
|
- BorgModuleClowning
|
||||||
- DawInstrumentMachineCircuitboard
|
- DawInstrumentMachineCircuitboard
|
||||||
- MassMediaCircuitboard
|
- MassMediaCircuitboard
|
||||||
|
- JukeboxCircuitBoard
|
||||||
|
|
||||||
- type: technology
|
- type: technology
|
||||||
id: RoboticCleanliness
|
id: RoboticCleanliness
|
||||||
|
|||||||
31
Resources/Textures/Structures/Machines/jukebox.rsi/meta.json
Normal file
31
Resources/Textures/Structures/Machines/jukebox.rsi/meta.json
Normal file
@@ -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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Resources/Textures/Structures/Machines/jukebox.rsi/off.png
Normal file
BIN
Resources/Textures/Structures/Machines/jukebox.rsi/off.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 770 B |
BIN
Resources/Textures/Structures/Machines/jukebox.rsi/on.png
Normal file
BIN
Resources/Textures/Structures/Machines/jukebox.rsi/on.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 673 B |
BIN
Resources/Textures/Structures/Machines/jukebox.rsi/select.png
Normal file
BIN
Resources/Textures/Structures/Machines/jukebox.rsi/select.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user