mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2025-12-16 12:08:36 +00:00
1230 lines
34 KiB
C++
1230 lines
34 KiB
C++
#include "game/ht.h"
|
|
#include "game/wistrings.h"
|
|
#include "game/stateframe.h"
|
|
|
|
namespace wi {
|
|
|
|
CommandQueue gcmdq;
|
|
|
|
#ifdef STATS_DISPLAY
|
|
extern int gcBitmapsDrawn;
|
|
#endif
|
|
extern int gcMessagesPerUpdate;
|
|
|
|
extern bool gfDragSelecting;
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Simulation implementation
|
|
|
|
Simulation::Simulation()
|
|
{
|
|
m_plvl = NULL;
|
|
m_fPaused = false;
|
|
m_nMiniMapScale = 1;
|
|
m_cgobVisible = 0;
|
|
m_wxViewSave = 0;
|
|
m_wyViewSave = 0;
|
|
m_cUpdatesSave = -1;
|
|
}
|
|
|
|
Simulation::~Simulation()
|
|
{
|
|
}
|
|
|
|
bool Simulation::OneTimeInit()
|
|
{
|
|
// Initialize all the Gob classes
|
|
|
|
Status("Parse GobTemplates.ini...");
|
|
IniReader *piniGobTemplates = LoadIniFile(gpakr, "GobTemplates.ini");
|
|
if (piniGobTemplates == NULL)
|
|
return false;
|
|
|
|
Status("Init SurfaceDecalGob...");
|
|
bool fSuccess = SurfaceDecalGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init SceneryGob...");
|
|
fSuccess = fSuccess && SceneryGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init SRInfantryGob...");
|
|
fSuccess = fSuccess && SRInfantryGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init HrcGob...");
|
|
fSuccess = fSuccess && HrcGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init ProcessorGob...");
|
|
fSuccess = fSuccess && ProcessorGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init ReactorGob...");
|
|
fSuccess = fSuccess && ReactorGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init MinerGob...");
|
|
fSuccess = fSuccess && MinerGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init VtsGob...");
|
|
fSuccess = fSuccess && VtsGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init RadarGob...");
|
|
fSuccess = fSuccess && RadarGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init HqGob...");
|
|
fSuccess = fSuccess && HqGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init ResearchGob...");
|
|
fSuccess = fSuccess && ResearchGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init LRInfantryGob...");
|
|
fSuccess = fSuccess && LRInfantryGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init LTankGob...");
|
|
fSuccess = fSuccess && LTankGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init MTankGob...");
|
|
fSuccess = fSuccess && MTankGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init RTankGob...");
|
|
fSuccess = fSuccess && RTankGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init GTankGob...");
|
|
fSuccess = fSuccess && GTankGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init SpInfantryGob...");
|
|
fSuccess = fSuccess && SpInfantryGob::InitClass(piniGobTemplates);
|
|
Status("Init OvermindGob...");
|
|
fSuccess = fSuccess && OvermindGob::InitClass(piniGobTemplates);
|
|
Status("Init TankShotGob...");
|
|
fSuccess = fSuccess && TankShotGob::InitClass(piniGobTemplates);
|
|
Status("Init RocketGob...");
|
|
fSuccess = fSuccess && RocketGob::InitClass(piniGobTemplates);
|
|
Status("Init BulletGob...");
|
|
fSuccess = fSuccess && BulletGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init AndyShotGob...");
|
|
fSuccess = fSuccess && AndyShotGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init WarehouseGob...");
|
|
fSuccess = fSuccess && WarehouseGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init GunTowerGob...");
|
|
fSuccess = fSuccess && GunTowerGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init RocketTowerGob...");
|
|
fSuccess = fSuccess && RocketTowerGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init MobileHqGob...");
|
|
fSuccess = fSuccess && MobileHqGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init ArtilleryGob...");
|
|
fSuccess = fSuccess && ArtilleryGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init ArtilleryShotGob...");
|
|
fSuccess = fSuccess && ArtilleryShotGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init ReplicatorGob...");
|
|
fSuccess = fSuccess && ReplicatorGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init AndyGob...");
|
|
fSuccess = fSuccess && AndyGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
Status("Init FoxGob...");
|
|
fSuccess = fSuccess && FoxGob::InitClass(piniGobTemplates);
|
|
Assert(fSuccess);
|
|
|
|
delete piniGobTemplates;
|
|
|
|
// Do some other one-time initialization that we didn't want to
|
|
// do at game startup because we want to startup snappily.
|
|
|
|
#ifdef DRAW_PATHS
|
|
// Load the direction arrow bitmaps
|
|
|
|
Status("Load path arrow bitmaps...");
|
|
LoadArrows();
|
|
#endif
|
|
|
|
return fSuccess;
|
|
}
|
|
|
|
void Simulation::OneTimeExit()
|
|
{
|
|
SRInfantryGob::ExitClass();
|
|
HrcGob::ExitClass();
|
|
ProcessorGob::ExitClass();
|
|
ReactorGob::ExitClass();
|
|
MinerGob::ExitClass();
|
|
VtsGob::ExitClass();
|
|
RadarGob::ExitClass();
|
|
HqGob::ExitClass();
|
|
ResearchGob::ExitClass();
|
|
LRInfantryGob::ExitClass();
|
|
LTankGob::ExitClass();
|
|
MTankGob::ExitClass();
|
|
RTankGob::ExitClass();
|
|
GTankGob::ExitClass();
|
|
SpInfantryGob::ExitClass();
|
|
TankShotGob::ExitClass();
|
|
RocketGob::ExitClass();
|
|
BulletGob::ExitClass();
|
|
AndyShotGob::ExitClass();
|
|
WarehouseGob::ExitClass();
|
|
GunTowerGob::ExitClass();
|
|
RocketTowerGob::ExitClass();
|
|
MobileHqGob::ExitClass();
|
|
ArtilleryGob::ExitClass();
|
|
ArtilleryShotGob::ExitClass();
|
|
ReplicatorGob::ExitClass();
|
|
AndyGob::ExitClass();
|
|
FoxGob::ExitClass();
|
|
|
|
#ifdef DRAW_PATHS
|
|
FreeArrows();
|
|
#endif
|
|
|
|
delete m_plvl;
|
|
m_plvl = NULL;
|
|
}
|
|
|
|
bool Simulation::PerLevelInit()
|
|
{
|
|
#ifdef DEBUG_HELPERS
|
|
void ClearLog();
|
|
ClearLog();
|
|
#endif
|
|
|
|
if (!gcmdq.Init())
|
|
return false;
|
|
|
|
m_wxView = m_wyView = 0;
|
|
m_cUpdates = -1;
|
|
m_tCurrent = 0;
|
|
m_fGameOver = false;
|
|
m_pbldm = new BuildMgr();
|
|
if (m_pbldm == NULL)
|
|
return false;
|
|
|
|
m_cupdTriggerMgrUpdateLast = 0;
|
|
|
|
// So visible gobs get recalced
|
|
|
|
m_wxViewSave = (WCoord)-1;
|
|
m_wyViewSave = (WCoord)-1;
|
|
m_cUpdatesSave = -2;
|
|
|
|
return true;
|
|
}
|
|
|
|
void Simulation::PerLevelExit()
|
|
{
|
|
gcmdq.Exit();
|
|
|
|
delete m_plvl;
|
|
m_plvl = NULL;
|
|
delete m_pbldm;
|
|
m_pbldm = NULL;
|
|
}
|
|
|
|
bool Simulation::LoadLevel(const char *pszLevelName)
|
|
{
|
|
Assert(m_plvl == NULL);
|
|
|
|
m_plvl = new Level();
|
|
if (!m_plvl->Init(pszLevelName)) {
|
|
delete m_plvl;
|
|
m_plvl = NULL;
|
|
return false;
|
|
}
|
|
|
|
// Initialize level-specific player info for each player
|
|
|
|
Player *pplr = gplrm.GetNextPlayer(NULL);
|
|
for (; pplr != NULL; pplr = gplrm.GetNextPlayer(pplr)) {
|
|
SideInfo *psidi = m_plvl->GetSideInfo(pplr->GetSide());
|
|
pplr->SetCredits(psidi->nInitialCredits, false);
|
|
|
|
// UNDONE: Level could (should?) provide this state
|
|
pplr->SetUpgrades(0);
|
|
|
|
if (pplr == gpplrLocal) {
|
|
|
|
// Center the view around the level-specified initial view position
|
|
|
|
Size siz;
|
|
gpupdSim->GetMapSize(&siz);
|
|
SetViewPos(psidi->wptInitialView.wx - (WcFromTc(siz.cx) / 2),
|
|
psidi->wptInitialView.wy - (WcFromTc(siz.cy) / 2), true);
|
|
}
|
|
|
|
// In a multiplayer game supporting a range of players (e.g. 2-4) dummy
|
|
// players are allocated for any 'human' sides that aren't in use by
|
|
// human players. The dummy players must be present so the level, which
|
|
// contains units for all sides, can load properly. Here as we load the
|
|
// level we remove all units owned by the unfulfilled players.
|
|
|
|
word wf = pplr->GetFlags();
|
|
if (wf & kfPlrUnfulfilled) {
|
|
Gob *pgobT;
|
|
for (pgobT = ggobm.GetFirstGob(); pgobT != NULL; ) {
|
|
|
|
// Get the next Gob BEFORE possibly deleting the one it points
|
|
// to
|
|
|
|
Gob *pgobNext = ggobm.GetNextGob(pgobT);
|
|
if (pgobT->GetOwner() == pplr) {
|
|
if (pgobT->GetFlags() & kfGobUnit) {
|
|
UnitGob *punt = (UnitGob *)pgobT;
|
|
punt->Deactivate();
|
|
punt->Delete();
|
|
|
|
// Don't count these units as ever having been built
|
|
pplr->DecUnitBuiltCount(punt->GetUnitType());
|
|
} else {
|
|
// Not a unit gob, but still owned by this player
|
|
// (for example, a tank shot). Remove / delete it.
|
|
ggobm.RemoveGob(pgobT);
|
|
delete pgobT;
|
|
}
|
|
}
|
|
pgobT = pgobNext;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#define knVerSimState 3
|
|
bool Simulation::LoadState(Stream *pstm)
|
|
{
|
|
// Do version handling
|
|
|
|
byte nVer = pstm->ReadByte();
|
|
if (nVer != knVerSimState)
|
|
return false;
|
|
|
|
// Load update count!
|
|
|
|
m_cUpdates = pstm->ReadDword();
|
|
|
|
// Load view
|
|
|
|
WCoord wxView = pstm->ReadWord();
|
|
WCoord wyView = pstm->ReadWord();
|
|
|
|
// Load Simulation tick count
|
|
|
|
m_tCurrent = pstm->ReadDword();
|
|
m_cupdTriggerMgrUpdateLast = pstm->ReadDword();
|
|
|
|
// Load level
|
|
|
|
Assert(m_plvl == NULL);
|
|
m_plvl = new Level();
|
|
if (m_plvl == NULL)
|
|
return false;
|
|
if (!m_plvl->LoadState(pstm)) {
|
|
delete m_plvl;
|
|
m_plvl = NULL;
|
|
return false;
|
|
}
|
|
|
|
// Load state machine mgr (delayed messages)
|
|
|
|
gsmm.LoadState(pstm);
|
|
|
|
// Save command queue (usually empty)
|
|
|
|
gcmdq.LoadState(pstm);
|
|
|
|
// Set view pos
|
|
|
|
SetViewPos(wxView, wyView, true);
|
|
|
|
// Done
|
|
|
|
return pstm->IsSuccess();
|
|
}
|
|
|
|
bool Simulation::SaveState(Stream *pstm)
|
|
{
|
|
// Save version
|
|
|
|
pstm->WriteByte(knVerSimState);
|
|
|
|
// Save update count
|
|
|
|
pstm->WriteDword(m_cUpdates);
|
|
|
|
// Save view x,y
|
|
|
|
pstm->WriteWord(m_wxView);
|
|
pstm->WriteWord(m_wyView);
|
|
|
|
// Save Simulation tick count
|
|
|
|
pstm->WriteDword((dword)m_tCurrent);
|
|
pstm->WriteDword(m_cupdTriggerMgrUpdateLast);
|
|
|
|
// Save level
|
|
|
|
m_plvl->SaveState(pstm);
|
|
|
|
// Save state machine mgr (delayed messages...)
|
|
|
|
gsmm.SaveState(pstm);
|
|
|
|
// Save command queue (usually empty, but just in case...)
|
|
|
|
gcmdq.SaveState(pstm);
|
|
|
|
return pstm->IsSuccess();
|
|
}
|
|
|
|
long Simulation::GetTickCount()
|
|
{
|
|
return m_tCurrent;
|
|
}
|
|
|
|
void Simulation::AddTimer(Timer *ptmr, long ct)
|
|
{
|
|
// fyi you are the first user of a timer based on simulation time.
|
|
// you'll need to add code to LoadState & SaveState to preserve
|
|
// your timer times because simulation time is preserved across load/save
|
|
Assert(false);
|
|
AddTimer(ptmr, ct);
|
|
}
|
|
|
|
// The tCurrent increment can't really ever be anything other than 8 (80 ms)
|
|
// because Gobs assume it as their update rate and increment animations, etc
|
|
// every time they're Updated.
|
|
|
|
void Simulation::Update(CommandQueue *pcmdq)
|
|
{
|
|
if (m_fPaused)
|
|
return;
|
|
|
|
// Give some time to sound servicing
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Advance Simulation time
|
|
|
|
m_tCurrent += kcmsUpdate / 10;
|
|
m_cUpdates++;
|
|
|
|
// Update triggers. This is done here so the triggers act on what the
|
|
// player is currently seeing (e.g., Credits will show >= NNNN when
|
|
// >= NNNN Credits condition is satisfied).
|
|
// PostEvent so that modal actions can occur
|
|
|
|
// Note - CountdownTimer trigger depends on this being better than once a second.
|
|
// TUNE:
|
|
#define kcupdTriggerMgrUpdate 6
|
|
|
|
if (m_cupdTriggerMgrUpdateLast == 0 || abs(m_cUpdates - m_cupdTriggerMgrUpdateLast) >= kcupdTriggerMgrUpdate) {
|
|
m_cupdTriggerMgrUpdateLast = m_cUpdates;
|
|
ggame.ScheduleUpdateTriggers();
|
|
}
|
|
|
|
// Update Players
|
|
|
|
gplrm.Update(m_cUpdates);
|
|
|
|
// Update unit groups.
|
|
|
|
m_plvl->GetUnitGroupMgr()->Update();
|
|
|
|
// Update Build Manager
|
|
// OPT: this doesn't have to be done every update
|
|
|
|
m_pbldm->Update();
|
|
|
|
// Process all queued commands
|
|
#ifdef DEBUG_HELPERS
|
|
extern void UpdateCommandQueueViewer();
|
|
UpdateCommandQueueViewer();
|
|
#endif
|
|
|
|
int cmsg = pcmdq->GetCount();
|
|
#ifdef STATS_DISPLAY
|
|
extern int gcbCommandsQueued;
|
|
gcbCommandsQueued += cmsg * sizeof(Message);
|
|
#endif
|
|
int i = 0;
|
|
Message *pmsg = pcmdq->GetFirst();
|
|
for (; i < cmsg; i++, pmsg++) {
|
|
// Disconnect is processed in order, by all clients, at the same time.
|
|
// To do this, it is turned into a message, since messages have a
|
|
// synchronization mechanism.
|
|
if (pmsg->mid == kmidPlayerDisconnect) {
|
|
HandlePlayerDisconnect(pmsg);
|
|
continue;
|
|
}
|
|
gsmm.RouteMessage(pmsg);
|
|
}
|
|
pcmdq->Clear();
|
|
|
|
gcMessagesPerUpdate = 0;
|
|
|
|
ScanDispatch(m_tCurrent);
|
|
|
|
// Send Update messages to all Gobs with state machines
|
|
|
|
Message msgUpdate;
|
|
memset(&msgUpdate, 0, sizeof(msgUpdate));
|
|
msgUpdate.mid = kmidReservedUpdate;
|
|
msgUpdate.smidSender = ksmidNull;
|
|
|
|
// Snapshot the gids of the Gobs we care about so we aren't vulnerable
|
|
// to the insertion or deletion of elements on the Gob list during the
|
|
// kmidReserveUpdate callbacks.
|
|
|
|
Gid agid[kcpgobMax];
|
|
Gid *pgidT = agid;
|
|
int cgid = 0;
|
|
Gob *pgobT;
|
|
for (pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
if (pgobT->GetFlags() & kfGobStateMachine) {
|
|
*pgidT++ = pgobT->GetId();
|
|
cgid++;
|
|
}
|
|
}
|
|
|
|
pgidT = agid;
|
|
for (i = 0; i < cgid; i++, pgidT++) {
|
|
|
|
// Gob might have been destroyed as a consequence of an earlier
|
|
// Gob's Update. If so, skip it.
|
|
|
|
pgobT = ggobm.GetGob(*pgidT, false);
|
|
if (pgobT == NULL)
|
|
continue;
|
|
|
|
#ifdef MP_DEBUG_SHAREDMEM
|
|
gsideCurrent = pgobT->GetOwner()->GetSide();
|
|
ggidCurrent = pgobT->GetId();
|
|
gcupdCurrent = m_cUpdates;
|
|
ggtCurrent = pgobT->GetType();
|
|
#endif
|
|
|
|
UpdateInterval *punvl = pgobT->GetUpdateInterval();
|
|
if (punvl->Decrement()) {
|
|
msgUpdate.smidReceiver = *pgidT;
|
|
gsmm.SendMsg(&msgUpdate);
|
|
|
|
#ifdef PIL
|
|
if (gfOS5Pa1Device) {
|
|
if ((i & 15) == 0)
|
|
HostSoundServiceProc();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Delayed messages are dispatched AFTER Update messages are sent so if
|
|
// respondants create new Gobs they won't receive an immediate Update
|
|
// before their first paint. This is important for cases like delayed shots.
|
|
|
|
#ifdef DEBUG_HELPERS
|
|
extern void UpdateDelayedMessageViewer();
|
|
UpdateDelayedMessageViewer();
|
|
#endif
|
|
gsmm.DispatchDelayedMessages();
|
|
|
|
long tCurrent = HostGetTickCount();
|
|
|
|
// Sound effects
|
|
|
|
// Credits increasing or decreasing?
|
|
#if 0
|
|
switch (gpplrLocal->GetCreditsDirection()) {
|
|
case 0:
|
|
break;
|
|
|
|
case 1:
|
|
gsndm.PlaySfx(ksfxGameCreditsIncreasing);
|
|
break;
|
|
|
|
case -1:
|
|
// If the credits user is for repair, only play decrease sound over a longer interval
|
|
// TODO: figure out ultimate approach for managing interval for credit spending
|
|
// TUNE:
|
|
#define kctIntervalRepairNotify (75)
|
|
if (true) { // gpplrLocal->GetCreditsConsumer() == knConsumerRepair) {
|
|
static long s_tLastRepairNotify = 0;
|
|
if (s_tLastRepairNotify == 0 || abs(tCurrent - s_tLastRepairNotify) >= kctIntervalRepairNotify) {
|
|
s_tLastRepairNotify = tCurrent;
|
|
gsndm.PlaySfx(ksfxGameCreditsDecreasing);
|
|
}
|
|
} else {
|
|
gsndm.PlaySfx(ksfxGameCreditsDecreasing);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
// Base under attack?
|
|
|
|
// TUNE:
|
|
#define kctIntervalAttackNotify (30 * 100)
|
|
if (gpplrLocal->GetFlags() & kfPlrStructureAttacked) {
|
|
gpplrLocal->SetFlags(gpplrLocal->GetFlags() & ~kfPlrStructureAttacked);
|
|
static long s_tLastAttackNotify = 0;
|
|
if (s_tLastAttackNotify == 0 || abs((int)(tCurrent - s_tLastAttackNotify)) >= kctIntervalAttackNotify) {
|
|
s_tLastAttackNotify = tCurrent;
|
|
gsndm.PlaySfx(ksfxGameBaseUnderAttack);
|
|
ShowAlert(kidsBaseUnderAttack);
|
|
}
|
|
}
|
|
|
|
// Power too low?
|
|
|
|
// TUNE:
|
|
#define kctIntervalPowerLowNotify (30 * 100)
|
|
if (gpplrLocal->GetPowerDemand() != 0 && gpplrLocal->GetPowerDemand() > gpplrLocal->GetPowerSupply()) {
|
|
static long s_tLastPowerLowNotify = 0;
|
|
if (s_tLastPowerLowNotify == 0 || (tCurrent - s_tLastPowerLowNotify) >= kctIntervalPowerLowNotify) {
|
|
s_tLastPowerLowNotify = tCurrent;
|
|
gsndm.PlaySfx(ksfxReactorPowerTooLow);
|
|
ShowAlert(kidsLowPower);
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef MP_STRESS
|
|
// Has to be at the end of Update() because gcmdq serves the needs of both
|
|
// collecting client commands and collecting multiplayer commands. This
|
|
// role switching occurs in in knmidScUpdate handling in
|
|
// SimUIForm::OnReceive.
|
|
|
|
extern void MPStressUpdate();
|
|
MPStressUpdate();
|
|
#endif
|
|
|
|
}
|
|
|
|
void Simulation::HandlePlayerDisconnect(Message *pmsg) {
|
|
// Re-route this as a NetMessage to get proper handling
|
|
if (gptra == NULL) {
|
|
return;
|
|
}
|
|
IGameCallback *pgcb = gptra->GetGameCallback();
|
|
if (pgcb == NULL) {
|
|
return;
|
|
}
|
|
|
|
// This must not be modal
|
|
PlayerDisconnectNetMessage pdnm;
|
|
pdnm.pid = pmsg->PlayerDisconnect.pid;
|
|
pdnm.nReason = pmsg->PlayerDisconnect.nReason;
|
|
NetMessage *pnm = &pdnm;
|
|
pgcb->OnNetMessage(&pnm);
|
|
}
|
|
|
|
#ifdef TRACKSTATE
|
|
void Simulation::TrackState(StateFrame *frame) {
|
|
// Sim stuff
|
|
|
|
int i = frame->AddCountedValue('SIMU');
|
|
frame->AddValue('SEED', (dword)GetRandomSeed(), i);
|
|
frame->AddValue('CMPU', (dword)gcMessagesPerUpdate, i);
|
|
frame->AddValue('CUPD', (dword)m_cUpdates, i);
|
|
|
|
// gobs
|
|
|
|
Gob *pgobT;
|
|
for (pgobT = ggobm.GetFirstGob(); pgobT != NULL;
|
|
pgobT = ggobm.GetNextGob(pgobT)) {
|
|
if (!(pgobT->GetFlags() & kfGobStateMachine)) {
|
|
continue;
|
|
}
|
|
pgobT->TrackState(frame);
|
|
}
|
|
|
|
// players
|
|
|
|
gplrm.TrackState(frame);
|
|
}
|
|
#endif
|
|
|
|
void Simulation::SetSelection(Rect *prc)
|
|
{
|
|
if (gfLassoSelection || !gfDragSelecting) {
|
|
return;
|
|
}
|
|
|
|
for (Gob *pgobT = ggobm.GetFirstGob(); pgobT != NULL;
|
|
pgobT = ggobm.GetNextGob(pgobT)) {
|
|
dword ff = pgobT->GetFlags();
|
|
|
|
// If were in select mode then only Gobs inside the selection rectangle
|
|
// get to be selected.
|
|
|
|
if ((ff & kfGobUnit) == 0) {
|
|
continue;
|
|
}
|
|
bool fSelect = false;
|
|
UnitGob *punt = (UnitGob *)pgobT;
|
|
if ((gfGodMode || punt->GetOwner() == gpplrLocal) &&
|
|
((ff & (kfGobActive | kfGobUnit)) == (kfGobActive | kfGobUnit))) {
|
|
if ((ff & kfGobStructure) == 0 ||
|
|
(punt->GetConsts()->um & kumTowers) != 0) {
|
|
WPoint wptGobCenter;
|
|
punt->GetCenter(&wptGobCenter);
|
|
if (prc->PtIn(wptGobCenter.wx, wptGobCenter.wy)) {
|
|
fSelect = true;
|
|
}
|
|
}
|
|
}
|
|
punt->Select(fSelect);
|
|
}
|
|
}
|
|
|
|
void Simulation::ClearGobSelection()
|
|
{
|
|
// Deselect any selected Gobs
|
|
|
|
for (Gob *pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
dword ff = pgobT->GetFlags();
|
|
if ((ff & (kfGobSelected | kfGobUnit)) == (kfGobSelected | kfGobUnit))
|
|
((UnitGob *)pgobT)->Select(false);
|
|
}
|
|
}
|
|
|
|
void Simulation::SetGobSelected(Gob *pgob)
|
|
{
|
|
ClearGobSelection();
|
|
if ((pgob->GetFlags() & (kfGobSelected | kfGobUnit)) == kfGobUnit)
|
|
((UnitGob *)pgob)->Select(true);
|
|
}
|
|
|
|
void Simulation::SelectSameUnitTypes(UnitGob *punt, TRect *ptrc)
|
|
{
|
|
Player *pplr = punt->GetOwner();
|
|
if (pplr != gpplrLocal)
|
|
return;
|
|
|
|
UnitType ut = punt->GetUnitType();
|
|
|
|
// Store the gobs on the stack.
|
|
Gob *apgob[kcpgobMax / 2];
|
|
|
|
int cpgob = ggobm.FindGobs(ptrc, apgob, ARRAYSIZE(apgob));
|
|
for (int i = 0; i < cpgob; i++) {
|
|
MobileUnitGob *pmuntT = (MobileUnitGob *)apgob[i];
|
|
dword ff = pmuntT->GetFlags();
|
|
if (!(ff & kfGobMobileUnit))
|
|
continue;
|
|
if (pmuntT->GetOwner() != pplr)
|
|
continue;
|
|
if (pmuntT->GetUnitType() != ut)
|
|
continue;
|
|
|
|
pmuntT->Select(true);
|
|
}
|
|
}
|
|
|
|
void Simulation::FindVisibleGobs(Gob ***pppgobVisible, int *pcgobVisible)
|
|
{
|
|
// Reuse the gob list if it's the same update count and view pos
|
|
|
|
if (m_wxView != m_wxViewSave || m_wyView != m_wyViewSave || m_cUpdates != m_cUpdatesSave) {
|
|
// Get visible gobs / refresh cache info
|
|
|
|
Size siz;
|
|
ggame.GetPlayfieldSize(&siz);
|
|
short xView = PcFromUwc(m_wxView) & 0xfffe;
|
|
short yView = PcFromUwc(m_wyView) & 0xfffe;
|
|
Rect rcVisible;
|
|
rcVisible.left = xView;
|
|
rcVisible.top = yView;
|
|
rcVisible.right = xView + siz.cx;
|
|
rcVisible.bottom = yView + siz.cy;
|
|
m_cgobVisible = ggobm.FindGobs(&rcVisible, m_apgobVisible, ARRAYSIZE(m_apgobVisible), m_plvl->GetFogMap()->GetMapPtr());
|
|
m_wxViewSave = m_wxView;
|
|
m_wyViewSave = m_wyView;
|
|
m_cUpdatesSave = m_cUpdates;
|
|
}
|
|
|
|
// Return if asked
|
|
|
|
if (pppgobVisible != NULL)
|
|
*pppgobVisible = m_apgobVisible;
|
|
if (pcgobVisible != NULL)
|
|
*pcgobVisible = m_cgobVisible;
|
|
}
|
|
|
|
void Simulation::DrawBackground(UpdateMap *pupd, DibBitmap *pbm)
|
|
{
|
|
// Draw tiles where the updatemap is invalid
|
|
|
|
short xView = PcFromUwc(m_wxView) & 0xfffe;
|
|
short yView = PcFromUwc(m_wyView) & 0xfffe;
|
|
FogMap *pfogm = m_plvl->GetFogMap();
|
|
TileMap *ptmap = m_plvl->GetTileMap();
|
|
Size sizDib;
|
|
pbm->GetSize(&sizDib);
|
|
ptmap->Draw(pbm, 0, 0, sizDib.cx, sizDib.cy, xView, yView, pfogm->GetMapPtr(), pupd);
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Draw galaxite where the updatemap is invalid
|
|
|
|
pfogm->DrawGalaxite(pbm, xView, yView, pupd, m_plvl->GetTerrainMap()->GetMapPtr());
|
|
HostSoundServiceProc();
|
|
}
|
|
|
|
void Simulation::Draw(UpdateMap *pupd, DibBitmap *pbm)
|
|
{
|
|
// Maps only draw on even coords
|
|
// OPT: can this be streamlined?
|
|
|
|
short xView = PcFromUwc(m_wxView) & 0xfffe;
|
|
short yView = PcFromUwc(m_wyView) & 0xfffe;
|
|
WCoord wxView = WcFromUpc(xView);
|
|
WCoord wyView = WcFromUpc(yView);
|
|
|
|
//
|
|
// Normal full-size map (not mini-map)
|
|
//
|
|
|
|
// Set the view origin so that blt-style map scrolling is possible
|
|
|
|
pupd->SetViewOrigin(xView, yView);
|
|
|
|
// Give some time to sound servicing
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Find visible Gobs
|
|
|
|
FindVisibleGobs();
|
|
|
|
#ifdef STATS_DISPLAY
|
|
gcBitmapsDrawn = 0;
|
|
#endif
|
|
|
|
#ifdef DRAW_PATHS
|
|
if (gfDrawPaths) {
|
|
|
|
// Draw paths
|
|
|
|
for (Gob *pgob = ggobm.GetFirstGob(); pgob != NULL; pgob = ggobm.GetNextGob(pgob)) {
|
|
if ((pgob->GetFlags() & kfGobMobileUnit) && ((MobileUnitGob *)pgob)->m_ppathUnit != NULL)
|
|
((MobileUnitGob *)pgob)->DrawPath(pbm, wxView, wyView);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Figure out what layers are in use. Much of the time only 1 or 2 layers are in use
|
|
|
|
dword ffLayers = 0;
|
|
Gob **ppgobStop = &m_apgobVisible[m_cgobVisible];
|
|
Gob **ppgobT;
|
|
for (ppgobT = m_apgobVisible; ppgobT < ppgobStop; ppgobT++)
|
|
ffLayers |= (*ppgobT)->GetFlags();
|
|
ffLayers &= kfGobLayerMask;
|
|
|
|
// Turn off symbols layers if off
|
|
|
|
if (!(gwfPerfOptions & kfPerfSymbolFlashing))
|
|
ffLayers &= ~kfGobLayerSymbols;
|
|
|
|
// Draw gobs. Skip layers that aren't in use; only call a gob if it wants to draw on
|
|
// this layer
|
|
|
|
dword ffLayerT = kfGobLayerSurfaceDecal;
|
|
for (int nLayer = knLayerSurfaceDecal; nLayer <= knLayerEnd; nLayer++, ffLayerT <<= 1) {
|
|
if (!(ffLayerT & ffLayers))
|
|
continue;
|
|
int cT = 0;
|
|
for (ppgobT = m_apgobVisible; ppgobT < ppgobStop; ppgobT++) {
|
|
Gob *pgobT = *ppgobT;
|
|
dword ff = pgobT->GetFlags();
|
|
|
|
// If gob is marked for redraw...
|
|
|
|
if (ff & kfGobRedraw) {
|
|
// If gob is painting on this layer then paint
|
|
|
|
if (ff & ffLayerT) {
|
|
pgobT->Draw(pbm, xView, yView, nLayer);
|
|
|
|
#ifdef PIL
|
|
if (gfOS5Pa1Device) {
|
|
cT++;
|
|
if ((cT & 15) == 0)
|
|
HostSoundServiceProc();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Give some time to sound servicing
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Clear kfGobRedraw since the gobs are now "valid".
|
|
// Do this after painting because some painting code ends up setting this bit
|
|
|
|
for (ppgobT = m_apgobVisible; ppgobT < ppgobStop; ppgobT++) {
|
|
Gob *pgobT = *ppgobT;
|
|
pgobT->SetFlags(pgobT->GetFlags() & ~kfGobRedraw);
|
|
}
|
|
|
|
#ifdef DRAW_LINES
|
|
if (gfDrawLines) {
|
|
// Draw target lines
|
|
|
|
for (Gob *pgob = ggobm.GetFirstGob(); pgob != NULL; pgob = ggobm.GetNextGob(pgob)) {
|
|
// Total hack-o-rama
|
|
if ((pgob->GetFlags() & kfGobMobileUnit) && ((MobileUnitGob *)pgob)->m_wptTarget.wx != kwxInvalid)
|
|
((MobileUnitGob *)pgob)->DrawTargetLine(pbm, xView, yView);
|
|
if ((pgob->GetType() == kgtRocketTower || pgob->GetType() == kgtMachineGunTower) &&
|
|
((TowerGob *)pgob)->m_wptTarget.wx != kwxInvalid)
|
|
((TowerGob *)pgob)->DrawTargetLine(pbm, xView, yView);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
#ifdef MARK_TILE_BOUNDARIES
|
|
{
|
|
Color clr = GetColor(kiclrWhite);
|
|
Size sizT;
|
|
ggame.GetPlayfieldSize(&sizT);
|
|
for (int y = -PcFromUwc(WcFrac(wyView)); y < sizT.cy; y += gcyTile) {
|
|
for (int x = -PcFromUwc(WcFrac(wxView)); x < sizT.cx; x += gcxTile) {
|
|
pbm->Fill(x, y, 2, 2, clr);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef MARK_OCCUPIED_TILES
|
|
{
|
|
Color clr = GetColor(kiclrWhite);
|
|
Size sizT;
|
|
ggame.GetPlayfieldSize(&sizT);
|
|
int ctxView = sizT.cx / gcxTile + 1;
|
|
int ctyView = sizT.cy / gcyTile + 1;
|
|
TerrainMap *ptrmap = m_plvl->GetTerrainMap();
|
|
|
|
int xView = PcFromWc(wxView);
|
|
int yView = PcFromWc(wyView);
|
|
TCoord txView = TcFromWc(wxView);
|
|
TCoord tyView = TcFromWc(wyView);
|
|
for (TCoord ty = tyView; ty < tyView + ctyView; ty++) {
|
|
for (TCoord tx = txView; tx < txView + ctxView; tx++) {
|
|
if (ptrmap->IsOccupied(tx, ty, 1, 1)) {
|
|
int x = PcFromTc(tx) - xView;
|
|
int y = PcFromTc(ty) - yView;
|
|
pbm->Fill(x + gcxTile / 2 - 1, y + gcyTile / 2 - 1, 2, 2, clr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
pupd->SetViewOrigin(0, 0);
|
|
HostSoundServiceProc();
|
|
|
|
// If the screen is larger than the map size we clear those areas to black color
|
|
|
|
Size sizDib;
|
|
pbm->GetSize(&sizDib);
|
|
Size sizMap;
|
|
m_plvl->GetTileMap()->GetMapSize(&sizMap);
|
|
|
|
if (sizMap.cx < sizDib.cx) {
|
|
Rect rc;
|
|
rc.Set(sizMap.cx, 0, sizDib.cx, sizMap.cy);
|
|
FillHelper(pbm, pupd, &rc, GetColor(kiclrBlack));
|
|
}
|
|
if (sizMap.cy < sizDib.cy) {
|
|
Rect rc;
|
|
rc.Set(0, sizMap.cy, sizDib.cx, sizDib.cy);
|
|
FillHelper(pbm, pupd, &rc, GetColor(kiclrBlack));
|
|
}
|
|
}
|
|
|
|
void Simulation::DrawFog(UpdateMap *pupd, DibBitmap *pbm)
|
|
{
|
|
// Draw fog map
|
|
|
|
short xView = PcFromUwc(m_wxView) & 0xfffe;
|
|
short yView = PcFromUwc(m_wyView) & 0xfffe;
|
|
m_plvl->GetFogMap()->Draw(pbm, xView, yView, pupd);
|
|
HostSoundServiceProc();
|
|
}
|
|
|
|
bool Simulation::SetViewPos(WCoord wx, WCoord wy, bool fInit)
|
|
{
|
|
// Pin the passed position to the bounds of the map
|
|
|
|
Size sizMap;
|
|
m_plvl->GetTileMap()->GetMapSize(&sizMap);
|
|
|
|
Size sizPlayfield;
|
|
ggame.GetPlayfieldSize(&sizPlayfield);
|
|
|
|
WCoord wcxMax, wcyMax;
|
|
int cxMax = sizMap.cx - sizPlayfield.cx;
|
|
int cyyMax = sizMap.cy - sizPlayfield.cy;
|
|
|
|
if (cxMax <= 0 || cxMax > kpcMax)
|
|
wcxMax = 0;
|
|
else
|
|
wcxMax = WcFromUpc(cxMax);
|
|
|
|
if (cyyMax <= 0 || cyyMax > kpcMax)
|
|
wcyMax = 0;
|
|
else
|
|
wcyMax = WcFromUpc(cyyMax);
|
|
|
|
if (wx < 0)
|
|
wx = 0;
|
|
else if (wx > wcxMax)
|
|
wx = wcxMax;
|
|
|
|
if (wy < 0)
|
|
wy = 0;
|
|
else if (wy > wcyMax)
|
|
wy = wcyMax;
|
|
|
|
// If we're initing, we're loading a new level. Invalidate and
|
|
// set the map offset
|
|
|
|
if (fInit) {
|
|
gpupdSim->Reset();
|
|
gpdisp->ResetScrollOffset();
|
|
m_wxView = WcFromPc(0);
|
|
m_wyView = WcFromPc(0);
|
|
}
|
|
|
|
if (m_wxView == wx && m_wyView == wy)
|
|
return false;
|
|
|
|
// Otherwise scroll the map
|
|
|
|
int dx = (PcFromWc(m_wxView) & 0xfffe) - (PcFromWc(wx) & 0xfffe);
|
|
int dy = (PcFromWc(m_wyView) & 0xfffe) - (PcFromWc(wy) & 0xfffe);
|
|
gpfrmmSim->Scroll(dx, dy);
|
|
|
|
// Set the new view pos
|
|
|
|
m_wxView = wx;
|
|
m_wyView = wy;
|
|
|
|
// Force redraw before next update
|
|
|
|
gevm.SetRedrawFlags(kfRedrawDirty | kfRedrawBeforeTimer);
|
|
|
|
// Debug
|
|
|
|
// Trace("View: %d,%d", PcFromWc(m_wxView) & 0xfffe, PcFromWc(m_wyView) & 0xfffe);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Simulation::GetViewPos(WCoord *pwx, WCoord *pwy)
|
|
{
|
|
Assert(pwx != NULL);
|
|
Assert(pwy != NULL);
|
|
|
|
*pwx = m_wxView;
|
|
*pwy = m_wyView;
|
|
}
|
|
|
|
// Passed coordinates are in world coordinates
|
|
|
|
bool Simulation::HitTest(Enum *penm, WCoord wx, WCoord wy, word wf, Gob **ppgob)
|
|
{
|
|
if (penm->m_pvNext == (void *)kEnmFirst) {
|
|
FindVisibleGobs();
|
|
penm->m_dwUser = 0;
|
|
}
|
|
|
|
for (; (int)penm->m_dwUser < m_cgobVisible; penm->m_dwUser++) {
|
|
Gob *pgob = m_apgobVisible[penm->m_dwUser];
|
|
if ((pgob->GetFlags() & wf) != wf)
|
|
continue;
|
|
|
|
WRect wrcT;
|
|
pgob->GetUIBounds(&wrcT);
|
|
if (!wrcT.PtIn(wx, wy))
|
|
continue;
|
|
|
|
*ppgob = pgob;
|
|
penm->m_dwUser++;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Passed coordinates are in world coordinates
|
|
|
|
Gob *Simulation::FingerHitTest(WCoord wx, WCoord wy, word wf,
|
|
bool *pfHitSurrounding)
|
|
{
|
|
// Finger hit testing is tricky, because currently at least,
|
|
// a given tile is smaller than a finger. It's difficult to pick
|
|
// a cell on a grid, when the cells are smaller than a finger. One
|
|
// solution is to make the tiles larger. Instead, the gobs will be
|
|
// hit-tested specially, so that close hits register, while taking
|
|
// special precaution for open terrain between closely packed
|
|
// gobs.
|
|
|
|
// Calc the world rect of the tile being hit
|
|
|
|
WRect wrcTileHit;
|
|
wrcTileHit.left = WcTrunc(wx);
|
|
wrcTileHit.top = WcTrunc(wy);
|
|
wrcTileHit.right = wrcTileHit.left + kwcTile;
|
|
wrcTileHit.bottom = wrcTileHit.top + kwcTile;
|
|
WRect wrcTileHitAccum;
|
|
wrcTileHitAccum.SetEmpty();
|
|
|
|
// Find the "closest" gob being hit, and keep track of hit area overlap
|
|
// with the tile being hit.
|
|
|
|
Gob *pgobHitBest = NULL;
|
|
WCoord wcDistanceBest = kwcMax;
|
|
|
|
FindVisibleGobs();
|
|
for (int i = 0; i < m_cgobVisible; i++) {
|
|
Gob *pgobHit = m_apgobVisible[i];
|
|
if ((pgobHit->GetFlags() & wf) != wf) {
|
|
continue;
|
|
}
|
|
|
|
// Calc the inside rect - at least a tile's width and height
|
|
// The inside rect is a direct hit on the gob. Return the
|
|
// first direct hit, if there is one.
|
|
|
|
WRect wrcInside;
|
|
pgobHit->GetUIBounds(&wrcInside);
|
|
WCoord cwxInflate = 0;
|
|
if (wrcInside.Width() < kwcTile) {
|
|
cwxInflate += (kwcTile - wrcInside.Width()) / 2;
|
|
}
|
|
WCoord cwyInflate = 0;
|
|
if (wrcInside.Height() < kwcTile) {
|
|
cwyInflate += (kwcTile - wrcInside.Height()) / 2;
|
|
}
|
|
wrcInside.Inflate(cwxInflate, cwyInflate);
|
|
|
|
// Calc distance to this. If 0, we're inside and we're done
|
|
// Remember "closest" gob.
|
|
|
|
WCoord wcDistance = wrcInside.GetDistance(wx, wy);
|
|
if (wcDistance == 0) {
|
|
*pfHitSurrounding = false;
|
|
return pgobHit;
|
|
}
|
|
if (wcDistance < wcDistanceBest) {
|
|
wcDistanceBest = wcDistance;
|
|
pgobHitBest = pgobHit;
|
|
}
|
|
|
|
// Calc the outside rect - accumulate overlap with the tile rect.
|
|
|
|
WRect wrcOutside = wrcInside;
|
|
cwxInflate = 0;
|
|
if (wrcOutside.Width() < kwcTile * 2) {
|
|
cwxInflate += (kwcTile * 2 - wrcOutside.Width()) / 2;
|
|
}
|
|
cwyInflate = 0;
|
|
if (wrcOutside.Height() < kwcTile * 2) {
|
|
cwyInflate += (kwcTile * 2 - wrcOutside.Height()) / 2;
|
|
}
|
|
wrcOutside.Inflate(cwxInflate, cwyInflate);
|
|
|
|
// Intersect and accumlate with tile rect.
|
|
|
|
WRect wrcT;
|
|
if (wrcT.Intersect(&wrcTileHit, &wrcOutside)) {
|
|
if (wrcTileHitAccum.IsEmpty()) {
|
|
wrcTileHitAccum = wrcT;
|
|
} else {
|
|
wrcTileHitAccum.Union(&wrcT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no gob or hit too far away, no hit.
|
|
|
|
if (pgobHitBest == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
// Structures get no slop since they are big already
|
|
if (pgobHitBest->GetFlags() & kfGobStructure) {
|
|
if (wcDistanceBest != 0) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Check if this is a mobile headquarters that is moving. In this case,
|
|
// selection is prioritized over local targetting, so that transforming
|
|
// is easier. Local targetting is higher priority if the mobile HQ
|
|
// is not moving.
|
|
|
|
if (pgobHitBest->GetType() == kgtMobileHeadquarters) {
|
|
MobileUnitGob *pmunt = (MobileUnitGob *)pgobHitBest;
|
|
if (pmunt->IsMobile()) {
|
|
if (wcDistanceBest <= kwcTile) {
|
|
*pfHitSurrounding = true;
|
|
return pgobHitBest;
|
|
}
|
|
}
|
|
}
|
|
|
|
// See if the unit is close enough to qualify for being hit on.
|
|
|
|
if (wcDistanceBest > kwcTileHalf) {
|
|
return NULL;
|
|
}
|
|
|
|
// Check to see if the tile hit is completely obscured by overlapping
|
|
// hit regions. If yes, then leave as open terrain (so that terrain can
|
|
// be hit between closely packed gobs).
|
|
|
|
if (wrcTileHitAccum.Equal(&wrcTileHit)) {
|
|
return NULL;
|
|
}
|
|
|
|
// Independent of hit thumbprint, if a gob is already selected, allow
|
|
// the terrain next to it to be selected.
|
|
|
|
if (pgobHitBest->GetFlags() & kfGobSelected) {
|
|
return NULL;
|
|
}
|
|
|
|
// Structures are big enough to hit the regular bounds.
|
|
if (pgobHitBest->GetFlags() & kfGobStructure) {
|
|
return NULL;
|
|
}
|
|
|
|
// This gob has been hit, in the "surrounding" area.
|
|
|
|
*pfHitSurrounding = true;
|
|
return pgobHitBest;
|
|
}
|
|
|
|
void Simulation::Pause(bool fPause)
|
|
{
|
|
m_fPaused = fPause;
|
|
}
|
|
|
|
bool Simulation::IsPaused()
|
|
{
|
|
return m_fPaused;
|
|
}
|
|
|
|
} // namespace wi
|