mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2025-12-16 12:08:36 +00:00
2824 lines
75 KiB
C++
2824 lines
75 KiB
C++
#include "game/ht.h"
|
|
#include "mpshared/netmessage.h"
|
|
#include "game/wistrings.h"
|
|
#include "game/statetracker.h"
|
|
#include "game/stateframe.h"
|
|
#include "game/uploader.h"
|
|
#include "game/serviceurls.h"
|
|
#include "game/chatter.h"
|
|
#include "game/gameform.h"
|
|
#include "base/sigslot.h"
|
|
#include <string>
|
|
|
|
namespace wi {
|
|
|
|
// Global representing the contiguous rect opaquing the map, in map relative tile coords
|
|
|
|
TRect *gptrcMapOpaque;
|
|
TRect gtrcMapOpaque;
|
|
|
|
// Handy dandy global flags used for enabling various test features
|
|
|
|
#ifdef STATS_DISPLAY
|
|
int gcBitmapsDrawn;
|
|
extern bool gfShowStats;
|
|
int gcbCommandsQueued;
|
|
#endif
|
|
int gcMessagesPerUpdate;
|
|
|
|
extern bool gfShowFPS;
|
|
extern bool gfSuspendUpdates;
|
|
extern bool gfSingleStep;
|
|
|
|
short gcPaint = 0;
|
|
#ifdef STATS_DISPLAY
|
|
static short s_cUpdateRects = 0;
|
|
#endif
|
|
static fix s_cfxFPS = 0;
|
|
static long s_tLastFPSUpdate = 0;
|
|
|
|
// Misc SimUI management variables
|
|
|
|
bool gfDragSelecting;
|
|
WPoint s_wptSelect1, s_wptSelect2;
|
|
WRect gwrcSelection;
|
|
WPoint s_awptSelection[250];
|
|
int s_cwptSelection;
|
|
bool gfLassoSelection = false;
|
|
#ifdef STATS_DISPLAY
|
|
long gcPathScoresCalced = 0;
|
|
#endif
|
|
|
|
bool PtInPolygon(WPoint *awpt, int cwpt, WCoord wx, WCoord wy) secSimUIForm;
|
|
|
|
//
|
|
// Form used for waiting
|
|
//
|
|
|
|
WaitForm::WaitForm(char *pszTitle, bool fFullScreen)
|
|
{
|
|
m_pszTitle = pszTitle;
|
|
m_fFullScreen = fFullScreen;
|
|
}
|
|
|
|
bool WaitForm::Init(FormMgr *pfrmm, IniReader *pini, word idf)
|
|
{
|
|
if (!Form::Init(pfrmm, pini, idf))
|
|
return false;
|
|
|
|
// Size the form to be full screen
|
|
|
|
DibBitmap *pbm = m_pfrmm->GetDib();
|
|
Size siz;
|
|
pbm->GetSize(&siz);
|
|
|
|
// Remember old bounds
|
|
|
|
m_rcOrig = m_rc;
|
|
|
|
// Set text if passed
|
|
|
|
if (m_pszTitle != NULL) {
|
|
LabelControl *plbl = (LabelControl *)GetControlPtr(kidcTitle);
|
|
plbl->SetText(m_pszTitle);
|
|
}
|
|
|
|
int xNew = (siz.cx - m_rc.Width()) / 2;
|
|
int yNew = (siz.cy - m_rc.Height()) / 2;
|
|
|
|
if (m_fFullScreen) {
|
|
// Reposition the controls
|
|
|
|
for (int n = 0; n < m_cctl; n++) {
|
|
Control *pctl = m_apctl[n];
|
|
Rect rcCtl;
|
|
pctl->GetRect(&rcCtl);
|
|
int xNewCtl = rcCtl.left + xNew;
|
|
int yNewCtl = rcCtl.top + yNew;
|
|
pctl->SetPosition(xNewCtl, yNewCtl);
|
|
}
|
|
Rect rcNew;
|
|
rcNew.Set(0, 0, siz.cx, siz.cy);
|
|
SetRect(&rcNew);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void WaitForm::OnPaintBackground(DibBitmap *pbm, UpdateMap *pupd)
|
|
{
|
|
Form::OnPaintBackground(pbm, pupd);
|
|
|
|
DrawBorder(pbm, &m_rcOrig, gcxyBorder, GetColor(kiclrCyanSideFirst), pupd);
|
|
}
|
|
|
|
// This is a hack so that sigslot.h doesn't need to be included from ht.h.
|
|
|
|
class SlotHack : public base::has_slots<> {
|
|
public:
|
|
void Connect(SimUIForm *sui, Chatter *chatter) {
|
|
if (chatter != NULL) {
|
|
chatter->SignalOnBlink.connect(this, &SlotHack::OnChatButtonBlink);
|
|
chatter->SignalOnPlayers.connect(this, &SlotHack::OnPlayersButton);
|
|
}
|
|
sui_ = sui;
|
|
chatter_ = chatter;
|
|
}
|
|
void Disconnect() {
|
|
if (chatter_ != NULL) {
|
|
chatter_->SignalOnBlink.disconnect(this);
|
|
chatter_->SignalOnPlayers.disconnect(this);
|
|
}
|
|
}
|
|
void OnChatButtonBlink(bool fOn) {
|
|
if (sui_ != NULL) {
|
|
sui_->OnChatButtonBlink(fOn);
|
|
}
|
|
}
|
|
void OnPlayersButton() {
|
|
if (sui_ != NULL) {
|
|
sui_->OnPlayersButton();
|
|
}
|
|
}
|
|
SimUIForm *sui_;
|
|
Chatter *chatter_;
|
|
};
|
|
SlotHack g_slothack;
|
|
|
|
//
|
|
// SimUIForm implementation
|
|
//
|
|
|
|
bool SimUIForm::s_fReadyForPaint;
|
|
|
|
SimUIForm::SimUIForm(word wfRole, dword gameid, Chatter *chatter)
|
|
{
|
|
s_fReadyForPaint = false;
|
|
gfDragSelecting = false;
|
|
s_cwptSelection = 0;
|
|
|
|
m_fTimerAdded = false;
|
|
|
|
m_wfRole = wfRole;
|
|
m_pcmdqServer = NULL;
|
|
m_pfrmWaitingForAllPlayers = NULL;
|
|
m_tLastCommunication = 0;
|
|
|
|
m_nStateMoveTarget = 0;
|
|
|
|
m_ppenh = NULL;
|
|
#if defined(IPHONE) || defined(__IPHONEOS__) || defined(__ANDROID__)
|
|
SetUIType(kuitFinger);
|
|
#else
|
|
SetUIType(kuitStylus);
|
|
#endif
|
|
m_punmrFirst = NULL;
|
|
|
|
m_gameid = gameid;
|
|
m_pchatter = chatter;
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->SetChatTitle("Game Chat");
|
|
}
|
|
#ifdef TRACKSTATE
|
|
m_ptracker = new StateTracker(m_gameid);
|
|
m_fSyncError = false;
|
|
#endif
|
|
|
|
g_slothack.Connect(this, chatter);
|
|
}
|
|
|
|
SimUIForm::~SimUIForm()
|
|
{
|
|
delete m_pfrmWaitingForAllPlayers;
|
|
delete m_pfrmLag;
|
|
if (m_fTimerAdded)
|
|
gtimm.RemoveTimer(this);
|
|
if (m_wfRole & kfRoleMultiplayer) {
|
|
gptra->SetCallback(NULL);
|
|
gptra->SetGameCallback(NULL);
|
|
}
|
|
|
|
if (m_pcmdqServer != NULL) {
|
|
m_pcmdqServer->Exit();
|
|
delete m_pcmdqServer;
|
|
}
|
|
|
|
delete m_ppenh;
|
|
|
|
#ifdef TRACKSTATE
|
|
delete m_ptracker;
|
|
#endif
|
|
|
|
g_slothack.Disconnect();
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->HideChat();
|
|
}
|
|
}
|
|
|
|
void SimUIForm::SetUIType(UIType uit)
|
|
{
|
|
switch (uit) {
|
|
case kuitFinger:
|
|
delete m_ppenh;
|
|
m_ppenh = new FingerHandler(this);
|
|
break;
|
|
|
|
case kuitStylus:
|
|
delete m_ppenh;
|
|
m_ppenh = new StylusHandler(this);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SimUIForm::OnChatButtonBlink(bool fOn) {
|
|
// Annoying since the button is visible during observe, and you can already
|
|
// see the chat on screen.
|
|
#if 0
|
|
if (gpplrLocal->GetFlags() & kfPlrObserver) {
|
|
GetControlPtr(kidcChat)->Show(fOn);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void SimUIForm::OnPlayersButton() {
|
|
std::string s = GameForm::GetPlayersString();
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->AddChat("", s.c_str(), true);
|
|
}
|
|
}
|
|
|
|
static SimUIForm *gpsui;
|
|
|
|
const long kctStartTimeout = 1500; // 15 seconds
|
|
|
|
bool SimUIForm::Init(FormMgr *pfrmm, IniReader *pini, word idf)
|
|
{
|
|
gpsui = this;
|
|
|
|
m_tStartTimeout = HostGetTickCount() + kctStartTimeout;
|
|
|
|
// Init move target
|
|
|
|
m_nStateMoveTarget = 0;
|
|
m_aniMoveTarget.Init(g_panidMoveTarget);
|
|
|
|
if (m_wfRole & kfRoleMultiplayer) {
|
|
m_pcmdqServer = new CommandQueue();
|
|
Assert(m_pcmdqServer != NULL, "out of memory!");
|
|
if (m_pcmdqServer == NULL)
|
|
return false;
|
|
if (!m_pcmdqServer->Init(kcmsgCommandQueueMax)) {
|
|
return false;
|
|
}
|
|
|
|
// Transport callback
|
|
|
|
gptra->SetCallback(this);
|
|
gptra->SetGameCallback(this);
|
|
|
|
// Create a big form to cover everything and show "Waiting for all
|
|
// players"
|
|
|
|
m_pfrmWaitingForAllPlayers = gpmfrmm->LoadForm(gpiniForms, kidfWaiting, new WaitForm("WAITING FOR ALL PLAYERS...", true));
|
|
if (m_pfrmWaitingForAllPlayers != NULL) {
|
|
gpmfrmm->DrawFrame(false);
|
|
}
|
|
}
|
|
|
|
// New multiplayer design: client maintains its own update timer.
|
|
// Client messages are sent "just in time" by server, scheduled to be
|
|
// executed on a given update. Clients know what update to expect the
|
|
// next message from the server. Every so often the client sends feedback
|
|
// to the server on latency (time of actual update - time message received)
|
|
// so that the server can tune when client messages are sent, in real
|
|
// time. This approach works better over the internet. The previous
|
|
// approach worked fine over local networks.
|
|
|
|
gtimm.AddTimer(this, gtGameSpeed);
|
|
m_fTimerAdded = true;
|
|
|
|
while (m_punmrFirst != NULL) {
|
|
UpdateNetMessageRecord *punmrT = m_punmrFirst;
|
|
m_punmrFirst = punmrT->punmrNext;
|
|
delete punmrT->punm;
|
|
delete punmrT;
|
|
}
|
|
|
|
// Let the base Form initialize the controls
|
|
|
|
bool fSuccess = Form::Init(pfrmm, pini, idf);
|
|
|
|
if (m_wfRole & kfRoleMultiplayer) {
|
|
m_cUpdatesBlock = 0;
|
|
m_msUpdatesBlock = 0;
|
|
SendUpdateResult(0, 0, 0);
|
|
}
|
|
|
|
// Size the rect to the dib
|
|
|
|
DibBitmap *pbmSim = m_pfrmm->GetDib();
|
|
Size sizSim;
|
|
pbmSim->GetSize(&sizSim);
|
|
Rect rcSim;
|
|
rcSim.Set(0, 0, sizSim.cx, sizSim.cy);
|
|
SetRect(&rcSim);
|
|
|
|
// Set control positions
|
|
|
|
Control *pctlT;
|
|
Rect rcT;
|
|
|
|
// Objective is at upper-left
|
|
|
|
pctlT = GetControlPtr(kidcObjective);
|
|
Assert(pctlT != NULL);
|
|
pctlT->SetPosition(0, 0);
|
|
|
|
// Countdown timer is at upper-right
|
|
|
|
pctlT = GetControlPtr(kidcCountdown);
|
|
Assert(pctlT != NULL);
|
|
pctlT->GetRect(&rcT);
|
|
pctlT->SetPosition(sizSim.cx - rcT.Width(), 0);
|
|
|
|
// FPS is at upper right, down 1 line to leave room for countdown timer
|
|
|
|
pctlT = GetControlPtr(kidcFps);
|
|
Assert(pctlT != NULL);
|
|
pctlT->GetRect(&rcT);
|
|
pctlT->SetPosition(sizSim.cx - rcT.Width(), rcT.Height());
|
|
|
|
// Chat is at upper right. Hide chat button for now. It only appears
|
|
// in observer mode.
|
|
|
|
pctlT = GetControlPtr(kidcChat);
|
|
pctlT->GetRect(&rcT);
|
|
pctlT->SetPosition(sizSim.cx - rcT.Width() - rcT.top, rcT.top);
|
|
pctlT->Show(false);
|
|
|
|
// Alert is at bottom left
|
|
|
|
pctlT = GetControlPtr(kidcAlert);
|
|
Assert(pctlT != NULL);
|
|
pctlT->GetRect(&rcT);
|
|
pctlT->SetPosition(0, sizSim.cy - rcT.Height());
|
|
|
|
// Reset lag state
|
|
|
|
m_pfrmLag = NULL;
|
|
m_pidLagging = kpidNeutral;
|
|
|
|
// Show / hide these controls
|
|
|
|
GetControlPtr(kidcFps)->Show(gfShowFPS);
|
|
GetControlPtr(kidcAlert)->Show(false);
|
|
Assert(gsim.GetLevel() != NULL);
|
|
Assert(gsim.GetLevel()->GetTriggerMgr() != NULL);
|
|
GetControlPtr(kidcCountdown)->Show(gsim.GetLevel()->GetTriggerMgr()->GetCountdownTimer()->GetFlags() & kfCtVisibleAtStart);
|
|
|
|
// This form wants pen2 events
|
|
m_wf |= kfFrmDemandPen2;
|
|
|
|
return fSuccess;
|
|
}
|
|
|
|
//
|
|
// IGameCallback implementation
|
|
//
|
|
|
|
void SimUIForm::OnNetMessage(NetMessage **ppnm)
|
|
{
|
|
#ifdef TRACKSTATE
|
|
// If there has been a sync error, do not respond to any messages
|
|
if (m_fSyncError) {
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// Setting *ppnm = NULL allows the callee to keep the NetMessage
|
|
// rather than the caller deleting it
|
|
NetMessage *pnm = *ppnm;
|
|
|
|
// Track the last time communication was received on this device
|
|
|
|
m_tLastCommunication = HostGetTickCount();
|
|
|
|
switch (pnm->nmid) {
|
|
case knmidScPlayerDisconnect:
|
|
{
|
|
// This must run at the same point on all clients, since
|
|
// it sets the kfPlrComputer flag.
|
|
PlayerDisconnectNetMessage *pdm = (PlayerDisconnectNetMessage*)pnm;
|
|
OnPlayerDisconnectNotify(pdm->pid, pdm->nReason);
|
|
|
|
// Asynchronously, check for game over
|
|
Event evt;
|
|
evt.eType = checkGameOverEvent;
|
|
evt.dw = pdm->pid;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
break;
|
|
|
|
case knmidScLagNotify:
|
|
// This player is being notified by the server of another player
|
|
// that is lagging.
|
|
|
|
OnLagNotify(((LagNotifyNetMessage *)pnm)->pidLagging,
|
|
((LagNotifyNetMessage *)pnm)->cSeconds);
|
|
break;
|
|
|
|
case knmidScUpdate:
|
|
QueueUpdateMessage((UpdateNetMessage *)*ppnm);
|
|
|
|
// Tell the caller to not delete
|
|
*ppnm = NULL;
|
|
break;
|
|
|
|
case knmidScCheckWin:
|
|
OnCheckWin(((CheckWinNetMessage *)pnm)->pid);
|
|
break;
|
|
|
|
#ifdef TRACKSTATE
|
|
case knmidScSyncError:
|
|
ReportSyncError();
|
|
break;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void SimUIForm::OnReceiveChat(const char *player, const char *chat) {
|
|
ShowAlert(base::Format::ToString("%s: %s", player, chat));
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->AddChat(player, chat, false);
|
|
}
|
|
}
|
|
|
|
void SimUIForm::OnGameDisconnect()
|
|
{
|
|
HtMessageBox(kfMbWhiteBorder, "Network",
|
|
"You've been disconnected from this game. Press OK to continue.");
|
|
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.eType = gameOverEvent;
|
|
evt.dw = knGoAbortLevel;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
|
|
void SimUIForm::OnStatusUpdate(char *pszStatus)
|
|
{
|
|
// nothing to do here
|
|
}
|
|
|
|
void SimUIForm::OnConnectionClose()
|
|
{
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.idf = m_idf;
|
|
evt.eType = connectionCloseEvent;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
|
|
std::string s_message;
|
|
|
|
void SimUIForm::OnShowMessage(const char *message)
|
|
{
|
|
s_message = message;
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.idf = m_idf;
|
|
evt.eType = showMessageEvent;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
|
|
bool SimUIForm::OnFilterEvent(Event *pevt) {
|
|
if (pevt->eType == connectionCloseEvent) {
|
|
// My connection to the server broke!
|
|
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->HideChat();
|
|
}
|
|
HtMessageBox(kfMbWhiteBorder, "Comm Problem",
|
|
"Your connection to the server dropped.");
|
|
|
|
if ((gpplrLocal->GetFlags() & kfPlrObserver) == 0) {
|
|
gpplrLocal->ShowObjectives(ksoWinSummary, false, true);
|
|
}
|
|
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.eType = gameOverEvent;
|
|
evt.dw = knGoAbortLevel;
|
|
gevm.PostEvent(&evt);
|
|
return true;
|
|
}
|
|
|
|
if (pevt->eType == showMessageEvent) {
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->HideChat();
|
|
}
|
|
HtMessageBox(kfMbWhiteBorder, "Server Message", s_message.c_str());
|
|
s_message = "";
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#ifdef TRACKSTATE
|
|
void SimUIForm::ReportSyncError()
|
|
{
|
|
// A sync error has occured, must remember this so no state changes
|
|
// from here forward
|
|
m_fSyncError = true;
|
|
|
|
// Remove the game timer
|
|
if (m_fTimerAdded) {
|
|
gtimm.RemoveTimer(this);
|
|
m_fTimerAdded = false;
|
|
}
|
|
|
|
// Alert the user to what is happening
|
|
HtMessageBox(kfMbWhiteBorder, "Sync Error",
|
|
"This mission has experienced a sync error and must end. "
|
|
"Please notify the mission author.");
|
|
gpmfrmm->DrawFrame(true);
|
|
|
|
// Upload the state tracker state
|
|
base::ByteBuffer *pbb = m_ptracker->ToJson();
|
|
const char *url = base::Format::ToString("%s?gameid=%d&pid=%d",
|
|
kszSyncErrorUploadUrl, m_gameid, gpplrLocal->GetId());
|
|
UploadByteBuffer(gphttp, pbb, url);
|
|
|
|
// End the game
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.eType = gameOverEvent;
|
|
evt.dw = knGoAbortLevel;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
#endif
|
|
|
|
void SimUIForm::QueueUpdateMessage(UpdateNetMessage *punm) {
|
|
#ifdef TRACKSTATE
|
|
m_ptracker->ExpireFrames(punm->cUpdatesSync);
|
|
#endif
|
|
|
|
// Remember the UpdateNetMessage
|
|
|
|
UpdateNetMessageRecord *punmr = new UpdateNetMessageRecord;
|
|
punmr->msReceived = HostGetMillisecondCount();
|
|
punmr->punm = punm;
|
|
punmr->punmrNext = NULL;
|
|
|
|
// Put it on the end of the list
|
|
|
|
UpdateNetMessageRecord **ppunmr = &m_punmrFirst;
|
|
while ((*ppunmr) != NULL) {
|
|
ppunmr = &(*ppunmr)->punmrNext;
|
|
}
|
|
*ppunmr = punmr;
|
|
|
|
// If the client is blocked waiting for this update, process it
|
|
// right away.
|
|
|
|
bool fUpdateNow = false;
|
|
if (gsim.GetUpdateCount() + 1 == m_cUpdatesBlock) {
|
|
fUpdateNow = true;
|
|
}
|
|
|
|
// It's possible the client gets behind. For example, if the client
|
|
// is blocked on network i/o and then sudden 5 updates come in,
|
|
// running the timer as normal would take too long for it to catch up with
|
|
// the server. So, process the commands here.
|
|
|
|
if (m_punmrFirst != NULL && m_punmrFirst->punmrNext != NULL) {
|
|
fUpdateNow = true;
|
|
}
|
|
|
|
// Running the simulation requires running from the event loop,
|
|
// because of the state set, and state checked, from within
|
|
// the event loop.
|
|
|
|
if (fUpdateNow) {
|
|
Event evt;
|
|
evt.eType = runUpdatesNowEvent;
|
|
evt.idf = m_idf;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
}
|
|
|
|
void SimUIForm::RunUpdatesNow()
|
|
{
|
|
// If blocked waiting for an update and there is one, process it
|
|
// and reset the timer
|
|
|
|
if (m_punmrFirst != NULL && gsim.GetUpdateCount() + 1 == m_cUpdatesBlock) {
|
|
gtimm.RemoveTimer(this);
|
|
OnTimer(0);
|
|
gtimm.AddTimer(this, gtGameSpeed);
|
|
ggame.UpdateTriggers();
|
|
}
|
|
|
|
// If there are more than one updates available, process them to
|
|
// catch up.
|
|
|
|
while (m_punmrFirst != NULL && m_punmrFirst->punmrNext != NULL) {
|
|
gtimm.RemoveTimer(this);
|
|
OnTimer(0);
|
|
gtimm.AddTimer(this, gtGameSpeed);
|
|
ggame.UpdateTriggers();
|
|
}
|
|
}
|
|
|
|
bool SimUIForm::ProcessUpdateMessage(CommandQueue *pcmdq)
|
|
{
|
|
// This should not happen
|
|
if (gsim.GetUpdateCount() + 1 > m_cUpdatesBlock) {
|
|
Trace("Broken!");
|
|
return false;
|
|
}
|
|
|
|
// Commands are processed at block points. If not at block point yet,
|
|
// continue the simulation.
|
|
|
|
if (gsim.GetUpdateCount() + 1 < m_cUpdatesBlock) {
|
|
pcmdq->Clear();
|
|
return true;
|
|
}
|
|
|
|
// At cUpdatesBlock for the first time? If so remember the timestamp
|
|
// for latency calc purposes
|
|
|
|
if (m_msUpdatesBlock == 0) {
|
|
m_msUpdatesBlock = HostGetMillisecondCount();
|
|
}
|
|
|
|
// No messages to process? Block until they come. Users don't like
|
|
// blocking since it results in on screen stutter, perceived as lag.
|
|
|
|
if (m_punmrFirst == NULL) {
|
|
Trace("BLOCK!");
|
|
return false;
|
|
}
|
|
|
|
// Take the next set of commands and queue them up
|
|
|
|
UpdateNetMessageRecord *punmr = m_punmrFirst;
|
|
m_punmrFirst = punmr->punmrNext;
|
|
UpdateNetMessage *punm = punmr->punm;
|
|
pcmdq->Clear();
|
|
for (int i = 0; i < punm->cmsgCommands; i++) {
|
|
pcmdq->Enqueue(&punm->amsgCommands[i]);
|
|
}
|
|
|
|
// Send the update result with latency info
|
|
// Don't send if cUpdatesBlock_ == 0. This is a special case
|
|
// that is handled elsewhere (search for calls to SendUpdatesResult).
|
|
|
|
int cmsLatency = (int)(m_msUpdatesBlock - punmr->msReceived);
|
|
|
|
#ifdef TRACKSTATE
|
|
dword hash = m_ptracker->GetHash();
|
|
Trace("GetHash: end update %d", gsim.GetUpdateCount());
|
|
m_ptracker->SetNextBlock();
|
|
#else
|
|
dword hash = 0;
|
|
#endif
|
|
|
|
#if 0
|
|
// Adjust the timer trigger, without changing the rate. If updates are
|
|
// coming a little too soon, wait a bit before triggering next. If updates
|
|
// are a bit old, trigger sooner next time. This maintains "just in time"
|
|
// update delivery.
|
|
if (cmsLatency <= 0) {
|
|
gtimm.BoostTimer(this, 1);
|
|
}
|
|
if (cmsLatency >= 200) {
|
|
gtimm.BoostTimer(this, -1);
|
|
}
|
|
#endif
|
|
|
|
if (m_cUpdatesBlock != 0) {
|
|
SendUpdateResult(m_cUpdatesBlock, cmsLatency, hash);
|
|
}
|
|
|
|
// This is the next point to block. Ideally the server sends another
|
|
// UpdateNetMessage before the clients get to this point. This is how
|
|
// stuttering is reduced, which users perceive as lag.
|
|
|
|
m_cUpdatesBlock = punm->cUpdatesBlock;
|
|
m_msUpdatesBlock = 0;
|
|
|
|
delete punm;
|
|
delete punmr;
|
|
|
|
// Ok to update the simulation
|
|
return true;
|
|
}
|
|
|
|
void SimUIForm::SendUpdateResult(int cUpdatesBlock, int cmsLatency,
|
|
dword hash)
|
|
{
|
|
UpdateResultNetMessage urnm;
|
|
urnm.ur.cUpdatesBlock = cUpdatesBlock;
|
|
urnm.ur.hash = hash;
|
|
urnm.ur.cmsLatency = cmsLatency;
|
|
gptra->SendNetMessage(&urnm);
|
|
}
|
|
|
|
void SimUIForm::OnTimer(long tCurrent) {
|
|
#ifdef TRACKSTATE
|
|
// If there has been a sync error, do nothing
|
|
if (m_fSyncError) {
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (gfSuspendUpdates && !gfSingleStep) {
|
|
gevm.SetRedrawFlags(kfRedrawDirty);
|
|
return;
|
|
}
|
|
gfSingleStep = false;
|
|
if (gsim.IsPaused()) {
|
|
return;
|
|
}
|
|
|
|
// TUNE:
|
|
#define kctLocalLag 200
|
|
if ((m_wfRole & (kfRoleMultiplayer | kfRoleServer)) == kfRoleMultiplayer) {
|
|
if (m_tLastCommunication != 0) {
|
|
long tCurrent = HostGetTickCount();
|
|
if (tCurrent - m_tLastCommunication >= kctLocalLag) {
|
|
ShowAlert("Network lag");
|
|
|
|
// Request redraw. When the client is waiting on the server it
|
|
// won't redraw unless asked to.
|
|
|
|
gevm.SetRedrawFlags(kfRedrawDirty);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send queued commands to server whenever there are any
|
|
|
|
if ((m_wfRole & (kfRoleMultiplayer | kfRoleServer)) == kfRoleMultiplayer) {
|
|
|
|
int cmsg = gcmdq.GetCount();
|
|
if (cmsg != 0) {
|
|
int cbcc = sizeof(ClientCommandsNetMessage) +
|
|
((cmsg - 1) * sizeof(Message));
|
|
ClientCommandsNetMessage *pcc =
|
|
(ClientCommandsNetMessage *)new byte[cbcc];
|
|
pcc->nmid = knmidCsClientCommands;
|
|
pcc->cb = cbcc;
|
|
pcc->cmsgCommands = cmsg;
|
|
memcpy(&pcc->amsgCommands, gcmdq.GetFirst(),
|
|
cmsg * sizeof(Message));
|
|
gptra->SendNetMessage(pcc);
|
|
delete[] pcc;
|
|
}
|
|
gcmdq.Clear();
|
|
}
|
|
|
|
if ((m_wfRole & (kfRoleMultiplayer | kfRoleServer)) == kfRoleMultiplayer) {
|
|
// Process the next update message
|
|
if (ProcessUpdateMessage(m_pcmdqServer)) {
|
|
#ifdef TRACKSTATE
|
|
BeginTrackState();
|
|
#endif
|
|
gsim.Update(m_pcmdqServer);
|
|
#ifdef TRACKSTATE
|
|
EndTrackState();
|
|
#endif
|
|
}
|
|
} else {
|
|
// Update (i.e., 'step') the Simulation
|
|
|
|
gsim.Update(&gcmdq);
|
|
}
|
|
|
|
// Update UI
|
|
|
|
Update();
|
|
|
|
#ifdef DEBUG_HELPERS
|
|
extern void UpdateLog();
|
|
UpdateLog();
|
|
#endif
|
|
|
|
// Check to see if something needs to be done with the lag form
|
|
|
|
CheckLagForm();
|
|
}
|
|
|
|
#ifdef TRACKSTATE
|
|
StateFrame *gpframeCurrent;
|
|
|
|
void SimUIForm::BeginTrackState()
|
|
{
|
|
long cUpdates = gsim.GetUpdateCount() + 1;
|
|
StateFrame *frame = m_ptracker->AddFrame(cUpdates);
|
|
int i = frame->AddCountedValue('SMUI');
|
|
frame->AddValue('BLCK', (dword)m_cUpdatesBlock, i);
|
|
|
|
// Set this so it can be used during gsim.Update()
|
|
gpframeCurrent = frame;
|
|
}
|
|
|
|
void SimUIForm::EndTrackState()
|
|
{
|
|
gsim.TrackState(gpframeCurrent);
|
|
gpframeCurrent = NULL;
|
|
}
|
|
#endif
|
|
|
|
bool SimUIForm::EventProc(Event *pevt)
|
|
{
|
|
switch (pevt->eType) {
|
|
case runUpdatesNowEvent:
|
|
RunUpdatesNow();
|
|
break;
|
|
}
|
|
|
|
return Form::EventProc(pevt);
|
|
}
|
|
|
|
void SimUIForm::CheckMultiplayerGameOver(Pid pidLeft) {
|
|
if (!gfMultiplayer) {
|
|
return;
|
|
}
|
|
|
|
// This should be handled by triggers, but for now it is
|
|
// hamfistedly hardwired here.
|
|
//
|
|
// If transitioning to a single player (or team) then assume this player
|
|
// has won. This isn't always the right choice, that is why it should
|
|
// be handled by triggers. But there are a lot of maps made that
|
|
// don't handle this case, and there is not enough conditions
|
|
// available currently to handle resigning / leaving the game with
|
|
// triggers.
|
|
|
|
if (gplrm.DetectTransitionToSingleHumanTeam(pidLeft)) {
|
|
|
|
// If this player isn't an observer it means they haven't
|
|
// officially won/lost yet. Let them know the game is over and set
|
|
// them to be an Observer.
|
|
|
|
if ((gpplrLocal->GetFlags() &
|
|
(kfPlrObserver | kfPlrSummaryShown)) == 0) {
|
|
gpplrLocal->ShowObjectives(ksoWinSummary, false, false);
|
|
|
|
if (!ggame.AskObserveGame()) {
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.eType = gameOverEvent;
|
|
evt.dw = knGoAbortLevel;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
} else {
|
|
// Some other player is victorious. Show alert.
|
|
// There is only one non-observer, non-computer player
|
|
// left.
|
|
|
|
Player *pplr = gplrm.GetNextHumanPlayer(NULL);
|
|
if (pplr != NULL) {
|
|
char szT[100];
|
|
sprintf(szT, "%s is victorious!", pplr->GetName());
|
|
ShowAlert(szT);
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->AddChat("", szT, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SimUIForm::SetObserving()
|
|
{
|
|
gpplrLocal->SetObjective("< Observing >");
|
|
gsim.GetLevel()->GetFogMap()->RevealAll(gpupdSim);
|
|
GetControlPtr(kidcFps)->Show(false);
|
|
GetControlPtr(kidcCountdown)->Show(false);
|
|
GetControlPtr(kidcChat)->Show(true);
|
|
}
|
|
|
|
void SimUIForm::OnCheckWin(Pid pid) {
|
|
if (gpplrLocal == NULL || gptra == NULL) {
|
|
return;
|
|
}
|
|
|
|
#if 0
|
|
// Disable challenges for now. It needs to be tested against mission
|
|
// scripting, and it needs to be updated to handle allies. Because challenge
|
|
// is disabled, it means the server believes the first client that says it
|
|
// won. So some clever hacker can cause mayhem.
|
|
|
|
// A client has claimed win. In order to be valid, the local player
|
|
// must be marked either winner (if == this pid) or a loser.
|
|
|
|
bool fValid = true;
|
|
if (gpplrLocal->GetId() == pid) {
|
|
if ((gpplrLocal->GetFlags() & (kfPlrWinner | kfPlrLoser)) !=
|
|
kfPlrWinner) {
|
|
fValid = false;
|
|
}
|
|
} else {
|
|
if ((gpplrLocal->GetFlags() & (kfPlrWinner | kfPlrLoser)) !=
|
|
kfPlrLoser) {
|
|
fValid = false;
|
|
}
|
|
}
|
|
if (!fValid) {
|
|
ChallengeWinNetMessage cwnm;
|
|
gptra->SendNetMessage(&cwnm);
|
|
}
|
|
#endif
|
|
|
|
// Assume this player won for purposes of chat system message
|
|
Player *pplr = gplrm.GetPlayerFromPid(pid);
|
|
if (pplr != NULL && m_pchatter != NULL) {
|
|
m_pchatter->AddChat(NULL, base::Format::ToString(
|
|
"%s has won this game.", pplr->GetName()), true);
|
|
}
|
|
}
|
|
|
|
void SimUIForm::OnPlayerDisconnectNotify(Pid pid, int nReason)
|
|
{
|
|
// We've been notified by the server of a player disconnecting. Display
|
|
// alert
|
|
|
|
Player *pplr = gplrm.GetPlayerFromPid(pid);
|
|
if (pplr != NULL) {
|
|
pplr->SetFlags(pplr->GetFlags() | kfPlrComputer);
|
|
|
|
// Set observer flag so that it is possible to identify the observing
|
|
// players later. Don't set it for the local player, since this
|
|
// flag is heavily overloaded and used to determine whether to show
|
|
// win/lose summaries and AskObserve form, at which point it will be
|
|
// set there. Clearly the win/lose paths need re-writing rather than
|
|
// patching.
|
|
if (pplr != gpplrLocal) {
|
|
pplr->SetFlags(pplr->GetFlags() | kfPlrObserver);
|
|
}
|
|
|
|
char szT[128];
|
|
char *pszReason = NULL;
|
|
char *pszChat = NULL;
|
|
switch (nReason) {
|
|
case knDisconnectReasonLeftGame:
|
|
if (!pplr->GetLeftGame()) {
|
|
pplr->SetLeftGame();
|
|
pszReason = "left the game";
|
|
pszChat = ".";
|
|
}
|
|
break;
|
|
|
|
case knDisconnectReasonResign:
|
|
pszReason = "resigned";
|
|
pszChat = ", and is now observing.";
|
|
break;
|
|
|
|
case knDisconnectReasonKilled:
|
|
pszReason = "kicked by another player";
|
|
pszChat = ", and left the game.";
|
|
break;
|
|
|
|
case knDisconnectReasonAbnormal:
|
|
pszReason = "dropped out";
|
|
pszChat = ", and left the game.";
|
|
break;
|
|
|
|
case knDisconnectReasonNotResponding:
|
|
pszReason = "disconnected - not responding";
|
|
pszChat = ", and left the game.";
|
|
break;
|
|
}
|
|
|
|
if (pszReason != NULL) {
|
|
sprintf(szT, "%s %s", pplr->GetName(), pszReason);
|
|
ShowAlert(szT);
|
|
}
|
|
if (pszChat != NULL && m_pchatter != NULL) {
|
|
m_pchatter->AddChat(NULL, base::Format::ToString("%s%s", szT,
|
|
pszChat), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SimUIForm::OnLagNotify(Pid pidLagging, int cSeconds)
|
|
{
|
|
// First thing's first, acknowledge this to the server,
|
|
// so the server knows this client is alive.
|
|
|
|
LagAcknowledgeNetMessage lanm;
|
|
gptra->SendNetMessage(&lanm);
|
|
|
|
// pidLagging is kpidNeutral if there are no laggards,
|
|
// which causes GetPlayerFromPid() to return NULL.
|
|
|
|
Player *pplr = gplrm.GetPlayerFromPid(pidLagging);
|
|
if (pplr == NULL) {
|
|
ShowAlert("");
|
|
if (m_pfrmLag != NULL) {
|
|
m_pfrmLag->Show(false);
|
|
delete m_pfrmLag;
|
|
m_pfrmLag = NULL;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We've been notified by the server of this player lagging. Display alert
|
|
|
|
ShowAlert(base::Format::ToString("%s lagging (%ds)", pplr->GetName(),
|
|
cSeconds));
|
|
|
|
// If cSeconds is not 0, hide the form. It only comes up at 0.
|
|
|
|
if (cSeconds != 0) {
|
|
if (m_pfrmLag != NULL) {
|
|
m_pfrmLag->Show(false);
|
|
delete m_pfrmLag;
|
|
m_pfrmLag = NULL;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// cSeconds is 0, then show the lag form. First see if it is for a
|
|
// different player. If so, hide it first.
|
|
|
|
if (m_pfrmLag != NULL) {
|
|
// If same pid is lagging, nothing to do; keep the form up
|
|
|
|
if (m_pidLagging == pidLagging) {
|
|
return;
|
|
}
|
|
|
|
// A different pid is lagging, yet the form is up. Delete the current
|
|
// one first.
|
|
|
|
m_pfrmLag->Show(false);
|
|
delete m_pfrmLag;
|
|
m_pfrmLag = NULL;
|
|
}
|
|
|
|
// Show the lagging form, modeless, so that it can be hidden and reshown
|
|
// at will. Check during update if in EndForm state, and handle
|
|
// appropriately.
|
|
|
|
m_pidLagging = pidLagging;
|
|
m_pfrmLag = CreateHtMessageBox(kidfMessageBoxQuery, kfMbWhiteBorder,
|
|
"LAG ALERT",
|
|
base::Format::ToString(
|
|
"%s is communicating erratically and causing the game to lag. Disconnect %s?",
|
|
pplr->GetName(), pplr->GetName()));
|
|
m_pfrmLag->SetFlags(m_pfrmLag->GetFlags() | kfFrmDoModal);
|
|
gsndm.PlaySfx(ksfxGuiFormShow);
|
|
m_pfrmLag->Show(true);
|
|
}
|
|
|
|
void SimUIForm::CheckLagForm()
|
|
{
|
|
if (m_pfrmLag == NULL || (m_pfrmLag->GetFlags() & kfFrmDoModal) != 0) {
|
|
return;
|
|
}
|
|
|
|
int nResult = m_pfrmLag->GetResult();
|
|
m_pfrmLag->Show(false);
|
|
delete m_pfrmLag;
|
|
m_pfrmLag = NULL;
|
|
|
|
// Send a KillLaggingPlayerNetMessage with the result
|
|
|
|
KillLaggingPlayerNetMessage klpnm;
|
|
klpnm.pid = m_pidLagging;
|
|
klpnm.fYes = (nResult == kidcOk) ? 1 : 0;
|
|
gptra->SendNetMessage(&klpnm);
|
|
}
|
|
|
|
void SimUIForm::Update()
|
|
{
|
|
#ifdef STRESS
|
|
if (gfStress) {
|
|
if (gsim.GetTickCount() > gtStressTimeout) {
|
|
Event evt;
|
|
memset(&evt, 0, sizeof(evt));
|
|
evt.eType = gameOverEvent;
|
|
evt.dw = knGoAbortLevel;
|
|
gevm.PostEvent(&evt);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Advance the move target while it's visible
|
|
|
|
if (m_nStateMoveTarget == 1) {
|
|
m_aniMoveTarget.Advance(1);
|
|
if (m_aniMoveTarget.GetFlags() & kfAniDone)
|
|
m_nStateMoveTarget = 2;
|
|
}
|
|
|
|
if (m_pfrmWaitingForAllPlayers != NULL) {
|
|
delete m_pfrmWaitingForAllPlayers;
|
|
m_pfrmWaitingForAllPlayers = NULL;
|
|
}
|
|
|
|
// Have the InputUI update its displays (e.g., credits, power)
|
|
|
|
InputUIForm *pfrmInputUI = ggame.GetInputUIForm();
|
|
pfrmInputUI->Update();
|
|
|
|
// Update the objective display
|
|
|
|
LabelControl *plbl = (LabelControl *)GetControlPtr(kidcObjective);
|
|
plbl->SetText(gpplrLocal->GetObjective());
|
|
|
|
// Calc stats
|
|
|
|
long tCurrent = HostGetTickCount();
|
|
if (s_tLastFPSUpdate == 0)
|
|
s_tLastFPSUpdate = tCurrent;
|
|
if (tCurrent - s_tLastFPSUpdate >= 100) {
|
|
fix32 fx1 = itofx32(gcPaint * 100);
|
|
fix32 fx2 = itofx32(tCurrent - s_tLastFPSUpdate);
|
|
if (fx2 == 0)
|
|
s_cfxFPS = 0;
|
|
else
|
|
s_cfxFPS = (fix)divfx32(fx1, fx2);
|
|
int cFpsWhole = fx32toi(s_cfxFPS);
|
|
int cFpsFraction = (int)fxtoi(mulfx(fracfx(s_cfxFPS), itofx(10)));
|
|
|
|
if (gfShowFPS) {
|
|
char szFPS[80];
|
|
sprintf(szFPS, "%d.%d", cFpsWhole, cFpsFraction);
|
|
((LabelControl *)GetControlPtr(kidcFps))->SetText(szFPS);
|
|
}
|
|
|
|
s_tLastFPSUpdate = tCurrent;
|
|
gcPaint = 0;
|
|
|
|
// Set the # of pen events to accept per second. We don't need anything faster
|
|
// than the frame rate.
|
|
|
|
static long s_tLastPaint;
|
|
if (s_tLastPaint == 0)
|
|
s_tLastPaint = tCurrent;
|
|
gevm.SetPenEventInterval((word)(tCurrent - s_tLastPaint));
|
|
s_tLastPaint = tCurrent;
|
|
}
|
|
|
|
// Let the minimap decide if it should invalidate.
|
|
|
|
gpmm->Update();
|
|
|
|
// Everything has been updated for (at least) the first time
|
|
// so we're ready to display it.
|
|
|
|
if (!s_fReadyForPaint) {
|
|
s_fReadyForPaint = true;
|
|
InvalidateRect(NULL);
|
|
}
|
|
|
|
// Set redraw
|
|
|
|
gevm.SetRedrawFlags(kfRedrawDirty);
|
|
}
|
|
|
|
void AddPointToLassoSelection(WPoint wpt)
|
|
{
|
|
// TUNE:
|
|
if (abs(s_awptSelection[s_cwptSelection - 1].wx - wpt.wx) >= WcFromTile16ths(4) ||
|
|
abs(s_awptSelection[s_cwptSelection - 1].wy - wpt.wy) >= WcFromTile16ths(4)) {
|
|
s_awptSelection[s_cwptSelection] = wpt;
|
|
if (s_cwptSelection < (sizeof(s_awptSelection) / sizeof(WPoint)) - 1)
|
|
s_cwptSelection++;
|
|
}
|
|
}
|
|
|
|
void SimUIForm::OnUpdateMapInvalidate(UpdateMap *pupd, Rect *prcOpaque)
|
|
{
|
|
#ifdef DRAW_LINES
|
|
if (gfDrawLines)
|
|
pupd->InvalidateRect();
|
|
#endif
|
|
|
|
#ifdef DRAW_PATHS
|
|
if (gfDrawPaths)
|
|
pupd->InvalidateRect();
|
|
#endif
|
|
|
|
#if defined(DRAW_OCCUPIED_TILE_INDICATOR) || defined(MARK_TILE_BOUNDARIES) || defined(MARK_OCCUPIED_TILES)
|
|
pupd->InvalidateRect();
|
|
#endif
|
|
|
|
// Invalidate move target
|
|
|
|
if (m_nStateMoveTarget != 0) {
|
|
pupd->InvalidateTile(TcFromWc(m_wxMoveTarget), TcFromWc(m_wyMoveTarget));
|
|
if (m_nStateMoveTarget == 2)
|
|
m_nStateMoveTarget = 0;
|
|
}
|
|
|
|
// Handle selection invalidation
|
|
|
|
InvalidateDragSelection();
|
|
|
|
// Calculate the current opaquing rect for the map.
|
|
// HACK: Store in a global since various places need it, such as when a gob gets removed or when
|
|
// galaxite gets invalidated.
|
|
|
|
Rect rcT;
|
|
bool fEmpty = prcOpaque->IsEmpty();
|
|
if (fEmpty) {
|
|
// Probably not worth it
|
|
#if 0
|
|
// Use the minimap since it covers the map
|
|
|
|
Control *pctl = GetControlPtr(kidcMiniMap);
|
|
if (pctl->GetFlags() & kfCtlVisible) {
|
|
pctl->GetRect(&rcT);
|
|
fEmpty = false;
|
|
}
|
|
#endif
|
|
} else {
|
|
// Use the opaque rect passed
|
|
|
|
rcT = *prcOpaque;
|
|
}
|
|
|
|
// Map it map relative in tile coords
|
|
|
|
if (!fEmpty) {
|
|
WCoord wxView, wyView;
|
|
gsim.GetViewPos(&wxView, &wyView);
|
|
Rect rcMapRelative;
|
|
rcMapRelative = rcT;
|
|
rcMapRelative.Offset(PcFromWc(wxView), PcFromWc(wyView));
|
|
gtrcMapOpaque.left = TcFromPc(rcMapRelative.left + gcxTile - 1);
|
|
gtrcMapOpaque.top = TcFromPc(rcMapRelative.top + gcyTile - 1);
|
|
gtrcMapOpaque.right = TcFromPc(rcMapRelative.right);
|
|
gtrcMapOpaque.bottom = TcFromPc(rcMapRelative.bottom);
|
|
|
|
// Expand the tile rect if the opaque rect is on the screen
|
|
// edge. This way gobs will get opaqued when on screen edge even
|
|
|
|
Size sizDib;
|
|
m_pfrmm->GetDib()->GetSize(&sizDib);
|
|
if (rcT.left <= 0)
|
|
gtrcMapOpaque.left = 0;
|
|
if (rcT.top <= 0)
|
|
gtrcMapOpaque.top = 0;
|
|
if (rcT.right >= sizDib.cx)
|
|
gtrcMapOpaque.right = ktcMax;
|
|
if (rcT.bottom >= sizDib.cy)
|
|
gtrcMapOpaque.bottom = ktcMax;
|
|
|
|
// Turn this opaque rect on by setting this global
|
|
|
|
gptrcMapOpaque = >rcMapOpaque;
|
|
} else {
|
|
gptrcMapOpaque = NULL;
|
|
}
|
|
|
|
// Get visible gobs
|
|
|
|
Gob **ppgobVisible;
|
|
int cgobVisible;
|
|
gsim.FindVisibleGobs(&ppgobVisible, &cgobVisible);
|
|
|
|
// Add known invalid gobs to the update map
|
|
|
|
Gob **ppgobStop = &ppgobVisible[cgobVisible];
|
|
Gob **ppgobT;
|
|
for (ppgobT = ppgobVisible; ppgobT < ppgobStop; ppgobT++) {
|
|
Gob *pgob = *ppgobT;
|
|
|
|
// Invalidate if asked to redraw
|
|
|
|
if (pgob->GetFlags() & kfGobRedraw)
|
|
pgob->Invalidate();
|
|
}
|
|
|
|
// Mark gobs not already marked for redraw that are touching invalid areas
|
|
// Add this "overflow" to the damage map; this is what is used to create
|
|
// a valid back buffer when scrolling
|
|
|
|
bool fNewInvalid = false;
|
|
do {
|
|
fNewInvalid = false;
|
|
for (ppgobT = ppgobVisible; ppgobT < ppgobStop; ppgobT++) {
|
|
Gob *pgob = *ppgobT;
|
|
dword ff = pgob->GetFlags();
|
|
if (!(ff & kfGobRedraw)) {
|
|
if (pupd->IsMapTileRectInvalidAndTrackDamage(&pgob->m_trcBoundingLast, &fNewInvalid))
|
|
pgob->SetFlags(ff | kfGobRedraw);
|
|
}
|
|
}
|
|
} while (fNewInvalid);
|
|
|
|
// Let the base invalidate controls as necessary
|
|
|
|
Form::OnUpdateMapInvalidate(pupd, prcOpaque);
|
|
}
|
|
|
|
void SimUIForm::InvalidateDragSelection()
|
|
{
|
|
if (gfDragSelecting) {
|
|
WCoord wxView, wyView;
|
|
gsim.GetViewPos(&wxView, &wyView);
|
|
|
|
if (gfLassoSelection) {
|
|
// The slow redraw this causes is unavoidable unless we remove lasso as an option.
|
|
|
|
WPoint wptLast;
|
|
wptLast.wx = s_awptSelection[0].wx - wxView;
|
|
wptLast.wy = s_awptSelection[0].wy - wyView;
|
|
for (int i = 1; i < s_cwptSelection; i++) {
|
|
WPoint wptNext;
|
|
wptNext.wx = s_awptSelection[i].wx - wxView;
|
|
wptNext.wy = s_awptSelection[i].wy - wyView;
|
|
int x1 = PcFromWc(wptLast.wx);
|
|
int y1 = PcFromWc(wptLast.wy);
|
|
int x2 = PcFromWc(wptNext.wx);
|
|
int y2 = PcFromWc(wptNext.wy);
|
|
Rect rcT;
|
|
rcT.left = _min(x1, x2);
|
|
rcT.top = _min(y1, y2);
|
|
rcT.right = rcT.left + abs(x2 - x1) + 1;
|
|
rcT.bottom = rcT.top + abs(y2 - y1) + 1;
|
|
gpupdSim->InvalidateRect(&rcT);
|
|
wptLast = wptNext;
|
|
}
|
|
} else {
|
|
// PERF: there is a way to make selection drawing faster by only invalidating edges
|
|
// that move, and redrawing dirty cells only on the control layer. Can do if this isn't fast enough.
|
|
|
|
int xLeft = PcFromWc(gwrcSelection.left - wxView);
|
|
int yTop = PcFromWc(gwrcSelection.top - wyView);
|
|
int xRight = PcFromWc(gwrcSelection.right - wxView);
|
|
int yBottom = PcFromWc(gwrcSelection.bottom - wyView);
|
|
|
|
Rect rcT;
|
|
rcT.Set(xLeft, yTop, xRight, yTop + 1);
|
|
gpupdSim->InvalidateRect(&rcT);
|
|
rcT.Set(xRight - 1, yTop, xRight, yBottom);
|
|
gpupdSim->InvalidateRect(&rcT);
|
|
rcT.Set(xLeft, yBottom - 1, xRight, yBottom);
|
|
gpupdSim->InvalidateRect(&rcT);
|
|
rcT.Set(xLeft, yTop, xLeft + 1, yBottom);
|
|
gpupdSim->InvalidateRect(&rcT);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SimUIForm::ScrollInvalidate(UpdateMap *pupd)
|
|
{
|
|
Form::ScrollInvalidate(pupd);
|
|
InvalidateDragSelection();
|
|
gpmm->Invalidate();
|
|
}
|
|
|
|
void SimUIForm::OnPaintBackground(DibBitmap *pbm, UpdateMap *pupd)
|
|
{
|
|
if (!s_fReadyForPaint)
|
|
return;
|
|
|
|
gsim.DrawBackground(pupd, pbm);
|
|
}
|
|
|
|
void SimUIForm::OnPaint(DibBitmap *pbm)
|
|
{
|
|
// We're not ready to paint until the first Update has executed because
|
|
// Update initializes things like the Credits and Power controls.
|
|
|
|
if (!s_fReadyForPaint)
|
|
return;
|
|
|
|
// Draw everything managed by the Simulation (map, Gobs, Gob UI)
|
|
|
|
gsim.Draw(m_pfrmm->GetUpdateMap(), pbm);
|
|
|
|
// Draw the selection rectangle or lasso lines, if any
|
|
|
|
if (gfDragSelecting) {
|
|
WCoord wxView, wyView;
|
|
gsim.GetViewPos(&wxView, &wyView);
|
|
|
|
if (gfLassoSelection) {
|
|
Color clr = GetColor(kiclrWhite);
|
|
WPoint wptLast;
|
|
wptLast.wx = s_awptSelection[0].wx - wxView;
|
|
wptLast.wy = s_awptSelection[0].wy - wyView;
|
|
for (int i = 1; i < s_cwptSelection; i++) {
|
|
WPoint wptNext;
|
|
wptNext.wx = s_awptSelection[i].wx - wxView;
|
|
wptNext.wy = s_awptSelection[i].wy - wyView;
|
|
pbm->DrawLine(PcFromWc(wptLast.wx), PcFromWc(wptLast.wy), PcFromWc(wptNext.wx), PcFromWc(wptNext.wy), clr);
|
|
wptLast = wptNext;
|
|
}
|
|
} else {
|
|
Rect rcT;
|
|
rcT.left = PcFromWc(gwrcSelection.left - wxView);
|
|
rcT.top = PcFromWc(gwrcSelection.top - wyView);
|
|
rcT.right = PcFromWc(gwrcSelection.right - wxView);
|
|
rcT.bottom = PcFromWc(gwrcSelection.bottom - wyView);
|
|
DrawBorder(pbm, &rcT, 1, GetColor(kiclrWhite));
|
|
}
|
|
}
|
|
|
|
// Handle placement form
|
|
|
|
gpfrmPlace->OnPaintSimUI(pbm);
|
|
|
|
// Let the input handler draw what it needs
|
|
|
|
m_ppenh->OnPaint(pbm);
|
|
|
|
// Call the default Form handling
|
|
|
|
Form::OnPaint(pbm);
|
|
}
|
|
|
|
void SimUIForm::OnPaintControls(DibBitmap *pbm, UpdateMap *pupd)
|
|
{
|
|
// We're not ready to paint until the first Update has executed because
|
|
// Update initializes things like the Credits and Power controls.
|
|
|
|
if (!s_fReadyForPaint)
|
|
return;
|
|
|
|
// Give some time to sound servicing
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Draw fog
|
|
|
|
gsim.DrawFog(pupd, pbm);
|
|
|
|
// Draw move indicator
|
|
|
|
if (m_nStateMoveTarget == 1) {
|
|
WCoord wxView, wyView;
|
|
gsim.GetViewPos(&wxView, &wyView);
|
|
m_aniMoveTarget.Draw(pbm, PcFromUwc(m_wxMoveTarget) - (PcFromWc(wxView) & 0xfffe), PcFromUwc(m_wyMoveTarget) - (PcFromWc(wyView) & 0xfffe));
|
|
}
|
|
|
|
// Give some time to sound servicing
|
|
|
|
HostSoundServiceProc();
|
|
|
|
// Draw controls
|
|
|
|
Form::OnPaintControls(pbm, pupd);
|
|
|
|
// Handle Structure Placement controls here
|
|
|
|
gpfrmPlace->OnPaintControlsSimUI(pbm, pupd);
|
|
|
|
// Draw stats
|
|
|
|
#ifdef STATS_DISPLAY
|
|
if (gfShowStats) {
|
|
Font *pfnt = gapfnt[kifntDefault];
|
|
int cy = pfnt->GetHeight();
|
|
int y = cy;
|
|
char szT[80];
|
|
|
|
// Display useful info
|
|
|
|
// Count of active Gobs
|
|
|
|
int cpgob = 0;
|
|
for (Gob *pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT), cpgob++);
|
|
sprintf(szT, "active Gobs: %d", cpgob);
|
|
pfnt->DrawText(pbm, szT, 1, y);
|
|
y += cy;
|
|
|
|
// Count of visible sprites
|
|
|
|
sprintf(szT, "bitmaps drawn: %d", gcBitmapsDrawn);
|
|
pfnt->DrawText(pbm, szT, 1, y);
|
|
y += cy;
|
|
|
|
// Count of messages per update
|
|
|
|
sprintf(szT, "msgs/update: %d", gcMessagesPerUpdate);
|
|
pfnt->DrawText(pbm, szT, 1, y);
|
|
y += cy;
|
|
|
|
// Count of updates
|
|
|
|
sprintf(szT, "update count: %ld", gsim.GetUpdateCount());
|
|
pfnt->DrawText(pbm, szT, 1, y);
|
|
y += cy;
|
|
|
|
// Invalidate
|
|
|
|
Rect rcT = m_rc;
|
|
rcT.top = cy;
|
|
rcT.bottom = y;
|
|
InvalidateRect(&rcT);
|
|
}
|
|
#endif
|
|
|
|
#ifdef STATS_DISPLAY
|
|
s_cUpdateRects += gpmfrmm->GetUpdateRectCount();
|
|
#endif
|
|
}
|
|
|
|
void SimUIForm::FrameComplete()
|
|
{
|
|
Form::FrameComplete();
|
|
}
|
|
|
|
void SimUIForm::OnControlSelected(word idc) {
|
|
switch (idc) {
|
|
case kidcChat:
|
|
if (m_pchatter != NULL) {
|
|
m_pchatter->ShowChat();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// SimUI handles input events which may effect the whole playfield and/or
|
|
// multiple Gobs. It does as much as it can independent of the Gobs but
|
|
// passes context-specific messages to the Gobs when specialized responses
|
|
// are desired.
|
|
//
|
|
// SimUI's OnPenEvent handler is responsible for
|
|
//
|
|
// - individual selection/multiple selection
|
|
// - input routing, i.e., cooking and passing pertinent events to the
|
|
// appropriate Gobs. For example, passing selected Gobs SetTarget and
|
|
// invoking Gob::PopupMenu.
|
|
//
|
|
|
|
bool SimUIForm::OnPenEvent(Event *pevt)
|
|
{
|
|
// Give the form controls first crack at handling the event. Form
|
|
// controls include: soft menu button, mode cancel button, status
|
|
// label)
|
|
|
|
if (Form::OnPenEvent(pevt)) {
|
|
return true;
|
|
}
|
|
|
|
// Debug gob identification
|
|
|
|
if (pevt->eType == penHoverEvent) {
|
|
#if defined(WIN) && defined(DEBUG)
|
|
WCoord wxTarget, wyTarget;
|
|
gsim.GetViewPos(&wxTarget, &wyTarget);
|
|
TCoord tx = TcFromWc(wxTarget + WcFromPc(pevt->x));
|
|
TCoord ty = TcFromWc(wyTarget + WcFromPc(pevt->y));
|
|
for (Gid gid = ggobm.GetFirstGid(tx, ty); gid != kgidNull;
|
|
gid = ggobm.GetNextGid(gid)) {
|
|
Gob *pgob = ggobm.GetGob(gid);
|
|
if (pgob != NULL) {
|
|
char sz[512];
|
|
pgob->ToString(sz);
|
|
Trace(sz);
|
|
}
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
bool fScrollOnly = false;
|
|
if (gpfrmPlace != NULL && gpfrmPlace->IsBusy()) {
|
|
fScrollOnly = true;
|
|
}
|
|
if (gpplrLocal->GetFlags() & kfPlrObserver) {
|
|
fScrollOnly = true;
|
|
}
|
|
|
|
return m_ppenh->OnPenEvent(pevt, fScrollOnly);
|
|
}
|
|
|
|
/*
|
|
var ar = new Array(64)
|
|
for (r = 0; r < 64; r++) {
|
|
c = 0;
|
|
for (y = 50; y >= -50; y--) {
|
|
for (x = 50; x >= -50; x--) {
|
|
d = Math.sqrt(x * x + y * y);
|
|
if (d <= r)
|
|
c++;
|
|
}
|
|
}
|
|
|
|
ar[r] = c;
|
|
}
|
|
|
|
for (r = 0; r < 64; r++) {
|
|
WScript.Echo("Range " + r + " fits " + ar[r]);
|
|
}
|
|
|
|
var str = "";
|
|
n = 0;
|
|
for (r = 0; r < 64; r++) {
|
|
while (n <= ar[r]) {
|
|
if (n % 16 == 0) {
|
|
WScript.Echo(str);
|
|
str = "";
|
|
}
|
|
str += r + ", ";
|
|
n++;
|
|
}
|
|
}
|
|
*/
|
|
|
|
// Map unit count to target range, i.e. what circle radius will fit a given count of units
|
|
|
|
byte gmpcUnitsToRadius[] =
|
|
{
|
|
0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3,
|
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4,
|
|
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
|
4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
|
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
|
5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
|
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
|
6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|
7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|
8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
|
|
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
|
|
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
|
|
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10,
|
|
};
|
|
|
|
int RadiusFromUnitCount(int cUnits)
|
|
{
|
|
TCoord tcRadius;
|
|
if (cUnits >= ARRAYSIZE(gmpcUnitsToRadius)) {
|
|
tcRadius = gmpcUnitsToRadius[ARRAYSIZE(gmpcUnitsToRadius) - 1];
|
|
} else {
|
|
tcRadius = gmpcUnitsToRadius[cUnits];
|
|
}
|
|
return tcRadius;
|
|
}
|
|
|
|
MobileUnitGob *SimUIForm::SetSelectionTargets(Gid gid, WCoord wxTarget, WCoord wyTarget)
|
|
{
|
|
// Find a target that is free and close
|
|
|
|
WPoint wptT;
|
|
FindNearestFreeTile(TcFromWc(wxTarget), TcFromWc(wyTarget), &wptT, 0);
|
|
wxTarget = wptT.wx;
|
|
wyTarget = wptT.wy;
|
|
|
|
// Find targets for all MobileUnits. Targets are a "closely packed" version
|
|
// of the current selection, centered around the midpoint of the selection,
|
|
// placed at the target.
|
|
|
|
// First figure out which units are mobile and attempt to retain their formation
|
|
|
|
WCoord wxMin = kwcMax;
|
|
WCoord wxMax = 0;
|
|
WCoord wyMin = kwcMax;
|
|
WCoord wyMax = 0;
|
|
|
|
WCoord wcMoveDistPerUpdate = (WCoord)kwcMax;
|
|
int cgobsSel = 0;
|
|
Gob *pgobT;
|
|
for (pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
if ((pgobT->GetFlags() & (kfGobSelected | kfGobMobileUnit)) == (kfGobSelected | kfGobMobileUnit) &&
|
|
((gfGodMode && !ggame.IsMultiplayer()) || pgobT->GetSide() == gpplrLocal->GetSide())) {
|
|
|
|
// Get selection dimensions
|
|
|
|
WPoint wpt;
|
|
pgobT->GetCenter(&wpt);
|
|
if (wpt.wx < wxMin)
|
|
wxMin = wpt.wx;
|
|
if (wpt.wx > wxMax)
|
|
wxMax = wpt.wx;
|
|
if (wpt.wy < wyMin)
|
|
wyMin = wpt.wy;
|
|
if (wpt.wy > wyMax)
|
|
wyMax = wpt.wy;
|
|
cgobsSel++;
|
|
|
|
// Get min speed per update
|
|
|
|
if (pgobT->GetFlags() & kfGobMobileUnit) {
|
|
MobileUnitGob *pmunt = (MobileUnitGob *)pgobT;
|
|
MobileUnitConsts *pmuntc = (MobileUnitConsts *)pmunt->GetConsts();
|
|
if (pmuntc->GetMoveDistPerUpdate() < wcMoveDistPerUpdate)
|
|
wcMoveDistPerUpdate = pmuntc->GetMoveDistPerUpdate();
|
|
}
|
|
}
|
|
}
|
|
WCoord cwxSel = wxMax - wxMin;
|
|
if (cwxSel <= 0)
|
|
cwxSel = 1;
|
|
WCoord cwySel = wyMax - wyMin;
|
|
if (cwySel <= 0)
|
|
cwySel = 1;
|
|
|
|
// The dest scaling rect is different if moving vs. attacking.
|
|
|
|
int ctxSel = TcFromWc(wxMax) - TcFromWc(wxMin) + 1;
|
|
int ctySel = TcFromWc(wyMax) - TcFromWc(wyMin) + 1;
|
|
int ctxTarg;
|
|
int ctyTarg;
|
|
if (gid == kgidNull) {
|
|
// Pick a destination scaling rect for movement
|
|
|
|
ctxTarg = ctxSel;
|
|
ctyTarg = ctySel;
|
|
|
|
// Pack the rect if it is <= 50% full
|
|
|
|
if (cgobsSel <= (ctxSel * ctySel + 1) / 2) {
|
|
int ctxT = ctxSel;
|
|
int ctyT = ctySel;
|
|
while (true) {
|
|
// Remember last
|
|
|
|
ctxTarg = ctxT;
|
|
ctyTarg = ctyT;
|
|
|
|
// Try smaller size, decrease whichever size is greater
|
|
|
|
if (ctxT > ctyT) {
|
|
ctxT--;
|
|
} else {
|
|
ctyT--;
|
|
}
|
|
|
|
if (ctxT * ctyT < cgobsSel + cgobsSel / 3)
|
|
break;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// Pick a destination scaling rect for attacking
|
|
|
|
Gob *pgob = ggobm.GetGob(gid);
|
|
ctxTarg = 1;
|
|
ctyTarg = 1;
|
|
if (pgob != NULL) {
|
|
TRect trc;
|
|
pgob->GetTileRect(&trc);
|
|
ctxTarg = trc.right - trc.left;
|
|
ctyTarg = trc.bottom - trc.top;
|
|
}
|
|
|
|
// Make the outside edge away from the gob; this hardwires some accounting
|
|
// for some firing distance, gives a larger edge for gobs to move to
|
|
|
|
ctxTarg += 2;
|
|
ctyTarg += 2;
|
|
}
|
|
|
|
WCoord cwxTarg = WcFromTc(ctxTarg - 1);
|
|
if (cwxTarg == 0)
|
|
cwxTarg = 1;
|
|
WCoord cwyTarg = WcFromTc(ctyTarg - 1);
|
|
if (cwyTarg == 0)
|
|
cwyTarg = 1;
|
|
|
|
// Figure out the range of this target
|
|
|
|
TCoord tcTargetRadius = RadiusFromUnitCount(cgobsSel);
|
|
|
|
// Find src center
|
|
|
|
WCoord wxCenterSrc = wxMin + (wxMax - wxMin) / 2;
|
|
WCoord wyCenterSrc = wyMin + (wyMax - wyMin) / 2;
|
|
|
|
// Find map size
|
|
|
|
Size sizMapTiles;
|
|
gsim.GetLevel()->GetTileMap()->GetTCoordMapSize(&sizMapTiles);
|
|
WCoord cwxMap = WcFromTc(sizMapTiles.cx);
|
|
WCoord cwyMap = WcFromTc(sizMapTiles.cy);
|
|
|
|
// Move & keep formation, or target destination if not a mobile unit
|
|
|
|
Gob *pgobTarget = ggobm.GetGob(gid);
|
|
|
|
MobileUnitGob *pmuntSetTarget = NULL;
|
|
for (pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
dword ff = pgobT->GetFlags();
|
|
if ((ff & kfGobSelected) &&
|
|
((gfGodMode && !ggame.IsMultiplayer()) || pgobT->GetSide() == gpplrLocal->GetSide() || (pgobTarget != NULL && pgobTarget->GetType() == kgtReplicator))) {
|
|
|
|
// If it's a mobile gob, give it a move command to a scaled coordinate
|
|
|
|
if (ff & kfGobMobileUnit) {
|
|
// Calc dst
|
|
|
|
MobileUnitGob *pmunt = (MobileUnitGob *)pgobT;
|
|
WPoint wptSrc;
|
|
pmunt->GetCenter(&wptSrc);
|
|
WCoord wxDst;
|
|
if (wptSrc.wx - wxCenterSrc >= 0) {
|
|
wxDst = (WCoord)(((long)(wptSrc.wx - wxCenterSrc) * cwxTarg * 8 + (cwxSel * 4)) / (cwxSel * 8) + wxTarget);
|
|
} else {
|
|
wxDst = (WCoord)(((long)(wptSrc.wx - wxCenterSrc) * cwxTarg * 8 - (cwxSel * 4)) / (cwxSel * 8) + wxTarget);
|
|
}
|
|
if (wxDst < 0)
|
|
wxDst = 0;
|
|
if (wxDst >= cwxMap)
|
|
wxDst = cwxMap - 1;
|
|
WCoord wyDst;
|
|
if (wptSrc.wy - wyCenterSrc >= 0) {
|
|
wyDst = (WCoord)(((long)(wptSrc.wy - wyCenterSrc) * cwyTarg * 8 + (cwySel * 4)) / (cwySel * 8) + wyTarget);
|
|
} else {
|
|
wyDst = (WCoord)(((long)(wptSrc.wy - wyCenterSrc) * cwyTarg * 8 - (cwySel * 4)) / (cwySel * 8) + wyTarget);
|
|
}
|
|
if (wyDst < 0)
|
|
wyDst = 0;
|
|
if (wyDst >= cwyMap)
|
|
wyDst = cwyMap - 1;
|
|
if (gfGodMode && !ggame.IsMultiplayer())
|
|
pmunt->ClearAction();
|
|
|
|
// Need to clean up the targets.
|
|
// 1. Targets can be either in open terrain or on the unit being targetted,
|
|
// not on other structures (other munts is ok)
|
|
// 2. For any targets on the munt being targetted, if it's a structure it needs
|
|
// to be checked for accessibility
|
|
// 3. Adjust the target radius to be on the same side of any blockage
|
|
|
|
// Clean up dest if attacking
|
|
|
|
if (pgobTarget != NULL) {
|
|
// Find the closest tile to this that is unblocked by terrain
|
|
// Ok if on a structure... which structure is important, that is checked next
|
|
|
|
WPoint wptT;
|
|
FindNearestFreeTile(TcFromWc(wxDst), TcFromWc(wyDst), &wptT, 0);
|
|
wxDst = wptT.wx;
|
|
wyDst = wptT.wy;
|
|
|
|
// Calc these now as wxDst, wyDst may have changed
|
|
|
|
TCoord txT = TcFromWc(wxDst);
|
|
TCoord tyT = TcFromWc(wyDst);
|
|
|
|
// If dest is on top of a structure make sure that is the target and dest
|
|
// is accessible
|
|
|
|
Assert(pgobTarget->GetFlags() & kfGobUnit);
|
|
UnitGob *puntTarget = (UnitGob *)pgobTarget;
|
|
Gob *pgobT = ggobm.GetShadowGob(txT, tyT);
|
|
if (pgobT != NULL) {
|
|
// The destination is on top of a structure. Need to
|
|
// check if this is ok If it isn't ok, get a new point
|
|
// that is accessible. If it is ok, make sure the
|
|
// position is accessible.
|
|
|
|
Assert(pgobT->GetFlags() & kfGobStructure);
|
|
StructGob *pstruT = (StructGob *)pgobT;
|
|
if (pgobT != pgobTarget) {
|
|
// Targetting something but wxDst, wyDst on a
|
|
// structure we're not targetting. Get a valid
|
|
// attack point from the real target
|
|
|
|
WPoint wptT;
|
|
puntTarget->GetAttackPoint(&wptT);
|
|
wxDst = wptT.wx;
|
|
wyDst = wptT.wy;
|
|
} else {
|
|
// wxDst, wyDst is on top of the structure we are
|
|
// targetting which is fine. Get an accessible
|
|
// position if the one we have isn't accessible
|
|
|
|
if (!pstruT->IsAccessible(txT, tyT)) {
|
|
WPoint wptT;
|
|
puntTarget->GetAttackPoint(&wptT);
|
|
wxDst = wptT.wx;
|
|
wyDst = wptT.wy;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Not attacking, just moving.
|
|
// Find a clear terrain spot that isn't terrain blocked and
|
|
// not on a structure
|
|
|
|
WPoint wptT;
|
|
FindNearestFreeTile(TcFromWc(wxDst), TcFromWc(wyDst), &wptT, kbfStructure);
|
|
wxDst = wptT.wx;
|
|
wyDst = wptT.wy;
|
|
}
|
|
|
|
// Adjust the target radius so we end up on the same side of
|
|
// any blockage
|
|
|
|
TCoord tcTargetRadiusT = tcTargetRadius;
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
TCoord txTo = TcFromWc(wxTarget);
|
|
TCoord tyTo = TcFromWc(wyTarget);
|
|
TCoord txFrom = TcFromWc(wxDst);
|
|
TCoord tyFrom = TcFromWc(wyDst);
|
|
TCoord txFree, tyFree;
|
|
if (ptrmap->FindLastUnoccupied(txTo, tyTo, txFrom, tyFrom, &txFree, &tyFree)) {
|
|
if (txFree != txFrom || tyFree != tyFrom) {
|
|
wxDst = WcFromTc(txFree) + kwcTileHalf;
|
|
wyDst = WcFromTc(tyFree) + kwcTileHalf;
|
|
int dtx = abs(txFree - txTo);
|
|
if (dtx < ARRAYSIZE(gmpDistFromDxy)) {
|
|
int dty = abs(tyFree - tyTo);
|
|
if (dty < ARRAYSIZE(gmpDistFromDxy)) {
|
|
tcTargetRadiusT = gmpDistFromDxy[dtx][dty];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send command
|
|
|
|
pmunt->SetTarget(gid, wxDst, wyDst, wxTarget, wyTarget, tcTargetRadiusT, wcMoveDistPerUpdate);
|
|
pmuntSetTarget = pmunt;
|
|
}
|
|
|
|
// Else give it a set target command with the original target
|
|
// coordinate
|
|
|
|
else if (ff & kfGobUnit) {
|
|
UnitGob *punt = (UnitGob *)pgobT;
|
|
punt->SetTarget(gid, wxTarget, wyTarget, 0, wcMoveDistPerUpdate);
|
|
}
|
|
}
|
|
}
|
|
|
|
return pmuntSetTarget;
|
|
}
|
|
|
|
void SimUIForm::ClearSelection()
|
|
{
|
|
s_wptSelect1.wx = 0;
|
|
s_wptSelect1.wy = 0;
|
|
s_wptSelect2 = s_wptSelect1;
|
|
s_cwptSelection = 0;
|
|
gwrcSelection.Set(s_wptSelect1, s_wptSelect2);
|
|
gsim.ClearGobSelection();
|
|
}
|
|
|
|
Gob *SimUIForm::HitTestGob(int x, int y, bool fFinger, WCoord *pwx,
|
|
WCoord *pwy, bool *pfHitSurrounding)
|
|
{
|
|
WCoord wxTarget, wyTarget;
|
|
gsim.GetViewPos(&wxTarget, &wyTarget);
|
|
wxTarget += WcFromPc(x);
|
|
wyTarget += WcFromPc(y);
|
|
|
|
// Pin to edges. Gobs created off the map will cause corruption
|
|
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
if (TcFromWc(wxTarget) >= ctx)
|
|
wxTarget = WcFromTc(ctx - 1);
|
|
if (TcFromWc(wyTarget) >= cty)
|
|
wyTarget = WcFromTc(cty - 1);
|
|
|
|
*pwx = wxTarget;
|
|
*pwy = wyTarget;
|
|
|
|
if (fFinger) {
|
|
return gsim.FingerHitTest(wxTarget, wyTarget, kfGobActive | kfGobUnit,
|
|
pfHitSurrounding);
|
|
} else {
|
|
Gob *pgobHit = NULL;
|
|
Enum enm;
|
|
gsim.HitTest(&enm, wxTarget, wyTarget, kfGobActive | kfGobUnit,
|
|
&pgobHit);
|
|
*pfHitSurrounding = false;
|
|
return pgobHit;
|
|
}
|
|
}
|
|
|
|
void SimUIForm::MoveOrAttackOrSelect(int x, int y, dword ff)
|
|
{
|
|
WCoord wxTarget, wyTarget;
|
|
bool fHitSurrounding;
|
|
Gob *pgobHit = HitTestGob(x, y, false, &wxTarget, &wyTarget,
|
|
&fHitSurrounding);
|
|
MoveOrAttackOrSelect(pgobHit, wxTarget, wyTarget, ff);
|
|
}
|
|
|
|
// This routine is called in two situations.
|
|
// 1. On pen down to select the unit tapped on (fSelectOnly == true)
|
|
// 2. On pen up to attack the unit tapped on or to move to the location tapped on (fSelectOnly == false)
|
|
|
|
const int kctDoubleTapDelay = 100; // TUNE:
|
|
|
|
void SimUIForm::MoveOrAttackOrSelect(Gob *pgobHit, WCoord wxTarget,
|
|
WCoord wyTarget, dword ff)
|
|
{
|
|
// TODO: break up this giant routine
|
|
|
|
if (pgobHit == NULL) {
|
|
if (ff & kfMasMove) {
|
|
MobileUnitGob *pmunt = SetSelectionTargets(kgidNull, wxTarget, wyTarget);
|
|
if (pmunt != NULL) {
|
|
// BUGBUG: fix this multiplayer problem (gob lists out of sync)
|
|
#if 0
|
|
if (!ggame.IsMultiplayer()) {
|
|
WCoord wxT = WcTrunc(wxTarget) + kwcTileHalf;
|
|
WCoord wyT = WcTrunc(wyTarget) + kwcTileHalf;
|
|
CreateAnimGob(wxT, wyT, kfAnmDeleteWhenDone | kfAnmSmokeFireLayer, NULL, g_panidMoveTarget);
|
|
}
|
|
#else
|
|
// Single and multi-player friendly move target
|
|
|
|
if (m_nStateMoveTarget != 0)
|
|
m_pfrmm->GetUpdateMap()->InvalidateTile(TcFromWc(m_wxMoveTarget), TcFromWc(m_wyMoveTarget));
|
|
m_aniMoveTarget.Start(0, 0, 0);
|
|
m_wxMoveTarget = WcTrunc(wxTarget) + kwcTileHalf;
|
|
m_wyMoveTarget = WcTrunc(wyTarget) + kwcTileHalf;
|
|
m_nStateMoveTarget = 1;
|
|
#endif
|
|
gsndm.PlaySfx(SfxFromCategory(((MobileUnitConsts *)gapuntc[pmunt->GetUnitType()])->sfxcMove));
|
|
}
|
|
|
|
#ifdef RALLY_POINTS
|
|
// TOTAL HACK:
|
|
// If the player has HRC or VTS selected and they've tapped the
|
|
// ground set that point as the structure's rally point. Of course
|
|
// this should be visualized, etc. Ah, if only we had time for such
|
|
// things.
|
|
|
|
for (Gob *pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
if ((pgobT->GetFlags() & (kfGobStructure | kfGobSelected)) != (kfGobStructure | kfGobSelected))
|
|
continue;
|
|
BuilderGob *pbldr = (BuilderGob *)pgobT;
|
|
|
|
// We know that if an enemy structure is selected then a local player's
|
|
// structure can't also be selected.
|
|
|
|
if (pgobT->GetOwner() != gpplrLocal)
|
|
break;
|
|
|
|
// VTS or HRC?
|
|
|
|
if (!((1UL << pbldr->GetUnitType()) & (kumVehicleTransportStation | kumHumanResourceCenter)))
|
|
break;
|
|
|
|
pbldr->SetRallyPoint(TcFromWc(wxTarget), TcFromWc(wyTarget));
|
|
WCoord wxT = WcTrunc(wxTarget) + kwcTileHalf;
|
|
WCoord wyT = WcTrunc(wyTarget) + kwcTileHalf;
|
|
CreateAnimGob(wxT, wyT, kfAnmDeleteWhenDone | kfAnmSmokeFireLayer, NULL, g_panidMoveTarget);
|
|
ShowAlert("Rally point set");
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Replicator is a special case. When it is specified as a target the
|
|
// selected units are directed to move to its input.
|
|
|
|
if (pgobHit->GetType() == kgtReplicator) {
|
|
if ((ff & (kfMasMove | kfMasAttack)) == 0) {
|
|
return;
|
|
}
|
|
|
|
MobileUnitGob *pmunt = SetSelectionTargets(pgobHit->GetId(), wxTarget, wyTarget);
|
|
if (pmunt != NULL)
|
|
gsndm.PlaySfx(SfxFromCategory(((MobileUnitConsts *)gapuntc[pmunt->GetUnitType()])->sfxcMove));
|
|
return;
|
|
}
|
|
|
|
|
|
// God mode is allowed only if gfGodMode is on when playing single player
|
|
|
|
bool fGodMode = false;
|
|
if (gfGodMode && !ggame.IsMultiplayer())
|
|
fGodMode = true;
|
|
|
|
// Selection?
|
|
|
|
if (IsSelectionCommand(pgobHit)) {
|
|
static long s_tLastSelected = 0;
|
|
static Gob *s_pgobLastSelected = NULL;
|
|
|
|
if (ff & kfMasSelect) {
|
|
bool fAlreadySelected = (pgobHit->GetFlags() & kfGobSelected) != 0;
|
|
|
|
// Selection. Cancel selection for units elsewhere, set selection
|
|
// for this unit
|
|
|
|
// If the unit is already selected and the last selection occured
|
|
// in within the double-tap window then select all units of the
|
|
// same type on-screen. UNDONE: this may not be the best place to
|
|
// do this.
|
|
|
|
if (fAlreadySelected && HostGetTickCount() - s_tLastSelected < kctDoubleTapDelay && s_pgobLastSelected == pgobHit) {
|
|
SelectSameUnitTypes(pgobHit, false);
|
|
} else {
|
|
gsim.SetGobSelected(pgobHit);
|
|
}
|
|
s_tLastSelected = HostGetTickCount();
|
|
s_pgobLastSelected = pgobHit;
|
|
|
|
// Play sfx
|
|
|
|
if (!gfGodMode) {
|
|
if (pgobHit->GetFlags() & kfGobStructure) {
|
|
gsndm.PlaySfx(((StructConsts *)gapuntc[((StructGob *)pgobHit)->GetUnitType()])->sfxSelect);
|
|
} else {
|
|
if (gfGodMode || pgobHit->GetSide() == gpplrLocal->GetSide())
|
|
gsndm.PlaySfx(SfxFromCategory(((MobileUnitConsts *)gapuntc[((UnitGob *)pgobHit)->GetUnitType()])->sfxcSelect));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ff & kfMasShowMenu) {
|
|
if (pgobHit->GetFlags() & kfGobStructure) {
|
|
ShowUnitMenu(pgobHit);
|
|
}
|
|
}
|
|
} else if (ff & kfMasAttack) {
|
|
// An attack. Reset the wxTarget, wyTarget to be the middle of the gob
|
|
// and issue targets
|
|
|
|
UnitGob *puntHit = (UnitGob *)pgobHit;
|
|
WPoint wptTarget;
|
|
puntHit->GetAttackPoint(&wptTarget);
|
|
wxTarget = wptTarget.wx;
|
|
wyTarget = wptTarget.wy;
|
|
MobileUnitGob *pmunt = SetSelectionTargets(puntHit->GetId(), wxTarget, wyTarget);
|
|
if (pmunt != NULL)
|
|
gsndm.PlaySfx(SfxFromCategory(((MobileUnitConsts *)gapuntc[((UnitGob *)pmunt)->GetUnitType()])->sfxcAttack));
|
|
}
|
|
}
|
|
|
|
bool SimUIForm::IsSelectionCommand(Gob *pgobHit)
|
|
{
|
|
// God mode is allowed only if gfGodMode is on when playing single player
|
|
|
|
bool fGodMode = false;
|
|
if (gfGodMode && !ggame.IsMultiplayer())
|
|
fGodMode = true;
|
|
|
|
// Figure out if this is a selection or an attack command:
|
|
//
|
|
// Player can select same side units
|
|
// Player can select enemy units if no friendly units are selected
|
|
// Player can attack enemy units
|
|
// GodMode can select any unit if that doesn't imply an attack (from
|
|
// either side)
|
|
// GodMode can attack any side if there are selected enemy units
|
|
|
|
// If there are any selected units that can attack pgobHit, it may be an
|
|
// attack.
|
|
|
|
bool fOwnSelection = false;
|
|
bool fSelection = true;
|
|
Side sideHit = pgobHit->GetSide();
|
|
for (Gob *pgobT = ggobm.GetFirstGob(); pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
if ((pgobT->GetFlags() & (kfGobUnit | kfGobSelected)) != (kfGobUnit | kfGobSelected))
|
|
continue;
|
|
UnitGob *puntT = (UnitGob *)pgobT;
|
|
if (puntT->GetOwner() == gpplrLocal)
|
|
fOwnSelection = true;
|
|
if (puntT->IsValidTarget(pgobHit)) {
|
|
fSelection = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If not in god mode and the local player does not own any of the already
|
|
// selected units then just select the new unit
|
|
|
|
if ((!fGodMode) && (!fOwnSelection)) {
|
|
fSelection = true;
|
|
}
|
|
|
|
return fSelection;
|
|
}
|
|
|
|
bool SimUIForm::HasSelectedUnits()
|
|
{
|
|
Gob *pgobT = ggobm.GetFirstGob();
|
|
for (; pgobT != NULL; pgobT = ggobm.GetNextGob(pgobT)) {
|
|
if ((pgobT->GetFlags() & (kfGobUnit | kfGobSelected | kfGobStructure))
|
|
!= (kfGobUnit | kfGobSelected)) {
|
|
continue;
|
|
}
|
|
if (((UnitGob *)pgobT)->GetOwner() == gpplrLocal) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SimUIForm::ShowUnitMenu(Gob *pgob) {
|
|
if (pgob != NULL && (pgob->GetFlags() & kfGobUnit) != 0) {
|
|
// Don't popup menu if the player doesn't own the Gob (unless in
|
|
// God mode). God mode is limited for multiplayer games.
|
|
if ((!gfGodMode || ggame.IsMultiplayer()) &&
|
|
pgob->GetSide() != gpplrLocal->GetSide()) {
|
|
return;
|
|
}
|
|
pgob->PopupMenu();
|
|
}
|
|
}
|
|
|
|
void SimUIForm::SelectSameUnitTypes(Gob *pgob, bool fSfx)
|
|
{
|
|
if ((pgob->GetFlags() & (kfGobUnit | kfGobStructure)) != kfGobUnit) {
|
|
return;
|
|
}
|
|
|
|
WCoord wxViewT, wyViewT;
|
|
gsim.GetViewPos(&wxViewT, &wyViewT);
|
|
TCoord txView = TcFromWc(wxViewT);
|
|
TCoord tyView = TcFromWc(wyViewT);
|
|
|
|
Size sizT;
|
|
ggame.GetPlayfieldSize(&sizT);
|
|
TCoord txRight = TcFromWc(wxViewT + WcFromPc(sizT.cx + gcxTile - 1));
|
|
TCoord tyBottom = TcFromWc(wyViewT + WcFromPc(sizT.cy + gcyTile - 1));
|
|
|
|
TRect trc;
|
|
trc.Set(txView, tyView, txRight, tyBottom);
|
|
gsim.SelectSameUnitTypes((UnitGob *)pgob, &trc);
|
|
|
|
if (gfGodMode || pgob->GetSide() == gpplrLocal->GetSide()) {
|
|
gsndm.PlaySfx(SfxFromCategory(((MobileUnitConsts *)
|
|
gapuntc[((UnitGob *)pgob)->GetUnitType()])->sfxcSelect));
|
|
}
|
|
}
|
|
|
|
void SimUIForm::CalcLevelSpecificConstants()
|
|
{
|
|
}
|
|
|
|
// OPT: this isn't hideous but could be made faster
|
|
|
|
bool PtInPolygon(WPoint *awpt, int cwpt, WCoord wx, WCoord wy)
|
|
{
|
|
bool fInside = false;
|
|
WPoint *pwptI = &awpt[0];
|
|
WPoint *pwptJ = &awpt[cwpt - 1];
|
|
WPoint *pwptEnd = pwptJ + 1;
|
|
|
|
for (int i = 0; pwptI < pwptEnd; pwptI++) {
|
|
if ((((pwptI->wy <= wy) && (wy < pwptJ->wy)) || ((pwptJ->wy <= wy) && (wy < pwptI->wy))) &&
|
|
(wx < (pwptJ->wx - pwptI->wx) * (wy - pwptI->wy) / (pwptJ->wy - pwptI->wy) + pwptI->wx))
|
|
fInside = !fInside;
|
|
pwptJ = pwptI;
|
|
}
|
|
|
|
return fInside;
|
|
}
|
|
|
|
//
|
|
// MiniMapControl
|
|
//
|
|
|
|
MiniMapControl *gpmm;
|
|
|
|
MiniMapControl::MiniMapControl()
|
|
{
|
|
m_pbm = NULL;
|
|
m_cxyBorder = 0;
|
|
m_wfMm = 0;
|
|
m_xOff = 0;
|
|
m_yOff = 0;
|
|
m_tInvalidateLast = 0;
|
|
}
|
|
|
|
MiniMapControl::~MiniMapControl()
|
|
{
|
|
gpmm = NULL;
|
|
delete m_pbm;
|
|
}
|
|
|
|
int MiniMapControl::CalcWidth()
|
|
{
|
|
// Figure out scale based on playfield size
|
|
|
|
Size siz;
|
|
ggame.GetPlayfieldSize(&siz);
|
|
int nScale = 1;
|
|
if (siz.cx >= 240)
|
|
nScale = 2;
|
|
|
|
// Width
|
|
|
|
Size sizMap;
|
|
gsim.GetLevel()->GetTileMap()->GetMapSize(&sizMap);
|
|
return sizMap.cx / (gcxTile / nScale) + 1;
|
|
}
|
|
|
|
bool MiniMapControl::Init(Form *pfrm, IniReader *pini, FindProp *pfind)
|
|
{
|
|
// Base initialization
|
|
|
|
if (!Control::Init(pfrm, pini, pfind))
|
|
return false;
|
|
|
|
// idc (x y cx cy) border
|
|
|
|
char szBorder[32];
|
|
int cArgs = pini->GetPropertyValue(pfind, "%*d (%*d %*d %*d %*d) %s", szBorder);
|
|
|
|
// Remember the size of the input UI area since our border drawing needs it
|
|
|
|
Size sizDib;
|
|
ggame.GetInputUIForm()->GetFormMgr()->GetDib()->GetSize(&sizDib);
|
|
m_cyInputUI = sizDib.cy;
|
|
|
|
// Our even aligned blts depend on having a border for odd width maps
|
|
|
|
m_cxyBorder = 1;
|
|
|
|
// Figure out scale based on playfield size
|
|
|
|
Size siz;
|
|
ggame.GetPlayfieldSize(&siz);
|
|
m_nScale = 1;
|
|
if (siz.cx >= 240 && siz.cx < 320) {
|
|
if (siz.cy > siz.cx)
|
|
m_nScale = 2;
|
|
}
|
|
if (siz.cx >= 320)
|
|
m_nScale = 2;
|
|
gsim.SetMiniMapScale(m_nScale);
|
|
|
|
// Size
|
|
|
|
Size sizMap;
|
|
gsim.GetLevel()->GetTileMap()->GetMapSize(&sizMap);
|
|
m_rc.left = m_rc.right - ((sizMap.cx / (gcxTile / m_nScale)) + m_cxyBorder);
|
|
m_rc.bottom = m_rc.top + (sizMap.cy / (gcyTile / m_nScale)) + m_cxyBorder;
|
|
|
|
// The map is aligned to an edge. The edge is even, and the dib starts on
|
|
// an even address, so we want the width to be even so our blts are aligned.
|
|
// If the map is odd, draw the border inside the dib to make it even. If the
|
|
// map is even, draw the border outside the dib.
|
|
|
|
int cx = m_rc.Width();
|
|
if (cx & 1) {
|
|
m_wfMm &= ~kfMmVertBorderInside;
|
|
cx -= m_cxyBorder;
|
|
m_xOff = 0;
|
|
m_yOff = m_cxyBorder;
|
|
} else {
|
|
m_wfMm |= kfMmVertBorderInside;
|
|
m_xOff = m_cxyBorder;
|
|
m_yOff = m_cxyBorder;
|
|
}
|
|
|
|
// Alloc a dib to hold it
|
|
|
|
m_pbm = CreateDibBitmap(NULL, cx, m_rc.Height());
|
|
if (m_pbm == NULL)
|
|
return false;
|
|
|
|
// Draw internal borders
|
|
|
|
int x = 0;
|
|
int y = 0;
|
|
if (m_cxyBorder != 0) {
|
|
int iclr = gfMultiplayer ? gaiclrSide[gpplrLocal->GetSide()] : kiclr0CyanSide;
|
|
Color clrBorder = GetColor(iclr);
|
|
if (m_wfMm & kfMmVertBorderInside) {
|
|
m_pbm->Fill(x + m_cxyBorder, y, m_rc.Width() - m_cxyBorder, m_cxyBorder, clrBorder);
|
|
m_pbm->Fill(x, y, m_cxyBorder, m_rc.Height() - m_cyInputUI + 1, clrBorder);
|
|
m_pbm->Fill(x, y + m_rc.Height() - m_cyInputUI + 1, m_cxyBorder, m_rc.Height(), GetColor(kiclrBlack));
|
|
} else {
|
|
m_pbm->Fill(x, y, m_rc.Width(), m_cxyBorder, clrBorder);
|
|
}
|
|
}
|
|
|
|
// Stuff constants
|
|
|
|
TileMap *ptmap = gsim.GetLevel()->GetTileMap();
|
|
MiniTileSetHeader *pmtseth = ptmap->GetMiniTileSetHeader(m_nScale);
|
|
m_pbTileData = (byte *)(pmtseth + 1);
|
|
m_pwTileMap = ptmap->m_pwMapData;
|
|
m_pbFogMap = gsim.GetLevel()->GetFogMap()->GetMapPtr();
|
|
ggobm.GetMapSize(&m_ctx, &m_cty);
|
|
m_cbRowBytes = m_pbm->GetRowBytes();
|
|
m_clrBlack = (byte)GetColor(kiclrBlack);
|
|
m_clrWhite = (byte)GetColor(kiclrWhite);
|
|
m_clrGalaxite = (byte)GetColor(kiclrGalaxite);
|
|
for (Side sideT = ksideNeutral; sideT < kcSides; sideT++)
|
|
m_aclrSide[sideT] = (byte)GetSideColor(sideT);
|
|
|
|
// Calc powered radar flag
|
|
|
|
if (!CalcPoweredRadar())
|
|
Redraw();
|
|
|
|
// Remember this object globally
|
|
|
|
gpmm = this;
|
|
return true;
|
|
}
|
|
|
|
void MiniMapControl::OnPaint(DibBitmap *pbm)
|
|
{
|
|
Rect rcForm;
|
|
m_pfrm->GetRect(&rcForm);
|
|
int x = m_rc.left + rcForm.left;
|
|
int y = m_rc.top + rcForm.top;
|
|
|
|
// Lay down bits
|
|
|
|
Size siz;
|
|
m_pbm->GetSize(&siz);
|
|
Rect rcSrc;
|
|
rcSrc.Set(0, 0, siz.cx, siz.cy);
|
|
|
|
if (m_wfMm & kfMmVertBorderInside) {
|
|
pbm->Blt(m_pbm, &rcSrc, x, y);
|
|
} else {
|
|
pbm->Blt(m_pbm, &rcSrc, x + m_cxyBorder, y);
|
|
int iclr = gfMultiplayer ? gaiclrSide[gpplrLocal->GetSide()] : kiclr0CyanSide;
|
|
pbm->Fill(x, y, m_cxyBorder, m_rc.Height() - m_cyInputUI + 1, GetColor(iclr));
|
|
pbm->Fill(x, y + m_rc.Height() - m_cyInputUI + 1, m_cxyBorder, m_rc.Height(), GetColor(kiclrBlack));
|
|
}
|
|
|
|
// Draw box showing the full-screen visible area of the map
|
|
|
|
WCoord wxViewT, wyViewT;
|
|
gsim.GetViewPos(&wxViewT, &wyViewT);
|
|
short xView = PcFromUwc(wxViewT) & 0xfffe;
|
|
short yView = PcFromUwc(wyViewT) & 0xfffe;
|
|
WCoord wxView = WcFromUpc(xView);
|
|
WCoord wyView = WcFromUpc(yView);
|
|
|
|
int cxTile = gcxTile / m_nScale;
|
|
int cyTile = gcyTile / m_nScale;
|
|
|
|
Rect rc;
|
|
rc.left = x + m_cxyBorder + PcFromUwc(wxView) / cxTile;
|
|
rc.top = y + m_cxyBorder + PcFromUwc(wyView) / cyTile;
|
|
ggame.GetPlayfieldSize(&siz);
|
|
rc.right = rc.left + (siz.cx + cxTile - 1) / cxTile;
|
|
rc.bottom = rc.top + (siz.cy + cyTile - 1) / cyTile;
|
|
DrawBorder(pbm, &rc, 1, GetColor(kiclrWhite));
|
|
}
|
|
|
|
int MiniMapControl::OnHitTest(Event *pevt)
|
|
{
|
|
// Don't use finger hit testing on the minimap control; this way
|
|
// all input over the map goes to the map.
|
|
|
|
if (pevt->ff & kfEvtFinger) {
|
|
pevt->ff = ~kfEvtFinger;
|
|
int result = Control::OnHitTest(pevt);
|
|
pevt->ff |= kfEvtFinger;
|
|
return result;
|
|
}
|
|
return Control::OnHitTest(pevt);
|
|
}
|
|
|
|
//TUNE:
|
|
#define kctMiniMapInvalidateRate 25
|
|
|
|
void MiniMapControl::Update()
|
|
{
|
|
// Gets called at Update time. If the minimap is dirty and it's been at
|
|
// least kctMiniMapUpdateRate ticks since the last invalidate, invalidate
|
|
// it now. This gives us immediate invalidates in many cases, but ensures
|
|
// there is at least kctMiniMapUpdateRate ticks between invalidates.
|
|
|
|
long tCurrent = gtimm.GetTickCount();
|
|
if (m_wfMm & kfMmRedraw) {
|
|
if (m_tInvalidateLast == 0 || tCurrent - m_tInvalidateLast >= kctMiniMapInvalidateRate) {
|
|
m_tInvalidateLast = tCurrent;
|
|
m_wfMm &= ~kfMmRedraw;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
// Enough time to give the second finger time to come down, and not long
|
|
// enough to make the minimap jerky
|
|
|
|
#define kctPenDownTimeout 25
|
|
|
|
if (m_wfMm & kfMmPenDownTimeout) {
|
|
if (tCurrent - m_tPenDown >= kctPenDownTimeout) {
|
|
m_wfMm &= ~kfMmPenDownTimeout;
|
|
OnPenEvent2(&m_evtPenDown);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MiniMapControl::OnBreakCapture()
|
|
{
|
|
// Forget about the pen down, if there is one
|
|
|
|
m_wfMm &= ~kfMmPenDownTimeout;
|
|
Trace("Breaking capture");
|
|
}
|
|
|
|
void MiniMapControl::Redraw()
|
|
{
|
|
TRect trc;
|
|
trc.Set(0, 0, m_ctx, m_cty);
|
|
RedrawTRect(&trc);
|
|
}
|
|
|
|
void MiniMapControl::OnPenEvent(Event *pevt)
|
|
{
|
|
// Process the pen down in waiting if there is a pen up
|
|
|
|
if (pevt->eType == penUpEvent) {
|
|
if (m_wfMm & kfMmPenDownTimeout) {
|
|
m_wfMm &= ~kfMmPenDownTimeout;
|
|
OnPenEvent2(&m_evtPenDown);
|
|
}
|
|
}
|
|
|
|
if (pevt->eType != penDownEvent && pevt->eType != penMoveEvent) {
|
|
return;
|
|
}
|
|
|
|
#if defined(IPHONE) || defined(__IPHONEOS__) || defined(__ANDROID__)
|
|
// If already waiting for pen down timeout, then just update the x,y
|
|
|
|
if (m_wfMm & kfMmPenDownTimeout) {
|
|
m_evtPenDown.x = pevt->x;
|
|
m_evtPenDown.y = pevt->y;
|
|
return;
|
|
}
|
|
|
|
// If pen down event, remember it and don't process it for a little bit.
|
|
// This gives enough time to forget about the pen down if the second
|
|
// finger goes down and the control is forced to give up capture.
|
|
|
|
if (pevt->eType == penDownEvent) {
|
|
m_tPenDown = gtimm.GetTickCount();
|
|
m_evtPenDown = *pevt;
|
|
m_wfMm |= kfMmPenDownTimeout;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
OnPenEvent2(pevt);
|
|
}
|
|
|
|
void MiniMapControl::OnPenEvent2(Event *pevt)
|
|
{
|
|
static int s_xPenDown;
|
|
static int s_yPenDown;
|
|
static WCoord s_wxViewDown;
|
|
static WCoord s_wyViewDown;
|
|
|
|
Rect rcForm;
|
|
m_pfrm->GetRect(&rcForm);
|
|
|
|
int xPen = pevt->x - (rcForm.left + m_rc.left + m_cxyBorder);
|
|
int yPen = pevt->y - (rcForm.top + m_rc.top + m_cxyBorder);
|
|
|
|
WCoord wxView, wyView;
|
|
gsim.GetViewPos(&wxView, &wyView);
|
|
|
|
Size sizPlayfield;
|
|
ggame.GetPlayfieldSize(&sizPlayfield);
|
|
|
|
if (pevt->eType == penDownEvent) {
|
|
// Calc the rectangle (in minimap pixel coordinates) bounding the map
|
|
// area currently being viewed.
|
|
|
|
Rect rcView;
|
|
rcView.left = TcFromWc(wxView) * m_nScale;
|
|
rcView.top = TcFromWc(wyView) * m_nScale;
|
|
rcView.right = rcView.left + (((sizPlayfield.cx + gcxTile - 1) / gcxTile) * m_nScale);
|
|
rcView.bottom = rcView.top + (((sizPlayfield.cy + gcyTile - 1) / gcyTile) * m_nScale);
|
|
|
|
#if 0
|
|
// This feels super sloppy, plus it isn't neeed for fine movement anymore,
|
|
// since the map scrolls nicely with a finger directly
|
|
// If this is finger UI, make the rect larger
|
|
|
|
if (pevt->ff & kfEvtFinger) {
|
|
int cxInflate = (gcxTile * 2 - rcView.Width()) / 2;
|
|
if (cxInflate < 0) {
|
|
cxInflate = 0;
|
|
}
|
|
int cyInflate = (gcyTile * 2 - rcView.Height()) / 2;
|
|
if (cyInflate < 0) {
|
|
cyInflate = 0;
|
|
}
|
|
rcView.Inflate(cxInflate, cyInflate);
|
|
}
|
|
#endif
|
|
|
|
// Pen down inside the view rect?
|
|
|
|
if (!rcView.PtIn(xPen, yPen)) {
|
|
|
|
// No, set the view to be centered around the point tapped
|
|
|
|
TCoord tx = xPen / m_nScale;
|
|
TCoord ty = yPen / m_nScale;
|
|
|
|
wxView = WcFromTc(tx - (sizPlayfield.cx / gcxTile) / 2);
|
|
wyView = WcFromTc(ty - ((sizPlayfield.cy + gcyTile - 1) / gcyTile) / 2);
|
|
}
|
|
|
|
s_xPenDown = xPen;
|
|
s_yPenDown = yPen;
|
|
s_wxViewDown = wxView;
|
|
s_wyViewDown = wyView;
|
|
} else {
|
|
int dx = xPen - s_xPenDown;
|
|
wxView = s_wxViewDown + WcFromTc(dx / m_nScale);
|
|
wxView += (dx % m_nScale) * kwcTile / m_nScale;
|
|
int dy = yPen - s_yPenDown;
|
|
wyView = s_wyViewDown + WcFromTc(dy / m_nScale);
|
|
wyView += (dy % m_nScale) * kwcTile / m_nScale;
|
|
}
|
|
|
|
gsim.SetViewPos(wxView, wyView);
|
|
Invalidate();
|
|
}
|
|
|
|
bool MiniMapControl::CalcPoweredRadar()
|
|
{
|
|
// gpmm uninitialized?
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wtautological-undefined-compare"
|
|
#endif
|
|
|
|
if (this == NULL)
|
|
return false;
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic pop
|
|
#endif
|
|
|
|
// Remember if the local player has fully powered Radar. This determines whether
|
|
// enemy units show on the mini-map.
|
|
|
|
bool fHasPoweredRadar = gpplrLocal->GetUnitCount(kutRadar) > 0 && gpplrLocal->GetPowerDemand() <= gpplrLocal->GetPowerSupply();
|
|
word wfMm = fHasPoweredRadar ? kfMmHasPoweredRadar : 0;
|
|
if ((wfMm ^ m_wfMm) != 0) {
|
|
m_wfMm &= ~kfMmHasPoweredRadar;
|
|
m_wfMm |= wfMm;
|
|
Redraw();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MiniMapControl::RedrawTile(TCoord tx, TCoord ty)
|
|
{
|
|
TRect trc;
|
|
trc.left = tx;
|
|
trc.top = ty;
|
|
trc.right = tx + 1;
|
|
trc.bottom = ty + 1;
|
|
RedrawTRect(&trc);
|
|
}
|
|
|
|
void MiniMapControl::RedrawTRect(TRect *ptrc)
|
|
{
|
|
// gpmm uninitialized?
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wtautological-undefined-compare"
|
|
#endif
|
|
|
|
if (this == NULL)
|
|
return;
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic pop
|
|
#endif
|
|
|
|
// Redraw this rect
|
|
|
|
byte *pbDst = m_pbm->GetBits() + (long)ptrc->top * m_cbRowBytes * m_nScale +
|
|
ptrc->left * m_nScale + (long)m_yOff * m_cbRowBytes + m_xOff;
|
|
int cbDstReturn = m_cbRowBytes - ptrc->Width() * m_nScale;
|
|
long offset = (long)ptrc->top * m_ctx + ptrc->left;
|
|
byte *pbFogMap = m_pbFogMap + offset;
|
|
word *pwTileMap = m_pwTileMap + offset;
|
|
int ctReturn = m_ctx - ptrc->Width();
|
|
|
|
if (m_nScale == 1) {
|
|
for (TCoord ty = ptrc->top; ty < ptrc->bottom; ty++) {
|
|
for (TCoord tx = ptrc->left; tx < ptrc->right; tx++, pbFogMap++, pwTileMap++) {
|
|
// Fogged?
|
|
|
|
if (IsFogOpaque(*pbFogMap)) {
|
|
*pbDst++ = m_clrBlack;
|
|
continue;
|
|
}
|
|
|
|
// Not fogged; remember to redraw the minimap to the screen next timer
|
|
|
|
m_wfMm |= kfMmRedraw;
|
|
|
|
// Unit gob?
|
|
|
|
UnitGob *punt = ggobm.GetUnitGob(tx, ty);
|
|
|
|
// don't show inactive munts
|
|
|
|
if (punt != NULL) {
|
|
dword wf = punt->GetFlags();
|
|
if ((wf & (kfGobMobileUnit | kfGobActive)) != (kfGobMobileUnit)) {
|
|
if (wf & kfGobSelected) {
|
|
*pbDst++ = m_clrWhite;
|
|
} else {
|
|
*pbDst++ = m_aclrSide[punt->GetSide()];
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Galaxite?
|
|
|
|
if (HasGalaxite(*pbFogMap)) {
|
|
*pbDst++ = m_clrGalaxite;
|
|
continue;
|
|
}
|
|
|
|
// Tile
|
|
|
|
int nTile = (BigWord(*pwTileMap) & 0x7fc);
|
|
*pbDst++ = m_pbTileData[nTile >> 2];
|
|
continue;
|
|
}
|
|
pwTileMap += ctReturn;
|
|
pbFogMap += ctReturn;
|
|
pbDst += cbDstReturn;
|
|
}
|
|
} else if (m_nScale == 2) {
|
|
for (TCoord ty = ptrc->top; ty < ptrc->bottom; ty++) {
|
|
for (TCoord tx = ptrc->left; tx < ptrc->right; tx++, pbFogMap++, pwTileMap++) {
|
|
// Fogged?
|
|
|
|
if (IsFogOpaque(*pbFogMap)) {
|
|
*pbDst++ = m_clrBlack;
|
|
*pbDst++ = m_clrBlack;
|
|
*(pbDst + m_cbRowBytes - 2) = m_clrBlack;
|
|
*(pbDst + m_cbRowBytes - 1) = m_clrBlack;
|
|
continue;
|
|
}
|
|
|
|
// Not fogged; remember to redraw the minimap to the screen next timer
|
|
|
|
m_wfMm |= kfMmRedraw;
|
|
|
|
// Unit gob?
|
|
|
|
UnitGob *punt = ggobm.GetUnitGob(tx, ty);
|
|
if (punt != NULL) {
|
|
dword wf = punt->GetFlags();
|
|
if ((wf & (kfGobMobileUnit | kfGobActive)) != (kfGobMobileUnit)) {
|
|
byte clr;
|
|
if (wf & kfGobSelected) {
|
|
clr = m_clrWhite;
|
|
} else {
|
|
clr = m_aclrSide[punt->GetSide()];
|
|
}
|
|
|
|
*pbDst++ = clr;
|
|
*pbDst++ = clr;
|
|
*(pbDst + m_cbRowBytes - 2) = clr;
|
|
*(pbDst + m_cbRowBytes - 1) = clr;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Galaxite?
|
|
|
|
if (HasGalaxite(*pbFogMap)) {
|
|
*pbDst++ = m_clrGalaxite;
|
|
*pbDst++ = m_clrGalaxite;
|
|
*(pbDst + m_cbRowBytes - 2) = m_clrGalaxite;
|
|
*(pbDst + m_cbRowBytes - 1) = m_clrGalaxite;
|
|
continue;
|
|
}
|
|
|
|
// Tile
|
|
|
|
int nTile = (BigWord(*pwTileMap) & 0x7fc);
|
|
byte *pbSrc = &m_pbTileData[nTile];
|
|
*pbDst++ = *pbSrc++;
|
|
*pbDst++ = *pbSrc++;
|
|
*(pbDst + m_cbRowBytes - 2) = *pbSrc++;
|
|
*(pbDst + m_cbRowBytes - 1) = *pbSrc++;
|
|
continue;
|
|
}
|
|
pwTileMap += ctReturn;
|
|
pbFogMap += ctReturn;
|
|
pbDst += cbDstReturn + m_cbRowBytes;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace wi
|