Skip to content

Commit

Permalink
Merge branch 'categorization'
Browse files Browse the repository at this point in the history
  • Loading branch information
memo33 committed Apr 19, 2024
2 parents a6629e4 + a59b2f7 commit 593cf20
Show file tree
Hide file tree
Showing 6 changed files with 687 additions and 136 deletions.
105 changes: 93 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Submenus for SimCity 4.
## Features

- enables submenu functionality: This DLL is a dependency for other submenu-compatible plugins.
- adds submenus for Plazas, Green Spaces and Sports Grounds to the Parks menu
- adds a number of standard submenus such as Plazas, Green Spaces, Sports.
- fixes the game's duplicate menu icons bug that occurs when loading some plugins, such as the NAM, twice
- implements Exemplar Patching functionality

Expand All @@ -23,18 +23,102 @@ Submenus for SimCity 4.

- Copy the DLLs into the top-level directory of either Plugins folder
(place it directly in `<Documents>\SimCity 4\Plugins` or `<SC4 install folder>\Plugins`, not in a subfolder).
- Copy the file `parks-submenus.dat` anywhere into your Plugins folder.
- Copy the file `submenu-essentials.dat` and optionally the individual submenu files anywhere into your Plugins folder.
You may skip installing an optional submenu if it contains neither Flora nor any sub-submenus.
In that case, the buildings of that submenu will appear in the default top-level menu instead.

## Troubleshooting

The plugin should write a `.log` file in the folder containing the plugin.
The log contains status information for the most recent run of the plugin.

If the parks submenus appear empty in the game,
there is a conflicting mod in your Plugins that overwrites properties of the Maxis parks.
Check if a submenu-compatible version of that mod is available
or move the `parks-submenus.dat` to a folder loading later.
In any case, the DLL can still be used as a dependency for other plugins.
If no `.log` file is written, the DLL failed to load.
In this case, make sure the [Microsoft Visual C++ 2022+ x86 Redistributable](https://aka.ms/vs/17/release/vc_redist.x86.exe) is installed.

## Credits

- Submenu icons created by Panda
- Exemplar Patching implemented in collaboration with Null 45

## Standard Submenus

This mod adds the following submenus. Additional menus may be added by third-party mods.

Residential:
- Ploppable R$ (0x93DADFE9)*
- Ploppable R$$ (0x984E5034)*
- Ploppable R$$$ (0x9F83F133)*

Commercial:
- Ploppable CS$ (0x11BF1CA9)*
- Ploppable CS$$ (0x24C43253)*
- Ploppable CS$$$ (0x9BDEFE2B)*
- Ploppable CO$$ (0xA7FF7CF0)*
- Ploppable CO$$$ (0xE27B7EF6)*

Industrial:
- Ploppable Farms (0xC220B7D8)*
- Ploppable Dirty Industry (0x62D82695)*
- Ploppable Manufacturing Industry (0x68B3E5FD)*
- Ploppable High-Tech Industry (0x954E20FE)*

Rail:
- Passenger Rail Stations (0x35380C75)*
- Freight Rail Stations (0x3557F0A1)*
- Yards (0x39BA25C7)* (non-functional lots, sidings, spurs)
- Hybrid Railway Stations (0x2B294CC2)*
- Monorail Stations (0x3A1D9854)*

Misc. Transit:
- Bus (0x1FDDE184)*
- GLR (0x26B51B28)*
- El-Rail (0x244F77E1)*
- Subway (0x231A97D3)*
- Multi-modal Stations (0x322C7959)*
- Parking (0x217B6C35)

Water Transit:
- Seaports and Ferry Terminals (0x07047B22)*
- Canals (0x03C6629C)*
- Seawalls (0x1CD18678)
- Waterfront (0x84D42CD6)*

Power:
- Dirty Energy (0x4B465151)*
- Clean Energy (0xCDE0316B)*
- Miscellaneous Power Utilities (0xD013F32D)* (substations, poles,…)

Police:
- Small (0x65D88585)*
- Large (0x7D6DC8BC)*
- Deluxe (0x8157CA0E)*
- Military (0x8BA49621)

Education:
- Elementary Schools (0x9FE5C428)*
- High Schools (0xA08063D0)* (including private schools)
- Higher Education (0xAC706063)* (colleges and universities)
- Libraries and Museums (0xAEDD9FAA)*

Health:
- Small (0xB1F7AC5B)*
- Medium (0xB7B594D6)*
- Large (0xBC251B69)*

Parks:
- Green Spaces (0xBF776D40)
- Plazas (0xEB75882C)
- Sports Grounds (0xCE21DBEB)
- Paths and Modular Parks (0xDEFFD960)
- Embankments and Retaining Walls (0xBB531946)
- Fillers (0xF034265C) (diagonal fillers, roundabout fillers, industrial fillers, etc.)

Many of these submenus are pre-filled automatically with suitable items from your plugins.
Others remain empty (and therefore hidden) until you install plugins that have been updated for submenu functionality.

Wether an item appears in a submenu can be explicit or implicit:
explicit if the item was modified to use a particular submenu;
implicit if the item belongs to a category associated with a submenu marked with `*`.

------------------------------------------------------------
## Information for modders
Expand Down Expand Up @@ -75,7 +159,7 @@ The following property is required, but does not usually need to be changed:

- `Item Button Class` (0x8a2602cc): set to `1 = Submenu Button`.

The following table lists the Button IDs for existing menus.
The following table lists the Button IDs for existing top-level menus.
The Occupant Groups are used as fallback to add buildings to a specific top-level menu if the DLL is not present.

Menu , Submenu Button ID , Occupant Group
Expand All @@ -100,9 +184,6 @@ The Occupant Groups are used as fallback to add buildings to a specific top-leve
Landmark , 0x09930709 , 0x150a
Reward , 0x34 , 0x150b
Park , 0x3 , 0x1006
Plazas , 0xeb75882c , -
Sports Grounds , 0xce21dbeb , -
Green Spaces , 0xbf776d40 , -

Note that empty submenus are hidden from the menus.

Expand Down Expand Up @@ -173,7 +254,7 @@ See [LICENSE.txt](LICENSE.txt) for more information.

#### 3rd party code

- [gzcom-dll](https://github.com/nsgomez/gzcom-dll/tree/master) Located in the vendor folder, MIT License.
- [gzcom-dll](https://github.com/nsgomez/gzcom-dll/tree/master) MIT License
- [Windows Implementation Library](https://github.com/microsoft/wil) MIT License
- [SC4Fix](https://github.com/nsgomez/sc4fix) MIT License
- [NAM-dll](https://github.com/NAMTeam/nam-dll) LGPL 3.0
Expand Down
252 changes: 252 additions & 0 deletions src/Categorization.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#include "Categorization.h"
#include "MenuIds.h"
#include "PropertyUtil.h"
#include "cGZPersistResourceKey.h"


const std::unordered_set<uint32_t> Categorization::toplevelMenuButtons = {
FLORA_BUTTON_ID,
ZONE_R_BUTTON_ID, ZONE_C_BUTTON_ID, ZONE_I_BUTTON_ID,
ROADWAY_BUTTON_ID, HIGHWAY_BUTTON_ID, RAIL_BUTTON_ID, MISCTRANSP_BUTTON_ID, AIRPORT_BUTTON_ID, SEAPORT_BUTTON_ID,
POWER_BUTTON_ID, WATER_BUTTON_ID, GARBAGE_BUTTON_ID,
POLICE_BUTTON_ID, FIRE_BUTTON_ID, EDUCATION_BUTTON_ID, HEALTH_BUTTON_ID, LANDMARK_BUTTON_ID, REWARD_BUTTON_ID, PARK_BUTTON_ID
};

const std::unordered_set<uint32_t> Categorization::autoPrefilledSubmenus = {
r1SubmenuId, r2SubmenuId, r3SubmenuId,
cs1SubmenuId, cs2SubmenuId, cs3SubmenuId,
co2SubmenuId, co3SubmenuId,
iaSubmenuId, idSubmenuId, imSubmenuId, ihtSubmenuId,
freightRailSubmenuId, passengerRailSubmenuId, monorailSubmenuId, hybridRailwaySubmenuId, yardsSubmenuId,
busSubmenuId, subwaySubmenuId, elRailSubmenuId, glrSubmenuId, multiModalStationsSubmenuId,
portFerrySubmenuId, canalSubmenuId, waterfrontSubmenuId,
energyDirtySubmenuId, energyCleanSubmenuId, miscPowerSubmenuId,
policeSmallSubmenuId, policeLargeSubmenuId, policeDeluxeSubmenuId,
elementarySchoolSubmenuId, highSchoolSubmenuId, collegeSubmenuId, libraryMuseumSubmenuId,
healthSmallSubmenuId, healthMediumSubmenuId, healthLargeSubmenuId,
};

Categorization::Categorization(std::unordered_set<uint32_t>* reachableSubmenus) : reachableSubmenus(reachableSubmenus)
{
}

static bool isHeightBelow(cISCPropertyHolder* propHolder, float_t height)
{
bool result = false;
if (propHolder->HasProperty((uint32_t)occupantSizePropId)) {
auto property = propHolder->GetProperty((uint32_t)occupantSizePropId);
property->AddRef();
const auto variant = property->GetPropertyValue();
variant->AddRef();
if (variant->GetType() == cIGZVariant::Type::Float32Array) {
uint32_t reps = variant->GetCount();
if (reps == 3) {
float_t* values = variant->RefFloat32();
result |= (values[1] <= height);
}
}
variant->Release();
property->Release();
}
return result;
}

static inline Categorization::TriState bool2tri(bool b)
{
return b ? Categorization::TriState::Yes : Categorization::TriState::No;
}

// Determines whether building exemplar belongs to a given submenu, taking into account that some submenus might not be reachable.
bool Categorization::belongsToSubmenu(cISCPropertyHolder* propHolder, uint32_t submenuId)
{
if (!reachableSubmenus->contains(submenuId))
{
return false;
}
else if (propHolder->HasProperty(itemSubmenuParentPropId) &&
PropertyUtil::arrayExists(propHolder, itemSubmenuParentPropId, nullptr, [this](uint32_t id) {
return this->reachableSubmenus->contains(id);
}))
{
return PropertyUtil::arrayContains(propHolder, itemSubmenuParentPropId, nullptr, submenuId);
}
else // no reachable submenu configured, so try to automatically put building into a submenu using OG or other available data
{
auto hasOg = [&propHolder](uint32_t og) { return PropertyUtil::arrayContains(propHolder, occupantGroupsPropId, nullptr, og); };
switch (submenuId)
{
case cs1SubmenuId: return hasOg(OgLandmark) && hasOg(OgCs1) && propHolder->HasProperty(capacitySatisfiedPropId);
case cs2SubmenuId: return hasOg(OgLandmark) && hasOg(OgCs2) && propHolder->HasProperty(capacitySatisfiedPropId);
case cs3SubmenuId: return hasOg(OgLandmark) && hasOg(OgCs3) && propHolder->HasProperty(capacitySatisfiedPropId);
case co2SubmenuId: return hasOg(OgLandmark) && hasOg(OgCo2) && propHolder->HasProperty(capacitySatisfiedPropId);
case co3SubmenuId: return hasOg(OgLandmark) && hasOg(OgCo3) && propHolder->HasProperty(capacitySatisfiedPropId);
case iaSubmenuId: return hasOg(OgLandmark) && hasOg(OgIa) && propHolder->HasProperty(capacitySatisfiedPropId);
case idSubmenuId: return hasOg(OgLandmark) && hasOg(OgId) && propHolder->HasProperty(capacitySatisfiedPropId);
case imSubmenuId: return hasOg(OgLandmark) && hasOg(OgIm) && propHolder->HasProperty(capacitySatisfiedPropId);
case ihtSubmenuId: return hasOg(OgLandmark) && hasOg(OgIht) && propHolder->HasProperty(capacitySatisfiedPropId);
case r1SubmenuId: return hasOg(OgLandmark) && hasOg(OgR1) && propHolder->HasProperty(capacitySatisfiedPropId);
case r2SubmenuId: return hasOg(OgLandmark) && hasOg(OgR2) && propHolder->HasProperty(capacitySatisfiedPropId);
case r3SubmenuId: return hasOg(OgLandmark) && hasOg(OgR3) && propHolder->HasProperty(capacitySatisfiedPropId);

case freightRailSubmenuId: return hasOg(OgRail) && hasOg(OgFreightRail) && !hasOg(OgAirport) && !hasOg(OgSeaport);
case passengerRailSubmenuId: return hasOg(OgRail) && hasOg(OgPassengerRail) && !hasOg(OgMonorail) && !hasOg(OgAirport) && !hasOg(OgSeaport) && !hasOg(OgLightrail);
case monorailSubmenuId: return hasOg(OgRail) && hasOg(OgMonorail) && !hasOg(OgPassengerRail) && !hasOg(OgAirport) && !hasOg(OgSeaport) && !hasOg(OgLightrail);
case hybridRailwaySubmenuId: return hasOg(OgRail) && hasOg(OgMonorail) && hasOg(OgPassengerRail) && !hasOg(OgAirport) && !hasOg(OgSeaport) && !hasOg(OgLightrail);
case yardsSubmenuId: return hasOg(OgRail) && !hasOg(OgPassengerRail) && !hasOg(OgFreightRail) && !hasOg(OgMonorail) && !hasOg(OgAirport) && !hasOg(OgSeaport);

case busSubmenuId: return hasOg(OgMiscTransit) && hasOg(OgBus) && !hasOg(OgSubway) && !hasOg(OgLightrail) && !hasOg(OgPassengerRail) && !hasOg(OgMonorail) && !hasOg(OgAirport) && !hasOg(OgSeaport);
case subwaySubmenuId: return hasOg(OgMiscTransit) && hasOg(OgSubway) && !hasOg(OgLightrail) && !hasOg(OgPassengerRail) && !hasOg(OgMonorail) && !hasOg(OgAirport) && !hasOg(OgSeaport) && !propHolder->HasProperty(capacitySatisfiedPropId);
case elRailSubmenuId: return hasOg(OgMiscTransit) && hasOg(OgLightrail) && !hasOg(OgPassengerRail) && !hasOg(OgMonorail) && !hasOg(OgAirport) && !hasOg(OgSeaport) && !isHeightBelow(propHolder, 15.5);
case glrSubmenuId: return hasOg(OgMiscTransit) && hasOg(OgLightrail) && !hasOg(OgPassengerRail) && !hasOg(OgMonorail) && !hasOg(OgAirport) && !hasOg(OgSeaport) && isHeightBelow(propHolder, 15.5);
case multiModalStationsSubmenuId: return (hasOg(OgRail) || hasOg(OgMiscTransit)) && hasOg(OgLightrail) && (hasOg(OgPassengerRail) || hasOg(OgMonorail)) && !hasOg(OgAirport) && !hasOg(OgSeaport);
// case parkingSubmenuId: // no auto-categorization

case portFerrySubmenuId: return hasOg(OgWaterTransit) && (hasOg(OgPassengerFerry) || hasOg(OgCarFerry) || hasOg(OgSeaport)) && !hasOg(OgAirport);
case canalSubmenuId: return (hasOg(OgWaterTransit) || hasOg(OgPark)) && hasOg(OgBteInlandWaterways);
// case seawallSubmenuId: // no auto-categorization
case waterfrontSubmenuId: return hasOg(OgWaterTransit) && hasOg(OgBteWaterfront) && !hasOg(OgBteInlandWaterways) && !(hasOg(OgPassengerFerry) || hasOg(OgCarFerry) || hasOg(OgSeaport));

case energyDirtySubmenuId:
return hasOg(OgPower) && (
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Coal) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::NaturalGas) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Oil) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Waste)
);
case energyCleanSubmenuId:
return hasOg(OgPower) && (
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Hydrogen) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Nuclear) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Solar) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Wind)
);
case miscPowerSubmenuId:
return hasOg(OgPower) && (
!propHolder->HasProperty(powerPlantTypePropId) ||
PropertyUtil::arrayContains(propHolder, powerPlantTypePropId, nullptr, PowerPlantType::Auxiliary)
);

case policeSmallSubmenuId: return hasOg(OgPolice) && (hasOg(OgPoliceSmall) || hasOg(OgPoliceKiosk));
case policeLargeSubmenuId: return hasOg(OgPolice) && hasOg(OgPoliceBig) && !hasOg(OgPoliceDeluxe);
case policeDeluxeSubmenuId: return hasOg(OgPolice) && hasOg(OgPoliceDeluxe);

case elementarySchoolSubmenuId: return hasOg(OgSchool) && hasOg(OgSchoolElementary);
case highSchoolSubmenuId: return hasOg(OgSchool) && (hasOg(OgSchoolHigh) || hasOg(OgSchoolPrivate));
case collegeSubmenuId: return hasOg(OgCollege);
case libraryMuseumSubmenuId: return hasOg(OgLibrary) || hasOg(OgMuseum);

case healthSmallSubmenuId: return hasOg(OgHealth) && hasOg(OgHospital) && !hasOg(OgHealthLarge) && !hasOg(OgHealthOther);
case healthMediumSubmenuId: return hasOg(OgHealth) && hasOg(OgHospital) && hasOg(OgHealthLarge) && !hasOg(OgHealthOther);
case healthLargeSubmenuId: return hasOg(OgHealth) && hasOg(OgHealthOther)
&& PropertyUtil::arrayContains(propHolder, budgetItemDepartmentPropId, nullptr, budgetItemDepartmentProp_HealthCoverage);

default: // generic submenu
return false;
}
}
}

// Determines whether building exemplar belongs to a given menu, taking into account that some submenus might not be reachable.
// `menuId` is a top-level button ID or a submenu ID
Categorization::TriState Categorization::belongsToMenu(cISCPropertyHolder* propHolder, uint32_t menuId)
{
if (!toplevelMenuButtons.contains(menuId))
{
return bool2tri(belongsToSubmenu(propHolder, menuId));
}
else if (propHolder->HasProperty(itemSubmenuParentPropId) &&
PropertyUtil::arrayExists(propHolder, itemSubmenuParentPropId, nullptr, [this](uint32_t id) {
return this->reachableSubmenus->contains(id);
}))
{
return bool2tri(PropertyUtil::arrayContains(propHolder, itemSubmenuParentPropId, nullptr, menuId));
}
else
{
auto hasOg = [&propHolder](uint32_t og) { return PropertyUtil::arrayContains(propHolder, occupantGroupsPropId, nullptr, og); };
switch (menuId)
{
case landmarkButtonId:
return bool2tri(hasOg(OgLandmark)
&& !belongsToSubmenu(propHolder, cs1SubmenuId)
&& !belongsToSubmenu(propHolder, cs2SubmenuId)
&& !belongsToSubmenu(propHolder, cs3SubmenuId)
&& !belongsToSubmenu(propHolder, co2SubmenuId)
&& !belongsToSubmenu(propHolder, co3SubmenuId)
&& !belongsToSubmenu(propHolder, iaSubmenuId)
&& !belongsToSubmenu(propHolder, idSubmenuId)
&& !belongsToSubmenu(propHolder, imSubmenuId)
&& !belongsToSubmenu(propHolder, ihtSubmenuId)
&& !belongsToSubmenu(propHolder, r1SubmenuId)
&& !belongsToSubmenu(propHolder, r2SubmenuId)
&& !belongsToSubmenu(propHolder, r3SubmenuId)
);

case railButtonId:
return bool2tri(hasOg(OgRail)
&& !belongsToSubmenu(propHolder, freightRailSubmenuId)
&& !belongsToSubmenu(propHolder, passengerRailSubmenuId)
&& !belongsToSubmenu(propHolder, monorailSubmenuId)
&& !belongsToSubmenu(propHolder, hybridRailwaySubmenuId)
&& !belongsToSubmenu(propHolder, multiModalStationsSubmenuId)
&& !belongsToSubmenu(propHolder, yardsSubmenuId)
);

case miscTransitButtonId:
return bool2tri(hasOg(OgMiscTransit)
&& !belongsToSubmenu(propHolder, busSubmenuId)
&& !belongsToSubmenu(propHolder, subwaySubmenuId)
&& !belongsToSubmenu(propHolder, elRailSubmenuId)
&& !belongsToSubmenu(propHolder, glrSubmenuId)
&& !belongsToSubmenu(propHolder, multiModalStationsSubmenuId)
// && !belongsToSubmenu(propHolder, parkingSubmenuId)
);

case seaportButtonId:
return bool2tri(hasOg(OgWaterTransit)
&& !belongsToSubmenu(propHolder, portFerrySubmenuId)
&& !belongsToSubmenu(propHolder, canalSubmenuId)
// && !belongsToSubmenu(propHolder, seawallSubmenuId)
&& !belongsToSubmenu(propHolder, waterfrontSubmenuId)
);

case powerButtonId:
return bool2tri(hasOg(OgPower) // originally also excludes OgLandfill
&& !belongsToSubmenu(propHolder, energyDirtySubmenuId)
&& !belongsToSubmenu(propHolder, energyCleanSubmenuId)
&& !belongsToSubmenu(propHolder, miscPowerSubmenuId)
);

case policeButtonId:
return bool2tri((hasOg(OgPolice) || hasOg(OgJail))
&& !belongsToSubmenu(propHolder, policeSmallSubmenuId)
&& !belongsToSubmenu(propHolder, policeLargeSubmenuId)
&& !belongsToSubmenu(propHolder, policeDeluxeSubmenuId)
);

case educationButtonId:
// this implementation looks redundant, but ensures that menu placement is correct even if some submenus are not installed
return bool2tri((hasOg(OgSchool) || hasOg(OgCollege) || hasOg(OgLibrary) || hasOg(OgMuseum))
&& !belongsToSubmenu(propHolder, elementarySchoolSubmenuId)
&& !belongsToSubmenu(propHolder, highSchoolSubmenuId)
&& !belongsToSubmenu(propHolder, collegeSubmenuId)
&& !belongsToSubmenu(propHolder, libraryMuseumSubmenuId)
);

case healthButtonId:
return bool2tri(hasOg(OgHealth)
&& !belongsToSubmenu(propHolder, healthSmallSubmenuId)
&& !belongsToSubmenu(propHolder, healthMediumSubmenuId)
&& !belongsToSubmenu(propHolder, healthLargeSubmenuId)
);

case parkButtonId:
return bool2tri(hasOg(OgPark)
&& !belongsToSubmenu(propHolder, canalSubmenuId)
);

default:
return Categorization::TriState::Maybe; // other top level menus require evaluating the original inclusion/exclusion rules of their occupant groups
}
}
}
Loading

0 comments on commit 593cf20

Please sign in to comment.