diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 6fa5601a80..1c0bd3050d 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -165,7 +165,7 @@ namespace OpenRA using (new PerfTimer("PrepareMap")) map = ModData.PrepareMap(mapUID); using (new PerfTimer("NewWorld")) - OrderManager.World = new World(map, OrderManager, type); + OrderManager.World = new World(ModData, map, OrderManager, type); worldRenderer = new WorldRenderer(ModData, OrderManager.World); diff --git a/OpenRA.Game/GameRules/Ruleset.cs b/OpenRA.Game/GameRules/Ruleset.cs index d256144bb4..54dbdd3bed 100644 --- a/OpenRA.Game/GameRules/Ruleset.cs +++ b/OpenRA.Game/GameRules/Ruleset.cs @@ -29,6 +29,7 @@ namespace OpenRA public readonly IReadOnlyDictionary Music; public readonly TileSet TileSet; public readonly SequenceProvider Sequences; + public readonly IReadOnlyDictionary ModelSequences; public Ruleset( IReadOnlyDictionary actors, @@ -37,7 +38,8 @@ namespace OpenRA IReadOnlyDictionary notifications, IReadOnlyDictionary music, TileSet tileSet, - SequenceProvider sequences) + SequenceProvider sequences, + IReadOnlyDictionary modelSequences) { Actors = actors; Weapons = weapons; @@ -46,6 +48,7 @@ namespace OpenRA Music = music; TileSet = tileSet; Sequences = sequences; + ModelSequences = modelSequences; foreach (var a in Actors.Values) { @@ -119,8 +122,11 @@ namespace OpenRA var music = MergeOrDefault("Manifest,Music", fs, m.Music, null, null, k => new MusicInfo(k.Key, k.Value)); + var modelSequences = MergeOrDefault("Manifest,ModelSequences", fs, m.VoxelSequences, null, null, + k => k); + // The default ruleset does not include a preferred tileset or sequence set - ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, null); + ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, null, modelSequences); }; if (modData.IsOnMainThread) @@ -145,12 +151,13 @@ namespace OpenRA var dr = modData.DefaultRules; var ts = modData.DefaultTileSets[tileSet]; var sequences = modData.DefaultSequences[tileSet]; - return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, ts, sequences); + + return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, ts, sequences, dr.ModelSequences); } public static Ruleset Load(ModData modData, IReadOnlyFileSystem fileSystem, string tileSet, MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications, - MiniYaml mapMusic, MiniYaml mapSequences) + MiniYaml mapMusic, MiniYaml mapSequences, MiniYaml mapModelSequences) { var m = modData.Manifest; var dr = modData.DefaultRules; @@ -180,8 +187,12 @@ namespace OpenRA var sequences = mapSequences == null ? modData.DefaultSequences[tileSet] : new SequenceProvider(fileSystem, modData, ts, mapSequences); - // TODO: Add support for custom voxel sequences - ruleset = new Ruleset(actors, weapons, voices, notifications, music, ts, sequences); + var modelSequences = dr.ModelSequences; + if (mapModelSequences != null) + modelSequences = MergeOrDefault("ModelSequences", fileSystem, m.VoxelSequences, mapModelSequences, dr.ModelSequences, + k => k); + + ruleset = new Ruleset(actors, weapons, voices, notifications, music, ts, sequences, modelSequences); }; if (modData.IsOnMainThread) diff --git a/OpenRA.Game/Graphics/Model.cs b/OpenRA.Game/Graphics/Model.cs new file mode 100644 index 0000000000..55f24690d7 --- /dev/null +++ b/OpenRA.Game/Graphics/Model.cs @@ -0,0 +1,69 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 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 OpenRA.FileSystem; + +namespace OpenRA.Graphics +{ + public interface IModel + { + uint Frames { get; } + uint Sections { get; } + + float[] TransformationMatrix(uint section, uint frame); + float[] Size { get; } + float[] Bounds(uint frame); + VoxelRenderData RenderData(uint section); + } + + public interface IModelCache : IDisposable + { + IModel GetModelSequence(string model, string sequence); + bool HasModelSequence(string model, string sequence); + IVertexBuffer VertexBuffer { get; } + } + + public interface IModelSequenceLoader + { + Action OnMissingModelError { get; set; } + IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary modelDefinitions); + } + + public class PlaceholderModelSequenceLoader : IModelSequenceLoader + { + public Action OnMissingModelError { get; set; } + + class PlaceholderModelCache : IModelCache + { + public IVertexBuffer VertexBuffer { get { throw new NotImplementedException(); } } + + public void Dispose() { } + + public IModel GetModelSequence(string model, string sequence) + { + throw new NotImplementedException(); + } + + public bool HasModelSequence(string model, string sequence) + { + throw new NotImplementedException(); + } + } + + public PlaceholderModelSequenceLoader(ModData modData) { } + + public IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary modelDefinitions) + { + return new PlaceholderModelCache(); + } + } +} diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index ba617f5320..35c993622b 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -31,6 +31,17 @@ namespace OpenRA } } + public sealed class ModelSequenceFormat : IGlobalModData + { + public readonly string Type; + public readonly IReadOnlyDictionary Metadata; + public ModelSequenceFormat(MiniYaml yaml) + { + Type = yaml.Value; + Metadata = new ReadOnlyDictionary(yaml.ToDictionary()); + } + } + public class ModMetadata { public string Title; diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index adf35dba2f..ccacae3002 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Reflection; using System.Text; using OpenRA.FileSystem; +using OpenRA.Graphics; using OpenRA.Support; using OpenRA.Traits; @@ -386,7 +387,7 @@ namespace OpenRA try { Rules = Ruleset.Load(modData, this, Tileset, RuleDefinitions, WeaponDefinitions, - VoiceDefinitions, NotificationDefinitions, MusicDefinitions, SequenceDefinitions); + VoiceDefinitions, NotificationDefinitions, MusicDefinitions, SequenceDefinitions, VoxelSequenceDefinitions); } catch (Exception e) { diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index 00a8c45be0..79b8a9b471 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -307,8 +307,9 @@ namespace OpenRA var musicDefinitions = LoadRuleSection(yaml, "Music"); var notificationDefinitions = LoadRuleSection(yaml, "Notifications"); var sequenceDefinitions = LoadRuleSection(yaml, "Sequences"); + var modelSequenceDefinitions = LoadRuleSection(yaml, "VoxelSequences"); var rules = Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions, - voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions); + voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions, modelSequenceDefinitions); var flagged = Ruleset.DefinesUnsafeCustomRules(modData, this, ruleDefinitions, weaponDefinitions, voiceDefinitions, notificationDefinitions, sequenceDefinitions); return Pair.New(rules, flagged); @@ -390,8 +391,9 @@ namespace OpenRA var musicDefinitions = LoadRuleSection(rulesYaml, "Music"); var notificationDefinitions = LoadRuleSection(rulesYaml, "Notifications"); var sequenceDefinitions = LoadRuleSection(rulesYaml, "Sequences"); + var modelSequenceDefinitions = LoadRuleSection(rulesYaml, "VoxelSequences"); var rules = Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions, - voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions); + voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions, modelSequenceDefinitions); var flagged = Ruleset.DefinesUnsafeCustomRules(modData, this, ruleDefinitions, weaponDefinitions, voiceDefinitions, notificationDefinitions, sequenceDefinitions); return Pair.New(rules, flagged); diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index a087892b3c..79a1c46d4c 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -30,6 +30,7 @@ namespace OpenRA public readonly ISoundLoader[] SoundLoaders; public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteSequenceLoader SpriteSequenceLoader; + public readonly IModelSequenceLoader ModelSequenceLoader; public ILoadScreen LoadScreen { get; private set; } public VoxelLoader VoxelLoader { get; private set; } public CursorProvider CursorProvider { get; private set; } @@ -73,13 +74,22 @@ namespace OpenRA var sequenceFormat = Manifest.Get(); var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader"); - var ctor = sequenceLoader != null ? sequenceLoader.GetConstructor(new[] { typeof(ModData) }) : null; - if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || ctor == null) + var sequenceCtor = sequenceLoader != null ? sequenceLoader.GetConstructor(new[] { typeof(ModData) }) : null; + if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || sequenceCtor == null) throw new InvalidOperationException("Unable to find a sequence loader for type '{0}'.".F(sequenceFormat.Type)); - SpriteSequenceLoader = (ISpriteSequenceLoader)ctor.Invoke(new[] { this }); + SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this }); SpriteSequenceLoader.OnMissingSpriteError = s => Log.Write("debug", s); + var modelFormat = Manifest.Get(); + var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader"); + var modelCtor = modelLoader != null ? modelLoader.GetConstructor(new[] { typeof(ModData) }) : null; + if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null) + throw new InvalidOperationException("Unable to find a model loader for type '{0}'.".F(modelFormat.Type)); + + ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this }); + ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s); + defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this)); defaultTileSets = Exts.Lazy(() => { diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index d852f45845..ccba686c73 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -256,6 +256,7 @@ + diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index a6311b4c10..37774d7db2 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -40,6 +40,7 @@ namespace OpenRA public Session LobbyInfo { get { return OrderManager.LobbyInfo; } } public readonly MersenneTwister SharedRandom; + public readonly IModelCache ModelCache; public Player[] Players = new Player[0]; @@ -147,7 +148,7 @@ namespace OpenRA } } - internal World(Map map, OrderManager orderManager, WorldType type) + internal World(ModData modData, Map map, OrderManager orderManager, WorldType type) { Type = type; OrderManager = orderManager; @@ -156,6 +157,8 @@ namespace OpenRA Timestep = orderManager.LobbyInfo.GlobalSettings.Timestep; SharedRandom = new MersenneTwister(orderManager.LobbyInfo.GlobalSettings.RandomSeed); + ModelCache = modData.ModelSequenceLoader.CacheModels(map, modData, map.Rules.ModelSequences); + var worldActorType = type == WorldType.Editor ? "EditorWorld" : "World"; WorldActor = CreateActor(worldActorType, new TypeDictionary()); ActorMap = WorldActor.Trait(); @@ -437,6 +440,8 @@ namespace OpenRA Game.Sound.StopAudio(); Game.Sound.StopVideo(); + ModelCache.Dispose(); + // Dispose newer actors first, and the world actor last foreach (var a in actors.Values.Reverse()) a.Dispose(); diff --git a/mods/all/mod.yaml b/mods/all/mod.yaml index b71f22ff5b..47b3e269a0 100644 --- a/mods/all/mod.yaml +++ b/mods/all/mod.yaml @@ -30,3 +30,5 @@ SoundFormats: SpriteFormats: SpriteSequenceFormat: DefaultSpriteSequence + +ModelSequenceFormat: PlaceholderModelSequence diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index f29ee495e5..371e951e02 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -197,6 +197,8 @@ SpriteSequenceFormat: TilesetSpecificSpriteSequence DESERT: .des JUNGLE: .jun +ModelSequenceFormat: PlaceholderModelSequence + GameSpeeds: slower: Name: Slower diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 5c60cdb37e..43768456ec 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -174,6 +174,8 @@ SpriteFormats: R8, ShpTD, TmpRA SpriteSequenceFormat: DefaultSpriteSequence +ModelSequenceFormat: PlaceholderModelSequence + GameSpeeds: slower: Name: Slower diff --git a/mods/modcontent/mod.yaml b/mods/modcontent/mod.yaml index da8167c691..488bdd1f18 100644 --- a/mods/modcontent/mod.yaml +++ b/mods/modcontent/mod.yaml @@ -55,3 +55,5 @@ SoundFormats: SpriteFormats: ShpTD SpriteSequenceFormat: DefaultSpriteSequence + +ModelSequenceFormat: PlaceholderModelSequence diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 7a0ec89396..e99add6f82 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -200,6 +200,8 @@ SpriteSequenceFormat: TilesetSpecificSpriteSequence INTERIOR: .int DESERT: .des +ModelSequenceFormat: PlaceholderModelSequence + GameSpeeds: slower: Name: Slower diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 4d1c3156a7..d33d29b011 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -236,6 +236,8 @@ SpriteSequenceFormat: TilesetSpecificSpriteSequence TEMPERATE: t SNOW: a +ModelSequenceFormat: PlaceholderModelSequence + GameSpeeds: slower: Name: Slower