summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 565edf2)
raw | patch | inline | side by side (parent: 565edf2)
author | Ketmar Dark <ketmar@ketmar.no-ip.org> | |
Fri, 11 Oct 2019 20:41:48 +0000 (23:41 +0300) | ||
committer | Ketmar Dark <ketmar@ketmar.no-ip.org> | |
Sat, 12 Oct 2019 15:54:12 +0000 (18:54 +0300) |
diff --git a/src/game/g_game.pas b/src/game/g_game.pas
index 2084d8876e43323c661c78b7f741a01567defe89..babcb8e2cd3e7f2a19b28a00218cc5d3ea107416 100644 (file)
--- a/src/game/g_game.pas
+++ b/src/game/g_game.pas
procedure g_Game_Restart();
procedure g_Game_RestartLevel();
procedure g_Game_RestartRound(NoMapRestart: Boolean = False);
-procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest);
+procedure g_Game_ClientWAD(NewWAD: String; const WHash: TMD5Digest);
procedure g_Game_SaveOptions();
function g_Game_StartMap(Map: String; Force: Boolean = False; const oldMapPath: AnsiString=''): Boolean;
procedure g_Game_ChangeMap(const MapPath: String);
OuterLoop := True;
while OuterLoop do
begin
- while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+ while (enet_host_service(NetHost, @NetEvent, 50) > 0) do
begin
if (NetEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
begin
if newResPath = '' then
begin
g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
- newResPath := g_Res_DownloadWAD(WadName);
+ newResPath := g_Res_DownloadMapWAD(WadName, gWADHash);
if newResPath = '' then
begin
g_FatalError(_lc[I_NET_ERR_HASH]);
ProcessLoading(true);
- if e_KeyPressed(IK_SPACE) or e_KeyPressed(IK_ESCAPE) or e_KeyPressed(VK_ESCAPE) or
- e_KeyPressed(JOY0_JUMP) or e_KeyPressed(JOY1_JUMP) or e_KeyPressed(JOY2_JUMP) or e_KeyPressed(JOY3_JUMP) then
+ if g_Net_UserRequestExit() then
begin
State := 0;
break;
@@ -4785,6 +4784,7 @@ function g_Game_StartMap(Map: String; Force: Boolean = False; const oldMapPath:
var
NewWAD, ResName: String;
I: Integer;
+ nws: AnsiString;
begin
g_Map_Free((Map <> gCurrentMapFileName) and (oldMapPath <> gCurrentMapFileName));
g_Player_RemoveAllCorpses();
ResName := g_ExtractFileName(Map);
if g_Game_IsServer then
begin
- gWADHash := MD5File(MapsDir + NewWAD);
- g_Game_LoadWAD(NewWAD);
+ nws := findDiskWad(MapsDir+NewWAD);
+ if (length(nws) = 0) then
+ begin
+ ResName := '';
+ end
+ else
+ begin
+ gWADHash := MD5File(nws);
+ //writeln('********: nws=', nws, ' : Map=', Map, ' : nw=', NewWAD, ' : resname=', ResName);
+ g_Game_LoadWAD(NewWAD);
+ end;
end else
// hash received in MC_RECV_GameEvent -> NET_EV_MAPSTART
g_Game_ClientWAD(NewWAD, gWADHash);
end else
ResName := Map;
- Result := g_Map_Load(MapsDir + gGameSettings.WAD + ':\' + ResName);
+ //writeln('********: gsw=', gGameSettings.WAD, '; rn=', ResName);
+ Result := (ResName <> '') and g_Map_Load(MapsDir + gGameSettings.WAD + ':\' + ResName);
if Result then
begin
g_Player_ResetAll(Force or gLastMap, gGameSettings.GameType = GT_SINGLE);
gNextMap := Map;
end;
-procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest);
+procedure g_Game_ClientWAD(NewWAD: String; const WHash: TMD5Digest);
var
gWAD: String;
begin
if gWAD = '' then
begin
g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
- gWAD := g_Res_DownloadWAD(ExtractFileName(NewWAD));
+ gWAD := g_Res_DownloadMapWAD(ExtractFileName(NewWAD), WHash);
if gWAD = '' then
begin
g_Game_Free();
diff --git a/src/game/g_net.pas b/src/game/g_net.pas
index dc3671261ad05f0fe27ee815ca47684f5a3e3a74..60d91c110a118bfe840059d4ffd01035a84ee95c 100644 (file)
--- a/src/game/g_net.pas
+++ b/src/game/g_net.pas
interface
uses
- e_log, e_msg, ENet, Classes, MAPDEF{$IFDEF USE_MINIUPNPC}, miniupnpc;{$ELSE};{$ENDIF}
+ e_log, e_msg, ENet, Classes, md5, MAPDEF{$IFDEF USE_MINIUPNPC}, miniupnpc;{$ELSE};{$ENDIF}
const
NET_PROTOCOL_VER = 181;
NET_MAXCLIENTS = 24;
- NET_CHANS = 11;
+ NET_CHANS = 12;
NET_CHAN_SERVICE = 0;
NET_CHAN_IMPORTANT = 1;
NET_CHAN_CHAT = 8;
NET_CHAN_DOWNLOAD = 9;
NET_CHAN_SHOTS = 10;
+ NET_CHAN_DOWNLOAD_EX = 11;
NET_NONE = 0;
NET_SERVER = 1;
NET_DISC_TEMPBAN: enet_uint32 = 7;
NET_DISC_BAN: enet_uint32 = 8;
NET_DISC_MAX: enet_uint32 = 8;
+ NET_DISC_FILE_TIMEOUT: enet_uint32 = 13;
NET_STATE_NONE = 0;
NET_STATE_AUTH = 1;
{$ENDIF}
type
+ TNetFileTransfer = record
+ diskName: string;
+ hash: TMD5Digest;
+ stream: TStream;
+ size: Integer; // file size in bytes
+ chunkSize: Integer;
+ lastSentChunk: Integer;
+ lastAckChunk: Integer;
+ lastAckTime: Int64; // msecs; if not "in progress", we're waiting for the first ack
+ inProgress: Boolean;
+ diskBuffer: PChar; // of `chunkSize` bytes
+ end;
+
TNetClient = record
ID: Byte;
Used: Boolean;
RequestedFullUpdate: Boolean;
RCONAuth: Boolean;
Voted: Boolean;
+ Transfer: TNetFileTransfer; // only one transfer may be active
NetOut: array [0..1] of TMsg;
end;
TBanRecord = record
function g_Net_ClientName_ByID(ID: Integer): string;
procedure g_Net_SendData(Data: AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
-function g_Net_Wait_Event(msgId: Word): TMemoryStream;
+//function g_Net_Wait_Event(msgId: Word): TMemoryStream;
+//function g_Net_Wait_FileInfo (var tf: TNetFileTransfer; asMap: Boolean; out resList: TStringList): Integer;
function IpToStr(IP: LongWord): string;
function StrToIp(IPstr: string; var IP: LongWord): Boolean;
function g_Net_ForwardPorts(ForwardPongPort: Boolean = True): Boolean;
procedure g_Net_UnforwardPorts();
+function g_Net_UserRequestExit: Boolean;
+
+function g_Net_SendMapRequest (): Boolean;
+function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; resList: TStringList): Integer;
+function g_Net_RequestResFileInfo (resIndex: LongInt; out tf: TNetFileTransfer): Integer;
+function g_Net_AbortResTransfer (var tf: TNetFileTransfer): Boolean;
+function g_Net_ReceiveResourceFile (resIndex: LongInt; var tf: TNetFileTransfer; strm: TStream): Integer;
+
+
implementation
uses
SysUtils,
e_input, g_nethandler, g_netmsg, g_netmaster, g_player, g_window, g_console,
- g_main, g_game, g_language, g_weapons, utils, ctypes;
+ g_main, g_game, g_language, g_weapons, utils, ctypes,
+ g_map;
+
+const
+ FILE_CHUNK_SIZE = 8192;
var
g_Net_DownloadTimeout: Single;
{ /// SERVICE FUNCTIONS /// }
+procedure clearNetClientTransfers (var nc: TNetClient);
+begin
+ nc.Transfer.stream.Free;
+ nc.Transfer.diskName := ''; // just in case
+ if (nc.Transfer.diskBuffer <> nil) then FreeMem(nc.Transfer.diskBuffer);
+ nc.Transfer.stream := nil;
+ nc.Transfer.diskBuffer := nil;
+end;
+
+
+procedure clearNetClient (var nc: TNetClient);
+begin
+ clearNetClientTransfers(nc);
+end;
+
+procedure clearNetClients (clearArray: Boolean);
+var
+ f: Integer;
+begin
+ for f := Low(NetClients) to High(NetClients) do clearNetClient(NetClients[f]);
+ if (clearArray) then SetLength(NetClients, 0);
+end;
function g_Net_FindSlot(): Integer;
NetClients[N].RCONAuth := False;
NetClients[N].Voted := False;
NetClients[N].Player := 0;
+ clearNetClientTransfers(NetClients[N]); // just in case
end;
Result := N;
NetOut.Clear();
NetBuf[NET_UNRELIABLE].Clear();
NetBuf[NET_RELIABLE].Clear();
- SetLength(NetClients, 0);
+ //SetLength(NetClients, 0);
+ clearNetClients(true); // clear array
NetPeer := nil;
NetHost := nil;
NetMyID := -1;
NetBuf[NET_UNRELIABLE].Clear();
NetBuf[NET_RELIABLE].Clear();
- SetLength(NetClients, 0);
+ //SetLength(NetClients, 0);
+ clearNetClients(true); // clear array
NetClientCount := 0;
NetPeer := nil;
NetClients[I].NetOut[NET_RELIABLE].Free();
end;
+ clearNetClients(false); // don't clear array
if (NetMPeer <> nil) and (NetMHost <> nil) then g_Net_Slist_Disconnect;
if NetPongSock <> ENET_SOCKET_NULL then
enet_socket_destroy(NetPongSock);
end;
end;
+
+const
+ // server packet type
+ NTF_SERVER_DONE = 10; // done with this file
+ NTF_SERVER_FILE_INFO = 11; // sent after client request
+ NTF_SERVER_CHUNK = 12; // next chunk; chunk number follows
+ NTF_SERVER_ABORT = 13; // server abort
+ NTF_SERVER_MAP_INFO = 14;
+
+ // client packet type
+ NTF_CLIENT_MAP_REQUEST = 100; // map file request; also, returns list of additional wads to download
+ NTF_CLIENT_FILE_REQUEST = 101; // resource file request (by index)
+ NTF_CLIENT_ABORT = 102; // do not send requested file, or abort current transfer
+ NTF_CLIENT_START = 103; // start transfer; client may resume download by sending non-zero starting chunk
+ NTF_CLIENT_ACK = 104; // chunk ack; chunk number follows
+
+
+procedure KillClientByFT (var nc: TNetClient);
+begin
+ e_LogWritefln('disconnected client #%d due to file transfer error', [nc.ID], TMsgType.Warning);
+ enet_peer_disconnect(nc.Peer, NET_DISC_FILE_TIMEOUT);
+ clearNetClientTransfers(nc);
+end;
+
+
+procedure ProcessHostFileTransfers (var nc: TNetClient);
+var
+ tf: ^TNetFileTransfer;
+ ct: Int64;
+ chunks: Integer;
+ rd: Integer;
+ pkt: PENetPacket;
+ omsg: TMsg;
+begin
+ tf := @nc.Transfer;
+ if (tf.stream = nil) then exit;;
+ ct := GetTimerMS();
+ // arbitrary timeout number
+ if (ct-tf.lastAckTime >= 5000) then
+ begin
+ KillClientByFT(nc);
+ exit;
+ end;
+ // check if we need to send something
+ if (not tf.inProgress) then exit; // waiting for the initial ack
+ // ok, we're sending chunks
+ if (tf.lastAckChunk <> tf.lastSentChunk) then exit;
+ Inc(tf.lastSentChunk);
+ // do it one chunk at a time; client ack will advance our chunk counter
+ chunks := (tf.size+tf.chunkSize-1) div tf.chunkSize;
+
+ if (tf.lastSentChunk > chunks) then
+ begin
+ KillClientByFT(nc);
+ exit;
+ end;
+
+ omsg.Alloc(NET_BUFSIZE);
+ try
+ omsg.Clear();
+ if (tf.lastSentChunk = chunks) then
+ begin
+ // we're done with this file
+ e_LogWritefln('download: client #%d, DONE sending chunks #%d/#%d', [nc.ID, tf.lastSentChunk, chunks]);
+ omsg.Write(Byte(NTF_SERVER_DONE));
+ clearNetClientTransfers(nc);
+ end
+ else
+ begin
+ // packet type
+ omsg.Write(Byte(NTF_SERVER_CHUNK));
+ omsg.Write(LongInt(tf.lastSentChunk));
+ // read chunk
+ rd := tf.size-(tf.lastSentChunk*tf.chunkSize);
+ if (rd > tf.chunkSize) then rd := tf.chunkSize;
+ omsg.Write(LongInt(rd));
+ e_LogWritefln('download: client #%d, sending chunk #%d/#%d (%d bytes)', [nc.ID, tf.lastSentChunk, chunks, rd]);
+ //FIXME: check for errors here
+ try
+ tf.stream.Seek(tf.lastSentChunk*tf.chunkSize, soFromBeginning);
+ tf.stream.ReadBuffer(tf.diskBuffer^, rd);
+ omsg.WriteData(tf.diskBuffer, rd);
+ except // sorry
+ KillClientByFT(nc);
+ exit;
+ end;
+ end;
+ // send packet
+ pkt := enet_packet_create(omsg.Data, omsg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then
+ begin
+ KillClientByFT(nc);
+ exit;
+ end;
+ if (enet_peer_send(nc.Peer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then
+ begin
+ KillClientByFT(nc);
+ exit;
+ end;
+ finally
+ omsg.Free();
+ end;
+end;
+
+
+// received packet is in `NetEvent`
+procedure ProcessDownloadExPacket ();
+var
+ f: Integer;
+ nc: ^TNetClient;
+ nid: Integer = -1;
+ msg: TMsg;
+ omsg: TMsg;
+ cmd: Byte;
+ tf: ^TNetFileTransfer;
+ fname: string;
+ pkt: PENetPacket;
+ chunk: Integer;
+ ridx: Integer;
+ dfn: AnsiString;
+ md5: TMD5Digest;
+ st: TStream;
+ size: LongInt;
+begin
+ // find client index by peer
+ for f := Low(NetClients) to High(NetClients) do
+ begin
+ if (not NetClients[f].Used) then continue;
+ //if (NetClients[f].Transfer.stream = nil) then continue;
+ if (NetClients[f].Peer = NetEvent.peer) then
+ begin
+ nid := f;
+ break;
+ end;
+ end;
+ e_LogWritefln('RECEIVE: dlpacket; client=%d (datalen=%u)', [nid, NetEvent.packet^.dataLength]);
+
+ if (nid < 0) then exit; // wtf?!
+ nc := @NetClients[nid];
+
+ if (NetEvent.packet^.dataLength = 0) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+
+ tf := @NetClients[nid].Transfer;
+ tf.lastAckTime := GetTimerMS();
+
+ cmd := Byte(NetEvent.packet^.data^);
+ e_LogWritefln('RECEIVE: nid=%d; cmd=%u', [nid, cmd]);
+ case cmd of
+ NTF_CLIENT_FILE_REQUEST: // file request
+ begin
+ if (tf.stream <> nil) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (NetEvent.packet^.dataLength < 2) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ // new transfer request; build packet
+ if not msg.Init(NetEvent.packet^.data+1, NetEvent.packet^.dataLength-1, True) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ // get resource index
+ ridx := msg.ReadLongInt();
+ if (ridx < -1) or (ridx >= gExternalResources.Count) then
+ begin
+ e_LogWritefln('Invalid resource index %d', [ridx], TMsgType.Warning);
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (ridx < 0) then fname := MapsDir+gGameSettings.WAD else fname := gExternalResources[ridx];
+ if (length(fname) = 0) then
+ begin
+ e_WriteLog('Invalid filename: '+fname, TMsgType.Warning);
+ KillClientByFT(nc^);
+ exit;
+ end;
+ tf.diskName := findDiskWad(fname);
+ if (length(tf.diskName) = 0) then tf.diskName := findDiskWad(GameDir+'/wads/'+fname);
+ if (length(tf.diskName) = 0) then
+ begin
+ e_LogWritefln('NETWORK: file "%s" not found!', [fname], TMsgType.Fatal);
+ KillClientByFT(nc^);
+ exit;
+ end;
+ // calculate hash
+ //TODO: cache hashes
+ tf.hash := MD5File(tf.diskName);
+ // create file stream
+ tf.diskName := findDiskWad(fname);
+ try
+ tf.stream := openDiskFileRO(tf.diskName);
+ except
+ tf.stream := nil;
+ end;
+ if (tf.stream = nil) then
+ begin
+ e_WriteLog(Format('NETWORK: file "%s" not found!', [fname]), TMsgType.Fatal);
+ KillClientByFT(nc^);
+ exit;
+ end;
+ e_LogWritefln('client #%d requested resource #%d (file is `%s` : `%s`)', [nc.ID, ridx, fname, tf.diskName]);
+ tf.size := tf.stream.size;
+ tf.chunkSize := FILE_CHUNK_SIZE; // arbitrary
+ tf.lastSentChunk := -1;
+ tf.lastAckChunk := -1;
+ tf.lastAckTime := GetTimerMS();
+ tf.inProgress := False; // waiting for the first ACK or for the cancel
+ GetMem(tf.diskBuffer, tf.chunkSize);
+ // sent file info message
+ omsg.Alloc(NET_BUFSIZE);
+ try
+ omsg.Clear();
+ omsg.Write(Byte(NTF_SERVER_FILE_INFO));
+ omsg.Write(tf.hash);
+ omsg.Write(tf.size);
+ omsg.Write(tf.chunkSize);
+ omsg.Write(ExtractFileName(fname));
+ pkt := enet_packet_create(omsg.Data, omsg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (enet_peer_send(nc.Peer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ finally
+ omsg.Free();
+ end;
+ end;
+ NTF_CLIENT_ABORT: // do not send requested file, or abort current transfer
+ begin
+ e_LogWritefln('client #%d aborted file transfer', [nc.ID]);
+ clearNetClientTransfers(nc^);
+ end;
+ NTF_CLIENT_START: // start transfer; client may resume download by sending non-zero starting chunk
+ begin
+ if not Assigned(tf.stream) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (tf.lastSentChunk <> -1) or (tf.lastAckChunk <> -1) or (tf.inProgress) then
+ begin
+ // double ack, get lost
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (NetEvent.packet^.dataLength < 2) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ // build packet
+ if not msg.Init(NetEvent.packet^.data+1, NetEvent.packet^.dataLength-1, True) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ chunk := msg.ReadLongInt();
+ if (chunk < 0) or (chunk > (tf.size+tf.chunkSize-1) div tf.chunkSize) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ e_LogWritefln('client #%d started file transfer from chunk %d', [nc.ID, chunk]);
+ // start sending chunks
+ tf.inProgress := True;
+ tf.lastSentChunk := chunk-1;
+ tf.lastAckChunk := chunk-1;
+ end;
+ NTF_CLIENT_ACK: // chunk ack; chunk number follows
+ begin
+ if not Assigned(tf.stream) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (tf.lastSentChunk < 0) or (not tf.inProgress) then
+ begin
+ // double ack, get lost
+ KillClientByFT(nc^);
+ exit;
+ end;
+ if (NetEvent.packet^.dataLength < 2) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ // build packet
+ if not msg.Init(NetEvent.packet^.data+1, NetEvent.packet^.dataLength-1, True) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ chunk := msg.ReadLongInt();
+ if (chunk < 0) or (chunk > (tf.size+tf.chunkSize-1) div tf.chunkSize) then
+ begin
+ KillClientByFT(nc^);
+ exit;
+ end;
+ // do it this way, so client may seek, or request retransfers for some reason
+ tf.lastAckChunk := chunk;
+ tf.lastSentChunk := chunk;
+ e_LogWritefln('client #%d acked file transfer chunk %d', [nc.ID, chunk]);
+ end;
+ NTF_CLIENT_MAP_REQUEST:
+ begin
+ e_LogWritefln('client #%d requested map info', [nc.ID]);
+ omsg.Alloc(NET_BUFSIZE);
+ try
+ omsg.Clear();
+ dfn := findDiskWad(MapsDir+gGameSettings.WAD);
+ if (dfn = '') then dfn := '!wad_not_found!.wad'; //FIXME
+ md5 := MD5File(dfn);
+ st := openDiskFileRO(dfn);
+ if not assigned(st) then exit; //wtf?!
+ size := st.size;
+ st.Free;
+ // packet type
+ omsg.Write(Byte(NTF_SERVER_MAP_INFO));
+ // map wad name
+ omsg.Write(gGameSettings.WAD);
+ // map wad md5
+ omsg.Write(md5);
+ // map wad size
+ omsg.Write(size);
+ // number of external resources for map
+ omsg.Write(LongInt(gExternalResources.Count));
+ // external resource names
+ for f := 0 to gExternalResources.Count-1 do
+ begin
+ omsg.Write(ExtractFileName(gExternalResources[f])); // GameDir+'/wads/'+ResList.Strings[i]
+ end;
+ // send packet
+ pkt := enet_packet_create(omsg.Data, omsg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then exit;
+ if (enet_peer_send(nc.Peer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+ finally
+ omsg.Free();
+ end;
+ end;
+ else
+ begin
+ KillClientByFT(NetClients[nid]);
+ exit;
+ end;
+ end;
+end;
+
+
function g_Net_Host_Update(): enet_size_t;
var
IP: string;
ID: Integer;
TC: pTNetClient;
TP: TPlayer;
+ f: Integer;
begin
IP := '';
Result := 0;
- if NetUseMaster then
- g_Net_Slist_Check;
+ if NetUseMaster then g_Net_Slist_Check;
g_Net_Host_CheckPings;
+ // process file transfers
+ for f := Low(NetClients) to High(NetClients) do
+ begin
+ if (not NetClients[f].Used) then continue;
+ if (NetClients[f].Transfer.stream = nil) then continue;
+ ProcessHostFileTransfers(NetClients[f]);
+ end;
+
while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
begin
case (NetEvent.kind) of
NetClients[ID].RCONAuth := False;
NetClients[ID].NetOut[NET_UNRELIABLE].Alloc(NET_BUFSIZE*2);
NetClients[ID].NetOut[NET_RELIABLE].Alloc(NET_BUFSIZE*2);
+ clearNetClientTransfers(NetClients[ID]); // just in case
enet_peer_timeout(NetEvent.peer, ENET_PEER_TIMEOUT_LIMIT * 2, ENET_PEER_TIMEOUT_MINIMUM * 2, ENET_PEER_TIMEOUT_MAXIMUM * 2);
ENET_EVENT_TYPE_RECEIVE:
begin
- ID := Byte(NetEvent.peer^.data^);
- if ID > High(NetClients) then Exit;
- TC := @NetClients[ID];
+ //e_LogWritefln('RECEIVE: chan=%u', [NetEvent.channelID]);
+ if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then
+ begin
+ ProcessDownloadExPacket();
+ end
+ else
+ begin
+ ID := Byte(NetEvent.peer^.data^);
+ if ID > High(NetClients) then Exit;
+ TC := @NetClients[ID];
- if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
- g_Net_Host_HandlePacket(TC, NetEvent.packet, g_Net_HostMsgHandler);
+ if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
+ g_Net_Host_HandlePacket(TC, NetEvent.packet, g_Net_HostMsgHandler);
+ end;
end;
ENET_EVENT_TYPE_DISCONNECT:
begin
ID := Byte(NetEvent.peer^.data^);
if ID > High(NetClients) then Exit;
+ clearNetClient(NetClients[ID]);
TC := @NetClients[ID];
if TC = nil then Exit;
enet_host_flush(NetHost);
end;
-function UserRequestExit: Boolean;
+function g_Net_UserRequestExit: Boolean;
begin
Result := e_KeyPressed(IK_SPACE) or
e_KeyPressed(IK_ESCAPE) or
e_KeyPressed(JOY3_JUMP)
end;
+{
function g_Net_Wait_Event(msgId: Word): TMemoryStream;
var
ev: ENetEvent;
status := 0 (* error: timeout *)
end;
ProcessLoading(true);
- until (status <> 2) or UserRequestExit();
+ until (status <> 2) or g_Net_UserRequestExit();
Result := stream
end;
+}
+
+
+function getNewTimeoutEnd (): Int64;
+begin
+ result := GetTimerMS();
+ if (g_Net_DownloadTimeout <= 0) then
+ begin
+ result := result+1000*60*3; // 3 minutes
+ end
+ else
+ begin
+ result := result+trunc(g_Net_DownloadTimeout*1000);
+ end;
+end;
+
+
+function g_Net_SendMapRequest (): Boolean;
+var
+ msg: TMsg;
+ pkt: PENetPacket;
+begin
+ result := false;
+ e_LogWritefln('sending map request...', []);
+ // send request
+ msg.Alloc(NET_BUFSIZE);
+ try
+ msg.Clear();
+ msg.Write(Byte(NTF_CLIENT_MAP_REQUEST));
+ e_LogWritefln(' request size is %d', [msg.CurSize]);
+ pkt := enet_packet_create(msg.Data, msg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then exit;
+ if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+ enet_host_flush(NetHost);
+ finally
+ msg.Free();
+ end;
+ result := true;
+end;
+
+
+// returns `false` on error or user abort
+// fills:
+// hash
+// size
+// chunkSize
+// returns:
+// <0 on error
+// 0 on success
+// 1 on user abort
+// 2 on server abort
+// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
+function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; resList: TStringList): Integer;
+var
+ ev: ENetEvent;
+ rMsgId: Byte;
+ Ptr: Pointer;
+ msg: TMsg;
+ freePacket: Boolean = false;
+ ct, ett: Int64;
+ status: cint;
+ s: AnsiString;
+ rc, f: LongInt;
+begin
+ FillChar(ev, SizeOf(ev), 0);
+ Result := -1;
+ try
+ ett := getNewTimeoutEnd();
+ repeat
+ status := enet_host_service(NetHost, @ev, 300);
+ if (status < 0) then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
+ Result := -1;
+ exit;
+ end;
+ if (status = 0) then
+ begin
+ // check for timeout
+ ct := GetTimerMS();
+ if (ct >= ett) then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
+ Result := -1;
+ exit;
+ end;
+ end
+ else
+ begin
+ // some event
+ case ev.kind of
+ ENET_EVENT_TYPE_RECEIVE:
+ begin
+ freePacket := true;
+ if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
+ begin
+ //e_LogWritefln('g_Net_Wait_MapInfo: skip message from non-transfer channel', []);
+ end
+ else
+ begin
+ ett := getNewTimeoutEnd();
+ if (ev.packet.dataLength < 1) then
+ begin
+ e_LogWritefln('g_Net_Wait_MapInfo: invalid server packet (no data)', []);
+ Result := -1;
+ exit;
+ end;
+ Ptr := ev.packet^.data;
+ rMsgId := Byte(Ptr^);
+ e_LogWritefln('g_Net_Wait_MapInfo: got message %u from server (dataLength=%u)', [rMsgId, ev.packet^.dataLength]);
+ if (rMsgId = NTF_SERVER_FILE_INFO) then
+ begin
+ e_LogWritefln('g_Net_Wait_MapInfo: waiting for map info reply, but got file info reply', []);
+ Result := -1;
+ exit;
+ end
+ else if (rMsgId = NTF_SERVER_ABORT) then
+ begin
+ e_LogWritefln('g_Net_Wait_MapInfo: server aborted transfer', []);
+ Result := 2;
+ exit;
+ end
+ else if (rMsgId = NTF_SERVER_MAP_INFO) then
+ begin
+ e_LogWritefln('g_Net_Wait_MapInfo: creating map info packet...', []);
+ if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
+ e_LogWritefln('g_Net_Wait_MapInfo: parsing map info packet (rd=%d; max=%d)...', [msg.ReadCount, msg.MaxSize]);
+ resList.Clear();
+ // map wad name
+ tf.diskName := msg.ReadString();
+ e_LogWritefln('g_Net_Wait_MapInfo: map wad is `%s`', [tf.diskName]);
+ // map wad md5
+ tf.hash := msg.ReadMD5();
+ // map wad size
+ tf.size := msg.ReadLongInt();
+ e_LogWritefln('g_Net_Wait_MapInfo: map wad size is %d', [tf.size]);
+ // number of external resources for map
+ rc := msg.ReadLongInt();
+ if (rc < 0) or (rc > 1024) then
+ begin
+ e_LogWritefln('g_Net_Wait_Event: invalid number of map external resources (%d)', [rc]);
+ Result := -1;
+ exit;
+ end;
+ e_LogWritefln('g_Net_Wait_MapInfo: map external resource count is %d', [rc]);
+ // external resource names
+ for f := 0 to rc-1 do
+ begin
+ s := ExtractFileName(msg.ReadString());
+ if (length(s) = 0) then
+ begin
+ Result := -1;
+ exit;
+ end;
+ resList.append(s);
+ end;
+ e_LogWritefln('g_Net_Wait_MapInfo: got map info', []);
+ Result := 0; // success
+ exit;
+ end
+ else
+ begin
+ e_LogWritefln('g_Net_Wait_Event: invalid server packet type', []);
+ Result := -1;
+ exit;
+ end;
+ end;
+ end;
+ ENET_EVENT_TYPE_DISCONNECT:
+ begin
+ if (ev.data <= NET_DISC_MAX) then
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' + _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + ev.data)], True);
+ Result := -1;
+ exit;
+ end;
+ else
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
+ result := -1;
+ exit;
+ end;
+ end;
+ if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
+ end;
+ ProcessLoading(true);
+ if g_Net_UserRequestExit() then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
+ Result := 1;
+ exit;
+ end;
+ until false;
+ finally
+ if (freePacket) then enet_packet_destroy(ev.packet);
+ end;
+end;
+
+
+// returns `false` on error or user abort
+// fills:
+// diskName (actually, base name)
+// hash
+// size
+// chunkSize
+// returns:
+// <0 on error
+// 0 on success
+// 1 on user abort
+// 2 on server abort
+// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
+function g_Net_RequestResFileInfo (resIndex: LongInt; out tf: TNetFileTransfer): Integer;
+var
+ ev: ENetEvent;
+ rMsgId: Byte;
+ Ptr: Pointer;
+ msg: TMsg;
+ freePacket: Boolean = false;
+ ct, ett: Int64;
+ status: cint;
+ pkt: PENetPacket;
+begin
+ // send request
+ msg.Alloc(NET_BUFSIZE);
+ try
+ msg.Clear();
+ msg.Write(Byte(NTF_CLIENT_FILE_REQUEST));
+ msg.Write(resIndex);
+ pkt := enet_packet_create(msg.Data, msg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then
+ begin
+ result := -1;
+ exit;
+ end;
+ if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then
+ begin
+ result := -1;
+ exit;
+ end;
+ finally
+ msg.Free();
+ end;
+
+ FillChar(ev, SizeOf(ev), 0);
+ Result := -1;
+ try
+ ett := getNewTimeoutEnd();
+ repeat
+ status := enet_host_service(NetHost, @ev, 300);
+ if (status < 0) then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
+ Result := -1;
+ exit;
+ end;
+ if (status = 0) then
+ begin
+ // check for timeout
+ ct := GetTimerMS();
+ if (ct >= ett) then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
+ Result := -1;
+ exit;
+ end;
+ end
+ else
+ begin
+ // some event
+ case ev.kind of
+ ENET_EVENT_TYPE_RECEIVE:
+ begin
+ freePacket := true;
+ if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
+ begin
+ //e_LogWriteln('g_Net_Wait_Event: skip message from non-transfer channel');
+ end
+ else
+ begin
+ ett := getNewTimeoutEnd();
+ if (ev.packet.dataLength < 1) then
+ begin
+ e_LogWriteln('g_Net_Wait_Event: invalid server packet (no data)');
+ Result := -1;
+ exit;
+ end;
+ Ptr := ev.packet^.data;
+ rMsgId := Byte(Ptr^);
+ e_LogWritefln('received transfer packet with id %d (%u bytes)', [rMsgId, ev.packet^.dataLength]);
+ if (rMsgId = NTF_SERVER_FILE_INFO) then
+ begin
+ if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
+ tf.hash := msg.ReadMD5();
+ tf.size := msg.ReadLongInt();
+ tf.chunkSize := msg.ReadLongInt();
+ tf.diskName := ExtractFileName(msg.readString());
+ if (tf.size < 0) or (tf.chunkSize <> FILE_CHUNK_SIZE) or (length(tf.diskName) = 0) then
+ begin
+ e_LogWritefln('g_Net_RequestResFileInfo: invalid file info packet', []);
+ Result := -1;
+ exit;
+ end;
+ e_LogWritefln('got file info for resource #%d: size=%d; name=%s', [resIndex, tf.size, tf.diskName]);
+ Result := 0; // success
+ exit;
+ end
+ else if (rMsgId = NTF_SERVER_ABORT) then
+ begin
+ e_LogWriteln('g_Net_RequestResFileInfo: server aborted transfer');
+ Result := 2;
+ exit;
+ end
+ else if (rMsgId = NTF_SERVER_MAP_INFO) then
+ begin
+ e_LogWriteln('g_Net_RequestResFileInfo: waiting for map info reply, but got file info reply');
+ Result := -1;
+ exit;
+ end
+ else
+ begin
+ e_LogWriteln('g_Net_RequestResFileInfo: invalid server packet type');
+ Result := -1;
+ exit;
+ end;
+ end;
+ end;
+ ENET_EVENT_TYPE_DISCONNECT:
+ begin
+ if (ev.data <= NET_DISC_MAX) then
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' + _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + ev.data)], True);
+ Result := -1;
+ exit;
+ end;
+ else
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
+ result := -1;
+ exit;
+ end;
+ end;
+ if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
+ end;
+ ProcessLoading(true);
+ if g_Net_UserRequestExit() then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
+ Result := 1;
+ exit;
+ end;
+ until false;
+ finally
+ if (freePacket) then enet_packet_destroy(ev.packet);
+ end;
+end;
+
+
+function g_Net_AbortResTransfer (var tf: TNetFileTransfer): Boolean;
+var
+ msg: TMsg;
+ pkt: PENetPacket;
+begin
+ result := false;
+ e_LogWritefln('aborting file transfer...', []);
+ // send request
+ msg.Alloc(NET_BUFSIZE);
+ try
+ msg.Clear();
+ msg.Write(Byte(NTF_CLIENT_ABORT));
+ pkt := enet_packet_create(msg.Data, msg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then exit;
+ if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+ enet_host_flush(NetHost);
+ finally
+ msg.Free();
+ end;
+ result := true;
+end;
+
+
+// returns `false` on error or user abort
+// fills:
+// hash
+// size
+// chunkSize
+// returns:
+// <0 on error
+// 0 on success
+// 1 on user abort
+// 2 on server abort
+// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
+function g_Net_ReceiveResourceFile (resIndex: LongInt; var tf: TNetFileTransfer; strm: TStream): Integer;
+var
+ ev: ENetEvent;
+ rMsgId: Byte;
+ Ptr: Pointer;
+ msg: TMsg;
+ omsg: TMsg;
+ freePacket: Boolean = false;
+ ct, ett: Int64;
+ status: cint;
+ nextChunk: Integer = 0;
+ chunk: Integer;
+ csize: Integer;
+ buf: PChar = nil;
+ pkt: PENetPacket;
+begin
+ // send request
+ msg.Alloc(NET_BUFSIZE);
+ try
+ msg.Clear();
+ msg.Write(Byte(NTF_CLIENT_START));
+ msg.Write(LongInt(0));
+ pkt := enet_packet_create(msg.Data, msg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then exit;
+ if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+ finally
+ msg.Free();
+ end;
+
+ // wait for reply data
+ FillChar(ev, SizeOf(ev), 0);
+ Result := -1;
+ GetMem(buf, tf.chunkSize);
+ try
+ ett := getNewTimeoutEnd();
+ repeat
+ status := enet_host_service(NetHost, @ev, 300);
+ if (status < 0) then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
+ Result := -1;
+ exit;
+ end;
+ if (status = 0) then
+ begin
+ // check for timeout
+ ct := GetTimerMS();
+ if (ct >= ett) then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
+ Result := -1;
+ exit;
+ end;
+ end
+ else
+ begin
+ // some event
+ case ev.kind of
+ ENET_EVENT_TYPE_RECEIVE:
+ begin
+ freePacket := true;
+ if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
+ begin
+ //e_LogWritefln('g_Net_Wait_Event: skip message from non-transfer channel', []);
+ end
+ else
+ begin
+ ett := getNewTimeoutEnd();
+ if (ev.packet.dataLength < 1) then
+ begin
+ e_LogWritefln('g_Net_ReceiveResourceFile: invalid server packet (no data)', []);
+ Result := -1;
+ exit;
+ end;
+ Ptr := ev.packet^.data;
+ rMsgId := Byte(Ptr^);
+ if (rMsgId = NTF_SERVER_DONE) then
+ begin
+ e_LogWritefln('file transfer complete.', []);
+ result := 0;
+ exit;
+ end
+ else if (rMsgId = NTF_SERVER_CHUNK) then
+ begin
+ if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
+ chunk := msg.ReadLongInt();
+ csize := msg.ReadLongInt();
+ if (chunk <> nextChunk) then
+ begin
+ e_LogWritefln('received chunk %d, but expected chunk %d', [chunk, nextChunk]);
+ Result := -1;
+ exit;
+ end;
+ if (csize < 0) or (csize > tf.chunkSize) then
+ begin
+ e_LogWritefln('received chunk with size %d, but expected chunk size is %d', [csize, tf.chunkSize]);
+ Result := -1;
+ exit;
+ end;
+ e_LogWritefln('got chunk #%d of #%d (csize=%d)', [chunk, (tf.size+tf.chunkSize-1) div tf.chunkSize, csize]);
+ msg.ReadData(buf, csize);
+ strm.WriteBuffer(buf^, csize);
+ nextChunk := chunk+1;
+ // send ack
+ omsg.Alloc(NET_BUFSIZE);
+ try
+ omsg.Clear();
+ omsg.Write(Byte(NTF_CLIENT_ACK));
+ omsg.Write(LongInt(chunk));
+ pkt := enet_packet_create(omsg.Data, omsg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then exit;
+ if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+ finally
+ omsg.Free();
+ end;
+ end
+ else if (rMsgId = NTF_SERVER_ABORT) then
+ begin
+ e_LogWritefln('g_Net_ReceiveResourceFile: server aborted transfer', []);
+ Result := 2;
+ exit;
+ end
+ else
+ begin
+ e_LogWritefln('g_Net_ReceiveResourceFile: invalid server packet type', []);
+ Result := -1;
+ exit;
+ end;
+ end;
+ end;
+ ENET_EVENT_TYPE_DISCONNECT:
+ begin
+ if (ev.data <= NET_DISC_MAX) then
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' + _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + ev.data)], True);
+ Result := -1;
+ exit;
+ end;
+ else
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
+ result := -1;
+ exit;
+ end;
+ end;
+ if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
+ end;
+ ProcessLoading(true);
+ if g_Net_UserRequestExit() then
+ begin
+ g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
+ Result := 1;
+ exit;
+ end;
+ until false;
+ finally
+ FreeMem(buf);
+ if (freePacket) then enet_packet_destroy(ev.packet);
+ end;
+end;
+
function g_Net_IsHostBanned(IP: LongWord; Perm: Boolean = False): Boolean;
var
initialization
conRegVar('cl_downloadtimeout', @g_Net_DownloadTimeout, 0.0, 1000000.0, '', 'timeout in seconds, 0 to disable it');
+ SetLength(NetClients, 0);
g_Net_DownloadTimeout := 60;
NetIn.Alloc(NET_BUFSIZE);
NetOut.Alloc(NET_BUFSIZE);
index beada6a9377c123a784b1c935bfa559f91db4d4d..925b910241c91bcdda8a5a4e2f7e645441c7fe5a 100644 (file)
--- a/src/game/g_netmaster.pas
+++ b/src/game/g_netmaster.pas
procedure g_Serverlist_Draw(var SL: TNetServerList; var ST: TNetServerTable);
procedure g_Serverlist_Control(var SL: TNetServerList; var ST: TNetServerTable);
+function GetTimerMS(): Int64;
+
+
implementation
uses
diff --git a/src/game/g_netmsg.pas b/src/game/g_netmsg.pas
index 6d72d117c55339c0f02f79c6ada7cd1da52dd09d..2541dfd37de2abcb9969a41fb9dace000ab37f71 100644 (file)
--- a/src/game/g_netmsg.pas
+++ b/src/game/g_netmsg.pas
NET_MSG_RES_REQUEST = 203;
NET_MSG_RES_RESPONSE = 204;
+ // chunked file transfers
+ // it goes this way:
+ // client requests file (FILE_REQUEST)
+ // server sends file header info (FILE_HEADER)
+ // client acks chunk -1 (CHUNK_ACK) to initiate transfer, or cancels (FILE_CANCEL)
+ // server start sending data chunks (one at a time, waiting for an ACK for each one)
+ // when client acks the last chunk, transfer is complete
+ // this scheme sux, of course; we can do better by spamming with unreliable unsequenced packets,
+ // and use client acks to drive server sends, but meh... let's do it this way first, and
+ // we can improve it later.
+
+ // client: request a file
+ NET_MSG_FILE_REQUEST = 210;
+ // server: file info response
+ NET_MSG_FILE_HEADER = 211;
+ // client: request transfer cancellation
+ // server: something went wrong, transfer cancelled, bomb out
+ NET_MSG_FILE_CANCEL = 212;
+ // server: file chunk data
+ NET_MSG_FILE_CHUNK_DATA = 213;
+ // client: file chunk ack
+ NET_MSG_FILE_CHUNK_ACK = 214;
+
+
NET_CHAT_SYSTEM = 0;
NET_CHAT_PLAYER = 1;
NET_CHAT_TEAM = 2;
procedure MC_SEND_MapRequest();
procedure MC_SEND_ResRequest(const resName: AnsiString);
+
type
TExternalResourceInfo = record
Name: string[255];
function MapDataFromMsgStream(msgStream: TMemoryStream):TMapDataMsg;
function ResDataFromMsgStream(msgStream: TMemoryStream):TResDataMsg;
+function IsValidFileName(const S: String): Boolean;
+function IsValidFilePath(const S: String): Boolean;
+
implementation
uses
procedure MH_RECV_MapRequest(C: pTNetClient; var M: TMsg);
var
- payload: AByte;
peer: pENetPeer;
+ payload: AByte;
mapDataMsg: TMapDataMsg;
begin
e_WriteLog('NET: Received map request from ' +
DecodeIPV4(C^.Peer.address.host), TMsgType.Notify);
+ (*
+ omsg.Alloc(NET_BUFSIZE);
+ try
+ omsg.Clear();
+ dfn := findDiskWad(MapsDir+gGameSettings.WAD);
+ if (dfn = '') then dfn := '!wad_not_found!.wad'; //FIXME
+ md5 := MD5File(dfn);
+ st := openDiskFileRO(dfn);
+ if not assigned(st) then exit; //wtf?!
+ size := st.size;
+ st.Free;
+ // packet type
+ omsg.Write(Byte({NTF_SERVER_MAP_INFO}NET_MSG_MAP_RESPONSE));
+ // map wad name
+ omsg.Write(gGameSettings.WAD);
+ // map wad md5
+ omsg.Write(md5);
+ // map wad size
+ omsg.Write(size);
+ // number of external resources for map
+ omsg.Write(LongInt(gExternalResources.Count));
+ // external resource names
+ for f := 0 to gExternalResources.Count-1 do
+ begin
+ omsg.Write(ExtractFileName(gExternalResources[f])); // GameDir+'/wads/'+ResList.Strings[i]
+ end;
+ // send packet
+ pkt := enet_packet_create(omsg.Data, omsg.CurSize, ENET_PACKET_FLAG_RELIABLE);
+ if not Assigned(pkt) then exit;
+ peer := NetClients[C^.ID].Peer;
+ if (enet_peer_send(Peer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+ finally
+ omsg.Free();
+ end;
+ *)
mapDataMsg := CreateMapDataMsg(MapsDir + gGameSettings.WAD, gExternalResources);
peer := NetClients[C^.ID].Peer;
end;
end;
+
end.
index 04048b693c983c558af1c61168f3948eee2c5823..a092437993d1601f774deb9235ff1ac037916e7d 100644 (file)
uses sysutils, Classes, md5, g_net, g_netmsg, g_console, g_main, e_log;
-function g_Res_SearchSameWAD(const path, filename: string; const resMd5: TMD5Digest): string;
-function g_Res_DownloadWAD(const FileName: string): string;
+function g_Res_SearchSameWAD(const path, filename: AnsiString; const resMd5: TMD5Digest): AnsiString;
+function g_Res_SearchResWad (asMap: Boolean; const resMd5: TMD5Digest): AnsiString;
+
+// download map wad from server (if necessary)
+// download all required map resource wads too
+// returns name of the map wad (relative to mapdir), or empty string on error
+function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
+
+// call this before downloading a new map from a server
+procedure g_Res_ClearReplacementWads ();
+// returns original name, or replacement name
+function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
+procedure g_Res_PutReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
+
implementation
-uses g_language, sfs, utils, wadreader, g_game;
+uses g_language, sfs, utils, wadreader, g_game, hashtable;
const DOWNLOAD_DIR = 'downloads';
-procedure FindFiles(const dirName, filename: string; var files: TStringList);
+type
+ TFileInfo = record
+ diskName: AnsiString; // lowercased
+ baseName: AnsiString; // lowercased
+ md5: TMD5Digest;
+ md5valid: Boolean;
+ nextBaseNameIndex: Integer;
+ end;
+
var
- searchResult: TSearchRec;
+ knownFiles: array of TFileInfo;
+ knownHash: THashStrInt = nil; // key: base name; value: index
+ scannedDirs: THashStrInt = nil; // key: lowercased dir name
+ replacements: THashStrStr = nil;
+
+
+function findKnownFile (diskName: AnsiString): Integer;
+var
+ idx: Integer;
+ baseName: AnsiString;
begin
- if FindFirst(dirName+'/*', faAnyFile, searchResult) = 0 then
+ result := -1;
+ if not assigned(knownHash) then exit;
+ if (length(diskName) = 0) then exit;
+ baseName := toLowerCase1251(ExtractFileName(diskName));
+ if (not knownHash.get(baseName, idx)) then exit;
+ if (idx < 0) or (idx >= length(knownFiles)) then raise Exception.Create('wutafuck?');
+ while (idx >= 0) do
+ begin
+ if (strEquCI1251(knownFiles[idx].diskName, diskName)) then begin result := idx; exit; end; // i found her!
+ idx := knownFiles[idx].nextBaseNameIndex;
+ end;
+end;
+
+
+function addKnownFile (diskName: AnsiString): Integer;
+var
+ idx: Integer;
+ lastIdx: Integer = -1;
+ baseName: AnsiString;
+ fi: ^TFileInfo;
+begin
+ result := -1;
+ if not assigned(knownHash) then knownHash := THashStrInt.Create();
+ if (length(diskName) = 0) then exit;
+ baseName := toLowerCase1251(ExtractFileName(diskName));
+ if (length(baseName) = 0) then exit;
+ // check if we already have this file
+ if (knownHash.get(baseName, idx)) then
+ begin
+ if (idx < 0) or (idx >= length(knownFiles)) then raise Exception.Create('wutafuck?');
+ while (idx >= 0) do
+ begin
+ if (strEquCI1251(knownFiles[idx].diskName, diskName)) then
+ begin
+ // already here
+ result := idx;
+ exit;
+ end;
+ lastIdx := idx;
+ idx := knownFiles[idx].nextBaseNameIndex;
+ end;
+ end;
+ // this file is not there, append it
+ idx := length(knownFiles);
+ result := idx;
+ SetLength(knownFiles, idx+1); // sorry
+ fi := @knownFiles[idx];
+ fi.diskName := diskName;
+ fi.baseName := baseName;
+ fi.md5valid := false;
+ fi.nextBaseNameIndex := -1;
+ if (lastIdx < 0) then
+ begin
+ // totally new one
+ knownHash.put(baseName, idx);
+ end
+ else
begin
- try
- repeat
- if (searchResult.Attr and faDirectory) = 0 then
+ knownFiles[lastIdx].nextBaseNameIndex := idx;
+ end;
+end;
+
+
+function getKnownFileWithMD5 (diskDir: AnsiString; baseName: AnsiString; const md5: TMD5Digest): AnsiString;
+var
+ idx: Integer;
+begin
+ result := '';
+ if not assigned(knownHash) then exit;
+ if (not knownHash.get(toLowerCase1251(baseName), idx)) then exit;
+ if (idx < 0) or (idx >= length(knownFiles)) then raise Exception.Create('wutafuck?');
+ while (idx >= 0) do
+ begin
+ if (strEquCI1251(knownFiles[idx].diskName, IncludeTrailingPathDelimiter(diskDir)+baseName)) then
+ begin
+ if (not knownFiles[idx].md5valid) then
+ begin
+ knownFiles[idx].md5 := MD5File(knownFiles[idx].diskName);
+ knownFiles[idx].md5valid := true;
+ end;
+ if (MD5Match(knownFiles[idx].md5, md5)) then
+ begin
+ result := knownFiles[idx].diskName;
+ exit;
+ end;
+ end;
+ idx := knownFiles[idx].nextBaseNameIndex;
+ end;
+end;
+
+
+// call this before downloading a new map from a server
+procedure g_Res_ClearReplacementWads ();
+begin
+ if assigned(replacements) then replacements.clear();
+ e_LogWriteln('cleared replacement wads');
+end;
+
+
+// returns original name, or replacement name
+function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
+var
+ fn: AnsiString;
+begin
+ result := oldname;
+ if not assigned(replacements) then exit;
+ if (replacements.get(toLowerCase1251(ExtractFileName(oldname)), fn)) then result := fn;
+end;
+
+
+procedure g_Res_PutReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
+begin
+ e_LogWritefln('adding replacement wad: oldname=%s; newname=%s', [oldname, newDiskName]);
+ replacements.put(toLowerCase1251(oldname), newDiskName);
+end;
+
+
+procedure scanDir (const dirName: AnsiString; calcMD5: Boolean);
+var
+ searchResult: TSearchRec;
+ dfn: AnsiString;
+ idx: Integer;
+begin
+ if not assigned(scannedDirs) then scannedDirs := THashStrInt.Create();
+ dfn := toLowerCase1251(IncludeTrailingPathDelimiter(dirName));
+ if scannedDirs.has(dfn) then exit;
+ scannedDirs.put(dfn, 42);
+
+ if (FindFirst(dirName+'/*', faAnyFile, searchResult) <> 0) then exit;
+ try
+ repeat
+ if (searchResult.Attr and faDirectory) = 0 then
+ begin
+ dfn := dirName+'/'+searchResult.Name;
+ idx := addKnownFile(dfn);
+ if (calcMD5) and (idx >= 0) then
begin
- if StrEquCI1251(searchResult.Name, filename) then
+ if (not knownFiles[idx].md5valid) then
begin
- files.Add(dirName+'/'+filename);
- Exit;
+ knownFiles[idx].md5 := MD5File(knownFiles[idx].diskName);
+ knownFiles[idx].md5valid := true;
end;
- end
- else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
- FindFiles(IncludeTrailingPathDelimiter(dirName)+searchResult.Name, filename, files);
- until FindNext(searchResult) <> 0;
- finally
- FindClose(searchResult);
- end;
+ end;
+ end
+ else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
+ begin
+ scanDir(IncludeTrailingPathDelimiter(dirName)+searchResult.Name, calcMD5);
+ end;
+ until (FindNext(searchResult) <> 0);
+ finally
+ FindClose(searchResult);
end;
end;
-function CompareFileHash(const filename: string; const resMd5: TMD5Digest): Boolean;
+
+function CompareFileHash(const filename: AnsiString; const resMd5: TMD5Digest): Boolean;
var
gResHash: TMD5Digest;
- fname: string;
+ fname: AnsiString;
begin
fname := findDiskWad(filename);
if length(fname) = 0 then begin result := false; exit; end;
Result := MD5Match(gResHash, resMd5);
end;
-function CheckFileHash(const path, filename: string; const resMd5: TMD5Digest): Boolean;
+function CheckFileHash(const path, filename: AnsiString; const resMd5: TMD5Digest): Boolean;
var
- fname: string;
+ fname: AnsiString;
begin
fname := findDiskWad(path+filename);
if length(fname) = 0 then begin result := false; exit; end;
Result := FileExists(fname) and CompareFileHash(fname, resMd5);
end;
-function g_Res_SearchSameWAD(const path, filename: string; const resMd5: TMD5Digest): string;
+
+function g_Res_SearchResWad (asMap: Boolean; const resMd5: TMD5Digest): AnsiString;
var
- res: string;
- files: TStringList;
- i: Integer;
+ f: Integer;
begin
- Result := '';
-
- if CheckFileHash(path, filename, resMd5) then
+ result := '';
+ //if not assigned(scannedDirs) then scannedDirs := THashStrInt.Create();
+ if (asMap) then
begin
- Result := path + filename;
- Exit;
+ scanDir(GameDir+'/maps/downloads', true);
+ end
+ else
+ begin
+ scanDir(GameDir+'/wads/downloads', true);
end;
-
- files := TStringList.Create;
-
- FindFiles(path, filename, files);
- for i := 0 to files.Count - 1 do
+ for f := Low(knownFiles) to High(knownFiles) do
begin
- res := files.Strings[i];
- if CompareFileHash(res, resMd5) then
+ if (not knownFiles[f].md5valid) then continue;
+ if (MD5Match(knownFiles[f].md5, resMd5)) then
begin
- Result := res;
- Break;
+ result := knownFiles[f].diskName;
+ exit;
end;
end;
+ //resStream := createDiskFile(GameDir+'/wads/'+mapData.ExternalResources[i].Name);
+end;
+
- files.Free;
+function g_Res_SearchSameWAD (const path, filename: AnsiString; const resMd5: TMD5Digest): AnsiString;
+begin
+ scanDir(path, false);
+ result := getKnownFileWithMD5(path, filename, resMd5);
end;
-function SaveWAD(const path, filename: string; const data: array of Byte): string;
+
+function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
var
- resFile: TStream;
- dpt: string;
+ tf: TNetFileTransfer;
+ resList: TStringList;
+ f, res: Integer;
+ strm: TStream;
+ mmd5: TMD5Digest;
+ fname: AnsiString;
+ idx: Integer;
+ wadname: AnsiString;
begin
+ //SetLength(mapData.ExternalResources, 0);
+ //result := g_Res_SearchResWad(true{asMap}, mapHash);
+ result := '';
+ g_Res_ClearReplacementWads();
+
try
- result := path+DOWNLOAD_DIR+'/'+filename;
- dpt := path+DOWNLOAD_DIR;
- if not findFileCI(dpt, true) then CreateDir(dpt);
- resFile := createDiskFile(result);
- resFile.WriteBuffer(data[0], Length(data));
- resFile.Free
+ CreateDir(GameDir+'/maps/downloads');
except
- Result := '';
end;
-end;
-function g_Res_DownloadWAD(const FileName: string): string;
-var
- msgStream: TMemoryStream;
- resStream: TStream;
- mapData: TMapDataMsg;
- i: Integer;
- resData: TResDataMsg;
-begin
- SetLength(mapData.ExternalResources, 0);
- g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
- e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
- g_Game_SetLoadingText(FileName + '...', 0, False);
- MC_SEND_MapRequest();
-
- msgStream := g_Net_Wait_Event(NET_MSG_MAP_RESPONSE);
- if msgStream <> nil then
- begin
- mapData := MapDataFromMsgStream(msgStream);
- msgStream.Free;
- end else
- mapData.FileSize := 0;
+ try
+ CreateDir(GameDir+'/wads/downloads');
+ except
+ end;
- for i := 0 to High(mapData.ExternalResources) do
- begin
- if not CheckFileHash(GameDir + '/wads/',
- mapData.ExternalResources[i].Name,
- mapData.ExternalResources[i].md5) then
- begin
- g_Console_Add(Format(_lc[I_NET_WAD_DL],
- [mapData.ExternalResources[i].Name]));
- e_WriteLog('Downloading Wad `' + mapData.ExternalResources[i].Name +
- '` from server', TMsgType.Notify);
- g_Game_SetLoadingText(mapData.ExternalResources[i].Name + '...', 0, False);
- MC_SEND_ResRequest(mapData.ExternalResources[i].Name);
+ resList := TStringList.Create();
- msgStream := g_Net_Wait_Event(NET_MSG_RES_RESPONSE);
- if msgStream = nil then
- continue;
+ try
+ g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
+ e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
+ g_Game_SetLoadingText(FileName + '...', 0, False);
+ //MC_SEND_MapRequest();
+ if (not g_Net_SendMapRequest()) then exit;
- resData := ResDataFromMsgStream(msgStream);
+ FileName := ExtractFileName(FileName);
+ if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
+ res := g_Net_Wait_MapInfo(tf, resList);
+ if (res <> 0) then exit;
- resStream := createDiskFile(GameDir+'/wads/'+mapData.ExternalResources[i].Name);
- resStream.WriteBuffer(resData.FileData[0], resData.FileSize);
+ // find or download a map
+ result := g_Res_SearchResWad(true{asMap}, mapHash);
+ if (length(result) = 0) then
+ begin
+ // download map
+ res := g_Net_RequestResFileInfo(-1{map}, tf);
+ if (res <> 0) then
+ begin
+ e_LogWriteln('error requesting map wad');
+ result := '';
+ exit;
+ end;
+ fname := GameDir+'/maps/downloads/'+FileName;
+ try
+ strm := createDiskFile(fname);
+ except
+ e_WriteLog('cannot create map file `'+FileName+'`', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ try
+ res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
+ except
+ e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+ strm.Free;
+ result := '';
+ exit;
+ end;
+ strm.Free;
+ if (res <> 0) then
+ begin
+ e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ mmd5 := MD5File(fname);
+ if (not MD5Match(mmd5, mapHash)) then
+ begin
+ e_WriteLog('error downloading map file `'+FileName+'` (bad hash)', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ idx := addKnownFile(fname);
+ if (idx < 0) then
+ begin
+ e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ knownFiles[idx].md5 := mmd5;
+ knownFiles[idx].md5valid := true;
+ result := fname;
+ end;
- resData.FileData := nil;
- resStream.Free;
- msgStream.Free;
+ // download resources
+ for f := 0 to resList.Count-1 do
+ begin
+ res := g_Net_RequestResFileInfo(f, tf);
+ if (res <> 0) then begin result := ''; exit; end;
+ wadname := g_Res_SearchResWad(false{asMap}, tf.hash);
+ if (length(wadname) <> 0) then
+ begin
+ // already here
+ g_Net_AbortResTransfer(tf);
+ g_Res_PutReplacementWad(tf.diskName, wadname);
+ end
+ else
+ begin
+ fname := GameDir+'/wads/downloads/'+tf.diskName;
+ try
+ strm := createDiskFile(fname);
+ except
+ e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ try
+ res := g_Net_ReceiveResourceFile(f, tf, strm);
+ except
+ e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+ strm.Free;
+ result := '';
+ exit;
+ end;
+ strm.Free;
+ if (res <> 0) then
+ begin
+ e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ idx := addKnownFile(fname);
+ if (idx < 0) then
+ begin
+ e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+ result := '';
+ exit;
+ end;
+ knownFiles[idx].md5 := tf.hash;
+ knownFiles[idx].md5valid := true;
+ g_Res_PutReplacementWad(tf.diskName, fname);
+ end;
end;
+ finally
+ resList.Free;
end;
-
- Result := SaveWAD(MapsDir, ExtractFileName(FileName), mapData.FileData);
- if mapData.FileSize = 0 then
- DeleteFile(Result);
end;
+
end.