Skip to content

Commit d9d8d1a

Browse files
authored
Display tile element map (#151)
* progress * add height and terrain colouring * move tile element map into s5file * add border around each tile element
1 parent f3ff1b0 commit d9d8d1a

File tree

4 files changed

+256
-44
lines changed

4 files changed

+256
-44
lines changed

Dat/Types/SCV5/S5File.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using OpenLoco.Dat.Data;
12
using OpenLoco.Dat.FileParsing;
23
using System.ComponentModel;
34

@@ -19,6 +20,9 @@ public record S5File(
1920
public bool Validate() => true;
2021
public const int StructLength = 0x20;
2122

23+
// convert the 1D TileElements into a more usable 2D array
24+
public List<TileElement>[,] TileElementMap { get; set; }
25+
2226
public static S5File Read(ReadOnlySpan<byte> data)
2327
{
2428
var header = SawyerStreamReader.ReadChunk<S5FileHeader>(ref data);
@@ -67,18 +71,42 @@ public static S5File Read(ReadOnlySpan<byte> data)
6771
// tile elements
6872
var tileElementData = SawyerStreamReader.ReadChunkCore(ref data);
6973
var numTileElements = tileElementData.Length / TileElement.StructLength;
74+
7075
List<TileElement> tileElements = [];
76+
var tileElementMap = new List<TileElement>[Limits.kMapColumns, Limits.kMapRows];
77+
78+
var x = 0;
79+
var y = 0;
7180
for (var i = 0; i < numTileElements; ++i)
7281
{
7382
var el = TileElement.Read(tileElementData[..TileElement.StructLength]);
7483
tileElementData = tileElementData[TileElement.StructLength..];
7584
tileElements.Add(el);
85+
86+
if (tileElementMap[x, y] == null)
87+
{
88+
tileElementMap[x, y] = [el];
89+
}
90+
else
91+
{
92+
tileElementMap[x, y].Add(el);
93+
}
94+
95+
if (el.IsLast())
96+
{
97+
if (x == Limits.kMapColumns - 1)
98+
{
99+
y = (y + 1) % Limits.kMapRows;
100+
}
101+
x = (x + 1) % Limits.kMapColumns;
102+
}
103+
76104
// el.IsLast() indicates its the last element on that tile
77105
// tiles are set out in rows
78106
// see TileManager.cpp::updateTilePointers in OpenLoco
79107
}
80108

81-
return new S5File(header, landscapeDetails, saveDetails, requiredObjects, gameState, tileElements, packedObjects);
109+
return new S5File(header, landscapeDetails, saveDetails, requiredObjects, gameState, tileElements, packedObjects) { TileElementMap = tileElementMap };
82110
}
83111

84112
//public ReadOnlySpan<byte> Write()

Dat/Types/SCV5/TileElement.cs

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public enum ElementType : uint8_t
1616
};
1717

1818
[LocoStructSize(StructLength)]
19-
public class TileElement
19+
public abstract class TileElement
2020
{
2121
public const int StructLength = 0x08;
2222

@@ -27,8 +27,6 @@ public class TileElement
2727
public uint8_t Flags { get; set; }
2828
public uint8_t BaseZ { get; set; }
2929
public uint8_t ClearZ { get; set; }
30-
[LocoArrayLength(4)]
31-
public uint8_t[] var_4 { get; set; }
3230

3331
void SetLast(bool value)
3432
{
@@ -52,14 +50,98 @@ void SetLast(bool value)
5250
public static TileElement Read(ReadOnlySpan<byte> data)
5351
{
5452
ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length, StructLength);
55-
return new TileElement
53+
54+
var Type = (ElementType)((data[0] & 0x3C) >> 2); // https://github.com/OpenLoco/OpenLoco/blob/master/src/OpenLoco/src/Map/Tile.cpp#L23
55+
56+
return Type switch
5657
{
57-
Type = (ElementType)data[0],
58-
Flags = data[1],
59-
BaseZ = data[2],
60-
ClearZ = data[3],
61-
var_4 = data[4..8].ToArray()
58+
ElementType.Building => new BuildingElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = BitConverter.ToUInt16(data[6..8]) },
59+
ElementType.Industry => new IndustryElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], IndustryId = data[4], _5 = data[5], _6 = BitConverter.ToUInt16(data[6..8]) },
60+
ElementType.Road => new RoadElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] },
61+
ElementType.Signal => new SignalElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], LeftSide = new SignalElement.Side() { _4 = data[4], _5 = data[5] }, RightSide = new SignalElement.Side() { _4 = data[6], _5 = data[7] } },
62+
ElementType.Station => new StationElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], StationId = BitConverter.ToUInt16(data[6..8]) },
63+
ElementType.Surface => new SurfaceElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], Slope = data[4], Water = data[5], Terrain = data[6], _7 = data[7] },
64+
ElementType.Track => new TrackElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] },
65+
ElementType.Tree => new TreeElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] },
66+
ElementType.Wall => new WallElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] },
67+
_ => throw new NotImplementedException(),
6268
};
6369
}
6470
}
71+
72+
public class BuildingElement : TileElement
73+
{
74+
public uint8_t _4 { get; set; }
75+
public uint8_t _5 { get; set; }
76+
public uint16_t _6 { get; set; }
77+
}
78+
79+
public class IndustryElement : TileElement
80+
{
81+
public uint8_t IndustryId { get; set; }
82+
public uint8_t _5 { get; set; }
83+
public uint16_t _6 { get; set; }
84+
}
85+
86+
public class RoadElement : TileElement
87+
{
88+
public uint8_t _4 { get; set; }
89+
public uint8_t _5 { get; set; }
90+
public uint8_t _6 { get; set; }
91+
public uint8_t _7 { get; set; }
92+
}
93+
94+
public class SignalElement : TileElement
95+
{
96+
public class Side
97+
{
98+
public uint8_t _4 { get; set; }
99+
public uint8_t _5 { get; set; }
100+
}
101+
102+
public Side LeftSide { get; set; }
103+
public Side RightSide { get; set; }
104+
}
105+
106+
public class StationElement : TileElement
107+
{
108+
public uint8_t _4 { get; set; }
109+
public uint8_t _5 { get; set; }
110+
public uint16_t StationId { get; set; }
111+
}
112+
113+
public class SurfaceElement : TileElement
114+
{
115+
public uint8_t Slope { get; set; }
116+
public uint8_t Water { get; set; }
117+
public uint8_t Terrain { get; set; }
118+
public uint8_t _7 { get; set; }
119+
120+
public bool IsWater() => (Water & 0x1F) != 0;
121+
public uint8_t TerrainType() => (uint8_t)(Terrain & 0x1F);
122+
}
123+
124+
public class TrackElement : TileElement
125+
{
126+
public uint8_t _4 { get; set; }
127+
public uint8_t _5 { get; set; }
128+
public uint8_t _6 { get; set; }
129+
public uint8_t _7 { get; set; }
130+
}
131+
132+
public class TreeElement : TileElement
133+
{
134+
public uint8_t _4 { get; set; }
135+
public uint8_t _5 { get; set; }
136+
public uint8_t _6 { get; set; }
137+
public uint8_t _7 { get; set; }
138+
}
139+
140+
public class WallElement : TileElement
141+
{
142+
public uint8_t _4 { get; set; }
143+
public uint8_t _5 { get; set; }
144+
public uint8_t _6 { get; set; }
145+
public uint8_t _7 { get; set; }
146+
}
65147
}

Gui/ViewModels/DatTypes/SCV5ViewModel.cs

Lines changed: 123 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
using Avalonia.Media.Imaging;
2+
using OpenLoco.Dat.Data;
13
using OpenLoco.Dat.FileParsing;
24
using OpenLoco.Dat.Types.SCV5;
35
using OpenLoco.Gui.Models;
6+
using PropertyModels.Extensions;
7+
using ReactiveUI;
48
using ReactiveUI.Fody.Helpers;
9+
using System;
10+
using System.Collections.Generic;
511
using System.ComponentModel;
12+
using System.ComponentModel.DataAnnotations;
13+
using System.Linq;
614

715
namespace OpenLoco.Gui.ViewModels
816
{
@@ -21,43 +29,130 @@ public SCV5ViewModel(FileSystemItem currentFile, ObjectEditorModel model)
2129
public BindingList<S5HeaderViewModel>? PackedObjects { get; set; }
2230

2331
[Reactive]
24-
public BindingList<TileElement>? TileElements { get; set; }
32+
public WriteableBitmap Map { get; set; }
2533

26-
//[Reactive]
27-
//public List<TileElement>[,] TileElementMap { get; set; }
34+
[Reactive]
35+
public Dictionary<ElementType, Bitmap> Maps { get; set; }
36+
37+
[Reactive, Range(0, Limits.kMapColumns - 1)]
38+
public int TileElementX { get; set; }
39+
40+
[Reactive, Range(0, Limits.kMapRows - 1)]
41+
public int TileElementY { get; set; }
2842

29-
//[Reactive]
30-
//public Image<Rgba32> Map { get; set; }
43+
public BindingList<TileElement> CurrentTileElements
44+
{
45+
get
46+
{
47+
if (CurrentS5File != null && TileElementX >= 0 && TileElementX < 384 && TileElementY >= 0 && TileElementY < 384)
48+
{
49+
return CurrentS5File.TileElementMap[TileElementX, TileElementY].ToBindingList();
50+
}
51+
return [];
52+
}
53+
}
3154

3255
public override void Load()
3356
{
3457
Logger?.Info($"Loading scenario from {CurrentFile.Filename}");
3558
CurrentS5File = SawyerStreamReader.LoadSave(CurrentFile.Filename, Model.Logger);
3659
RequiredObjects = new BindingList<S5HeaderViewModel>(CurrentS5File!.RequiredObjects.ConvertAll(x => new S5HeaderViewModel(x)));
3760
PackedObjects = new BindingList<S5HeaderViewModel>(CurrentS5File!.PackedObjects.ConvertAll(x => new S5HeaderViewModel(x.Item1))); // note: cannot bind to this, but it'll allow us to display at least
38-
TileElements = new BindingList<TileElement>(CurrentS5File!.TileElements);
39-
40-
//Map = new Image<Rgba32>(384, 384);
41-
42-
// todo: find a way to actually render this in the UI. code here works fine, just dunno the ui
43-
//TileElementMap = new List<TileElement>[Limits.kMapColumns, Limits.kMapRows];
44-
45-
//var i = 0;
46-
//for (var y = 0; y < Limits.kMapRows; ++y)
47-
//{
48-
// for (var x = 0; x < Limits.kMapColumns; ++x)
49-
// {
50-
// TileElementMap[x, y] = [];
51-
// do
52-
// {
53-
// TileElementMap[x, y].Add(CurrentS5File!.TileElements[i]);
54-
// i++;
55-
// }
56-
// while (!CurrentS5File!.TileElements[i - 1].IsLast());
57-
// }
58-
//}
59-
60-
//TileElements = new BindingList<TileElement>(CurrentS5File!.TileElements);
61+
62+
_ = this.WhenAnyValue(o => o.TileElementX)
63+
.Subscribe(_ => this.RaisePropertyChanged(nameof(CurrentTileElements)));
64+
65+
_ = this.WhenAnyValue(o => o.TileElementY)
66+
.Subscribe(_ => this.RaisePropertyChanged(nameof(CurrentTileElements)));
67+
68+
Map = new WriteableBitmap(new Avalonia.PixelSize(384, 384), new Avalonia.Vector(92, 92), Avalonia.Platform.PixelFormat.Rgba8888);
69+
using (var fb = Map.Lock())
70+
{
71+
var teMap = CurrentS5File!.TileElementMap;
72+
for (var y = 0; y < teMap.GetLength(1); ++y)
73+
{
74+
for (var x = 0; x < teMap.GetLength(0); ++x)
75+
{
76+
var el = teMap[x, y].Last();
77+
unsafe
78+
{
79+
var rgba = (byte*)fb.Address;
80+
var idx = ((x * 384) + y) * 4; // not sure why this has to be reversed to match loco
81+
82+
if (el.Type == ElementType.Surface)
83+
{
84+
var els = el as SurfaceElement;
85+
if (els!.IsWater())
86+
{
87+
rgba[idx + 0] = (byte)(74 + el.BaseZ);
88+
rgba[idx + 1] = (byte)(118 + el.BaseZ);
89+
rgba[idx + 2] = (byte)(124 + el.BaseZ);
90+
}
91+
else
92+
{
93+
rgba[idx + 0] = (byte)(111 + el.BaseZ - (els.TerrainType() * 8));
94+
rgba[idx + 1] = (byte)(75 + el.BaseZ + (els.TerrainType() * 8));
95+
rgba[idx + 2] = (byte)(23 + el.BaseZ + (els.TerrainType() * 8));
96+
97+
//rgbaValues[idx + 0] = (byte)(el.Terrain() == 1 ? 255 : 0);
98+
//rgbaValues[idx + 1] = (byte)(el.Terrain() == 1 ? 255 : 0);
99+
//rgbaValues[idx + 2] = (byte)(el.Terrain() == 1 ? 255 : 0);
100+
}
101+
}
102+
else if (el.Type == ElementType.Track)
103+
{
104+
rgba[idx + 0] = 131;
105+
rgba[idx + 1] = 151;
106+
rgba[idx + 2] = 151;
107+
}
108+
else if (el.Type == ElementType.Station)
109+
{
110+
rgba[idx + 0] = 255;
111+
rgba[idx + 1] = 163;
112+
rgba[idx + 2] = 79;
113+
}
114+
else if (el.Type == ElementType.Signal)
115+
{
116+
rgba[idx] = 255;
117+
rgba[idx + 1] = 0;
118+
rgba[idx + 2] = 0;
119+
}
120+
else if (el.Type == ElementType.Building)
121+
{
122+
rgba[idx] = 179;
123+
rgba[idx + 1] = 79;
124+
rgba[idx + 2] = 79;
125+
}
126+
else if (el.Type == ElementType.Tree)
127+
{
128+
rgba[idx] = 71;
129+
rgba[idx + 1] = 175;
130+
rgba[idx + 2] = 39;
131+
}
132+
else if (el.Type == ElementType.Wall)
133+
{
134+
rgba[idx] = 200;
135+
rgba[idx + 1] = 200;
136+
rgba[idx + 2] = 0;
137+
}
138+
else if (el.Type == ElementType.Road)
139+
{
140+
rgba[idx] = 47;
141+
rgba[idx + 1] = 67;
142+
rgba[idx + 2] = 67;
143+
}
144+
else if (el.Type == ElementType.Industry)
145+
{
146+
rgba[idx] = 139;
147+
rgba[idx + 1] = 139;
148+
rgba[idx + 2] = 191;
149+
}
150+
151+
rgba[idx + 3] = 255;
152+
}
153+
}
154+
}
155+
}
61156
}
62157

63158
public override void Save() => Logger?.Error("Saving SC5/SV5 is not implemented yet");

Gui/Views/MainWindow.axaml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -403,19 +403,26 @@
403403
</TabItem>
404404
<TabItem Header="Tile Elements">
405405
<DockPanel>
406-
<TextBlock Text="{Binding TileElements.Count}" DockPanel.Dock="Top" />
407-
<ScrollViewer>
408-
<ItemsRepeater ItemsSource="{Binding TileElements}">
406+
<Image Source="{Binding Map}" RenderOptions.BitmapInterpolationMode="None" DockPanel.Dock="Right"/>
407+
<StackPanel Orientation="Vertical" DockPanel.Dock="Left" MaxWidth="256">
408+
<StackPanel Orientation="Horizontal" >
409+
<TextBox Watermark="X" Text="{Binding TileElementX}"></TextBox>
410+
<TextBox Watermark="Y" Text="{Binding TileElementY}"></TextBox>
411+
</StackPanel>
412+
<ItemsRepeater ItemsSource="{Binding CurrentTileElements}">
409413
<ItemsRepeater.Layout>
410-
<UniformGridLayout Orientation="Horizontal"/>
414+
<UniformGridLayout Orientation="Vertical"/>
411415
</ItemsRepeater.Layout>
412416
<ItemsRepeater.ItemTemplate>
413417
<DataTemplate>
414-
<ContentControl Content="{Binding}"></ContentControl>
418+
<Border Margin="4" BorderThickness="1">
419+
<ContentControl Content="{Binding}"></ContentControl>
420+
421+
</Border>
415422
</DataTemplate>
416423
</ItemsRepeater.ItemTemplate>
417424
</ItemsRepeater>
418-
</ScrollViewer>
425+
</StackPanel>
419426
</DockPanel>
420427
</TabItem>
421428
</TabControl>

0 commit comments

Comments
 (0)