New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactoring Map.cs to Enable Mod-defined Map Formats #20582
base: bleed
Are you sure you want to change the base?
Refactoring Map.cs to Enable Mod-defined Map Formats #20582
Conversation
d22ecab
to
18fdf40
Compare
OpenRA.Game/Map/MapPreview.cs
Outdated
@@ -273,16 +273,17 @@ public MapPreview(Map map, ModData modData) | |||
|
|||
innerData = new InnerData | |||
{ | |||
MapFormat = map.MapFormat, | |||
// TO DO: Check if this MapFormat retrieval is correct and working | |||
MapFormat = Map.GetMapFormat(Package), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetMapFormat is only used to get the format before yaml parsing. You are using it after
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, this now uses a cast of IMap
as with the other attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still unresolved. Keep the original implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new map crashes
This was due to a typo in the If statement for Grid.MaximumTerrainHeight > 0 in |
fcba3fa
to
ff70c18
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change makes sense.
Don't find it a logical choice to keep map implementing all the logic of all interfaces. Better would be to isolate the logic in other objects i.e. Map.GetTileManager().Tiles. But i understand that makes the rewrite even bigger.
Casts are now done without checks. Because the default mods are known to implement them. But to support mods, better would be to try the cast and skip the functionality if not implemented by the mod.
byte[] SavePreview(); | ||
} | ||
|
||
public interface IMapElevation : IMap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code above does not check if a map implements this interface. does a direct cast. likely because also maps without elevation implement it(?).
may as well add this to IMap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if you referenced the right code above as I don't see the cast. However the idea is that functions that are not implemented would not be called in the first place, because the mod would not call them. E.g.: Elevation isn't used in Red Alert, so it would never call functions that have casts to IMapElevation
.
@@ -273,16 +273,16 @@ public MapPreview(Map map, ModData modData) | |||
|
|||
innerData = new InnerData | |||
{ | |||
MapFormat = map.MapFormat, | |||
MapFormat = ((IMap)map).MapFormat, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnessary cast? i assume map implements IMap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Map does not implement IMap
, DefaultMap
does. Map
now only has the core functionality central to all maps, which is basically loading and checking the map, and getting its map format (see the new Map.cs for the remaining functionality of Map
, which is now only 164 lines of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebase fail. The first commit makes a change that is then unmade
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is correct because the IMap
interface is first introduced in the second commit. First commit only deals with abstracting the map loader.
OpenRA.Game/World.cs
Outdated
@@ -189,7 +189,7 @@ internal World(ModData modData, Map map, OrderManager orderManager, WorldType ty | |||
this.modData = modData; | |||
Type = type; | |||
OrderManager = orderManager; | |||
Map = map; | |||
Map = (IMap)map; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not pass an IMap in constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two reasons. First World.cs
is part of OpenRA.Game
, so it is not mod code and therefore may need to use Map.cs
without the interface. Secondly it does call some functions that rely on Map
not IMap
, such as CacheModels
, which is out of scope to refactor at this stage. However since World.cs
will likely always need to be in OpenRA.Game
, there will always be core map functions that it can and will need to access that are not part of IMap
, so a cast to IMap
while retaining the Map
parameter makes the most sense here.
@@ -56,7 +56,7 @@ public IEnumerable<IActorPreview> RenderPreviewSprites(ActorPreviewInitializer i | |||
|
|||
// Some mods may define terrain-specific bibs | |||
var sequence = Sequence; | |||
if (map.Tiles.Contains(cell)) | |||
if (((IMapTiles)map).Tiles.Contains(cell)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to me it seems more logical to let a map own a TileManager. i.e. map.GetTileManager(). promoting splitting logic.
i.e. otherwise you could have a map implement endless interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Map no longer has Tiles
, this functionality now belongs to IMapTiles
and the current implementation sits in DefaultMap
rather than Map
. And Tiles
is a CellLayer class which seems to work the same as your proposed TileManager
class. In any case changing how Tiles are managed is not the goal of this PR. All I am doing here is moving the functionality out of Map.cs
and into DefaultMap
, behind an interface IMapTiles
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i understand. but then still, it does not make sense to have IMapTiles inherit from IMap. Completely different things. Like letting ITrait inherit from IWidget. just because some class will implement both.
but i understand how you came to this solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but this is how it was done with the Tilesets refactoring as well (see below link), with ITemplatedTerrainInfo
and ITerrainInfoNotifyMapCreated
both inheriting from ITerrainInfo
. The downside is it adds a dependency to IMap
where it may not need to be there, but on the plus side it means less casting is necessary, since you can cast to IMapTiles
and still use the functionality from IMap
, rather than having to cast twice.
@@ -42,6 +42,8 @@ public TSEditorResourceLayer(Actor self, TSEditorResourceLayerInfo info) | |||
|
|||
bool IsValidVeinNeighbour(CPos cell, CPos neighbour) | |||
{ | |||
var mapRamp = ((IMapElevation)Map).Ramp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try the cast, to allows mods to keep using this logic, even if disabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean here. Mods can keep using this logic if they want and it is still part of the default functionality found in DefaultMap
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise here.
af75344
to
7325fb1
Compare
892f19a
to
1b179ee
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed the first commit. Running make
throws tons of errors, and the commit should be renamed to something like: "Abstracted map loading to DefaultMapLoader : IMapLoader"
@@ -273,16 +273,16 @@ public MapPreview(Map map, ModData modData) | |||
|
|||
innerData = new InnerData | |||
{ | |||
MapFormat = map.MapFormat, | |||
MapFormat = ((IMap)map).MapFormat, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebase fail. The first commit makes a change that is then unmade
// Empty rules that can be added to by the importers. | ||
// Will be dropped on save if nothing is added to it | ||
RuleDefinitions = new MiniYaml(""); | ||
|
||
Tiles = new CellLayer<TerrainTile>(Grid.Type, size); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebase fail. First commit comments this out
It's weird how you added a massive interface of IMapCell.cs, even adding a new file in third commit and then removed it in 6th commit. There are a bunch of errors in the first commits that then get fixed in following commits, even when they couldn't have not been introduced in the first place |
The purpose of keeping the commit is purely to show the genesis of the project, since it went through a revision of separating out the |
6403514
to
3548cdf
Compare
How this PR came to be or it's evolution is irrelevant and just noise making it harder to find regressions |
68f7759
to
2b39b3d
Compare
Sure, okay, I've squashed the commits and renamed the first commit. |
@@ -38,7 +38,7 @@ public static void HandleFatalError(Exception ex) | |||
if (Game.OrderManager != null && Game.OrderManager.World != null && Game.OrderManager.World.Map != null) | |||
{ | |||
var map = Game.OrderManager.World.Map; | |||
Log.Write("exception", $"on map {map.Uid} ({map.Title} by {map.Author})."); | |||
Log.Write("exception", $"on map {((Map)map).Uid} ({((Map)map).Title} by {((Map)map).Author})."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
casts could be moved higher up
var mapResources = worldRenderer.World.Map.Resources; | ||
var map = worldRenderer.World.Map; | ||
var gridType = map.Grid.Type; | ||
var mapResource = (IMapResource)worldRenderer.World.Map; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var mapResource = (IMapResource)worldRenderer.World.Map; | |
var mapResource = (IMapResource)map; |
@@ -67,12 +69,15 @@ public void WorldLoaded(World w, WorldRenderer wr) | |||
|
|||
void ConvertBridgeToActor(World w, CPos cell) | |||
{ | |||
var map = w.Map; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this variable isn't needed
97f83a1
to
642df58
Compare
9c1d13a
to
65621cd
Compare
70bc065
to
65103d9
Compare
Map Load(ModData modData, IReadOnlyPackage package); | ||
Map Create(ModData modData, ITerrainInfo terrainInfo, int width, int height); | ||
string ComputeUID(ModData modData, IReadOnlyPackage package); | ||
void UpdatePreview(ModData modData, MapPreview mapPreview, IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couldn't we just pass ModData in the IMapLoader constructor and avoid passing it in every function? MapPreview also contains a reference to MapData so it's unnecessary to pass
#pragma warning disable IDE1006 // Naming Styles | ||
protected readonly ModData modData; | ||
protected Translation Translation; | ||
#pragma warning restore IDE1006 // Naming Styles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just follow the rule. There are only a few uses of these variables anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From memory following this rule actually isn't possible because modData
must be distinct from ModData
, yet the naming rule does not allow lowercase 'm'. So the only way to follow the rule is to do something like ModDataInstance
, which would be nastier than disabling the rule for those two lines imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps rename file to MapInterfaces and move IMapLoader here
} | ||
|
||
static int GetMapFormat(IReadOnlyPackage p) | ||
public static int GetMapFormat(IReadOnlyPackage p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's weird to have this defined in Map.cs when it is only used in DefaultMap for uid extraction
OpenRA.Game/Map/MapPreview.cs
Outdated
@@ -273,16 +273,17 @@ public MapPreview(Map map, ModData modData) | |||
|
|||
innerData = new InnerData | |||
{ | |||
MapFormat = map.MapFormat, | |||
// TO DO: Check if this MapFormat retrieval is correct and working | |||
MapFormat = Map.GetMapFormat(Package), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still unresolved. Keep the original implementation
OpenRA.Game/Map/MapPreview.cs
Outdated
@@ -63,8 +63,8 @@ public class RemoteMapData | |||
|
|||
public class MapPreview : IDisposable, IReadOnlyFileSystem | |||
{ | |||
/// <summary>Wrapper that enables map data to be replaced in an atomic fashion.</summary> | |||
class InnerData | |||
/// <summary>Wrapper that enables map data to be replaced in an atomic fashion</summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
new MapField("Voices", "VoiceDefinitions", required: false), | ||
new MapField("Music", "MusicDefinitions", required: false), | ||
new MapField("Notifications", "NotificationDefinitions", required: false), | ||
//new MapField("Translations", "TranslationDefinitions", required: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebase error
|
||
if (yaml.TryGetValue("MapFormat", out var temp)) | ||
{ | ||
var format = FieldLoader.GetValue<int>("MapFormat", temp.Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newData.MapFormat is never set
SequenceSet Sequences { get; } | ||
CellRegion AllCells { get; } | ||
List<CPos> AllEdgeCells { get; } | ||
WDist CellHeightStep { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't this on the Elevation interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -97,7 +97,7 @@ void IUtilityCommand.Run(Utility utility, string[] args) | |||
if (package == null) | |||
continue; | |||
|
|||
using (var testMap = new Map(modData, package)) | |||
var testMap = modData.MapLoader.Load(modData, package); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the using
OpenRA.Game/Map/Map.cs
Outdated
else | ||
throw new InvalidDataException($"Unknown binary map format '{Format}'"); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"An opening brace should not be followed by a blank line"
@@ -11,25 +11,28 @@ | |||
|
|||
using System; | |||
using OpenRA.Server; | |||
using OpenRA.Mods.Common.MapFormats; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Using directives should be ordered alphabetically by the namespaces"
{ | ||
public const int SupportedMapFormat = 12; | ||
|
||
/// <summary>Defines the order of the fields in map.yaml</summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Documentation text should end with a period"
catch (Exception e) | ||
{ | ||
Log.Write("debug", "Failed to load rules for {0} with error {1}", Title, e); | ||
rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Unnecessary assignment of a value to 'rules'"
Something went really wrong here. rules
is set but not used
@@ -106,7 +106,7 @@ public EditorResourceLayer(Actor self, EditorResourceLayerInfo info) | |||
kv => kv.Value.ResourceIndex, | |||
kv => kv.Key); | |||
|
|||
Map.Resources.CellEntryChanged += UpdateCell; | |||
((IMapResource)Map).Resources.CellEntryChanged += UpdateCell; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few lines above don't do Map = self.World.Map;
. You have no idea whether that would give you a map implementation you can use. Check the type and only use if it implements IMapResource
. Otherwise you are free to throw here so the modder knows not to use this trait with an incompatible map format.
Then remove all the casts like this one.
Repeat that for every affected trait in this commit.
The extraction of |
I'd say MapPreview is completely out of scope of this PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general (and i'm pretty sure I have already explained this more than once!) this is very difficult to review and has a high risk of regressions because it is attempting to do things backwards and touching a lot of things twice.
Like I showed with the tileset generalisation, the simpler approach is to start by introducing the interfaces, changing the way that world.Map
is accessed without changing anything about how that object is created. This separates out concerns allowing each interface to be done with its own commit.
Only at the end do we try to replace Map
with DefaultMap
etc - none of the users of world.Map
need to change at this point because they are all already using the interfaces.
A very partial specific review below.
@@ -32,7 +32,7 @@ public sealed class TerrainSpriteLayer : IDisposable | |||
readonly bool restrictToBounds; | |||
|
|||
readonly WorldRenderer worldRenderer; | |||
readonly Map map; | |||
readonly IMap map; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TerrainSpriteLayer
constructor should be checking that world.Map
implements IMapTiles
and throw an exception if it doesn't (see how the tileset code does this for examples).
@@ -88,7 +88,7 @@ public void Update(CPos cell, Sprite sprite, PaletteReference palette, float sca | |||
var xyz = float3.Zero; | |||
if (sprite != null) | |||
{ | |||
var cellOrigin = map.CenterOfCell(cell) - new WVec(0, 0, map.Grid.Ramps[map.Ramp[cell]].CenterHeightOffset); | |||
var cellOrigin = map.CenterOfCell(cell) - new WVec(0, 0, map.Grid.Ramps[((IMapElevation)map).Ramp[cell]].CenterHeightOffset); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var cellOrigin = map.CenterOfCell(cell);
if (map is IMapElevation mapElevation)
cellOrigin -= new WVec(0, 0, map.Grid.Ramps[mapElevation.Ramp[cell]].CenterHeightOffset);
@@ -242,6 +242,7 @@ public CPos ViewToWorld(int2 view) | |||
{ | |||
var world = worldRenderer.Viewport.ViewToWorldPx(view); | |||
var map = worldRenderer.World.Map; | |||
var mapRamp = ((IMapElevation)map).Ramp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise here, Viewport
should be able to handle maps that don't implement IMapElevation
(this is one of the main usecases in the upstream mods for these interfaces!)
@@ -126,7 +126,7 @@ public class MapGrid : IGlobalModData | |||
|
|||
public CellRamp[] Ramps { get; } | |||
|
|||
internal readonly CVec[][] TilesByDistance; | |||
public readonly CVec[][] TilesByDistance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only used within DefaultMap
. Move it there instead of making it public.
@@ -45,7 +45,7 @@ public ProjectedCellRegion(Map map, PPos topLeft, PPos bottomRight) | |||
var heightOffset = map.Grid.Type == MapGridType.RectangularIsometric ? maxHeight : maxHeight / 2; | |||
|
|||
// Use the map Height data array to clamp the bottom coordinate so it doesn't overflow the map | |||
mapBottomRight = map.Height.Clamp(new MPos(bottomRight.U, bottomRight.V + heightOffset)); | |||
mapBottomRight = ((IMapElevation)map).Height.Clamp(new MPos(bottomRight.U, bottomRight.V + heightOffset)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should support maps that don't implement IMapElevation
.
@@ -56,7 +56,7 @@ public JumpjetActorLayer(Actor self, JumpjetActorLayerInfo info) | |||
continue; | |||
|
|||
neighbourCount++; | |||
neighbourHeight += map.Height[neighbour]; | |||
neighbourHeight += ((IMapElevation)map).Height[neighbour]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check that map implements the interface first and throw if it doesn't (see tileset code).
@@ -42,6 +42,8 @@ public TSEditorResourceLayer(Actor self, TSEditorResourceLayerInfo info) | |||
|
|||
bool IsValidVeinNeighbour(CPos cell, CPos neighbour) | |||
{ | |||
var mapRamp = ((IMapElevation)Map).Ramp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise here.
65103d9
to
cdde97b
Compare
…s, and IMapElevation
b96d009
to
6d42063
Compare
Sorry, but I have to agree with @pchote here. I don't think this PR can be merged in it's current state, but should be split into more digestible parts. |
This is a rebased and working version of Ice Reaper's PR #18378, with the addition of splitting out the functionality of
Map.cs
into optional plug-and-play interfaces. Together this allows the creation of Map formats, which are custom versions ofMap.cs
that may or may not implement all the existing Map.cs functions (depending on whether they are needed by the mod), and additionally can decide on how these functions are implemented. The most notable example of this currently is isometric vs rectangular cells, these can be defined as two different map formats implementing the same interface, removing the need for numerous 'if else' clauses within the existing Map.cs to check for which C&C mod is currently being used. Additionally this also means that most of the existing functionality in Map.cs can now belong to OpenRA.Mods.Common, which enables users of the mod SDK to modify this behaviour without having to fork the OpenRA repo.Below are the changes:
IMap
now incorporates all the base functionality ofMap.cs
IMap
, rather than theMap
class directlyIMapElevation
rather than toMap
directlyIMapTiles
andIMapResources
respectivelyDefaultMap
andDefaultMapLoader
have been created as the initial Map format, and map format loader, however these will eventually be replaced in future PRs with more specialised map formats (e.g. Gen1Map, Gen1MapLoader for TD/RA and Gen2Map, Gen2MapLoader for TS). This implementation sits in OpenRA.Mods.Common rather than OpenRA.Game.Also please see this comment from pchote for the motivation behind most of the above. The majority of these changes have been implemented in this PR.
NOTE: I have kept my commits unsquashed for now, to make it easier to split out the PR in the future if this becomes necessary.