From 8f5d5b700022732e21511affb218f8179344c90e Mon Sep 17 00:00:00 2001 From: Ketmar Dark Date: Fri, 11 Oct 2019 23:41:48 +0300 Subject: [PATCH] net: some Preliminary (but half-working) code for new map/resource download scheme --- src/game/g_game.pas | 30 +- src/game/g_net.pas | 1017 ++++++++++++++++++++++++++++++++- src/game/g_netmaster.pas | 3 + src/game/g_netmsg.pas | 66 ++- src/game/g_res_downloader.pas | 443 ++++++++++---- 5 files changed, 1434 insertions(+), 125 deletions(-) diff --git a/src/game/g_game.pas b/src/game/g_game.pas index 2084d88..babcb8e 100644 --- a/src/game/g_game.pas +++ b/src/game/g_game.pas @@ -101,7 +101,7 @@ procedure g_Game_StartClient(Addr: String; Port: Word; PW: String); 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); @@ -4625,7 +4625,7 @@ begin 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 @@ -4658,7 +4658,7 @@ 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]); @@ -4721,8 +4721,7 @@ begin 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(); @@ -4808,15 +4808,25 @@ begin 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); @@ -4989,7 +4999,7 @@ begin gNextMap := Map; end; -procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest); +procedure g_Game_ClientWAD(NewWAD: String; const WHash: TMD5Digest); var gWAD: String; begin @@ -5001,7 +5011,7 @@ 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 dc36712..60d91c1 100644 --- a/src/game/g_net.pas +++ b/src/game/g_net.pas @@ -18,13 +18,13 @@ unit g_net; 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; @@ -37,6 +37,7 @@ const NET_CHAN_CHAT = 8; NET_CHAN_DOWNLOAD = 9; NET_CHAN_SHOTS = 10; + NET_CHAN_DOWNLOAD_EX = 11; NET_NONE = 0; NET_SERVER = 1; @@ -60,6 +61,7 @@ const 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; @@ -77,6 +79,19 @@ const {$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; @@ -86,6 +101,7 @@ type RequestedFullUpdate: Boolean; RCONAuth: Boolean; Voted: Boolean; + Transfer: TNetFileTransfer; // only one transfer may be active NetOut: array [0..1] of TMsg; end; TBanRecord = record @@ -186,7 +202,8 @@ function g_Net_Client_ByPlayer(PID: Word): pTNetClient; 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; @@ -207,18 +224,53 @@ procedure g_Net_DumpEnd(); 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; @@ -266,6 +318,7 @@ begin NetClients[N].RCONAuth := False; NetClients[N].Voted := False; NetClients[N].Player := 0; + clearNetClientTransfers(NetClients[N]); // just in case end; Result := N; @@ -281,7 +334,8 @@ begin 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; @@ -364,7 +418,8 @@ begin NetBuf[NET_UNRELIABLE].Clear(); NetBuf[NET_RELIABLE].Clear(); - SetLength(NetClients, 0); + //SetLength(NetClients, 0); + clearNetClients(true); // clear array NetClientCount := 0; NetPeer := nil; @@ -501,6 +556,7 @@ begin 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); @@ -586,6 +642,368 @@ begin 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; @@ -593,14 +1011,22 @@ var 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 @@ -642,6 +1068,7 @@ begin 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); @@ -651,18 +1078,27 @@ begin 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; @@ -1011,7 +1447,7 @@ begin 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 @@ -1022,6 +1458,7 @@ begin e_KeyPressed(JOY3_JUMP) end; +{ function g_Net_Wait_Event(msgId: Word): TMemoryStream; var ev: ENetEvent; @@ -1074,9 +1511,558 @@ begin 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 @@ -1343,6 +2329,7 @@ end; 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); diff --git a/src/game/g_netmaster.pas b/src/game/g_netmaster.pas index beada6a..925b910 100644 --- a/src/game/g_netmaster.pas +++ b/src/game/g_netmaster.pas @@ -78,6 +78,9 @@ procedure g_Serverlist_GenerateTable(SL: TNetServerList; var ST: TNetServerTable 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 6d72d11..2541dfd 100644 --- a/src/game/g_netmsg.pas +++ b/src/game/g_netmsg.pas @@ -71,6 +71,30 @@ const 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; @@ -244,6 +268,7 @@ procedure MC_SEND_Vote(Start: Boolean = False; Command: string = 'a'); procedure MC_SEND_MapRequest(); procedure MC_SEND_ResRequest(const resName: AnsiString); + type TExternalResourceInfo = record Name: string[255]; @@ -266,6 +291,9 @@ type function MapDataFromMsgStream(msgStream: TMemoryStream):TMapDataMsg; function ResDataFromMsgStream(msgStream: TMemoryStream):TResDataMsg; +function IsValidFileName(const S: String): Boolean; +function IsValidFilePath(const S: String): Boolean; + implementation uses @@ -3115,13 +3143,48 @@ end; 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; @@ -3163,4 +3226,5 @@ begin end; end; + end. diff --git a/src/game/g_res_downloader.pas b/src/game/g_res_downloader.pas index 04048b6..a092437 100644 --- a/src/game/g_res_downloader.pas +++ b/src/game/g_res_downloader.pas @@ -19,44 +19,207 @@ interface 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; @@ -64,115 +227,197 @@ begin 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. -- 2.29.2