├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── GooeeUI.cs ├── InfoLoom.csproj ├── InfoLoom.csproj.user ├── InfoLoom.sln ├── Makefile ├── Mod.cs ├── Patches.cs ├── Properties ├── PublishConfiguration.xml ├── PublishProfiles │ ├── PublishNewMod.pubxml │ ├── PublishNewMod.pubxml.user │ ├── PublishNewVersion.pubxml │ ├── PublishNewVersion.pubxml.user │ ├── UpdatePublishedConfiguration.pubxml │ └── UpdatePublishedConfiguration.pubxml.user └── Thumbnail.png ├── README.md ├── Setting.cs ├── Systems ├── BuildingDemandUISystem.cs ├── CommercialDemandUISystem.cs ├── IndustrialDemandUISystem.cs ├── PopulationStructureUISystem.cs ├── ResidentialDemandUISystem.cs ├── WorkforceInfoLoomUISystem.cs └── WorkplacesInfoLoomUISystem.cs ├── Thunderstore ├── 0Harmony.dll ├── InfoLoom.dll ├── README.md ├── Thunderstore.zip ├── icon.png └── manifest.json ├── _not_used ├── InfoLoom_BepInEx.csproj ├── Patcher.cs ├── Plugin.cs └── Utils.cs ├── docs ├── commercial.png ├── consumption.png ├── demandfactors.png ├── demographics.png ├── industrial.png ├── residential.png └── workforce_jobs.png ├── package-lock.json ├── package.json └── ui_src ├── commercial.jsx ├── demandfactors.jsx ├── demographics.jsx ├── gooee-menu.json ├── industrial.jsx ├── panel.jsx ├── residential.jsx ├── styles.css ├── ui.jsx ├── use-data-update.js ├── workforce.jsx └── workplaces.jsx /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | bin/ 3 | obj/ 4 | ts-build/ 5 | node_modules/ 6 | .vs/ 7 | Library/ilpp.pid -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | - v0.9.1 (2024-04-13) 4 | - Residential Data now calculates totals for buildings and free ratio. 5 | - Fix for null-refererence-crash in WorkplacesInfoLoomUISystem courtesy of khallmark. Thank you. 6 | 7 | - v0.9 (2024-04-04) 8 | - Mod migrated to PDXMods platform. 9 | 10 | - v0.8.3 (2024-03-21) 11 | - Mod updated for v1.1 of the game. 12 | 13 | - v0.8.2 (2024-02-12) 14 | - Debug Watches for demand and consumption. 15 | 16 | - v0.8.1 (2024-02-09) 17 | - Fixed: null reference in Editor Mode. 18 | - Fixed: student ratio in empty city. 19 | - Fixed: demographics bars sometimes going beyond the window. 20 | 21 | - v0.8 (2024-02-04) 22 | - New feature: industrial and office data. 23 | - New feature: household demand and student chance. 24 | - Fixed: demographics proper scaling. 25 | 26 | - v0.7 (2024-01-14) 27 | - New features: number of commuters as employees, unemployment and structures as percentages, number of companies. 28 | - Fixed: Incorrect counting of Office and Leisure jobs. 29 | - Fixed: Issue with Asset Editor. 30 | 31 | - v0.6 (2024-01-04) 32 | - New feature: residential data. 33 | 34 | - v0.5 (2024-01-02) 35 | - New feature: commercial data, homeless count. 36 | - Population bars in Demographics window are scalable now. 37 | - Fixed: Demographics now correctly shows Tourists and Commuters. 38 | 39 | - v0.4.1 (2023-12-22) 40 | - New feature: demographics. 41 | 42 | - v0.3 (2023-12-20) 43 | - New features: worforce structure and workplaces distribution. 44 | 45 | - v0.2.2 (2023-12-17) 46 | - Demand factors window is reformatted, to be more aligned with game's visual style. 47 | - New features: shows all factors, building demand and resources consumption. 48 | 49 | - v0.1 (2023-12-16) 50 | - Initial build, includes Demand Factors. 51 | -------------------------------------------------------------------------------- /GooeeUI.cs: -------------------------------------------------------------------------------- 1 | using Gooee.Plugins.Attributes; 2 | using Gooee.Plugins; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using Game; 7 | 8 | namespace InfoLoom.Gooee; 9 | 10 | public class InfoLoomModel : Model 11 | { 12 | // simple data to control the window visibility 13 | public bool IsVisibleDemographics { get; set; } 14 | public bool IsVisibleWorkforce { get; set; } 15 | public bool IsVisibleWorkplaces { get; set; } 16 | public bool IsVisibleDemand { get; set; } 17 | public bool IsVisibleResidential { get; set; } 18 | public bool IsVisibleCommercial { get; set; } 19 | public bool IsVisibleIndustrial { get; set; } 20 | } 21 | 22 | 23 | public partial class InfoLoomController : Controller 24 | { 25 | public override InfoLoomModel Configure() 26 | { 27 | return new InfoLoomModel(); 28 | } 29 | 30 | [OnTrigger] 31 | private void OnToggleVisible(string key) 32 | { 33 | // stub method to avoid errors 34 | } 35 | 36 | [OnTrigger] 37 | private void OnToggleVisibleDemographics(string key) 38 | { 39 | base.Model.IsVisibleDemographics = !Model.IsVisibleDemographics; 40 | base.TriggerUpdate(); 41 | } 42 | 43 | [OnTrigger] 44 | private void OnToggleVisibleWorkforce(string key) 45 | { 46 | base.Model.IsVisibleWorkforce = !Model.IsVisibleWorkforce; 47 | base.TriggerUpdate(); 48 | } 49 | 50 | [OnTrigger] 51 | private void OnToggleVisibleWorkplaces(string key) 52 | { 53 | base.Model.IsVisibleWorkplaces = !Model.IsVisibleWorkplaces; 54 | base.TriggerUpdate(); 55 | } 56 | 57 | [OnTrigger] 58 | private void OnToggleVisibleDemand(string key) 59 | { 60 | base.Model.IsVisibleDemand = !Model.IsVisibleDemand; 61 | base.TriggerUpdate(); 62 | } 63 | 64 | [OnTrigger] 65 | private void OnToggleVisibleResidential(string key) 66 | { 67 | base.Model.IsVisibleResidential = !Model.IsVisibleResidential; 68 | base.TriggerUpdate(); 69 | } 70 | 71 | [OnTrigger] 72 | private void OnToggleVisibleCommercial(string key) 73 | { 74 | base.Model.IsVisibleCommercial = !Model.IsVisibleCommercial; 75 | base.TriggerUpdate(); 76 | } 77 | 78 | [OnTrigger] 79 | private void OnToggleVisibleIndustrial(string key) 80 | { 81 | base.Model.IsVisibleIndustrial = !Model.IsVisibleIndustrial; 82 | base.TriggerUpdate(); 83 | } 84 | } 85 | 86 | 87 | [ControllerTypes(typeof(InfoLoomController))] 88 | //[PluginToolbar(typeof(InfoLoom_Controller), "OnToggleVisible", "InfoLoom", "Media/Game/Icons/Workers.svg")] // generic hook version 89 | [PluginToolbar(typeof(InfoLoomController), "InfoLoom.ui_src.gooee-menu.json")] 90 | public class InfoLoomGooeePluginWithControllers : IGooeePluginWithControllers, IGooeeStyleSheet 91 | { 92 | public string Name => "InfoLoom"; 93 | public string Version => "0.9.0"; 94 | public string ScriptResource => "InfoLoom.dist.ui.js"; 95 | public string StyleResource => "InfoLoom.dist.ui.css"; 96 | 97 | public IController[] Controllers { get; set; } 98 | 99 | } 100 | 101 | // MENU 102 | /* 103 | OnClickMethod = controllerType.GetMethod(pluginToolbarItem.OnClick, BindingFlags.Instance | BindingFlags.NonPublic), 104 | OnGetChildren = (string.IsNullOrEmpty(pluginToolbarItem.OnGetChildren) ? null : controllerType.GetMethod(pluginToolbarItem.OnGetChildren, BindingFlags.Instance | BindingFlags.NonPublic)), 105 | OnClickKey = pluginToolbarItem.OnClickKey, 106 | Label = pluginToolbarItem.Label, 107 | Icon = pluginToolbarItem.Icon, 108 | IconClassName = pluginToolbarItem.IconClassName, 109 | IsFAIcon = pluginToolbarItem.IsFAIcon 110 | */ 111 | 112 | 113 | 114 | // UM 115 | 116 | // window.$_gooee.register("ultimatemonitor", "UltimateMonitorWindow", ToolWindow, "main-container", "ultimatemonitor"); 117 | 118 | /* 119 | 120 | //[ControllerDepends(SystemUpdatePhase.GameSimulation, typeof(UnemploymentMonitorSystem))] 121 | //[ControllerDepends(SystemUpdatePhase.GameSimulation, typeof(UltimateMonitorDataSystem))] 122 | public class UltimateMonitorController : Controller 123 | { 124 | public override UltimateMonitorViewModel Configure() 125 | { 126 | UltimateMonitorViewModel model = new UltimateMonitorViewModel(); 127 | return model; 128 | } 129 | 130 | [OnTrigger] 131 | private void OnToggleAddWindow() 132 | { 133 | base.Model.ShowAddWindow = !base.Model.ShowAddWindow; 134 | base.TriggerUpdate(); 135 | } 136 | 137 | [OnTrigger] 138 | private void OnAddMonitor(string json) 139 | { 140 | } 141 | } 142 | 143 | 144 | [ControllerTypes(new Type[] { typeof(UltimateMonitorController) })] 145 | [PluginToolbar(typeof(UltimateMonitorController), "UltimateMonitor.Resources.gooee-menu.json")] 146 | public class UltimateMonitorPlugin : IGooeePluginWithControllers, IGooeePlugin //, IGooeeChangeLog, IGooeeLanguages, IGooeeStyleSheet 147 | { 148 | public string Name => "UltimateMonitor"; 149 | 150 | public string Version => "1.4.0"; 151 | 152 | public string ScriptResource => "UltimateMonitor.Resources.ui.js"; 153 | 154 | //public string ChangeLogResource => "UltimateMonitor.Resources.changelog.md"; 155 | 156 | //public string StyleResource => null; 157 | 158 | public IController[] Controllers { get; set; } 159 | 160 | //public string LanguageResourceFolder => "UltimateMonitor.Resources.lang"; 161 | } 162 | 163 | public class UltimateMonitorViewModel : Model 164 | { 165 | //public List Items { get; set; } 166 | 167 | //public List Windows { get; set; } 168 | 169 | public bool ShowAddWindow { get; set; } 170 | } 171 | 172 | */ -------------------------------------------------------------------------------- /InfoLoom.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 9 | 10 | 11 | Library 12 | net472 13 | Debug;Release 14 | 0.9.1 15 | 11.0 16 | 17 | 18 | C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\Managed 19 | 20 | 21 | 22 | 23 | 24 | 25 | $(USERPROFILE)\Desktop\pdx_account.txt 26 | 27 | 28 | Properties\PublishConfiguration.xml 29 | 30 | 31 | $(UserProfile)\AppData\LocalLow\Colossal Order\Cities Skylines II\Mods 32 | $(UserProfile)\AppData\LocalLow\Colossal Order\Cities Skylines II\.cache\Mods\mods_subscribed\ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 53 | 54 | 58 | 65 | 66 | 67 | 68 | $(PDXModsPath)\77225_8\Gooee.dll 69 | false 70 | 71 | 72 | false 73 | 74 | 75 | false 76 | 77 | 78 | false 79 | 80 | 81 | false 82 | 83 | 84 | false 85 | 86 | 87 | false 88 | 89 | 90 | false 91 | 92 | 93 | false 94 | 95 | 96 | false 97 | 98 | 99 | false 100 | 101 | 102 | false 103 | 104 | 105 | false 106 | 107 | 108 | false 109 | 110 | 111 | 112 | 113 | 114 | false 115 | 116 | 117 | false 118 | 119 | 120 | false 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 135 | 136 | 137 | 138 | 139 | 148 | 149 | 150 | 155 | 162 | 163 | 166 | 167 | 172 | 173 | 176 | 177 | 178 | 179 | 184 | 185 | 190 | 207 | 208 | 212 | 217 | -------------------------------------------------------------------------------- /InfoLoom.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | <_LastSelectedProfileId>C:\Repos\CS2-Mods\CS2-InfoLoom\Properties\PublishProfiles\UpdatePublishedConfiguration.pubxml 5 | 6 | -------------------------------------------------------------------------------- /InfoLoom.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34330.188 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InfoLoom", "InfoLoom.csproj", "{B874AF15-B836-4585-8BCF-F280F9068DEC}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {B874AF15-B836-4585-8BCF-F280F9068DEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {B874AF15-B836-4585-8BCF-F280F9068DEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {B874AF15-B836-4585-8BCF-F280F9068DEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {B874AF15-B836-4585-8BCF-F280F9068DEC}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {F39BC0DE-B081-4C90-82A6-8CB0DCDD0448} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | BEPINEX_VERSION = 5 3 | 4 | clean: 5 | @dotnet clean 6 | 7 | restore: 8 | @dotnet restore 9 | 10 | build-ui: 11 | @npm install 12 | @npx esbuild ui_src/ui.jsx --bundle --outfile=dist/ui.js 13 | # @npx esbuild ui_src/demographics.jsx --bundle --outfile=dist/demographics.js 14 | # @npx esbuild ui_src/workforce.jsx --bundle --outfile=dist/workforce.js 15 | # @npx esbuild ui_src/demandfactors.jsx --bundle --outfile=dist/demandfactors.js 16 | # @npx esbuild ui_src/workplaces.jsx --bundle --outfile=dist/workplaces.js 17 | # @npx esbuild ui_src/commercial.jsx --bundle --outfile=dist/commercial.js 18 | # @npx esbuild ui_src/residential.jsx --bundle --outfile=dist/residential.js 19 | # @npx esbuild ui_src/industrial.jsx --bundle --outfile=dist/industrial.js 20 | 21 | copy-ui: 22 | copy dist\demographics.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.demographics.js" 23 | copy dist\workforce.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.workforce.js" 24 | copy dist\demandfactors.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.demandfactors.js" 25 | copy dist\workplaces.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.workplaces.js" 26 | copy dist\commercial.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.commercial.js" 27 | copy dist\residential.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.residential.js" 28 | copy dist\industrial.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.industrial.js" 29 | 30 | build: clean restore build-ui 31 | @dotnet build /p:BepInExVersion=$(BEPINEX_VERSION) 32 | 33 | dev-ui: 34 | @npx esbuild ui_src/ui.jsx --bundle --outfile=dist/ui.js 35 | copy dist\ui.js "C:\Users\Grzegorz\AppData\LocalLow\Colossal Order\Cities Skylines II\Mods\Gooee\Plugins\InfoLoom.js" 36 | 37 | dev-demand: 38 | @npx esbuild ui_src/demandfactors.jsx --bundle --outfile=dist/demandfactors.js 39 | copy dist\demandfactors.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.demandfactors.js" 40 | 41 | dev-workforce: 42 | @npx esbuild ui_src/workforce.jsx --bundle --outfile=dist/workforce.js 43 | copy dist\workforce.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.workforce.js" 44 | 45 | dev-workplaces: 46 | @npx esbuild ui_src/workplaces.jsx --bundle --outfile=dist/workplaces.js 47 | copy dist\workplaces.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.workplaces.js" 48 | 49 | dev-demo: 50 | @npx esbuild ui_src/demographics.jsx --bundle --outfile=dist/demographics.js 51 | copy dist\demographics.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.demographics.js" 52 | 53 | dev-comm: 54 | @npx esbuild ui_src/commercial.jsx --bundle --outfile=dist/commercial.js 55 | copy dist\commercial.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.commercial.js" 56 | 57 | dev-res: 58 | @npx esbuild ui_src/residential.jsx --bundle --outfile=dist/residential.js 59 | copy dist\residential.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.residential.js" 60 | 61 | dev-ind: 62 | @npx esbuild ui_src/industrial.jsx --bundle --outfile=dist/industrial.js 63 | copy dist\industrial.js "C:\Steam\steamapps\common\Cities Skylines II\Cities2_Data\StreamingAssets\~UI~\HookUI\Extensions\panel.infoloom.industrial.js" 64 | 65 | package-win: 66 | @-mkdir dist 67 | @cmd /c copy /y "bin\Debug\netstandard2.1\0Harmony.dll" "dist\" 68 | @cmd /c copy /y "bin\Debug\netstandard2.1\PopStruct.dll" "dist\" 69 | @echo Packaged to dist/ 70 | 71 | package-unix: build 72 | @-mkdir dist 73 | @cp bin/Debug/netstandard2.1/0Harmony.dll dist 74 | @cp bin/Debug/netstandard2.1/PopStruct.dll dist 75 | @echo Packaged to dist/ -------------------------------------------------------------------------------- /Mod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Unity.Entities; 5 | using Colossal.IO.AssetDatabase; 6 | using Colossal.Logging; 7 | using Game; 8 | using Game.Modding; 9 | using Game.SceneFlow; 10 | using Game.Prefabs; 11 | using Game.Economy; 12 | using Game.Common; 13 | using HarmonyLib; 14 | 15 | namespace InfoLoom; 16 | 17 | public class Mod : IMod 18 | { 19 | public static readonly string harmonyId = "Infixo." + nameof(InfoLoom); 20 | 21 | // mod's instance and asset 22 | public static Mod instance { get; private set; } 23 | public static ExecutableAsset modAsset { get; private set; } 24 | // logging 25 | public static ILog log = LogManager.GetLogger($"{nameof(InfoLoom)}").SetShowsErrorsInUI(false); 26 | 27 | // setting 28 | public static Setting setting { get; private set; } 29 | 30 | public void OnLoad(UpdateSystem updateSystem) 31 | { 32 | log.Info(nameof(OnLoad)); 33 | 34 | if (GameManager.instance.modManager.TryGetExecutableAsset(this, out var asset)) 35 | { 36 | log.Info($"Current mod asset at {asset.path}"); 37 | modAsset = asset; 38 | } 39 | 40 | // Setting 41 | setting = new Setting(this); 42 | setting.RegisterInOptionsUI(); 43 | setting._Hidden = false; 44 | AssetDatabase.global.LoadSettings(nameof(InfoLoom), setting, new Setting(this)); 45 | 46 | // Locale 47 | GameManager.instance.localizationManager.AddSource("en-US", new LocaleEN(setting)); 48 | 49 | // Harmony 50 | var harmony = new Harmony(harmonyId); 51 | harmony.PatchAll(typeof(Mod).Assembly); 52 | var patchedMethods = harmony.GetPatchedMethods().ToArray(); 53 | log.Info($"Plugin {harmonyId} made patches! Patched methods: " + patchedMethods.Length); 54 | foreach (var patchedMethod in patchedMethods) 55 | { 56 | log.Info($"Patched method: {patchedMethod.Module.Name}:{patchedMethod.DeclaringType.Name}.{patchedMethod.Name}"); 57 | } 58 | 59 | // Systems 60 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 61 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 62 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 63 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 64 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 65 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 66 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 67 | 68 | // Check if SeparateConsumption feature is enabled 69 | if (!setting.SeparateConsumption) 70 | { 71 | MethodBase mb = typeof(Game.UI.InGame.ProductionUISystem).GetMethod("GetData", BindingFlags.NonPublic | BindingFlags.Instance); 72 | if (mb != null) 73 | { 74 | log.Info($"Removing {mb.Name} patch from {harmonyId} because Separate Consumption is disabled."); 75 | harmony.Unpatch(mb, HarmonyPatchType.Prefix, harmonyId); 76 | } 77 | else 78 | log.Warn("Cannot remove GetData patch."); 79 | } 80 | } 81 | 82 | public void OnDispose() 83 | { 84 | log.Info(nameof(OnDispose)); 85 | if (setting != null) 86 | { 87 | setting.UnregisterInOptionsUI(); 88 | setting = null; 89 | } 90 | 91 | // Harmony 92 | var harmony = new Harmony(harmonyId); 93 | harmony.UnpatchAll(harmonyId); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Patches.cs: -------------------------------------------------------------------------------- 1 | using Colossal.UI.Binding; 2 | using Game; 3 | using Game.Economy; 4 | using Game.Prefabs; 5 | using Game.UI.InGame; 6 | using HarmonyLib; 7 | using Unity.Collections; 8 | using Unity.Entities; 9 | using Unity.Jobs; 10 | 11 | namespace InfoLoom; 12 | 13 | [HarmonyPatch] 14 | static class GamePatches 15 | { 16 | /* not used 17 | [HarmonyPatch(typeof(Game.Common.SystemOrder), "Initialize")] 18 | [HarmonyPostfix] 19 | public static void Initialize_Postfix(UpdateSystem updateSystem) 20 | { 21 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 22 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 23 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 24 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 25 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 26 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 27 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 28 | } 29 | */ 30 | 31 | [HarmonyPatch(typeof(Game.UI.InGame.CityInfoUISystem), "WriteDemandFactors")] 32 | [HarmonyPrefix] 33 | public static bool WriteDemandFactors_Prefix(CityInfoUISystem __instance, IJsonWriter writer, NativeArray factors, JobHandle deps) 34 | { 35 | deps.Complete(); 36 | NativeList list = FactorInfo.FromFactorArray(factors, Allocator.Temp); 37 | list.Sort(); 38 | try 39 | { 40 | // int num = math.min(5, list.Length); 41 | int num = list.Length; 42 | writer.ArrayBegin(num); 43 | for (int i = 0; i < num; i++) 44 | { 45 | list[i].WriteDemandFactor(writer); 46 | } 47 | writer.ArrayEnd(); 48 | } 49 | finally 50 | { 51 | list.Dispose(); 52 | } 53 | return false; // don't execute the original 54 | } 55 | 56 | [HarmonyPatch(typeof(Game.UI.InGame.ProductionUISystem), "GetData")] 57 | [HarmonyPrefix] 58 | static bool GetData(ref (int, int, int) __result, Game.UI.InGame.ProductionUISystem __instance, Entity entity, 59 | // private members that are used in the routine, start with 3 underscores 60 | PrefabSystem ___m_PrefabSystem, 61 | NativeList ___m_ProductionCache, 62 | NativeList ___m_CommercialConsumptionCache, 63 | NativeList ___m_IndustrialConsumptionCache) 64 | { 65 | int resourceIndex = EconomyUtils.GetResourceIndex(EconomyUtils.GetResource(___m_PrefabSystem.GetPrefab(entity).m_Resource)); 66 | //int num = ___m_ProductionCache[resourceIndex]; 67 | //int num2 = ___m_CommercialConsumptionCache[resourceIndex] + ___m_IndustrialConsumptionCache[resourceIndex]; 68 | //int num3 = math.min(num2, num); 69 | //int num4 = math.min(num2, num3); 70 | //int item = num - num3; 71 | //int item2 = num2 - num4; 72 | __result = (___m_ProductionCache[resourceIndex], ___m_CommercialConsumptionCache[resourceIndex], ___m_IndustrialConsumptionCache[resourceIndex]); 73 | return false; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Properties/PublishConfiguration.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Adds several new UI windows with extra information. 12 | 13 | Currently: 14 | * Demographics 15 | * Workforce structure 16 | * Workplaces distribution 17 | * Residential data 18 | * Commercial data 19 | * Industrial and Office data 20 | * Demand factors 21 | * Resources consumption (optional) 22 | 23 | Please see GitHub page for **full mod description** and more **details about each window**. 24 | 25 | ## Technical 26 | 27 | * The options are applied only once, during the mod initialization. For new setting to take effect, you need to **restart the game**. 28 | * The mod does **not** modify savefiles. 29 | * The mod does **not** modify any game systems. 30 | * The mod uses **Gooee** framework by **Cities2Modding**. Thank you for creating it and helping with the integration. 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | - Residential Data now calculates totals for buildings and free ratio. 54 | - Fix for null-refererence-crash in WorkplacesInfoLoomUISystem courtesy of khallmark. Thank you. 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/PublishNewMod.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | FileSystem 7 | <_TargetId>Folder 8 | net472 9 | bin\$(Configuration)\$(TargetFramework)\ 10 | Publish 11 | 12 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/PublishNewMod.pubxml.user: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | True|2024-04-04T21:05:29.5372303Z;False|2024-04-04T23:04:50.9521420+02:00; 8 | 9 | 10 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/PublishNewVersion.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | FileSystem 7 | <_TargetId>Folder 8 | net472 9 | bin\$(Configuration)\$(TargetFramework)\ 10 | NewVersion 11 | 12 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/PublishNewVersion.pubxml.user: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | True|2024-04-13T14:41:17.2671392Z; 8 | 9 | 10 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/UpdatePublishedConfiguration.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | FileSystem 7 | <_TargetId>Folder 8 | net472 9 | bin\$(Configuration)\$(TargetFramework)\ 10 | Update 11 | 12 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/UpdatePublishedConfiguration.pubxml.user: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | True|2024-04-13T14:45:57.7853469Z; 8 | 9 | 10 | -------------------------------------------------------------------------------- /Properties/Thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/Properties/Thumbnail.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InfoLoom 2 | InfoLoom is a Cities Skylines II mod that adds several new UI windows with extra information. 3 | Currently: 4 | - Demographics 5 | - Workforce structure 6 | - Workplaces distribution 7 | - Residential data 8 | - Commercial data 9 | - Industrial and Office data 10 | - Demand factors 11 | - Resources consumption (optional) 12 | 13 | ## Features 14 | 15 | ### Demographics 16 | - Key information about current city demographics in one place. 17 | - Age distribution histogram that also shows cims' activity structure. 18 | 19 | ![Demographics](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/demographics.png) 20 | 21 | ### Workforce structure 22 | - Shows workforce structure by education level. 23 | - Total: all potential workers, i.e. teens and adults, that are moved in and not moving away, excluding students. 24 | - Unemployment by education level and structure also shown as percentage. 25 | - Under: cims working on positions below their education level. 26 | - Outside: cims working outside of the city. 27 | - Homeless: unemployed cims with no home. 28 | 29 | ### Workplaces distribution 30 | - Shows available workplaces split into: city services, commercial sales, commercial leisure, industry extractors, industry manufacturing, and office. 31 | - Leisure are commercial companies selling immaterial resources. 32 | - Commute are cims employed from outside of the city, they commute to work. 33 | - Shows number of companies in each category. 34 | 35 | ![Workforce and Workplaces](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/workforce_jobs.png) 36 | 37 | ### Residential data 38 | - Shows residential data used to calculate Residential Demand Factors. 39 | - Residential properties by density: total count, occupied, free. 40 | - Total households and homeless households. 41 | - Average happiness, weighted tax rate, open study positions. 42 | - Household demand which is hidden in the game, it controls if new households spawn. 43 | - For new households, a chance that a Student Household will spawn. 44 | 45 | ![Residential data](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/residential.png) 46 | 47 | ### Commercial data 48 | - Shows commercial data used to calculate Commercial Demand Factors. 49 | - Empty buildings and companies that have no property, and average tax rate. 50 | - Service capacity utilization and sales capacity to consumption ratio - these are crucial in calculating LocalDemand, TouristDemand and PetrolLocalDemand. 51 | - Actual numbers of available workforce, and employee capacity ratio. 52 | - Lists resources that currently are not demanded, thus the engine ignores them when calculating Commercial Demand Factors. 53 | 54 | ![Commercial data](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/commercial.png) 55 | 56 | ### Industrial and Office data 57 | - Shows industrial and office data used to calculate Industrial and Office Demand Factors. 58 | - Empty buildings and companies that have no property, and average tax rate. 59 | - Local Demand (which is production capacity in fact) and Input Utilization (which tells if resources are available as input). 60 | - Actual numbers of available workforce, and employee capacity ratio. 61 | - Lists resources that currently are not demanded, thus the engine ignores them when calculating Industrial and Office Factors. 62 | - Storage section that shows Demanded Types info which controls if new warehouses are spawned. 63 | 64 | ![Industrial and Office data](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/industrial.png) 65 | 66 | ### Demand factors 67 | - Shows individual demand factor VALUES for each demand type. Helpful to learn actual importance of each factor. 68 | - Enables showing all demand factors, not only 5. Useful for industrial demand which is the only one that utilizes up yo 7 factors. 69 | - Additional section shows directly building demand for each zone type and STORAGE demand. Please note that the "main" demand factor is actually a building demand, however UI shows it as moving. In fact it doesn't move, it is just a visualization. 70 | - Also, industrial demand is a higher value from the two: industrial building demand and storage demand. 71 | 72 | ![Demand factors](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/demandfactors.png) 73 | 74 | ### Resources consumption 75 | - This feature is disabled by default because it changes the data shown on the vanilla UI. Enable it in the config file by setting SeparateConsumption to true. 76 | - Instead of showing surplus and deficit, the production UI will show commercial consumption and industrial consumption. It is imho much more informative than just surplus/deficit, because it also tells what causes said surplus or deficit. 77 | - Disclaimer. I don't yet how to change existing UI, so the titles "Surplus" and "Deficit" will still be there. Sorry. 78 | 79 | ![Resources consumption](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/consumption.png) 80 | 81 | ## Technical 82 | 83 | ### Requirements and Compatibility 84 | - Cities Skylines II v1.1.0f1 or later; check GitHub or Discord if the mod is compatible with the latest game version. 85 | - Gooee framework by Cities2Modding. 86 | - The mod does NOT modify savefiles. 87 | - The mod does NOT modify any game systems. 88 | 89 | ### Known Issues 90 | - Nothing atm. 91 | 92 | ### [Changelog](./CHANGELOG.md) 93 | 94 | ### Support 95 | - Please report bugs and issues on [GitHub](https://github.com/Infixo/CS2-InfoLoom). 96 | - You may also leave comments on [Discord](https://discord.com/channels/1169011184557637825/1198627819475976263). 97 | -------------------------------------------------------------------------------- /Setting.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Colossal; 3 | using Colossal.IO.AssetDatabase; 4 | using Game.Modding; 5 | using Game.Settings; 6 | using Game.UI.InGame; 7 | 8 | namespace InfoLoom; 9 | 10 | [FileLocation(nameof(InfoLoom))] 11 | [SettingsUIGroupOrder(kToggleGroup)] 12 | [SettingsUIShowGroupName(kToggleGroup)] 13 | public class Setting : ModSetting 14 | { 15 | public const string kSection = "Main"; 16 | 17 | public const string kToggleGroup = "Options"; 18 | 19 | public Setting(IMod mod) : base(mod) 20 | { 21 | SetDefaults(); 22 | } 23 | 24 | /// 25 | /// Gets or sets a value indicating whether: Used to force saving of Modsettings if settings would result in empty Json. 26 | /// 27 | [SettingsUIHidden] 28 | public bool _Hidden { get; set; } 29 | 30 | [SettingsUISection(kSection, kToggleGroup)] 31 | public bool SeparateConsumption { get; set; } 32 | 33 | public override void SetDefaults() 34 | { 35 | _Hidden = true; 36 | SeparateConsumption = true; 37 | } 38 | } 39 | 40 | public class LocaleEN : IDictionarySource 41 | { 42 | private readonly Setting m_Setting; 43 | public LocaleEN(Setting setting) 44 | { 45 | m_Setting = setting; 46 | } 47 | public IEnumerable> ReadEntries(IList errors, Dictionary indexCounts) 48 | { 49 | Dictionary dict = new Dictionary() 50 | { 51 | { m_Setting.GetSettingsLocaleID(), $"InfoLoom {Mod.modAsset.version}" }, 52 | { m_Setting.GetOptionTabLocaleID(Setting.kSection), "Main" }, 53 | 54 | { m_Setting.GetOptionGroupLocaleID(Setting.kToggleGroup), "Options" }, 55 | 56 | { m_Setting.GetOptionLabelLocaleID(nameof(Setting.SeparateConsumption)), "Separate consumption" }, 57 | { m_Setting.GetOptionDescLocaleID(nameof(Setting.SeparateConsumption)), "Enables showing commercial and industrial consumption instead of surplus/deficit in the Production UI." }, 58 | }; 59 | 60 | if (Mod.setting.SeparateConsumption) 61 | { 62 | dict.Add("EconomyPanel.PRODUCTION_PAGE_SURPLUS", "Population"); 63 | dict.Add("EconomyPanel.PRODUCTION_PAGE_DEFICIT", "Companies"); 64 | } 65 | 66 | return dict; 67 | } 68 | public void Unload() 69 | { 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Systems/BuildingDemandUISystem.cs: -------------------------------------------------------------------------------- 1 | using Colossal.UI.Binding; 2 | using Game; 3 | using Game.Simulation; 4 | using Game.UI; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using Unity.Collections; 10 | using Unity.Entities; 11 | using UnityEngine.Scripting; 12 | 13 | namespace InfoLoom.Systems; 14 | 15 | [CompilerGenerated] 16 | public partial class BuildingDemandUISystem : UISystemBase 17 | { 18 | // systems to get the data from 19 | private SimulationSystem m_SimulationSystem; 20 | private ResidentialDemandSystem m_ResidentialDemandSystem; 21 | private CommercialDemandSystem m_CommercialDemandSystem; 22 | private IndustrialDemandSystem m_IndustrialDemandSystem; 23 | 24 | // ui bindings 25 | private RawValueBinding m_uiBuildingDemand; 26 | //private RawValueBinding m_uiCompanyDemand; 27 | 28 | // building demands 29 | private NativeArray m_BuildingDemand; 30 | // 0 - low res (ResidentialDemandSystem.BuildingDemand.z) 31 | // 1 - med res (ResidentialDemandSystem.BuildingDemand.y) 32 | // 2 - high res (ResidentialDemandSystem.BuildingDemand.x) 33 | // 3 - commercial (CommercialDemandSystem.m_BuildingDemand) 34 | // 4 - industry (IndustrialDemandSystem.m_IndustrialBuildingDemand) 35 | // 5 - storage (IndustrialDemandSystem.m_StorageBuildingDemand) 36 | // 6 - office (IndustrialDemandSystem.m_OfficeBuildingDemand) 37 | 38 | // company demands 39 | /* 40 | private NativeArray m_CompanyDemand; 41 | // 0 - low res 42 | // 1 - med res 43 | // 2 - high res 44 | // 3 - commercial (CommercialDemandSystem.m_BuildingDemand) 45 | // 4 - industry (IndustrialDemandSystem.m_IndustrialCompanyDemand) 46 | // 5 - storage (IndustrialDemandSystem.m_StorageCompanyDemand) 47 | // 6 - office (IndustrialDemandSystem.m_OfficeCompanyDemand) 48 | */ 49 | 50 | // 240209 Set gameMode to avoid errors in the Editor 51 | public override GameMode gameMode => GameMode.Game; 52 | 53 | //[Preserve] 54 | protected override void OnCreate() 55 | { 56 | base.OnCreate(); 57 | // get access to other systems 58 | m_SimulationSystem = base.World.GetOrCreateSystemManaged(); 59 | m_ResidentialDemandSystem = base.World.GetOrCreateSystemManaged(); 60 | m_CommercialDemandSystem = base.World.GetOrCreateSystemManaged(); 61 | m_IndustrialDemandSystem = base.World.GetOrCreateSystemManaged(); 62 | 63 | // ui binding doe building demand data 64 | AddBinding(m_uiBuildingDemand = new RawValueBinding("cityInfo", "ilBuildingDemand", delegate (IJsonWriter binder) 65 | { 66 | binder.ArrayBegin(m_BuildingDemand.Length); 67 | for (int i = 0; i < m_BuildingDemand.Length; i++) 68 | binder.Write(m_BuildingDemand[i]); 69 | binder.ArrayEnd(); 70 | })); 71 | 72 | // allocate storage 73 | m_BuildingDemand = new NativeArray(7, Allocator.Persistent); 74 | Mod.log.Info("BuildingDemandUISystem created."); 75 | } 76 | 77 | protected override void OnUpdate() 78 | { 79 | if (m_SimulationSystem.frameIndex % 128 != 77) 80 | return; 81 | 82 | //Plugin.Log($"OnUpdate at frame {m_SimulationSystem.frameIndex}"); 83 | base.OnUpdate(); 84 | 85 | m_BuildingDemand[0] = m_ResidentialDemandSystem.buildingDemand.z; 86 | m_BuildingDemand[1] = m_ResidentialDemandSystem.buildingDemand.y; 87 | m_BuildingDemand[2] = m_ResidentialDemandSystem.buildingDemand.x; 88 | m_BuildingDemand[3] = m_CommercialDemandSystem.buildingDemand; 89 | m_BuildingDemand[4] = m_IndustrialDemandSystem.industrialBuildingDemand; 90 | m_BuildingDemand[5] = m_IndustrialDemandSystem.storageBuildingDemand; 91 | m_BuildingDemand[6] = m_IndustrialDemandSystem.officeBuildingDemand; 92 | 93 | m_uiBuildingDemand.Update(); 94 | } 95 | 96 | //[Preserve] 97 | protected override void OnDestroy() 98 | { 99 | m_BuildingDemand.Dispose(); 100 | base.OnDestroy(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Systems/PopulationStructureUISystem.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Colossal.UI.Binding; 3 | using Game.Agents; 4 | using Game.Citizens; 5 | using Game.Common; 6 | using Game.Simulation; 7 | using Game.Tools; 8 | using Unity.Burst; 9 | using Unity.Burst.Intrinsics; 10 | using Unity.Collections; 11 | using Unity.Entities; 12 | using UnityEngine.Scripting; 13 | using Game.UI; 14 | using System.Linq; 15 | using System.Reflection; 16 | using System.Collections.Generic; 17 | using System; 18 | using Game.Buildings; 19 | using Game; 20 | 21 | namespace InfoLoom.Systems; 22 | 23 | // This System is based on PopulationInfoviewUISystem by CO 24 | [CompilerGenerated] 25 | public partial class PopulationStructureUISystem : UISystemBase 26 | { 27 | /// 28 | /// Holds info about population at Age 29 | /// 30 | private struct PopulationAtAgeInfo 31 | { 32 | public int Age; 33 | public int Total; // asserion: Total is a sum of the below parts 34 | public int School1; // elementary school 35 | public int School2; // high school 36 | public int School3; // college 37 | public int School4; // university 38 | public int Work; // working 39 | public int Other; // not working, not student 40 | public PopulationAtAgeInfo(int _age) { Age = _age; } 41 | } 42 | 43 | private static void WriteData(IJsonWriter writer, PopulationAtAgeInfo info) 44 | { 45 | writer.TypeBegin("populationAtAgeInfo"); 46 | writer.PropertyName("age"); 47 | writer.Write(info.Age); 48 | writer.PropertyName("total"); 49 | writer.Write(info.Total); 50 | writer.PropertyName("school1"); 51 | writer.Write(info.School1); 52 | writer.PropertyName("school2"); 53 | writer.Write(info.School2); 54 | writer.PropertyName("school3"); 55 | writer.Write(info.School3); 56 | writer.PropertyName("school4"); 57 | writer.Write(info.School4); 58 | writer.PropertyName("work"); 59 | writer.Write(info.Work); 60 | writer.PropertyName("other"); 61 | writer.Write(info.Other); 62 | writer.TypeEnd(); 63 | } 64 | 65 | //[BurstCompile] 66 | private struct PopulationStructureJob : IJobChunk 67 | { 68 | [ReadOnly] 69 | public EntityTypeHandle m_EntityType; 70 | 71 | [ReadOnly] 72 | public ComponentTypeHandle m_CitizenType; 73 | 74 | [ReadOnly] 75 | public ComponentTypeHandle m_StudentType; 76 | 77 | [ReadOnly] 78 | public ComponentTypeHandle m_WorkerType; 79 | 80 | //[ReadOnly] 81 | //public BufferTypeHandle m_HouseholdCitizenHandle; 82 | 83 | //[ReadOnly] 84 | //public ComponentTypeHandle m_HouseholdType; 85 | 86 | [ReadOnly] 87 | public ComponentTypeHandle m_HealthProblemType; 88 | 89 | [ReadOnly] 90 | public ComponentTypeHandle m_HouseholdMemberType; 91 | 92 | //[ReadOnly] 93 | //public ComponentLookup m_WorkerFromEntity; 94 | 95 | //[ReadOnly] 96 | //public ComponentLookup m_HealthProblems; 97 | 98 | [ReadOnly] 99 | public ComponentLookup m_MovingAways; 100 | 101 | //[ReadOnly] 102 | //public ComponentLookup m_Citizens; 103 | 104 | //[ReadOnly] 105 | //public ComponentLookup m_Students; 106 | 107 | [ReadOnly] 108 | public ComponentLookup m_Households; 109 | 110 | [ReadOnly] 111 | public ComponentLookup m_HomelessHouseholds; 112 | 113 | [ReadOnly] 114 | public ComponentLookup m_PropertyRenters; 115 | 116 | public TimeData m_TimeData; 117 | 118 | //public uint m_UpdateFrameIndex; 119 | 120 | public uint m_SimulationFrame; 121 | 122 | public NativeArray m_Totals; 123 | 124 | public NativeArray m_Results; 125 | 126 | // this job is based on AgingJob 127 | public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) 128 | { 129 | //PopulationStructureUISystem.LogChunk(chunk); 130 | // access data in the chunk 131 | NativeArray entities = chunk.GetNativeArray(m_EntityType); 132 | NativeArray citizenArray = chunk.GetNativeArray(ref m_CitizenType); 133 | NativeArray studentArray = chunk.GetNativeArray(ref m_StudentType); 134 | NativeArray workerArray = chunk.GetNativeArray(ref m_WorkerType); 135 | NativeArray healthProblemArray = chunk.GetNativeArray(ref m_HealthProblemType); 136 | NativeArray householdMemberArray = chunk.GetNativeArray(ref m_HouseholdMemberType); 137 | bool isStudent = chunk.Has(ref m_StudentType); // are there students in this chunk? 138 | bool isWorker = chunk.Has(ref m_WorkerType); // are there workers in this chunk? 139 | bool isHealthProblem = chunk.Has(ref m_HealthProblemType); // for checking dead cims 140 | int day = TimeSystem.GetDay(m_SimulationFrame, m_TimeData); 141 | //Plugin.Log($"day {day} chunk: {entities.Length} entities, {citizens.Length} citizens, {isStudent} {students.Length} students, {isWorker} {workers.Length} workers"); 142 | 143 | for (int i = 0; i < citizenArray.Length; i++) 144 | { 145 | Entity entity = entities[i]; 146 | //Plugin.Log($"{entity}"); 147 | //List list = PopulationStructureUISystem.ListEntityComponents(); 148 | Citizen citizen = citizenArray[i]; 149 | Entity household = householdMemberArray[i].m_Household; 150 | 151 | // skip: non-existing households (technical), and with flag not set 152 | if (!m_Households.HasComponent(household) || m_Households[household].m_Flags == HouseholdFlags.None) 153 | { 154 | continue; 155 | } 156 | 157 | // skip but count dead citizens 158 | if (isHealthProblem && CitizenUtils.IsDead(healthProblemArray[i])) 159 | { 160 | m_Totals[8]++; // dead 161 | continue; 162 | } 163 | 164 | // citizen data 165 | bool isCommuter = ((citizen.m_State & CitizenFlags.Commuter) != CitizenFlags.None); 166 | bool isTourist = ((citizen.m_State & CitizenFlags.Tourist) != CitizenFlags.None); 167 | bool isMovedIn = ((m_Households[household].m_Flags & HouseholdFlags.MovedIn) != HouseholdFlags.None); 168 | 169 | // are components in sync with flags? 170 | //if (isTourist || m_TouristHouseholds.HasComponent(household)) 171 | //Plugin.Log($"{entity.Index}: tourist {isTourist} {isTouristHousehold} {m_Households[household].m_Flags}"); 172 | //if (isCommuter || m_CommuterHouseholds.HasComponent(household)) 173 | //Plugin.Log($"{entity.Index}: commuter {isCommuter} {isCommuterHousehold} {m_Households[household].m_Flags}"); 174 | // Infixo: notes for the future 175 | // Tourists: citizen flag is always set, component sometimes exists, sometimes not 176 | // most of them don't have MovedIn flag set, just Tourist flag in household 177 | // usually Tourist household flag is correlated with TouristHousehold component, but NOT always 178 | // MovedIn tourists DON'T have TouristHousehold component - why??? where do they stay? 179 | // tl;dr CitizenFlags.Tourist is the only reliable way 180 | // Commuters: very similar logic, CitizenFlag is always SET 181 | // CommuterHousehold component is present when household flag is Commuter 182 | // is not present when flag is MovedIn 183 | 184 | // count All, Tourists and Commuters 185 | m_Totals[0]++; // all 186 | if (isTourist) m_Totals[2]++; // tourists 187 | else if (isCommuter) m_Totals[3]++; // commuters 188 | if (isTourist || isCommuter) 189 | continue; // not local, go for the next 190 | 191 | // skip but count citizens moving away 192 | // 231230 moved after Tourist & Commuter check, so it will show only Locals that are moving away (more important info) 193 | if (m_MovingAways.HasComponent(household)) 194 | { 195 | m_Totals[7]++; // moving aways 196 | continue; 197 | } 198 | 199 | // finally, count local population 200 | if (isMovedIn) m_Totals[1]++; // locals; game Population is: MovedIn, not Tourist & Commuter, not dead 201 | else 202 | { 203 | // skip glitches e.g. there is a case with Tourist household, but citizen is NOT Tourist 204 | //Plugin.Log($"Warning: unknown citizen {citizen.m_State} household {m_Households[household].m_Flags}"); 205 | continue; 206 | } 207 | 208 | // homeless citizens, already MovingAway are not included (they are gone, anyway) 209 | if (m_HomelessHouseholds.HasComponent(household) || !m_PropertyRenters.HasComponent(household)) 210 | { 211 | m_Totals[9]++; // homeless 212 | } 213 | 214 | // get age 215 | int ageInDays = day - citizen.m_BirthDay; 216 | if (ageInDays > m_Totals[6]) m_Totals[6] = ageInDays; // oldest cim 217 | //ageInDays = ageInDays/2; // INFIXO: TODO 218 | if (ageInDays>109) ageInDays = 109; 219 | PopulationAtAgeInfo info = m_Results[ageInDays]; 220 | // process at-age info 221 | info.Total++; 222 | // process a student 223 | if (isStudent) 224 | { 225 | m_Totals[4]++; // students 226 | Game.Citizens.Student student = studentArray[i]; 227 | // check what school level 228 | switch (studentArray[i].m_Level) 229 | { 230 | case 1: info.School1++; break; 231 | case 2: info.School2++; break; 232 | case 3: info.School3++; break; 233 | case 4: info.School4++; break; 234 | default: 235 | #if DEBUG 236 | Mod.log.Info($"WARNING: incorrect school level, {studentArray[i].m_Level}"); 237 | #endif 238 | break; 239 | } 240 | } 241 | // process a worker 242 | if (isWorker) 243 | { 244 | // we can check the level here? what is it actually? 245 | m_Totals[5]++; // workers 246 | info.Work++; 247 | } 248 | // not a student, not a worker 249 | if (!isStudent && !isWorker) 250 | { 251 | info.Other++; 252 | } 253 | m_Results[ageInDays] = info; 254 | //Plugin.Log($"{ageInDays} yo, school {isStudent}, work {isWorker}"); 255 | } 256 | } 257 | 258 | void IJobChunk.Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) 259 | { 260 | Execute(in chunk, unfilteredChunkIndex, useEnabledMask, in chunkEnabledMask); 261 | } 262 | } 263 | 264 | private struct TypeHandle 265 | { 266 | [ReadOnly] 267 | public EntityTypeHandle __Unity_Entities_Entity_TypeHandle; 268 | 269 | [ReadOnly] 270 | public ComponentTypeHandle __Game_Citizens_Citizen_RO_ComponentTypeHandle; 271 | 272 | [ReadOnly] 273 | public ComponentTypeHandle __Game_Citizens_Student_RO_ComponentTypeHandle; 274 | 275 | [ReadOnly] 276 | public ComponentTypeHandle __Game_Citizens_Worker_RO_ComponentTypeHandle; 277 | 278 | [ReadOnly] 279 | public ComponentTypeHandle __Game_Citizens_HealthProblem_RO_ComponentTypeHandle; 280 | 281 | [ReadOnly] 282 | public ComponentTypeHandle __Game_Citizens_HouseholdMember_RO_ComponentTypeHandle; 283 | 284 | //public BufferTypeHandle __Game_Citizens_HouseholdCitizen_RW_BufferTypeHandle; 285 | 286 | //public ComponentTypeHandle __Game_Citizens_Household_RW_ComponentTypeHandle; 287 | 288 | //[ReadOnly] 289 | //public ComponentLookup __Game_Citizens_Worker_RO_ComponentLookup; 290 | 291 | //[ReadOnly] 292 | //public ComponentLookup __Game_Citizens_Citizen_RO_ComponentLookup; 293 | 294 | //[ReadOnly] 295 | //public ComponentLookup __Game_Citizens_HealthProblem_RO_ComponentLookup; 296 | 297 | //[ReadOnly] 298 | //public ComponentLookup __Game_Citizens_Student_RO_ComponentLookup; 299 | 300 | [ReadOnly] 301 | public ComponentLookup __Game_Agents_MovingAway_RO_ComponentLookup; 302 | 303 | [ReadOnly] 304 | public ComponentLookup __Game_Buildings_PropertyRenter_RO_ComponentLookup; 305 | 306 | [ReadOnly] 307 | public ComponentLookup __Game_Citizens_Household_RO_ComponentLookup; 308 | 309 | [ReadOnly] 310 | public ComponentLookup __Game_Citizens_HomelessHousehold_RO_ComponentLookup; 311 | 312 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 313 | public void __AssignHandles(ref SystemState state) 314 | { 315 | // components 316 | __Unity_Entities_Entity_TypeHandle = state.GetEntityTypeHandle(); 317 | __Game_Citizens_Citizen_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 318 | __Game_Citizens_Student_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 319 | __Game_Citizens_Worker_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 320 | __Game_Citizens_HealthProblem_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 321 | __Game_Citizens_HouseholdMember_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 322 | 323 | // lookups 324 | __Game_Agents_MovingAway_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 325 | __Game_Buildings_PropertyRenter_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 326 | __Game_Citizens_Household_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 327 | __Game_Citizens_HomelessHousehold_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 328 | } 329 | } 330 | 331 | private const string kGroup = "populationInfo"; 332 | 333 | //private CitySystem m_CitySystem; 334 | 335 | private SimulationSystem m_SimulationSystem; 336 | 337 | private RawValueBinding m_uiTotals; 338 | 339 | private RawValueBinding m_uiResults; 340 | 341 | private EntityQuery m_TimeDataQuery; 342 | 343 | //private EntityQuery m_HouseholdQuery; 344 | 345 | private EntityQuery m_CitizenQuery; 346 | 347 | //private EntityQuery m_WorkProviderQuery; 348 | 349 | //private EntityQuery m_WorkProviderModifiedQuery; 350 | 351 | private NativeArray m_Totals; // final results, totals at city level 352 | // 0 - num citizens in the city 0 = 1+2+3 353 | // 1 - num locals 354 | // 2 - num tourists 355 | // 3 - num commuters 356 | // 4 - num students (in locals) 4 <= 1 357 | // 5 - num workers (in locals) 5 <= 1 358 | // 6 - oldest cim 359 | // 7 - moving aways 360 | // 8 - dead cims 361 | // 9 - homeless citizens 362 | 363 | private NativeArray m_Results; // final results, will be filled via jobs and then written as output 364 | 365 | private TypeHandle __TypeHandle; 366 | 367 | // 240209 Set gameMode to avoid errors in the Editor 368 | public override GameMode gameMode => GameMode.Game; 369 | 370 | //[Preserve] 371 | protected override void OnCreate() 372 | { 373 | base.OnCreate(); 374 | 375 | //m_CitySystem = base.World.GetOrCreateSystemManaged(); 376 | m_SimulationSystem = base.World.GetOrCreateSystemManaged(); 377 | m_TimeDataQuery = GetEntityQuery(ComponentType.ReadOnly()); 378 | 379 | m_CitizenQuery = GetEntityQuery(new EntityQueryDesc 380 | { 381 | All = new ComponentType[1] { ComponentType.ReadOnly() }, 382 | None = new ComponentType[2] { ComponentType.ReadOnly(), ComponentType.ReadOnly() } 383 | }); 384 | RequireForUpdate(m_CitizenQuery); 385 | 386 | AddBinding(m_uiTotals = new RawValueBinding(kGroup, "structureTotals", delegate (IJsonWriter binder) 387 | { 388 | // city level info 389 | binder.ArrayBegin(m_Totals.Length); 390 | for (int i = 0; i < m_Totals.Length; i++) 391 | binder.Write(m_Totals[i]); 392 | binder.ArrayEnd(); 393 | })); 394 | 395 | AddBinding(m_uiResults = new RawValueBinding(kGroup, "structureDetails", delegate(IJsonWriter binder) 396 | { 397 | binder.ArrayBegin(m_Results.Length); 398 | for (int i = 0; i < m_Results.Length; i++) 399 | { 400 | WriteData(binder, m_Results[i]); 401 | } 402 | binder.ArrayEnd(); 403 | })); 404 | 405 | // TEST 406 | AddUpdateBinding(new GetterValueBinding(kGroup, "oldest_citizen", () => { 407 | return m_Totals[6]; 408 | })); 409 | 410 | // allocate memory for results 411 | m_Totals = new NativeArray(10, Allocator.Persistent); 412 | m_Results = new NativeArray(110, Allocator.Persistent); // INFIXO: TODO 413 | Mod.log.Info("PopulationStructureUISystem created."); 414 | } 415 | 416 | //[Preserve] 417 | protected override void OnDestroy() 418 | { 419 | m_Totals.Dispose(); 420 | m_Results.Dispose(); 421 | base.OnDestroy(); 422 | } 423 | 424 | protected override void OnUpdate() 425 | { 426 | if (m_SimulationSystem.frameIndex % 128 != 44) 427 | return; 428 | 429 | //Plugin.Log($"OnUpdate at frame {m_SimulationSystem.frameIndex}"); 430 | base.OnUpdate(); 431 | ResetResults(); 432 | 433 | // code based on AgingJob 434 | 435 | //uint updateFrame = SimulationUtils.GetUpdateFrame(m_SimulationSystem.frameIndex, 1, 16); 436 | //__TypeHandle.__Game_Citizens_TravelPurpose_RO_ComponentLookup.Update(ref base.CheckedStateRef); 437 | //__TypeHandle.__Game_Buildings_Student_RO_BufferLookup.Update(ref base.CheckedStateRef); 438 | //__TypeHandle.__Game_Simulation_UpdateFrame_SharedComponentTypeHandle.Update(ref base.CheckedStateRef); 439 | __TypeHandle.__Unity_Entities_Entity_TypeHandle.Update(ref base.CheckedStateRef); 440 | __TypeHandle.__Game_Citizens_Citizen_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 441 | __TypeHandle.__Game_Citizens_Student_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 442 | __TypeHandle.__Game_Citizens_Worker_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 443 | __TypeHandle.__Game_Citizens_HealthProblem_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 444 | 445 | __TypeHandle.__Game_Agents_MovingAway_RO_ComponentLookup.Update(ref base.CheckedStateRef); 446 | __TypeHandle.__Game_Buildings_PropertyRenter_RO_ComponentLookup.Update(ref base.CheckedStateRef); 447 | __TypeHandle.__Game_Citizens_HouseholdMember_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 448 | __TypeHandle.__Game_Citizens_Household_RO_ComponentLookup.Update(ref base.CheckedStateRef); 449 | __TypeHandle.__Game_Citizens_HomelessHousehold_RO_ComponentLookup.Update(ref base.CheckedStateRef); 450 | 451 | PopulationStructureJob structureJob = default(PopulationStructureJob); 452 | structureJob.m_EntityType = __TypeHandle.__Unity_Entities_Entity_TypeHandle; 453 | structureJob.m_CitizenType = __TypeHandle.__Game_Citizens_Citizen_RO_ComponentTypeHandle; 454 | structureJob.m_StudentType = __TypeHandle.__Game_Citizens_Student_RO_ComponentTypeHandle; 455 | structureJob.m_WorkerType = __TypeHandle.__Game_Citizens_Worker_RO_ComponentTypeHandle; 456 | structureJob.m_MovingAways = __TypeHandle.__Game_Agents_MovingAway_RO_ComponentLookup; 457 | structureJob.m_HealthProblemType = __TypeHandle.__Game_Citizens_HealthProblem_RO_ComponentTypeHandle; 458 | structureJob.m_HouseholdMemberType = __TypeHandle.__Game_Citizens_HouseholdMember_RO_ComponentTypeHandle; 459 | structureJob.m_Households = __TypeHandle.__Game_Citizens_Household_RO_ComponentLookup; 460 | structureJob.m_PropertyRenters = __TypeHandle.__Game_Buildings_PropertyRenter_RO_ComponentLookup; 461 | structureJob.m_HomelessHouseholds = __TypeHandle.__Game_Citizens_HomelessHousehold_RO_ComponentLookup; 462 | //structureJob.m_UpdateFrameType = __TypeHandle.__Game_Simulation_UpdateFrame_SharedComponentTypeHandle; 463 | //structureJob.m_Students = __TypeHandle.__Game_Buildings_Student_RO_BufferLookup; 464 | //structureJob.m_Purposes = __TypeHandle.__Game_Citizens_TravelPurpose_RO_ComponentLookup; 465 | //structureJob.m_MoveFromHomeQueue = m_MoveFromHomeQueue.AsParallelWriter(); 466 | structureJob.m_SimulationFrame = m_SimulationSystem.frameIndex; 467 | structureJob.m_TimeData = m_TimeDataQuery.GetSingleton(); 468 | //structureJob.m_UpdateFrameIndex = updateFrame; 469 | //structureJob.m_CommandBuffer = m_EndFrameBarrier.CreateCommandBuffer().AsParallelWriter(); 470 | //structureJob.m_DebugAgeAllCitizens = s_DebugAgeAllCitizens; 471 | //PopulationStructureJob jobData = structureJob; 472 | //base.Dependency = JobChunkExtensions.ScheduleParallel(jobData, m_CitizenGroup, base.Dependency); 473 | //m_EndFrameBarrier.AddJobHandleForProducer(base.Dependency); // Infixo: frame barrier is not used because we wait to complete the job 474 | structureJob.m_Totals = m_Totals; 475 | structureJob.m_Results = m_Results; 476 | JobChunkExtensions.Schedule(structureJob, m_CitizenQuery, base.Dependency).Complete(); 477 | 478 | //int ageInDays = m_Results[2]; 479 | //int num2 = m_Results[1]; 480 | //int num3 = m_Results[4]; 481 | //int newValue = m_Results[5]; 482 | //int num4 = num2 + ageInDays - m_Results[6]; 483 | //float newValue2 = (((float)num4 > 0f) ? ((float)(num4 - num3) / (float)num4 * 100f) : 0f); 484 | //m_Jobs.Update(newValue); 485 | //m_Employed.Update(num3); 486 | //m_Unemployment.Update(newValue2); 487 | //Population componentData = base.EntityManager.GetComponentData(m_CitySystem.City); 488 | //m_Population.Update(componentData.m_Population); 489 | 490 | /* DEBUG 491 | Plugin.Log($"results: {m_Totals[0]} {m_Totals[1]} {m_Totals[2]} {m_Totals[3]} students {m_Totals[4]} workers {m_Totals[5]}"); 492 | for (int i = 0; i < m_Results.Length; i++) 493 | { 494 | PopulationAtAgeInfo info = m_Results[i]; 495 | Plugin.Log($"...[{i}]: {info.Age} {info.Total} students {info.School1} {info.School2} {info.School3} {info.School4} workers {info.Work} other {info.Other}"); 496 | } 497 | */ 498 | 499 | m_uiTotals.Update(); 500 | m_uiResults.Update(); 501 | 502 | //InspectComponentsInQuery(m_CitizenQuery); 503 | 504 | /* 505 | //UpdateStatistics(); 506 | //Plugin.Log("jobs",true); 507 | if (!dumped) 508 | { 509 | Plugin.Log("Chunks with Citizen component"); 510 | Entities.WithAll().ForEach((ArchetypeChunk chunk) => 511 | { 512 | Plugin.Log("CHUNK START"); 513 | // Iterate over the component types in the chunk 514 | var componentTypes = chunk.Archetype.GetComponentTypes(); 515 | 516 | // Log the component types 517 | foreach (var componentType in componentTypes) 518 | { 519 | Plugin.Log($"Component Type in Chunk: {componentType.GetManagedType()}"); 520 | } 521 | 522 | // Your processing logic here 523 | }).Schedule(); 524 | dumped = true; 525 | } 526 | */ 527 | } 528 | 529 | private void ResetResults() 530 | { 531 | for (int i = 0; i < m_Totals.Length; i++) 532 | { 533 | m_Totals[i] = 0; 534 | } 535 | for (int i = 0; i < m_Results.Length; i++) 536 | { 537 | m_Results[i] = new PopulationAtAgeInfo(i); 538 | } 539 | //Plugin.Log("reset",true); 540 | } 541 | 542 | /* 543 | private void UpdateAgeData(IJsonWriter binder) 544 | { 545 | int children = m_Results[0]; 546 | int teens = m_Results[1]; 547 | int adults = m_Results[2]; 548 | int seniors = m_Results[3]; 549 | UpdateAgeDataBinding(binder, children, teens, adults, seniors); 550 | } 551 | */ 552 | /* 553 | private static void UpdateAgeDataBinding(IJsonWriter binder, NativeArray m_Results) 554 | { 555 | binder.TypeBegin("populationStructure"); 556 | binder.PropertyName("values"); 557 | binder.ArrayBegin(m_Results.Length); 558 | 559 | for (int i = 0; i < m_Results.Length; i++) 560 | { 561 | WriteData(binder, m_Results[i]); 562 | } 563 | 564 | binder.ArrayEnd(); 565 | binder.TypeEnd(); 566 | } 567 | */ 568 | /* Infixo: not used 569 | private void UpdateStatistics() 570 | { 571 | m_BirthRate.Update(m_CityStatisticsSystem.GetStatisticValue(StatisticType.BirthRate)); 572 | m_DeathRate.Update(m_CityStatisticsSystem.GetStatisticValue(StatisticType.DeathRate)); 573 | m_MovedIn.Update(m_CityStatisticsSystem.GetStatisticValue(StatisticType.CitizensMovedIn)); 574 | m_MovedAway.Update(m_CityStatisticsSystem.GetStatisticValue(StatisticType.CitizensMovedAway)); 575 | } 576 | */ 577 | 578 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 579 | private void __AssignQueries(ref SystemState state) 580 | { 581 | } 582 | 583 | protected override void OnCreateForCompiler() 584 | { 585 | base.OnCreateForCompiler(); 586 | __AssignQueries(ref base.CheckedStateRef); 587 | __TypeHandle.__AssignHandles(ref base.CheckedStateRef); 588 | } 589 | 590 | //[Preserve] 591 | public PopulationStructureUISystem() 592 | { 593 | } 594 | 595 | public static void LogChunk(in ArchetypeChunk chunk) 596 | { 597 | var componentTypes = chunk.Archetype.GetComponentTypes(); 598 | Mod.log.Info($"chunk: {chunk.Count}, {string.Join(", ", componentTypes.Select(ct => ct.GetType().GetTypeInfo().FullName ))}"); 599 | } 600 | 601 | public string[] ListEntityComponents(Entity entity) 602 | { 603 | var componentTypes = new List(); 604 | 605 | if (!EntityManager.Exists(entity)) 606 | throw new ArgumentException("Entity does not exist."); 607 | 608 | //using (NativeArray types = EntityManager.GetComponentTypes(entity, Allocator.Temp)) 609 | //{ 610 | //foreach (var type in types) 611 | //{ 612 | //componentTypes.Add(type); 613 | //} 614 | //} 615 | 616 | NativeArray NativeArray = EntityManager.GetComponentTypes(entity, Allocator.Temp); 617 | string[] ToReturn = NativeArray.Select(T => T.GetManagedType().Name).ToArray(); 618 | NativeArray.Dispose(); 619 | return ToReturn; 620 | 621 | //return componentTypes; 622 | } 623 | 624 | public void InspectComponentsInQuery(EntityQuery query) 625 | { 626 | Dictionary CompDict = new Dictionary(); 627 | NativeArray entities = m_CitizenQuery.ToEntityArray(Allocator.Temp); 628 | for (int i = 0; i < entities.Length; i++) 629 | { 630 | Entity entity = entities[i]; 631 | string[] comps = ListEntityComponents(entity); 632 | foreach (string comp in comps) 633 | { 634 | if (CompDict.ContainsKey(comp)) CompDict[comp]++; 635 | else CompDict.Add(comp, 1); 636 | } 637 | } 638 | entities.Dispose(); 639 | // show the dictionary 640 | Mod.log.Info("=== Components in selected chunks ==="); 641 | foreach (var pair in CompDict) 642 | { 643 | Mod.log.Info($"{pair.Key} {pair.Value}"); 644 | } 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /Systems/WorkplacesInfoLoomUISystem.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Colossal.UI.Binding; 3 | using Game.Buildings; 4 | using Game.Common; 5 | using Game.Companies; 6 | using Game.Prefabs; 7 | using Game.Tools; 8 | using Unity.Burst; 9 | using Unity.Burst.Intrinsics; 10 | using Unity.Collections; 11 | using Unity.Entities; 12 | using UnityEngine.Scripting; 13 | using Game; 14 | using Game.UI; 15 | using Game.Simulation; // TODO: use UIUpdateState and Advance() eventully... 16 | using Game.UI.InGame; // EmploymentData TODO: remove when not used 17 | using Game.Economy; // Resources 18 | using Game.Citizens; 19 | 20 | namespace InfoLoom.Systems; 21 | 22 | // This system is based on game's WorkplacesInfoviewUISystem 23 | 24 | [CompilerGenerated] 25 | public partial class WorkplacesInfoLoomUISystem : UISystemBase 26 | { 27 | private struct WorkplacesAtLevelInfo 28 | { 29 | public int Level; 30 | public int Total; // assertions: Total=Service+Commercial+Industry+Office, Total=Employee+Empty 31 | public int Service; // jobs in city services 32 | public int Commercial; // jobs in commercial zones 33 | public int Leisure; // jobs in commercial zones 34 | public int Extractor; // jobs in industrial zones 35 | public int Industrial; // jobs in industrial zones 36 | public int Office; // jobs in office zones 37 | public int Employee; // employees 38 | public int Open; // open positions 39 | public int Commuter; // commuters 40 | public WorkplacesAtLevelInfo(int _level) { Level = _level; } 41 | } 42 | 43 | private static void WriteData(IJsonWriter writer, WorkplacesAtLevelInfo info) 44 | { 45 | writer.TypeBegin("workplacesAtLevelInfo"); 46 | writer.PropertyName("level"); 47 | writer.Write(info.Level); 48 | writer.PropertyName("total"); 49 | writer.Write(info.Total); 50 | writer.PropertyName("service"); 51 | writer.Write(info.Service); 52 | writer.PropertyName("commercial"); 53 | writer.Write(info.Commercial); 54 | writer.PropertyName("leisure"); 55 | writer.Write(info.Leisure); 56 | writer.PropertyName("extractor"); 57 | writer.Write(info.Extractor); 58 | writer.PropertyName("industry"); 59 | writer.Write(info.Industrial); 60 | writer.PropertyName("office"); 61 | writer.Write(info.Office); 62 | writer.PropertyName("employee"); 63 | writer.Write(info.Employee); 64 | writer.PropertyName("open"); 65 | writer.Write(info.Open); 66 | writer.PropertyName("commuter"); 67 | writer.Write(info.Commuter); 68 | writer.TypeEnd(); 69 | } 70 | 71 | /* not used 72 | private enum Result 73 | { 74 | Workplaces, 75 | Employees, 76 | Count 77 | } 78 | */ 79 | 80 | //[BurstCompile] 81 | private struct CalculateWorkplaceDataJob : IJobChunk 82 | { 83 | [ReadOnly] 84 | public EntityTypeHandle m_EntityHandle; 85 | 86 | [ReadOnly] 87 | public BufferTypeHandle m_EmployeeHandle; 88 | 89 | //[ReadOnly] 90 | //public BufferTypeHandle m_ResourcesHandle; 91 | 92 | [ReadOnly] 93 | public ComponentTypeHandle m_WorkProviderHandle; 94 | 95 | [ReadOnly] 96 | public ComponentTypeHandle m_PropertyRenterHandle; 97 | 98 | [ReadOnly] 99 | public ComponentTypeHandle m_PrefabRefHandle; 100 | 101 | [ReadOnly] 102 | public ComponentTypeHandle m_IndustrialCompanyHandle; 103 | 104 | [ReadOnly] 105 | public ComponentTypeHandle m_ExtractorCompanyHandle; 106 | 107 | [ReadOnly] 108 | public ComponentTypeHandle m_CommercialCompanyHandle; 109 | 110 | [ReadOnly] 111 | public ComponentLookup m_PrefabRefFromEntity; 112 | 113 | [ReadOnly] 114 | public ComponentLookup m_WorkplaceDataFromEntity; 115 | 116 | [ReadOnly] 117 | public ComponentLookup m_SpawnableBuildingFromEntity; 118 | 119 | [ReadOnly] 120 | public ComponentLookup m_IndustrialProcessDataFromEntity; 121 | 122 | [ReadOnly] 123 | public ComponentLookup m_CitizenFromEntity; 124 | 125 | //[ReadOnly] 126 | //public ComponentLookup m_ResourceDatas; // TODO: this is for future use to access resource prefab data 127 | 128 | //[ReadOnly] 129 | //public ResourcePrefabs m_ResourcePrefabs; // TODO: this is for future use to access resource prefab data 130 | 131 | //public NativeArray m_IntResults; 132 | 133 | //public NativeArray m_EmploymentDataResults; 134 | 135 | public NativeArray m_Results; 136 | 137 | // notes on offices - they are detected usually by resource weight :( 138 | //ResourceData resourceData2 = m_ResourceDatas[m_ResourcePrefabs[iterator.resource]]; 139 | //bool flag4 = resourceData2.m_Weight == 0f; 140 | 141 | public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) 142 | { 143 | NativeArray nativeArray = chunk.GetNativeArray(m_EntityHandle); 144 | NativeArray nativeArray2 = chunk.GetNativeArray(ref m_PrefabRefHandle); 145 | NativeArray nativeArray3 = chunk.GetNativeArray(ref m_PropertyRenterHandle); 146 | NativeArray nativeArray4 = chunk.GetNativeArray(ref m_WorkProviderHandle); 147 | BufferAccessor bufferAccessor = chunk.GetBufferAccessor(ref m_EmployeeHandle); 148 | //BufferAccessor bufferResources = chunk.GetBufferAccessor(ref m_ResourcesHandle); 149 | bool isExtractor = chunk.Has(ref m_ExtractorCompanyHandle); 150 | bool isIndustrial = chunk.Has(ref m_IndustrialCompanyHandle); 151 | bool isCommercial = chunk.Has(ref m_CommercialCompanyHandle); 152 | bool isService = !(isIndustrial || isCommercial); 153 | WorkplacesAtLevelInfo count = m_Results[6]; 154 | for (int i = 0; i < nativeArray.Length; i++) 155 | { 156 | int buildingLevel = 1; 157 | WorkProvider workProvider = nativeArray4[i]; 158 | DynamicBuffer employees = bufferAccessor[i]; 159 | PrefabRef prefabRef = nativeArray2[i]; 160 | WorkplaceData workplaceData = m_WorkplaceDataFromEntity[prefabRef.m_Prefab]; 161 | 162 | if (chunk.Has(ref m_PropertyRenterHandle)) 163 | { 164 | buildingLevel = m_PrefabRefFromEntity.TryGetComponent(nativeArray3[i].m_Property, out PrefabRef prefabRef2) 165 | && m_SpawnableBuildingFromEntity.TryGetComponent(prefabRef2.m_Prefab, out SpawnableBuildingData componentData2) ? (int)componentData2.m_Level : 1; 166 | } 167 | // this holds workplaces for each level 168 | EmploymentData workplacesData = EmploymentData.GetWorkplacesData(workProvider.m_MaxWorkers, buildingLevel, workplaceData.m_Complexity); 169 | // this holds employees for each level 170 | EmploymentData employeesData = EmploymentData.GetEmployeesData(employees, workplacesData.total - employees.Length); 171 | //m_IntResults[0] += workplacesData.total; 172 | //m_IntResults[1] += employees.Length; 173 | //m_EmploymentDataResults[0] += workplacesData; 174 | //m_EmploymentDataResults[1] += employeesData; 175 | 176 | //Plugin.Log($"company {isCommercial}/{isMoney}/{isIndustrial}/{isExtractor}/{isOffice}:{lastRes} " + 177 | // $"{workplacesData.uneducated} {workplacesData.poorlyEducated} {workplacesData.educated} {workplacesData.wellEducated} {workplacesData.highlyEducated}"); 178 | 179 | // 240112, fix for incorrect counting of Leisure and Offices 180 | // Previous version with resource analysis was incorrect - last resource could also be an Upkeep resource like Timber or Petrochemicals 181 | // This approach uses IndustrialProcessData and checks Output resource 182 | bool isOffice = false; 183 | bool isLeisure = false; 184 | //string processTxt = ""; // debug 185 | if (m_IndustrialProcessDataFromEntity.HasComponent(prefabRef.m_Prefab)) 186 | { 187 | IndustrialProcessData process = m_IndustrialProcessDataFromEntity[prefabRef.m_Prefab]; 188 | Resource outputRes = process.m_Output.m_Resource; 189 | isLeisure = (outputRes & (Resource.Meals | Resource.Entertainment | Resource.Recreation | Resource.Lodging)) != Resource.NoResource; 190 | isOffice = (outputRes & (Resource.Software | Resource.Telecom | Resource.Financial | Resource.Media)) != Resource.NoResource; 191 | //processTxt = $"{process.m_Input1.m_Resource}+{process.m_Input2.m_Resource}={process.m_Output.m_Resource}"; // debug 192 | } 193 | 194 | // 240113 Count Commuters among Employees 195 | int[] commuters = new int[5]; // by level 196 | for (int k = 0; k < employees.Length; k++) 197 | { 198 | Entity worker = employees[k].m_Worker; 199 | if (m_CitizenFromEntity.HasComponent(worker)) 200 | { 201 | Citizen citizen = m_CitizenFromEntity[worker]; 202 | if ((citizen.m_State & CitizenFlags.Commuter) != CitizenFlags.None) 203 | commuters[employees[k].m_Level]++; 204 | } 205 | } 206 | 207 | // debug 208 | //string resTxt = ""; 209 | //for (int r = 0; r < resources.Length; r++) 210 | //resTxt += resources[r].m_Resource + "|"; 211 | //Plugin.Log($"[{processTxt}] {resTxt} off {isOffice} lei {isLeisure}"); 212 | 213 | // Work with a local variable to avoid CS0206 error 214 | WorkplacesAtLevelInfo ProcessLevel(WorkplacesAtLevelInfo info, int workplaces, int employees, int commuters) 215 | { 216 | info.Total += workplaces; 217 | if (isService) info.Service += workplaces; 218 | if (isCommercial) 219 | { 220 | if (isLeisure) info.Leisure += workplaces; 221 | else info.Commercial += workplaces; 222 | } 223 | if (isIndustrial) 224 | { 225 | if (isExtractor) info.Extractor += workplaces; 226 | else if(isOffice) info.Office += workplaces; 227 | else info.Industrial += workplaces; 228 | } 229 | info.Employee += employees; 230 | info.Open += workplaces - employees; 231 | info.Commuter += commuters; 232 | return info; 233 | } 234 | 235 | // uneducated 236 | m_Results[0] = ProcessLevel(m_Results[0], workplacesData.uneducated, employeesData.uneducated, commuters[0]); 237 | // poorlyEducated 238 | m_Results[1] = ProcessLevel(m_Results[1], workplacesData.poorlyEducated, employeesData.poorlyEducated, commuters[1]); 239 | // educated 240 | m_Results[2] = ProcessLevel(m_Results[2], workplacesData.educated, employeesData.educated, commuters[2]); 241 | // wellEducated 242 | m_Results[3] = ProcessLevel(m_Results[3], workplacesData.wellEducated, employeesData.wellEducated, commuters[3]); 243 | // highlyEducated 244 | m_Results[4] = ProcessLevel(m_Results[4], workplacesData.highlyEducated, employeesData.highlyEducated, commuters[4]); 245 | 246 | // 240114 Count work providers 247 | count.Total++; 248 | if (isService) count.Service++; 249 | if (isCommercial) 250 | { 251 | if (isLeisure) count.Leisure++; 252 | else count.Commercial++; 253 | } 254 | if (isIndustrial) 255 | { 256 | if (isExtractor) count.Extractor++; 257 | else if (isOffice) count.Office++; 258 | else count.Industrial++; 259 | } 260 | m_Results[6] = count; 261 | } 262 | } 263 | 264 | void IJobChunk.Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) 265 | { 266 | Execute(in chunk, unfilteredChunkIndex, useEnabledMask, in chunkEnabledMask); 267 | } 268 | } 269 | 270 | private struct TypeHandle 271 | { 272 | [ReadOnly] 273 | public EntityTypeHandle __Unity_Entities_Entity_TypeHandle; 274 | 275 | [ReadOnly] 276 | public BufferTypeHandle __Game_Companies_Employee_RO_BufferTypeHandle; 277 | 278 | [ReadOnly] 279 | public BufferTypeHandle __Game_Economy_Resources_RO_BufferTypeHandle; 280 | 281 | [ReadOnly] 282 | public ComponentTypeHandle __Game_Companies_WorkProvider_RO_ComponentTypeHandle; 283 | 284 | [ReadOnly] 285 | public ComponentTypeHandle __Game_Buildings_PropertyRenter_RO_ComponentTypeHandle; 286 | 287 | [ReadOnly] 288 | public ComponentTypeHandle __Game_Companies_ExtractorCompany_RO_ComponentTypeHandle; 289 | 290 | [ReadOnly] 291 | public ComponentTypeHandle __Game_Companies_IndustrialCompany_RO_ComponentTypeHandle; 292 | 293 | [ReadOnly] 294 | public ComponentTypeHandle __Game_Companies_CommercialCompany_RO_ComponentTypeHandle; 295 | 296 | [ReadOnly] 297 | public ComponentTypeHandle __Game_Prefabs_PrefabRef_RO_ComponentTypeHandle; 298 | 299 | [ReadOnly] 300 | public ComponentLookup __Game_Prefabs_PrefabRef_RO_ComponentLookup; 301 | 302 | [ReadOnly] 303 | public ComponentLookup __Game_Prefabs_WorkplaceData_RO_ComponentLookup; 304 | 305 | [ReadOnly] 306 | public ComponentLookup __Game_Prefabs_SpawnableBuildingData_RO_ComponentLookup; 307 | 308 | [ReadOnly] 309 | public ComponentLookup __Game_Prefabs_IndustrialProcessData_RO_ComponentLookup; 310 | 311 | [ReadOnly] 312 | public ComponentLookup __Game_Citizens_Citizen_RO_ComponentLookup; 313 | 314 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 315 | public void __AssignHandles(ref SystemState state) 316 | { 317 | __Unity_Entities_Entity_TypeHandle = state.GetEntityTypeHandle(); 318 | __Game_Companies_Employee_RO_BufferTypeHandle = state.GetBufferTypeHandle(isReadOnly: true); 319 | __Game_Economy_Resources_RO_BufferTypeHandle = state.GetBufferTypeHandle(isReadOnly: true); 320 | __Game_Companies_WorkProvider_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 321 | __Game_Companies_ExtractorCompany_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 322 | __Game_Companies_IndustrialCompany_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 323 | __Game_Companies_CommercialCompany_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 324 | __Game_Buildings_PropertyRenter_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 325 | __Game_Prefabs_PrefabRef_RO_ComponentTypeHandle = state.GetComponentTypeHandle(isReadOnly: true); 326 | __Game_Prefabs_PrefabRef_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 327 | __Game_Prefabs_WorkplaceData_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 328 | __Game_Prefabs_SpawnableBuildingData_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 329 | __Game_Prefabs_IndustrialProcessData_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 330 | __Game_Citizens_Citizen_RO_ComponentLookup = state.GetComponentLookup(isReadOnly: true); 331 | } 332 | } 333 | 334 | private const string kGroup = "workplaces"; 335 | 336 | private SimulationSystem m_SimulationSystem; 337 | 338 | //private ResourceSystem m_ResourceSystem; // TODO: for future 339 | 340 | private EntityQuery m_WorkplaceQuery; 341 | 342 | //private EntityQuery m_WorkplaceModifiedQuery; 343 | 344 | //private GetterValueBinding m_EmployeesData; 345 | 346 | //private GetterValueBinding m_WorkplacesData; 347 | 348 | //private GetterValueBinding m_Workplaces; 349 | 350 | //private GetterValueBinding m_Workers; 351 | 352 | private RawValueBinding m_uiResults; 353 | 354 | //private NativeArray m_IntResults; 355 | 356 | //private NativeArray m_EmploymentDataResults; 357 | 358 | // 0-4: education levels, 5: totals, 6: number of work providers (mostly companies) 359 | private NativeArray m_Results; 360 | 361 | private TypeHandle __TypeHandle; 362 | 363 | /* not used 364 | protected override bool Active 365 | { 366 | get 367 | { 368 | if (!base.Active && !m_EmployeesData.active) 369 | { 370 | return m_WorkplacesData.active; 371 | } 372 | return true; 373 | } 374 | } 375 | 376 | protected override bool Modified => !m_WorkplaceModifiedQuery.IsEmptyIgnoreFilter; 377 | */ 378 | 379 | // 240209 Set gameMode to avoid errors in the Editor 380 | public override GameMode gameMode => GameMode.Game; 381 | 382 | //[Preserve] 383 | protected override void OnCreate() 384 | { 385 | base.OnCreate(); 386 | 387 | m_SimulationSystem = base.World.GetOrCreateSystemManaged(); // TODO: use UIUpdateState eventually 388 | //m_ResourceSystem = base.World.GetOrCreateSystemManaged(); 389 | 390 | m_WorkplaceQuery = GetEntityQuery(new EntityQueryDesc 391 | { 392 | All = new ComponentType[3] 393 | { 394 | ComponentType.ReadOnly(), 395 | ComponentType.ReadOnly(), 396 | ComponentType.ReadOnly() 397 | }, 398 | Any = new ComponentType[2] 399 | { 400 | ComponentType.ReadOnly(), 401 | ComponentType.ReadOnly() 402 | }, 403 | None = new ComponentType[1] { ComponentType.ReadOnly() } 404 | }); 405 | 406 | /* not used 407 | m_WorkplaceModifiedQuery = GetEntityQuery(new EntityQueryDesc 408 | { 409 | All = new ComponentType[3] 410 | { 411 | ComponentType.ReadOnly(), 412 | ComponentType.ReadOnly(), 413 | ComponentType.ReadOnly() 414 | }, 415 | Any = new ComponentType[3] 416 | { 417 | ComponentType.ReadOnly(), 418 | ComponentType.ReadOnly(), 419 | ComponentType.ReadOnly() 420 | }, 421 | None = new ComponentType[1] { ComponentType.ReadOnly() } 422 | }); 423 | m_IntResults = new NativeArray(2, Allocator.Persistent); 424 | m_EmploymentDataResults = new NativeArray(2, Allocator.Persistent); 425 | */ 426 | 427 | m_Results = new NativeArray(7, Allocator.Persistent); // there are 5 education levels + 1 for totals + 1 for companies 428 | 429 | //AddBinding(m_WorkplacesData = new GetterValueBinding(kGroup, "ilWorkplacesData", () => (!m_EmploymentDataResults.IsCreated || m_EmploymentDataResults.Length != 2) ? default(EmploymentData) : m_EmploymentDataResults[0], new ValueWriter())); 430 | //AddBinding(m_EmployeesData = new GetterValueBinding(kGroup, "ilEmployeesData", () => (!m_EmploymentDataResults.IsCreated || m_EmploymentDataResults.Length != 2) ? default(EmploymentData) : m_EmploymentDataResults[1], new ValueWriter())); 431 | //AddBinding(m_Workplaces = new GetterValueBinding(kGroup, "ilWorkplaces", () => (m_IntResults.IsCreated && m_IntResults.Length == 2) ? m_IntResults[0] : 0)); 432 | //AddBinding(m_Workers = new GetterValueBinding(kGroup, "ilEmployees", () => (m_IntResults.IsCreated && m_IntResults.Length == 2) ? m_IntResults[1] : 0)); 433 | 434 | AddBinding(m_uiResults = new RawValueBinding(kGroup, "ilWorkplaces", delegate (IJsonWriter binder) 435 | { 436 | binder.ArrayBegin(m_Results.Length); 437 | for (int i = 0; i < m_Results.Length; i++) 438 | WriteData(binder, m_Results[i]); 439 | binder.ArrayEnd(); 440 | })); 441 | 442 | Mod.log.Info("WorkplacesInfoLoomUISystem created."); 443 | } 444 | 445 | //[Preserve] 446 | protected override void OnDestroy() 447 | { 448 | //m_IntResults.Dispose(); 449 | //m_EmploymentDataResults.Dispose(); 450 | m_Results.Dispose(); 451 | base.OnDestroy(); 452 | } 453 | 454 | protected override void OnUpdate() // original: PerformUpdate() 455 | { 456 | if (m_SimulationSystem.frameIndex % 128 != 22) 457 | return; 458 | 459 | ResetResults(); 460 | 461 | // update handles 462 | __TypeHandle.__Game_Prefabs_SpawnableBuildingData_RO_ComponentLookup.Update(ref base.CheckedStateRef); 463 | __TypeHandle.__Game_Prefabs_IndustrialProcessData_RO_ComponentLookup.Update(ref base.CheckedStateRef); 464 | __TypeHandle.__Game_Citizens_Citizen_RO_ComponentLookup.Update(ref base.CheckedStateRef); 465 | __TypeHandle.__Game_Prefabs_WorkplaceData_RO_ComponentLookup.Update(ref base.CheckedStateRef); 466 | __TypeHandle.__Game_Prefabs_PrefabRef_RO_ComponentLookup.Update(ref base.CheckedStateRef); 467 | __TypeHandle.__Game_Prefabs_PrefabRef_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 468 | __TypeHandle.__Game_Buildings_PropertyRenter_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 469 | __TypeHandle.__Game_Companies_WorkProvider_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 470 | __TypeHandle.__Game_Companies_ExtractorCompany_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 471 | __TypeHandle.__Game_Companies_IndustrialCompany_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 472 | __TypeHandle.__Game_Companies_CommercialCompany_RO_ComponentTypeHandle.Update(ref base.CheckedStateRef); 473 | __TypeHandle.__Game_Companies_Employee_RO_BufferTypeHandle.Update(ref base.CheckedStateRef); 474 | __TypeHandle.__Game_Economy_Resources_RO_BufferTypeHandle.Update(ref base.CheckedStateRef); 475 | __TypeHandle.__Unity_Entities_Entity_TypeHandle.Update(ref base.CheckedStateRef); 476 | 477 | // prepare and schedule job 478 | CalculateWorkplaceDataJob jobData = default(CalculateWorkplaceDataJob); 479 | jobData.m_EntityHandle = __TypeHandle.__Unity_Entities_Entity_TypeHandle; 480 | jobData.m_EmployeeHandle = __TypeHandle.__Game_Companies_Employee_RO_BufferTypeHandle; 481 | //jobData.m_ResourcesHandle = __TypeHandle.__Game_Economy_Resources_RO_BufferTypeHandle; // Game.Economy.Resources 482 | jobData.m_WorkProviderHandle = __TypeHandle.__Game_Companies_WorkProvider_RO_ComponentTypeHandle; 483 | jobData.m_PropertyRenterHandle = __TypeHandle.__Game_Buildings_PropertyRenter_RO_ComponentTypeHandle; 484 | jobData.m_ExtractorCompanyHandle = __TypeHandle.__Game_Companies_ExtractorCompany_RO_ComponentTypeHandle; 485 | jobData.m_IndustrialCompanyHandle = __TypeHandle.__Game_Companies_IndustrialCompany_RO_ComponentTypeHandle; // Game.Companies.IndustrialCompany 486 | jobData.m_CommercialCompanyHandle = __TypeHandle.__Game_Companies_CommercialCompany_RO_ComponentTypeHandle; 487 | jobData.m_PrefabRefHandle = __TypeHandle.__Game_Prefabs_PrefabRef_RO_ComponentTypeHandle; 488 | jobData.m_PrefabRefFromEntity = __TypeHandle.__Game_Prefabs_PrefabRef_RO_ComponentLookup; 489 | jobData.m_WorkplaceDataFromEntity = __TypeHandle.__Game_Prefabs_WorkplaceData_RO_ComponentLookup; 490 | jobData.m_SpawnableBuildingFromEntity = __TypeHandle.__Game_Prefabs_SpawnableBuildingData_RO_ComponentLookup; 491 | jobData.m_IndustrialProcessDataFromEntity = __TypeHandle.__Game_Prefabs_IndustrialProcessData_RO_ComponentLookup; 492 | jobData.m_CitizenFromEntity = __TypeHandle.__Game_Citizens_Citizen_RO_ComponentLookup; 493 | //jobData.m_IntResults = m_IntResults; 494 | //jobData.m_EmploymentDataResults = m_EmploymentDataResults; 495 | jobData.m_Results = m_Results; 496 | //jobData.m_ResourcePrefabs = m_ResourceSystem.GetPrefabs(); // TODO 497 | JobChunkExtensions.Schedule(jobData, m_WorkplaceQuery, base.Dependency).Complete(); 498 | 499 | // calculate totals 500 | WorkplacesAtLevelInfo totals = new WorkplacesAtLevelInfo(-1); 501 | for (int i = 0; i < 5; i++) 502 | { 503 | totals.Total += m_Results[i].Total; 504 | totals.Service += m_Results[i].Service; 505 | totals.Commercial += m_Results[i].Commercial; 506 | totals.Leisure += m_Results[i].Leisure; 507 | totals.Extractor += m_Results[i].Extractor; 508 | totals.Industrial += m_Results[i].Industrial; 509 | totals.Office += m_Results[i].Office; 510 | totals.Employee += m_Results[i].Employee; 511 | totals.Commuter += m_Results[i].Commuter; 512 | totals.Open += m_Results[i].Open; 513 | } 514 | m_Results[5] = totals; 515 | // update ui bindings 516 | //m_EmployeesData.Update(); 517 | //m_WorkplacesData.Update(); 518 | //m_Workplaces.Update(); 519 | //m_Workers.Update(); 520 | m_uiResults.Update(); 521 | // DEBUG 522 | //Utils.InspectComponentsInChunks(EntityManager, m_WorkplaceQuery, "JOBS"); 523 | } 524 | 525 | private void ResetResults() 526 | { 527 | /* not used 528 | for (int i = 0; i < 2; i++) 529 | { 530 | m_EmploymentDataResults[i] = default(EmploymentData); 531 | m_IntResults[i] = 0; 532 | } 533 | */ 534 | for (int i = 0; i < 6; i++) // there are 5 education levels + 1 for totals 535 | { 536 | m_Results[i] = new WorkplacesAtLevelInfo(i); 537 | } 538 | m_Results[6] = new WorkplacesAtLevelInfo(-2); 539 | } 540 | 541 | /* not used 542 | private int GetWorkplaces() 543 | { 544 | if (!m_IntResults.IsCreated || m_IntResults.Length != 2) 545 | { 546 | return 0; 547 | } 548 | return m_IntResults[0]; 549 | } 550 | 551 | private int GetWorkers() 552 | { 553 | if (!m_IntResults.IsCreated || m_IntResults.Length != 2) 554 | { 555 | return 0; 556 | } 557 | return m_IntResults[1]; 558 | } 559 | 560 | private EmploymentData GetWorkplacesData() 561 | { 562 | if (!m_EmploymentDataResults.IsCreated || m_EmploymentDataResults.Length != 2) 563 | { 564 | return default(EmploymentData); 565 | } 566 | return m_EmploymentDataResults[0]; 567 | } 568 | 569 | private EmploymentData GetEmployeesData() 570 | { 571 | if (!m_EmploymentDataResults.IsCreated || m_EmploymentDataResults.Length != 2) 572 | { 573 | return default(EmploymentData); 574 | } 575 | return m_EmploymentDataResults[1]; 576 | } 577 | */ 578 | 579 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 580 | private void __AssignQueries(ref SystemState state) 581 | { 582 | } 583 | 584 | protected override void OnCreateForCompiler() 585 | { 586 | base.OnCreateForCompiler(); 587 | __AssignQueries(ref base.CheckedStateRef); 588 | __TypeHandle.__AssignHandles(ref base.CheckedStateRef); 589 | } 590 | 591 | //[Preserve] 592 | public WorkplacesInfoLoomUISystem() 593 | { 594 | } 595 | } 596 | -------------------------------------------------------------------------------- /Thunderstore/0Harmony.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/Thunderstore/0Harmony.dll -------------------------------------------------------------------------------- /Thunderstore/InfoLoom.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/Thunderstore/InfoLoom.dll -------------------------------------------------------------------------------- /Thunderstore/README.md: -------------------------------------------------------------------------------- 1 | # InfoLoom 2 | InfoLoom is a Cities Skylines II mod that adds several new UI windows with extra information. 3 | Currently: 4 | - Demographics 5 | - Workforce structure 6 | - Workplaces distribution 7 | - Residential data 8 | - Commercial data 9 | - Industrial and Office data 10 | - Demand factors 11 | - Resources consumption (optional) 12 | 13 | ## Features 14 | 15 | ### Demographics 16 | - Key information about current city demographics in one place. 17 | - Age distribution histogram that also shows cims' activity structure. 18 | 19 | ![Demographics](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/demographics.png) 20 | 21 | ### Workforce structure 22 | - Shows workforce structure by education level. 23 | - Total: all potential workers, i.e. teens and adults, that are moved in and not moving away, excluding students. 24 | - Unemployment by education level and structure also shown as percentage. 25 | - Under: cims working on positions below their education level. 26 | - Outside: cims working outside of the city. 27 | - Homeless: unemployed cims with no home. 28 | 29 | ### Workplaces distribution 30 | - Shows available workplaces split into: city services, commercial sales, commercial leisure, industry extractors, industry manufacturing, and office. 31 | - Leisure are commercial companies selling immaterial resources. 32 | - Commute are cims employed from outside of the city, they commute to work. 33 | - Shows number of companies in each category. 34 | 35 | ![Workforce and Workplaces](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/workforce_jobs.png) 36 | 37 | ### Residential data 38 | - Shows residential data used to calculate Residential Demand Factors. 39 | - Residential properties by density: total count, occupied, free. 40 | - Total households and homeless households. 41 | - Average happiness, weighted tax rate, open study positions. 42 | - Household demand which is hidden in the game, it controls if new households spawn. 43 | - For new households, a chance that a Student Household will spawn. 44 | 45 | ![Residential data](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/residential.png) 46 | 47 | ### Commercial data 48 | - Shows commercial data used to calculate Commercial Demand Factors. 49 | - Empty buildings and companies that have no property, and average tax rate. 50 | - Service capacity utilization and sales capacity to consumption ratio - these are crucial in calculating LocalDemand, TouristDemand and PetrolLocalDemand. 51 | - Actual numbers of available workforce, and employee capacity ratio. 52 | - Lists resources that currently are not demanded, thus the engine ignores them when calculating Commercial Demand Factors. 53 | 54 | ![Commercial data](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/commercial.png) 55 | 56 | ### Industrial and Office data 57 | - Shows industrial and office data used to calculate Industrial and Office Demand Factors. 58 | - Empty buildings and companies that have no property, and average tax rate. 59 | - Local Demand (which is production capacity in fact) and Input Utilization (which tells if resources are available as input). 60 | - Actual numbers of available workforce, and employee capacity ratio. 61 | - Lists resources that currently are not demanded, thus the engine ignores them when calculating Industrial and Office Factors. 62 | - Storage section that shows Demanded Types info which controls if new warehouses are spawned. 63 | 64 | ![Industrial and Office data](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/industrial.png) 65 | 66 | ### Demand factors 67 | - Shows individual demand factor VALUES for each demand type. Helpful to learn actual importance of each factor. 68 | - Enables showing all demand factors, not only 5. Useful for industrial demand which is the only one that utilizes up yo 7 factors. 69 | - Additional section shows directly building demand for each zone type and STORAGE demand. Please note that the "main" demand factor is actually a building demand, however UI shows it as moving. In fact it doesn't move, it is just a visualization. 70 | - Also, industrial demand is a higher value from the two: industrial building demand and storage demand. 71 | 72 | ![Demand factors](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/demandfactors.png) 73 | 74 | ### Resources consumption 75 | - This feature is disabled by default because it changes the data shown on the vanilla UI. Enable it in the config file by setting SeparateConsumption to true. 76 | - Instead of showing surplus and deficit, the production UI will show commercial consumption and industrial consumption. It is imho much more informative than just surplus/deficit, because it also tells what causes said surplus or deficit. 77 | - Disclaimer. I don't yet how to change existing UI, so the titles "Surplus" and "Deficit" will still be there. Sorry. 78 | 79 | ![Resources consumption](https://raw.githubusercontent.com/infixo/cs2-infoloom/main/docs/consumption.png) 80 | 81 | ## Technical 82 | 83 | ### Requirements and Compatibility 84 | - Cities Skylines II v1.1.0f1 or later; check GitHub or Discord if the mod is compatible with the latest game version. 85 | - BepInEx 5. 86 | - HookUI v1.1.0 or later. 87 | - The mod does NOT modify savefiles. 88 | - The mod does NOT modify any game systems. 89 | 90 | ### Installation 91 | 1. Place the `InfoLoom.dll` file in your BepInEx `Plugins` folder. 92 | 2. The config file is automatically created in BepInEx/config folder when the game is run once. 93 | 94 | ### Known Issues 95 | - Nothing atm. 96 | 97 | ### Changelog 98 | - v0.8.3 (2024-03-21) 99 | - Mod updated for v1.1 of the game. 100 | - v0.8.2 (2024-02-12) 101 | - Debug Watches for demand and consumption. 102 | - v0.8.1 (2024-02-09) 103 | - Fixed: null reference in Editor Mode. 104 | - Fixed: student ratio in empty city. 105 | - Fixed: demographics bars sometimes going beyond the window. 106 | - v0.8.0 (2024-02-04) 107 | - New feature: industrial and office data. 108 | - New feature: household demand and student chance. 109 | - Fixed: demographics proper scaling. 110 | - v0.7.0 (2024-01-14) 111 | - New features: number of commuters as employees, unemployment and structures as percentages, number of companies. 112 | - Fixed: Incorrect counting of Office and Leisure jobs. 113 | - Fixed: Issue with Asset Editor. 114 | - v0.6.0 (2024-01-04) 115 | - New feature: residential data. 116 | - v0.5.0 (2024-01-02) 117 | - New feature: commercial data, homeless count. 118 | - Population bars in Demographics window are scalable now. 119 | - Fixed: Demographics now correctly shows Tourists and Commuters. 120 | - v0.4.1 (2023-12-22) 121 | - New feature: demographics. 122 | - v0.3.0 (2023-12-20) 123 | - New features: worforce structure and workplaces distribution. 124 | - v0.2.2 (2023-12-17) 125 | - Demand factors window is reformatted, to be more aligned with game's visual style. 126 | - New features: shows all factors, building demand and resources consumption. 127 | - v0.1.0 (2023-12-16) 128 | - Initial build, includes Demand Factors. 129 | 130 | ### Support 131 | - Please report bugs and issues on [GitHub](https://github.com/Infixo/CS2-InfoLoom). 132 | - You may also leave comments on [Discord1](https://discord.com/channels/1169011184557637825/1198627819475976263) or [Discord2](https://discord.com/channels/1024242828114673724/1185672922212347944). 133 | -------------------------------------------------------------------------------- /Thunderstore/Thunderstore.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/Thunderstore/Thunderstore.zip -------------------------------------------------------------------------------- /Thunderstore/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/Thunderstore/icon.png -------------------------------------------------------------------------------- /Thunderstore/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Info_Loom", 3 | "description": "Adds new UI screens with some extra information.", 4 | "version_number": "0.8.3", 5 | "dependencies": [ 6 | "BepInEx-BepInExPack-5.4.2100", 7 | "Cities2Modding-HookUI-1.1.0" 8 | ], 9 | "website_url": "https://github.com/Infixo/CS2-InfoLoom" 10 | } -------------------------------------------------------------------------------- /_not_used/InfoLoom_BepInEx.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 9 | 10 | 11 | netstandard2.1 12 | InfoLoom 13 | InfoLoom 14 | Extra City information and demographics data (population, education and workforce structure). 15 | 0.8.5 16 | true 17 | latest 18 | 19 | https://api.nuget.org/v3/index.json; 20 | https://nuget.bepinex.dev/v3/index.json; 21 | https://nuget.samboy.dev/v3/index.json 22 | 23 | 24 | true 25 | 26 | MSB3277 27 | 28 | 29 | 33 | 34 | C:\Steam\steamapps\common\Cities Skylines II 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | $(Cities2_Location)\BepInEx\plugins\Gooee\Gooee.dll 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 69 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 5 90 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | $(DefineConstants);BEPINEX_V6 107 | 108 | 109 | 110 | 111 | 112 | 113 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /_not_used/Patcher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using HarmonyLib; 3 | 4 | namespace InfoLoom; 5 | 6 | public static class Patcher 7 | { 8 | private const string HarmonyId = MyPluginInfo.PLUGIN_GUID + "_Cities2Harmony"; 9 | private static bool patched = false; 10 | private static Dictionary myPatches = new(); 11 | 12 | public static void PatchAll() 13 | { 14 | if (patched) { Plugin.Log($"WARNING: Not patched!"); return; } 15 | //Harmony.DEBUG = true; 16 | var harmony = new Harmony(HarmonyId); 17 | harmony.PatchAll(); 18 | if (Harmony.HasAnyPatches(HarmonyId)) 19 | { 20 | Plugin.Log($"OK methods patched"); 21 | patched = true; 22 | //var myOriginalMethods = harmony.GetPatchedMethods(); 23 | foreach (System.Reflection.MethodBase method in harmony.GetPatchedMethods()) 24 | { 25 | Plugin.Log($"...method {method.DeclaringType}.{method.Name}"); 26 | myPatches[method.DeclaringType + "." + method.Name] = method; 27 | } 28 | } 29 | else 30 | Plugin.Log($"ERROR methods not patched."); 31 | //Harmony.DEBUG = false; 32 | } 33 | 34 | /// 35 | /// Removes Transpilers and Prefixes from other plugins. 36 | /// 37 | /// Method to clean. 38 | private static void RemoveConflictingPatches(System.Reflection.MethodBase original, params string[] mods) 39 | { 40 | var harmony = new Harmony(HarmonyId); 41 | Patches patches = Harmony.GetPatchInfo(original); 42 | foreach (Patch patch in patches.Transpilers) 43 | { 44 | //Debug.Log($" Transpiler: {patch.index} {patch.owner} {patch.PatchMethod.Name}"); 45 | if (patch.owner != HarmonyId) 46 | { 47 | Plugin.Log($"REMOVING {patch.PatchMethod.Name} from {patch.owner}"); 48 | harmony.Unpatch(original, HarmonyPatchType.Transpiler, patch.owner); 49 | } 50 | } 51 | foreach (Patch patch in patches.Prefixes) 52 | { 53 | //Debug.Log($" Prefix: {patch.index} {patch.owner} {patch.PatchMethod.Name}"); 54 | if (patch.owner != HarmonyId) 55 | { 56 | Plugin.Log($"REMOVING {patch.PatchMethod.Name} from {patch.owner}"); 57 | harmony.Unpatch(original, HarmonyPatchType.Prefix, patch.owner); 58 | } 59 | } 60 | } 61 | 62 | 63 | /// 64 | /// Removes Postix from the specified plugin. 65 | /// 66 | /// Method to clean. 67 | private static void RemovePostfixPatch(System.Reflection.MethodBase original, string plugin) 68 | { 69 | var harmony = new Harmony(HarmonyId); 70 | Patches patches = Harmony.GetPatchInfo(original); 71 | foreach (Patch patch in patches.Postfixes) 72 | { 73 | //Debug.Log($" Transpiler: {patch.index} {patch.owner} {patch.PatchMethod.Name}"); 74 | if (patch.owner != HarmonyId && patch.owner == plugin) 75 | { 76 | Plugin.Log($"REMOVING {patch.PatchMethod.Name} from {patch.owner}"); 77 | harmony.Unpatch(original, HarmonyPatchType.Postfix, patch.owner); 78 | } 79 | } 80 | } 81 | 82 | 83 | public static void RemoveConflictingPatches() 84 | { 85 | const string lrr = "com.github.algernon-A.csl.lifecyclerebalancerevisited"; 86 | // OutsideConnectionAI.StartConnectionTransferImpl 87 | RemoveConflictingPatches(myPatches["OutsideConnectionAI.StartConnectionTransferImpl"]); 88 | RemoveConflictingPatches(myPatches["Citizen.GetAgeGroup"]); 89 | RemoveConflictingPatches(myPatches["Citizen.GetAgePhase"]); 90 | RemovePostfixPatch(myPatches["Citizen.GetCitizenHomeBehaviour"], lrr); 91 | RemoveConflictingPatches(myPatches["HumanAI.FindVisitPlace"]); 92 | RemoveConflictingPatches(myPatches["ResidentAI.CanMakeBabies"]); 93 | RemoveConflictingPatches(myPatches["ResidentAI.UpdateAge"]); 94 | RemoveConflictingPatches(myPatches["ResidentAI.UpdateWorkplace"]); 95 | } 96 | 97 | /// 98 | /// Dumps to a log all Harmony patches, marking potential conflicts. 99 | /// 100 | public static void ListAllPatches() 101 | { 102 | foreach (System.Reflection.MethodBase original in Harmony.GetAllPatchedMethods()) 103 | { 104 | bool check = myPatches.ContainsKey(original.DeclaringType + "." + original.Name); 105 | Plugin.Log($"Method: {original.DeclaringType}.{original.Name}"); 106 | Patches patches = Harmony.GetPatchInfo(original); 107 | foreach (Patch patch in patches.Transpilers) 108 | Plugin.Log($" Transpiler {patch.index}: {patch.owner} {patch.PatchMethod.DeclaringType}.{patch.PatchMethod.Name}" + ((check && patch.owner != HarmonyId) ? " potential CONFLICT" : "")); 109 | foreach (Patch patch in patches.Postfixes) 110 | Plugin.Log($" Postfix {patch.index}: {patch.owner} {patch.PatchMethod.DeclaringType}.{patch.PatchMethod.Name}" + ((check && patch.owner != HarmonyId) ? " could be safe, but check for CONFLICT" : "")); 111 | foreach (Patch patch in patches.Prefixes) 112 | Plugin.Log($" Prefix {patch.index}: {patch.owner} {patch.PatchMethod.DeclaringType}.{patch.PatchMethod.Name}" + ((check && patch.owner != HarmonyId) ? " potential CONFLICT" : "")); 113 | } 114 | } 115 | 116 | public static void UnpatchAll() 117 | { 118 | if (!patched) { Plugin.Log($"WARNING: Not patched!"); return; } 119 | //Harmony.DEBUG = true; 120 | var harmony = new Harmony(HarmonyId); 121 | //harmony.UnpatchAll(HarmonyId); // deprecated 122 | harmony.UnpatchSelf(); // deprecated 123 | Plugin.Log($"OK methods unpatched."); 124 | patched = false; 125 | //Harmony.DEBUG = false; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /_not_used/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Reflection; 5 | using BepInEx; 6 | using BepInEx.Logging; 7 | using BepInEx.Configuration; 8 | using HarmonyLib; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | using Colossal.Logging; 12 | using HookUILib.Core; 13 | 14 | #if BEPINEX_V6 15 | using BepInEx.Unity.Mono; 16 | #endif 17 | 18 | namespace InfoLoom; 19 | 20 | [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] 21 | public class Plugin : BaseUnityPlugin 22 | { 23 | private const string HarmonyId = MyPluginInfo.PLUGIN_GUID + "_Cities2Harmony"; 24 | 25 | internal static new ManualLogSource Logger; // BepInEx logging 26 | private static ILog s_Log; // CO logging 27 | 28 | public static void Log(string text, bool bMethod = false) 29 | { 30 | if (bMethod) text = GetCallingMethod(2) + ": " + text; 31 | Logger.LogInfo(text); 32 | s_Log.Info(text); 33 | } 34 | 35 | public static void LogStack(string text) 36 | { 37 | //string msg = GetCallingMethod(2) + ": " + text + " STACKTRACE"; 38 | Logger.LogInfo(text + " STACKTRACE"); 39 | s_Log.logStackTrace = true; 40 | s_Log.Info(text + "STACKTRACE"); 41 | s_Log.logStackTrace = false; 42 | } 43 | 44 | /// 45 | /// Gets the method from the specified . 46 | /// 47 | public static string GetCallingMethod(int frame) 48 | { 49 | StackTrace st = new StackTrace(); 50 | MethodBase mb = st.GetFrame(frame).GetMethod(); // 0 - GetCallingMethod, 1 - Log, 2 - actual function calling a Log method 51 | return mb.DeclaringType + "." + mb.Name; 52 | } 53 | 54 | // mod settings 55 | public static ConfigEntry SeparateConsumption; 56 | 57 | private void Awake() 58 | { 59 | Logger = base.Logger; 60 | 61 | // CO logging standard as described here https://cs2.paradoxwikis.com/Logging 62 | s_Log = LogManager.GetLogger(MyPluginInfo.PLUGIN_NAME); 63 | 64 | Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!"); 65 | 66 | var harmony = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), HarmonyId); 67 | var patchedMethods = harmony.GetPatchedMethods().ToArray(); 68 | 69 | Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} made patches! Patched methods: " + patchedMethods.Length); 70 | 71 | foreach (var patchedMethod in patchedMethods) { 72 | Logger.LogInfo($"Patched method: {patchedMethod.Module.Name}:{patchedMethod.Name}"); 73 | } 74 | 75 | // settings 76 | SeparateConsumption = base.Config.Bind("Settings", "SeparateConsumption", false, "Enables showing commercial and industrial consumption instead of surplus/deficit in the Production UI"); 77 | 78 | // check if SeparateConsumption feature is enabled 79 | if (!SeparateConsumption.Value) 80 | { 81 | MethodBase mb = typeof(Game.UI.InGame.ProductionUISystem).GetMethod("GetData", BindingFlags.NonPublic | BindingFlags.Instance); 82 | if (mb != null) 83 | { 84 | Plugin.Log($"REMOVING {mb.Name} patch from {HarmonyId}"); 85 | harmony.Unpatch(mb, HarmonyPatchType.Prefix, HarmonyId); 86 | } 87 | else 88 | Plugin.Log("WARNING: Cannot remove GetData patch."); 89 | } 90 | 91 | //Plugin.Log("===== all patches ====="); 92 | //Patcher.ListAllPatches(); 93 | } 94 | } 95 | 96 | public class InfoLoom_Demographics : UIExtension 97 | { 98 | public new readonly string extensionID = "infoloom.demographics"; 99 | public new readonly string extensionContent; 100 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 101 | 102 | public InfoLoom_Demographics() 103 | { 104 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.demographics.js"); 105 | } 106 | } 107 | 108 | public class InfoLoom_Workforce : UIExtension 109 | { 110 | public new readonly string extensionID = "infoloom.workforce"; 111 | public new readonly string extensionContent; 112 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 113 | 114 | public InfoLoom_Workforce() 115 | { 116 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.workforce.js"); 117 | } 118 | } 119 | 120 | public class InfoLoom_Workplaces : UIExtension 121 | { 122 | public new readonly string extensionID = "infoloom.workplaces"; 123 | public new readonly string extensionContent; 124 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 125 | 126 | public InfoLoom_Workplaces() 127 | { 128 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.workplaces.js"); 129 | } 130 | } 131 | 132 | public class InfoLoom_DemandFactors : UIExtension 133 | { 134 | public new readonly string extensionID = "infoloom.demandfactors"; 135 | public new readonly string extensionContent; 136 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 137 | 138 | public InfoLoom_DemandFactors() 139 | { 140 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.demandfactors.js"); 141 | } 142 | } 143 | 144 | public class InfoLoom_Commercial : UIExtension 145 | { 146 | public new readonly string extensionID = "infoloom.commercial"; 147 | public new readonly string extensionContent; 148 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 149 | 150 | public InfoLoom_Commercial() 151 | { 152 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.commercial.js"); 153 | } 154 | } 155 | 156 | public class InfoLoom_Residential : UIExtension 157 | { 158 | public new readonly string extensionID = "infoloom.residential"; 159 | public new readonly string extensionContent; 160 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 161 | 162 | public InfoLoom_Residential() 163 | { 164 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.residential.js"); 165 | } 166 | } 167 | 168 | public class InfoLoom_Industrial : UIExtension 169 | { 170 | public new readonly string extensionID = "infoloom.industrial"; 171 | public new readonly string extensionContent; 172 | public new readonly ExtensionType extensionType = ExtensionType.Panel; 173 | 174 | public InfoLoom_Industrial() 175 | { 176 | this.extensionContent = this.LoadEmbeddedResource("InfoLoom.dist.industrial.js"); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /_not_used/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Reflection; 6 | using Unity.Collections; 7 | using Unity.Entities; 8 | using Unity.Entities.UniversalDelegates; 9 | 10 | namespace InfoLoom; 11 | 12 | internal static class Utils 13 | { 14 | public static void LogChunk(in ArchetypeChunk chunk) 15 | { 16 | var componentTypes = chunk.Archetype.GetComponentTypes(); 17 | Plugin.Log($"chunk: {chunk.Count}, {string.Join(", ", componentTypes.Select(ct => ct.GetType().GetTypeInfo().FullName))}"); 18 | } 19 | 20 | public static string[] ListEntityComponents(EntityManager manager, Entity entity) 21 | { 22 | var componentTypes = new List(); 23 | 24 | if (!manager.Exists(entity)) 25 | throw new ArgumentException("Entity does not exist."); 26 | 27 | NativeArray NativeArray = manager.GetComponentTypes(entity, Allocator.Temp); 28 | string[] ToReturn = NativeArray.Select(T => T.GetManagedType().Name).ToArray(); 29 | NativeArray.Dispose(); 30 | return ToReturn; 31 | } 32 | 33 | public static void InspectComponentsInChunk(EntityManager manager, in ArchetypeChunk chunk, string name) 34 | { 35 | EntityTypeHandle m_EntityHandle; 36 | NativeArray entities = chunk.GetNativeArray(m_EntityHandle); 37 | if (entities.Length > 0) 38 | { 39 | Entity firstEntity = entities[0]; 40 | string[] components = ListEntityComponents(manager, firstEntity); 41 | Plugin.Log($"{name} {entities.Length}: {string.Join(" ", components)}"); 42 | } 43 | } 44 | 45 | public static void InspectComponentsInQuery(EntityManager manager, EntityQuery query, string name) 46 | { 47 | Dictionary CompDict = new Dictionary(); 48 | NativeArray entities = query.ToEntityArray(Allocator.Temp); 49 | for (int i = 0; i < entities.Length; i++) 50 | { 51 | Entity entity = entities[i]; 52 | string[] comps = ListEntityComponents(manager, entity); 53 | foreach (string comp in comps) 54 | { 55 | if (CompDict.ContainsKey(comp)) CompDict[comp]++; 56 | else CompDict.Add(comp, 1); 57 | } 58 | } 59 | entities.Dispose(); 60 | // show the dictionary 61 | Plugin.Log($"===== {name} ====="); 62 | foreach (var pair in CompDict) 63 | { 64 | Plugin.Log($"{pair.Key} {pair.Value}"); 65 | } 66 | } 67 | 68 | public static void InspectComponentsInChunks(EntityManager manager, EntityQuery query, string name) 69 | { 70 | Plugin.Log($"===== Components in chunks of query {name} ====="); 71 | NativeArray chunks = query.ToArchetypeChunkArray(Allocator.Temp); 72 | for (int i = 0; i < chunks.Length; i++) 73 | { 74 | InspectComponentsInChunk(manager, chunks[i], name); 75 | } 76 | chunks.Dispose(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /docs/commercial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/commercial.png -------------------------------------------------------------------------------- /docs/consumption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/consumption.png -------------------------------------------------------------------------------- /docs/demandfactors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/demandfactors.png -------------------------------------------------------------------------------- /docs/demographics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/demographics.png -------------------------------------------------------------------------------- /docs/industrial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/industrial.png -------------------------------------------------------------------------------- /docs/residential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/residential.png -------------------------------------------------------------------------------- /docs/workforce_jobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infixo/CS2-InfoLoom/e66eddf84bb6df097184ebb3a888dd1f94ccbc00/docs/workforce_jobs.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cs2-vehicle-counter", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "cs2-vehicle-counter", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "esbuild": "^0.19.8", 13 | "hookui-framework": "github:captain-of-coit/hookui-framework" 14 | } 15 | }, 16 | "node_modules/@esbuild/android-arm": { 17 | "version": "0.19.8", 18 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", 19 | "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", 20 | "cpu": [ 21 | "arm" 22 | ], 23 | "dev": true, 24 | "optional": true, 25 | "os": [ 26 | "android" 27 | ], 28 | "engines": { 29 | "node": ">=12" 30 | } 31 | }, 32 | "node_modules/@esbuild/android-arm64": { 33 | "version": "0.19.8", 34 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", 35 | "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", 36 | "cpu": [ 37 | "arm64" 38 | ], 39 | "dev": true, 40 | "optional": true, 41 | "os": [ 42 | "android" 43 | ], 44 | "engines": { 45 | "node": ">=12" 46 | } 47 | }, 48 | "node_modules/@esbuild/android-x64": { 49 | "version": "0.19.8", 50 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", 51 | "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", 52 | "cpu": [ 53 | "x64" 54 | ], 55 | "dev": true, 56 | "optional": true, 57 | "os": [ 58 | "android" 59 | ], 60 | "engines": { 61 | "node": ">=12" 62 | } 63 | }, 64 | "node_modules/@esbuild/darwin-arm64": { 65 | "version": "0.19.8", 66 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", 67 | "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", 68 | "cpu": [ 69 | "arm64" 70 | ], 71 | "dev": true, 72 | "optional": true, 73 | "os": [ 74 | "darwin" 75 | ], 76 | "engines": { 77 | "node": ">=12" 78 | } 79 | }, 80 | "node_modules/@esbuild/darwin-x64": { 81 | "version": "0.19.8", 82 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", 83 | "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", 84 | "cpu": [ 85 | "x64" 86 | ], 87 | "dev": true, 88 | "optional": true, 89 | "os": [ 90 | "darwin" 91 | ], 92 | "engines": { 93 | "node": ">=12" 94 | } 95 | }, 96 | "node_modules/@esbuild/freebsd-arm64": { 97 | "version": "0.19.8", 98 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", 99 | "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", 100 | "cpu": [ 101 | "arm64" 102 | ], 103 | "dev": true, 104 | "optional": true, 105 | "os": [ 106 | "freebsd" 107 | ], 108 | "engines": { 109 | "node": ">=12" 110 | } 111 | }, 112 | "node_modules/@esbuild/freebsd-x64": { 113 | "version": "0.19.8", 114 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", 115 | "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", 116 | "cpu": [ 117 | "x64" 118 | ], 119 | "dev": true, 120 | "optional": true, 121 | "os": [ 122 | "freebsd" 123 | ], 124 | "engines": { 125 | "node": ">=12" 126 | } 127 | }, 128 | "node_modules/@esbuild/linux-arm": { 129 | "version": "0.19.8", 130 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", 131 | "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", 132 | "cpu": [ 133 | "arm" 134 | ], 135 | "dev": true, 136 | "optional": true, 137 | "os": [ 138 | "linux" 139 | ], 140 | "engines": { 141 | "node": ">=12" 142 | } 143 | }, 144 | "node_modules/@esbuild/linux-arm64": { 145 | "version": "0.19.8", 146 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", 147 | "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", 148 | "cpu": [ 149 | "arm64" 150 | ], 151 | "dev": true, 152 | "optional": true, 153 | "os": [ 154 | "linux" 155 | ], 156 | "engines": { 157 | "node": ">=12" 158 | } 159 | }, 160 | "node_modules/@esbuild/linux-ia32": { 161 | "version": "0.19.8", 162 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", 163 | "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", 164 | "cpu": [ 165 | "ia32" 166 | ], 167 | "dev": true, 168 | "optional": true, 169 | "os": [ 170 | "linux" 171 | ], 172 | "engines": { 173 | "node": ">=12" 174 | } 175 | }, 176 | "node_modules/@esbuild/linux-loong64": { 177 | "version": "0.19.8", 178 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", 179 | "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", 180 | "cpu": [ 181 | "loong64" 182 | ], 183 | "dev": true, 184 | "optional": true, 185 | "os": [ 186 | "linux" 187 | ], 188 | "engines": { 189 | "node": ">=12" 190 | } 191 | }, 192 | "node_modules/@esbuild/linux-mips64el": { 193 | "version": "0.19.8", 194 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", 195 | "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", 196 | "cpu": [ 197 | "mips64el" 198 | ], 199 | "dev": true, 200 | "optional": true, 201 | "os": [ 202 | "linux" 203 | ], 204 | "engines": { 205 | "node": ">=12" 206 | } 207 | }, 208 | "node_modules/@esbuild/linux-ppc64": { 209 | "version": "0.19.8", 210 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", 211 | "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", 212 | "cpu": [ 213 | "ppc64" 214 | ], 215 | "dev": true, 216 | "optional": true, 217 | "os": [ 218 | "linux" 219 | ], 220 | "engines": { 221 | "node": ">=12" 222 | } 223 | }, 224 | "node_modules/@esbuild/linux-riscv64": { 225 | "version": "0.19.8", 226 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", 227 | "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", 228 | "cpu": [ 229 | "riscv64" 230 | ], 231 | "dev": true, 232 | "optional": true, 233 | "os": [ 234 | "linux" 235 | ], 236 | "engines": { 237 | "node": ">=12" 238 | } 239 | }, 240 | "node_modules/@esbuild/linux-s390x": { 241 | "version": "0.19.8", 242 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", 243 | "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", 244 | "cpu": [ 245 | "s390x" 246 | ], 247 | "dev": true, 248 | "optional": true, 249 | "os": [ 250 | "linux" 251 | ], 252 | "engines": { 253 | "node": ">=12" 254 | } 255 | }, 256 | "node_modules/@esbuild/linux-x64": { 257 | "version": "0.19.8", 258 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", 259 | "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", 260 | "cpu": [ 261 | "x64" 262 | ], 263 | "dev": true, 264 | "optional": true, 265 | "os": [ 266 | "linux" 267 | ], 268 | "engines": { 269 | "node": ">=12" 270 | } 271 | }, 272 | "node_modules/@esbuild/netbsd-x64": { 273 | "version": "0.19.8", 274 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", 275 | "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", 276 | "cpu": [ 277 | "x64" 278 | ], 279 | "dev": true, 280 | "optional": true, 281 | "os": [ 282 | "netbsd" 283 | ], 284 | "engines": { 285 | "node": ">=12" 286 | } 287 | }, 288 | "node_modules/@esbuild/openbsd-x64": { 289 | "version": "0.19.8", 290 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", 291 | "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", 292 | "cpu": [ 293 | "x64" 294 | ], 295 | "dev": true, 296 | "optional": true, 297 | "os": [ 298 | "openbsd" 299 | ], 300 | "engines": { 301 | "node": ">=12" 302 | } 303 | }, 304 | "node_modules/@esbuild/sunos-x64": { 305 | "version": "0.19.8", 306 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", 307 | "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", 308 | "cpu": [ 309 | "x64" 310 | ], 311 | "dev": true, 312 | "optional": true, 313 | "os": [ 314 | "sunos" 315 | ], 316 | "engines": { 317 | "node": ">=12" 318 | } 319 | }, 320 | "node_modules/@esbuild/win32-arm64": { 321 | "version": "0.19.8", 322 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", 323 | "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", 324 | "cpu": [ 325 | "arm64" 326 | ], 327 | "dev": true, 328 | "optional": true, 329 | "os": [ 330 | "win32" 331 | ], 332 | "engines": { 333 | "node": ">=12" 334 | } 335 | }, 336 | "node_modules/@esbuild/win32-ia32": { 337 | "version": "0.19.8", 338 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", 339 | "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", 340 | "cpu": [ 341 | "ia32" 342 | ], 343 | "dev": true, 344 | "optional": true, 345 | "os": [ 346 | "win32" 347 | ], 348 | "engines": { 349 | "node": ">=12" 350 | } 351 | }, 352 | "node_modules/@esbuild/win32-x64": { 353 | "version": "0.19.8", 354 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", 355 | "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", 356 | "cpu": [ 357 | "x64" 358 | ], 359 | "dev": true, 360 | "optional": true, 361 | "os": [ 362 | "win32" 363 | ], 364 | "engines": { 365 | "node": ">=12" 366 | } 367 | }, 368 | "node_modules/esbuild": { 369 | "version": "0.19.8", 370 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", 371 | "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", 372 | "dev": true, 373 | "hasInstallScript": true, 374 | "bin": { 375 | "esbuild": "bin/esbuild" 376 | }, 377 | "engines": { 378 | "node": ">=12" 379 | }, 380 | "optionalDependencies": { 381 | "@esbuild/android-arm": "0.19.8", 382 | "@esbuild/android-arm64": "0.19.8", 383 | "@esbuild/android-x64": "0.19.8", 384 | "@esbuild/darwin-arm64": "0.19.8", 385 | "@esbuild/darwin-x64": "0.19.8", 386 | "@esbuild/freebsd-arm64": "0.19.8", 387 | "@esbuild/freebsd-x64": "0.19.8", 388 | "@esbuild/linux-arm": "0.19.8", 389 | "@esbuild/linux-arm64": "0.19.8", 390 | "@esbuild/linux-ia32": "0.19.8", 391 | "@esbuild/linux-loong64": "0.19.8", 392 | "@esbuild/linux-mips64el": "0.19.8", 393 | "@esbuild/linux-ppc64": "0.19.8", 394 | "@esbuild/linux-riscv64": "0.19.8", 395 | "@esbuild/linux-s390x": "0.19.8", 396 | "@esbuild/linux-x64": "0.19.8", 397 | "@esbuild/netbsd-x64": "0.19.8", 398 | "@esbuild/openbsd-x64": "0.19.8", 399 | "@esbuild/sunos-x64": "0.19.8", 400 | "@esbuild/win32-arm64": "0.19.8", 401 | "@esbuild/win32-ia32": "0.19.8", 402 | "@esbuild/win32-x64": "0.19.8" 403 | } 404 | }, 405 | "node_modules/hookui-framework": { 406 | "version": "0.1.0", 407 | "resolved": "git+ssh://git@github.com/captain-of-coit/hookui-framework.git#894649447e7432cec90ec5520a5cdc7690e9fceb", 408 | "dev": true, 409 | "license": "MIT", 410 | "dependencies": { 411 | "esbuild": "^0.19.7", 412 | "react": "^18.2.0" 413 | } 414 | }, 415 | "node_modules/js-tokens": { 416 | "version": "4.0.0", 417 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 418 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 419 | "dev": true 420 | }, 421 | "node_modules/loose-envify": { 422 | "version": "1.4.0", 423 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 424 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 425 | "dev": true, 426 | "dependencies": { 427 | "js-tokens": "^3.0.0 || ^4.0.0" 428 | }, 429 | "bin": { 430 | "loose-envify": "cli.js" 431 | } 432 | }, 433 | "node_modules/react": { 434 | "version": "18.2.0", 435 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 436 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 437 | "dev": true, 438 | "dependencies": { 439 | "loose-envify": "^1.1.0" 440 | }, 441 | "engines": { 442 | "node": ">=0.10.0" 443 | } 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cs2-vehicle-counter", 3 | "version": "1.0.0", 4 | "description": "This repository template allows you to get started with Cities: Skylines 2 modding easily, all the way to building your mod on commit with GitHub Actions and publishing your mod automatically on Thunderstore.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Captain-of-Coit", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "esbuild": "^0.19.8", 14 | "hookui-framework": "github:captain-of-coit/hookui-framework" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui_src/commercial.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const DemandSection2 = ({title, value, factors }) => { 6 | return ( 7 |
8 | {/* title */} 9 |
10 |
{title}
11 | { value >= 0 && (
{Math.round(value*100)}
) } 12 |
13 |
14 | {/* factors */} 15 | {factors.map((item, index) => ( 16 |
17 |
{item["factor"]}
18 |
19 | {item["weight"] < 0 ? 20 |
{item["weight"]}
: 21 |
{item["weight"]}
} 22 |
23 |
24 | ))} 25 |
26 | ); 27 | }; 28 | 29 | const RowWithTwoColumns = ({left, right}) => { 30 | return ( 31 |
32 |
{left}
33 |
{right}
34 |
35 | ); 36 | }; 37 | 38 | const RowWithThreeColumns = ({left, leftSmall, right1, flag1, right2, flag2}) => { 39 | const centerStyle = { 40 | width: right2 === undefined ? '40%' : '20%', 41 | justifyContent: 'center', 42 | }; 43 | const right1text = `${right1} %`; 44 | const right2text = `${right2} %`; 45 | return ( 46 |
47 |
48 |

{left}

49 |

{leftSmall}

50 |
51 | {flag1 ? 52 |
{right1text}
: 53 |
{right1text}
} 54 | {right2 && ( 55 | flag2 ? 56 |
{right2text}
: 57 |
{right2text}
)} 58 |
59 | ); 60 | }; 61 | 62 | // simple horizontal line 63 | const DataDivider = () => { 64 | return ( 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | const ColumnCommercialData = ({ data }) => { 72 | return ( 73 |
74 | 75 |
76 |
EMPTY BUILDINGS
77 |
{data[0]}
78 |
79 |
80 |
PROPERTYLESS COMPANIES
81 |
{data[1]}
82 |
83 | 84 | 85 | 86 | 100} /> 87 | 88 | 89 | 90 |
91 |
92 |
Standard
93 |
Leisure
94 |
95 | 96 | 97 | 100} right2={data[6]} flag2={data[6]>100} /> 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | AVAILABLE WORKFORCE 108 |
109 |
110 | 111 | 112 |
113 |
114 |
115 | ); 116 | }; 117 | 118 | const ColumnExcludedResources = ({ resources }) => { 119 | return ( 120 |
121 |
No demand for:
122 |
    123 | {resources.map((item, index) => ( 124 |
  • 125 |
    {item}
    126 |
  • 127 | ))} 128 |
129 |
130 | ); 131 | }; 132 | 133 | const $Commercial = ({ react }) => { 134 | 135 | // commercial data 136 | const [commercialData, setCommercialData] = react.useState([]) 137 | useDataUpdate(react, 'cityInfo.ilCommercial', setCommercialData) 138 | 139 | // excluded resources 140 | const [excludedResources, setExcludedResources] = react.useState([]) 141 | useDataUpdate(react, 'cityInfo.ilCommercialExRes', setExcludedResources) 142 | 143 | const onClose = () => { 144 | // HookUI 145 | //const data = { type: "toggle_visibility", id: 'infoloom.commercial' }; 146 | //const event = new CustomEvent('hookui', { detail: data }); 147 | //window.dispatchEvent(event); 148 | // Gooee 149 | engine.trigger("infoloom.infoloom.OnToggleVisibleCommercial", "Commercial"); 150 | engine.trigger("audio.playSound", "close-panel", 1); 151 | }; 152 | 153 | return <$Panel react={react} title="Commercial Data" onClose={onClose} initialSize={{ width: window.innerWidth * 0.25, height: window.innerHeight * 0.26 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 154 | {commercialData.length === 0 ? ( 155 |

Waiting...

156 | ) : ( 157 |
158 | 159 | 160 |
161 | )} 162 | 163 | }; 164 | 165 | export default $Commercial 166 | 167 | // Registering the panel with HookUI so it shows up in the menu 168 | /* 169 | window._$hookui.registerPanel({ 170 | id: "infoloom.commercial", 171 | name: "InfoLoom: Commercial Data", 172 | icon: "Media/Game/Icons/ZoneCommercial.svg", 173 | component: $Commercial 174 | }); 175 | */ -------------------------------------------------------------------------------- /ui_src/demandfactors.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const AlignedParagraph = ({ left, right }) => { 6 | // Set color based on value of left 7 | let color; 8 | if (left < -50) { 9 | color = 'red'; 10 | } else if (left > 50) { 11 | color = '#00CC00'; 12 | } else { 13 | color = 'white'; // default 14 | }; 15 | const containerStyle = { 16 | display: 'flex', 17 | justifyContent: 'space-between', 18 | textAlign: 'justify', 19 | marginBottom: '0.1em', // Add some spacing between the

tags 20 | }; 21 | const leftTextStyle = { 22 | color: color, 23 | fontSize: '80%', 24 | width: '20%', 25 | marginLeft: '10%', // Start 10% from the left edge 26 | }; 27 | const rightTextStyle = { 28 | fontSize: '80%', 29 | width: '60%', 30 | marginRight: '10%', // Start 10% from the right edge 31 | textAlign: 'right', 32 | }; 33 | return ( 34 |

35 | {left} 36 | {right} 37 |

38 | ); 39 | }; 40 | 41 | 42 | 43 | const DemandSection2 = ({title, value, factors }) => { 44 | return ( 45 |
46 | {/* title */} 47 |
48 |
{title}
49 | { value >= 0 && (
{Math.round(value*100)}
) } 50 |
51 |
52 | {/* factors */} 53 | {factors.map((item, index) => ( 54 |
55 |
{item["factor"]}
56 |
57 | {item["weight"] < 0 ? 58 |
{item["weight"]}
: 59 |
{item["weight"]}
} 60 |
61 |
62 | ))} 63 |
64 | ); 65 | }; 66 | 67 | const DemandSection1 = ({ title, value, factors }) => { 68 | // this is for 2 columns 69 | //
70 | return ( 71 |
72 |
73 |

{title}: {Math.round(value*100)}

74 |
75 |
76 |
    77 | {factors.map((item, index) => ( 78 |
  1. 79 | 80 |
  2. 81 | ))} 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | 89 | 90 | const $DemandFactors = ({ react }) => { 91 | 92 | // demand values are just single numbers 93 | const [residentialLowDemand, setResidentialLowDemand] = react.useState(0) 94 | useDataUpdate(react, 'cityInfo.residentialLowDemand', setResidentialLowDemand) 95 | const [residentialMediumDemand, setResidentialMediumDemand] = react.useState(0) 96 | useDataUpdate(react, 'cityInfo.residentialMediumDemand', setResidentialMediumDemand) 97 | const [residentialHighDemand, setResidentialHighDemand] = react.useState(0) 98 | useDataUpdate(react, 'cityInfo.residentialHighDemand', setResidentialHighDemand) 99 | const [commercialDemand, setCommercialDemand] = react.useState(0) 100 | useDataUpdate(react, 'cityInfo.commercialDemand', setCommercialDemand) 101 | const [industrialDemand, setIndustrialDemand] = react.useState(0) 102 | useDataUpdate(react, 'cityInfo.industrialDemand', setIndustrialDemand) 103 | const [officeDemand, setOfficeDemand] = react.useState(0) 104 | useDataUpdate(react, 'cityInfo.officeDemand', setOfficeDemand) 105 | 106 | // demand factors: an array of variable number of elements with properties: __Type, factor, weight 107 | const [residentialLowFactors, setResidentialLowFactors] = react.useState([]) 108 | useDataUpdate(react, 'cityInfo.residentialLowFactors', setResidentialLowFactors) 109 | const [residentialMediumFactors, setResidentialMediumFactors] = react.useState([]) 110 | useDataUpdate(react, 'cityInfo.residentialMediumFactors', setResidentialMediumFactors) 111 | const [residentialHighFactors, setResidentialHighFactors] = react.useState([]) 112 | useDataUpdate(react, 'cityInfo.residentialHighFactors', setResidentialHighFactors) 113 | const [commercialFactors, setCommercialFactors] = react.useState([]) 114 | useDataUpdate(react, 'cityInfo.commercialFactors', setCommercialFactors) 115 | const [industrialFactors, setIndustrialFactors] = react.useState([]) 116 | useDataUpdate(react, 'cityInfo.industrialFactors', setIndustrialFactors) 117 | const [officeFactors, setOfficeFactors] = react.useState([]) 118 | useDataUpdate(react, 'cityInfo.officeFactors', setOfficeFactors) 119 | 120 | // building demand 121 | const [buildingDemand, setbuildingDemand] = react.useState([]) 122 | useDataUpdate(react, 'cityInfo.ilBuildingDemand', setbuildingDemand) 123 | 124 | // convert buildingDemand array into "demand factors" 125 | const titles = ['Residential Low','Residential Medium','Residential High','Commercial','Industrial','Storage','Office']; 126 | const buildingDemandFactors = titles.map((factor, index) => ({ factor, weight: buildingDemand[index] })); 127 | 128 | const onClose = () => { 129 | // HookUI 130 | //const data = { type: "toggle_visibility", id: 'infoloom.demandfactors' }; 131 | //const event = new CustomEvent('hookui', { detail: data }); 132 | //window.dispatchEvent(event); 133 | // Gooee 134 | engine.trigger("infoloom.infoloom.OnToggleVisibleDemand", "Demand"); 135 | engine.trigger("audio.playSound", "close-panel", 1); 136 | }; 137 | 138 | return <$Panel react={react} title="Demand" onClose={onClose} initialSize={{ width: window.innerWidth * 0.1, height: window.innerHeight * 0.83 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | }; 148 | 149 | export default $DemandFactors 150 | 151 | // Registering the panel with HookUI so it shows up in the menu 152 | /* 153 | window._$hookui.registerPanel({ 154 | id: "infoloom.demandfactors", 155 | name: "InfoLoom: Demand Factors", 156 | icon: "Media/Game/Icons/ZoningDemand.svg", 157 | component: $DemandFactors 158 | }); 159 | */ -------------------------------------------------------------------------------- /ui_src/demographics.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const AlignedParagraph = ({ left, right }) => { 6 | const containerStyle = { 7 | display: 'flex', 8 | justifyContent: 'space-between', 9 | textAlign: 'justify', 10 | marginBottom: '0.25em', // Add some spacing between the

tags 11 | }; 12 | const leftTextStyle = { 13 | width: '40%', 14 | marginLeft: '10%', // Start 10% from the left edge 15 | }; 16 | const rightTextStyle = { 17 | width: '40%', 18 | marginRight: '10%', // Start 10% from the right edge 19 | textAlign: 'right', 20 | }; 21 | return ( 22 |

23 |
{left}
24 |
{right}
25 |
26 | ); 27 | }; 28 | 29 | // length - maximum length that corresponds with base 30 | // base - represents entire bar 31 | // xpos, ypos - location of the bar 32 | const PopulationBarSVG = ({ react, xpos, ypos, length, base, info}) => { 33 | const barH = 12; 34 | const barY = ypos + info.age * barH; // TODO: bar height 35 | const x_work = length*info.work/base; 36 | const x_school1 = length*info.school1/base; 37 | const x_school2 = length*info.school2/base; 38 | const x_school3 = length*info.school3/base; 39 | const x_school4 = length*info.school4/base; 40 | const x_other = length*info.other/base; 41 | 42 | return ( 43 | <> 44 | {info.age} 45 | {info.total} 46 | // work, pastel blue 47 | // elementary, pale lime 48 | // high school, mint green 49 | // college, light blue 50 | // university, lavender blue 51 | // other, light brown 52 | 53 | ); 54 | }; 55 | 56 | // length - maximum length that corresponds with base 57 | // base - represents entire bar 58 | // xpos, ypos - location of the bar 59 | const PopulationBar = ({ legend, length, base, info, barH}) => { 60 | const x_age = legend * 20/100; 61 | const x_total = legend * 80/100; 62 | const x_work = length*info.work/base; 63 | const x_school1 = length*info.school1/base; 64 | const x_school2 = length*info.school2/base; 65 | const x_school3 = length*info.school3/base; 66 | const x_school4 = length*info.school4/base; 67 | const x_other = length*info.other/base; 68 | 69 | return ( 70 |
71 |
{info.age}
72 |
{info.total}
73 |
{/* light brown */} 74 |
{/* pale lime */} 75 |
{/* mint green */} 76 |
{/* turquoise */} 77 |
{/* bright blue */} 78 |
{/* silver gray */} 79 |
80 | ); 81 | }; 82 | 83 | 84 | const $Demographics = ({react}) => { 85 | 86 | // 0 - num citizens in the city 0 = 1+2+3 87 | // 1 - num locals 88 | // 2 - num tourists 89 | // 3 - num commuters 90 | // 4 - num students (in locals) 4 <= 1 91 | // 5 - num workers (in locals) 5 <= 1 92 | // 6 - oldest cim 93 | const [totals, setTotals] = react.useState([]) 94 | useDataUpdate(react, 'populationInfo.structureTotals', setTotals) 95 | 96 | const [details, setDetails] = react.useState([]) 97 | useDataUpdate(react, 'populationInfo.structureDetails', setDetails) 98 | 99 | const onClose = () => { 100 | // 101 | //const data = { type: "toggle_visibility", id: 'infoloom.demographics' }; 102 | //const event = new CustomEvent('hookui', { detail: data }); 103 | //window.dispatchEvent(event); 104 | // Gooee 105 | engine.trigger("infoloom.infoloom.OnToggleVisibleDemographics", "Demographics"); 106 | engine.trigger("audio.playSound", "close-panel", 1); 107 | }; 108 | 109 | const panWidth = window.innerWidth * 0.20; 110 | const panHeight = window.innerHeight * 0.86; 111 | const barHeight = panHeight * 0.77 / 110; // TODO: 110 is number of bars - should correspond with backend 112 | const lineSpan = panWidth * 0.9 / 5; // 1000 pop lines span 113 | 114 | const gridLines = Array.from({ length: 5 }, (_, index) => ( 115 | 120 | )); 121 | 122 | // find the largest bar and scale it to fit the window properly 123 | const largestBar = details.length > 0 ? Math.max(...details.map(info => info.total)) : 0; 124 | const barBase = largestBar < 100 ? 100 : 50 * Math.ceil(largestBar/50); 125 | //console.log(largestBar, divisor, barBase); 126 | 127 | return <$Panel react={react} title="Demographics" onClose={onClose} initialSize={{ width: panWidth, height: panHeight }} initialPosition={{ top: window.innerHeight * 0.009, left: window.innerWidth * 0.053 }}> 128 | 129 |
130 |
131 | 132 | 133 | 134 | 135 | 136 |
137 |
138 | 139 | 140 | 141 | 142 | 143 |
144 |
145 |
146 |
147 |
Work
{/* light brown */} 148 |
Elementary
{/* pale lime */} 149 |
High school
{/* mint green */} 150 |
College
{/* turquoise */} 151 |
University
{/* bright blue */} 152 |
Other
{/* silver gray */} 153 |
154 |
155 | 156 | {gridLines} 157 | {(() => { 158 | const bars = []; 159 | details.forEach( info => { 160 | bars.push( 161 | 162 | ); 163 | }); 164 | return bars; 165 | })()} 166 | 167 | 168 | {/* DEBUG 169 |
170 | {(() => { 171 | const paragraphs = []; 172 | details.forEach( info => { 173 | paragraphs.push(

{info["age"]} {info["total"]} {info["school1"]} {info["school2"]} {info["school3"]} {info["school4"]} {info["work"]} {info["other"]}

); 174 | }); 175 | return paragraphs; 176 | })()} 177 |
178 | */} 179 | 180 | 181 | } 182 | 183 | export default $Demographics 184 | 185 | // Registering the panel with HookUI so it shows up in the menu 186 | /* 187 | window._$hookui.registerPanel({ 188 | id: "infoloom.demographics", 189 | name: "InfoLoom: Demographics", 190 | icon: "Media/Game/Icons/Population.svg", 191 | component: $Demographics 192 | }) 193 | */ -------------------------------------------------------------------------------- /ui_src/gooee-menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "Icon": "Media/Game/Icons/ZoningDemand.svg", 3 | "IsFAIcon": false, 4 | "Label": "InfoLoom", 5 | "OnClick": "OnToggleVisible", 6 | "OnClickKey": "Default", 7 | "Children": [ 8 | { 9 | "Icon": "Media/Game/Icons/Population.svg", 10 | "IsFAIcon": false, 11 | "Label": "Demographics", 12 | "OnClick": "OnToggleVisibleDemographics", 13 | "OnClickKey": "Demographics" 14 | }, 15 | { 16 | "Icon": "Media/Game/Icons/Workers.svg", 17 | "IsFAIcon": false, 18 | "Label": "Workforce", 19 | "OnClick": "OnToggleVisibleWorkforce", 20 | "OnClickKey": "Workforce" 21 | }, 22 | { 23 | "Icon": "Media/Game/Icons/Workers.svg", 24 | "IsFAIcon": false, 25 | "Label": "Workplaces", 26 | "OnClick": "OnToggleVisibleWorkplaces", 27 | "OnClickKey": "Workplaces" 28 | }, 29 | { 30 | "Icon": "Media/Game/Icons/ZoningDemand.svg", 31 | "IsFAIcon": false, 32 | "Label": "Demand Factors", 33 | "OnClick": "OnToggleVisibleDemand", 34 | "OnClickKey": "Demand" 35 | }, 36 | { 37 | "Icon": "Media/Game/Icons/ZoneResidential.svg", 38 | "IsFAIcon": false, 39 | "Label": "Residential Data", 40 | "OnClick": "OnToggleVisibleResidential", 41 | "OnClickKey": "Residential" 42 | }, 43 | { 44 | "Icon": "Media/Game/Icons/ZoneCommercial.svg", 45 | "IsFAIcon": false, 46 | "Label": "Commercial Data", 47 | "OnClick": "OnToggleVisibleCommercial", 48 | "OnClickKey": "Commercial" 49 | }, 50 | { 51 | "Icon": "Media/Game/Icons/ZoneIndustrial.svg", 52 | "IsFAIcon": false, 53 | "Label": "Industrial and Office Data", 54 | "OnClick": "OnToggleVisibleIndustrial", 55 | "OnClickKey": "Industrial" 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /ui_src/industrial.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const RowWithTwoColumns = ({left, right}) => { 6 | return ( 7 |
8 |
{left}
9 |
{right}
10 |
11 | ); 12 | }; 13 | 14 | const RowWithThreeColumns = ({left, leftSmall, right1, flag1, right2, flag2}) => { 15 | const centerStyle = { 16 | width: right2 === undefined ? '40%' : '20%', 17 | justifyContent: 'center', 18 | }; 19 | const right1text = `${right1} %`; 20 | const right2text = `${right2} %`; 21 | return ( 22 |
23 |
24 |

{left}

25 |

{leftSmall}

26 |
27 | {flag1 ? 28 |
{right1text}
: 29 |
{right1text}
} 30 | {right2 && ( 31 | flag2 ? 32 |
{right2text}
: 33 |
{right2text}
)} 34 |
35 | ); 36 | }; 37 | 38 | // simple horizontal line 39 | const DataDivider = () => { 40 | return ( 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | // centered value, if flag exists then uses colors for negative/positive 48 | // width is 20% by default 49 | const SingleValue = ({ value, flag, width, small }) => { 50 | const rowClass = ( small ? "row_S2v small_ExK" : "row_S2v"); 51 | const centerStyle = { 52 | width: width === undefined ? '20%' : width, 53 | justifyContent: 'center', 54 | }; 55 | return ( 56 | flag === undefined ? ( 57 |
{value}
58 | ) : ( 59 | flag ? 60 |
{value}
: 61 |
{value}
) 62 | ); 63 | }; 64 | 65 | const ColumnIndustrialData = ({ data }) => { 66 | return ( 67 |
68 | 69 |
70 |
71 | 72 | 73 |
74 | 75 |
76 |
EMPTY BUILDINGS
77 | 78 | 79 |
80 |
81 |
PROPERTYLESS COMPANIES
82 |
{data[1]}
83 |
{data[11]}
84 |
85 | 86 | 87 | 88 | 100} right2={data[12]/10} flag2={data[12]>100} /> 89 | 90 | 91 | 92 | 100} /> 93 | 100} /> 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 |
103 | AVAILABLE WORKFORCE 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 | 113 |
114 |
115 |

STORAGE

116 |

The game will spawn warehouses when DEMANDED TYPES exist.

117 |
118 |
119 | 120 | 121 | 122 |
123 |
124 | 125 |
126 | ); 127 | }; 128 | 129 | const ColumnExcludedResources = ({ resources }) => { 130 | return ( 131 |
132 |
No demand for:
133 |
    134 | {resources.map((item, index) => ( 135 |
  • 136 |
    {item}
    137 |
  • 138 | ))} 139 |
140 |
141 | ); 142 | }; 143 | 144 | const $Industrial = ({ react }) => { 145 | 146 | // commercial data 147 | const [industrialData, setIndustrialData] = react.useState([]) 148 | useDataUpdate(react, 'cityInfo.ilIndustrial', setIndustrialData) 149 | 150 | // excluded resources 151 | const [excludedResources, setExcludedResources] = react.useState([]) 152 | useDataUpdate(react, 'cityInfo.ilIndustrialExRes', setExcludedResources) 153 | 154 | const onClose = () => { 155 | // HookUI 156 | //const data = { type: "toggle_visibility", id: 'infoloom.industrial' }; 157 | //const event = new CustomEvent('hookui', { detail: data }); 158 | //window.dispatchEvent(event); 159 | // Gooee 160 | engine.trigger("infoloom.infoloom.OnToggleVisibleIndustrial", "Industrial"); 161 | engine.trigger("audio.playSound", "close-panel", 1); 162 | }; 163 | 164 | return <$Panel react={react} title="Industrial and Office Data" onClose={onClose} initialSize={{ width: window.innerWidth * 0.30, height: window.innerHeight * 0.32 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 165 | {industrialData.length === 0 ? ( 166 |

Waiting...

167 | ) : ( 168 |
169 | 170 | 171 |
172 | )} 173 | 174 | }; 175 | 176 | export default $Industrial 177 | 178 | // Registering the panel with HookUI so it shows up in the menu 179 | /* 180 | window._$hookui.registerPanel({ 181 | id: "infoloom.industrial", 182 | name: "InfoLoom: Industrial and Office Data", 183 | icon: "Media/Game/Icons/ZoneIndustrial.svg", 184 | component: $Industrial 185 | }); 186 | */ -------------------------------------------------------------------------------- /ui_src/panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const defaultStyle = { 4 | position: 'absolute', 5 | width: "300rem", 6 | height: "600rem", 7 | } 8 | 9 | const Resizer = ({ onMouseDown }) => { 10 | const style = { 11 | position: 'absolute', 12 | bottom: 0, 13 | right: 0, 14 | width: '20px', 15 | height: '20px', 16 | cursor: 'nwse-resize', 17 | zIndex: 10, 18 | } 19 | const triangle = { 20 | width: 20, 21 | height: 20, 22 | opacity: 0.2, 23 | //background: 'linear-gradient(to right bottom, transparent 0%, transparent 50%, var(--accentColorDark) 50%, var(--accentColorDark) 100%)' 24 | //background: linear-gradient(45deg, to right, to bottom, transparent 0%, transparent 50%, var(--accentColorDark) 50%, var(--accentColorDark) 100%) 25 | //background-repeat: no-repeat, 26 | //background-size: 100% 100% 27 | } 28 | return
29 | {/* */} 30 |
31 |
32 | }; 33 | 34 | const $CloseButton = ({onClick}) => { 35 | return 38 | } 39 | 40 | const $Panel = ({ 41 | react, 42 | children, 43 | title, 44 | style, 45 | onClose, 46 | initialPosition, 47 | initialSize, 48 | onPositionChange = () => {}, 49 | onSizeChange = () => {}, 50 | }) => { 51 | // TODO these two should be settable by parent 52 | const [position, setPosition] = react.useState(initialPosition || { top: 100, left: 10 }); 53 | const [size, setSize] = react.useState(initialSize || { width: 300, height: 600 }); 54 | 55 | const initialSizeRef = react.useRef({ width: 0, height: 0 }); 56 | const [dragging, setDragging] = react.useState(false); 57 | const [resizing, setResizing] = react.useState(false); 58 | const [rel, setRel] = react.useState({ x: 0, y: 0 }); // Position relative to the cursor 59 | 60 | const onMouseDown = (e) => { 61 | if (e.button !== 0) return; 62 | setDragging(true); 63 | const panelElement = e.target.closest(".panel_YqS"); 64 | const rect = panelElement.getBoundingClientRect(); 65 | setRel({ 66 | x: e.clientX - rect.left, 67 | y: e.clientY - rect.top, 68 | }); 69 | e.stopPropagation(); 70 | e.preventDefault(); 71 | } 72 | 73 | const onMouseUp = () => { 74 | setDragging(false); 75 | setResizing(false); 76 | window.removeEventListener('mousemove', onMouseMove); 77 | window.removeEventListener('mouseup', onMouseUp); 78 | window.removeEventListener('mousemove', onResizeMouseMove); 79 | } 80 | 81 | const onMouseMove = (e) => { 82 | if (!dragging || resizing) return; 83 | 84 | const newTop = e.clientY - rel.y; 85 | const newLeft = e.clientX - rel.x; 86 | 87 | const newPosition = { 88 | top: newTop > 0 ? newTop : 0, 89 | left: newLeft > 0 ? newLeft : 0, 90 | }; 91 | 92 | setPosition(newPosition); 93 | onPositionChange(newPosition); 94 | e.stopPropagation(); 95 | e.preventDefault(); 96 | } 97 | 98 | const onResizeMouseDown = (e) => { 99 | setResizing(true); 100 | initialSizeRef.current = { width: size.width, height: size.height }; // Store initial size 101 | setRel({ x: e.clientX, y: e.clientY }); 102 | e.stopPropagation(); 103 | e.preventDefault(); 104 | } 105 | 106 | const onResizeMouseMove = (e) => { 107 | if (!resizing) return; 108 | 109 | const widthChange = e.clientX - rel.x; 110 | const heightChange = e.clientY - rel.y; 111 | const newSize = { 112 | width: Math.max(initialSizeRef.current.width + widthChange, 100), 113 | height: Math.max(initialSizeRef.current.height + heightChange, 100) 114 | }; 115 | setSize(newSize); 116 | onSizeChange(newSize); 117 | setRel({ x: e.clientX, y: e.clientY }); 118 | e.stopPropagation(); 119 | e.preventDefault(); 120 | } 121 | 122 | react.useEffect(() => { 123 | if (dragging || resizing) { 124 | window.addEventListener('mousemove', dragging ? onMouseMove : onResizeMouseMove); 125 | window.addEventListener('mouseup', onMouseUp); 126 | } 127 | 128 | return () => { 129 | window.removeEventListener('mousemove', dragging ? onMouseMove : onResizeMouseMove); 130 | window.removeEventListener('mouseup', onMouseUp); 131 | }; 132 | }, [dragging, resizing]); 133 | 134 | const draggableStyle = { 135 | ...defaultStyle, 136 | ...style, 137 | top: `${position.top}px`, 138 | left: `${position.left}px`, 139 | width: `${size.width}px`, 140 | height: `${size.height}px` 141 | } 142 | 143 | return ( 144 |
145 |
147 |
148 |
149 |
{title}
150 | <$CloseButton onClick={onClose}/> 151 |
152 |
153 |
154 | {children} 155 |
156 | 157 |
158 | ); 159 | } 160 | 161 | export default $Panel -------------------------------------------------------------------------------- /ui_src/residential.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const RowWithTwoColumns = ({left, right}) => { 6 | return ( 7 |
8 |
{left}
9 |
{right}
10 |
11 | ); 12 | }; 13 | 14 | const RowWithThreeColumns = ({left, leftSmall, right1, flag1, right2, flag2}) => { 15 | const centerStyle = { 16 | width: right2 === undefined ? '30%' : '15%', 17 | justifyContent: 'center', 18 | }; 19 | const right1text = `${right1}`; 20 | const right2text = `${right2}`; 21 | return ( 22 |
23 |
24 |

{left}

25 |

{leftSmall}

26 |
27 | {flag1 ? 28 |
{right1text}
: 29 |
{right1text}
} 30 | {right2 && ( 31 | flag2 ? 32 |
{right2text}
: 33 |
{right2text}
)} 34 |
35 | ); 36 | }; 37 | 38 | // simple horizontal line 39 | const DataDivider = () => { 40 | return ( 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | // centered value, if flag exists then uses colors for negative/positive 48 | // width is 20% by default 49 | const SingleValue = ({ value, flag, width, small }) => { 50 | const rowClass = ( small ? "row_S2v small_ExK" : "row_S2v"); 51 | const centerStyle = { 52 | width: width === undefined ? '16%' : width, 53 | justifyContent: 'center', 54 | }; 55 | return ( 56 | flag === undefined ? ( 57 |
{value}
58 | ) : ( 59 | flag ? 60 |
{value}
: 61 |
{value}
) 62 | ); 63 | }; 64 | 65 | const BuildingDemandSection = ({ data }) => { 66 | const freeL = data[0]-data[3]; 67 | const freeM = data[1]-data[4]; 68 | const freeH = data[2]-data[5]; 69 | const ratio = data[6]/10; 70 | const ratioString = `$No demand at {ratio}%`; 71 | const needL = Math.max(1, Math.floor(ratio * data[0] / 100)); 72 | const needM = Math.max(1, Math.floor(ratio * data[1] / 100)); 73 | const needH = Math.max(1, Math.floor(ratio * data[2] / 100)); 74 | const demandL = Math.floor((1 - freeL / needL) * 100); 75 | const demandM = Math.floor((1 - freeM / needM) * 100); 76 | const demandH = Math.floor((1 - freeH / needH) * 100); 77 | // calculate totals and free ratio 78 | const totalRes = data[0] + data[1] + data[2]; 79 | const totalOcc = data[3] + data[4] + data[5]; 80 | const totalFree = totalRes - totalOcc; 81 | const freeRatio = (totalRes > 0 ? Math.round(1000*totalFree/totalRes)/10 : 0); 82 | return ( 83 |
84 |
85 |
86 | 87 | 88 | 89 | 90 |
91 |
92 |
93 |
Total properties
94 | 95 | 96 | 97 | 98 |
99 |
100 |
101 |
- Occupied properties
102 | 103 | 104 | 105 | 106 |
107 |
108 |
109 |
= Empty properties
110 | needL} /> 111 | needM} /> 112 | needH} /> 113 | 114 |
115 |
116 |
117 |
{"No demand at " + ratio + "%"}
118 | 119 | 120 | 121 |
122 |
123 |
124 |
125 |
BUILDING DEMAND
126 | 127 | 128 | 129 | 130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | const $Residential = ({ react }) => { 137 | 138 | // residential data 139 | const [residentialData, setResidentialData] = react.useState([]) 140 | useDataUpdate(react, 'cityInfo.ilResidential', setResidentialData) 141 | 142 | const onClose = () => { 143 | // HookUI 144 | //const data = { type: "toggle_visibility", id: 'infoloom.residential' }; 145 | //const event = new CustomEvent('hookui', { detail: data }); 146 | //window.dispatchEvent(event); 147 | // Gooee 148 | engine.trigger("infoloom.infoloom.OnToggleVisibleResidential", "Residential"); 149 | engine.trigger("audio.playSound", "close-panel", 1); 150 | }; 151 | 152 | const homelessThreshold = Math.round(residentialData[12] * residentialData[13] / 1000); 153 | 154 | return <$Panel react={react} title="Residential Data" onClose={onClose} initialSize={{ width: window.innerWidth * 0.25, height: window.innerHeight * 0.26 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 155 | {residentialData.length === 0 ? ( 156 |

Waiting...

157 | ) : ( 158 |
159 | 160 | {/* OTHER DATA, two columns */} 161 |
162 |
163 |
164 | 165 | 166 | 167 | 168 | residentialData[10]/10} /> 169 | 170 | 171 |
172 |
173 |
174 |
175 | 176 | 177 | homelessThreshold} /> 178 | 179 | 100} /> 180 | 181 | 182 |
183 |
184 |
185 |
186 | )} 187 | 188 | }; 189 | 190 | export default $Residential 191 | 192 | // Registering the panel with HookUI so it shows up in the menu 193 | /* 194 | window._$hookui.registerPanel({ 195 | id: "infoloom.residential", 196 | name: "InfoLoom: Residential Data", 197 | icon: "Media/Game/Icons/ZoneResidential.svg", 198 | component: $Residential 199 | }); 200 | */ -------------------------------------------------------------------------------- /ui_src/styles.css: -------------------------------------------------------------------------------- 1 | /* Simple fix to make old version of InfoLoom compatible with Gooee */ 2 | .gooee .fixdiv div { 3 | width: initial; 4 | } 5 | -------------------------------------------------------------------------------- /ui_src/ui.jsx: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | import $Demographics from "./demographics"; 3 | import $Workforce from "./workforce"; 4 | import $Workplaces from "./workplaces"; 5 | import $DemandFactors from "./demandfactors"; 6 | import $Residential from "./residential"; 7 | import $Commercial from "./commercial"; 8 | import $Industrial from "./industrial"; 9 | 10 | // Gooee 11 | const MyWindow = ({ react, setupController }) => { 12 | const { model, update, trigger, _L } = setupController(); 13 | 14 | return ( 15 |
16 |
{model.IsVisibleDemographics ? <$Demographics react={react} /> : null}
17 |
{model.IsVisibleWorkforce ? <$Workforce react={react} /> : null}
18 |
{model.IsVisibleWorkplaces ? <$Workplaces react={react} /> : null}
19 |
{model.IsVisibleDemand ? <$DemandFactors react={react} /> : null}
20 |
{model.IsVisibleResidential ? <$Residential react={react} /> : null}
21 |
{model.IsVisibleCommercial ? <$Commercial react={react} /> : null}
22 |
{model.IsVisibleIndustrial ? <$Industrial react={react} /> : null}
23 |
24 | ); 25 | }; 26 | window.$_gooee.register("infoloom", "MyWindow", MyWindow, "main-container", "infoloom"); 27 | -------------------------------------------------------------------------------- /ui_src/use-data-update.js: -------------------------------------------------------------------------------- 1 | // Credits: Captain-Of-Coit 2 | // The function is from 'hookui-framework' 3 | // https://github.com/Captain-Of-Coit/hookui-framework/blob/master/src/helpers/use-data-update.js 4 | 5 | const useDataUpdate = (react, event, onUpdate, deps) => { 6 | return react.useEffect(() => { 7 | const updateEvent = event + ".update" 8 | const subscribeEvent = event + ".subscribe" 9 | const unsubscribeEvent = event + ".unsubscribe" 10 | 11 | var sub = engine.on(updateEvent, (data) => { 12 | onUpdate && onUpdate(data) 13 | }) 14 | 15 | engine.trigger(subscribeEvent) 16 | return () => { 17 | engine.trigger(unsubscribeEvent) 18 | sub.clear() 19 | }; 20 | }, deps || []) 21 | } 22 | 23 | export default useDataUpdate -------------------------------------------------------------------------------- /ui_src/workforce.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const AlignedParagraph = ({ left, right }) => { 6 | const containerStyle = { 7 | display: 'flex', 8 | justifyContent: 'space-between', 9 | textAlign: 'justify', 10 | marginBottom: '0.25em', // Add some spacing between the

tags 11 | }; 12 | const leftTextStyle = { 13 | width: '40%', 14 | marginLeft: '10%', // Start 10% from the left edge 15 | }; 16 | const rightTextStyle = { 17 | width: '40%', 18 | marginRight: '10%', // Start 10% from the right edge 19 | textAlign: 'right', 20 | }; 21 | return ( 22 |

23 | {left} 24 | {right} 25 |

26 | ); 27 | }; 28 | 29 | const HorizontalLine = ({ length }) => { 30 | const lineStyle = { 31 | width: `${length}px`, 32 | borderBottom: '5px solid white', // Adjust the border style as needed 33 | margin: '1px 0', // Adjust the margin as needed 34 | }; 35 | return
; 36 | }; 37 | 38 | // Define styles outside the component 39 | const tableStyles = { 40 | tableContainer: { 41 | margin: '20px', 42 | }, 43 | customTable: { 44 | borderCollapse: 'collapse', 45 | //width: '100%', 46 | }, 47 | tableCell: { 48 | border: '1px solid #ddd', 49 | padding: '8px', 50 | textAlign: 'center', 51 | width: '10%', 52 | }, 53 | tableHeader: { 54 | backgroundColor: '#f2f2f2', 55 | }, 56 | }; 57 | 58 | 59 | const WorkforceLevel = ({levelColor, levelName, levelValues, total}) => { 60 | //console.log(levelColor); console.log(levelName); console.log(levelValues); 61 | //
62 | const percent = ( total > 0 ? (100*levelValues.total/total).toFixed(1)+"%" : ""); 63 | const unemployment = ( levelValues.total > 0 ? (100*levelValues.unemployed/levelValues.total).toFixed(1)+"%" : ""); 64 | return ( 65 |
66 |
67 |
68 |
69 |
{levelName}
70 |
71 |
{levelValues["total"]}
72 |
{percent}
73 |
{levelValues["worker"]}
74 |
{levelValues["unemployed"]}
75 |
{unemployment}
76 |
{levelValues["under"]}
77 |
{levelValues["outside"]}
78 |
{levelValues["homeless"]}
79 |
80 | ); 81 | }; 82 | 83 | const $Workforce = ({react}) => { 84 | 85 | const [workforce, setWorkforce] = react.useState([]) 86 | useDataUpdate(react, 'populationInfo.ilWorkforce', setWorkforce) 87 | 88 | const onClose = () => { 89 | // HookUI 90 | //const data = { type: "toggle_visibility", id: 'infoloom.workforce' }; 91 | //const event = new CustomEvent('hookui', { detail: data }); 92 | //window.dispatchEvent(event); 93 | // Gooee 94 | engine.trigger("infoloom.infoloom.OnToggleVisibleWorkforce", "Workforce"); 95 | engine.trigger("audio.playSound", "close-panel", 1); 96 | }; 97 | 98 | const headers = { 99 | total: 'Total', 100 | worker: 'Workers', 101 | unemployed: 'Unemployed', 102 | homeless: 'Homeless', 103 | employable: 'Employable', 104 | under: 'Under', 105 | outside: 'Outside', 106 | }; 107 | 108 | //if (workplaces.length !== 0) { 109 | // console.log("workplaces=", workplaces); 110 | //console.log(workplaces[0]); console.log(workplaces[1]); 111 | //} else { 112 | // console.log("workplaces has 0 elements"); 113 | // } 114 | 115 | return <$Panel react={react} title="Workforce Structure" onClose={onClose} initialSize={{ width: window.innerWidth * 0.33, height: window.innerHeight * 0.20 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 116 | {workforce.length === 0 ? ( 117 |

Waiting...

118 | ) : ( 119 |
120 |
121 | 122 |
123 | 124 | 125 | 126 | 127 | 128 |
129 | 130 |
131 | )} 132 | 133 | 134 | } 135 | 136 | export default $Workforce 137 | 138 | // Registering the panel with HookUI so it shows up in the menu 139 | /* 140 | window._$hookui.registerPanel({ 141 | id: "infoloom.workforce", 142 | name: "InfoLoom: Workforce", 143 | icon: "Media/Game/Icons/Workers.svg", 144 | component: $Workforce 145 | }) 146 | */ -------------------------------------------------------------------------------- /ui_src/workplaces.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useDataUpdate from './use-data-update.js' 3 | import $Panel from './panel' 4 | 5 | const AlignedParagraph = ({ left, right }) => { 6 | const containerStyle = { 7 | display: 'flex', 8 | justifyContent: 'space-between', 9 | textAlign: 'justify', 10 | marginBottom: '0.25em', // Add some spacing between the

tags 11 | }; 12 | const leftTextStyle = { 13 | width: '40%', 14 | marginLeft: '10%', // Start 10% from the left edge 15 | }; 16 | const rightTextStyle = { 17 | width: '40%', 18 | marginRight: '10%', // Start 10% from the right edge 19 | textAlign: 'right', 20 | }; 21 | return ( 22 |

23 | {left} 24 | {right} 25 |

26 | ); 27 | }; 28 | 29 | const HorizontalLine = ({ length }) => { 30 | const lineStyle = { 31 | width: `${length}px`, 32 | borderBottom: '5px solid white', // Adjust the border style as needed 33 | margin: '1px 0', // Adjust the margin as needed 34 | }; 35 | return
; 36 | }; 37 | 38 | // Define styles outside the component 39 | const tableStyles = { 40 | tableContainer: { 41 | margin: '20px', 42 | }, 43 | customTable: { 44 | borderCollapse: 'collapse', 45 | //width: '100%', 46 | }, 47 | tableCell: { 48 | border: '1px solid #ddd', 49 | padding: '8px', 50 | textAlign: 'center', 51 | width: '10%', 52 | }, 53 | tableHeader: { 54 | backgroundColor: '#f2f2f2', 55 | }, 56 | }; 57 | 58 | 59 | const WorkforceLevel = ({levelColor, levelName, levelValues, total, showAll}) => { 60 | //console.log(levelColor); console.log(levelName); console.log(levelValues); 61 | //
62 | const percent = ( total > 0 ? (100*levelValues.total/total).toFixed(1)+"%" : ""); 63 | return ( 64 |
65 |
66 |
67 |
68 |
{levelName}
69 |
70 |
{levelValues["total"]}
71 |
{percent}
72 |
{levelValues["service"]}
73 |
{levelValues["commercial"]}
74 |
{levelValues["leisure"]}
75 |
{levelValues["extractor"]}
76 |
{levelValues["industry"]}
77 |
{levelValues["office"]}
78 |
{showAll ?? levelValues["employee"]}
79 |
{showAll ?? levelValues["commuter"]}
80 |
{showAll ?? levelValues["open"]}
81 |
82 | ); 83 | }; 84 | 85 | const $Workplaces = ({react}) => { 86 | 87 | // 0..4 - data by education levels 88 | // 5 - totals 89 | // 6 - companies 90 | const [workplaces, setWorkplaces] = react.useState([]) 91 | useDataUpdate(react, 'workplaces.ilWorkplaces', setWorkplaces) 92 | 93 | const onClose = () => { 94 | // HookUI 95 | //const data = { type: "toggle_visibility", id: 'infoloom.workplaces' }; 96 | //const event = new CustomEvent('hookui', { detail: data }); 97 | //window.dispatchEvent(event); 98 | // Gooee 99 | engine.trigger("infoloom.infoloom.OnToggleVisibleWorkplaces", "Workplaces"); 100 | engine.trigger("audio.playSound", "close-panel", 1); 101 | 102 | }; 103 | 104 | const headers = { 105 | total: 'Total', 106 | service: 'City', 107 | commercial: 'Sales', 108 | leisure: 'Leisure', 109 | extractor: 'Extract', 110 | industry: 'Industry', 111 | office: 'Office', 112 | employee: 'Employees', 113 | commuter: 'Commute', 114 | open: 'Open', 115 | }; 116 | 117 | //if (workplaces.length !== 0) { 118 | // console.log("workplaces=", workplaces); 119 | //console.log(workplaces[0]); console.log(workplaces[1]); 120 | //} else { 121 | // console.log("workplaces has 0 elements"); 122 | // } 123 | 124 | /* 125 | return <$Panel react={react} title="Workforce" onClose={onClose} initialSize={{ width: window.innerWidth * 0.5, height: window.innerHeight * 0.3 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 126 |

TEST

127 | 128 | */ 129 | return <$Panel react={react} title="Workplace Distribution" onClose={onClose} initialSize={{ width: window.innerWidth * 0.38, height: window.innerHeight * 0.22 }} initialPosition={{ top: window.innerHeight * 0.05, left: window.innerWidth * 0.005 }}> 130 | {workplaces.length === 0 ? ( 131 |

Waiting...

132 | ) : ( 133 |
134 |
135 | 136 |
137 | 138 | 139 | 140 | 141 | 142 |
143 | 144 | 145 |
146 | )} 147 | 148 | 149 | } 150 | 151 | export default $Workplaces 152 | 153 | // Registering the panel with HookUI so it shows up in the menu 154 | /* 155 | window._$hookui.registerPanel({ 156 | id: "infoloom.workplaces", 157 | name: "InfoLoom: Workplaces", 158 | icon: "Media/Game/Icons/Workers.svg", 159 | component: $Workplaces 160 | }) 161 | */ --------------------------------------------------------------------------------