Files
2026-05-30 02:50:21 -07:00

723 lines
19 KiB
C++

//******************************************************************
// nthread.cpp
// contains network code which runs in either main thread
// or auxiliary "progress" thread
//******************************************************************
#include "diablo.h"
#pragma hdrstop
#include <process.h>
#include "storm/h/storm.h"
#include "msg.h"
#include "multi.h"
#include "gendung.h"
#include "sound.h"
#include "items.h"
#include "player.h"
//******************************************************************
// debugging
//******************************************************************
#define PRINT_STATUS 1 // 0 in final
#define STATUS_LASTERR 0 // 0 in final
#define DUMP_STATUS 0 // 0 in final
#ifdef NDEBUG
#undef PRINT_STATUS
#undef STATUS_LASTERR
#undef DUMP_STATUS
#define PRINT_STATUS 0
#define STATUS_LASTERR 0
#define DUMP_STATUS 0
#endif
//******************************************************************
// public
//******************************************************************
BYTE gbGameLoopsPerPacket;
DWORD gdwTurnsInTransit;
DWORD gdwNormalMsgSize; // MIN_MSG_SIZE..MAX_MSG_SIZE
DWORD gdwLargestMsgSize; // MIN_MSG_SIZE..MAX_MSG_SIZE
DWORD gdwDeltaBytesSec;
// results from receiving the last synchronous turn
DWORD gdwMsgLenTbl[MAX_PLRS];
DWORD gdwMsgStatTbl[MAX_PLRS];
LPVOID glpMsgTbl[MAX_PLRS];
#ifndef NDEBUG
DWORD gdwAsyncRecvTbl[MAX_PLRS];
DWORD gdwAsyncSendTbl[MAX_PLRS];
#endif
//******************************************************************
// private -- packets
//******************************************************************
static DWORD sgdwRequestDeltaFlag;
static BYTE sgbRunThread;
static BYTE sgbThreadIsRunning;
static CCritSect sgCrit;
static HANDLE sghThread = INVALID_HANDLE_VALUE;
static unsigned sgThreadID;
// are we waiting for a packet?
static BYTE sgbGotPacketOK;
// countdown timer to the next time we need to send an
// async packet and value to initialize countdown timer
static BYTE sgbPacketCountdown;
// countdown timer for synchronous message
static BYTE sgbSyncCountdown;
// timer which marches forward to match the real time clock
// used to determine when to run game loops
static long sglGameClock = 0;
// if game clock and the real clock differ by more than CATCH_UP_DELTA
// then reset the game clock to be equal to the real clock. This ensures
// that the game will attempt to maintain a normal frame rate, but if it
// falls too far outside normal bounds, it will not struggle indefinitely.
#define CATCH_UP_DELTA 500 // milliseconds
#if PRINT_STATUS
static BYTE sgbPrintStatus;
static DWORD sgdwPrintTime;
#if STATUS_LASTERR
static DWORD sgdwPrintTbl[MAX_PLRS];
#endif
#endif
#if DUMP_STATUS
static BYTE sgbDumpStatus;
static BYTE sgbDumpStatusOK;
static BYTE sbDumpInit = TRUE;
#endif
#if ALLOW_TRACE_FCN
static BYTE sgbTraceFcn;
static const char * sgpszLastTrace;
#endif
//******************************************************************
//******************************************************************
#if ALLOW_TRACE_FCN
void trace_fcn(const char * pszFcn) {
// don't write stuff if we're not the active application
if (! bActive) return;
if (! sgbTraceFcn) return;
if (! lpDDSPrimary) return;
if (sgpszLastTrace == pszFcn) return;
sgpszLastTrace = pszFcn;
// ooh -- how cheesy...
if (! pszFcn) pszFcn = " ";
HDC hDC;
HRESULT ddr = lpDDSPrimary->GetDC(&hDC);
if (ddr != DD_OK) return;
TextOut(hDC,5,370,pszFcn,strlen(pszFcn));
lpDDSPrimary->ReleaseDC(hDC);
}
#endif
//******************************************************************
//******************************************************************
void nthread_check_snet_error(const char * pszFcn) {
app_assert(pszFcn);
DWORD dwErr = GetLastError();
// we don't keep track of players, so we may have sent a
// message to somebody who isn't there anymore.
if (dwErr == SNET_ERROR_INVALID_PLAYER)
NULL;
else if (dwErr == SNET_ERROR_GAME_TERMINATED)
gbGameDestroyed = TRUE;
else if (dwErr == SNET_ERROR_NOT_IN_GAME)
gbGameDestroyed = TRUE;
else
app_fatal("%s:\n%s",pszFcn,strGetLastError());
}
//******************************************************************
//******************************************************************
#ifdef _DEBUG
BOOL is_pat_debug_cmd(WPARAM wKey) {
// only a debug key if the control key is down
if (! (GetKeyState(VK_CONTROL) & 0x8000))
return FALSE;
switch (wKey) {
case 0x03: // ctrl - break;
// debugger
if (!fullscreen) {
void myDebugBreak();
myDebugBreak();
force_redraw = FULLDRAW;
}
break;
case 0x13: // ctrl - S
// stall the network by sleeping
Sleep(50);
break;
#if PRINT_STATUS && STATUS_LASTERR
case 0x12: // ctrl - R
ZeroMemory(sgdwPrintTbl,sizeof(sgdwPrintTbl));
break;
#endif
#if PRINT_STATUS
case 0x10: // ctrl - P
// toggle print status
sgbPrintStatus = !sgbPrintStatus;
sgdwPrintTime = GetTickCount();
break;
#endif
#if DUMP_STATUS
case 0x4: // ctrl - D
sgbDumpStatus = !sgbDumpStatus;
sgbDumpStatusOK = TRUE;
sbDumpInit = TRUE;
#if PRINT_STATUS
sgbPrintStatus = sgbDumpStatus;
#endif
break;
#endif
#if ALLOW_TRACE_FCN
case 0xa: // ctrl - J
sgbTraceFcn = !sgbTraceFcn;
sgpszLastTrace = NULL;
break;
#endif
default:
return FALSE;
}
return TRUE;
}
#endif
//******************************************************************
//******************************************************************
DWORD nthread_fill_sync_queue(DWORD dwCounter,DWORD dwIncrement) {
// MAKE SURE THERE ARE ALWAYS SEVERAL SYNC TURNS IN TRANSIT
DWORD turns;
TRACE_FCN("SNetGetTurnsInTransit");
if (! SNetGetTurnsInTransit(&turns)) {
TRACE_FCN(NULL);
nthread_check_snet_error("SNetGetTurnsInTransit");
return 0;
}
TRACE_FCN(NULL);
app_assert(gdwTurnsInTransit);
while (turns++ < gdwTurnsInTransit) {
// counter = low 31 bits
// hi bit = request delta flag or zero
// reset flag after first use
DWORD dwTemp = dwCounter & TURN_COUNTER_MASK;
dwTemp |= sgdwRequestDeltaFlag;
sgdwRequestDeltaFlag = 0;
// send turn
TRACE_FCN("SNetSendTurn");
if (! SNetSendTurn(&dwTemp,sizeof dwTemp)) {
TRACE_FCN(NULL);
nthread_check_snet_error("SNetSendTurn");
return 0;
}
TRACE_FCN(NULL);
// increment counter and reset wraparound
dwCounter += dwIncrement;
if (dwCounter >= TURN_COUNTER_MASK)
dwCounter &= TURN_COUNTER_RESET_MASK;
}
return dwCounter;
}
//******************************************************************
//******************************************************************
#if DUMP_STATUS
void __cdecl dump_string(const char * pszFmt,...) {
app_assert(pszFmt);
// open dumpfile
FILE * f = fopen("c:\\netdump.txt",sbDumpInit ? "wb" : "ab");
if (! f) return;
sbDumpInit = FALSE;
va_list args;
va_start(args,pszFmt);
vfprintf(f,pszFmt,args);
va_end(args);
fclose(f);
}
#endif
//******************************************************************
//******************************************************************
#if DUMP_STATUS
static void DumpStatus() {
// don't write stuff if we're not the active application
if (! bActive) return;
for (int i = 0; i < MAX_PLRS; i++) {
if (! (gdwMsgStatTbl[i] & SNET_PSF_RESPONDING))
break;
}
if (i >= MAX_PLRS) {
if (sgbDumpStatusOK) return;
sgbDumpStatusOK = TRUE;
}
else {
sgbDumpStatusOK = FALSE;
}
// create status string
char szBuf[64];
char * pszBuf = szBuf;
for (i = 0; i < MAX_PLRS; i++) {
if (i == myplr) *pszBuf++ = '>';
*pszBuf++ = (char) (gdwMsgStatTbl[i] >> 16) + '0';
if (plr[i].plractive) *pszBuf++ = '*';
*pszBuf++ = ' ';
}
pszBuf--;
*pszBuf++ = '\r';
*pszBuf++ = '\n';
*pszBuf = 0;
dump_string(szBuf);
}
#endif
//******************************************************************
//******************************************************************
#if PRINT_STATUS
static void print_status() {
// don't write stuff if we're not the active application
if (! bActive) return;
// create status string
char szBuf[128];
char * pszBuf = szBuf;
for (int i = 0; i < MAX_PLRS; i++) {
*pszBuf++ = (i == myplr) ? '<' : ' ';
#if STATUS_LASTERR
*pszBuf++ = (char) (sgdwPrintTbl[i] >> 16) + '0';
#else
*pszBuf++ = (char) (gdwMsgStatTbl[i] >> 16) + '0';
#endif
*pszBuf++ = plr[i].plractive ? 'A' : 'I';
*pszBuf++ = (i == myplr) ? '>' : ' ';
}
*pszBuf++ = ' ';
*pszBuf = 0;
if (! lpDDSPrimary) return;
HDC hDC;
HRESULT ddr = lpDDSPrimary->GetDC(&hDC);
if (ddr != DD_OK) return;
COLORREF oldTextColor = SetTextColor(hDC,RGB(0xff,0xff,0));
COLORREF oldBkColor = SetBkColor(hDC,RGB(0,0,0));
int oldBkMode = SetBkMode(hDC,OPAQUE);
TextOut(hDC,5,385,szBuf,strlen(szBuf));
#ifndef NDEBUG
sprintf(szBuf,"%02x %02x %02x %02x",
gdwAsyncRecvTbl[0] & 0xff,
gdwAsyncRecvTbl[1] & 0xff,
gdwAsyncRecvTbl[2] & 0xff,
gdwAsyncRecvTbl[3] & 0xff
);
TextOut(hDC,5,400,szBuf,strlen(szBuf));
sprintf(szBuf,"%02x %02x %02x %02x",
gdwAsyncSendTbl[0] & 0xff,
gdwAsyncSendTbl[1] & 0xff,
gdwAsyncSendTbl[2] & 0xff,
gdwAsyncSendTbl[3] & 0xff
);
TextOut(hDC,5,415,szBuf,strlen(szBuf));
#endif
static DWORD foo = 0;
sprintf(szBuf,"%d",foo++);
TextOut(hDC,5,430,szBuf,strlen(szBuf));
/*
DWORD dwCurrTime = GetTickCount();
if (dwCurrTime - sgdwPrintTime >= 1000) {
sgdwPrintTime = dwCurrTime;
DWORD dwTurn,dwBytes;
SNetGetPerformanceData(SNET_PERFID_TURN,&dwTurn,NULL,NULL,NULL,NULL);
sprintf(szBuf,"turn: 0x%08x ",dwTurn);
TextOut(hDC,5,400,szBuf,strlen(szBuf));
static DWORD sgdwLastBytes;
SNetGetPerformanceData(SNET_PERFID_BYTESSENTONWIRE,&dwBytes,NULL,NULL,NULL,NULL);
sprintf(szBuf,"bytes: %8d ",dwBytes - sgdwLastBytes);
sgdwLastBytes = dwBytes;
TextOut(hDC,5,415,szBuf,strlen(szBuf));
DWORD dwUser,dwTotal;
static DWORD sdwLastUser;
static DWORD sdwLastTotal;
SNetGetPerformanceData(SNET_PERFID_USERBYTESSENT,&dwUser,NULL,NULL,NULL,NULL);
SNetGetPerformanceData(SNET_PERFID_TOTALBYTESSENT,&dwTotal,NULL,NULL,NULL,NULL);
dwBytes = dwTotal - sdwLastTotal;
if (! dwBytes) dwBytes = 1;
dwBytes = ((dwUser - sdwLastUser) * 100) / dwBytes;
sdwLastUser = dwUser;
sdwLastTotal = dwTotal;
sprintf(szBuf,"user: %3d%% ",dwBytes);
TextOut(hDC,5,430,szBuf,strlen(szBuf));
}
*/
SetTextColor(hDC,oldTextColor);
SetBkColor(hDC,oldBkColor);
SetBkMode(hDC,oldBkMode);
lpDDSPrimary->ReleaseDC(hDC);
}
#endif
//******************************************************************
//******************************************************************
BOOL nthread_msg_check(BOOL * pfSendAsync) {
app_assert(pfSendAsync);
*pfSendAsync = FALSE;
#if PRINT_STATUS
if (sgbPrintStatus) print_status();
#endif
// count down until time to send async packet
app_assert(sgbPacketCountdown);
if (--sgbPacketCountdown) {
// advance the game clock to the next
// time we will need to run a game loop
sglGameClock += 1000 / GAME_FRAMES_PER_SECOND;
return TRUE;
}
// reset async countdown timer
sgbPacketCountdown = gbGameLoopsPerPacket;
// is it time to receive sync message?
app_assert(sgbSyncCountdown);
if (! --sgbSyncCountdown) {
TRACE_FCN("SNetReceiveTurns");
if (! SNetReceiveTurns(0,MAX_PLRS,glpMsgTbl,gdwMsgLenTbl,gdwMsgStatTbl)) {
TRACE_FCN(NULL);
DWORD dwErr = GetLastError();
if (dwErr != SNET_ERROR_NO_MESSAGES_WAITING)
nthread_check_snet_error("SNetReceiveTurns");
// we didn't successfully receive a turn, so go run some
// user stuff, and come back here to try again next time
sgbPacketCountdown = sgbSyncCountdown = 1;
#if PRINT_STATUS && STATUS_LASTERR
// copy results of last bad status info to private table
CopyMemory(sgdwPrintTbl,gdwMsgStatTbl,sizeof(sgdwPrintTbl));
#endif
#if DUMP_STATUS
DumpStatus();
#endif
sgbGotPacketOK = 0;
return FALSE;
}
else if (! sgbGotPacketOK) {
TRACE_FCN(NULL);
// we were waiting for the packet. Since
// there should be a bunch of messages in the
// queue, we should never have to wait.
sgbGotPacketOK = 1;
sglGameClock = GetTickCount();
#if DUMP_STATUS
DumpStatus();
#endif
}
else {
TRACE_FCN(NULL);
}
// reset sync countdown timer
sgbSyncCountdown = ASYNC_CYCLES_PER_SYNC;
// process synchronous turn
process_turn();
}
// it's time to send an async message
*pfSendAsync = TRUE;
// advance the game clock to the next
// time we will need to run a game loop
sglGameClock += 1000 / GAME_FRAMES_PER_SECOND;
return TRUE;
}
//******************************************************************
//******************************************************************
static unsigned __stdcall net_thread_proc(void *) {
long lDelta;
while (sgbRunThread) {
sgCrit.Enter();
// if nthread_free() was called, it may have released the
// critical section so that this thread could run. However,
// we're just supposed to get out of here...
if (! sgbRunThread) {
sgCrit.Leave();
return 0;
}
// fill queue with minimum number of synchronous
// messages before we attempt to read any
BOOL bSendAsync;
nthread_fill_sync_queue(0,0);
if (nthread_msg_check(&bSendAsync)) {
// sleep until it is approximately time
// to send the next synchronous turn
lDelta = sglGameClock - (long) GetTickCount();
}
else {
// we are waiting for a message from one of the other players
// since it is not here yet, sleep for a longish while so
// that we don't do any busy waiting
lDelta = 1000 / GAME_FRAMES_PER_SECOND;
}
sgCrit.Leave();
if (lDelta > 0) Sleep(lDelta);
// pjw.patch2.start
#if CHEATS
static DWORD sdwSkips = 0;
sdwSkips++;
if (lpDDSPrimary)
{
HDC hDC;
HRESULT ddr = lpDDSPrimary->GetDC(&hDC);
if (ddr == DD_OK) {
char szBuf[16];
wsprintf(szBuf,"n:%u",sdwSkips);
TextOut(hDC,5,440,szBuf,strlen(szBuf));
lpDDSPrimary->ReleaseDC(hDC);
}
}
#endif
// pjw.patch2.end
}
return 0;
}
//******************************************************************
//******************************************************************
void nthread_set_delta_request() {
sgdwRequestDeltaFlag = TURN_REQUEST_DELTA_FLAG;
}
//******************************************************************
//******************************************************************
void nthread_init(BOOL bRequestDelta) {
// start game clock and countdown timers
sglGameClock = GetTickCount();
sgbPacketCountdown = 1;
sgbSyncCountdown = 1;
sgbGotPacketOK = 1;
if (bRequestDelta) nthread_set_delta_request();
else sgdwRequestDeltaFlag = 0;
// calculate the number of packets we should send
// per game loop based on the provider latency
SNETCAPS caps;
caps.size = sizeof caps;
if (! SNetGetProviderCaps(&caps))
app_fatal("SNetGetProviderCaps:\n%s",strGetLastError());
// find out how many messages we should put into the queue
if (caps.defaultturnsintransit)
gdwTurnsInTransit = caps.defaultturnsintransit;
else
gdwTurnsInTransit = 1;
// calculate the number of game loops to run per packet
// based on the speed of the network connection
if (caps.defaultturnssec > GAME_FRAMES_PER_SECOND || ! caps.defaultturnssec)
gbGameLoopsPerPacket = 1;
else
gbGameLoopsPerPacket = (BYTE) (GAME_FRAMES_PER_SECOND / caps.defaultturnssec);
// size of largest packet which can be sent over network
gdwLargestMsgSize = min(caps.maxmessagesize,MAX_MSG_SIZE);
app_assert(gdwLargestMsgSize >= MIN_MSG_SIZE);
// calculate how big our messages can be based on the available
// bandwidth and the number of messages we'll be sending per turn
// (bytes/sec) * (frames/pkt) / (frames/sec) = (bytes/pkt)
gdwNormalMsgSize = caps.bytessec * gbGameLoopsPerPacket / GAME_FRAMES_PER_SECOND;
// use 1/4 of the channel for sending delta info to other players
gdwDeltaBytesSec = caps.bytessec * 1 / 4;
// use 3/4 of channel for player messages
gdwNormalMsgSize *= 3;
gdwNormalMsgSize /= 4;
// Divide by the number of players since we have to send a msg to each
// NOTE: we could subtract out the local player, since we send no
// info over the wire to local system, but leave this entry in
// to allow for "overhead"
app_assert(caps.maxplayers);
if (caps.maxplayers > MAX_PLRS) caps.maxplayers = MAX_PLRS;
gdwNormalMsgSize /= caps.maxplayers;
// if our packet is too small, send fewer messages per turn
while (gdwNormalMsgSize < MIN_MSG_SIZE) {
gdwNormalMsgSize *= 2;
gbGameLoopsPerPacket *= 2;
}
// bounds check maximum value
if (gdwNormalMsgSize > gdwLargestMsgSize)
gdwNormalMsgSize = gdwLargestMsgSize;
// create synchronization object for thread
if (gbMaxPlayers > 1) {
sgbThreadIsRunning = FALSE;
sgCrit.Enter();
// create loader thread
sgbRunThread = TRUE;
app_assert(sghThread == INVALID_HANDLE_VALUE);
sghThread = (HANDLE) _beginthreadex(
NULL, // no security info
0, // stack size
net_thread_proc, // start address
NULL, // argument list
0, // initial state
&sgThreadID // sgThreadID
);
if (sghThread == INVALID_HANDLE_VALUE)
app_fatal(TEXT("nthread2:\n%s"),strGetLastError());
// make sure this thread runs at higher priority than
// SFile, and at the same priority as SNet
SetThreadPriority(sghThread,THREAD_PRIORITY_HIGHEST);
}
}
//******************************************************************
//******************************************************************
void nthread_free() {
// kill off the thread
sgbRunThread = FALSE;
// set parameters to invalid values
gdwTurnsInTransit = 0;
gdwNormalMsgSize = 0;
gdwLargestMsgSize = 0;
// wait until it finishes
if (sghThread != INVALID_HANDLE_VALUE) {
if (sgThreadID != GetCurrentThreadId()) {
if (! sgbThreadIsRunning) sgCrit.Leave();
if (WAIT_FAILED == WaitForSingleObject(sghThread,INFINITE))
app_fatal(TEXT("nthread3:\n(%s)"),strGetLastError());
CloseHandle(sghThread);
sghThread = INVALID_HANDLE_VALUE;
}
}
}
//******************************************************************
//******************************************************************
void nthread_perform_keepalive(BOOL bStart) {
if (sghThread == INVALID_HANDLE_VALUE)
return;
app_assert(sgbThreadIsRunning != bStart);
if (bStart) {
// allow thread to run
sgCrit.Leave();
}
else {
// prevent thread from running
sgCrit.Enter();
}
sgbThreadIsRunning = bStart;
}
//******************************************************************
//******************************************************************
BOOL nthread_run_gameloop(BOOL bReloop) {
long lRealClock = (long) GetTickCount();
long lDelta = lRealClock - sglGameClock;
// if it has been too long since we last ran a game loop,
// then pretend we have caught up and are running normally
if (gbMaxPlayers == 1 && lDelta > CATCH_UP_DELTA) {
sglGameClock = lRealClock;
lDelta = 0;
}
return (lDelta >= 0);
}
//******************************************************************
//******************************************************************
int nthread_get_render_interp()
{
long lFrameMs = 1000 / GAME_FRAMES_PER_SECOND;
long lUntilNextFrame = sglGameClock - (long)GetTickCount();
long lElapsed = lFrameMs - lUntilNextFrame;
if (lElapsed <= 0)
return 0;
if (lElapsed >= lFrameMs)
return 256;
return (int)((lElapsed * 256) / lFrameMs);
}