Skip to content

Commit

Permalink
Merge branch 'conditional-buildings'
Browse files Browse the repository at this point in the history
  • Loading branch information
memo33 committed Jun 2, 2024
2 parents c827e0c + a528a91 commit 8c68897
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 8 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Submenus for SimCity 4.
## Features

- enables submenu functionality: This DLL is a dependency for other submenu-compatible plugins.
- adds a number of standard submenus such as Plazas, Green Spaces, Sports.
- fixes the game's menu rendering issues caused by duplicate or missing menu icons
- adds a number of standard submenus such as Plazas, Green Spaces, Sports and many more.
- fixes the game's menu rendering issues caused by duplicate or missing menu icons.
- implements Exemplar Patching functionality

## System requirements
Expand Down Expand Up @@ -107,6 +107,11 @@ Health:
- Medium (0xB7B594D6)*
- Large (0xBC251B69)*

Landmark:
- Government (0x9FAF7A3B)*
- Churches and Cemeteries (0x26EB3057)*
- Entertainment (0xBE9FDA0C)* (theaters, operas, cinemas, stadiums, night clubs, etc.)

Parks:
- Green Spaces (0xBF776D40)
- Plazas (0xEB75882C)
Expand Down Expand Up @@ -195,6 +200,20 @@ Add the properties `Item Submenu Parent ID` and `Item Button Class` to the catal
The latter property is set to `2 = Network Item in Submenu` or `4 = Flora Item in Submenu`.
(The `Building Submenus` property is not used for these items.)

### Reward buildings

Reward buildings normally do not require any special considerations for submenus.
They can be included in submenus just like any other buildings.

If you want to include a *hidden* reward building in the Reward menu *and* in a submenu,
and want the item to be hidden in the Reward menu while unlocked, but unhidden in the submenu, then:
- include the two menu IDs in the `Building Submenus` property
- `Item Button Class` (0x8a2602cc): set to `8 = Building unhidden in Submenu`.
For example, this applies to the House of Worship.
(For *non-hidden* reward buildings such as the Mayor's House, this is not needed.)

This way, unlocked rewards can be found in the Reward menu as usual, but also in a thematic submenu.

### Exemplar patching

Exemplar patching is a technique to change the behavior of Exemplar files on-the-fly at runtime, without altering the original DBPF files.
Expand Down
65 changes: 59 additions & 6 deletions src/Categorization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const std::unordered_set<uint32_t> Categorization::autoPrefilledSubmenus = {
policeSmallSubmenuId, policeLargeSubmenuId, policeDeluxeSubmenuId,
elementarySchoolSubmenuId, highSchoolSubmenuId, collegeSubmenuId, libraryMuseumSubmenuId,
healthSmallSubmenuId, healthMediumSubmenuId, healthLargeSubmenuId,
governmentSubmenuId, religionSubmenuId, entertainmentSubmenuId,
};

Categorization::Categorization(std::unordered_set<uint32_t>* reachableSubmenus) : reachableSubmenus(reachableSubmenus)
Expand Down Expand Up @@ -51,6 +52,29 @@ static bool isHeightBelow(cISCPropertyHolder* propHolder, float_t height)
return result;
}

static bool isPatientCapacityGreaterThan(cISCPropertyHolder* propHolder, uint32_t threshold)
{
if (propHolder->HasProperty(hospitalPatientCapacityPropId)) {
auto property = propHolder->GetProperty(hospitalPatientCapacityPropId);
uint32_t capacity = threshold;
bool success = property->GetPropertyValue()->GetValUint32(capacity);
if (success) {
return capacity > threshold;
}
}
return false;
}

static bool isConditional(cISCPropertyHolder* propHolder)
{
bool result = false;
if (propHolder->HasProperty(conditionalBuildingPropId)) {
auto property = propHolder->GetProperty(conditionalBuildingPropId);
property->GetPropertyValue()->GetValBool(result);
}
return result;
}

static inline Categorization::TriState bool2tri(bool b)
{
return b ? Categorization::TriState::Yes : Categorization::TriState::No;
Expand Down Expand Up @@ -102,9 +126,9 @@ bool Categorization::belongsToSubmenu(cISCPropertyHolder* propHolder, uint32_t s
// 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 canalSubmenuId: return (hasOg(OgWaterTransit) || hasOg(OgPark)) && (hasOg(OgBteInlandWaterways) || hasOg(OgSgWaterway));
// case seawallSubmenuId: // no auto-categorization
case waterfrontSubmenuId: return hasOg(OgWaterTransit) && hasOg(OgBteWaterfront) && !hasOg(OgBteInlandWaterways) && !(hasOg(OgPassengerFerry) || hasOg(OgCarFerry) || hasOg(OgSeaport));
case waterfrontSubmenuId: return hasOg(OgWaterTransit) && hasOg(OgBteWaterfront) && !hasOg(OgBteInlandWaterways) && !hasOg(OgSgWaterway) && !(hasOg(OgPassengerFerry) || hasOg(OgCarFerry) || hasOg(OgSeaport));

case energyDirtySubmenuId:
return hasOg(OgPower) && (
Expand Down Expand Up @@ -135,10 +159,24 @@ bool Categorization::belongsToSubmenu(cISCPropertyHolder* propHolder, uint32_t s
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);
case healthSmallSubmenuId: return hasOg(OgHealth) && hasOg(OgHospital) && !hasOg(OgHealthOther) && (hasOg(OgClinic) || !hasOg(OgHealthLarge) && !hasOg(OgAmbulanceMaker));
case healthMediumSubmenuId: return hasOg(OgHealth) && hasOg(OgHospital) && !hasOg(OgHealthOther) && !(hasOg(OgClinic) || !hasOg(OgHealthLarge) && !hasOg(OgAmbulanceMaker))
&& !isPatientCapacityGreaterThan(propHolder, 20000);
case healthLargeSubmenuId: return hasOg(OgHealth) && (
hasOg(OgHealthOther) && PropertyUtil::arrayContains(propHolder, budgetItemDepartmentPropId, nullptr, budgetItemDepartmentProp_HealthCoverage)
|| isPatientCapacityGreaterThan(propHolder, 20000)
);

case governmentSubmenuId:
return PropertyUtil::arrayContains(propHolder, budgetItemDepartmentPropId, nullptr, budgetItemDepartmentProp_GovernmentBuildings)
|| hasOg(OgMayorHouse) || hasOg(OgBureaucracy) || hasOg(OgConventionCrowd) || hasOg(OgStockExchange)
|| (hasOg(OgCourthouse) && !hasOg(OgLandmark)); // to exclude US Capitol
case religionSubmenuId: return hasOg(OgWorship) || hasOg(OgCemetery) || hasOg(OgBteReligious);
case entertainmentSubmenuId: return hasOg(OgStadium) || hasOg(OgOpera) || hasOg(OgNiteClub) || hasOg(OgZoo) || hasOg(OgStateFair)
// || hasOg(OgCommercialCinema) || hasOg(OgCommercialMaxisSimTheatre) || (hasOg(OgCommercialMovie) && !hasOg(OgCommercialDrivein))
|| hasOg(OgCasino) || hasOg(OgTvStation)
|| PropertyUtil::arrayContains(propHolder, queryExemplarGuidPropId, nullptr, queryExemplarGuidProp_RadioStation)
|| hasOg(OgSgEntertainment) || hasOg(OgBteCommEntertainment);

default: // generic submenu
return false;
Expand Down Expand Up @@ -180,6 +218,9 @@ Categorization::TriState Categorization::belongsToMenu(cISCPropertyHolder* propH
&& !belongsToSubmenu(propHolder, r1SubmenuId)
&& !belongsToSubmenu(propHolder, r2SubmenuId)
&& !belongsToSubmenu(propHolder, r3SubmenuId)
&& !belongsToSubmenu(propHolder, governmentSubmenuId)
&& !belongsToSubmenu(propHolder, religionSubmenuId)
&& !belongsToSubmenu(propHolder, entertainmentSubmenuId)
);

case railButtonId:
Expand Down Expand Up @@ -240,9 +281,21 @@ Categorization::TriState Categorization::belongsToMenu(cISCPropertyHolder* propH
&& !belongsToSubmenu(propHolder, healthLargeSubmenuId)
);

// for buildings in submenus (e.g. Religion), we keep a copy here
// if they are *conditional* reward buildings (to make them easy to find when unlocked)
case rewardButtonId:
return bool2tri(hasOg(OgReward) && (
isConditional(propHolder)
|| !belongsToSubmenu(propHolder, governmentSubmenuId)
&& !belongsToSubmenu(propHolder, religionSubmenuId)
&& !belongsToSubmenu(propHolder, entertainmentSubmenuId)
));

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

default:
Expand Down
36 changes: 36 additions & 0 deletions src/MenuIds.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ static constexpr uint32_t itemSubmenuParentPropId = 0xaa1dd399; // occupantGrou
static constexpr uint32_t capacitySatisfiedPropId = 0x27812834;
static constexpr uint32_t budgetItemDepartmentPropId = 0xea54d283;
static constexpr uint32_t budgetItemDepartmentProp_HealthCoverage = 0xaa538cb3;
static constexpr uint32_t budgetItemDepartmentProp_GovernmentBuildings = 0xea59717a;
static constexpr uint32_t occupantSizePropId = 0x27812810;
static constexpr uint32_t powerPlantTypePropId = 0x27812853;
enum PowerPlantType : uint32_t { Coal = 1, Hydrogen = 2, NaturalGas = 3, Nuclear = 5, Oil = 6, Solar = 7, Waste = 8, Wind = 9, Auxiliary = 0xA };
static constexpr uint32_t conditionalBuildingPropId = 0xea3209f8;
static constexpr uint32_t hospitalPatientCapacityPropId = 0x69220415;
static constexpr uint32_t queryExemplarGuidPropId = 0x2a499f85;
static constexpr uint32_t queryExemplarGuidProp_RadioStation = 0x0A8B9C43;

static constexpr uint32_t railButtonId = RAIL_BUTTON_ID;
static constexpr uint32_t miscTransitButtonId = MISCTRANSP_BUTTON_ID;
Expand All @@ -45,6 +50,7 @@ static constexpr uint32_t policeButtonId = POLICE_BUTTON_ID;
static constexpr uint32_t educationButtonId = EDUCATION_BUTTON_ID;
static constexpr uint32_t healthButtonId = HEALTH_BUTTON_ID;
static constexpr uint32_t landmarkButtonId = LANDMARK_BUTTON_ID;
static constexpr uint32_t rewardButtonId = REWARD_BUTTON_ID;
static constexpr uint32_t parkButtonId = PARK_BUTTON_ID;

static constexpr uint32_t OgRail = 0x1300;
Expand All @@ -63,6 +69,7 @@ static constexpr uint32_t OgWaterTransit = 0x1519;

static constexpr uint32_t OgBteWaterfront = 0xB5C00DD6;
static constexpr uint32_t OgBteInlandWaterways = 0xB5C00DD8;
static constexpr uint32_t OgSgWaterway = 0xB5C00185;

static constexpr uint32_t OgPower = 0x1400;

Expand All @@ -82,9 +89,11 @@ static constexpr uint32_t OgSchoolHigh = 0x1510;
static constexpr uint32_t OgSchoolPrivate = 0x1514;

static constexpr uint32_t OgHealth = 0x1507;
static constexpr uint32_t OgClinic = 0x1512;
static constexpr uint32_t OgHospital = 0x1513;
static constexpr uint32_t OgHealthLarge = 0x151a;
static constexpr uint32_t OgHealthOther = 0x151c;
static constexpr uint32_t OgAmbulanceMaker = 0x1904;

static constexpr uint32_t OgLandmark = 0x150a;
static constexpr uint32_t OgR1 = 0x11010;
Expand All @@ -100,6 +109,29 @@ static constexpr uint32_t OgId = 0x14200;
static constexpr uint32_t OgIm = 0x14300;
static constexpr uint32_t OgIht = 0x14400;

static constexpr uint32_t OgReward = 0x150B;
static constexpr uint32_t OgMayorHouse = 0x1938;
static constexpr uint32_t OgCourthouse = 0x1511; // or city hall
static constexpr uint32_t OgBureaucracy = 0x1905; // DMV
static constexpr uint32_t OgStockExchange = 0x1913; // Biz Lawyer Attack
static constexpr uint32_t OgConventionCrowd = 0x1921;
static constexpr uint32_t OgWorship = 0x1907;
static constexpr uint32_t OgCemetery = 0x1700;
static constexpr uint32_t OgBteReligious = 0xB5C00DDF;
static constexpr uint32_t OgStadium = 0x1906;
static constexpr uint32_t OgNiteClub = 0x1908;
static constexpr uint32_t OgOpera = 0x1909;
static constexpr uint32_t OgTvStation = 0x1910;
static constexpr uint32_t OgZoo = 0x1702;
static constexpr uint32_t OgStateFair = 0x1925;
static constexpr uint32_t OgCasino = 0x1940;
static constexpr uint32_t OgCommercialCinema = 0x1112;
static constexpr uint32_t OgCommercialMaxisSimTheatre = 0x1113;
static constexpr uint32_t OgCommercialMovie = 0x21003;
static constexpr uint32_t OgCommercialDrivein = 0x1103;
static constexpr uint32_t OgSgEntertainment = 0xB5C00157;
static constexpr uint32_t OgBteCommEntertainment = 0xB5C00A0A;

static constexpr uint32_t OgPark = 0x1006;

static constexpr uint32_t passengerRailSubmenuId = 0x35380C75;
Expand Down Expand Up @@ -137,6 +169,10 @@ static constexpr uint32_t healthSmallSubmenuId = 0xB1F7AC5B; // medical clinic
static constexpr uint32_t healthMediumSubmenuId = 0xB7B594D6; // hospital with helicopter
static constexpr uint32_t healthLargeSubmenuId = 0xBC251B69; // medical center with helicopter

static constexpr uint32_t governmentSubmenuId = 0x9FAF7A3B;
static constexpr uint32_t religionSubmenuId = 0x26EB3057;
static constexpr uint32_t entertainmentSubmenuId = 0xBE9FDA0C;

static constexpr uint32_t r1SubmenuId = 0x93DADFE9;
static constexpr uint32_t r2SubmenuId = 0x984E5034;
static constexpr uint32_t r3SubmenuId = 0x9F83F133;
Expand Down
66 changes: 66 additions & 0 deletions src/SubmenusDllDirector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
#define ITEM_BUTTON_CLASS_SUBMENU 0x00000001
#define ITEM_BUTTON_CLASS_NETWORK 0x00000002
#define ITEM_BUTTON_CLASS_FLORA 0x00000004 // currently not enforced (subject to change)
#define ITEM_BUTTON_CLASS_BUILDING_UNHIDE 0x00000008 // (hidden reward) building that should be unhidden in submenus

// the catalog item type of submenu buttons
#define SUBMENU_ITEM_TYPE 0x9ab38dac
Expand Down Expand Up @@ -128,6 +129,9 @@ static uint32_t HandleButtonActivated_ContinueJump_Transport = 0x7f4add;
static uint32_t HandleButtonActivated2_InjectPoint = 0x7f4eb5;
static uint32_t HandleButtonActivated2_ContinueJump = 0x7f4ebe;

static constexpr uint32_t HandleButtonActivatedReward_InjectPoint = 0x7f4dbe;
static constexpr uint32_t HandleButtonActivatedReward_ContinueJump = 0x7f4ea0;

static uint32_t DoUtilitiesMenu_InjectPoint = 0x7f3b5d;
static uint32_t DoUtilitiesMenu_ContinueJump = 0x7f3b64;

Expand Down Expand Up @@ -155,6 +159,9 @@ static constexpr uint32_t AddBuildingsToItemList_ContinueJump_UseOrigProp = 0x7f
static constexpr uint32_t AddBuildingsToItemList2_InjectPoint = 0x7f036a;
static constexpr uint32_t AddBuildingsToItemList2_ContinueJump = 0x7f0371;

static constexpr uint32_t AddBuildingsToItemList3_InjectPoint = 0x7f0493;
static constexpr uint32_t AddBuildingsToItemList3_ContinueJump = 0x7f049c;

static uint32_t CreateBuildingMenu_InjectPoint = 0x7f074e;
static uint32_t CreateBuildingMenu_ContinueJump = 0x7f0756;

Expand Down Expand Up @@ -418,6 +425,43 @@ namespace
}
}

bool shouldForciblyUnhideBuilding(cISCPropertyHolder* propHolder)
{
bool isSubmenuActive = (lastVirtualButtonId == VIRTUAL_SUBMENU_ITEM_TYPE);
if (!isSubmenuActive) {
return false; // we are in a top-level menu (like Reward menu) in which we do not unhide hidden buildings
} else {
uint32_t propValueBuffer = 0;
propHolder->GetProperty((uint32_t)ITEM_BUTTON_CLASS_PROP, propValueBuffer);
return propValueBuffer == ITEM_BUTTON_CLASS_BUILDING_UNHIDE;
}
}

// forcibly unhide (locked) reward buildings if they are inside a submenu and have the corresponding property set
void NAKED_FUN Hook_AddBuildingsToItemList3(void)
{
__asm {
test byte ptr [esi], 0x1;
jz skipHiding;

push eax; // store
push ecx; // store
push edx; // store
push edi; // propHolder
call shouldForciblyUnhideBuilding; // (cdecl)
add esp, 0x4;
pop edx; // restore
pop ecx; // restore
test al, al;
pop eax; // restore
jnz skipHiding;
mov byte ptr [esp + 0x12], bl; // hides hidden building
skipHiding:
push AddBuildingsToItemList3_ContinueJump;
ret;
}
}

uint32_t menuFramePngFlora = FLORA_MENU_ID;
uint32_t menuFramePngZone = ZONE_MENU_ID;
uint32_t menuFramePngTransport = TRANSPORT_MENU_ID;
Expand Down Expand Up @@ -937,6 +981,7 @@ namespace

std::unordered_map<uint32_t, CatalogState*> catalogStates = {};

// also handles reward menu button
CatalogState* getVirtualButtonCatalogState(const uint32_t virtualButtonId)
{
if (catalogStates.contains(virtualButtonId))
Expand Down Expand Up @@ -998,6 +1043,25 @@ namespace
}
}

// adds catalog state for reward menu
void NAKED_FUN Hook_HandleButtonActivatedReward(void)
{
__asm {
push eax; // store
push ecx; // store
push edx; // store
push dword ptr [rewardButtonId];
call getVirtualButtonCatalogState; // (cdecl)
add esp, 0x4;
mov ebx, eax; // pointer to catalog state of reward menu
pop edx; // restore
pop ecx; // restore
pop eax; // restore
push HandleButtonActivatedReward_ContinueJump;
ret;
}
}

void InstallDuplicateIconsPatch(const uint16_t gameVersion)
{
Logger& logger = Logger::GetInstance();
Expand Down Expand Up @@ -1048,7 +1112,9 @@ namespace
InstallHook(CreateBuildingMenu_InjectPoint, Hook_CreateBuildingMenu);
InstallHook(CreateCatalogView_InjectPoint, Hook_CreateCatalogView);
InstallHook(AddBuildingsToItemList_InjectPoint, Hook_AddBuildingsToItemList);
InstallHook(AddBuildingsToItemList3_InjectPoint, Hook_AddBuildingsToItemList3);
InstallHook(InvokeTertiaryMenu_InjectPoint, Hook_InvokeTertiaryMenu);
InstallHook(HandleButtonActivatedReward_InjectPoint, Hook_HandleButtonActivatedReward);

// With the following replacement, plugins can be designed for use with and without the DLL.
// The property ITEM_SUBMENU_PARENT_ID_PROP is not loaded without the DLL, but otherwise behaves like ITEM_SUBMENU_PROP.
Expand Down
2 changes: 2 additions & 0 deletions vendor/new_properties.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5244,6 +5244,8 @@ For use with Submenus DLL: classifier that determines the function of this item
</OPTION>
<OPTION Value="0x0004" Name="Flora Item in Submenu">
</OPTION>
<OPTION Value="0x0008" Name="Building unhidden in Submenu">
</OPTION>
</PROPERTY>
<PROPERTY Name="Port Type Exemplar IDs" ID="0x8a270fc3" Type="Uint32" Count="-1" Default="0x00000000" ShowAsHex="Y">
<HELP>
Expand Down

0 comments on commit 8c68897

Please sign in to comment.