Add .vxl support to the asset browser.
This commit is contained in:
committed by
Paul Chote
parent
9d181e88d2
commit
fb20479379
@@ -45,6 +45,7 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public interface IModelCache : IDisposable
|
public interface IModelCache : IDisposable
|
||||||
{
|
{
|
||||||
|
IModel GetModel(string model);
|
||||||
IModel GetModelSequence(string model, string sequence);
|
IModel GetModelSequence(string model, string sequence);
|
||||||
bool HasModelSequence(string model, string sequence);
|
bool HasModelSequence(string model, string sequence);
|
||||||
IVertexBuffer<Vertex> VertexBuffer { get; }
|
IVertexBuffer<Vertex> VertexBuffer { get; }
|
||||||
@@ -66,6 +67,11 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|
||||||
|
public IModel GetModel(string model)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public IModel GetModelSequence(string model, string sequence)
|
public IModel GetModelSequence(string model, string sequence)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ namespace OpenRA.Mods.Cnc.Graphics
|
|||||||
return loader.Load(vxl, hva);
|
return loader.Load(vxl, hva);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IModel GetModel(string model)
|
||||||
|
{
|
||||||
|
return loader.Load(model, model);
|
||||||
|
}
|
||||||
|
|
||||||
public IModel GetModelSequence(string model, string sequence)
|
public IModel GetModelSequence(string model, string sequence)
|
||||||
{
|
{
|
||||||
try { return models[model][sequence]; }
|
try { return models[model][sequence]; }
|
||||||
|
|||||||
@@ -43,10 +43,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
string currentFilename;
|
string currentFilename;
|
||||||
IReadOnlyPackage currentPackage;
|
IReadOnlyPackage currentPackage;
|
||||||
Sprite[] currentSprites;
|
Sprite[] currentSprites;
|
||||||
|
IModel currentVoxel;
|
||||||
VqaPlayerWidget player = null;
|
VqaPlayerWidget player = null;
|
||||||
bool isVideoLoaded = false;
|
bool isVideoLoaded = false;
|
||||||
bool isLoadError = false;
|
bool isLoadError = false;
|
||||||
int currentFrame;
|
int currentFrame;
|
||||||
|
WRot modelOrientation;
|
||||||
|
|
||||||
[ObjectCreator.UseCtor]
|
[ObjectCreator.UseCtor]
|
||||||
public AssetBrowserLogic(Widget widget, Action onExit, ModData modData, World world, Dictionary<string, MiniYaml> logicArgs)
|
public AssetBrowserLogic(Widget widget, Action onExit, ModData modData, World world, Dictionary<string, MiniYaml> logicArgs)
|
||||||
@@ -79,13 +81,24 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
spriteWidget.GetSprite = () => currentSprites != null ? currentSprites[currentFrame] : null;
|
spriteWidget.GetSprite = () => currentSprites != null ? currentSprites[currentFrame] : null;
|
||||||
currentPalette = spriteWidget.Palette;
|
currentPalette = spriteWidget.Palette;
|
||||||
spriteWidget.GetPalette = () => currentPalette;
|
spriteWidget.GetPalette = () => currentPalette;
|
||||||
spriteWidget.IsVisible = () => !isVideoLoaded && !isLoadError;
|
spriteWidget.IsVisible = () => !isVideoLoaded && !isLoadError && currentSprites != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerWidget = panel.GetOrNull<VqaPlayerWidget>("PLAYER");
|
var playerWidget = panel.GetOrNull<VqaPlayerWidget>("PLAYER");
|
||||||
if (playerWidget != null)
|
if (playerWidget != null)
|
||||||
playerWidget.IsVisible = () => isVideoLoaded && !isLoadError;
|
playerWidget.IsVisible = () => isVideoLoaded && !isLoadError;
|
||||||
|
|
||||||
|
var modelWidget = panel.GetOrNull<ModelWidget>("VOXEL");
|
||||||
|
if (modelWidget != null)
|
||||||
|
{
|
||||||
|
modelWidget.GetVoxel = () => currentVoxel;
|
||||||
|
currentPalette = modelWidget.Palette;
|
||||||
|
modelWidget.GetPalette = () => currentPalette;
|
||||||
|
modelWidget.GetPlayerPalette = () => currentPalette;
|
||||||
|
modelWidget.GetRotation = () => modelOrientation;
|
||||||
|
modelWidget.IsVisible = () => !isVideoLoaded && !isLoadError && currentVoxel != null;
|
||||||
|
}
|
||||||
|
|
||||||
var errorLabelWidget = panel.GetOrNull("ERROR");
|
var errorLabelWidget = panel.GetOrNull("ERROR");
|
||||||
if (errorLabelWidget != null)
|
if (errorLabelWidget != null)
|
||||||
errorLabelWidget.IsVisible = () => isLoadError;
|
errorLabelWidget.IsVisible = () => isLoadError;
|
||||||
@@ -210,6 +223,46 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
prevButton.IsVisible = () => !isVideoLoaded;
|
prevButton.IsVisible = () => !isVideoLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var voxelContainer = panel.GetOrNull("VOXEL_SELECTOR");
|
||||||
|
if (voxelContainer != null)
|
||||||
|
voxelContainer.IsVisible = () => currentVoxel != null;
|
||||||
|
|
||||||
|
var rollSlider = panel.GetOrNull<SliderWidget>("ROLL_SLIDER");
|
||||||
|
if (rollSlider != null)
|
||||||
|
{
|
||||||
|
rollSlider.OnChange += x =>
|
||||||
|
{
|
||||||
|
var roll = (int)x;
|
||||||
|
modelOrientation = modelOrientation.WithRoll(new WAngle(roll));
|
||||||
|
};
|
||||||
|
|
||||||
|
rollSlider.GetValue = () => modelOrientation.Roll.Angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pitchSlider = panel.GetOrNull<SliderWidget>("PITCH_SLIDER");
|
||||||
|
if (pitchSlider != null)
|
||||||
|
{
|
||||||
|
pitchSlider.OnChange += x =>
|
||||||
|
{
|
||||||
|
var pitch = (int)x;
|
||||||
|
modelOrientation = modelOrientation.WithPitch(new WAngle(pitch));
|
||||||
|
};
|
||||||
|
|
||||||
|
pitchSlider.GetValue = () => modelOrientation.Pitch.Angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var yawSlider = panel.GetOrNull<SliderWidget>("YAW_SLIDER");
|
||||||
|
if (yawSlider != null)
|
||||||
|
{
|
||||||
|
yawSlider.OnChange += x =>
|
||||||
|
{
|
||||||
|
var yaw = (int)x;
|
||||||
|
modelOrientation = modelOrientation.WithYaw(new WAngle(yaw));
|
||||||
|
};
|
||||||
|
|
||||||
|
yawSlider.GetValue = () => modelOrientation.Yaw.Angle;
|
||||||
|
}
|
||||||
|
|
||||||
var assetBrowserModData = modData.Manifest.Get<AssetBrowser>();
|
var assetBrowserModData = modData.Manifest.Get<AssetBrowser>();
|
||||||
allowedExtensions = assetBrowserModData.SupportedExtensions;
|
allowedExtensions = assetBrowserModData.SupportedExtensions;
|
||||||
|
|
||||||
@@ -342,12 +395,24 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSprites = world.Map.Rules.Sequences.SpriteCache[prefix + filename];
|
if (Path.GetExtension(filename.ToLowerInvariant()) == ".vxl")
|
||||||
currentFrame = 0;
|
|
||||||
if (frameSlider != null)
|
|
||||||
{
|
{
|
||||||
frameSlider.MaximumValue = (float)currentSprites.Length - 1;
|
var voxelName = Path.GetFileNameWithoutExtension(filename);
|
||||||
frameSlider.Ticks = currentSprites.Length;
|
currentVoxel = world.ModelCache.GetModel(voxelName);
|
||||||
|
currentSprites = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentSprites = world.Map.Rules.Sequences.SpriteCache[prefix + filename];
|
||||||
|
currentFrame = 0;
|
||||||
|
|
||||||
|
if (frameSlider != null)
|
||||||
|
{
|
||||||
|
frameSlider.MaximumValue = (float)currentSprites.Length - 1;
|
||||||
|
frameSlider.Ticks = currentSprites.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVoxel = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
217
OpenRA.Mods.Common/Widgets/ModelWidget.cs
Normal file
217
OpenRA.Mods.Common/Widgets/ModelWidget.cs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version. For more
|
||||||
|
* information, see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenRA.Graphics;
|
||||||
|
using OpenRA.Mods.Common.Graphics;
|
||||||
|
using OpenRA.Widgets;
|
||||||
|
|
||||||
|
namespace OpenRA.Mods.Common.Widgets
|
||||||
|
{
|
||||||
|
public class ModelWidget : Widget
|
||||||
|
{
|
||||||
|
public string Palette = "terrain";
|
||||||
|
public string PlayerPalette = "player";
|
||||||
|
public string NormalsPalette = "normals";
|
||||||
|
public string ShadowPalette = "shadow";
|
||||||
|
public float Scale = 12f;
|
||||||
|
public int LightPitch = 142;
|
||||||
|
public int LightYaw = 682;
|
||||||
|
public float[] LightAmbientColor = new float[] { 0.6f, 0.6f, 0.6f };
|
||||||
|
public float[] LightDiffuseColor = new float[] { 0.4f, 0.4f, 0.4f };
|
||||||
|
public WRot Rotation = WRot.None;
|
||||||
|
public WAngle CameraAngle = WAngle.FromDegrees(40);
|
||||||
|
|
||||||
|
public Func<string> GetPalette;
|
||||||
|
public Func<string> GetPlayerPalette;
|
||||||
|
public Func<string> GetNormalsPalette;
|
||||||
|
public Func<string> GetShadowPalette;
|
||||||
|
public Func<float[]> GetLightAmbientColor;
|
||||||
|
public Func<float[]> GetLightDiffuseColor;
|
||||||
|
public Func<float> GetScale;
|
||||||
|
public Func<int> GetLightPitch;
|
||||||
|
public Func<int> GetLightYaw;
|
||||||
|
public Func<IModel> GetVoxel;
|
||||||
|
public Func<WRot> GetRotation;
|
||||||
|
public Func<WAngle> GetCameraAngle;
|
||||||
|
public int2 IdealPreviewSize { get; private set; }
|
||||||
|
|
||||||
|
protected readonly WorldRenderer WorldRenderer;
|
||||||
|
|
||||||
|
IFinalizedRenderable renderable;
|
||||||
|
|
||||||
|
[ObjectCreator.UseCtor]
|
||||||
|
public ModelWidget(WorldRenderer worldRenderer)
|
||||||
|
{
|
||||||
|
GetPalette = () => Palette;
|
||||||
|
GetPlayerPalette = () => PlayerPalette;
|
||||||
|
GetNormalsPalette = () => NormalsPalette;
|
||||||
|
GetShadowPalette = () => ShadowPalette;
|
||||||
|
GetLightAmbientColor = () => LightAmbientColor;
|
||||||
|
GetLightDiffuseColor = () => LightDiffuseColor;
|
||||||
|
GetScale = () => Scale;
|
||||||
|
GetRotation = () => Rotation;
|
||||||
|
GetLightPitch = () => LightPitch;
|
||||||
|
GetLightYaw = () => LightYaw;
|
||||||
|
GetCameraAngle = () => CameraAngle;
|
||||||
|
WorldRenderer = worldRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ModelWidget(ModelWidget other)
|
||||||
|
: base(other)
|
||||||
|
{
|
||||||
|
Palette = other.Palette;
|
||||||
|
GetPalette = other.GetPalette;
|
||||||
|
GetVoxel = other.GetVoxel;
|
||||||
|
|
||||||
|
WorldRenderer = other.WorldRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Widget Clone()
|
||||||
|
{
|
||||||
|
return new ModelWidget(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
IModel cachedVoxel;
|
||||||
|
string cachedPalette;
|
||||||
|
string cachedPlayerPalette;
|
||||||
|
string cachedNormalsPalette;
|
||||||
|
string cachedShadowPalette;
|
||||||
|
float cachedScale;
|
||||||
|
WRot cachedRotation;
|
||||||
|
float[] cachedLightAmbientColor = new float[] { 0, 0, 0 };
|
||||||
|
float[] cachedLightDiffuseColor = new float[] { 0, 0, 0 };
|
||||||
|
int cachedLightPitch;
|
||||||
|
int cachedLightYaw;
|
||||||
|
WAngle cachedCameraAngle;
|
||||||
|
PaletteReference paletteReference;
|
||||||
|
PaletteReference paletteReferencePlayer;
|
||||||
|
PaletteReference paletteReferenceNormals;
|
||||||
|
PaletteReference paletteReferenceShadow;
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
if (renderable == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
renderable.Render(WorldRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PrepareRenderables()
|
||||||
|
{
|
||||||
|
var voxel = GetVoxel();
|
||||||
|
var palette = GetPalette();
|
||||||
|
var playerPalette = GetPlayerPalette();
|
||||||
|
var normalsPalette = GetNormalsPalette();
|
||||||
|
var shadowPalette = GetShadowPalette();
|
||||||
|
var scale = GetScale();
|
||||||
|
var rotation = GetRotation();
|
||||||
|
var lightAmbientColor = GetLightAmbientColor();
|
||||||
|
var lightDiffuseColor = GetLightDiffuseColor();
|
||||||
|
var lightPitch = GetLightPitch();
|
||||||
|
var lightYaw = GetLightYaw();
|
||||||
|
var cameraAngle = GetCameraAngle();
|
||||||
|
|
||||||
|
if (voxel == null || palette == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (voxel != cachedVoxel)
|
||||||
|
cachedVoxel = voxel;
|
||||||
|
|
||||||
|
if (palette != cachedPalette)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(palette) && string.IsNullOrEmpty(playerPalette))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var paletteName = string.IsNullOrEmpty(palette) ? playerPalette : palette;
|
||||||
|
paletteReference = WorldRenderer.Palette(paletteName);
|
||||||
|
cachedPalette = paletteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerPalette != cachedPlayerPalette)
|
||||||
|
{
|
||||||
|
paletteReferencePlayer = WorldRenderer.Palette(playerPalette);
|
||||||
|
cachedPlayerPalette = playerPalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalsPalette != cachedNormalsPalette)
|
||||||
|
{
|
||||||
|
paletteReferenceNormals = WorldRenderer.Palette(normalsPalette);
|
||||||
|
cachedNormalsPalette = normalsPalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shadowPalette != cachedShadowPalette)
|
||||||
|
{
|
||||||
|
paletteReferenceShadow = WorldRenderer.Palette(shadowPalette);
|
||||||
|
cachedShadowPalette = shadowPalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale != cachedScale)
|
||||||
|
cachedScale = scale;
|
||||||
|
|
||||||
|
if (rotation != cachedRotation)
|
||||||
|
cachedRotation = rotation;
|
||||||
|
|
||||||
|
if (lightPitch != cachedLightPitch)
|
||||||
|
cachedLightPitch = lightPitch;
|
||||||
|
|
||||||
|
if (lightYaw != cachedLightYaw)
|
||||||
|
cachedLightYaw = lightYaw;
|
||||||
|
|
||||||
|
if (cachedLightAmbientColor[0] != lightAmbientColor[0] || cachedLightAmbientColor[1] != lightAmbientColor[1] || cachedLightAmbientColor[2] != lightAmbientColor[2])
|
||||||
|
cachedLightAmbientColor = lightAmbientColor;
|
||||||
|
|
||||||
|
if (cachedLightDiffuseColor[0] != lightDiffuseColor[0] || cachedLightDiffuseColor[1] != lightDiffuseColor[1] || cachedLightDiffuseColor[2] != lightDiffuseColor[2])
|
||||||
|
cachedLightDiffuseColor = lightDiffuseColor;
|
||||||
|
|
||||||
|
if (cameraAngle != cachedCameraAngle)
|
||||||
|
cachedCameraAngle = cameraAngle;
|
||||||
|
|
||||||
|
if (cachedVoxel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var animation = new ModelAnimation(
|
||||||
|
cachedVoxel,
|
||||||
|
() => WVec.Zero,
|
||||||
|
() => cachedRotation,
|
||||||
|
() => false,
|
||||||
|
() => 0,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var animations = new ModelAnimation[] { animation };
|
||||||
|
|
||||||
|
ModelPreview preview = new ModelPreview(
|
||||||
|
new ModelAnimation[] { animation }, WVec.Zero, 0,
|
||||||
|
cachedScale,
|
||||||
|
new WAngle(cachedLightPitch),
|
||||||
|
new WAngle(cachedLightYaw),
|
||||||
|
cachedLightAmbientColor,
|
||||||
|
cachedLightDiffuseColor,
|
||||||
|
cachedCameraAngle,
|
||||||
|
paletteReference,
|
||||||
|
paletteReferenceNormals,
|
||||||
|
paletteReferenceShadow);
|
||||||
|
|
||||||
|
var screenBounds = animation.ScreenBounds(WPos.Zero, WorldRenderer, scale);
|
||||||
|
IdealPreviewSize = new int2(screenBounds.Width, screenBounds.Height);
|
||||||
|
var origin = RenderOrigin + new int2(RenderBounds.Size.Width / 2, RenderBounds.Size.Height / 2);
|
||||||
|
|
||||||
|
var camera = new WRot(WAngle.Zero, cachedCameraAngle - new WAngle(256), new WAngle(256));
|
||||||
|
var modelRenderable = new UIModelRenderable(
|
||||||
|
animations, WPos.Zero, origin, 0, camera, scale,
|
||||||
|
WRot.None, cachedLightAmbientColor, cachedLightDiffuseColor,
|
||||||
|
paletteReferencePlayer, paletteReferenceNormals, paletteReferenceShadow);
|
||||||
|
|
||||||
|
renderable = modelRenderable.PrepareRender(WorldRenderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,11 @@ Background@ASSETBROWSER_PANEL:
|
|||||||
Width: PARENT_RIGHT
|
Width: PARENT_RIGHT
|
||||||
Height: PARENT_BOTTOM
|
Height: PARENT_BOTTOM
|
||||||
AspectRatio: 1
|
AspectRatio: 1
|
||||||
|
Model@VOXEL:
|
||||||
|
Width: PARENT_RIGHT
|
||||||
|
Height: PARENT_BOTTOM
|
||||||
|
Palette: colorpicker
|
||||||
|
PlayerPalette: colorpicker
|
||||||
Label@ERROR:
|
Label@ERROR:
|
||||||
Y: 1
|
Y: 1
|
||||||
X: 5
|
X: 5
|
||||||
@@ -189,6 +194,55 @@ Background@ASSETBROWSER_PANEL:
|
|||||||
Height: 25
|
Height: 25
|
||||||
Font: TinyBold
|
Font: TinyBold
|
||||||
Align: Left
|
Align: Left
|
||||||
|
Container@VOXEL_SELECTOR:
|
||||||
|
X: 60
|
||||||
|
Y: 425
|
||||||
|
Children:
|
||||||
|
Label@ROLL:
|
||||||
|
X: 140
|
||||||
|
Y: 1
|
||||||
|
Width: 40
|
||||||
|
Height: 25
|
||||||
|
Font: TinyBold
|
||||||
|
Align: Left
|
||||||
|
Text: Roll
|
||||||
|
Slider@ROLL_SLIDER:
|
||||||
|
X: 165
|
||||||
|
Y: 3
|
||||||
|
Width: 100
|
||||||
|
Height: 20
|
||||||
|
MinimumValue: 1
|
||||||
|
MaximumValue: 1023
|
||||||
|
Label@PITCH:
|
||||||
|
X: 310
|
||||||
|
Y: 1
|
||||||
|
Width: 40
|
||||||
|
Height: 25
|
||||||
|
Font: TinyBold
|
||||||
|
Align: Left
|
||||||
|
Text: Pitch
|
||||||
|
Slider@PITCH_SLIDER:
|
||||||
|
X: 335
|
||||||
|
Y: 3
|
||||||
|
Width: 100
|
||||||
|
Height: 20
|
||||||
|
MinimumValue: 1
|
||||||
|
MaximumValue: 1023
|
||||||
|
Label@YAW:
|
||||||
|
X: 480
|
||||||
|
Y: 1
|
||||||
|
Width: 40
|
||||||
|
Height: 25
|
||||||
|
Font: TinyBold
|
||||||
|
Align: Left
|
||||||
|
Text: Yaw
|
||||||
|
Slider@YAW_SLIDER:
|
||||||
|
X: 505
|
||||||
|
Y: 3
|
||||||
|
Width: 100
|
||||||
|
Height: 20
|
||||||
|
MinimumValue: 1
|
||||||
|
MaximumValue: 1023
|
||||||
Button@CLOSE_BUTTON:
|
Button@CLOSE_BUTTON:
|
||||||
Key: escape
|
Key: escape
|
||||||
X: PARENT_RIGHT - 180
|
X: PARENT_RIGHT - 180
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ SpriteSequenceFormat: TilesetSpecificSpriteSequence
|
|||||||
ModelSequenceFormat: VoxelModelSequence
|
ModelSequenceFormat: VoxelModelSequence
|
||||||
|
|
||||||
AssetBrowser:
|
AssetBrowser:
|
||||||
SupportedExtensions: .shp, .tem, .sno, .vqa
|
SupportedExtensions: .shp, .tem, .sno, .vqa, .vxl
|
||||||
|
|
||||||
GameSpeeds:
|
GameSpeeds:
|
||||||
slowest:
|
slowest:
|
||||||
|
|||||||
Reference in New Issue
Block a user