OpenLoco/src/OpenLoco/GameCommands/GameCommands.cpp

467 lines
20 KiB
C++

#include "GameCommands.h"
#include "../Audio/Audio.h"
#include "../Company.h"
#include "../CompanyManager.h"
#include "../Localisation/FormatArguments.hpp"
#include "../Localisation/StringIds.h"
#include "../Map/Tile.h"
#include "../Objects/ObjectManager.h"
#include "../Objects/RoadObject.h"
#include "../Objects/TrackObject.h"
#include "../StationManager.h"
#include "../Ui/WindowManager.h"
#include "../Vehicles/Vehicle.h"
#include <cassert>
using namespace OpenLoco::Ui;
using namespace OpenLoco::Map;
namespace OpenLoco::GameCommands
{
static loco_global<CompanyId_t, 0x009C68EB> _updating_company_id;
static loco_global<uint8_t, 0x00508F08> game_command_nest_level;
static loco_global<CompanyId_t[2], 0x00525E3C> _player_company;
static loco_global<uint8_t, 0x00508F17> paused_state;
static uint16_t _gameCommandFlags;
static loco_global<TileElement*, 0x009C68D0> _9C68D0;
static loco_global<Pos3, 0x009C68E0> _gGameCommandPosition;
static loco_global<string_id, 0x009C68E6> _gGameCommandErrorText;
static loco_global<string_id, 0x009C68E8> _gGameCommandErrorTitle;
static loco_global<uint8_t, 0x009C68EA> _gGameCommandExpenditureType; // premultiplied by 4
static loco_global<uint8_t, 0x009C68EE> _errorCompanyId;
static loco_global<string_id[8], 0x112C826> _commonFormatArgs;
using GameCommandFunc = void (*)(registers& regs);
struct GameCommandInfo
{
GameCommand id;
GameCommandFunc implementation;
uintptr_t originalAddress; // original array: 0x004F9548
bool unpausesGame; // original array: 0x004F9688
};
// clang-format off
static constexpr GameCommandInfo _gameCommandDefinitions[82] = {
{ GameCommand::vehicleRearrange, nullptr, 0x004AF1DF, true },
{ GameCommand::vehiclePlace, nullptr, 0x004B01B6, true },
{ GameCommand::vehiclePickup, vehiclePickup, 0x004B0826, true },
{ GameCommand::vehicleReverse, nullptr, 0x004ADAA8, true },
{ GameCommand::vehiclePassSignal, nullptr, 0x004B0B50, true },
{ GameCommand::vehicleCreate, Vehicles::create, 0x004AE5E4, true },
{ GameCommand::vehicleSell, nullptr, 0x004AED34, true },
{ GameCommand::createTrack, nullptr, 0x0049BB98, true },
{ GameCommand::gc_unk_8, nullptr, 0x0049C7F2, true },
{ GameCommand::changeLoan, nullptr, 0x0046DE88, false },
{ GameCommand::vehicleRename, Vehicles::rename, 0x004B6572, false },
{ GameCommand::changeStationName, renameStation, 0x00490756, false },
{ GameCommand::vehicleLocalExpress, nullptr, 0x004B694B, true },
{ GameCommand::createSignal, nullptr, 0x00488BDB, true },
{ GameCommand::removeSignal, nullptr, 0x004891E4, true },
{ GameCommand::gc_unk_15, nullptr, 0x0048BB20, true },
{ GameCommand::removeTrackStation, nullptr, 0x0048C402, true },
{ GameCommand::createTrackMod, nullptr, 0x004A6479, true },
{ GameCommand::removeTrackMod, nullptr, 0x004A668A, true },
{ GameCommand::changeCompanyColourScheme, changeCompanyColour, 0x0043483D, false },
{ GameCommand::pauseGame, togglePause, 0x00431E32, false },
{ GameCommand::loadSaveQuitGame, loadSaveQuit, 0x0043BFCB, false },
{ GameCommand::removeTree, removeTree, 0x004BB392, true },
{ GameCommand::createTree, nullptr, 0x004BB138, true },
{ GameCommand::changeLandMaterial, nullptr, 0x00468EDD, true },
{ GameCommand::raiseLand, nullptr, 0x00463702, true },
{ GameCommand::lowerLand, nullptr, 0x004638C6, true },
{ GameCommand::lowerRaiseLandMountain, nullptr, 0x00462DCE, true },
{ GameCommand::raiseWater, nullptr, 0x004C4F19, true },
{ GameCommand::lowerWater, nullptr, 0x004C5126, true },
{ GameCommand::changeCompanyName, nullptr, 0x00434914, false },
{ GameCommand::changeCompanyOwnerName, nullptr, 0x00434A58, false },
{ GameCommand::createWall, nullptr, 0x004C436C, true },
{ GameCommand::removeWall, nullptr, 0x004C466C, true },
{ GameCommand::gc_unk_34, nullptr, 0x004C4717, false },
{ GameCommand::vehicleOrderInsert, nullptr, 0x0047036E, false },
{ GameCommand::vehicleOrderDelete, nullptr, 0x0047057A, false },
{ GameCommand::vehicleOrderSkip, Vehicles::orderSkip, 0x0047071A, false },
{ GameCommand::createRoad, nullptr, 0x00475FBC, true },
{ GameCommand::removeRoad, nullptr, 0x004775A5, true },
{ GameCommand::createRoadMod, nullptr, 0x0047A21E, true },
{ GameCommand::removeRoadMod, nullptr, 0x0047A42F, true },
{ GameCommand::gc_unk_42, nullptr, 0x0048C708, true },
{ GameCommand::removeRoadStation, nullptr, 0x0048D2AC, true },
{ GameCommand::createBuilding, nullptr, 0x0042D133, true },
{ GameCommand::removeBuilding, nullptr, 0x0042D74E, true },
{ GameCommand::renameTown, renameTown, 0x0049B11E, false },
{ GameCommand::createIndustry, nullptr, 0x0045436B, true },
{ GameCommand::removeIndustry, nullptr, 0x00455943, true },
{ GameCommand::createTown, nullptr, 0x00496C22, true },
{ GameCommand::removeTown, nullptr, 0x0049711F, true },
{ GameCommand::gc_unk_51, nullptr, 0x004A6FDC, true },
{ GameCommand::gc_unk_52, nullptr, 0x004A734F, true },
{ GameCommand::gc_unk_53, nullptr, 0x0047AF0B, true },
{ GameCommand::buildCompanyHeadquarters, nullptr, 0x0042ECFC, true },
{ GameCommand::removeCompanyHeadquarters, nullptr, 0x0042EEAF, true },
{ GameCommand::gc_unk_56, nullptr, 0x00492C41, true },
{ GameCommand::removeAirport, nullptr, 0x00493559, true },
{ GameCommand::vehiclePlaceAir, nullptr, 0x004267BE, true },
{ GameCommand::vehiclePickupAir, nullptr, 0x00426B29, true },
{ GameCommand::gc_unk_60, nullptr, 0x00493AA7, true },
{ GameCommand::removePort, nullptr, 0x00494570, true },
{ GameCommand::vehiclePlaceWater, nullptr, 0x0042773C, true },
{ GameCommand::vehiclePickupWater, nullptr, 0x004279CC, true },
{ GameCommand::vehicleRefit, nullptr, 0x0042F6DB, false },
{ GameCommand::changeCompanyFace, nullptr, 0x00435506, false },
{ GameCommand::clearLand, nullptr, 0x00469CCB, true },
{ GameCommand::loadMultiplayerMap, nullptr, 0x00444DA0, false },
{ GameCommand::gc_unk_68, nullptr, 0x0046F8A5, false },
{ GameCommand::gc_unk_69, nullptr, 0x004454BE, false },
{ GameCommand::gc_unk_70, nullptr, 0x004456C8, false },
{ GameCommand::sendChatMessage, nullptr, 0x0046F976, false },
{ GameCommand::multiplayerSave, nullptr, 0x004A0ACD, false },
{ GameCommand::updateOwnerStatus, nullptr, 0x004383CA, false },
{ GameCommand::vehicleSpeedControl, nullptr, 0x004BAB63, true },
{ GameCommand::vehicleOrderUp, nullptr, 0x00470CD2, false },
{ GameCommand::vehicleOrderDown, nullptr, 0x00470E06, false },
{ GameCommand::vehicleApplyShuntCheat, nullptr, 0x004BAC53, false },
{ GameCommand::applyFreeCashCheat, nullptr, 0x00438A08, false },
{ GameCommand::renameIndustry, renameIndustry, 0x00455029, false },
{ GameCommand::vehicleClone, Vehicles::cloneVehicle, 0, true },
{ GameCommand::cheat, cheat, 0, true },
};
// clang-format on
void registerHooks()
{
registerHook(
0x00431315,
[](registers& regs) FORCE_ALIGN_ARG_POINTER -> uint8_t {
registers backup = regs;
auto ebx = doCommand(GameCommand(regs.esi), backup);
regs = backup;
regs.ebx = ebx;
return 0;
});
}
static uint32_t loc_4314EA();
static uint32_t loc_4313C6(int esi, const registers& regs);
static bool commandRequiresUnpausingGame(GameCommand command, uint16_t flags)
{
if ((flags & (Flags::flag_4 | Flags::flag_6)) != 0)
return false;
auto& gameCommand = _gameCommandDefinitions[static_cast<uint32_t>(command)];
if (!gameCommand.unpausesGame || isPauseOverrideEnabled())
return false;
return true;
}
// 0x00431315
uint32_t doCommand(GameCommand command, const registers& regs)
{
uint16_t flags = regs.bx;
uint32_t esi = static_cast<uint32_t>(command);
_gameCommandFlags = regs.bx;
if (game_command_nest_level != 0)
return loc_4313C6(esi, regs);
if ((flags & Flags::apply) == 0)
{
return loc_4313C6(esi, regs);
}
if (commandRequiresUnpausingGame(command, flags) && _updating_company_id == _player_company[0])
{
if (getPauseFlags() & 1)
{
paused_state = paused_state ^ 1;
WindowManager::invalidate(WindowType::timeToolbar);
Audio::unpauseSound();
Ui::Windows::PlayerInfoPanel::invalidateFrame();
}
if (getGameSpeed() != 0)
{
setGameSpeed(0);
WindowManager::invalidate(WindowType::timeToolbar);
}
if (isPaused())
{
_gGameCommandErrorText = StringIds::empty;
return 0x80000000;
}
}
if (_updating_company_id == _player_company[0] && isNetworked())
{
assert(false);
registers fnRegs = regs;
call(0x0046E34A, fnRegs); // some network stuff. Untested
}
return loc_4313C6(esi, regs);
}
static void callGameCommandFunction(uint32_t command, registers& regs)
{
auto& gameCommand = _gameCommandDefinitions[command];
if (gameCommand.implementation != nullptr)
{
gameCommand.implementation(regs);
}
else
{
auto addr = gameCommand.originalAddress;
call(addr, regs);
}
}
static uint32_t loc_4313C6(int esi, const registers& regs)
{
uint16_t flags = regs.bx;
_gGameCommandErrorText = StringIds::null;
game_command_nest_level++;
uint16_t flagsBackup = _gameCommandFlags;
registers fnRegs1 = regs;
fnRegs1.bl &= ~Flags::apply;
callGameCommandFunction(esi, fnRegs1);
int32_t ebx = fnRegs1.ebx;
_gameCommandFlags = flagsBackup;
if (ebx != static_cast<int32_t>(0x80000000))
{
if (isEditorMode())
ebx = 0;
if (game_command_nest_level == 1)
{
if ((_gameCommandFlags & Flags::flag_2) == 0
&& (_gameCommandFlags & Flags::flag_6) == 0
&& ebx != 0)
{
registers regs2;
regs2.ebp = ebx;
call(0x0046DD06, regs2);
ebx = regs2.ebp;
}
}
}
if (ebx == static_cast<int32_t>(0x80000000))
{
if (flags & Flags::apply)
{
return loc_4314EA();
}
else
{
game_command_nest_level--;
return ebx;
}
}
if ((flags & 1) == 0)
{
game_command_nest_level--;
return ebx;
}
uint16_t flagsBackup2 = _gameCommandFlags;
registers fnRegs2 = regs;
callGameCommandFunction(esi, fnRegs2);
int32_t ebx2 = fnRegs2.ebx;
_gameCommandFlags = flagsBackup2;
if (ebx2 == static_cast<int32_t>(0x80000000))
{
return loc_4314EA();
}
if (isEditorMode())
{
ebx = 0;
}
if (ebx2 < ebx)
{
ebx = ebx2;
}
game_command_nest_level--;
if (game_command_nest_level != 0)
return ebx;
if ((flagsBackup2 & Flags::flag_5) != 0)
return ebx;
// Apply to company money
CompanyManager::applyPaymentToCompany(CompanyManager::updatingCompanyId(), ebx, getExpenditureType());
if (ebx != 0 && _updating_company_id == _player_company[0])
{
// Add flying cost text
CompanyManager::spendMoneyEffect(getPosition() + Map::Pos3{ 0, 0, 24 }, _updating_company_id, ebx);
}
return ebx;
}
static uint32_t loc_4314EA()
{
game_command_nest_level--;
if (game_command_nest_level != 0)
return 0x80000000;
if (_updating_company_id != _player_company[0])
return 0x80000000;
if (_gameCommandFlags & Flags::flag_3)
return 0x80000000;
if (_gGameCommandErrorText != 0xFFFE)
{
Windows::showError(_gGameCommandErrorTitle, _gGameCommandErrorText);
return 0x80000000;
}
// advanced errors
if (_9C68D0 != (void*)-1)
{
auto tile = (TileElement*)_9C68D0;
switch (tile->type())
{
case ElementType::track: // 4
{
auto trackElement = tile->asTrack();
if (trackElement == nullptr)
break; // throw exception?
TrackObject* pObject = ObjectManager::get<TrackObject>(trackElement->trackObjectId());
if (pObject == nullptr)
break;
auto formatter = FormatArguments::common();
formatter.push(pObject->name);
formatter.push(CompanyManager::get(_errorCompanyId)->name);
Windows::Error::openWithCompetitor(_gGameCommandErrorTitle, StringIds::error_reason_stringid_belongs_to, _errorCompanyId);
return 0x80000000;
}
case ElementType::road: //0x1C
{
auto roadElement = tile->asRoad();
if (roadElement == nullptr)
break; // throw exception?
RoadObject* pObject = ObjectManager::get<RoadObject>(roadElement->roadObjectId());
if (pObject == nullptr)
break;
auto formatter = FormatArguments::common();
formatter.push(pObject->name);
formatter.push(CompanyManager::get(_errorCompanyId)->name);
Windows::Error::openWithCompetitor(_gGameCommandErrorTitle, StringIds::error_reason_stringid_belongs_to, _errorCompanyId);
return 0x80000000;
}
case ElementType::station: // 8
{
auto stationElement = tile->asStation();
if (stationElement == nullptr)
break; // throw exception?
Station* pStation = StationManager::get(stationElement->stationId());
if (pStation == nullptr)
break;
auto formatter = FormatArguments::common();
formatter.push(pStation->name);
formatter.push(pStation->town);
formatter.push(CompanyManager::get(_errorCompanyId)->name);
Windows::Error::openWithCompetitor(_gGameCommandErrorTitle, StringIds::error_reason_stringid_belongs_to, _errorCompanyId);
return 0x80000000;
}
case ElementType::signal: // 0x0C
{
auto formatter = FormatArguments::common();
formatter.push(CompanyManager::get(_errorCompanyId)->name);
Windows::Error::openWithCompetitor(_gGameCommandErrorTitle, StringIds::error_reason_signal_belongs_to, _errorCompanyId);
return 0x80000000;
}
default:
break;
}
}
// fallback
auto formatter = FormatArguments::common();
formatter.push(CompanyManager::get(_errorCompanyId)->name);
Windows::Error::openWithCompetitor(_gGameCommandErrorTitle, StringIds::error_reason_stringid_belongs_to, _errorCompanyId);
return 0x80000000;
}
// 0x00431E6A
// al : company
// esi : tile
bool sub_431E6A(const CompanyId_t company, Map::TileElement* const tile /*= nullptr*/)
{
if (company == CompanyId::neutral)
{
return true;
}
if (_updating_company_id == company || _updating_company_id == CompanyId::neutral)
{
return true;
}
_gGameCommandErrorText = -2;
_errorCompanyId = company;
_9C68D0 = tile == nullptr ? reinterpret_cast<Map::TileElement*>(-1) : tile;
return false;
}
const Map::Pos3& getPosition()
{
return _gGameCommandPosition;
}
void setPosition(const Map::Pos3& pos)
{
_gGameCommandPosition = pos;
}
void setErrorText(const string_id message)
{
_gGameCommandErrorText = message;
}
string_id getErrorText()
{
return _gGameCommandErrorText;
}
void setErrorTitle(const string_id title)
{
_gGameCommandErrorTitle = title;
}
ExpenditureType getExpenditureType()
{
return ExpenditureType(_gGameCommandExpenditureType / 4);
}
void setExpenditureType(const ExpenditureType type)
{
_gGameCommandExpenditureType = static_cast<uint8_t>(type) * 4;
}
}