/* Copyright (C) 1997-2001 Id Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* Modifications Copyright 2003-2004 Bytonic Software Copyright 2010 Google Inc. */ package com.googlecode.gwtquake.shared.client; import java.io.IOException; import java.io.RandomAccessFile; import com.googlecode.gwtquake.shared.common.AsyncCallback; import com.googlecode.gwtquake.shared.common.Buffers; import com.googlecode.gwtquake.shared.common.CM; import com.googlecode.gwtquake.shared.common.Com; import com.googlecode.gwtquake.shared.common.CommandBuffer; import com.googlecode.gwtquake.shared.common.Constants; import com.googlecode.gwtquake.shared.common.ExecutableCommand; import com.googlecode.gwtquake.shared.common.Globals; import com.googlecode.gwtquake.shared.common.QuakeFileSystem; import com.googlecode.gwtquake.shared.game.Commands; import com.googlecode.gwtquake.shared.game.EntityState; import com.googlecode.gwtquake.shared.render.Model; import com.googlecode.gwtquake.shared.sound.Sound; import com.googlecode.gwtquake.shared.sys.Sys; import com.googlecode.gwtquake.shared.util.Lib; /** * CL_parse */ public class ClientParser { //// cl_parse.c -- parse a message received from the server public static String svc_strings[] = { "svc_bad", "svc_muzzleflash", "svc_muzzlflash2", "svc_temp_entity", "svc_layout", "svc_inventory", "svc_nop", "svc_disconnect", "svc_reconnect", "svc_sound", "svc_print", "svc_stufftext", "svc_serverdata", "svc_configstring", "svc_spawnbaseline", "svc_centerprint", "svc_download", "svc_playerinfo", "svc_packetentities", "svc_deltapacketentities", "svc_frame" }; // ============================================================================= public static String DownloadFileName(String fn) { return QuakeFileSystem.Gamedir() + "/" + fn; } /* * =============== CL_CheckOrDownloadFile * * Returns true if the file exists, otherwise it attempts to start a * download from the server. =============== */ public static boolean CheckOrDownloadFile(String filename) { RandomAccessFile fp; String name; if (filename.indexOf("..") != -1) { Com.Printf("Refusing to download a path with ..\n"); return true; } if (QuakeFileSystem.FileLength(filename) > 0) { // it exists, no need to download return true; } Globals.cls.downloadname = filename; // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left Globals.cls.downloadtempname = Com .StripExtension(Globals.cls.downloadname); Globals.cls.downloadtempname += ".tmp"; // ZOID // check to see if we already have a tmp for this file, if so, try to // resume // open the file if not opened yet name = DownloadFileName(Globals.cls.downloadtempname); fp = Lib.fopen(name, "r+b"); if (fp != null) { // it exists long len = 0; try { len = fp.length(); } catch (IOException e) { } Globals.cls.download = fp; // give the server an offset to start the download Com.Printf("Resuming " + Globals.cls.downloadname + "\n"); Buffers.writeByte(Globals.cls.netchan.message, Constants.clc_stringcmd); Buffers.WriteString(Globals.cls.netchan.message, "download " + Globals.cls.downloadname + " " + len); } else { Com.Printf("Downloading " + Globals.cls.downloadname + "\n"); Buffers.writeByte(Globals.cls.netchan.message, Constants.clc_stringcmd); Buffers.WriteString(Globals.cls.netchan.message, "download " + Globals.cls.downloadname); } Globals.cls.downloadnumber++; return false; } /* * =============== CL_Download_f * * Request a download from the server =============== */ public static ExecutableCommand downloadCommand = new ExecutableCommand() { public void execute() { String filename; if (Commands.Argc() != 2) { Com.Printf("Usage: download <filename>\n"); return; } filename = Commands.Argv(1); if (filename.indexOf("..") != -1) { Com.Printf("Refusing to download a path with ..\n"); return; } if (QuakeFileSystem.LoadFile(filename) != null) { // it exists, no need to // download Com.Printf("File already exists.\n"); return; } Globals.cls.downloadname = filename; Com.Printf("Downloading " + Globals.cls.downloadname + "\n"); // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left Globals.cls.downloadtempname = Com .StripExtension(Globals.cls.downloadname); Globals.cls.downloadtempname += ".tmp"; Buffers.writeByte(Globals.cls.netchan.message, Constants.clc_stringcmd); Buffers.WriteString(Globals.cls.netchan.message, "download " + Globals.cls.downloadname); Globals.cls.downloadnumber++; } }; /* * ====================== CL_RegisterSounds ====================== */ public static void RegisterSounds() { Sound.BeginRegistration(); ClientTent.RegisterTEntSounds(); for (int i = 1; i < Constants.MAX_SOUNDS; i++) { if (Globals.cl.configstrings[Constants.CS_SOUNDS + i] == null || Globals.cl.configstrings[Constants.CS_SOUNDS + i] .equals("") || Globals.cl.configstrings[Constants.CS_SOUNDS + i] .equals("\0")) break; Globals.cl.sound_precache[i] = Sound .RegisterSound(Globals.cl.configstrings[Constants.CS_SOUNDS + i]); Sys.SendKeyEvents(); // pump message loop } Sound.EndRegistration(); } /* * ===================== CL_ParseDownload * * A download message has been received from the server * ===================== */ public static void ParseDownload() { // read the data int size = Globals.net_message.getShort(); int percent = Buffers.readUnsignedByte(Globals.net_message); if (size == -1) { Com.Printf("Server does not have this file.\n"); if (Globals.cls.download != null) { // if here, we tried to resume a file but the server said no try { Globals.cls.download.close(); } catch (IOException e) { } Globals.cls.download = null; } Client.requestNextDownload(); return; } // open the file if not opened yet if (Globals.cls.download == null) { String name = DownloadFileName(Globals.cls.downloadtempname).toLowerCase(); QuakeFileSystem.CreatePath(name); Globals.cls.download = Lib.fopen(name, "rw"); if (Globals.cls.download == null) { Globals.net_message.readcount += size; Com.Printf("Failed to open " + Globals.cls.downloadtempname + "\n"); Client.requestNextDownload(); return; } } //fwrite(net_message.data[net_message.readcount], 1, size, // cls.download); try { Globals.cls.download.write(Globals.net_message.data, Globals.net_message.readcount, size); } catch (Exception e) { } Globals.net_message.readcount += size; if (percent != 100) { // request next block // change display routines by zoid Globals.cls.downloadpercent = percent; Buffers.writeByte(Globals.cls.netchan.message, Constants.clc_stringcmd); Buffers.Print(Globals.cls.netchan.message, "nextdl"); } else { String oldn, newn; //char oldn[MAX_OSPATH]; //char newn[MAX_OSPATH]; // Com.Printf ("100%%\n"); try { Globals.cls.download.close(); } catch (IOException e) { } // rename the temp file to it's final name oldn = DownloadFileName(Globals.cls.downloadtempname); newn = DownloadFileName(Globals.cls.downloadname); int r = Lib.rename(oldn, newn); if (r != 0) Com.Printf("failed to rename.\n"); Globals.cls.download = null; Globals.cls.downloadpercent = 0; // get another file if needed Client.requestNextDownload(); } } /* * ===================================================================== * * SERVER CONNECTING MESSAGES * * ===================================================================== */ /* * ================== CL_ParseServerData ================== */ //checked once, was ok. public static void ParseServerData() { String str; int i; Com.DPrintf("ParseServerData():Serverdata packet received.\n"); // // wipe the client_state_t struct // Client.clearState(); Globals.cls.state = Constants.ca_connected; // parse protocol version number i = Globals.net_message.getInt(); Globals.cls.serverProtocol = i; // BIG HACK to let demos from release work with the 3.0x patch!!! if (Globals.server_state != 0 && Constants.PROTOCOL_VERSION == 34) { } else if (i != Constants.PROTOCOL_VERSION) Com.Error(Constants.ERR_DROP, "Server returned version " + i + ", not " + Constants.PROTOCOL_VERSION); Globals.cl.servercount = Globals.net_message.getInt(); Globals.cl.attractloop = Buffers.readUnsignedByte(Globals.net_message) != 0; // game directory str = Buffers.getString(Globals.net_message); Globals.cl.gamedir = str; Com.dprintln("gamedir=" + str); // set gamedir // TODO(jgw): I think this is irrelevant now. // if (str.length() > 0 // && (FS.fs_gamedirvar.string == null // || FS.fs_gamedirvar.string.length() == 0 || FS.fs_gamedirvar.string // .equals(str)) // || (str.length() == 0 && (FS.fs_gamedirvar.string != null || FS.fs_gamedirvar.string // .length() == 0))) // Cvar.Set("game", str); // parse player entity number Globals.cl.playernum = Globals.net_message.getShort(); Com.dprintln("numplayers=" + Globals.cl.playernum); // get the full level name str = Buffers.getString(Globals.net_message); Com.dprintln("levelname=" + str); if (Globals.cl.playernum == -1) { // playing a cinematic or showing a // pic, not a level Screen.PlayCinematic(str); } else { // seperate the printfs so the server message can have a color // Com.Printf( // "\n\n\35\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\37\n\n"); // Com.Printf('\02' + str + "\n"); Com.Printf("Levelname:" + str + "\n"); // need to prep refresh at next oportunity Globals.cl.refresh_prepped = false; } } /* * ================== CL_ParseBaseline ================== */ public static void ParseBaseline() { EntityState es; int newnum; EntityState nullstate = new EntityState(null); //memset(nullstate, 0, sizeof(nullstate)); int bits[] = { 0 }; newnum = ClientEntities.ParseEntityBits(bits); es = Globals.cl_entities[newnum].baseline; ClientEntities.ParseDelta(nullstate, es, newnum, bits[0]); } /* * ================ CL_LoadClientinfo * * ================ */ public static void LoadClientinfo(final ClientInfo ci, String s) { int i; int t; String model_name, skin_name, model_filename, skin_filename; String[] weapon_filenames = new String[Constants.MAX_CLIENTWEAPONMODELS]; int num_weapon_filenames = 0; ci.cinfo = s; // isolate the player's name ci.name = s; t = s.indexOf('\\'); if (t != -1) { ci.name = s.substring(0, t); s = s.substring(t + 1, s.length()); //s = t + 1; } if (Globals.cl_noskins.value != 0 || s.length() == 0) { model_filename = ("players/male/tris.md2"); weapon_filenames[0] = ("players/male/weapon.md2"); num_weapon_filenames = 1; skin_filename = ("players/male/grunt.pcx"); ci.iconname = ("/players/male/grunt_i.pcx"); ci.skin = Globals.re.RegisterSkin(skin_filename); ci.icon = Globals.re.RegisterPic(ci.iconname); } else { // isolate the model name int pos = s.indexOf('/'); if (pos == -1) pos = s.indexOf('/'); if (pos == -1) { pos = 0; Com.Error(Constants.ERR_FATAL, "Invalid model name:" + s); } model_name = s.substring(0, pos); // isolate the skin name skin_name = s.substring(pos + 1, s.length()); // model file model_filename = "players/" + model_name + "/tris.md2"; if (ci.model == null) { model_name = "male"; model_filename = "players/male/tris.md2"; } // skin file skin_filename = "players/" + model_name + "/" + skin_name + ".pcx"; ci.skin = Globals.re.RegisterSkin(skin_filename); // if we don't have the skin and the model wasn't male, // see if the male has it (this is for CTF's skins) if (ci.skin == null && !model_name.equalsIgnoreCase("male")) { // change model to male model_name = "male"; model_filename = "players/male/tris.md2"; // see if the skin exists for the male model skin_filename = "players/" + model_name + "/" + skin_name + ".pcx"; ci.skin = Globals.re.RegisterSkin(skin_filename); } // if we still don't have a skin, it means that the male model // didn't have // it, so default to grunt if (ci.skin == null) { // see if the skin exists for the male model skin_filename = "players/" + model_name + "/grunt.pcx"; ci.skin = Globals.re.RegisterSkin(skin_filename); } // weapon file num_weapon_filenames = ClientView.num_cl_weaponmodels; for (i = 0; i < ClientView.num_cl_weaponmodels; i++) { weapon_filenames[i] = "players/" + model_name + "/" + ClientView.cl_weaponmodels[i]; // TODO(jgw): move this failover code into loadWeaponModel() somehow. // if (null == ci.weaponmodel[i] && model_name.equals("cyborg")) { // // try male // weapon_filename = "players/male/" // + CL_view.cl_weaponmodels[i]; // ci.weaponmodel[i] = Globals.re // .RegisterModel(weapon_filename); // } if (0 == Globals.cl_vwep.value) break; // only one when vwep is off } // icon file ci.iconname = "/players/" + model_name + "/" + skin_name + "_i.pcx"; ci.icon = Globals.re.RegisterPic(ci.iconname); } // Load player model and weapon models. Globals.re.RegisterModel(model_filename, new AsyncCallback<Model>() { public void onSuccess(Model response) { ci.model = response; } public void onFailure(Throwable e) { // TODO(jgw): fail? } }); ci.weaponmodel = new Model[Constants.MAX_CLIENTWEAPONMODELS]; for (i = 0; i < num_weapon_filenames; ++i) { loadWeaponModel(ci, i, weapon_filenames[i]); } // must have loaded all data types to be valud if (ci.skin == null || ci.icon == null || ci.model == null || ci.weaponmodel[0] == null) { ci.skin = null; ci.icon = null; ci.model = null; ci.weaponmodel[0] = null; return; } } private static void loadWeaponModel(final ClientInfo ci, final int i, String weapon_filename) { Globals.re.RegisterModel(weapon_filename, new AsyncCallback<Model>() { public void onSuccess(Model response) { ci.weaponmodel[i] = response; } public void onFailure(Throwable e) { // TODO(jgw): fail? } }); } /* * ================ CL_ParseClientinfo * * Load the skin, icon, and model for a client ================ */ public static void ParseClientinfo(int player) { String s = Globals.cl.configstrings[player + Constants.CS_PLAYERSKINS]; ClientInfo ci = Globals.cl.clientinfo[player]; LoadClientinfo(ci, s); } /* * ================ CL_ParseConfigString ================ */ public static void ParseConfigString() { int i = Globals.net_message.getShort(); if (i < 0 || i >= Constants.MAX_CONFIGSTRINGS) Com.Error(Constants.ERR_DROP, "configstring > MAX_CONFIGSTRINGS"); String s = Buffers.getString(Globals.net_message); String olds = Globals.cl.configstrings[i]; Globals.cl.configstrings[i] = s; //Com.dprintln("ParseConfigString(): configstring[" + i + "]=<"+s+">"); // do something apropriate if (i >= Constants.CS_LIGHTS && i < Constants.CS_LIGHTS + Constants.MAX_LIGHTSTYLES) { ClientEffects.SetLightstyle(i - Constants.CS_LIGHTS); } else if (i >= Constants.CS_MODELS && i < Constants.CS_MODELS + Constants.MAX_MODELS) { if (Globals.cl.refresh_prepped) { // TODO(jgw): It looks to be oimpossible that an inline bsp model ("*1", etc) // could show up here referencing anything except the main bsp, which should // always be loaded by this point. If not, we should see a failure in // refexport_t.RegisterModel() or CM.InlineModel(). final int model_draw_index = i; Globals.re.RegisterModel(Globals.cl.configstrings[i], new AsyncCallback<Model>() { public void onSuccess(Model response) { Globals.cl.model_draw[model_draw_index - Constants.CS_MODELS] = response; } public void onFailure(Throwable e) { // TODO(jgw): fail? } }); if (Globals.cl.configstrings[i].startsWith("*")) Globals.cl.model_clip[i - Constants.CS_MODELS] = CM.InlineModel(Globals.cl.configstrings[i]); else Globals.cl.model_clip[i - Constants.CS_MODELS] = null; } } else if (i >= Constants.CS_SOUNDS && i < Constants.CS_SOUNDS + Constants.MAX_MODELS) { // if (Globals.cl.refresh_prepped) Globals.cl.sound_precache[i - Constants.CS_SOUNDS] = Sound .RegisterSound(Globals.cl.configstrings[i]); } else if (i >= Constants.CS_IMAGES && i < Constants.CS_IMAGES + Constants.MAX_MODELS) { if (Globals.cl.refresh_prepped) Globals.cl.image_precache[i - Constants.CS_IMAGES] = Globals.re .RegisterPic(Globals.cl.configstrings[i]); } else if (i >= Constants.CS_PLAYERSKINS && i < Constants.CS_PLAYERSKINS + Constants.MAX_CLIENTS) { if (Globals.cl.refresh_prepped && !olds.equals(s)) ParseClientinfo(i - Constants.CS_PLAYERSKINS); } } /* * ===================================================================== * * ACTION MESSAGES * * ===================================================================== */ private static final float[] pos_v = { 0, 0, 0 }; /* * ================== CL_ParseStartSoundPacket ================== */ public static void ParseStartSoundPacket() { int flags = Buffers.readUnsignedByte(Globals.net_message); int sound_num = Buffers.readUnsignedByte(Globals.net_message); float volume; if ((flags & Constants.SND_VOLUME) != 0) volume = Buffers.readUnsignedByte(Globals.net_message) / 255.0f; else volume = Constants.DEFAULT_SOUND_PACKET_VOLUME; float attenuation; if ((flags & Constants.SND_ATTENUATION) != 0) attenuation = Buffers.readUnsignedByte(Globals.net_message) / 64.0f; else attenuation = Constants.DEFAULT_SOUND_PACKET_ATTENUATION; float ofs; if ((flags & Constants.SND_OFFSET) != 0) ofs = Buffers.readUnsignedByte(Globals.net_message) / 1000.0f; else ofs = 0; int channel; int ent; if ((flags & Constants.SND_ENT) != 0) { // entity reletive channel = Globals.net_message.getShort(); ent = channel >> 3; if (ent > Constants.MAX_EDICTS) Com.Error(Constants.ERR_DROP, "CL_ParseStartSoundPacket: ent = " + ent); channel &= 7; } else { ent = 0; channel = 0; } float pos[]; if ((flags & Constants.SND_POS) != 0) { // positioned in space Buffers.getPos(Globals.net_message, pos_v); // is ok. sound driver copies pos = pos_v; } else // use entity number pos = null; if (null == Globals.cl.sound_precache[sound_num]) return; Sound.StartSound(pos, ent, channel, Globals.cl.sound_precache[sound_num], volume, attenuation, ofs); } public static void SHOWNET(String s) { if (Globals.cl_shownet.value >= 2) Com.Printf(Globals.net_message.readcount - 1 + ":" + s + "\n"); } /* * ===================== CL_ParseServerMessage ===================== */ public static void ParseServerMessage() { // // if recording demos, copy the message out // //if (cl_shownet.value == 1) //Com.Printf(net_message.cursize + " "); //else if (cl_shownet.value >= 2) //Com.Printf("------------------\n"); // // parse the message // while (true) { if (Globals.net_message.readcount > Globals.net_message.cursize) { Com.Error(Constants.ERR_FATAL, "CL_ParseServerMessage: Bad server message:"); break; } int cmd = Buffers.readUnsignedByte(Globals.net_message); if (cmd == -1) { SHOWNET("END OF MESSAGE"); break; } if (Globals.cl_shownet.value >= 2) { if (null == svc_strings[cmd]) Com.Printf(Globals.net_message.readcount - 1 + ":BAD CMD " + cmd + "\n"); else SHOWNET(svc_strings[cmd]); } // other commands switch (cmd) { default: Com.Error(Constants.ERR_DROP, "CL_ParseServerMessage: Illegible server message\n"); break; case Constants.svc_nop: // Com.Printf ("svc_nop\n"); break; case Constants.svc_disconnect: Com.Error(Constants.ERR_DISCONNECT, "Server disconnected\n"); break; case Constants.svc_reconnect: Com.Printf("Server disconnected, reconnecting\n"); if (Globals.cls.download != null) { //ZOID, close download try { Globals.cls.download.close(); } catch (IOException e) { } Globals.cls.download = null; } Globals.cls.state = Constants.ca_connecting; Globals.cls.connect_time = -99999; // CL_CheckForResend() will // fire immediately break; case Constants.svc_print: int i = Buffers.readUnsignedByte(Globals.net_message); if (i == Constants.PRINT_CHAT) { Sound.StartLocalSound("misc/talk.wav"); Globals.con.ormask = 128; } String msgText = Buffers.getString(Globals.net_message); Com.Printf(msgText); WebIntegration.onNetMessage(msgText); Globals.con.ormask = 0; break; case Constants.svc_centerprint: Screen.CenterPrint(Buffers.getString(Globals.net_message)); break; case Constants.svc_stufftext: String s = Buffers.getString(Globals.net_message); Com.DPrintf("stufftext: " + s + "\n"); CommandBuffer.AddText(s); break; case Constants.svc_serverdata: CommandBuffer.Execute(); // make sure any stuffed commands are done ParseServerData(); break; case Constants.svc_configstring: ParseConfigString(); break; case Constants.svc_sound: ParseStartSoundPacket(); break; case Constants.svc_spawnbaseline: ParseBaseline(); break; case Constants.svc_temp_entity: ClientTent.ParseTEnt(); break; case Constants.svc_muzzleflash: ClientEffects.ParseMuzzleFlash(); break; case Constants.svc_muzzleflash2: ClientEffects.ParseMuzzleFlash2(); break; case Constants.svc_download: // TODO(jgw): Ditch downloads entirely. throw new RuntimeException("SVC_DOWNLOAD"); case Constants.svc_frame: ClientEntities.ParseFrame(); break; case Constants.svc_inventory: ClientInventory.ParseInventory(); break; case Constants.svc_layout: Globals.cl.layout = Buffers.getString(Globals.net_message); break; case Constants.svc_playerinfo: case Constants.svc_packetentities: case Constants.svc_deltapacketentities: Com.Error(Constants.ERR_DROP, "Out of place frame data"); break; } } ClientView.AddNetgraph(); // // we don't know if it is ok to save a demo message until // after we have parsed the frame // if (Globals.cls.demorecording && !Globals.cls.demowaiting) Client.writeDemoMessage(); } }