hostile-takeover/game/SimUI.cpp
2016-08-31 23:55:30 -04:00

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 = &gtrcMapOpaque;
} 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