RSI support (#552)

* RSI WiP

* More work but we're doing bsdiff now

* RSI loading seems to mostly work.

* Vector2u deserialization test.

* Add in packages again.

* This is the part where I realize I need a path manipulation library.

* The start of a path class but it's late so I'm going to bed.

* HIGHLY theoretical ResourcePath code.

Partially tested but not really.

* Allow x86 for unit tests I guess jesus christ.

Thanks Microsoft for still not shipping x64 VS in 2018.

* Resource paths work & are tested.

* I missed a doc spot.

* ResourcePaths implemented on the server.

* Client works with resource paths.

TIME FOR A REFACTOR.

* Some work but this might be a stupid idea so I migh throw it in the trash.

* Resources refactored completely.

They now only get the requested resourcepath.
They're in charge of opening files to load.

* RSI Loader WORKS.

* Update AudioResource for new loading support.

* Fix package references.

* Fix more references.

* Gonna work now?
This commit is contained in:
Pieter-Jan Briers
2018-04-12 21:53:19 +02:00
committed by GitHub
parent 796555fad5
commit d7414930ff
49 changed files with 1726 additions and 298 deletions

View File

@@ -7,6 +7,7 @@
<Copy SourceFiles="@(_ResourceFiles)" DestinationFolder="$(OutputPath)Resources\%(RecursiveDir)" />
</Target>
<Target Name="CopyBsdiffWrap">
<Exec Command="$(Python) ../Tools/download_bsdiffwrap.py $(Platform) $(TargetOS) $(OutputPath)" CustomErrorRegularExpression="^Error" />
<Exec Condition="'$(Platform)' == 'x64'" Command="$(Python) ../Tools/download_bsdiffwrap.py $(Platform) $(TargetOS) $(OutputPath)" CustomErrorRegularExpression="^Error" />
<Warning Condition="'$(Platform)' != 'x64'" Text="Did not download bsdiff because the platform is not set to x64. Only use this build for unit testing!" />
</Target>
</Project>

View File

@@ -25,6 +25,7 @@ using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Network.Messages;
using SS14.Shared.Prototypes;
using SS14.Shared.Utility;
using System;
namespace SS14.Client
@@ -77,6 +78,10 @@ namespace SS14.Client
public override void Main(Godot.SceneTree tree)
{
#if !X64
throw new InvalidOperationException("The client cannot start outside x64.");
#endif
PreInitIoC();
IoCManager.Resolve<ISceneTreeHolder>().Initialize(tree);
InitIoC();
@@ -113,7 +118,7 @@ namespace SS14.Client
_tileDefinitionManager.Initialize();
_networkManager.Initialize(false);
_console.Initialize();
_prototypeManager.LoadDirectory(@"./Prototypes/");
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
_prototypeManager.Resync();
_mapManager.Initialize();
placementManager.Initialize();

View File

@@ -22,7 +22,7 @@ namespace SS14.Client.GameObjects
public void SetIcon(string name)
{
Icon = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>($@"./Textures/{name}");
Icon = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>($@"/Textures/{name}");
}
}
}

View File

@@ -82,7 +82,7 @@ namespace SS14.Client.GameObjects
{
radius = FloatMath.Clamp(value, 2, 10);
var mgr = IoCManager.Resolve<IResourceCache>();
var tex = mgr.GetResource<TextureResource>($"Textures/Effects/Light/lighting_falloff_{(int)radius}.png");
var tex = mgr.GetResource<TextureResource>($"/Textures/Effects/Light/lighting_falloff_{(int)radius}.png");
// TODO: Maybe editing the global texture resource is not a good idea.
tex.Texture.GodotTexture.SetFlags(tex.Texture.GodotTexture.GetFlags() | (int)Godot.Texture.FlagsEnum.Filter);
Light.Texture = tex.Texture;

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SS14.Client.Graphics;
using SS14.Client.Graphics.ClientEye;
@@ -20,6 +19,7 @@ using SS14.Shared.Map;
using SS14.Shared.Maths;
using SS14.Shared.Utility;
using YamlDotNet.RepresentationModel;
using Path = System.IO.Path;
// Warning: Shitcode ahead!
namespace SS14.Client.GameObjects
@@ -147,7 +147,7 @@ namespace SS14.Client.GameObjects
}
var manager = IoCManager.Resolve<IResourceCache>();
if (manager.TryGetResource<TextureResource>($@"./Textures/{spriteKey}", out var sprite))
if (manager.TryGetResource<TextureResource>($@"/Textures/{spriteKey}", out var sprite))
{
AddSprite(spriteKey, sprite.Texture);
}
@@ -189,7 +189,7 @@ namespace SS14.Client.GameObjects
var ext = Path.GetExtension(curr.Key);
var withoutExt = Path.ChangeExtension(curr.Key, null);
string name = $"{withoutExt}_{dir.ToLowerInvariant()}{ext}";
if (resMgr.TryGetResource<TextureResource>(@"./Textures/" + name, out var res))
if (resMgr.TryGetResource<TextureResource>(@"/Textures/" + name, out var res))
dirSprites.Add(name, res.Texture);
}
}

View File

@@ -267,7 +267,7 @@ namespace SS14.Client.GameObjects
public Effect(EffectSystemMessage effectcreation, IResourceCache resourceCache)
{
EffectSprite = resourceCache.GetResource<TextureResource>("Textures/" + effectcreation.EffectSprite).Texture;
EffectSprite = resourceCache.GetResource<TextureResource>("/Textures/" + effectcreation.EffectSprite).Texture;
Coordinates = effectcreation.Coordinates;
EmitterCoordinates = effectcreation.EmitterCoordinates;
Velocity = effectcreation.Velocity;

View File

@@ -88,7 +88,7 @@ namespace SS14.Client.Graphics.Lighting
deferredViewport.AddChild(canvasModulate);
rootViewport.AddChild(deferredViewport);
var whiteTex = resourceCache.GetResource<TextureResource>(@"./Textures/Effects/Light/white.png");
var whiteTex = resourceCache.GetResource<TextureResource>(@"/Textures/Effects/Light/white.png");
deferredMaskBackground = new Godot.Sprite()
{
Name = "DeferredMaskBackground",

View File

@@ -0,0 +1,63 @@
using SS14.Shared.Maths;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace SS14.Client.Graphics
{
public sealed partial class RSI
{
/// <summary>
/// Represents a single icon state inside an RSI.
/// </summary>
public sealed class State
{
public Vector2u Size { get; }
public StateId StateId { get; }
public DirectionType Directions { get; }
public int DirectionsCount
{
get
{
switch (Directions)
{
case DirectionType.Dir1:
return 1;
case DirectionType.Dir4:
return 4;
case DirectionType.Dir8:
return 8;
default:
throw new InvalidOperationException("Unknown direction");
}
}
}
private (Texture icon, float delay)[][] Icons;
public State(Vector2u size, StateId stateId, DirectionType direction, (Texture icon, float delay)[][] icons)
{
Size = size;
StateId = stateId;
Directions = direction;
Icons = icons;
}
public enum DirectionType : byte
{
Dir1,
Dir4,
Dir8,
}
public (Texture icon, float delay) GetFrame(int direction, int frame)
{
return Icons[direction][frame];
}
public IReadOnlyCollection<(Texture icon, float delay)> GetDirectionFrames(int direction)
{
return Icons[direction];
}
}
}
}

View File

@@ -0,0 +1,119 @@
using Newtonsoft.Json.Linq;
using NJsonSchema;
using SS14.Shared.Log;
using SS14.Shared.Maths;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Shared.Utility;
namespace SS14.Client.Graphics
{
/// <summary>
/// Type to handle Robust Station Image (RSI) files.
/// </summary>
public sealed partial class RSI : IEnumerable<RSI.State>
{
/// <summary>
/// The size of this RSI, width x height.
/// </summary>
public Vector2u Size { get; private set; }
private Dictionary<StateId, State> States = new Dictionary<StateId, State>();
public State this[StateId key]
{
get => States[key];
}
public void AddState(State state)
{
States[state.StateId] = state;
}
public void RemoveState(StateId stateId)
{
States.Remove(stateId);
}
public bool TryGetState(StateId stateId, out State state)
{
return States.TryGetValue(stateId, out state);
}
public RSI(Vector2u size)
{
Size = size;
}
public IEnumerator<State> GetEnumerator()
{
return States.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
[Flags]
public enum Selectors
{
None = 0,
}
/// <summary>
/// Represents a name+selector pair used to reference states in an RSI.
/// </summary>
public struct StateId
{
public readonly string Name;
public readonly Selectors Selectors;
public StateId(string name, Selectors selectors)
{
Name = name;
Selectors = selectors;
}
public override string ToString()
{
return Name;
}
public static implicit operator StateId(string key)
{
return new StateId(key, Selectors.None);
}
public override bool Equals(object obj)
{
return obj is StateId id && Equals(id);
}
public bool Equals(StateId id)
{
return id.Name == Name && id.Selectors == Selectors;
}
public static bool operator ==(StateId a, StateId b)
{
return a.Equals(b);
}
public static bool operator !=(StateId a, StateId b)
{
return !a.Equals(b);
}
public override int GetHashCode()
{
return Name.GetHashCode() ^ Selectors.GetHashCode();
}
}
}
}

View File

@@ -0,0 +1,51 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "RSI Image Format Validation Schema V1",
"description": "Robust Station Image",
"type": "object",
"definitions": {
"size": {
"type": "object",
"properties": {
"x": {"type": "integer", "minimum": 1},
"y": {"type": "integer", "minimum": 1}
},
"required": ["x","y"]
},
"directions": {
"type": "integer",
"enum": [1,4,8]
},
"state": {
"type": "object",
"properties": {
"name": {"type": "string"},
"select": {
"type": "array",
"items": {"type": "string"}
},
"flags": {"type": "object"}, //To be de-serialized as a Dictionary
"directions": {"$ref": "#/definitions/directions"},
"delays": {
"type": "array",
"minItems": 1,
"items": {
"type": "array",
"items": {"type": "number", "minimum": 0, "exclusiveMinimum": true} //number == float
}
}
},
"required": ["name","select","flags","directions"] //'delays' is marked as optional in the spec
}
},
"properties": {
"version": {"type": "integer", "minimum": 1, "maximum": 1},
"size": {"$ref": "#/definitions/size"},
"states": {
"type": "array",
"items": {"$ref": "#/definitions/state"},
"minItems": 1
}
},
"required": ["version","size","states"]
}

View File

@@ -1,28 +1,39 @@
using Godot;
using SS14.Client.ResourceManagement;
using SS14.Shared.GameObjects;
using System.Collections.Generic;
using System.IO;
using SS14.Client.ResourceManagement;
using SS14.Shared.Interfaces;
using SS14.Shared.Utility;
namespace SS14.Client.Interfaces.ResourceManagement
{
public interface IResourceCache
public interface IResourceCache : IResourceManager
{
// For convenience.
void LoadLocalResources();
void LoadBaseResources();
/// <summary>
/// TEMPORARY: We need this because Godot can't load most resources without the disk easily.
/// </summary>
bool TryGetDiskFilePath(ResourcePath path, out string diskPath);
T GetResource<T>(string path, bool useFallback = true)
where T : BaseResource, new();
T GetResource<T>(ResourcePath path, bool useFallback = true)
where T : BaseResource, new();
bool TryGetResource<T>(string path, out T resource)
where T : BaseResource, new();
bool TryGetResource<T>(ResourcePath path, out T resource)
where T : BaseResource, new();
void CacheResource<T>(string path, T resource)
where T : BaseResource, new();
void CacheResource<T>(ResourcePath path, T resource)
where T : BaseResource, new();
T GetFallback<T>()
where T : BaseResource, new();
}

View File

@@ -27,7 +27,7 @@ namespace SS14.Client.Map
TileSet.CreateTile(ret);
if (!string.IsNullOrEmpty(tileDef.SpriteName))
{
var texture = resourceCache.GetResource<TextureResource>($@"./Textures/Tiles/{tileDef.SpriteName}.png");
var texture = resourceCache.GetResource<TextureResource>($@"/Textures/Tiles/{tileDef.SpriteName}.png");
TileSet.TileSetTexture(ret, texture.Texture.GodotTexture);
Textures[ret] = texture;
}

View File

@@ -333,7 +333,7 @@ namespace SS14.Client.Placement
//Will break if states not ordered correctly.
var spriteName = spriteParam == null ? "" : spriteParam.GetValue<string>();
var sprite = ResourceCache.GetResource<TextureResource>("Textures/" + spriteName);
var sprite = ResourceCache.GetResource<TextureResource>("/Textures/" + spriteName);
CurrentBaseSprite = sprite;
CurrentBaseSpriteKey = spriteName;
@@ -346,7 +346,7 @@ namespace SS14.Client.Placement
{
var tileDefs = _tileDefManager;
CurrentBaseSprite = ResourceCache.GetResource<TextureResource>("Textures/UserInterface/tilebuildoverlay.png");
CurrentBaseSprite = ResourceCache.GetResource<TextureResource>("/Textures/UserInterface/tilebuildoverlay.png");
CurrentBaseSpriteKey = "UserInterface/tilebuildoverlay.png";
IsActive = true;

View File

@@ -48,12 +48,12 @@ namespace SS14.Client.Placement
public TextureResource GetSprite(string key)
{
return pManager.ResourceCache.GetResource<TextureResource>("Textures/" + key);
return pManager.ResourceCache.GetResource<TextureResource>("/Textures/" + key);
}
public bool TryGetSprite(string key, out TextureResource sprite)
{
return pManager.ResourceCache.TryGetResource<TextureResource>("Textures/" + key, out sprite);
return pManager.ResourceCache.TryGetResource<TextureResource>("/Textures/" + key, out sprite);
}
public void SetSprite()
@@ -112,4 +112,3 @@ namespace SS14.Client.Placement
}
}
}

View File

@@ -1,5 +1,4 @@
using SS14.Shared.IoC;
using SS14.Shared.Reflection;
using SS14.Shared.Reflection;
using System.Collections.Generic;
namespace SS14.Client.Reflection

View File

@@ -1,6 +1,6 @@
using System;
using System.IO;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Shared.Utility;
namespace SS14.Client.ResourceManagement
{
@@ -25,7 +25,7 @@ namespace SS14.Client.ResourceManagement
/// Deserializes the resource from the VFS.
/// </summary>
/// <param name="cache">ResourceCache this resource is being loaded into.</param>
/// <param name="path">Path of the resource relative to the root of the ResourceCache.</param>
public abstract void Load(IResourceCache cache, string path);
/// <param name="path">Path of the resource requested on the VFS.</param>
public abstract void Load(IResourceCache cache, ResourcePath path);
}
}

View File

@@ -1,22 +1,16 @@
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Shared.ContentPack;
using SS14.Shared.Interfaces;
using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SS14.Client.ResourceManagement;
using SS14.Shared.Configuration;
namespace SS14.Client.ResourceManagement
{
public class ResourceCache : ResourceManager, IResourceCache, IDisposable
public partial class ResourceCache : ResourceManager, IResourceCache, IDisposable
{
private Dictionary<(string, Type), BaseResource> CachedResources = new Dictionary<(string, Type), BaseResource>();
private Dictionary<(ResourcePath, Type), BaseResource> CachedResources = new Dictionary<(ResourcePath, Type), BaseResource>();
public void LoadBaseResources()
{
@@ -30,7 +24,7 @@ namespace SS14.Client.ResourceManagement
MountContentDirectory(@"Resources/");
#else
MountContentDirectory(@"../../Resources/");
MountContentDirectory(@"Resources/Assemblies", "Assemblies/");
MountContentDirectory(@"Resources/Assemblies", new ResourcePath("/Assemblies/"));
#endif
//_resources.MountContentPack(@"./EngineContentPack.zip");
}
@@ -41,6 +35,11 @@ namespace SS14.Client.ResourceManagement
}
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
{
return GetResource<T>(new ResourcePath(path), useFallback);
}
public T GetResource<T>(ResourcePath path, bool useFallback = true) where T : BaseResource, new()
{
if (CachedResources.TryGetValue((path, typeof(T)), out var cached))
{
@@ -50,11 +49,7 @@ namespace SS14.Client.ResourceManagement
var _resource = new T();
try
{
if (!TryGetDiskFilePath(path, out var diskPath))
{
throw new FileNotFoundException(path);
}
_resource.Load(this, diskPath);
_resource.Load(this, path);
CachedResources[(path, typeof(T))] = _resource;
return _resource;
}
@@ -74,6 +69,11 @@ namespace SS14.Client.ResourceManagement
}
public bool TryGetResource<T>(string path, out T resource) where T : BaseResource, new()
{
return TryGetResource(new ResourcePath(path), out resource);
}
public bool TryGetResource<T>(ResourcePath path, out T resource) where T : BaseResource, new()
{
if (CachedResources.TryGetValue((path, typeof(T)), out var cached))
{
@@ -83,11 +83,7 @@ namespace SS14.Client.ResourceManagement
var _resource = new T();
try
{
if (!TryGetDiskFilePath(path, out var diskPath))
{
throw new FileNotFoundException(path);
}
_resource.Load(this, diskPath);
_resource.Load(this, path);
resource = _resource;
CachedResources[(path, typeof(T))] = resource;
return true;
@@ -100,11 +96,21 @@ namespace SS14.Client.ResourceManagement
}
public bool HasResource<T>(string path) where T : BaseResource, new()
{
return HasResource<T>(new ResourcePath(path));
}
public bool HasResource<T>(ResourcePath path) where T : BaseResource, new()
{
return TryGetResource<T>(path, out var _);
}
public void CacheResource<T>(string path, T resource) where T : BaseResource, new()
{
CacheResource(new ResourcePath(path), resource);
}
public void CacheResource<T>(ResourcePath path, T resource) where T : BaseResource, new()
{
CachedResources[(path, typeof(T))] = resource;
}

View File

@@ -1,5 +1,7 @@
using SS14.Client.Audio;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Shared.Utility;
using System;
using System.IO;
namespace SS14.Client.ResourceManagement
@@ -8,24 +10,26 @@ namespace SS14.Client.ResourceManagement
{
public AudioStream AudioStream { get; private set; }
public override void Load(IResourceCache cache, string diskPath)
public override void Load(IResourceCache cache, ResourcePath path)
{
if (!File.Exists(diskPath))
if (!cache.ContentFileExists(path))
{
throw new FileNotFoundException(diskPath);
throw new FileNotFoundException("Content file does not exist for audio sample.");
}
var data = File.ReadAllBytes(diskPath);
var stream = new Godot.AudioStreamOGGVorbis()
using (var fileStream = cache.ContentFileRead(path))
{
Data = data
};
if (stream.GetLength() == 0)
{
throw new InvalidDataException();
var stream = new Godot.AudioStreamOGGVorbis()
{
Data = fileStream.ToArray(),
};
if (stream.GetLength() == 0)
{
throw new InvalidDataException();
}
AudioStream = new GodotAudioStreamSource(stream);
Shared.Log.Logger.Debug($"{stream.GetLength()}");
}
AudioStream = new GodotAudioStreamSource(stream);
Shared.Log.Logger.Debug($"{stream.GetLength()}");
}
public static implicit operator AudioStream(AudioResource res)

View File

@@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks;
using SS14.Client.Interfaces.ResourceManagement;
using System.IO;
using SS14.Shared.Utility;
namespace SS14.Client.ResourceManagement
{
@@ -14,14 +15,18 @@ namespace SS14.Client.ResourceManagement
public DynamicFont Font { get; private set; }
public DynamicFontData FontData { get; private set; }
public override void Load(IResourceCache cache, string path)
public override void Load(IResourceCache cache, ResourcePath path)
{
if (!System.IO.File.Exists(path))
if (!cache.ContentFileExists(path))
{
throw new FileNotFoundException(path);
throw new FileNotFoundException("Content file does not exist for texture");
}
if (!cache.TryGetDiskFilePath(path, out string diskPath))
{
throw new InvalidOperationException("Textures can only be loaded from disk.");
}
var res = ResourceLoader.Load(path);
var res = ResourceLoader.Load(diskPath);
if (!(res is DynamicFontData fontData))
{
throw new InvalidDataException("Path does not point to a font.");

View File

@@ -0,0 +1,190 @@
using Newtonsoft.Json.Linq;
using NJsonSchema;
using SS14.Client.Graphics;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Shared.Log;
using SS14.Shared.Maths;
using SS14.Shared.Utility;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
namespace SS14.Client.ResourceManagement
{
/// <summary>
/// Handles the loading code for RSI files.
/// See <see cref="RSI"/> for the RSI API itself.
/// </summary>
public sealed class RSIResource : BaseResource
{
public RSI RSI { get; private set; }
/// <summary>
/// The minimum version of RSI we can load.
/// </summary>
public const uint MINIMUM_RSI_VERSION = 1;
/// <summary>
/// The maximum version of RSI we can load.
/// </summary>
public const uint MAXIMUM_RSI_VERSION = 1;
public override void Load(IResourceCache cache, ResourcePath path)
{
var manifestPath = path / "meta.json";
string manifestContents;
using (var manifestFile = cache.ContentFileRead(manifestPath))
using (var reader = new StreamReader(manifestFile))
{
manifestContents = reader.ReadToEnd();
}
var errors = RSISchema.Validate(manifestContents);
if (errors.Count != 0)
{
Logger.Error($"Unable to load RSI from '{path}', {errors.Count} errors:");
foreach (var error in errors)
{
Logger.Error(error.ToString());
}
throw new RSILoadException($"{errors.Count} errors while loading RSI. See console.");
}
// Ok schema validated just fine.
var manifestJson = JObject.Parse(manifestContents);
var size = manifestJson["size"].ToObject<Vector2u>();
var rsi = new RSI(size);
// Do every state.
foreach (var stateObject in manifestJson["states"].Cast<JObject>())
{
var stateName = stateObject["name"].ToObject<string>();
var dirValue = stateObject["directions"].ToObject<int>();
RSI.State.DirectionType directions;
switch (dirValue)
{
case 1:
directions = RSI.State.DirectionType.Dir1;
break;
case 4:
directions = RSI.State.DirectionType.Dir4;
break;
case 8:
directions = RSI.State.DirectionType.Dir8;
break;
default:
throw new RSILoadException($"Invalid direction: {dirValue}");
}
// We can ignore selectors and flags for now,
// because they're not used yet!
// Get the lists of delays.
float[][] delays;
if (stateObject.TryGetValue("delays", out var delayToken))
{
delays = delayToken.ToObject<float[][]>();
if (delays.Length != dirValue)
{
throw new RSILoadException($"Directions count does not match amount of delays specified.");
}
for (var i = 0; i < delays.Length; i++)
{
var delayList = delays[i];
if (delayList.Length == 0)
{
delays[i] = new float[] { 1 };
}
}
}
else
{
delays = new float[dirValue][];
// No delays specified, default to 1 frame per dir.
for (var i = 0; i < dirValue; i++)
{
delays[i] = new float[] { 1 };
}
}
var texPath = path / (stateName + ".png");
var texture = cache.GetResource<TextureResource>(texPath).Texture;
if (texture.Width % size.X != 0 || texture.Height % size.Y != 0)
{
throw new RSILoadException("State image size is not a multiple of the icon size.");
}
// Amount of icons per row of the sprite sheet.
var sheetWidth = texture.Width / size.X;
var iconFrames = new(Texture, float)[dirValue][];
var counter = 0;
for (var j = 0; j < iconFrames.Length; j++)
{
var delayList = delays[j];
var directionFrames = new(Texture, float)[delayList.Length];
for (var i = 0; i < delayList.Length; i++)
{
var PosX = (counter % sheetWidth) * size.X;
var PosY = (counter / sheetWidth) * size.Y;
var atlasTexture = new Godot.AtlasTexture()
{
Atlas = texture,
Region = new Godot.Rect2(PosX, PosY, size.X, size.Y)
};
directionFrames[i] = (new GodotTextureSource(atlasTexture), delayList[i]);
counter++;
}
iconFrames[j] = directionFrames;
}
var state = new RSI.State(size, stateName, directions, iconFrames);
rsi.AddState(state);
}
RSI = rsi;
}
private static readonly JsonSchema4 RSISchema = GetSchema();
private static JsonSchema4 GetSchema()
{
string schema;
using (var schemaStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("SS14.Client.Graphics.RSI.RSISchema.json"))
using (var schemaReader = new StreamReader(schemaStream))
{
schema = schemaReader.ReadToEnd();
}
return JsonSchema4.FromJsonAsync(schema).Result;
}
}
[Serializable]
public class RSILoadException : Exception
{
public RSILoadException()
{
}
public RSILoadException(string message) : base(message)
{
}
public RSILoadException(string message, Exception inner) : base(message, inner)
{
}
protected RSILoadException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
}

View File

@@ -1,21 +1,27 @@
using SS14.Client.Graphics;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Shared.Log;
using SS14.Shared.Utility;
using System;
using System.IO;
namespace SS14.Client.ResourceManagement
{
public class TextureResource : BaseResource
{
public override string Fallback => "Textures/noSprite.png";
public override string Fallback => "/Textures/noSprite.png";
public Texture Texture { get; private set; }
private Godot.ImageTexture godotTexture;
public override void Load(IResourceCache cache, string diskPath)
public override void Load(IResourceCache cache, ResourcePath path)
{
if (!File.Exists(diskPath))
if (!cache.ContentFileExists(path))
{
throw new FileNotFoundException(diskPath);
throw new FileNotFoundException("Content file does not exist for texture");
}
if (!cache.TryGetDiskFilePath(path, out string diskPath))
{
throw new InvalidOperationException("Textures can only be loaded from disk.");
}
godotTexture = new Godot.ImageTexture();
godotTexture.Load(diskPath);

View File

@@ -33,12 +33,12 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DefineConstants>DEBUG;TRACE;X64</DefineConstants>
<DebugType>portable</DebugType>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<DefineConstants>TRACE;RELEASE</DefineConstants>
<DefineConstants>TRACE;RELEASE;X64</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
@@ -57,8 +57,16 @@
</PropertyGroup>
<Import Project="..\MSBuild\SS14.DefineConstants.targets" />
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NJsonSchema, Version=9.10.42.0, Culture=neutral, PublicKeyToken=c2f9c3bdfae56102, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\NJsonSchema.9.10.42\lib\net45\NJsonSchema.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Net" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
@@ -155,6 +163,9 @@
<Compile Include="ResourceManagement\ResourceCache.cs" />
<Compile Include="ResourceManagement\ResourceTypes\AudioResource.cs" />
<Compile Include="ResourceManagement\ResourceTypes\FontResource.cs" />
<Compile Include="Graphics\RSI\RSI.cs" />
<Compile Include="Graphics\RSI\RSI.State.cs" />
<Compile Include="ResourceManagement\ResourceTypes\RSIResource.cs" />
<Compile Include="ResourceManagement\ResourceTypes\TextureResource.cs" />
<Compile Include="SceneTreeHolder.cs" />
<Compile Include="UserInterface\Controls\BaseButton.cs" />
@@ -258,7 +269,12 @@
<Name>SS14.Shared</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<EmbeddedResource Include="Graphics\RSI\RSISchema.json" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\MSBuild\SS14.Engine.targets" />
<Target Name="AfterBuild" DependsOnTargets="CopyBsdiffWrap" />
</Project>
</Project>

View File

@@ -161,7 +161,7 @@ namespace SS14.Client.UserInterface.CustomControls
if (spriteNameParam != null)
spriteName = spriteNameParam.GetValue<string>();
var tex = resourceCache.GetResource<TextureResource>("Textures/" + spriteName);
var tex = resourceCache.GetResource<TextureResource>("/Textures/" + spriteName);
var rect = container.GetChild("TextureWrap").GetChild<TextureRect>("TextureRect");
if (tex != null)
{

11
SS14.Client/app.config Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net451" />
<package id="NJsonSchema" version="9.10.42" targetFramework="net451" />
<package id="SharpZipLib" version="0.86.0" targetFramework="net451" />
<package id="YamlDotNet" version="4.3.0" targetFramework="net451" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net451" />
</packages>
<package id="YamlDotNet" version="4.3.0" targetFramework="net451" />
</packages>

View File

@@ -37,6 +37,7 @@ using SS14.Server.Player;
using SS14.Shared.Enums;
using SS14.Shared.Reflection;
using SS14.Shared.Timing;
using SS14.Shared.Utility;
namespace SS14.Server
{
@@ -180,7 +181,7 @@ namespace SS14.Server
// Load from the resources dir in the repo root instead.
// It's a debug build so this is fine.
_resources.MountContentDirectory(@"../../Resources/");
_resources.MountContentDirectory(@"Resources/Assemblies", "Assemblies/");
_resources.MountContentDirectory(@"Resources/Assemblies", new ResourcePath("/Assemblies/"));
#endif
//mount the engine content pack
@@ -219,7 +220,7 @@ namespace SS14.Server
// because of 'reasons' this has to be called after the last assembly is loaded
// otherwise the prototypes will be cleared
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.LoadDirectory(@"Prototypes");
prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes"));
prototypeManager.Resync();
var clientConsole = IoCManager.Resolve<IClientConsoleHost>();
@@ -325,7 +326,7 @@ namespace SS14.Server
//TODO: This should prob shutdown all managers in a loop.
// remove all maps
if(_runLevel == ServerRunLevel.Game)
if (_runLevel == ServerRunLevel.Game)
{
var mapMgr = IoCManager.Resolve<IMapManager>();

View File

@@ -87,11 +87,11 @@ namespace SS14.Server.Chat
private void LoadEmotes()
{
if (!_resources.TryContentFileRead(@"emotes.xml", out var emoteFileStream))
if (!_resources.TryContentFileRead(@"/emotes.xml", out var emoteFileStream))
return;
var serializer = new XmlSerializer(typeof(List<Emote>));
var emotes = (List<Emote>) serializer.Deserialize(emoteFileStream);
var emotes = (List<Emote>)serializer.Deserialize(emoteFileStream);
emoteFileStream.Close();
foreach (var emote in emotes)

View File

@@ -10,6 +10,7 @@ using SS14.Shared.Log;
using SS14.Shared.Map;
using SS14.Shared.Prototypes;
using YamlDotNet.RepresentationModel;
using SS14.Shared.Utility;
namespace SS14.Server.Maps
{
@@ -42,7 +43,7 @@ namespace SS14.Server.Maps
var document = new YamlDocument(root);
var rootPath = _resMan.ConfigDirectory;
var path = Path.Combine(rootPath, "./", yamlPath);
var path = Path.Combine(rootPath.ToString(), "./", yamlPath);
var fullPath = Path.GetFullPath(path);
var dir = Path.GetDirectoryName(fullPath);
@@ -72,7 +73,7 @@ namespace SS14.Server.Maps
Logger.Info($"[MAP] No user blueprint path: {fullPath}");
// fallback to content
if (_resMan.TryContentFileRead(path, out var contentReader))
if (_resMan.TryContentFileRead(ResourcePath.Root / path, out var contentReader))
{
reader = new StreamReader(contentReader);
}

View File

@@ -50,6 +50,9 @@ namespace SS14.Server
{
private static void Main(string[] args)
{
#if !X64
throw new InvalidOperationException("The server cannot start outside x64.");
#endif
//Register minidump dumper only if the app isn't being debugged. No use filling up hard drives with shite
RegisterIoC();
LoadContentAssemblies();

View File

@@ -50,13 +50,13 @@
<AllowedReferenceRelatedFileExtensions>.dll.config</AllowedReferenceRelatedFileExtensions>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
<DefineConstants>TRACE;DEBUG</DefineConstants>
<DefineConstants>TRACE;DEBUG;X64</DefineConstants>
<DebugSymbols>True</DebugSymbols>
<Optimize>False</Optimize>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
<DefineConstants>TRACE;RELEASE</DefineConstants>
<DefineConstants>TRACE;RELEASE;X64</DefineConstants>
<Optimize>True</Optimize>
<PlatformTarget>x64</PlatformTarget>
<AllowedReferenceRelatedFileExtensions>.dll.config</AllowedReferenceRelatedFileExtensions>

View File

@@ -28,6 +28,10 @@
<assemblyIdentity name="ICSharpCode.SharpZipLib" publicKeyToken="1b03e6acf1164f73" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.86.0.518" newVersion="0.86.0.518" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -177,17 +177,17 @@ namespace SS14.Shared.ContentPack
public static bool TryLoadAssembly<T>(IResourceManager resMan, string assemblyName) where T : GameShared
{
// get the assembly from the file system
if (resMan.TryContentFileRead($@"Assemblies/{assemblyName}.dll", out MemoryStream gameDll))
if (resMan.TryContentFileRead($@"/Assemblies/{assemblyName}.dll", out MemoryStream gameDll))
{
Logger.Debug($"[SRV] Loading {assemblyName} DLL");
// see if debug info is present
if (resMan.TryContentFileRead($@"Assemblies/{assemblyName}.pdb", out MemoryStream gamePdb))
if (resMan.TryContentFileRead($@"/Assemblies/{assemblyName}.pdb", out MemoryStream gamePdb))
{
try
{
// load the assembly into the process, and bootstrap the GameServer entry point.
AssemblyLoader.LoadGameAssembly<T>(gameDll.ToArray(), gamePdb.ToArray());
LoadGameAssembly<T>(gameDll.ToArray(), gamePdb.ToArray());
return true;
}
catch (Exception e)
@@ -201,7 +201,7 @@ namespace SS14.Shared.ContentPack
try
{
// load the assembly into the process, and bootstrap the GameServer entry point.
AssemblyLoader.LoadGameAssembly<T>(gameDll.ToArray());
LoadGameAssembly<T>(gameDll.ToArray());
return true;
}
catch (Exception e)

View File

@@ -1,65 +1,71 @@
using SS14.Shared.Log;
using SS14.Shared.Utility;
using System.Collections.Generic;
using System.IO;
namespace SS14.Shared.ContentPack
{
/// <summary>
/// Holds info about a directory that is mounted in the VFS.
/// </summary>
internal class DirLoader : IContentRoot
public partial class ResourceManager
{
private readonly DirectoryInfo _directory;
/// <summary>
/// Constructor.
/// Holds info about a directory that is mounted in the VFS.
/// </summary>
/// <param name="directory">Directory to mount.</param>
public DirLoader(DirectoryInfo directory)
class DirLoader : IContentRoot
{
_directory = directory;
}
private readonly DirectoryInfo _directory;
/// <inheritdoc />
public bool Mount()
{
// Exists returns false if it actually exists, but no perms to read it
return _directory.Exists;
}
/// <inheritdoc />
public MemoryStream GetFile(string relPath)
{
var path = GetPath(relPath);
if (path == null)
/// <summary>
/// Constructor.
/// </summary>
/// <param name="directory">Directory to mount.</param>
public DirLoader(DirectoryInfo directory)
{
return null;
_directory = directory;
}
var bytes = File.ReadAllBytes(path);
return new MemoryStream(bytes, false);
}
internal string GetPath(string relPath)
{
var fullPath = Path.GetFullPath(Path.Combine(_directory.FullName, relPath));
if (!File.Exists(fullPath))
return null;
return fullPath;
}
/// <inheritdoc />
public IEnumerable<string> FindFiles(string path)
{
var fullPath = Path.GetFullPath(Path.Combine(_directory.FullName, path));
var paths = PathHelpers.GetFiles(fullPath);
// GetFiles returns full paths, we want them relative to root
foreach (var filePath in paths)
/// <inheritdoc />
public void Mount()
{
yield return filePath.Substring(_directory.FullName.Length);
// Looks good to me
// Nothing to check here since the ResourceManager handles checking permissions.
}
/// <inheritdoc />
public bool TryGetFile(ResourcePath relPath, out MemoryStream fileStream)
{
var path = GetPath(relPath);
if (!File.Exists(path))
{
fileStream = null;
return false;
}
var bytes = File.ReadAllBytes(path);
fileStream = new MemoryStream(bytes, false);
return true;
}
internal string GetPath(ResourcePath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
}
/// <inheritdoc />
public IEnumerable<ResourcePath> FindFiles(ResourcePath path)
{
var fullPath = GetPath(path);
if (!Directory.Exists(fullPath))
{
yield break;
}
var paths = PathHelpers.GetFiles(fullPath);
// GetFiles returns full paths, we want them relative to root
foreach (var filePath in paths)
{
var relpath = filePath.Substring(_directory.FullName.Length);
yield return ResourcePath.FromRelativeSystemPath(relpath);
}
}
}
}

View File

@@ -1,31 +1,35 @@
using System.Collections.Generic;
using SS14.Shared.Utility;
using System.Collections.Generic;
using System.IO;
namespace SS14.Shared.ContentPack
{
/// <summary>
/// Common interface for mounting various things in the VFS
/// </summary>
internal interface IContentRoot
public partial class ResourceManager
{
/// <summary>
/// Initializes the content root.
/// Common interface for mounting various things in the VFS.
/// </summary>
/// <returns>If the content was mounted properly.</returns>
bool Mount();
protected interface IContentRoot
{
/// <summary>
/// Initializes the content root.
/// Throws an exception if the content root failed to mount.
/// </summary>
void Mount();
/// <summary>
/// Gets a file from the content root using the relative path.
/// </summary>
/// <param name="relPath">Relative path from the root directory.</param>
/// <returns>A stream of the file loaded into memory.</returns>
MemoryStream GetFile(string relPath);
/// <summary>
/// Gets a file from the content root using the relative path.
/// </summary>
/// <param name="relPath">Relative path from the root directory.</param>
/// <returns>A stream of the file loaded into memory.</returns>
bool TryGetFile(ResourcePath relPath, out MemoryStream stream);
/// <summary>
/// Recursively finds all files in a directory and all sub directories.
/// </summary>
/// <param name="path">Directory to search inside of.</param>
/// <returns>Enumeration of all relative file paths of the files found.</returns>
IEnumerable<string> FindFiles(string path);
/// <summary>
/// Recursively finds all files in a directory and all sub directories.
/// </summary>
/// <param name="path">Directory to search inside of.</param>
/// <returns>Enumeration of all relative file paths of the files found.</returns>
IEnumerable<ResourcePath> FindFiles(ResourcePath path);
}
}
}

View File

@@ -2,65 +2,70 @@
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using SS14.Shared.Log;
using SS14.Shared.Utility;
namespace SS14.Shared.ContentPack
{
/// <summary>
/// Loads a zipped content pack into the VFS.
/// </summary>
internal class PackLoader : IContentRoot
public partial class ResourceManager
{
private readonly FileInfo _pack;
private ZipFile _zip;
/// <summary>
/// Constructor.
/// Loads a zipped content pack into the VFS.
/// </summary>
/// <param name="pack">The zip file to mount in the VFS.</param>
public PackLoader(FileInfo pack)
class PackLoader : IContentRoot
{
_pack = pack;
}
private readonly FileInfo _pack;
private ZipFile _zip;
/// <inheritdoc />
public bool Mount()
{
Logger.Info($"[RES] Loading ContentPack: {_pack.FullName}...");
var zipFileStream = File.OpenRead(_pack.FullName);
_zip = new ZipFile(zipFileStream);
return true;
}
/// <inheritdoc />
public MemoryStream GetFile(string relPath)
{
var entry = _zip.GetEntry(relPath);
if (entry == null)
return null;
// this caches the deflated entry stream in memory
// this way people can read the stream however many times they want to,
// without the performance hit of deflating it every time.
var memStream = new MemoryStream();
using (var zipStream = _zip.GetInputStream(entry))
/// <summary>
/// Constructor.
/// </summary>
/// <param name="pack">The zip file to mount in the VFS.</param>
public PackLoader(FileInfo pack)
{
zipStream.CopyTo(memStream);
memStream.Position = 0;
_pack = pack;
}
return memStream;
}
/// <inheritdoc />
public IEnumerable<string> FindFiles(string path)
{
foreach (ZipEntry zipEntry in _zip)
/// <inheritdoc />
public void Mount()
{
if (zipEntry.IsFile && zipEntry.Name.StartsWith(path))
yield return zipEntry.Name;
Logger.Info($"[RES] Loading ContentPack: {_pack.FullName}...");
var zipFileStream = File.OpenRead(_pack.FullName);
_zip = new ZipFile(zipFileStream);
}
/// <inheritdoc />
public bool TryGetFile(ResourcePath relPath, out MemoryStream fileStream)
{
var entry = _zip.GetEntry(relPath.ToRootedPath().ToString());
if (entry == null)
{
fileStream = null;
return false;
}
// this caches the deflated entry stream in memory
// this way people can read the stream however many times they want to,
// without the performance hit of deflating it every time.
fileStream = new MemoryStream();
using (var zipStream = _zip.GetInputStream(entry))
{
zipStream.CopyTo(fileStream);
fileStream.Position = 0;
}
return true;
}
/// <inheritdoc />
public IEnumerable<ResourcePath> FindFiles(ResourcePath path)
{
foreach (ZipEntry zipEntry in _zip)
{
if (zipEntry.IsFile && zipEntry.Name.StartsWith(path.ToRootedPath().ToString()))
yield return new ResourcePath(zipEntry.Name).ToRelativePath();
}
}
}
}

View File

@@ -7,13 +7,14 @@ using SS14.Shared.Interfaces;
using SS14.Shared.Interfaces.Configuration;
using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Utility;
namespace SS14.Shared.ContentPack
{
/// <summary>
/// Virtual file system for all disk resources.
/// </summary>
public class ResourceManager : IResourceManager
public partial class ResourceManager : IResourceManager
{
private const string DataFolderName = "Space Station 14";
@@ -21,7 +22,7 @@ namespace SS14.Shared.ContentPack
private readonly IConfigurationManager _config;
private DirectoryInfo _configRoot;
private readonly List<(string prefix, IContentRoot root)> _contentRoots = new List<(string, IContentRoot)>();
private readonly List<(ResourcePath prefix, IContentRoot root)> _contentRoots = new List<(ResourcePath, IContentRoot)>();
/// <inheritdoc />
public string ConfigDirectory => _configRoot.FullName;
@@ -55,107 +56,142 @@ namespace SS14.Shared.ContentPack
}
/// <inheritdoc />
public void MountContentPack(string pack, string prefix=null)
public void MountContentPack(string pack, ResourcePath prefix = null)
{
if (prefix == null)
{
prefix = ResourcePath.Root;
}
if (!prefix.IsRooted)
{
throw new ArgumentException("Prefix must be rooted.", nameof(prefix));
}
pack = PathHelpers.ExecutableRelativeFile(pack);
var packInfo = new FileInfo(pack);
if (!packInfo.Exists)
{
throw new FileNotFoundException("Specified ContentPack does not exist: " + packInfo.FullName);
}
//create new PackLoader
var loader = new PackLoader(packInfo);
if (loader.Mount())
_contentRoots.Add((prefix, loader));
loader.Mount();
_contentRoots.Add((prefix, loader));
}
/// <inheritdoc />
public void MountContentDirectory(string path, string prefix=null)
public void MountContentDirectory(string path, ResourcePath prefix = null)
{
if (prefix == null)
{
prefix = ResourcePath.Root;
}
if (!prefix.IsRooted)
{
throw new ArgumentException("Prefix must be rooted.", nameof(prefix));
}
path = PathHelpers.ExecutableRelativeFile(path);
var pathInfo = new DirectoryInfo(path);
if (!pathInfo.Exists)
{
throw new DirectoryNotFoundException("Specified directory does not exist: " + pathInfo.FullName);
}
var loader = new DirLoader(pathInfo);
if (loader.Mount())
{
_contentRoots.Add((prefix, loader));
}
else
{
Logger.Error($"Unable to mount content directory: {path}");
}
loader.Mount();
_contentRoots.Add((prefix, loader));
}
/// <inheritdoc />
public MemoryStream ContentFileRead(string path)
{
// loop over each root trying to get the file
foreach ((var prefix, var root) in _contentRoots)
{
if (!TryHandlePrefix(path, prefix, out var tempPath))
{
continue;
}
var file = root.GetFile(tempPath);
if (file != null)
return file;
}
return null;
return ContentFileRead(new ResourcePath(path));
}
// TODO: Remove this when/if we can get Godot to load from not-the filesystem.
protected bool TryGetDiskFilePath(string path, out string diskPath)
/// <inheritdoc />
public MemoryStream ContentFileRead(ResourcePath path)
{
// loop over each root trying to get the file
foreach ((var prefix, var root) in _contentRoots)
if (TryContentFileRead(path, out var fileStream))
{
if (!(root is DirLoader dirLoader) || !TryHandlePrefix(path, prefix, out var tempPath))
{
continue;
}
diskPath = dirLoader.GetPath(tempPath);
if (diskPath != null)
return true;
return fileStream;
}
diskPath = null;
return false;
throw new FileNotFoundException($"Path does not exist in the VFS: '{path}'");
}
/// <inheritdoc />
public bool TryContentFileRead(string path, out MemoryStream fileStream)
{
var file = ContentFileRead(path);
if (file != null)
return TryContentFileRead(new ResourcePath(path), out fileStream);
}
/// <inheritdoc />
public bool TryContentFileRead(ResourcePath path, out MemoryStream fileStream)
{
if (path == null)
{
fileStream = file;
return true;
throw new ArgumentNullException(nameof(path));
}
fileStream = default(MemoryStream);
if (!path.IsRooted)
{
throw new ArgumentException("Path must be rooted", nameof(path));
}
foreach ((var prefix, var root) in _contentRoots)
{
if (!path.TryRelativeTo(prefix, out var relative))
{
continue;
}
if (root.TryGetFile(relative, out fileStream))
{
return true;
}
}
fileStream = null;
return false;
}
/// <inheritdoc />
public bool ContentFileExists(string path)
{
throw new NotImplementedException();
return ContentFileExists(new ResourcePath(path));
}
/// <inheritdoc />
public IEnumerable<string> ContentFindFiles(string path)
public bool ContentFileExists(ResourcePath path)
{
var alreadyReturnedFiles = new HashSet<string>();
return TryContentFileRead(path, out var _);
}
/// <inheritdoc />
public IEnumerable<ResourcePath> ContentFindFiles(string path)
{
return ContentFindFiles(new ResourcePath(path));
}
/// <inheritdoc />
public IEnumerable<ResourcePath> ContentFindFiles(ResourcePath path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (!path.IsRooted)
{
throw new ArgumentException("Path is not rooted", nameof(path));
}
var alreadyReturnedFiles = new HashSet<ResourcePath>();
foreach ((var prefix, var root) in _contentRoots)
{
if (!TryHandlePrefix(path, prefix, out var tempPath))
if (!path.TryRelativeTo(prefix, out var relative))
{
continue;
}
foreach (var filename in root.FindFiles(tempPath))
foreach (var filename in root.FindFiles(relative))
{
var newpath = prefix + filename;
var newpath = prefix / filename;
if (!alreadyReturnedFiles.Contains(newpath))
{
alreadyReturnedFiles.Add(newpath);
@@ -165,24 +201,22 @@ namespace SS14.Shared.ContentPack
}
}
private bool TryHandlePrefix(string path, string prefix, out string actualPath)
// TODO: Remove this when/if we can get Godot to load from not-the-filesystem.
public bool TryGetDiskFilePath(ResourcePath path, out string diskPath)
{
if (string.IsNullOrWhiteSpace(prefix))
// loop over each root trying to get the file
foreach ((var prefix, var root) in _contentRoots)
{
actualPath = path;
return true;
}
if (path.StartsWith(prefix))
{
actualPath = path.Substring(prefix.Length);
return true;
}
else
{
actualPath = null;
return false;
if (!(root is DirLoader dirLoader) || !path.TryRelativeTo(prefix, out var tempPath))
{
continue;
}
diskPath = dirLoader.GetPath(tempPath);
if (File.Exists(diskPath))
return true;
}
diskPath = null;
return false;
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using SS14.Shared.Utility;
using System;
using System.Collections.Generic;
using System.IO;
namespace SS14.Shared.Interfaces
@@ -22,47 +24,108 @@ namespace SS14.Shared.Interfaces
/// Loads a content pack from disk into the VFS. The path is relative to
/// the executable location on disk.
/// </summary>
/// <param name="pack"></param>
void MountContentPack(string pack, string prefix=null);
/// <param name="pack">The path of the pack to load on disk.</param>
/// <param name="prefix">The resource path to which all files in the pack will be relative to in the VFS.</param>
/// <exception cref="FileNotFoundException">Thrown if <paramref name="pack"/> does not exist on disk.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="prefix"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="pack"/> is null.</exception>
void MountContentPack(string pack, ResourcePath prefix = null);
/// <summary>
/// Adds a directory to search inside of to the VFS. The directory is relative to
/// the executable location on disk.
/// </summary>
/// <param name="path"></param>
void MountContentDirectory(string path, string prefix=null);
/// <param name="path">The path of the directory to add to the VFS on disk.</param>
/// <param name="prefix">The resource path to which all files in the directory will be relative to in the VFS.</param>
/// <exception cref="DirectoryNotFoundException">Thrown if <paramref name="path"/> does not exist on disk.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="prefix"/> passed is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
void MountContentDirectory(string path, ResourcePath prefix = null);
/// <summary>
/// Read a file from the mounted content roots.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <param name="path">The path to the file in the VFS. Must be rooted.</param>
/// <returns>The memory stream of the file.</returns>
/// <exception cref="FileNotFoundException">Thrown if <paramref name="path"/> does not exist in the VFS.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
MemoryStream ContentFileRead(ResourcePath path);
/// <summary>
/// Read a file from the mounted content roots.
/// </summary>
/// <param name="path">The path to the file in the VFS. Must be rooted.</param>
/// <returns>The memory stream of the file.</returns>
/// <exception cref="FileNotFoundException">Thrown if <paramref name="path"/> does not exist in the VFS.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
MemoryStream ContentFileRead(string path);
/// <summary>
/// Check if a file exists in any of the mounted content roots.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <param name="path">The path of the file to check.</param>
/// <returns>True if the file exists, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
bool ContentFileExists(ResourcePath path);
/// <summary>
/// Check if a file exists in any of the mounted content roots.
/// </summary>
/// <param name="path">The path of the file to check.</param>
/// <returns>True if the file exists, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
bool ContentFileExists(string path);
/// <summary>
/// Try to read a file from the mounted content roots.
/// </summary>
/// <param name="path"></param>
/// <param name="fileStream"></param>
/// <returns></returns>
/// <param name="path">The path of the file to try to read.</param>
/// <param name="fileStream">The memory stream of the file's contents. Null if the file could not be loaded.</param>
/// <returns>True if the file could be loaded, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
bool TryContentFileRead(ResourcePath path, out MemoryStream fileStream);
/// <summary>
/// Try to read a file from the mounted content roots.
/// </summary>
/// <param name="path">The path of the file to try to read.</param>
/// <param name="fileStream">The memory stream of the file's contents. Null if the file could not be loaded.</param>
/// <returns>True if the file could be loaded, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
bool TryContentFileRead(string path, out MemoryStream fileStream);
/// <summary>
/// Recursively finds all files in a directory and all sub directories.
/// </summary>
/// <remarks>
/// If the directory does not exist, an empty enumerable is returned.
/// </remarks>
/// <param name="path">Directory to search inside of.</param>
/// <returns>Enumeration of all relative file paths of the files found.</returns>
IEnumerable<string> ContentFindFiles(string path);
/// <returns>Enumeration of all relative file paths of the files found, that is they are relative to <paramref name="path"/>.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
IEnumerable<ResourcePath> ContentFindFiles(ResourcePath path);
/// <summary>
/// Absolute path to the configuration directory for the game. If you are writing any files,
/// Recursively finds all files in a directory and all sub directories.
/// </summary>
/// <remarks>
/// If the directory does not exist, an empty enumerable is returned.
/// </remarks>
/// <param name="path">Directory to search inside of.</param>
/// <returns>Enumeration of all relative file paths of the files found, that is they are relative to <paramref name="path"/>.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is not rooted.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
IEnumerable<ResourcePath> ContentFindFiles(string path);
/// <summary>
/// Absolute disk path to the configuration directory for the game. If you are writing any files,
/// they need to be inside of this directory.
/// </summary>
string ConfigDirectory { get; }

View File

@@ -1,8 +1,10 @@
using Newtonsoft.Json;
using System;
using System.Runtime.InteropServices;
namespace SS14.Shared.Maths
{
[JsonObject(memberSerialization: MemberSerialization.Fields)]
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct Vector2u : IEquatable<Vector2u>

View File

@@ -56,7 +56,7 @@ namespace SS14.Shared.Prototypes
/// <summary>
/// Load prototypes from files in a directory, recursively.
/// </summary>
void LoadDirectory(string path);
void LoadDirectory(ResourcePath path);
void LoadFromStream(TextReader stream);
/// <summary>
/// Clear out all prototypes and reset to a blank slate.
@@ -181,7 +181,7 @@ namespace SS14.Shared.Prototypes
}
/// <inheritdoc />
public void LoadDirectory(string path)
public void LoadDirectory(ResourcePath path)
{
foreach (var filePath in _resources.ContentFindFiles(path))
{

View File

@@ -84,6 +84,9 @@
<Reference Include="Nett, Version=0.7.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Nett.0.7.0\lib\Net40\Nett.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System">
<Name>System</Name>
</Reference>
@@ -97,6 +100,8 @@
<Name>System.Data.DataSetExtensions</Name>
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Net" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Windows.Forms">
<Name>System.Windows.Forms</Name>
</Reference>
@@ -262,6 +267,7 @@
<Compile Include="Timing\GameLoop.cs" />
<Compile Include="Timing\IStopwatch.cs" />
<Compile Include="Timing\Stopwatch.cs" />
<Compile Include="Utility\ResourcePath.cs" />
<Compile Include="Utility\QuadTree.cs" />
<Compile Include="Reflection\ReflectAttribute.cs" />
<Compile Include="Serialization\NetSerializableAttribute.cs" />
@@ -394,4 +400,4 @@
<Visible>False</Visible>
</Resources>
</ItemGroup>
</Project>
</Project>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
namespace SS14.Shared.Utility
@@ -13,12 +13,11 @@ namespace SS14.Shared.Utility
// they are the same. Mono/linux has both as '/', for example.
// Hardcode the only platforms we care about.
var separators = new char [] { '/', '\\' };
var separators = new char[] { '/', '\\' };
string newpath = "";
foreach (string tmp in pathname.Split(separators))
newpath = Path.Combine (newpath, tmp);
newpath = System.IO.Path.Combine(newpath, tmp);
return newpath;
}
}
}

View File

@@ -0,0 +1,543 @@
// Because System.IO.Path sucks.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SS14.Shared.Utility
{
/// <summary>
/// Provides object-oriented path manipulation for resource paths.
/// ResourcePaths are immutable.
/// </summary>
public class ResourcePath
{
/// <summary>
/// The separator for the file system of the system we are compiling to.
/// Backslash on Windows, forward slash on sane systems.
/// </summary>
#if WINDOWS
public const string SYSTEM_SEPARATOR = "\\";
#else
public const string SYSTEM_SEPARATOR = "/";
#endif
/// <summary>
/// "." as a static. Separator used is <c>/</c>.
/// </summary>
public static readonly ResourcePath Self = new ResourcePath(".");
/// <summary>
/// "/" (root) as a static. Separator used is <c>/</c>.
/// </summary>
public static readonly ResourcePath Root = new ResourcePath("/");
/// <summary>
/// List of the segments of the path.
/// This is pretty much a split of the input string path by separator,
/// except for the root, which is represented as the separator in position #0.
/// </summary>
private readonly string[] Segments;
/// <summary>
/// The separator between "segments"/"directories" for this path.
/// </summary>
public string Separator { get; }
/// <summary>
/// Create a new path from a string, splitting it by the separator provided.
/// </summary>
/// <param name="path">The string path to turn into a resource path.</param>
/// <param name="separator">The separator for the resource path.</param>
/// <exception cref="ArgumentException">Thrown if you try to use "." as separator.</exception>
/// <exception cref="ArgumentNullException">Thrown if either argument is null.</exception>
public ResourcePath(string path, string separator = "/")
{
if (separator == ".")
{
throw new ArgumentException("Yeah no.", nameof(separator));
}
Separator = separator;
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (separator == null)
{
throw new ArgumentNullException(nameof(separator));
}
if (path == "")
{
Segments = new string[] { "." };
return;
}
var segments = new List<string>();
var splitsegments = path.Split(new string[] { separator }, StringSplitOptions.None);
var i = 0;
if (splitsegments[0] == "")
{
i = 1;
segments.Add(separator);
}
for (; i < splitsegments.Length; i++)
{
var segment = splitsegments[i];
if (segment == "" || (segment == "." && segments.Count != 0))
{
continue;
}
if (i == 1 && segments[0] == ".")
{
segments[0] = segment;
}
else
{
segments.Add(segment);
}
}
Segments = segments.ToArray();
}
private ResourcePath(string[] segments, string separator)
{
Segments = segments;
Separator = separator;
}
/// <inheritdoc />
public override string ToString()
{
var builder = new StringBuilder();
var i = 0;
if (IsRooted)
{
i = 1;
builder.Append(Separator);
}
for (; i < Segments.Length; i++)
{
builder.Append(Segments[i]);
if (i + 1 < Segments.Length)
{
builder.Append(Separator);
}
}
return builder.ToString();
}
/// <summary>
/// Returns true if the path is rooted (starts with the separator).
/// </summary>
/// <seealso cref="IsRelative" />
/// <seealso cref="ToRootedPath"/>
public bool IsRooted => Segments[0] == Separator;
/// <summary>
/// Returns true if the path is not rooted.
/// </summary>
/// <seealso cref="IsRooted" />
/// <seealso cref="ToRelativePath"/>
public bool IsRelative => !IsRooted;
/// <summary>
/// Returns true if the path is equal to "."
/// </summary>
public bool IsSelf => Segments.Length == 1 && Segments[0] == ".";
/// <summary>
/// Returns the file extension of file path, if any.
/// Returns "" if there is no file extension.
/// The extension returned does NOT include a period.
/// </summary>
public string Extension
{
get
{
var filename = Filename;
if (string.IsNullOrWhiteSpace(filename))
{
return "";
}
var index = filename.LastIndexOf('.');
if (index == 0 || index == -1 || index == filename.Length - 1)
{
// The path is a dotfile (like .bashrc),
// or there's no period at all,
// or the period is at the very end.
// Non of these cases are truly an extension.
return "";
}
return filename.Substring(index + 1);
}
}
/// <summary>
/// Returns the file name.
/// </summary>
public string Filename
{
get
{
if (Segments.Length == 1 && IsRooted)
{
return "";
}
return Segments[Segments.Length - 1];
}
}
/// <summary>
/// Returns the file name, without extension.
/// </summary>
public string FilenameWithoutExtension
{
get
{
var filename = Filename;
if (string.IsNullOrWhiteSpace(filename))
{
return filename;
}
var index = filename.LastIndexOf('.');
if (index == 0 || index == -1 || index == filename.Length - 1)
{
return filename;
}
return filename.Substring(0, index);
}
}
/// <summary>
/// Returns a new instance with a different separator set.
/// </summary>
/// <param name="newSeparator">The new separator to use.</param>
/// <exception cref="ArgumentException">Thrown if the new separator is "."</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="newSeparator"/> is null.</exception>
public ResourcePath ChangeSeparator(string newSeparator)
{
if (newSeparator == ".")
{
throw new ArgumentException("Yeah no.", nameof(newSeparator));
}
if (newSeparator == null)
{
throw new ArgumentNullException(nameof(newSeparator));
}
// Convert the segments into a string path, then re-parse it.
// Solves the edge case of the segments containing the new separator.
var path = new ResourcePath(Segments, newSeparator).ToString();
return new ResourcePath(path, newSeparator);
}
/// <summary>
/// Joins two resource paths together, with separator in between.
/// If the second path is absolute, the first path is completely ignored.
/// </summary>
/// <exception cref="ArgumentException">Thrown if the separators of the two paths do not match.</exception>
// "Why use / instead of +" you may think:
// * It's clever, although I got the idea from Python's pathlib.
// * It avoids confusing operator precedence causing you to join two strings,
// because path + string + string != path + (string + string),
// whereas path / (string / string) doesn't compile.
public static ResourcePath operator /(ResourcePath a, ResourcePath b)
{
if (a.Separator != b.Separator)
{
throw new ArgumentException("Both separators must be the same.");
}
if (b.IsRooted)
{
return b;
}
if (b.IsSelf)
{
return a;
}
string[] segments = new string[a.Segments.Length + b.Segments.Length];
a.Segments.CopyTo(segments, 0);
b.Segments.CopyTo(segments, a.Segments.Length);
return new ResourcePath(segments, a.Separator);
}
/// <summary>
/// Adds a new segment to the path as string.
/// </summary>
public static ResourcePath operator /(ResourcePath path, string b)
{
return path / new ResourcePath(b, path.Separator);
}
/// <summary>
/// "Cleans" the resource path, removing <c>..</c>.
/// </summary>
/// <remarks>
/// If .. appears at the base of a path, it is left alone. If it appears at root level (like /..) it is removed entirely.
/// </remarks>
public ResourcePath Clean()
{
var segments = new List<string>();
foreach (var segment in Segments)
{
// If you have ".." cleaning that up doesn't remove that.
if (segment == ".." && segments.Count != 0)
{
// Trying to do /.. results in /
if (segments.Count == 1 && segments[0] == Separator)
{
continue;
}
var pos = segments.Count - 1;
if (segments[pos] != "..")
{
segments.RemoveAt(pos);
continue;
}
}
segments.Add(segment);
}
if (segments.Count == 0)
{
return new ResourcePath(".", Separator);
}
return new ResourcePath(segments.ToArray(), Separator);
}
/// <summary>
/// Check whether a path is clean, i.e. <see cref="Clean"/> would not modify it.
/// </summary>
/// <returns></returns>
public bool IsClean()
{
for (var i = 0; i < Segments.Length; i++)
{
if (Segments[i] == "..")
{
if (IsRooted)
{
return false;
}
if (i > 0 && Segments[i - 1] != "..")
{
return false;
}
}
}
return true;
}
/// <summary>
/// Turns the path into a rooted path by prepending it with the separator.
/// Does nothing if the path is already rooted.
/// </summary>
/// <seealso cref="IsRooted" />
/// <seealso cref="ToRelativePath" />
public ResourcePath ToRootedPath()
{
if (IsRooted)
{
return this;
}
var segments = new string[Segments.Length + 1];
Segments.CopyTo(segments, 1);
segments[0] = Separator;
return new ResourcePath(segments, Separator);
}
/// <summary>
/// Turns the path into a relative path by removing the root separator, if any.
/// Does nothing if the path is already relative.
/// </summary>
/// <seealso cref="IsRelative"/>
/// <seealso cref="ToRootedPath" />
public ResourcePath ToRelativePath()
{
if (IsRelative)
{
return this;
}
var segments = new string[Segments.Length - 1];
Array.Copy(Segments, 1, segments, 0, Segments.Length - 1);
return new ResourcePath(segments, Separator);
}
/// <summary>
/// Turns the path into a relative path with system-specific separator.
/// For usage in disk I/O.
/// </summary>
public string ToRelativeSystemPath()
{
return ChangeSeparator(SYSTEM_SEPARATOR).ToRelativePath().ToString();
}
/// <summary>
/// Converts a relative disk path back into a resource path.
/// </summary>
/// <exception cref="ArgumentNullException">Thrown if either argument is null.</exception>
public static ResourcePath FromRelativeSystemPath(string path, string newseparator = "/")
{
return new ResourcePath(path, SYSTEM_SEPARATOR).ChangeSeparator(newseparator);
}
/// <summary>
/// Returns the path of how this instance is "relative" to <paramref name="basePath"/>,
/// such that <c>basePath/result == this</c>.
/// </summary>
/// <example>
/// <code>
/// var path1 = new ResourcePath("/a/b/c");
/// var path2 = new ResourcePath("/a");
/// Console.WriteLine(path1.RelativeTo(path2)); // prints "b/c".
/// </code>
/// </example>
/// <exception cref="ArgumentException">Thrown if we are not relative to the base path or the separators are not the same.</exception>
public ResourcePath RelativeTo(ResourcePath basePath)
{
if (TryRelativeTo(basePath, out var relative))
{
return relative;
}
throw new ArgumentException($"{this} does not start with {basePath}.");
}
/// <summary>
/// Try pattern version of <see cref="RelativeTo(ResourcePath)"/>
/// </summary>
/// <param name="relative">The path of how we are relative to <paramref name="basePath"/>, if at all.</param>
/// <returns>True if we are relative to <paramref name="basePath"/>, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if the separators are not the same.</exception>
public bool TryRelativeTo(ResourcePath basePath, out ResourcePath relative)
{
if (basePath.Separator != Separator)
{
throw new ArgumentException("Separators must be the same.", nameof(basePath));
}
if (Segments.Length < basePath.Segments.Length)
{
relative = null;
return false;
}
if (Segments.Length == basePath.Segments.Length)
{
if (this == basePath)
{
relative = new ResourcePath(".", Separator);
return true;
}
else
{
relative = null;
return false;
}
}
var i = 0;
for (; i < basePath.Segments.Length; i++)
{
if (Segments[i] != basePath.Segments[i])
{
relative = null;
return false;
}
}
var segments = new string[Segments.Length - basePath.Segments.Length];
Array.Copy(Segments, basePath.Segments.Length, segments, 0, segments.Length);
relative = new ResourcePath(segments, Separator);
return true;
}
/// <summary>
/// Gets the common base of two paths.
/// </summary>
/// <example>
/// <code>
/// var path1 = new ResourcePath("/a/b/c");
/// var path2 = new ResourcePath("/a/e/d");
/// Console.WriteLine(path1.RelativeTo(path2)); // prints "/a".
/// </code>
/// </example>
/// <param name="other">The other path.</param>
/// <exception cref="ArgumentException">Thrown if there is no common base between the two paths.</exception>
public ResourcePath CommonBase(ResourcePath other)
{
if (other.Separator != Separator)
{
throw new ArgumentException("Separators must match.");
}
var i = 0;
for (; i < Segments.Length && i < other.Segments.Length; i++)
{
if (Segments[i] != other.Segments[i])
{
break;
}
}
if (i == 0)
{
throw new ArgumentException($"{this} and {other} have no common base.");
}
var segments = new string[i];
Array.Copy(Segments, segments, i);
return new ResourcePath(segments, Separator);
}
/// <inheritdoc />
public override int GetHashCode()
{
var code = Separator.GetHashCode();
foreach (var segment in Segments)
{
code |= segment.GetHashCode();
}
return code;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is ResourcePath path && Equals(path);
}
/// <summary>
/// Checks that we are equal with <paramref name="path"/>.
/// This method does NOT clean the paths beforehand, so paths that point to the same location may fail if they are not cleaned beforehand.
/// Paths are never equal if they do not have the same separator.
/// </summary>
/// <param name="path">The path to check equality with.</param>
/// <returns>True if the paths are equal, false otherwise.</returns>
public bool Equals(ResourcePath path)
{
if (path == null)
{
return false;
}
return path.Separator == Separator && Segments.SequenceEqual(path.Segments);
}
public static bool operator ==(ResourcePath a, ResourcePath b)
{
if ((object)a == null)
{
return (object)b == null;
}
return a.Equals(b);
}
public static bool operator !=(ResourcePath a, ResourcePath b)
{
return !(a == b);
}
}
}

View File

@@ -6,6 +6,10 @@
<assemblyIdentity name="ICSharpCode.SharpZipLib" publicKeyToken="1b03e6acf1164f73" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.86.0.518" newVersion="0.86.0.518" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -3,7 +3,8 @@
<package id="Mono.Cecil" version="0.9.6.4" targetFramework="net451" />
<package id="NetSerializer" version="4.1.0" targetFramework="net451" />
<package id="Nett" version="0.7.0" targetFramework="net451" />
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net451" />
<package id="SharpZipLib" version="0.86.0" targetFramework="net451" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net451" />
<package id="YamlDotNet" version="4.3.0" targetFramework="net451" />
</packages>
</packages>

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.props" Condition="Exists('$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.props')" />
<Import Project="$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.props" Condition="Exists('$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.props')" />
<Import Project="$(SolutionDir)packages\NUnit.3.10.1\build\NUnit.props" Condition="Exists('$(SolutionDir)packages\NUnit.3.10.1\build\NUnit.props')" />
<Import Project="$(SolutionDir)packages\NUnit3TestAdapter.3.10.0\build\net35\NUnit3TestAdapter.props" Condition="Exists('$(SolutionDir)packages\NUnit3TestAdapter.3.10.0\build\net35\NUnit3TestAdapter.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
@@ -22,6 +26,8 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Optimize>false</Optimize>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<DebugSymbols>true</DebugSymbols>
@@ -45,17 +51,22 @@
<Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.CodeCoverage.Shim, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Microsoft.CodeCoverage.1.0.3\lib\netstandard1.0\Microsoft.VisualStudio.CodeCoverage.Shim.dll</HintPath>
</Reference>
<Reference Include="Moq, Version=4.8.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Moq.4.8.2\lib\net45\Moq.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<SpecificVersion>False</SpecificVersion>
<HintPath>$(SolutionDir)packages\NUnit.3.7.1\lib\net45\nunit.framework.dll</HintPath>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.10.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\NUnit.3.10.1\lib\net45\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Threading.Tasks.Extensions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll</HintPath>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\System.Threading.Tasks.Extensions.4.4.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.ValueTuple">
<HintPath>$(SolutionDir)packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
@@ -89,6 +100,7 @@
<Compile Include="Shared\Maths\Angle_Test.cs" />
<Compile Include="Shared\Maths\Matrix3_Test.cs" />
<Compile Include="Shared\Maths\Ray_Test.cs" />
<Compile Include="Shared\Maths\Vector2u_Test.cs" />
<Compile Include="Shared\Maths\Vector2_Test.cs" />
<Compile Include="Shared\Maths\Direction_Test.cs" />
<Compile Include="Shared\Physics\CollisionManager_Test.cs" />
@@ -98,6 +110,7 @@
<Compile Include="Shared\Timing\GameLoop_Test.cs" />
<Compile Include="Shared\Timing\GameTiming_Test.cs" />
<Compile Include="Shared\Utility\CollectionExtensions_Test.cs" />
<Compile Include="Shared\Utility\ResourcePath_Test.cs" />
<Compile Include="Shared\Utility\YamlHelpers_Test.cs" />
<Compile Include="Shared\ColorUtils_Test.cs" />
<Compile Include="SS14UnitTest.cs" />
@@ -147,4 +160,15 @@
<DefineConstants Condition="'$(HEADLESS)'!=''">$(DefineConstants);HEADLESS</DefineConstants>
</PropertyGroup>
<Target Name="AfterBuild" DependsOnTargets="CopyResourcesFromShared" />
</Project>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)packages\NUnit3TestAdapter.3.10.0\build\net35\NUnit3TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)packages\NUnit3TestAdapter.3.10.0\build\net35\NUnit3TestAdapter.props'))" />
<Error Condition="!Exists('$(SolutionDir)packages\NUnit.3.10.1\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)packages\NUnit.3.10.1\build\NUnit.props'))" />
<Error Condition="!Exists('$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.props'))" />
<Error Condition="!Exists('$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.targets'))" />
</Target>
<Import Project="$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.targets" Condition="Exists('$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.targets')" />
<Import Project="$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.targets" Condition="Exists('$(SolutionDir)packages\Microsoft.NET.Test.Sdk.15.7.0\build\net45\Microsoft.Net.Test.Sdk.targets')" />
</Project>

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
using NUnit.Framework;
using SS14.Shared.Maths;
namespace SS14.UnitTesting.Shared.Maths
{
[TestFixture]
[Parallelizable]
[TestOf(typeof(Vector2u))]
public class Vector2u_Test
{
[Test]
public void TestJsonDeserialization()
{
Assert.That(JsonConvert.DeserializeObject<Vector2u>("{\"x\": 10, \"y\": 10}"), Is.EqualTo(new Vector2u(10, 10)));
}
}
}

View File

@@ -0,0 +1,211 @@
using NUnit.Framework;
using SS14.Shared.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SS14.UnitTesting.Shared.Utility
{
[TestFixture]
[Parallelizable(ParallelScope.Fixtures | ParallelScope.All)]
[TestOf(typeof(ResourcePath))]
public class ResourcePath_Test
{
public static List<(string, string)> InputClean_Values = new List<(string, string)>
{
("/Textures", "/Textures"),
("Textures", "Textures"),
("Textures/", "Textures"),
("Textures/Laser.png", "Textures/Laser.png"),
("Textures//Laser.png", "Textures/Laser.png"),
("Textures/..//Radio.png/", "Textures/../Radio.png"),
("", "."),
(".", "."),
("./foo", "foo"),
("foo/.", "foo"),
("foo/./bar", "foo/bar"),
("./", "."),
("/.", "/"),
("/", "/"),
(" ", " "), // Note the spaces here.
(" / ", " / "),
(". ", ". ")
};
// Tests whether input and output remains unchanged.
[Test]
public void InputClean_Test([ValueSource(nameof(InputClean_Values))] (string input, string expected) path)
{
var respath = new ResourcePath(path.input);
Assert.That(respath.ToString(), Is.EqualTo(path.expected));
}
public static List<(string, string)> Extension_Values = new List<(string, string)>
{
("foo", ""),
("foo.png", "png"),
("test/foo.png", "png"),
(".bashrc", ""),
("..png", "png"),
("x.y.z", "z")
};
[Test]
public void Extension_Test([ValueSource(nameof(Extension_Values))] (string path, string expected) data)
{
var respath = new ResourcePath(data.path);
Assert.That(respath.Extension, Is.EqualTo(data.expected));
}
public static List<(string, string)> Filename_Values = new List<(string, string)>
{
("foo", "foo"),
("foo.png", "foo.png"),
("x/y/z", "z"),
("/bar", "bar"),
("foo/", "foo") // Trailing / gets trimmed.
};
[Test]
public void Filename_Test([ValueSource(nameof(Filename_Values))] (string path, string expected) data)
{
var respath = new ResourcePath(data.path);
Assert.That(respath.Filename, Is.EqualTo(data.expected));
}
public static List<(string, string)> FilenameWithoutExtension_Values = new List<(string, string)>
{
("foo", "foo"),
("foo.png", "foo"),
("test/foo.png", "foo"),
("derp/.bashrc", ".bashrc"),
("..png", "."),
("x.y.z", "x.y")
};
[Test]
public void FilenameWithoutExtension_Test([ValueSource(nameof(FilenameWithoutExtension_Values))] (string path, string expected) data)
{
var respath = new ResourcePath(data.path);
Assert.That(respath.FilenameWithoutExtension, Is.EqualTo(data.expected));
}
[Test]
public void ChangeSeparator_Test()
{
var respath = new ResourcePath("a/b/c").ChangeSeparator("👏");
Assert.That(respath.ToString(), Is.EqualTo("a👏b👏c"));
}
[Test]
public void Combine_Test()
{
var path1 = new ResourcePath("/a/b");
var path2 = new ResourcePath("c/d.png");
Assert.That((path1 / path2).ToString(), Is.EqualTo("/a/b/c/d.png"));
Assert.That((path1 / "z").ToString(), Is.EqualTo("/a/b/z"));
}
public static List<(string, string)> Clean_Values = new List<(string, string)>
{
("//a/b/../c/./ss14.png", "/a/c/ss14.png"),
("../a", "../a"),
("../a/..", ".."),
("../..", "../.."),
("a/..", "."),
("/../a", "/a"),
("/..", "/"),
};
[Test]
public void Clean_Test([ValueSource(nameof(Clean_Values))] (string path, string expected) data)
{
var path = new ResourcePath(data.path);
var cleaned = path.Clean();
Assert.Multiple(() =>
{
if (path == cleaned)
{
Assert.That(path.IsClean());
}
Assert.That(path.Clean(), Is.EqualTo(new ResourcePath(data.expected)));
Assert.That(cleaned.IsClean());
});
}
[Test]
public void RootedConversions_Test()
{
var path = new ResourcePath("/a/b");
Assert.That(path.IsRooted);
Assert.That(path.ToRootedPath(), Is.EqualTo(path));
var relative = path.ToRelativePath();
Assert.That(relative, Is.EqualTo(new ResourcePath("a/b")));
Assert.That(relative.IsRelative);
Assert.That(relative.ToRelativePath(), Is.EqualTo(relative));
Assert.That(relative.ToRootedPath(), Is.EqualTo(path));
}
public static List<(string, string, string)> RelativeTo_Values = new List<(string, string, string)>
{
("/a/b", "/a", "b"),
("/a", "/", "a"),
("/a/b/c", "/", "a/b/c"),
("/a", "/a", "."),
("a/b", "a", "b"),
("/Textures/Weapons/laser.png", "/Textures/", "Weapons/laser.png")
};
[Test]
public void RelativeTo_Test([ValueSource(nameof(RelativeTo_Values))] (string source, string basePath, string expected) value)
{
var path = new ResourcePath(value.source);
var basePath = new ResourcePath(value.basePath);
Assert.That(path.RelativeTo(basePath), Is.EqualTo(new ResourcePath(value.expected)));
}
public static List<(string, string)> RelativeToFail_Values = new List<(string, string)>
{
("/a/b", "/b"),
("/a", "/c/d"),
("/a/b", "/a/d"),
(".", "/"),
("/", ".")
};
[Test]
public void RelativeToFail_Test([ValueSource(nameof(RelativeToFail_Values))] (string source, string basePath) value)
{
var path = new ResourcePath(value.source);
var basePath = new ResourcePath(value.basePath);
Assert.That(() => path.RelativeTo(basePath), Throws.ArgumentException);
}
public static List<(string, string, string)> CommonBase_Values = new List<(string, string, string)>
{
("/a/b", "/a/c", "/a"),
("a/b", "a/c", "a"),
("/usr", "/bin", "/")
};
[Test]
public void CommonBase_Test([ValueSource(nameof(CommonBase_Values))] (string a, string b, string expected) value)
{
var path = new ResourcePath(value.a);
var basePath = new ResourcePath(value.b);
Assert.That(path.CommonBase(basePath), Is.EqualTo(new ResourcePath(value.expected)));
}
[Test]
public void CommonBaseFail_Test()
{
var path = new ResourcePath("a/b");
var basePath = new ResourcePath("b/a");
Assert.That(() => path.CommonBase(basePath), Throws.ArgumentException);
}
}
}

View File

@@ -6,6 +6,14 @@
<assemblyIdentity name="ICSharpCode.SharpZipLib" publicKeyToken="1b03e6acf1164f73" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.86.0.518" newVersion="0.86.0.518" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Castle.Core" version="4.2.1" targetFramework="net451" />
<package id="Microsoft.CodeCoverage" version="1.0.3" targetFramework="net451" />
<package id="Microsoft.NET.Test.Sdk" version="15.7.0" targetFramework="net451" />
<package id="Moq" version="4.8.2" targetFramework="net451" />
<package id="NUnit" version="3.7.1" targetFramework="net451" />
<package id="NUnit.ConsoleRunner" version="3.7.0" targetFramework="net451" />
<package id="NUnit3TestAdapter" version="3.8.0" targetFramework="net451" />
<package id="System.Threading.Tasks.Extensions" version="4.3.0" targetFramework="net451" />
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net451" />
<package id="NUnit" version="3.10.1" targetFramework="net451" />
<package id="NUnit.ConsoleRunner" version="3.8.0" targetFramework="net451" />
<package id="NUnit3TestAdapter" version="3.10.0" targetFramework="net451" />
<package id="System.Threading.Tasks.Extensions" version="4.4.0" targetFramework="net451" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net451" />
<package id="YamlDotNet" version="4.3.0" targetFramework="net451" />
</packages>