Skip to content

Commit 5a3f756

Browse files
Add Settings page
1 parent fea62e6 commit 5a3f756

File tree

6 files changed

+238
-45
lines changed

6 files changed

+238
-45
lines changed

CityMonitor.csproj

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@
3131
and also allow the build to move the mod DLLs into the plugins directory for you
3232
-->
3333
<PropertyGroup>
34-
<Cities2_Location>C:\Program Files (x86)\Steam\steamapps\common\Cities Skylines II</Cities2_Location>
34+
<Cities2_Location>G:\SteamLibrary\steamapps\common\Cities Skylines II</Cities2_Location>
3535
</PropertyGroup>
3636

3737
<!--
3838
This is all the references to the DLLs directly from your game directory. The Cities2_Location property
3939
above needs to be uncommented for this to work
4040
-->
4141
<ItemGroup>
42-
<Reference Include="$(Cities2_Location)\Cities2_Data\Managed\Colossal.*.dll" Private="False"/>
43-
<Reference Include="$(Cities2_Location)\Cities2_Data\Managed\Game.dll" Private="False"/>
44-
<Reference Include="$(Cities2_Location)\Cities2_Data\Managed\Unity.*.dll" Private="False"/>
42+
<Reference Include="$(Cities2_Location)\Cities2_Data\Managed\Colossal.*.dll" Private="False" />
43+
<Reference Include="$(Cities2_Location)\Cities2_Data\Managed\Game.dll" Private="False" />
44+
<Reference Include="$(Cities2_Location)\Cities2_Data\Managed\Unity.*.dll" Private="False" />
4545
</ItemGroup>
4646

4747
<!--
@@ -57,9 +57,9 @@
5757
DO NOT make the proprietary DLLs for the game public, as the files are owned by PDX/CO.
5858
-->
5959
<ItemGroup>
60-
<Reference Include="libcs2/Colossal.*.dll" Private="False"/>
61-
<Reference Include="libcs2/Game.dll" Private="False"/>
62-
<Reference Include="libcs2/Unity.*.dll" Private="False"/>
60+
<Reference Include="libcs2/Colossal.*.dll" Private="False" />
61+
<Reference Include="libcs2/Game.dll" Private="False" />
62+
<Reference Include="libcs2/Unity.*.dll" Private="False" />
6363
</ItemGroup>
6464

6565
<!--
@@ -68,6 +68,7 @@
6868
<ItemGroup>
6969
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.0.0" />
7070
<PackageReference Include="HarmonyX" Version="2.10.2"></PackageReference>
71+
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />
7172
<PackageReference Include="UnityEngine.Modules" Version="2022.3.7" IncludeAssets="compile" />
7273
<PackageReference Include="HookUILib" Version="0.1.0" />
7374
</ItemGroup>
@@ -87,24 +88,26 @@
8788
</ItemGroup>
8889

8990
<ItemGroup Condition="'$(BepInExVersion)' == '5'">
90-
<PackageReference Include="BepInEx.Core" Version="5.4.21" IncludeAssets="compile"/>
91+
<PackageReference Include="BepInEx.Core" Version="5.4.21" IncludeAssets="compile" />
9192
</ItemGroup>
9293

9394
<PropertyGroup Condition="'$(BepInExVersion)' == '6'">
9495
<DefineConstants>$(DefineConstants);BEPINEX_V6</DefineConstants>
9596
</PropertyGroup>
9697

9798
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
98-
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2"
99-
PrivateAssets="all" />
99+
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
100+
</ItemGroup>
101+
102+
<ItemGroup>
103+
<Folder Include="Patches\" />
100104
</ItemGroup>
101105

102106
<!--
103107
This will try to copy the resulting DLLs from builds directly into your game directory,
104108
as long as we're not in CI
105109
-->
106110
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(CI)' != 'true'">
107-
<Exec
108-
Command="if not exist &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)&quot; mkdir &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)&quot;&#xD;&#xA;copy /Y &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)\0Harmony.dll&quot;&#xD;&#xA;copy /Y &quot;$(TargetDir)$(ProjectName).dll&quot; &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)\$(ProjectName).dll&quot;" />
111+
<Exec Command="if not exist &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)&quot; mkdir &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)&quot;&#xD;&#xA;copy /Y &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)\0Harmony.dll&quot;&#xD;&#xA;copy /Y &quot;$(TargetDir)$(ProjectName).dll&quot; &quot;$(Cities2_Location)\BepInEx\plugins\$(ProjectName)\$(ProjectName).dll&quot;" />
109112
</Target>
110113
</Project>

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
all: build
2-
BEPINEX_VERSION = 6
2+
BEPINEX_VERSION = 5
33

44
clean:
55
@dotnet clean
@@ -11,6 +11,9 @@ build-ui:
1111
@npm install
1212
@npx esbuild ui_src/city_monitor.jsx --bundle --outfile=dist/bundle.js
1313

14+
dev-ui:
15+
@npx esbuild ui_src/city_monitor.jsx --watch --bundle --outfile="G:/SteamLibrary/steamapps/common/Cities Skylines II/Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.example.city_monitor.js"
16+
1417
build: clean restore build-ui
1518
@dotnet build /p:BepInExVersion=$(BEPINEX_VERSION)
1619

@@ -25,4 +28,7 @@ package-unix: build
2528
@-mkdir dist
2629
@cp bin/Debug/netstandard2.1/0Harmony.dll dist
2730
@cp bin/Debug/netstandard2.1/CityMonitor.dll dist
28-
@echo Packaged to dist/
31+
@echo Packaged to dist/
32+
33+
package-dev: package-unix
34+
@cp -r dist\CityMonitor.dll G:\Thunderstore\CitiesSkylines2\profiles\HookUI\BepInEx\plugins\CityMonitor\CityMonitor.dll

Patches/CityMonitorPatches.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Game;
2+
using Game.Common;
3+
using HarmonyLib;
4+
using CityMonitor.Systems;
5+
6+
namespace CityMonitor.Patches {
7+
8+
[HarmonyPatch(typeof(SystemOrder))]
9+
public static class SystemOrderPatch {
10+
[HarmonyPatch("Initialize")]
11+
[HarmonyPostfix]
12+
public static void Postfix(UpdateSystem updateSystem) {
13+
updateSystem.UpdateAt<CityMonitorUISystem>(SystemUpdatePhase.UIUpdate);
14+
}
15+
}
16+
}

Systems/CityMonitorUISystem.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using Game.UI;
2+
using Colossal.UI.Binding;
3+
using System.Collections.Generic;
4+
using Colossal.Annotations;
5+
using System;
6+
using System.Collections.Immutable;
7+
8+
namespace CityMonitor.Systems {
9+
10+
public record MeterSetting : IJsonWritable {
11+
public string label;
12+
public string eventName;
13+
public int index;
14+
public string gradientName;
15+
public bool isEnabled;
16+
17+
public void Write(IJsonWriter writer) {
18+
writer.TypeBegin(this.GetType().FullName);
19+
20+
writer.PropertyName("label");
21+
writer.Write(this.label);
22+
23+
writer.PropertyName("eventName");
24+
writer.Write(this.eventName);
25+
26+
writer.PropertyName("index");
27+
writer.Write(this.index);
28+
29+
writer.PropertyName("gradientName");
30+
writer.Write(this.gradientName);
31+
32+
writer.PropertyName("isEnabled");
33+
writer.Write(this.isEnabled);
34+
35+
writer.TypeEnd();
36+
}
37+
}
38+
39+
class CityMonitorUISystem : UISystemBase {
40+
private ImmutableDictionary<string, MeterSetting> meters;
41+
42+
private string kGroup = "city_monitor";
43+
protected override void OnCreate() {
44+
base.OnCreate();
45+
46+
var builder = ImmutableDictionary.CreateBuilder<string, MeterSetting>();
47+
48+
AddMeter(builder, 0, "electricity", "Electricity Availability", "electricityInfo.electricityAvailability", "maxGood");
49+
AddMeter(builder, 1, "water", "Water Availability", "waterInfo.waterAvailability", "maxGood");
50+
AddMeter(builder, 2, "sewage", "Sewage", "waterInfo.sewageAvailability", "maxGood");
51+
AddMeter(builder, 3, "landfill", "Landfill Usage", "garbageInfo.landfillAvailability", "maxGood");
52+
AddMeter(builder, 4, "healthcare", "Healthcare Availability", "healthcareInfo.healthcareAvailability", "maxGood");
53+
AddMeter(builder, 5, "health", "Average Health", "healthcareInfo.averageHealth", "maxGood");
54+
AddMeter(builder, 6, "cemetery", "Cemetery Availability", "healthcareInfo.cemeteryAvailability", "maxGood");
55+
AddMeter(builder, 7, "fire_hazard", "Fire Hazard", "fireAndRescueInfo.averageFireHazard", "minGood");
56+
AddMeter(builder, 8, "crime", "Crime Rate", "policeInfo.averageCrimeProbability", "minGood");
57+
AddMeter(builder, 9, "jail", "Jail Availability", "policeInfo.jailAvailability", "maxGood");
58+
AddMeter(builder, 10, "elementary", "Elementary School Availability", "educationInfo.elementaryAvailability", "maxGood");
59+
AddMeter(builder, 11, "highschool", "High School Availability", "educationInfo.highSchoolAvailability", "maxGood");
60+
AddMeter(builder, 12, "collage", "College Availability", "educationInfo.collegeAvailability", "maxGood");
61+
AddMeter(builder, 13, "university", "University Availability", "educationInfo.universityAvailability", "maxGood");
62+
63+
meters = builder.ToImmutableDictionary();
64+
65+
this.AddUpdateBinding(new GetterValueBinding<ImmutableDictionary<string, MeterSetting>>(kGroup, "meters", () => {
66+
return meters;
67+
}, new MyDictionaryWriter<string, MeterSetting>()));
68+
69+
70+
this.AddBinding(new TriggerBinding<string, bool>(kGroup, "toggle_visibility", new Action<string, bool>(ToggleVisibility)));
71+
}
72+
73+
private void AddMeter(ImmutableDictionary<string, MeterSetting>.Builder builder, int index, string key, string label, string eventName, string gradient) {
74+
builder.Add(key, new MeterSetting {
75+
label = label,
76+
eventName = eventName,
77+
index = index,
78+
gradientName = gradient,
79+
isEnabled = false,
80+
});
81+
}
82+
83+
private void ToggleVisibility(string key, bool newValue) {
84+
var oldMeter = meters[key];
85+
oldMeter.isEnabled = newValue;
86+
87+
var newMeters = meters.Remove(key).Add(key, oldMeter);
88+
this.meters = newMeters;
89+
}
90+
}
91+
92+
public class MyDictionaryWriter<K, V> : IWriter<IDictionary<K, V>> {
93+
[NotNull]
94+
private readonly IWriter<K> m_KeyWriter;
95+
96+
[NotNull]
97+
private readonly IWriter<V> m_ValueWriter;
98+
99+
public MyDictionaryWriter(IWriter<K> keyWriter = null, IWriter<V> valueWriter = null) {
100+
m_KeyWriter = keyWriter ?? ValueWriters.Create<K>();
101+
m_ValueWriter = valueWriter ?? ValueWriters.Create<V>();
102+
}
103+
104+
public void Write(IJsonWriter writer, IDictionary<K, V> value) {
105+
if (value != null) {
106+
writer.MapBegin(value.Count);
107+
foreach (KeyValuePair<K, V> item in value) {
108+
m_KeyWriter.Write(writer, item.Key);
109+
110+
if (item.Value is IDictionary<K, V> nestedDictionary) {
111+
Write(writer, nestedDictionary);
112+
}
113+
else {
114+
m_ValueWriter.Write(writer, item.Value);
115+
}
116+
}
117+
writer.MapEnd();
118+
return;
119+
}
120+
121+
writer.WriteNull();
122+
throw new ArgumentNullException("value", "Null passed to non-nullable dictionary writer");
123+
}
124+
}
125+
}

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui_src/city_monitor.jsx

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import {$Meter, $Panel, useDataUpdate} from 'hookui-framework'
2+
import {$Meter, $Panel, $Button, $Field, useDataUpdate} from 'hookui-framework'
33

44
const engineEffect = (react, event, setFunc) => {
55
const updateEvent = event + ".update"
@@ -8,7 +8,7 @@ const engineEffect = (react, event, setFunc) => {
88

99
return react.useEffect(() => {
1010
var clear = engine.on(updateEvent, (data) => {
11-
console.log(updateEvent, data)
11+
// console.log(updateEvent, data)
1212
if (data.current !== undefined && data.min !== undefined && data.max !== undefined) {
1313
const percentage = ((data.current - data.min) / (data.max - data.min)) * 100;
1414
setFunc(percentage);
@@ -25,36 +25,79 @@ const engineEffect = (react, event, setFunc) => {
2525
}, [])
2626
}
2727

28-
const eventsToListenTo = [
29-
['Electricity', 'electricityInfo.electricityAvailability', "maxGood"],
30-
['Water Availability', 'waterInfo.waterAvailability', "maxGood"],
31-
['Sewage', 'waterInfo.sewageAvailability', "maxGood"],
32-
['Landfill Usage', 'garbageInfo.landfillAvailability', "maxGood"],
33-
// TODO Incineration
34-
['Healthcare Availability', 'healthcareInfo.healthcareAvailability', "maxGood"],
35-
['Average Health', 'healthcareInfo.averageHealth', "maxGood"],
36-
['Cemetery Availability', 'healthcareInfo.cemeteryAvailability', "maxGood"],
37-
// TODO Crematorium
38-
['Fire Hazard', 'fireAndRescueInfo.averageFireHazard', "minGood"],
39-
['Crime Rate', 'policeInfo.averageCrimeProbability', "minGood"],
40-
['Jail Availability', 'policeInfo.jailAvailability', "maxGood"],
41-
['Elementary School Availability', 'educationInfo.elementaryAvailability', "maxGood"],
42-
['High School Availability', 'educationInfo.highSchoolAvailability', "maxGood"],
43-
['College Availability', 'educationInfo.collegeAvailability', "maxGood"],
44-
['University Availability', 'educationInfo.universityAvailability', "maxGood"],
45-
// TODO Employment Rate
46-
]
28+
const sortFunc = (data) => {
29+
return (a, b) => {
30+
if(data[a].index < data[b].index) {
31+
return -1
32+
} else if (data[a].index > data[b].index) {
33+
return 1
34+
}
35+
return 0
36+
}
37+
}
4738

48-
const $CityMonitor = ({react}) => {
49-
const meters = eventsToListenTo.map(([label, eventName, gradient]) => {
50-
const [read, set] = react.useState(-1)
51-
engineEffect(react, eventName, set)
52-
return <$Meter key={eventName} label={label} value={read} gradient={gradient}/>
39+
const $MetersPage = ({react, data}) => {
40+
let keys = Object.keys(data)
41+
keys.sort(sortFunc(data))
42+
43+
const meters = keys.map((k) => {
44+
const {label, eventName, gradientName, isEnabled} = data[k]
45+
if (isEnabled) {
46+
const [meterState, setMeterState] = react.useState(-1)
47+
engineEffect(react, eventName, setMeterState)
48+
return <$Meter key={eventName} label={label} value={meterState} gradient={gradientName}/>
49+
}
50+
}).filter(i => i)
51+
52+
return <div>
53+
{...meters}
54+
</div>
55+
}
56+
57+
const $SettingsPage = ({react, data, onToggle}) => {
58+
let keys = Object.keys(data)
59+
keys.sort(sortFunc(data))
60+
61+
const toggles = keys.map((k) => {
62+
const {label, isEnabled} = data[k]
63+
return <$Field label={label} checked={isEnabled} onToggle={(newCheckedValue) => {
64+
onToggle(k, newCheckedValue)
65+
}}/>
5366
})
5467

5568
return <div>
56-
<$Panel title="City Monitor" react={react}>
57-
{...meters}
69+
{...toggles}
70+
</div>
71+
}
72+
73+
const $CityMonitor = ({react}) => {
74+
const [showSettings, setShowSettings] = react.useState(false)
75+
76+
const [data, setData] = react.useState({})
77+
78+
useDataUpdate(react, "city_monitor.meters", setData)
79+
80+
const handleToggle = (k, newValue) => {
81+
engine.trigger('city_monitor.toggle_visibility', k, newValue)
82+
}
83+
84+
let toRender = null
85+
if (showSettings) {
86+
toRender = <$SettingsPage react={react} data={data} onToggle={handleToggle}/>
87+
} else {
88+
toRender = <$MetersPage react={react} data={data}/>
89+
}
90+
91+
const buttonLabel = showSettings ? "Meters" : "Settings";
92+
93+
const style = {
94+
height: "auto"
95+
}
96+
97+
return <div>
98+
<$Panel title="City Monitor" react={react} style={style}>
99+
<$Button label={buttonLabel} onClick={() => setShowSettings(!showSettings)}/>
100+
{toRender}
58101
</$Panel>
59102
</div>
60103
}

0 commit comments

Comments
 (0)