#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