Repository: fte-team/fteqw Branch: master Commit: 3584377302cd Files: 981 Total size: 27.0 MB Directory structure: gitextract_twr6mi4o/ ├── .forgejo/ │ └── workflows/ │ └── bin-linux64-release.yaml ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build_qc.sh ├── build_setup.sh ├── build_wip.sh ├── dist/ │ └── linux/ │ ├── com.riverbankcomputing.qscintilla.install.patch │ ├── linter-exceptions.json │ ├── org.fteqw.fteqw.desktop │ ├── org.fteqw.fteqw.fixdownloads.patch │ ├── org.fteqw.fteqw.fixhomedir.patch │ ├── org.fteqw.fteqw.metainfo.xml │ ├── org.fteqw.fteqw.sh │ └── org.fteqw.fteqw.yml ├── documentation/ │ ├── Building.md │ ├── Credits.md │ ├── Dependencies.md │ ├── QuickStart.md │ └── Tools.md ├── dounifdef.sh ├── engine/ │ ├── BSDmakefile │ ├── LICENSE │ ├── Makefile │ ├── README.MSVC │ ├── client/ │ │ ├── anorms.h │ │ ├── api_menu.h │ │ ├── bymorphed.h │ │ ├── cd_linux.c │ │ ├── cd_null.c │ │ ├── cd_sdl.c │ │ ├── cd_win.c │ │ ├── cdaudio.h │ │ ├── cl_cam.c │ │ ├── cl_demo.c │ │ ├── cl_ents.c │ │ ├── cl_ignore.c │ │ ├── cl_ignore.h │ │ ├── cl_input.c │ │ ├── cl_main.c │ │ ├── cl_master.h │ │ ├── cl_parse.c │ │ ├── cl_plugin.inc │ │ ├── cl_pred.c │ │ ├── cl_screen.c │ │ ├── cl_tent.c │ │ ├── clhl_game.c │ │ ├── client.h │ │ ├── clq2_cin.c │ │ ├── clq2_ents.c │ │ ├── console.c │ │ ├── fragstats.c │ │ ├── fte_eukara64.h │ │ ├── image.c │ │ ├── image_astc.h │ │ ├── in_generic.c │ │ ├── in_morphos.c │ │ ├── in_morphos.h │ │ ├── in_raw.h │ │ ├── in_sdl.c │ │ ├── in_win.c │ │ ├── input.h │ │ ├── keys.c │ │ ├── keys.h │ │ ├── lhfont.h │ │ ├── m_download.c │ │ ├── m_items.c │ │ ├── m_master.c │ │ ├── m_mp3.c │ │ ├── m_multi.c │ │ ├── m_native.c │ │ ├── m_options.c │ │ ├── m_script.c │ │ ├── m_single.c │ │ ├── menu.c │ │ ├── menu.h │ │ ├── merged.h │ │ ├── modelgen.h │ │ ├── net_master.c │ │ ├── p_classic.c │ │ ├── p_null.c │ │ ├── p_script.c │ │ ├── pr_clcmd.c │ │ ├── pr_csqc.c │ │ ├── pr_menu.c │ │ ├── pr_skelobj.c │ │ ├── q2anorms.h │ │ ├── q2m_flash.c │ │ ├── quake.manifest │ │ ├── quakedef.h │ │ ├── r_2d.c │ │ ├── r_d3.c │ │ ├── r_part.c │ │ ├── r_partset.c │ │ ├── r_partset.h │ │ ├── r_surf.c │ │ ├── render.h │ │ ├── renderer.c │ │ ├── renderque.c │ │ ├── renderque.h │ │ ├── resource.h │ │ ├── roq.h │ │ ├── roq_read.c │ │ ├── sbar.c │ │ ├── sbar.h │ │ ├── screen.h │ │ ├── skin.c │ │ ├── snd_al.c │ │ ├── snd_alsa.c │ │ ├── snd_directx.c │ │ ├── snd_dma.c │ │ ├── snd_droid.c │ │ ├── snd_linux.c │ │ ├── snd_macos.c │ │ ├── snd_mem.c │ │ ├── snd_mix.c │ │ ├── snd_morphos.c │ │ ├── snd_mp3.c │ │ ├── snd_opensl.c │ │ ├── snd_ov.c │ │ ├── snd_pulse.c │ │ ├── snd_sblaster.c │ │ ├── snd_sdl.c │ │ ├── snd_sndio.c │ │ ├── snd_wasapi.c │ │ ├── snd_win.c │ │ ├── snd_xaudio.c │ │ ├── sound.h │ │ ├── spritegn.h │ │ ├── sys_axfte.cpp │ │ ├── sys_dos.c │ │ ├── sys_droid.c │ │ ├── sys_linux.c │ │ ├── sys_morphos.c │ │ ├── sys_plugfte.c │ │ ├── sys_plugfte.h │ │ ├── sys_sdl.c │ │ ├── sys_win.c │ │ ├── sys_xdk.c │ │ ├── teamplay.c │ │ ├── textedit.c │ │ ├── valid.c │ │ ├── vid.h │ │ ├── vid_headless.c │ │ ├── view.c │ │ ├── view.h │ │ ├── vr.h │ │ ├── wad.c │ │ ├── wad.h │ │ ├── winamp.h │ │ ├── winquake.h │ │ ├── winquake.rc │ │ └── zqtp.c │ ├── common/ │ │ ├── bothdefs.h │ │ ├── bspfile.h │ │ ├── cmd.c │ │ ├── cmd.h │ │ ├── com_bih.c │ │ ├── com_bih.h │ │ ├── com_mesh.c │ │ ├── com_mesh.h │ │ ├── com_phys_bullet.cpp │ │ ├── com_phys_ode.c │ │ ├── common.c │ │ ├── common.h │ │ ├── config_freecs.h │ │ ├── config_fteqw.h │ │ ├── config_fteqw_noweb.h │ │ ├── config_minimal.h │ │ ├── config_nocompat.h │ │ ├── config_wastes.h │ │ ├── console.h │ │ ├── crc.c │ │ ├── cvar.c │ │ ├── cvar.h │ │ ├── fs.c │ │ ├── fs.h │ │ ├── fs_dzip.c │ │ ├── fs_pak.c │ │ ├── fs_stdio.c │ │ ├── fs_win32.c │ │ ├── fs_xz.c │ │ ├── fs_zip.c │ │ ├── gl_q2bsp.c │ │ ├── huff.c │ │ ├── json.c │ │ ├── log.c │ │ ├── mathlib.c │ │ ├── mathlib.h │ │ ├── md4.c │ │ ├── md5.c │ │ ├── net.h │ │ ├── net_chan.c │ │ ├── net_ice.c │ │ ├── net_ssl_gnutls.c │ │ ├── net_ssl_winsspi.c │ │ ├── net_wins.c │ │ ├── netinc.h │ │ ├── particles.h │ │ ├── plugin.c │ │ ├── pmove.c │ │ ├── pmove.h │ │ ├── pmovetst.c │ │ ├── pr_bgcmd.c │ │ ├── pr_common.h │ │ ├── protocol.h │ │ ├── q1bsp.c │ │ ├── q2pmove.c │ │ ├── q3api.h │ │ ├── qvm.c │ │ ├── sha1.c │ │ ├── sha2.c │ │ ├── sys.h │ │ ├── sys_linux_threads.c │ │ ├── sys_win_threads.c │ │ ├── translate.c │ │ ├── translate.h │ │ ├── ui_public.h │ │ ├── vm.h │ │ ├── world.h │ │ ├── zone.c │ │ └── zone.h │ ├── d3d/ │ │ ├── d3d11_backend.c │ │ ├── d3d11_image.c │ │ ├── d3d11_shader.c │ │ ├── d3d8_backend.c │ │ ├── d3d8_image.c │ │ ├── d3d_backend.c │ │ ├── d3d_image.c │ │ ├── d3d_shader.c │ │ ├── vid_d3d.c │ │ ├── vid_d3d11.c │ │ └── vid_d3d8.c │ ├── droid/ │ │ ├── AndroidManifest.xml │ │ ├── configs/ │ │ │ └── touch.cfg │ │ ├── default.fmf │ │ ├── fte.cfg │ │ ├── res/ │ │ │ ├── drawable/ │ │ │ │ ├── touch_attack.tga │ │ │ │ ├── touch_jump.tga │ │ │ │ ├── touch_moveback.tga │ │ │ │ ├── touch_moveforward.tga │ │ │ │ ├── touch_moveleft.tga │ │ │ │ ├── touch_moveright.tga │ │ │ │ ├── touch_turnleft.tga │ │ │ │ └── touch_turnright.tga │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── src/ │ │ └── com/ │ │ └── fteqw/ │ │ ├── FTEDroidActivity.java │ │ ├── FTEDroidEngine.java │ │ └── FTENativeActivity.java │ ├── gl/ │ │ ├── gl_alias.c │ │ ├── gl_backend.c │ │ ├── gl_bloom.c │ │ ├── gl_draw.c │ │ ├── gl_draw.h │ │ ├── gl_font.c │ │ ├── gl_heightmap.c │ │ ├── gl_hlmdl.c │ │ ├── gl_model.c │ │ ├── gl_model.h │ │ ├── gl_ngraph.c │ │ ├── gl_rlight.c │ │ ├── gl_rmain.c │ │ ├── gl_rmisc.c │ │ ├── gl_rsurf.c │ │ ├── gl_screen.c │ │ ├── gl_shader.c │ │ ├── gl_shadow.c │ │ ├── gl_terrain.h │ │ ├── gl_vidcocoa.m │ │ ├── gl_vidcommon.c │ │ ├── gl_viddroid.c │ │ ├── gl_videgl.c │ │ ├── gl_videgl.h │ │ ├── gl_vidlinuxglx.c │ │ ├── gl_vidmacos.c │ │ ├── gl_vidmorphos.c │ │ ├── gl_vidnt.c │ │ ├── gl_vidnull.c │ │ ├── gl_vidrpi.c │ │ ├── gl_vidsdl.c │ │ ├── gl_vidtinyglstubs.c │ │ ├── gl_vidwayland.c │ │ ├── gl_warp.c │ │ ├── gl_warp_sin.h │ │ ├── glmod_doom.c │ │ ├── glquake.h │ │ ├── glsupp.h │ │ ├── ltface.c │ │ ├── model_hl.h │ │ ├── r_bishaders.h │ │ └── shader.h │ ├── http/ │ │ ├── ftpclient.c │ │ ├── ftpserver.c │ │ ├── httpclient.c │ │ ├── httpserver.c │ │ ├── iweb.h │ │ ├── iwebiface.c │ │ └── webgen.c │ ├── makeconfig.sh │ ├── partcfgs/ │ │ ├── faithful.cfg │ │ ├── generatebuiltin.c │ │ ├── h2part.cfg │ │ ├── high.cfg │ │ ├── highfps.cfg │ │ ├── minimal.cfg │ │ ├── q2part.cfg │ │ ├── spikeset.cfg │ │ └── tsshaft.cfg │ ├── qclib/ │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── cmdlib.h │ │ ├── comprout.c │ │ ├── decomp.c │ │ ├── execloop.h │ │ ├── fteqcc.rc │ │ ├── gui.h │ │ ├── hash.c │ │ ├── hash.h │ │ ├── initlib.c │ │ ├── packager.c │ │ ├── pr_comp.h │ │ ├── pr_edict.c │ │ ├── pr_exec.c │ │ ├── pr_multi.c │ │ ├── pr_x86.c │ │ ├── progsint.h │ │ ├── progslib.h │ │ ├── progtype.h │ │ ├── qcc.h │ │ ├── qcc_cmdlib.c │ │ ├── qcc_pr_comp.c │ │ ├── qcc_pr_lex.c │ │ ├── qccgui.c │ │ ├── qccguiqt.cpp │ │ ├── qccguistuff.c │ │ ├── qccmain.c │ │ ├── qcctui.c │ │ ├── qcd.h │ │ ├── qcd_main.c │ │ ├── qcdecomp.c │ │ ├── readme.txt │ │ └── test.c │ ├── server/ │ │ ├── net_preparse.c │ │ ├── pr_cmds.c │ │ ├── pr_lua.c │ │ ├── pr_q1qvm.c │ │ ├── progdefs.h │ │ ├── progs.h │ │ ├── q2game.h │ │ ├── savegame.c │ │ ├── server.h │ │ ├── sqlite3.h │ │ ├── sv_ccmds.c │ │ ├── sv_chat.c │ │ ├── sv_cluster.c │ │ ├── sv_demo.c │ │ ├── sv_ents.c │ │ ├── sv_init.c │ │ ├── sv_main.c │ │ ├── sv_master.c │ │ ├── sv_move.c │ │ ├── sv_mvd.c │ │ ├── sv_nchan.c │ │ ├── sv_phys.c │ │ ├── sv_rankin.c │ │ ├── sv_send.c │ │ ├── sv_sql.c │ │ ├── sv_sql.h │ │ ├── sv_sys_unix.c │ │ ├── sv_sys_win.c │ │ ├── sv_user.c │ │ ├── svhl_game.c │ │ ├── svhl_gcapi.h │ │ ├── svhl_phys.c │ │ ├── svhl_world.c │ │ ├── svq2_ents.c │ │ ├── svq2_game.c │ │ └── world.c │ ├── shaders/ │ │ ├── Makefile │ │ ├── generatebuiltinsl.c │ │ ├── glsl/ │ │ │ ├── altwater.glsl │ │ │ ├── bloom_blur.glsl │ │ │ ├── bloom_filter.glsl │ │ │ ├── bloom_final.glsl │ │ │ ├── colourtint.glsl │ │ │ ├── crepuscular_opaque.glsl │ │ │ ├── crepuscular_rays.glsl │ │ │ ├── crepuscular_sky.glsl │ │ │ ├── default2d.glsl │ │ │ ├── default2danim.glsl │ │ │ ├── defaultadditivesprite.glsl │ │ │ ├── defaultfill.glsl │ │ │ ├── defaultgammacb.glsl │ │ │ ├── defaultskin.glsl │ │ │ ├── defaultsky.glsl │ │ │ ├── defaultskybox.glsl │ │ │ ├── defaultsprite.glsl │ │ │ ├── defaultwall.glsl │ │ │ ├── defaultwarp.glsl │ │ │ ├── depthonly.glsl │ │ │ ├── drawflat_wall.glsl │ │ │ ├── fixedemu.glsl │ │ │ ├── fxaa.glsl │ │ │ ├── itemtimer.glsl │ │ │ ├── lpp_depthnorm.glsl │ │ │ ├── lpp_light.glsl │ │ │ ├── lpp_wall.glsl │ │ │ ├── menutint.glsl │ │ │ ├── postproc_ascii.glsl │ │ │ ├── postproc_equirectangular.glsl │ │ │ ├── postproc_fisheye.glsl │ │ │ ├── postproc_laea.glsl │ │ │ ├── postproc_panini.glsl │ │ │ ├── postproc_panorama.glsl │ │ │ ├── postproc_stereographic.glsl │ │ │ ├── rtlight.glsl │ │ │ ├── terrain.glsl │ │ │ ├── underwaterwarp.glsl │ │ │ └── wireframe.glsl │ │ ├── hlsl11/ │ │ │ ├── default2d.hlsl │ │ │ ├── defaultfill.hlsl │ │ │ ├── defaultskin.hlsl │ │ │ ├── defaultsky.hlsl │ │ │ ├── defaultskybox.hlsl │ │ │ ├── defaultsprite.hlsl │ │ │ ├── defaultwall.hlsl │ │ │ ├── defaultwarp.hlsl │ │ │ ├── depthonly.hlsl │ │ │ ├── drawflat_wall.hlsl │ │ │ ├── fixedemu.hlsl │ │ │ ├── menutint.hlsl │ │ │ ├── rtlight.hlsl │ │ │ ├── terrain.hlsl │ │ │ └── terraindebug.hlsl │ │ ├── hlsl9/ │ │ │ ├── defaultskin.hlsl │ │ │ ├── defaultsky.hlsl │ │ │ ├── defaultskybox.hlsl │ │ │ ├── defaultwall.hlsl │ │ │ ├── defaultwarp.hlsl │ │ │ ├── drawflat_wall.hlsl │ │ │ ├── rtlight.hlsl │ │ │ └── terrain.hlsl │ │ ├── makevulkanblob.c │ │ └── vulkan/ │ │ ├── altwater.glsl │ │ ├── bloom_blur.glsl │ │ ├── bloom_filter.glsl │ │ ├── bloom_final.glsl │ │ ├── default2d.glsl │ │ ├── defaultadditivesprite.glsl │ │ ├── defaultfill.glsl │ │ ├── defaultgammacb.glsl │ │ ├── defaultskin.glsl │ │ ├── defaultsky.glsl │ │ ├── defaultskybox.glsl │ │ ├── defaultsprite.glsl │ │ ├── defaultwall.glsl │ │ ├── defaultwarp.glsl │ │ ├── depthonly.glsl │ │ ├── drawflat_wall.glsl │ │ ├── fixedemu.glsl │ │ ├── fixedemu_flat.glsl │ │ ├── fxaa.glsl │ │ ├── menutint.glsl │ │ ├── postproc_fisheye.glsl │ │ ├── postproc_panorama.glsl │ │ ├── postproc_stereographic.glsl │ │ ├── rtlight.glsl │ │ ├── sys/ │ │ │ ├── defs.h │ │ │ ├── fog.h │ │ │ ├── offsetmapping.h │ │ │ └── skeletal.h │ │ └── underwaterwarp.glsl │ ├── sw/ │ │ ├── sw.h │ │ ├── sw_backend.c │ │ ├── sw_image.c │ │ ├── sw_rast.c │ │ ├── sw_spans.h │ │ ├── sw_viddos.c │ │ └── sw_vidwin.c │ ├── vk/ │ │ ├── vk_backend.c │ │ ├── vk_init.c │ │ └── vkrenderer.h │ ├── web/ │ │ ├── fs_web.c │ │ ├── fte_pwa.json │ │ ├── fte_pwa_sw.js │ │ ├── ftejslib.h │ │ ├── ftejslib.js │ │ ├── fteshell.html │ │ ├── gl_vidweb.c │ │ ├── prejs.js │ │ └── sys_web.c │ └── xdk/ │ ├── FTEQW_XDK.sln │ ├── FTEQW_XDK.suo │ └── FTEQW_XDK.vcproj ├── fte.m4 ├── ftechrootbuild.sh ├── fteqtv/ │ ├── LICENSE │ ├── Makefile │ ├── bsd_string.h │ ├── bsp.c │ ├── cmd.h │ ├── control.c │ ├── crc.c │ ├── forward.c │ ├── httpsv.c │ ├── libqtvc/ │ │ ├── Makefile │ │ ├── glibc_sucks.c │ │ └── msvc_sucks.c │ ├── mdfour.c │ ├── menu.c │ ├── msg.c │ ├── net_qtv.h │ ├── netchan.c │ ├── nq_api.c │ ├── parse.c │ ├── pmove.c │ ├── protocol.h │ ├── qtv.h │ ├── qw.c │ ├── rcon.c │ ├── relay.c │ ├── sc_dsound.c │ ├── source.c │ └── sp_dsound.c ├── fteqw_readme.txt ├── ftetools_readme.txt ├── games/ │ ├── fortressone.fmf │ ├── freehl.fmf │ ├── hexen2-demo.fmf │ ├── ktx.fmf │ ├── quake-demo.fmf │ └── xonotic_85.fmf ├── imgtool.c ├── iqm/ │ ├── LICENSE │ ├── Makefile │ ├── Makefile.mingw │ ├── README.txt │ ├── iqm.cpp │ ├── iqm.h │ └── util.h ├── plugins/ │ ├── Makefile │ ├── avplug/ │ │ ├── avaudio.c │ │ ├── avdecode.c │ │ ├── avencode.c │ │ ├── msvc_libc/ │ │ │ ├── inttypes.h │ │ │ └── stdint.h │ │ └── readme.txt │ ├── berkelium/ │ │ ├── Makefile │ │ ├── plugapi.cpp │ │ └── readme.txt │ ├── botlib/ │ │ └── makebotlibdll.bat │ ├── bullet/ │ │ └── bulletplug.cpp │ ├── cef/ │ │ └── cef.c │ ├── cod/ │ │ ├── codbsp.c │ │ ├── codiwi.c │ │ ├── codmat.c │ │ └── codmod.c │ ├── emailnot/ │ │ ├── emailnot.q3asm │ │ ├── imapnoti.c │ │ ├── md5.c │ │ └── pop3noti.c │ ├── engine.h │ ├── ezhud/ │ │ ├── builtin_huds.h │ │ ├── ezquakeisms.c │ │ ├── ezquakeisms.h │ │ ├── hud.c │ │ ├── hud.h │ │ ├── hud_common.c │ │ ├── hud_common.h │ │ ├── hud_editor.c │ │ └── hud_editor.h │ ├── ezscript/ │ │ ├── ezscript.c │ │ └── ezscript.q3asm │ ├── hl2/ │ │ ├── Makefile │ │ ├── fs_gma.c │ │ ├── fs_vpk.c │ │ ├── fs_vpk_vtmb.c │ │ ├── glsl/ │ │ │ └── vmt/ │ │ │ ├── lightmapped.glsl │ │ │ ├── refract.glsl │ │ │ ├── rt.glsl │ │ │ ├── transition.glsl │ │ │ ├── unlit.glsl │ │ │ ├── vertexlit.glsl │ │ │ └── water.glsl │ │ ├── hl2.c │ │ ├── img_tth.c │ │ ├── img_vtf.c │ │ ├── mat_vmt.c │ │ ├── mat_vmt_progs.h │ │ ├── mod_hl2.c │ │ └── mod_vbsp.c │ ├── hud/ │ │ ├── qwui.q3asm │ │ └── ui_sbar.c │ ├── irc/ │ │ ├── ircclient.c │ │ ├── ircclient.q3asm │ │ └── raw codes.txt │ ├── jabber/ │ │ ├── jabbercl.q3asm │ │ ├── jabberclient.c │ │ ├── jingle.c │ │ ├── sift.c │ │ ├── xml.c │ │ ├── xml.h │ │ └── xmpp.h │ ├── models/ │ │ ├── draco.cpp │ │ ├── exportiqm.c │ │ ├── gltf.c │ │ └── models.c │ ├── mpq/ │ │ ├── blast.c │ │ ├── blast.h │ │ └── fs_mpq.c │ ├── namemaker/ │ │ ├── namemaker.c │ │ └── namemaker.q3asm │ ├── net_ssl_openssl.c │ ├── openxr.c │ ├── plugin.c │ ├── plugin.def │ ├── plugin.h │ ├── qi/ │ │ └── qi.c │ ├── quake3/ │ │ ├── botlib/ │ │ │ ├── aasfile.h │ │ │ ├── be_aas.h │ │ │ ├── be_aas_bsp.h │ │ │ ├── be_aas_bspq3.c │ │ │ ├── be_aas_cluster.c │ │ │ ├── be_aas_cluster.h │ │ │ ├── be_aas_debug.c │ │ │ ├── be_aas_debug.h │ │ │ ├── be_aas_def.h │ │ │ ├── be_aas_entity.c │ │ │ ├── be_aas_entity.h │ │ │ ├── be_aas_file.c │ │ │ ├── be_aas_file.h │ │ │ ├── be_aas_funcs.h │ │ │ ├── be_aas_main.c │ │ │ ├── be_aas_main.h │ │ │ ├── be_aas_move.c │ │ │ ├── be_aas_move.h │ │ │ ├── be_aas_optimize.c │ │ │ ├── be_aas_optimize.h │ │ │ ├── be_aas_reach.c │ │ │ ├── be_aas_reach.h │ │ │ ├── be_aas_route.c │ │ │ ├── be_aas_route.h │ │ │ ├── be_aas_routealt.c │ │ │ ├── be_aas_routealt.h │ │ │ ├── be_aas_sample.c │ │ │ ├── be_aas_sample.h │ │ │ ├── be_ai_char.c │ │ │ ├── be_ai_char.h │ │ │ ├── be_ai_chat.c │ │ │ ├── be_ai_chat.h │ │ │ ├── be_ai_gen.c │ │ │ ├── be_ai_gen.h │ │ │ ├── be_ai_goal.c │ │ │ ├── be_ai_goal.h │ │ │ ├── be_ai_move.c │ │ │ ├── be_ai_move.h │ │ │ ├── be_ai_weap.c │ │ │ ├── be_ai_weap.h │ │ │ ├── be_ai_weight.c │ │ │ ├── be_ai_weight.h │ │ │ ├── be_ea.c │ │ │ ├── be_ea.h │ │ │ ├── be_interface.c │ │ │ ├── be_interface.h │ │ │ ├── botlib.h │ │ │ ├── l_crc.c │ │ │ ├── l_crc.h │ │ │ ├── l_libvar.c │ │ │ ├── l_libvar.h │ │ │ ├── l_log.c │ │ │ ├── l_log.h │ │ │ ├── l_memory.c │ │ │ ├── l_memory.h │ │ │ ├── l_precomp.c │ │ │ ├── l_precomp.h │ │ │ ├── l_script.c │ │ │ ├── l_script.h │ │ │ ├── l_struct.c │ │ │ ├── l_struct.h │ │ │ ├── l_utils.h │ │ │ ├── q_platform.h │ │ │ ├── q_shared.h │ │ │ ├── standalone.c │ │ │ └── surfaceflags.h │ │ ├── clq3_cg.c │ │ ├── clq3_parse.c │ │ ├── clq3_ui.c │ │ ├── clq3defs.h │ │ ├── q3common.c │ │ ├── q3common.h │ │ ├── q3g_public.h │ │ └── svq3_game.c │ ├── serverb/ │ │ ├── cl_master.h │ │ ├── m_master.c │ │ └── net_master.c │ ├── spaceinv/ │ │ ├── spaceinv.c │ │ └── spaceinv.q3asm │ ├── terrorgen/ │ │ └── terragen.c │ ├── winamp/ │ │ ├── winamp.c │ │ └── winamp.h │ └── xsv/ │ ├── Xmd.h │ ├── Xproto.h │ ├── Xprotostr.h │ ├── bigreqstr.h │ ├── keysymdef.h │ ├── m_x.c │ ├── qux.h │ ├── x.h │ ├── x_reqs.c │ └── x_res.c ├── q3asm2/ │ ├── Makefile │ ├── opstrings.h │ └── q3asm2.c ├── quakec/ │ ├── autoext/ │ │ ├── lists/ │ │ │ └── extlist.txt │ │ └── src/ │ │ ├── autoext.qc │ │ ├── builtins.qc │ │ ├── defs.qc │ │ └── progs.src │ ├── basemod/ │ │ ├── ai.qc │ │ ├── basemod.txt │ │ ├── boss.qc │ │ ├── buttons.qc │ │ ├── client.qc │ │ ├── combat.qc │ │ ├── defs.qc │ │ ├── demon.qc │ │ ├── dog.qc │ │ ├── doors.qc │ │ ├── effects.qc │ │ ├── enforcer.qc │ │ ├── engine.qc │ │ ├── fight.qc │ │ ├── fish.qc │ │ ├── hknight.qc │ │ ├── items.qc │ │ ├── knight.qc │ │ ├── misc.qc │ │ ├── monsters.qc │ │ ├── nomonst.qc │ │ ├── obituary.qc │ │ ├── ogre.qc │ │ ├── oldone.qc │ │ ├── plats.qc │ │ ├── player.qc │ │ ├── progs.src │ │ ├── proj.qc │ │ ├── replace.qc │ │ ├── shalrath.qc │ │ ├── shambler.qc │ │ ├── soldier.qc │ │ ├── spectate.qc │ │ ├── sprites.qc │ │ ├── subs.qc │ │ ├── tarbaby.qc │ │ ├── triggers.qc │ │ ├── weapons.qc │ │ ├── wizard.qc │ │ ├── world.qc │ │ └── zombie.qc │ ├── csaddon/ │ │ └── src/ │ │ ├── brush_draw.qc │ │ ├── brush_history.qc │ │ ├── brush_manip.qc │ │ ├── brush_selection.qc │ │ ├── brush_vertedit.qc │ │ ├── cam.qc │ │ ├── csaddon.qc │ │ ├── csaddon.src │ │ ├── csfixups.qc │ │ ├── csplat.qc │ │ ├── editor_brushes.qc │ │ ├── editor_ents.qc │ │ ├── editor_lights.qc │ │ ├── editor_particles.qc │ │ ├── editor_terrain.qc │ │ ├── menu.qc │ │ └── textfield.qc │ ├── csqctest/ │ │ └── src/ │ │ ├── common/ │ │ │ ├── classes.qc │ │ │ ├── econstants.qc │ │ │ ├── makeallstatic.qc │ │ │ ├── mconstants.qc │ │ │ └── pmove.qc │ │ ├── cs/ │ │ │ ├── constants.qc │ │ │ ├── defs.qc │ │ │ ├── editor_lights.qc │ │ │ ├── editor_terrain.qc │ │ │ ├── entrypoints.qc │ │ │ ├── fun/ │ │ │ │ ├── osgk.qc │ │ │ │ ├── skinchooser.qc │ │ │ │ └── tetris.qc │ │ │ ├── hlpm.qc │ │ │ ├── hud.qc │ │ │ ├── map.qc │ │ │ ├── menu.qc │ │ │ ├── movetypes.qc │ │ │ ├── player.qc │ │ │ ├── playerframes.inc │ │ │ ├── prediction.qc │ │ │ ├── q3playerm.qc │ │ │ ├── q4player.qc │ │ │ ├── tempent.qc │ │ │ ├── test.qc │ │ │ └── weapons.qc │ │ ├── csprogs.src │ │ ├── optsall.qc │ │ ├── optsmenu.qc │ │ ├── progs.src │ │ ├── ss/ │ │ │ ├── ai.qc │ │ │ ├── boss.qc │ │ │ ├── buttons.qc │ │ │ ├── client.qc │ │ │ ├── combat.qc │ │ │ ├── defs.qc │ │ │ ├── demon.qc │ │ │ ├── dog.qc │ │ │ ├── doors.qc │ │ │ ├── enforcer.qc │ │ │ ├── fight.qc │ │ │ ├── fish.qc │ │ │ ├── flag.qc │ │ │ ├── hknight.qc │ │ │ ├── items.qc │ │ │ ├── knight.qc │ │ │ ├── misc.qc │ │ │ ├── monsters.qc │ │ │ ├── ogre.qc │ │ │ ├── oldone.qc │ │ │ ├── plats.qc │ │ │ ├── player.qc │ │ │ ├── shalrath.qc │ │ │ ├── shambler.qc │ │ │ ├── soldier.qc │ │ │ ├── subs.qc │ │ │ ├── tarbaby.qc │ │ │ ├── triggers.qc │ │ │ ├── weapons.qc │ │ │ ├── wizard.qc │ │ │ ├── world.qc │ │ │ └── zombie.qc │ │ └── ssqc.src │ ├── dpsymbols.src │ ├── fallout2/ │ │ ├── ai.qc │ │ ├── boss.qc │ │ ├── buttons.qc │ │ ├── client.qc │ │ ├── cmds.qc │ │ ├── combat.qc │ │ ├── csprogs.src │ │ ├── csqc/ │ │ │ ├── builtins.qc │ │ │ ├── constants.qc │ │ │ ├── invent.qc │ │ │ ├── main.qc │ │ │ ├── stdbuiltins.qc │ │ │ ├── stdconstants.qc │ │ │ └── system.qc │ │ ├── defs.qc │ │ ├── demon.qc │ │ ├── dog.qc │ │ ├── doors.qc │ │ ├── enforcer.qc │ │ ├── fight.qc │ │ ├── fish.qc │ │ ├── hknight.qc │ │ ├── hos.qc │ │ ├── inventory.qc │ │ ├── items.qc │ │ ├── knight.qc │ │ ├── menus.qc │ │ ├── misc.qc │ │ ├── mod_buy.qc │ │ ├── mod_menus.qc │ │ ├── mod_other.qc │ │ ├── modbuy.qc │ │ ├── models.qc │ │ ├── monsters.qc │ │ ├── name.qc │ │ ├── ogre.qc │ │ ├── plats.qc │ │ ├── player.qc │ │ ├── progs.src │ │ ├── server.qc │ │ ├── shalrath.qc │ │ ├── shambler.qc │ │ ├── soldier.qc │ │ ├── spectate.qc │ │ ├── sprites.qc │ │ ├── subs.qc │ │ ├── tf.qc │ │ ├── triggers.qc │ │ ├── turrets.qc │ │ ├── weapons.qc │ │ ├── wizard.qc │ │ ├── world.qc │ │ └── zombie.qc │ └── menusys/ │ ├── README.TXT │ ├── cs/ │ │ └── entrypoints.qc │ ├── csprogs.src │ ├── fteextensions.qc │ ├── menu/ │ │ ├── cvars.qc │ │ ├── loadsave.qc │ │ ├── main.qc │ │ ├── mods.qc │ │ ├── newgame.qc │ │ ├── options.qc │ │ ├── options_audio.qc │ │ ├── options_basic.qc │ │ ├── options_configs.qc │ │ ├── options_effects.qc │ │ ├── options_hud.qc │ │ ├── options_keys.qc │ │ ├── options_particles.qc │ │ ├── options_video.qc │ │ ├── presets.qc │ │ ├── quit.qc │ │ ├── servers.qc │ │ └── updates.qc │ ├── menu.src │ └── menusys/ │ ├── mitem_bind.qc │ ├── mitem_checkbox.qc │ ├── mitem_colours.qc │ ├── mitem_combo.qc │ ├── mitem_console.qc │ ├── mitem_desktop.qc │ ├── mitem_edittext.qc │ ├── mitem_exmenu.qc │ ├── mitem_frame.qc │ ├── mitem_grid.qc │ ├── mitem_menu.qc │ ├── mitem_slider.qc │ ├── mitem_spinnymodel.qc │ ├── mitem_tabs.qc │ ├── mitems.qc │ ├── mitems_common.qc │ └── readme.txt └── specs/ ├── antilag.txt ├── browser.txt ├── browserexample.html ├── bspx.txt ├── changelevels.md ├── chunkeddownloads.txt ├── compile farm scripts/ │ ├── linux/ │ │ ├── .bitchxrc │ │ ├── build.sh │ │ ├── build_ccache.sh │ │ ├── build_fteqcc.sh │ │ ├── build_fteqtv.sh │ │ ├── build_wip.sh │ │ ├── build_wip_ccache.sh │ │ ├── build_wip_fteqcc.sh │ │ ├── build_wip_fteqtv.sh │ │ ├── buildnumber.sh │ │ ├── ccache-alias.sh │ │ ├── count.sh │ │ ├── count_wip.sh │ │ ├── svninfo.sh │ │ └── svninfo_wip.sh │ └── macosx/ │ └── build.sh ├── console.txt ├── csqc_for_idiots.txt ├── distort.pk3 ├── distort.txt ├── example.shader ├── ext_csqc_1.txt ├── fonts.txt ├── fte_manifests.txt ├── glsl.md ├── hosting.txt ├── mapcluster.txt ├── modmaking.txt ├── multiprogs.txt ├── particles.txt ├── qc_extensions.txt ├── replacementdeltas.txt ├── rotating-brushes.md ├── rtlights.txt ├── scriptable menus.txt ├── skeletal.txt ├── spawning-entities-from-external-bsps.md ├── splitscreen.txt ├── videocapture.txt └── viewmodels.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .forgejo/workflows/bin-linux64-release.yaml ================================================ name: bin-linux64-release on: push: branches: [ master ] jobs: build: runs-on: docker steps: - name: clone src uses: actions/checkout@v3 with: fetch-depth: 1 - name: apt-get dependencies run: | apt-get -qq update apt-get -qq install --no-install-recommends \ zip \ gettext \ libasound2-dev \ libgnutls28-dev \ libopenxr-dev \ libqscintilla2-qt5-dev \ libsdl2-dev \ libvulkan-dev \ libwayland-dev \ libxcursor-dev \ libxkbcommon-dev \ libxrandr-dev \ qtbase5-dev - name: makelibs run: cd engine && make makelibs FTE_TARGET=linux64 - name: build fteqw run: cd engine && make m-rel FTE_TARGET=linux64 BRANDFLAGS="-DLINK_EZHUD=1" - name: build fteqcc run: cd engine && make qcc-rel FTE_TARGET=linux64 - name: build dedicated-server run: cd engine && make sv-rel FTE_TARGET=linux64 - name: build plugins run: cd engine && make plugins-rel FTE_TARGET=linux64 NATIVE_PLUGINS="ode cod ezhud hl2 irc models xmpp openxr openssl quake3 qi" - name: build imgtool run: cd engine && make imgtool FTE_TARGET=linux64 - name: build iqmtool run: cd engine && make iqmtool FTE_TARGET=linux64 - name: upload release uses: actions/forgejo-release@4d26949b75e208a9d85204fdd1d6685af0f876a8 with: direction: upload release-dir: ./engine/release token: ${{ secrets.TOKEN }} override: true ================================================ FILE: .github/workflows/main.yml ================================================ name: main on: [ push, pull_request, workflow_dispatch ] jobs: cmake: strategy: matrix: os: [ ubuntu-latest ] profile: [ "Debug", "Release" ] runs-on: ${{ matrix.os }} steps: - name: Get number of CPU cores uses: SimenB/github-actions-cpu-cores@v2 id: cpu-cores - uses: actions/checkout@v4 - name: "Install Dependencies" run: | sudo apt-get -qq update sudo apt-get -qq install --no-install-recommends \ gettext \ libasound2-dev \ libbullet-dev \ libgnutls28-dev \ libopenxr-dev \ libqscintilla2-qt5-dev \ libsdl2-dev \ libpng-dev \ libvorbis-dev \ libvulkan-dev \ libwayland-dev \ libxcursor-dev \ libxkbcommon-dev \ libxrandr-dev \ qtbase5-dev # Disabled due to warnings that break the debug build with -Werror # libode-dev # libavcodec-dev libavformat-dev libavutil-dev libswscale-dev - name: "Configure: ${{ matrix.profile }}" run: | cmake -B build_${{ matrix.profile }} -DCMAKE_BUILD_TYPE=${{ matrix.profile }} - name: "Build: ${{ matrix.profile }}" run: | cmake --build build_${{ matrix.profile }} --parallel ${{ steps.cpu-cores.outputs.count }} make: strategy: matrix: include: - name: web fte_target: web make_targets: "gl-rel" os: ubuntu-latest - name: linux64 fte_target: linux64 make_targets: "m-rel sv-rel qtv-rel qcc-rel" os: ubuntu-latest packages: "libpng-dev libasound2-dev libgl-dev libegl1-mesa-dev libwayland-dev libxcursor-dev libxi-dev libxkbcommon-dev libxrandr-dev libxss-dev" - name: win32 fte_target: win32 make_targets: "m-rel sv-rel qcc-rel" os: ubuntu-latest packages: "binutils-mingw-w64-i686 gcc-mingw-w64-i686 g++-mingw-w64-i686" - name: win64 fte_target: win64 make_targets: "m-rel sv-rel qcc-rel" os: ubuntu-latest packages: "binutils-mingw-w64-x86-64 gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64" - name: macos-arm64 fte_target: SDL2 make_targets: "gl-rel sv-rel qcc-rel" args: "ARCH=arm STRIPFLAGS=" packages: "sdl2" os: macos-latest name: make-${{ matrix.name }} runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - name: Get number of CPU cores uses: SimenB/github-actions-cpu-cores@v2 id: cpu-cores - uses: actions/checkout@v4 - name: Extract versions run: grep 'VER=' engine/Makefile | tee makelibs_versions - uses: actions/cache@v4 id: cache with: path: engine/libs-* key: ${{ runner.os }}-${{ matrix.fte_target }}-${{ hashFiles('makelibs_versions') }} - uses: mymindstorm/setup-emsdk@v14 with: version: "2.0.12" actions-cache-folder: "emsdk-cache-2.0.12" cache-key: "emsdk-2.0.12" if: matrix.fte_target == 'web' - name: Verify emscripten run: emcc -v if: matrix.fte_target == 'web' - name: Install dependencies (linux) run: sudo apt-get -qq update && sudo apt-get -qq install --no-install-recommends ${{ matrix.packages }} if: matrix.packages != '' && matrix.os == 'ubuntu-latest' - name: Install dependencies (macos) run: brew install ${{ matrix.packages }} if: matrix.packages != '' && matrix.os == 'macos-latest' - name: Build dependencies working-directory: engine run: make FTE_TARGET=${{ matrix.fte_target }} makelibs ${{ matrix.args }} if: steps.cache.outputs.cache-hit != 'true' - uses: ammaraskar/gcc-problem-matcher@0.3.0 - name: Build ${{ matrix.name }} working-directory: engine run: | make -j ${{ steps.cpu-cores.outputs.count }} FTE_TARGET=${{ matrix.fte_target }} ${{ matrix.make_targets }} ${{ matrix.args }} LINK_EZHUD=1 LINK_OPENSSL=1 - name: Attach macOS docs run: | cat < engine/release/fte_macos.txt To allow executables to run issue for example: chmod +x fteqw-glsdl2 xattr -d com.apple.quarantine fteqw-glsdl2 If you don't have SDL2 installed, run: brew install sdl2 EOF if: matrix.os == 'macos-latest' - name: Get version id: version run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: fteqw-${{ matrix.name }}-${{ steps.version.outputs.short_sha }} path: | engine/release/fte* !engine/release/*.db compression-level: 9 flatpak: name: make-flatpak-${{ matrix.arch.name }} timeout-minutes: 30 strategy: matrix: arch: - name: x86_64 runner: ubuntu-latest - name: aarch64 runner: ubuntu-24.04-arm fail-fast: false runs-on: ${{ matrix.arch.runner }} container: image: ghcr.io/flathub-infra/flatpak-github-actions:kde-5.15-23.08 options: --privileged steps: - name: Checkout uses: actions/checkout@v4 - name: Lint metadata and build manifest run: | echo "::group::Lint metainfo" flatpak-builder-lint \ --user-exceptions=dist/linux/linter-exceptions.json --exceptions \ appstream ./dist/linux/org.fteqw.fteqw.metainfo.xml \ || export FLATPAK_LINT_FAILED=1 echo "::endgroup::" echo "::group::Lint Flatpak manifest" flatpak-builder-lint \ --user-exceptions=dist/linux/linter-exceptions.json --exceptions \ manifest ./dist/linux/org.fteqw.fteqw.yml \ || export FLATPAK_LINT_FAILED=1 echo "::endgroup::" if [[ ! -z "$FLATPAK_LINT_FAILED" ]]; then echo "::error::Metadata linting failed" exit 1 fi git config --global --add safe.directory $(pwd) echo "SHORT_SHA=`git rev-parse --short HEAD`" >> $GITHUB_ENV - name: Build flatpak uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: org.fteqw.fteqw.${{ env.SHORT_SHA }}.flatpak manifest-path: dist/linux/org.fteqw.fteqw.yml cache-key: flatpak-builder-${{ matrix.arch }}-${{ env.SHORT_SHA }} arch: ${{ matrix.arch.name }} - name: Lint build directory run: | flatpak-builder-lint \ --user-exceptions=dist/linux/linter-exceptions.json --exceptions \ builddir flatpak_app ================================================ FILE: .gitignore ================================================ #misc build artifacts build *.o *.o.d engine/debug/ engine/release/ engine/libs-*/ engine/bullet3-*/ engine/*.tar.gz engine/shaders/generatebuiltinsl engine/shaders/makevulkanblob engine/shaders/vulkanblobs/ plugins/cef/cef_linux64/ plugins/cef/cef_windows64/ #quakec build artifacts... quakec/csaddon/csaddon.dat quakec/csaddon/csaddon.lno quakec/csqctest/csprogs.dat quakec/csqctest/csprogs.lno quakec/csqctest/progs.dat quakec/csqctest/progs.lno quakec/menu.dat quakec/menu.lno # clangd artifacts compile_commands.json .cache/ #for qtcreator users. CMakeLists.txt.user # android target has a load of debris engine/droid/bin/ engine/droid/gen/ engine/droid/libs/ engine/droid/default.properties engine/droid/local.properties engine/droid/proguard.cfg engine/droid/build.xml engine/droid/ftekeystore engine/droid/proguard-project.txt engine/droid/project.properties # Flatpak build artifacts .flatpak-builder ================================================ FILE: CMakeLists.txt ================================================ #Note: this file was made primarily to support msvc and its project file incompatibilities nightmare. #Its also useful for various other IDEs like QtCreator etc. #It uses system libraries, so it will have dependancy issues with public releases where those dependancies are distro/version-specific. #Public builds are still built using the (overcomplicated) traditional (g)makefile. CMAKE_MINIMUM_REQUIRED(VERSION 3.0...3.10) cmake_policy(SET CMP0063 NEW) PROJECT(FTEQuake) INCLUDE_DIRECTORIES( engine/common engine/client engine/qclib engine/gl engine/server engine ) IF (EXISTS ${CMAKE_SOURCE_DIR}/.svn AND NOT DEFINED FTE_REVISON) EXECUTE_PROCESS(COMMAND "svnversion" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE FTE_REVISON ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) IF (DEFINED FTE_REVISON) MESSAGE(STATUS "FTE SVN Revision ${FTE_REVISON}") IF(FTE_REVISON MATCHES "M") MESSAGE(STATUS "--- PRIVATE CHANGES DETECTED ---") SET(FTE_REVISON SVNREVISION=${FTE_REVISON}) ELSE() MESSAGE(STATUS "No local changes") EXECUTE_PROCESS(COMMAND svn info --show-item last-changed-date --no-newline WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE FTE_DATE ) SET(FTE_REVISON SVNREVISION=${FTE_REVISON} SVNDATE=${FTE_DATE}) ENDIF() ENDIF() ENDIF() IF (EXISTS ${CMAKE_SOURCE_DIR}/.git AND NOT DEFINED FTE_REVISON) EXECUTE_PROCESS(COMMAND git describe --always --long --dirty WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE FTE_REVISON_GIT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) EXECUTE_PROCESS(COMMAND git log -1 --format=%cs WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE FTE_DATE ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) EXECUTE_PROCESS(COMMAND git branch --show-current WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE FTE_BRANCH ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) EXECUTE_PROCESS(COMMAND git rev-parse --is-shallow-repository WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE FTE_GIT_IS_SHALLOW ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) IF(FTE_GIT_IS_SHALLOW STREQUAL true) MESSAGE(STATUS "shallow clone prevents calculation of revision number.") SET(SVNREVISION "git-${FTE_REVISON_GIT}") #if its a shallow clone then we can't count commits properly so don't know what revision we actually are. ELSE() EXECUTE_PROCESS(COMMAND git rev-list HEAD --count WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE SVNREVISION ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) MATH(EXPR SVNREVISION "${SVNREVISION} + 29") #not all svn commits managed to appear im the git repo, so we have a small bias to keep things consistent. IF (FTE_BRANCH STREQUAL "master" OR FTE_BRANCH STREQUAL "") SET(SVNREVISION "${SVNREVISION}-git-${FTE_REVISON_GIT}") ELSE() SET(SVNREVISION "${FTE_BRANCH}-${SVNREVISION}-git-${FTE_REVISON_GIT}") #weird branches get a different form of revision, to reduce confusion. ENDIF() ENDIF() MESSAGE(STATUS "FTE GIT ${FTE_BRANCH} Revision ${SVNREVISION}, ${FTE_DATE}") SET(FTE_REVISON SVNREVISION=${SVNREVISION} SVNDATE=${FTE_DATE} FTE_BRANCH=${FTE_BRANCH}) ENDIF() #plugins need visibility hidden in order to avoid conflicts with function names that match the engine. #this is consistent with how windows works so no great loss. #plus it means that gcc can inline more (with LTO), including optimising args. set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_C_VISIBILITY_PRESET hidden) IF(${CMAKE_VERSION} VERSION_LESS "3.9.0") MESSAGE(STATUS "no LTO - old cmake.") ELSE() cmake_policy(SET CMP0069 NEW) IF(NOT CMAKE_BUILD_TYPE MATCHES "Debug") #use LTO where possible. reportedly requires cmake 3.9 to actually work INCLUDE(CheckIPOSupported) check_ipo_supported(RESULT result) IF(result) SET(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) MESSAGE(STATUS "Using LTO.") ELSE() MESSAGE(STATUS "no LTO - not supported.") ENDIF() ELSE() MESSAGE(STATUS "no LTO - debug.") ENDIF() ENDIF() # Added these to solve some build issues I ran into - Brad IF(FTE_BIG_ENDIAN) ADD_DEFINITIONS(-DFTE_BIG_ENDIAN) ENDIF() IF(FTE_LITTLE_ENDIAN) ADD_DEFINITIONS(-DFTE_LITTLE_ENDIAN) ENDIF() # libepoll-shim needs to be installed on the BSDs and Mac OSX to get # some of the server code to compile and work correctly on those platforms - Brad IF(CMAKE_SYSTEM_NAME MATCHES "BSD" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") INCLUDE(FetchContent) FetchContent_Declare( epoll-shim GIT_REPOSITORY https://github.com/jiixyj/epoll-shim.git GIT_TAG master ) SET(epoll-shim BUILD_TESTS OFF CACHE INTERNAL "") SET(epoll-shim BUILD_SHARED_LIBS OFF CACHE INTERNAL "") FetchContent_MakeAvailable(epoll-shim) SET(EPOLL_INC_DIR "${epoll-shim_SOURCE_DIR}/include") ENDIF() SET(FTE_BUILD_CONFIG ${PROJECT_SOURCE_DIR}/engine/common/config_fteqw.h CACHE FILEPATH "Which build config file to use to control supported features.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};CONFIG_FILE_NAME=${FTE_BUILD_CONFIG}) SET(FTE_USE_SDL false CACHE BOOL "Force the use of SDL2 instead of using native builds.") INCLUDE(GNUInstallDirs) SET(FTE_INSTALL_BINDIR games CACHE STRING "Binary dir to install to.") SET(FTE_INSTALL_LIBDIR fteqw CACHE STRING "Binary dir to install to.") IF(NOT WIN32) SET(SYS_LIBS ${SYS_LIBS} m) ELSE() SET(SYS_LIBS ${SYS_LIBS}) ENDIF() SET(FTE_DEP_ZLIB true CACHE BOOL "Link against zlib.") IF(FTE_DEP_ZLIB) FIND_PACKAGE(ZLIB) ENDIF() IF(ZLIB_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};ZLIB_STATIC) SET(FTE_LIBS ${FTE_LIBS} ${ZLIB_LIBRARIES}) SET(FTESV_LIBS ${FTESV_LIBS} ${ZLIB_LIBRARIES}) SET(FTEQCC_LIBS ${FTEQCC_LIBS} ${ZLIB_LIBRARIES}) SET(FTEQTV_LIBS ${FTEQTV_LIBS} ${ZLIB_LIBRARIES}) ELSE() MESSAGE(WARNING "libz library NOT available. compressed pk3, ICE, Q2E, etc etc, yada yada, blah blah will not be available.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_ZLIB) ENDIF() SET(FTE_DEP_BZIP2 true CACHE BOOL "Link against libbzip2.") IF(FTE_DEP_BZIP2) FIND_PACKAGE(BZip2) ENDIF() IF(BZIP2_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};AVAIL_BZLIB;BZLIB_STATIC) SET(FTE_LIBS ${FTE_LIBS} bz2) SET(FTESV_LIBS ${FTESV_LIBS} bz2) # MESSAGE(STATUS "bzip2 library found. bz2-compressed pk3s will work for the price of extra bloat! yay!") ELSE() MESSAGE(WARNING "bzip2 library NOT available. bz2-compressed pk3s will not be available, as if anyone cares.") ENDIF() SET(OpenGL_GL_PREFERENCE LEGACY) FIND_PACKAGE(OpenGL) IF(OpenGL_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};GLQUAKE) ELSE() MESSAGE(WARNING "opengl library NOT available. Will depend upon vulkan.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_OPENGL) ENDIF() SET(FTE_DEP_JPEG true CACHE BOOL "Link against libjpeg.") IF(FTE_DEP_JPEG) FIND_PACKAGE(JPEG) ENDIF() IF(JPEG_FOUND) INCLUDE_DIRECTORIES( ${JPEG_INCLUDE_DIRS} ) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};LIBJPEG_STATIC) SET(FTE_LIBS ${FTE_LIBS} ${JPEG_LIBRARIES}) ELSE() MESSAGE(WARNING "libjpeg library NOT available. Who cares?") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_JPEG) SET(JPEG_LIBRARIES) ENDIF() IF(NOT ${WIN32}) SET(FTE_DEP_DBUS true CACHE BOOL "Link against libdbus.") IF(FTE_DEP_DBUS) FIND_PACKAGE(DBus1) ENDIF() IF(DBUS1_FOUND) INCLUDE_DIRECTORIES( ${DBus1_INCLUDE_DIRS} ) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};HAVE_DBUS) SET(FTE_LIBS ${FTE_LIBS} ${DBus1_LIBRARIES}) ELSE() MESSAGE(WARNING "libdbus-1 library NOT available. Who cares?") ENDIF() ENDIF() SET(FTE_DEP_PNG true CACHE BOOL "Link against libpng.") IF(FTE_DEP_PNG) FIND_PACKAGE(PNG) ENDIF() IF(PNG_FOUND) INCLUDE_DIRECTORIES( ${PNG_INCLUDE_DIRS} ) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};LIBPNG_STATIC) SET(FTE_LIBS ${FTE_LIBS} ${PNG_LIBRARIES}) ELSE() MESSAGE(WARNING "libpng library NOT available. Good luck with screenshots.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_PNG) SET(PNG_LIBRARIES) ENDIF() SET(FTE_DEP_FREETYPE true CACHE BOOL "Link against libfreetype.") IF(FTE_DEP_FREETYPE) FIND_PACKAGE(Freetype) ENDIF() IF(FREETYPE_FOUND) INCLUDE_DIRECTORIES( ${FREETYPE_INCLUDE_DIRS} ) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};LIBFREETYPE_STATIC) SET(FTE_LIBS ${FTE_LIBS} ${FREETYPE_LIBRARIES}) FIND_PACKAGE(Fontconfig) IF(Fontconfig_FOUND) INCLUDE_DIRECTORIES( ${Fontconfig_INCLUDE_DIRS} ) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};LIBFONTCONFIG_STATIC) SET(FTE_LIBS ${FTE_LIBS} ${Fontconfig_LIBRARIES}) ELSE() MESSAGE(WARNING "fontconfig library NOT available. I hope you're not using any system fonts.") ENDIF() ELSE() MESSAGE(WARNING "freetype library NOT available. I hope you're okay with ascii.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_FREETYPE) ENDIF() #this is just for headers. FIND_PATH(VULKAN_INCLUDE_DIR vulkan/vulkan.h) IF(VULKAN_INCLUDE_DIR) INCLUDE_DIRECTORIES( ${VULKAN_INCLUDE_DIR} ) SET(FTE_DEFINES ${FTE_DEFINES};VKQUAKE) #no libs required, thankfully ELSE() MESSAGE(WARNING "Vulkan headers NOT available.") ENDIF() SET(FTE_DEP_VORBISFILE true CACHE BOOL "Link against libvorbisfile.") IF(FTE_DEP_VORBISFILE) FIND_LIBRARY(VORBISFILE_LIBRARY NAMES vorbisfile) ENDIF() IF(NOT VORBISFILE_LIBRARY) INCLUDE_DIRECTORIES( ${VORBISFILE_INCLUDE_DIRS} ) MESSAGE(WARNING "libvorbisfile library NOT available. Who listens to the bgm anyway?") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_OGG) ENDIF() IF(CMAKE_BUILD_TYPE MATCHES "Debug") SET(FTE_WERROR true CACHE BOOL "Warnings as errors.") ELSE() SET(FTE_WERROR false CACHE BOOL "Warnings as errors.") ENDIF() IF(FTE_WERROR) SET(FTE_WERROR_ARG "-Werror") ELSE() SET(FTE_WERROR_ARG "") ENDIF() IF(CMAKE_C_COMPILER_ID MATCHES "Clang") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-sign") IF(CMAKE_BUILD_TYPE MATCHES "Debug") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall ${FTE_WERROR_ARG} -Wno-pointer-sign -Wno-unknown-pragmas -Wno-format-zero-length -Wno-strict-aliasing -Wno-error=cpp") ELSE() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 ${FTE_WERROR_ARG}") ENDIF() endif() IF(CMAKE_C_COMPILER_ID MATCHES "GNU") SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wstrict-prototypes") # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wold-style-definition") #k&r c is weird and can't cope with 64bit types. SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wmissing-parameter-type") #k&r c is weird and can't cope with 64bit types. SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wold-style-declaration") # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wpointer-arith") #void* stuff SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wvla") #msvc doesn't support vla SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wdeclaration-after-statement") #msvc doesn't allow defs after statements, and they're so very tempting... set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wformat-truncation=1") #TODO SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wmissing-prototypes") #for finding missing statics. #SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-function") # # set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wswitch-enum") #to warn about omitted enums despite default. #might as well do this, public builds use the regular Makefile. IF(CMAKE_SYSTEM_PROCESSOR MATCHES "ppc64le" OR CMAKE_SYSTEM_PROCESSOR MATCHES "ppc64") SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=native") ELSE() SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=native") ENDIF() IF(CMAKE_BUILD_TYPE MATCHES "Debug") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall ${FTE_WERROR_} -Wno-pointer-sign -Wno-unknown-pragmas -Wno-format-zero-length -Wno-strict-aliasing -Wno-error=cpp") ELSE() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 ${FTE_WERROR_}") ENDIF() IF (NOT FTE_USE_SDL) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,--warn-common") ENDIF() #SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wc++-compat") #lul, thousands of errors! ENDIF() IF(CMAKE_BUILD_TYPE MATCHES "Debug") IF(NOT ${WIN32}) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu89") ENDIF() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEBUG") ENDIF() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_FILE_OFFSET_BITS=64") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DFTE_LIBRARY_PATH=${CMAKE_INSTALL_FULL_LIBDIR}/${FTE_INSTALL_LIBDIR} -DFTE_DATA_DIR=${CMAKE_INSTALL_FULL_DATAROOTDIR}") FUNCTION(EMBED_PLUGIN_META PLUGNAME PLUGTITLE PLUGDESC) SET_TARGET_PROPERTIES(plug_${PLUGNAME} PROPERTIES OUTPUT_NAME "${PLUGNAME}") SET_TARGET_PROPERTIES(plug_${PLUGNAME} PROPERTIES PREFIX "fteplug_") SET_TARGET_PROPERTIES(plug_${PLUGNAME} PROPERTIES LINK_FLAGS "-Wl,--no-undefined") SET(INSTALLTARGS ${INSTALLTARGS} "plug_${PLUGNAME}" PARENT_SCOPE) #sadly we need to use a temp zip file, because otherwise zip insists on using zip64 extensions which breaks zip -A (as well as any attempts to read any files). ADD_CUSTOM_COMMAND( TARGET plug_${PLUGNAME} POST_BUILD COMMAND /bin/echo -e "{\\n package fteplug_${PLUGNAME}\\n ver \"${SVNREVISION}\"\\n category Plugins\\n title \"${PLUGTITLE}\"\\n gamedir \"\"\\n desc \"${PLUGDESC}\"\\n}" | zip -q -9 -fz- $.zip - COMMAND cmake -E cat $.zip >> "$" COMMAND zip -A "$" COMMAND cmake -E rm $.zip VERBATIM) ENDFUNCTION() SET(FTE_DEP_GNUTLS true CACHE BOOL "Link against gnutls") IF(FTE_DEP_GNUTLS) FIND_PACKAGE(GnuTLS) IF(NOT GNUTLS_FOUND) MESSAGE(WARNING "gnutls library NOT available. HTTPS/DTLS will not be available.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_GNUTLS) ELSE() IF(WIN32) SET(GNUTLS_STATIC true CACHE BOOL "Link gnutls statically.") #usually as an .so though. :/ ELSE() SET(GNUTLS_STATIC false CACHE BOOL "Link gnutls statically.") #usually as an .so though. :/ ENDIF() IF(GNUTLS_STATIC) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};GNUTLS_STATIC) SET(FTE_LIBS ${FTE_LIBS} ${GNUTLS_LIBRARY}) SET(FTESV_LIBS ${FTESV_LIBS} ${GNUTLS_LIBRARY}) ENDIF() ENDIF() ENDIF() IF(WIN32) SET(FTE_DEP_WINSSPI true CACHE BOOL "Link against winsspi(schannel)") IF(NOT FTE_DEP_WINSSPI) SET(FTE_DEFINES ${FTE_DEFINES};NO_WINSSPI) ENDIF() ENDIF() SET(FTE_DEP_SDL3 false CACHE BOOL "Use SDL3 if available (disabling many platform-specific behaviours).") IF(FTE_DEP_SDL3) FIND_PACKAGE(SDL3 QUIET) ENDIF() IF(SDL3_FOUND) #SDL3 MESSAGE(STATUS "SDL3 library found, woo, modern, for now.") # FIND_PACKAGE(PkgConfig REQUIRED) # PKG_SEARCH_MODULE(sdl3 REQUIRED sdl3) FIND_PACKAGE(SDL3 REQUIRED) INCLUDE_DIRECTORIES(${FREETYPE_INCLUDE_DIRS} ${SDL3_INCLUDE_DIRS}) SET(FTE_DEFINES ${FTE_DEFINES};FTE_SDL3;MULTITHREAD) SET(FTE_LIBS ${FTE_LIBS} ${SYS_LIBS} ${CMAKE_DL_LIBS} ${SDL3_LIBRARIES}) SET(FTE_ARCH_FILES engine/client/sys_sdl.c engine/client/snd_al.c engine/client/snd_sdl.c engine/client/in_sdl.c engine/client/cd_sdl.c engine/gl/gl_vidsdl.c ) SET(FTESV_DEFINES ${FTESV_DEFINES};MULTITHREAD) SET(FTESV_LIBS ${FTESV_LIBS} ${SYS_LIBS} ${CMAKE_DL_LIBS} ${SDL3_LIBRARIES}) IF(WIN32) SET(FTE_LIBS ${FTE_LIBS} wsock32 gdi32 ole32) SET(FTE_DEFINES ${FTE_DEFINES}) SET(FTE_ARCH_FILES ${FTE_ARCH_FILES} engine/client/winquake.rc engine/common/net_ssl_winsspi.c engine/common/fs_win32.c ) SET(FTESV_ARCH_FILES ${FTESV_ARCH_FILES} engine/client/winquake.rc engine/common/net_ssl_winsspi.c engine/common/fs_win32.c engine/server/sv_sys_win.c ) ELSE() SET(FTE_ARCH_FILES ${FTE_ARCH_FILES} engine/common/net_ssl_gnutls.c ) SET(FTESV_ARCH_FILES ${FTESV_ARCH_FILES} engine/common/net_ssl_gnutls.c engine/common/sys_linux_threads.c engine/server/sv_sys_unix.c ) SET(FTESV_LIBS ${FTESV_LIBS} pthread) ENDIF() ELSEIF(${ANDROID}) # FIND_PACKAGE(Freetype REQUIRED) # INCLUDE_DIRECTORIES( ${FREETYPE_INCLUDE_DIRS} ) SET(FTE_DEFINES ${FTE_DEFINES};ANDROID;VKQUAKE;MULTITHREAD) SET(FTE_LIBS ${FTE_LIBS} android log EGL ${SYS_LIBS} ${CMAKE_DL_LIBS}) SET(FTE_ARCH_FILES engine/client/sys_droid.c engine/common/sys_linux_threads.c engine/client/snd_droid.c engine/client/cd_null.c engine/gl/gl_viddroid.c ) ELSEIF(WIN32 AND NOT FTE_USE_SDL) INCLUDE_DIRECTORIES(engine/libs engine/libs/freetype2/include) # LINK_DIRECTORIES(engine/libs/mingw64-libs) # engine/server/sv_sys_win.c SET(FTE_LIBS ${FTE_LIBS} ole32 gdi32 wsock32 winmm dxguid) SET(FTE_DEFINES ${FTE_DEFINES};D3D9QUAKE;D3D11QUAKE) SET(FTE_ARCH_FILES engine/client/winquake.rc engine/common/sys_win_threads.c engine/common/net_ssl_winsspi.c engine/common/net_ssl_gnutls.c engine/common/fs_win32.c engine/client/cd_win.c engine/client/in_win.c engine/client/snd_al.c engine/client/snd_directx.c engine/client/snd_wasapi.c engine/client/snd_win.c engine/client/snd_xaudio.c engine/client/sys_win.c engine/gl/gl_vidnt.c engine/d3d/d3d_backend.c engine/d3d/d3d_image.c engine/d3d/d3d_shader.c engine/d3d/d3d11_backend.c engine/d3d/d3d11_image.c engine/d3d/d3d11_shader.c engine/d3d/d3d8_backend.c engine/d3d/d3d8_image.c engine/d3d/vid_d3d.c engine/d3d/vid_d3d11.c engine/d3d/vid_d3d8.c ) SET(FTESV_LIBS ${FTESV_LIBS} wsock32 winmm) SET(FTESV_ARCH_FILES engine/client/winquake.rc engine/common/sys_win_threads.c engine/common/net_ssl_winsspi.c engine/common/net_ssl_gnutls.c engine/common/fs_win32.c engine/server/sv_sys_win.c ) ELSEIF(UNIX AND NOT FTE_USE_SDL) #linux(ish) #openbsd will have issues with snd_linux.c #linux-only packages FIND_PACKAGE(ALSA) IF(ALSA_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};AUDIO_ALSA;AUDIO_PULSE) ELSE() MESSAGE(WARNING "asound (alsa) library NOT available.") ENDIF() FIND_PACKAGE(X11) IF(X11_FOUND) IF (NOT X11_Xcursor_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_X11_CURSOR) MESSAGE(WARNING "Xcursor library NOT available.") ENDIF() IF (NOT X11_Xrandr_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_X11_RANDR) MESSAGE(WARNING "Xrandr library NOT available.") ENDIF() IF (NOT X11_Xscreensaver_FOUND) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_X11_XSS) MESSAGE(WARNING "Xss library NOT available.") ENDIF() ELSE() MESSAGE(WARNING "x11 library NOT available.") SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};NO_X11) ENDIF() SET(FTE_DEFINES ${FTE_DEFINES};DYNAMIC_SDL;MULTITHREAD) SET(FTE_LIBS ${FTE_LIBS} ${SYS_LIBS} ${CMAKE_DL_LIBS} pthread ${SDL2_LIBRARIES}) SET(FTE_ARCH_FILES ${FTE_ARCH_FILES} engine/client/sys_linux.c engine/common/sys_linux_threads.c engine/common/net_ssl_gnutls.c engine/client/snd_al.c engine/client/snd_alsa.c engine/client/snd_linux.c engine/client/snd_pulse.c engine/client/snd_sdl.c #we use SDL audio even without sys_sdl, because of pulseaudio fucking over alsa, alsa fucking over oss3, and oss4 not being used. Either way, openal should be the default anyway. engine/client/cd_linux.c engine/gl/gl_vidlinuxglx.c engine/gl/gl_videgl.c # engine/gl/gl_vidrpi.c # engine/gl/gl_vidwayland.c ) #openbsd uses a libossaudio library for all the oss stuff, use that to ensure that we still get sound FIND_LIBRARY( OSSAUDIO_LIBRARY NAMES ossaudio ) IF(OSSAUDIO_LIBRARY) SET(FTE_LIBS ${FTE_LIBS} ${OSSAUDIO_LIBRARY}) ENDIF() #on linux, use wayland (we normally dynamically link, but we still need headers). FIND_LIBRARY( WAYLAND_CLIENT_LIBRARY NAMES wayland-client libwayland-client ) FIND_LIBRARY( WAYLAND_EGL NAMES wayland-egl ) FIND_LIBRARY( HAVE_XKBCOMMON NAMES xkbcommon ) IF(NOT HAVE_XKBCOMMON) MESSAGE(WARNING "xkbcommon library not found, needed for wayland to be usable.") UNSET(WAYLAND_CLIENT_LIBRARY) ENDIF() IF(WAYLAND_CLIENT_LIBRARY AND WAYLAND_EGL) # SET(FTE_LIBS ${FTE_LIBS} ${WAYLAND_CLIENT_LIBRARY} ${HAVE_XKBCOMMON} ${WAYLAND_EGL}) # SET(FTE_DEFINES ${FTE_DEFINES};STATIC_WAYLAND) SET(FTE_DEFINES ${FTE_DEFINES};WAYLANDQUAKE;USE_EGL) SET(FTE_ARCH_FILES ${FTE_ARCH_FILES} engine/gl/gl_vidwayland.c ) ELSE() MESSAGE(WARNING "Wayland library NOT available. X11 will live forever anyway.") IF(NOT X11_FOUND) MESSAGE(WARNING "No renderers supported!") SET(FTE_NO_RENDERERS 1) ENDIF() ENDIF() IF(CMAKE_SYSTEM_NAME MATCHES "BSD" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") FIND_LIBRARY(epoll-shim REQUIRED) INCLUDE_DIRECTORIES(${EPOLL_INC_DIR}) SET(FTESV_LIBS ${FTESV_LIBS} epoll-shim) SET(FTESV_DEFINES ${FTESV_DEFINES};HAVE_EPOLL) ENDIF() SET(FTESV_DEFINES ${FTESV_DEFINES};MULTITHREAD) SET(FTESV_ARCH_FILES ${FTESV_ARCH_FILES} engine/server/sv_sys_unix.c engine/common/sys_linux_threads.c engine/common/net_ssl_gnutls.c ) SET(FTESV_LIBS ${FTESV_LIBS} ${SYS_LIBS} ${CMAKE_DL_LIBS} pthread) ELSEIF(1) #SDL2 # FIND_PACKAGE(Freetype REQUIRED) # INCLUDE_DIRECTORIES(engine/libs engine/libs/freetype2/include) FIND_PACKAGE(PkgConfig REQUIRED) PKG_SEARCH_MODULE(sdl2 REQUIRED sdl2) FIND_PACKAGE(SDL2 REQUIRED) INCLUDE_DIRECTORIES(${FREETYPE_INCLUDE_DIRS} ${SDL2_INCLUDE_DIRS}) #SDL2.0.7 supports vulkan, so lets use it. SET(FTE_DEFINES ${FTE_DEFINES};FTE_SDL;MULTITHREAD) SET(FTE_LIBS ${FTE_LIBS} ${SYS_LIBS} ${CMAKE_DL_LIBS} ${SDL2_LIBRARIES}) SET(FTE_ARCH_FILES engine/client/sys_sdl.c engine/client/snd_al.c engine/client/snd_sdl.c engine/client/in_sdl.c engine/client/cd_sdl.c engine/gl/gl_vidsdl.c ) SET(FTESV_DEFINES ${FTESV_DEFINES};MULTITHREAD) SET(FTESV_LIBS ${FTESV_LIBS} ${SYS_LIBS} ${CMAKE_DL_LIBS} ${SDL2_LIBRARIES}) IF(WIN32) SET(FTE_LIBS ${FTE_LIBS} wsock32 gdi32 ole32) SET(FTE_DEFINES ${FTE_DEFINES};NO_DIRECTX) SET(FTE_ARCH_FILES ${FTE_ARCH_FILES} engine/client/winquake.rc engine/common/net_ssl_winsspi.c engine/common/fs_win32.c ) SET(FTESV_ARCH_FILES ${FTESV_ARCH_FILES} engine/client/winquake.rc engine/common/net_ssl_winsspi.c engine/common/fs_win32.c engine/server/sv_sys_win.c ) ELSE() SET(FTE_ARCH_FILES ${FTE_ARCH_FILES} engine/common/net_ssl_gnutls.c ) SET(FTESV_ARCH_FILES ${FTESV_ARCH_FILES} engine/common/net_ssl_gnutls.c engine/common/sys_linux_threads.c engine/server/sv_sys_unix.c ) SET(FTESV_LIBS ${FTESV_LIBS} pthread) ENDIF() ELSE() # engine/common/sys_linux_threads.c # engine/common/net_ssl_gnutls.c # engine/server/sv_sys_unix.c # engine/client/snd_alsa.c # engine/client/snd_droid.c # engine/client/snd_linux.c # engine/client/snd_macos.c # engine/client/snd_morphos.c # engine/client/snd_sblaster.c # engine/client/snd_sdl.c # engine/client/snd_sndio.c # engine/client/sys_dos.c # engine/client/sys_droid.c # engine/client/sys_linux.c # engine/client/sys_morphos.c # engine/client/sys_plugfte.c # engine/client/sys_sdl.c # engine/client/sys_xdk.c # engine/client/cd_linux.c # engine/client/cd_null.c # engine/client/cd_sdl.c # engine/client/in_morphos.c # engine/client/in_sdl.c # engine/gl/gl_viddroid.c # engine/gl/gl_videgl.c # engine/gl/gl_vidlinuxglx.c # engine/gl/gl_vidmacos.c # engine/gl/gl_vidmorphos.c # engine/gl/gl_vidnull.c # engine/gl/gl_vidrpi.c # engine/gl/gl_vidsdl.c # engine/gl/gl_vidtinyglstubs.c # engine/gl/gl_vidwayland.c ENDIF() SET(FTE_GL_FILES #These are GL-specific, but can be left even if no gl is supported. engine/gl/gl_backend.c engine/gl/gl_bloom.c engine/gl/gl_draw.c engine/gl/gl_rmain.c engine/gl/gl_rmisc.c engine/gl/gl_rsurf.c engine/gl/gl_screen.c engine/gl/gl_vidcommon.c engine/gl/glmod_doom.c ) SET(FTE_VK_FILES engine/vk/vk_backend.c engine/vk/vk_init.c ) SET(FTE_QCVM_FILES engine/qclib/comprout.c engine/qclib/initlib.c engine/qclib/pr_edict.c engine/qclib/pr_exec.c engine/qclib/pr_multi.c engine/qclib/qcc_cmdlib.c engine/qclib/qcc_pr_comp.c engine/qclib/qcc_pr_lex.c # engine/qclib/decomp.c # engine/qclib/packager.c # engine/qclib/pr_x86.c # engine/qclib/qccgui.c # engine/qclib/qccguistuff.c # engine/qclib/qcctui.c engine/qclib/qccmain.c engine/qclib/qcd_main.c engine/qclib/qcdecomp.c ) SET(FTE_COMMON_FILES #these files are common to both server-only and client+server builds. engine/common/cmd.c engine/common/com_mesh.c engine/common/com_bih.c engine/common/common.c engine/common/json.c engine/common/crc.c engine/common/cvar.c engine/common/fs.c engine/common/fs_dzip.c engine/common/fs_pak.c engine/common/fs_stdio.c engine/common/fs_xz.c engine/common/fs_zip.c engine/common/gl_q2bsp.c engine/common/huff.c engine/common/log.c engine/common/mathlib.c engine/common/md4.c engine/common/md5.c engine/common/net_chan.c engine/common/net_ice.c engine/common/net_wins.c engine/common/plugin.c engine/common/pmove.c engine/common/pmovetst.c engine/common/pr_bgcmd.c engine/common/q1bsp.c engine/common/q2pmove.c engine/common/qvm.c engine/common/sha1.c engine/common/sha2.c engine/common/translate.c engine/common/zone.c #important headers engine/common/bothdefs.h engine/common/config_fteqw.h engine/common/config_minimal.h engine/common/config_nocompat.h engine/common/config_wastes.h engine/common/config_freecs.h engine/common/config_fteqw_noweb.h #useless headers that I'll never search for engine/client/api_menu.h engine/client/cdaudio.h engine/client/client.h engine/client/cl_ignore.h engine/client/cl_master.h engine/client/input.h engine/client/keys.h engine/client/menu.h engine/client/merged.h engine/client/modelgen.h engine/client/quakedef.h engine/client/render.h engine/client/sbar.h engine/client/screen.h engine/client/sound.h engine/client/spritegn.h # engine/client/sys_plugfte.h engine/client/vid.h engine/client/view.h engine/client/wad.h # engine/client/winquake.h engine/common/bothdefs.h engine/common/bspfile.h engine/common/cmd.h engine/common/com_mesh.h engine/common/common.h engine/common/console.h engine/common/cvar.h engine/common/fs.h engine/common/mathlib.h engine/common/net.h engine/common/netinc.h engine/common/particles.h engine/common/pmove.h engine/common/pr_common.h engine/common/protocol.h engine/common/sys.h engine/common/translate.h engine/common/ui_public.h engine/common/vm.h engine/common/world.h engine/common/zone.h engine/gl/gl_draw.h engine/gl/gl_model.h engine/gl/glquake.h engine/gl/glsupp.h engine/gl/gl_terrain.h engine/gl/gl_videgl.h engine/gl/model_hl.h engine/gl/shader.h engine/http/iweb.h engine/qclib/cmdlib.h engine/qclib/execloop.h engine/qclib/gui.h engine/qclib/hash.h engine/qclib/pr_comp.h engine/qclib/progsint.h engine/qclib/progslib.h engine/qclib/progtype.h engine/qclib/qcc.h engine/qclib/qcd.h engine/server/progdefs.h engine/server/progs.h engine/server/q2game.h engine/server/server.h #engine/server/svhl_gcapi.h engine/server/sv_sql.h #engine/sw/sw.h #engine/sw/sw_spans.h engine/vk/vkrenderer.h engine/web/ftejslib.h #sigh engine/client/pr_skelobj.c engine/client/m_download.c engine/client/net_master.c engine/client/r_d3.c #these are here because of hitmodel etc engine/gl/gl_heightmap.c engine/gl/gl_hlmdl.c engine/gl/gl_model.c engine/server/sv_move.c engine/server/sv_phys.c engine/server/world.c ${FTE_QCVM_FILES} engine/qclib/hash.c engine/http/httpclient.c ) SET(FTE_SERVER_FILES engine/server/net_preparse.c engine/server/pr_cmds.c engine/server/pr_lua.c engine/server/pr_q1qvm.c engine/server/savegame.c engine/server/sv_ccmds.c engine/server/sv_chat.c engine/server/sv_cluster.c engine/server/sv_demo.c engine/server/sv_ents.c engine/server/sv_init.c engine/server/sv_main.c engine/server/sv_master.c engine/server/sv_mvd.c engine/server/sv_nchan.c engine/server/sv_rankin.c engine/server/sv_send.c engine/server/sv_sql.c engine/server/sv_user.c # engine/server/svhl_game.c # engine/server/svhl_phys.c # engine/server/svhl_world.c engine/server/svq2_ents.c engine/server/svq2_game.c ) #these files are only in the client SET(FTE_CLIENT_FILES engine/client/cl_cam.c engine/client/cl_demo.c engine/client/cl_ents.c engine/client/cl_ignore.c engine/client/cl_input.c engine/client/cl_main.c engine/client/cl_parse.c engine/client/cl_pred.c engine/client/cl_screen.c engine/client/cl_tent.c # engine/client/clhl_game.c engine/client/clq2_cin.c engine/client/clq2_ents.c engine/client/console.c engine/client/fragstats.c engine/client/image.c engine/client/in_generic.c engine/client/keys.c engine/client/m_items.c engine/client/m_master.c engine/client/m_mp3.c engine/client/m_multi.c engine/client/m_options.c engine/client/m_script.c engine/client/m_native.c engine/client/m_single.c engine/client/menu.c engine/client/p_classic.c engine/client/p_null.c engine/client/p_script.c engine/client/pr_clcmd.c engine/client/pr_csqc.c engine/client/pr_menu.c engine/client/r_2d.c engine/client/r_d3.c engine/client/r_part.c engine/client/r_partset.c engine/client/r_surf.c engine/client/renderer.c engine/client/renderque.c engine/client/roq_read.c engine/client/sbar.c engine/client/skin.c engine/client/snd_dma.c engine/client/snd_mem.c engine/client/snd_mix.c engine/client/snd_mp3.c engine/client/snd_ov.c engine/client/textedit.c engine/client/valid.c engine/client/view.c engine/client/wad.c engine/client/zqtp.c #These are generic renderer files and no longer gl-specific (for the most part) engine/gl/gl_alias.c engine/gl/gl_font.c engine/gl/gl_ngraph.c engine/gl/gl_rlight.c engine/gl/gl_shader.c engine/gl/gl_shadow.c engine/gl/gl_warp.c engine/gl/ltface.c #these are renderer-specific engine/client/vid_headless.c ${FTE_GL_FILES} ${FTE_VK_FILES} ) SET(FTE_Q3_FILES plugins/quake3/botlib/be_aas_bspq3.c plugins/quake3/botlib/be_aas_entity.c plugins/quake3/botlib/be_aas_move.c plugins/quake3/botlib/be_aas_routealt.c plugins/quake3/botlib/be_ai_char.c plugins/quake3/botlib/be_ai_goal.c plugins/quake3/botlib/be_ai_weight.c plugins/quake3/botlib/l_crc.c plugins/quake3/botlib/l_memory.c plugins/quake3/botlib/l_struct.c plugins/quake3/botlib/be_aas_cluster.c plugins/quake3/botlib/be_aas_file.c plugins/quake3/botlib/be_aas_optimize.c plugins/quake3/botlib/be_aas_route.c plugins/quake3/botlib/be_ai_chat.c plugins/quake3/botlib/be_ai_move.c plugins/quake3/botlib/be_ea.c plugins/quake3/botlib/l_libvar.c plugins/quake3/botlib/l_precomp.c plugins/quake3/botlib/be_aas_debug.c plugins/quake3/botlib/be_aas_main.c plugins/quake3/botlib/be_aas_reach.c plugins/quake3/botlib/be_aas_sample.c plugins/quake3/botlib/be_ai_gen.c plugins/quake3/botlib/be_ai_weap.c plugins/quake3/botlib/be_interface.c plugins/quake3/botlib/l_log.c plugins/quake3/botlib/l_script.c plugins/quake3/botlib/standalone.c plugins/quake3/clq3_cg.c plugins/quake3/clq3_ui.c plugins/quake3/clq3_parse.c plugins/quake3/svq3_game.c plugins/quake3/q3common.c plugins/quake3/q3common.h plugins/quake3/clq3defs.h plugins/quake3/q3g_public.h ) #For annoying compressed gltf2 files. SET(FTE_DEP_DRACO false CACHE BOOL "Link against libdraco (apache2).") IF(FTE_DEP_DRACO) FIND_LIBRARY( DRACO_LIBRARY NAMES draco ) IF(DRACO_LIBRARY) SET(DRACO_FILES plugins/models/draco.cpp) SET(DRACO_CFLAGS HAVE_DRACO) SET(FTE_COMMON_FILES ${FTE_COMMON_FILES} ${DRACO_FILES}) SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES};${DRACO_CFLAGS}) SET(FTE_LIBS ${FTE_LIBS} ${DRACO_LIBRARY}) SET(FTESV_LIBS ${FTESV_LIBS} ${DRACO_LIBRARY}) ELSE() MESSAGE(WARNING "draco library not found, needed for GLTF's KHR_draco_mesh_compression to be usable.") ENDIF() ENDIF() SET(FTE_PLUG_QUAKE3 true CACHE BOOL "Compile Quake3 plugin.") IF(FTE_PLUG_QUAKE3) IF (0) SET(FTE_DEFINES ${FTE_DEFINES};${Q3_DEFINES}) SET(FTE_LIBS ${FTE_LIBS} quake3) #define the modules and make sure they're linked (one generic, one for server-only builds. ADD_LIBRARY(quake3 STATIC ${FTE_Q3_FILES}) SET_TARGET_PROPERTIES(quake3 PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON};BOTLIB;BOTLIB_STATIC;FTEPLUGIN;STATIC_Q3") TARGET_LINK_LIBRARIES(quake3 ${SYS_LIBS}) #ADD_LIBRARY(q3sv STATIC EXCLUDE_FROM_ALL ${FTE_Q3_FILES}) #SET_TARGET_PROPERTIES(q3sv PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON};BOTLIB;BOTLIB_STATIC;SERVERONLY") #SET_TARGET_PROPERTIES(q3sv PROPERTIES LINK_FLAGS "-Wl,--no-undefined") #TARGET_LINK_LIBRARIES(q3sv ${SYS_LIBS}) #SET(FTESV_LIBS ${FTESV_LIBS} q3sv) ELSE() #define the modules and make sure they're linked (one generic, one for server-only builds. ADD_LIBRARY(plug_quake3 MODULE ${FTE_Q3_FILES} plugins/plugin.c) SET_TARGET_PROPERTIES(plug_quake3 PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON};BOTLIB;BOTLIB_STATIC;FTEPLUGIN") TARGET_LINK_LIBRARIES(plug_quake3 ${SYS_LIBS}) EMBED_PLUGIN_META(quake3 "Quake3 Compat" "Provides compatability with Quake3's gamecode.") ENDIF() ENDIF() #still a wip, so disabled by default SET(FTE_PLUG_COD true CACHE BOOL "Compile Call of Duty plugin.") IF(FTE_PLUG_COD) ADD_LIBRARY(plug_cod MODULE plugins/cod/codmod.c plugins/cod/codbsp.c plugins/cod/codmat.c plugins/cod/codiwi.c #plugins/cod/codff.c plugins/plugin.c) SET_TARGET_PROPERTIES(plug_cod PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON};FTEPLUGIN") TARGET_LINK_LIBRARIES(plug_cod ${SYS_LIBS} #${ZLIB_LIBRARIES} ) EMBED_PLUGIN_META(cod "CoD Formats" "Provides compatability with Call Of Duty's file formats.") ENDIF() FILE(STRINGS "${FTE_BUILD_CONFIG}" BULLET_INTERNAL REGEX "^#define[\t ]+USE_INTERNAL_BULLET") IF(BULLET_INTERNAL) #Built-in bullet physics plugin... FIND_PACKAGE(Bullet REQUIRED) SET(FTE_COMMON_FILES ${FTE_COMMON_FILES} plugins/bullet/bulletplug.cpp) INCLUDE_DIRECTORIES( ${BULLET_INCLUDE_DIRS} ) SET(FTE_LIBS ${FTE_LIBS} ${BULLET_LIBRARIES}) SET(FTESV_LIBS ${FTESV_LIBS} ${BULLET_LIBRARIES}) ELSE() #Bullet Physics library plugin SET(FTE_PLUG_BULLET true CACHE BOOL "Compile bullet rigid body physics plugin.") IF(FTE_PLUG_BULLET) FIND_PACKAGE(Bullet) IF (BULLET_FOUND) ADD_LIBRARY(plug_bullet MODULE plugins/plugin.c plugins/bullet/bulletplug.cpp ) TARGET_INCLUDE_DIRECTORIES(plug_bullet PUBLIC ${BULLET_INCLUDE_DIRS}) SET_TARGET_PROPERTIES(plug_bullet PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_bullet ${SYS_LIBS} ${BULLET_LIBRARIES}) EMBED_PLUGIN_META(bullet "Bullet Physics Plugin" "Provides Rigid Body Physics.") ELSE() MESSAGE(WARNING "bullet library not detected, skipping plugin") ENDIF() ENDIF() ENDIF() #ODE Physics library plugin SET(FTE_PLUG_ODE true CACHE BOOL "Compile ODE rigid body physics plugin.") SET_PROPERTY(CACHE FTE_PLUG_ODE PROPERTY STRINGS false true static) IF(FTE_PLUG_ODE) FIND_PATH(LIBODE_INCLUDE_DIR ode/ode.h) IF (LIBODE_INCLUDE_DIR) FIND_LIBRARY(LIBODE_LIBRARY ode) ENDIF() IF (LIBODE_LIBRARY) IF (FTE_PLUG_ODE STREQUAL "static") #SET (FTE_COMMON_FILES ${FTE_COMMON_FILES} engine/common/com_phys_ode.c) SET(FTE_LIB_DEFINES "${FTE_LIB_DEFINES};USE_INTERNAL_ODE;ODE_STATIC") SET(FTE_LIBS ${FTE_LIBS} ${LIBODE_LIBRARY}) SET(FTESV_LIBS ${FTESV_LIBS} ${LIBODE_LIBRARY}) SET(FTE_INCLUDES ${FTE_INCLUDES} ${ODE_INCLUDE_DIRS}) ELSE() ADD_LIBRARY(plug_ode MODULE plugins/plugin.c engine/common/com_phys_ode.c engine/common/mathlib.c ) TARGET_INCLUDE_DIRECTORIES(plug_ode PUBLIC ${ODE_INCLUDE_DIRS}) SET_TARGET_PROPERTIES(plug_ode PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;ODE_STATIC") TARGET_LINK_LIBRARIES(plug_ode ${SYS_LIBS} ${LIBODE_LIBRARY}) EMBED_PLUGIN_META(ode "ODE Physics" "Provides Rigid Body Physics behaviours.") ENDIF() ELSE() MESSAGE(WARNING "ODE library not found, no ode plugin for you") ENDIF() ENDIF() IF(ANDROID) #android sucks. everything is a library. so we build the engine as a shared library and completely ignore dedicated servers+tools SET(FTE_ENGINE_FTEDROID true CACHE BOOL "Compile ftedroid engine shared library.") IF(FTE_ENGINE_FTEDROID) ADD_LIBRARY(ftedroid MODULE ${FTE_ARCH_FILES} ${FTE_COMMON_FILES} ${FTE_CLIENT_FILES} ) SET_TARGET_PROPERTIES(ftedroid PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_DEFINES};${FTE_REVISON}") TARGET_INCLUDE_DIRECTORIES(ftedroid PUBLIC ${FTE_INCLUDES}) TARGET_LINK_LIBRARIES(ftedroid ${FTE_LIBS} ) SET(INSTALLTARGS ${INSTALLTARGS} ftedroid) ENDIF() ELSE() #systems that actually have executables... SET(FTE_ENGINE true CACHE BOOL "Compile fteqw engine binary.") IF(FTE_ENGINE) ADD_EXECUTABLE(fteqw WIN32 ${FTE_ARCH_FILES} ${FTE_COMMON_FILES} ${FTE_CLIENT_FILES} ${FTE_SERVER_FILES} ) IF(CMAKE_SYSTEM_NAME MATCHES "BSD" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") FIND_LIBRARY(epoll-shim REQUIRED) SET(FTE_INCLUDES ${FTE_INCLUDES} "${EPOLL_INC_DIR}") SET(FTE_LIBS ${FTE_LIBS} epoll-shim) SET(FTE_DEFINES ${FTE_DEFINES};HAVE_EPOLL) ENDIF() SET_TARGET_PROPERTIES(fteqw PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_DEFINES};${FTE_REVISON}") TARGET_INCLUDE_DIRECTORIES(fteqw PUBLIC ${FTE_INCLUDES}) TARGET_LINK_LIBRARIES(fteqw ${FTE_LIBS}) SET(INSTALLTARGS ${INSTALLTARGS} fteqw) ADD_CUSTOM_TARGET(fteqw-i18n ALL VERBATIM WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMMAND xgettext --output=${CMAKE_CURRENT_BINARY_DIR}/fteqw.pot -k -kCon_TPrintf -kCon_SafeTPrintf -kNetchan_OutOfBandTPrintf:4 -kSV_OutOfBandTPrintf:4 -klangtext -kSV_TPrintToClient:3 -kSV_ClientTPrintf:3 -kSV_BroadcastTPrintf:2 -kCVARAFCD:6 -kCVARAFD:5 -kCVARFCD:5 -kCVARAD:4 -kCVARFD:4 -kCVARCD:4 -kCVARD:3 -kCmd_AddCommandD:3 -kCmd_AddCommandAD:4 -kMenu_Prompt:4:5:6 -kHost_EndGame ${FTE_ARCH_FILES} ${FTE_COMMON_FILES} ${FTE_CLIENT_FILES} ${FTE_SERVER_FILES} BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/fteqw.pot" SOURCES ${FTE_ARCH_FILES} ${FTE_COMMON_FILES} ${FTE_CLIENT_FILES} ${FTE_SERVER_FILES} ) ENDIF() SET(FTE_ENGINE_SERVER_ONLY true CACHE BOOL "Compile fteqw-sv (server only) engine binary.") IF(FTE_ENGINE_SERVER_ONLY) ADD_EXECUTABLE(fteqw-sv ${FTESV_ARCH_FILES} ${FTE_COMMON_FILES} ${FTE_SERVER_FILES} ) SET_TARGET_PROPERTIES(fteqw-sv PROPERTIES COMPILE_DEFINITIONS "SERVERONLY;${FTE_LIB_DEFINES};${FTESV_DEFINES};${FTE_REVISON}") TARGET_INCLUDE_DIRECTORIES(fteqw-sv PUBLIC ${FTE_INCLUDES}) TARGET_LINK_LIBRARIES(fteqw-sv ${FTESV_LIBS}) SET(INSTALLTARGS ${INSTALLTARGS} fteqw-sv) ENDIF() SET(FTE_ENGINE_CLIENT_ONLY false CACHE BOOL "Compile ftedw-cl (client-only) engine binary.") IF(FTE_ENGINE_CLIENT_ONLY) ADD_EXECUTABLE(fteqw-cl ${FTE_ARCH_FILES} ${FTE_COMMON_FILES} ${FTE_CLIENT_FILES} ) SET_TARGET_PROPERTIES(fteqw-cl PROPERTIES COMPILE_DEFINITIONS "CLIENTONLY;${FTE_LIB_DEFINES};${FTE_DEFINES};${FTE_REVISON}") TARGET_INCLUDE_DIRECTORIES(fteqw-cl PUBLIC ${FTE_INCLUDES}) TARGET_LINK_LIBRARIES(fteqw-cl ${FTE_LIBS}) SET(INSTALLTARGS ${INSTALLTARGS} fteqw-cl) ENDIF() IF(FTE_ENGINE OR FTE_ENGINE_CLIENT) FIND_PACKAGE(SDL2) IF(SDL2_FOUND) SET(FTE_SDL2 FTE_SDL) SET(FTE_SDL2_INCLUDES ${SDL2_INCLUDE_DIRS}) ENDIF() ENDIF() SET(FTE_TOOL_IQM true CACHE BOOL "Compile IQM Tool.") IF(FTE_TOOL_IQM) ADD_EXECUTABLE(iqmtool iqm/iqm.cpp plugins/models/gltf.c ${DRACO_FILES} engine/common/json.c engine/client/image.c imgtool.c iqm/iqm.h ) SET_TARGET_PROPERTIES(iqmtool PROPERTIES COMPILE_DEFINITIONS "IQMTOOL;${DRACO_CFLAGS};${FTE_LIB_DEFINES};${FTE_REVISON}") TARGET_LINK_LIBRARIES(iqmtool ${CMAKE_DL_LIBS} ${DRACO_LIBRARY} ${JPEG_LIBRARIES} ${PNG_LIBRARIES}) SET(INSTALLTARGS ${INSTALLTARGS} iqmtool) ENDIF() SET(FTE_TOOL_IMAGE true CACHE BOOL "Compile Image Tool.") IF(FTE_TOOL_IMAGE) ADD_EXECUTABLE(imgtool engine/client/image.c imgtool.c ) TARGET_INCLUDE_DIRECTORIES(imgtool PUBLIC ${FTE_SDL2_INCLUDES}) SET_TARGET_PROPERTIES(imgtool PROPERTIES COMPILE_DEFINITIONS "IMGTOOL;${FTE_LIB_DEFINES};${FTE_DEFINES};${FTE_REVISON};${FTE_SDL2}") TARGET_LINK_LIBRARIES(imgtool ${FTE_LIBS} ) SET(INSTALLTARGS ${INSTALLTARGS} imgtool) ENDIF() SET(FTE_TOOL_QTV true CACHE BOOL "Compile qtv server.") IF(FTE_TOOL_QTV) ADD_EXECUTABLE(qtv fteqtv/netchan.c fteqtv/parse.c fteqtv/msg.c fteqtv/qw.c fteqtv/source.c fteqtv/bsp.c fteqtv/rcon.c fteqtv/relay.c fteqtv/mdfour.c engine/common/md5.c fteqtv/crc.c fteqtv/control.c fteqtv/forward.c fteqtv/pmove.c fteqtv/menu.c fteqtv/httpsv.c fteqtv/libqtvc/glibc_sucks.c engine/common/sha1.c ) SET_TARGET_PROPERTIES(qtv PROPERTIES COMPILE_DEFINITIONS "${FTE_REVISON};${FTE_LIB_DEFINES}") IF(WIN32) TARGET_LINK_LIBRARIES(qtv ws2_32 winmm ${SYS_LIBS} ${FTEQTV_LIBS}) ELSEIF(CMAKE_SYSTEM_NAME MATCHES "BSD" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") # Add Epoll-shim for the kqueue Unixes here - Brad FIND_LIBRARY(epoll-shim REQUIRED) TARGET_INCLUDE_DIRECTORIES(qtv PUBLIC "${EPOLL_INC_DIR}") TARGET_LINK_LIBRARIES(qtv epoll-shim ${SYS_LIBS} ${FTEQTV_LIBS}) SET_TARGET_PROPERTIES(qtv PROPERTIES COMPILE_DEFINITIONS "${FTE_REVISON};${FTE_LIB_DEFINES};HAVE_EPOLL") ELSE() TARGET_LINK_LIBRARIES(qtv ${SYS_LIBS} ${FTEQTV_LIBS}) ENDIF() SET(INSTALLTARGS ${INSTALLTARGS} qtv) ENDIF() IF(WIN32) SET(FTE_TOOL_MASTER false CACHE BOOL "Compile master server.") ELSE() SET(FTE_TOOL_MASTER true CACHE BOOL "Compile master server.") ENDIF() IF(FTE_TOOL_MASTER) ADD_EXECUTABLE(ftemaster ${FTESV_ARCH_FILES} engine/server/sv_master.c engine/common/net_ice.c #for the stun responses. engine/common/net_wins.c engine/common/cvar.c engine/common/cmd.c engine/common/sha1.c #for websockets engine/common/sha2.c #for fingerprints engine/http/httpclient.c #for the pipe stuff engine/common/log.c engine/common/fs.c engine/common/fs_stdio.c engine/common/common.c engine/common/translate.c engine/common/zone.c engine/qclib/hash.c ) SET_TARGET_PROPERTIES(ftemaster PROPERTIES COMPILE_DEFINITIONS "MASTERONLY;${FTE_LIB_DEFINES};${FTESV_DEFINES};${FTE_REVISON}") TARGET_LINK_LIBRARIES(ftemaster ${FTESV_LIBS}) SET(INSTALLTARGS ${INSTALLTARGS} ftemaster) ENDIF() SET(FTE_TOOL_HTTPSV true CACHE BOOL "Compile small http server.") IF(FTE_TOOL_HTTPSV) ADD_EXECUTABLE(httpserver engine/common/fs_stdio.c engine/http/httpserver.c engine/http/iwebiface.c engine/http/ftpserver.c ) SET_TARGET_PROPERTIES(httpserver PROPERTIES COMPILE_DEFINITIONS "WEBSERVER;WEBSVONLY;${FTE_REVISON}") IF(WIN32) TARGET_LINK_LIBRARIES(httpserver ws2_32) ELSEIF(CMAKE_SYSTEM_NAME MATCHES "BSD" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") FIND_LIBRARY(epoll-shim REQUIRED) TARGET_INCLUDE_DIRECTORIES(httpserver PUBLIC "${EPOLL_INC_DIR}") TARGET_LINK_LIBRARIES(httpserver epoll-shim) SET_TARGET_PROPERTIES(httpserver PROPERTIES COMPILE_DEFINITIONS "WEBSERVER;WEBSVONLY;${FTE_REVISON};HAVE_EPOLL") ENDIF() #SET(INSTALLTARGS ${INSTALLTARGS} httpserver) ENDIF() SET(FTE_TOOL_QCVM false CACHE BOOL "Compile standalone qcvm.") IF(FTE_TOOL_QCVM) ADD_EXECUTABLE(qcvm engine/qclib/test.c engine/qclib/hash.c ${FTE_QCVM_FILES} ) SET_TARGET_PROPERTIES(qcvm PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON}") TARGET_LINK_LIBRARIES(qcvm ${FTEQCC_LIBS} ${SYS_LIBS}) SET(INSTALLTARGS ${INSTALLTARGS} qcvm) ENDIF() SET(FTE_TOOL_QCC true CACHE BOOL "Compile commandline qc compiler.") IF(FTE_TOOL_QCC) ADD_EXECUTABLE(fteqcc engine/qclib/qcctui.c engine/qclib/comprout.c engine/qclib/hash.c engine/qclib/qcc_cmdlib.c engine/qclib/qcc_pr_comp.c engine/qclib/qcc_pr_lex.c engine/qclib/qccmain.c engine/qclib/qcd_main.c engine/qclib/decomp.c engine/qclib/packager.c ) SET_TARGET_PROPERTIES(fteqcc PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON}") TARGET_LINK_LIBRARIES(fteqcc ${FTEQCC_LIBS} ${SYS_LIBS}) SET(INSTALLTARGS ${INSTALLTARGS} fteqcc) ENDIF() SET(FTE_TOOL_QCCGUI true CACHE BOOL "Compile gui qc compiler.") IF(FTE_TOOL_QCCGUI) IF(${WIN32}) ADD_EXECUTABLE(fteqccgui WIN32 engine/qclib/qccgui.c engine/qclib/qccguistuff.c engine/qclib/comprout.c engine/qclib/hash.c engine/qclib/qcc_cmdlib.c engine/qclib/qcc_pr_comp.c engine/qclib/qcc_pr_lex.c engine/qclib/qccmain.c engine/qclib/decomp.c engine/qclib/packager.c engine/qclib/qcd_main.c ) SET_TARGET_PROPERTIES(fteqccgui PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON}") TARGET_LINK_LIBRARIES(fteqccgui ${FTEQCC_LIBS} shlwapi ole32 comctl32 comdlg32) SET(INSTALLTARGS ${INSTALLTARGS} fteqccgui) ELSE() FIND_PACKAGE(Qt5Widgets) FIND_PATH(QSCINTILLA_INCLUDE_DIR NAMES Qsci/qsciglobal.h PATHS ${Qt5Widgets_INCLUDE_DIRS} PATH_SUFFIXES Qsci ) FIND_LIBRARY(QSCINTILLA_LIBRARY NAMES qscintilla2_qt5 PATHS ${QT_LIBRARY_DIR} /usr/local/lib /usr/local/lib/qt5 /usr/lib ) IF (QSCINTILLA_INCLUDE_DIR AND QSCINTILLA_LIBRARY AND Qt5Widgets_FOUND) ADD_EXECUTABLE(fteqccgui engine/qclib/qccguiqt.cpp engine/qclib/qccguistuff.c engine/qclib/comprout.c engine/qclib/hash.c engine/qclib/qcc_cmdlib.c engine/qclib/qcc_pr_comp.c engine/qclib/qcc_pr_lex.c engine/qclib/qccmain.c engine/qclib/decomp.c # engine/qclib/packager.c engine/qclib/qcd_main.c ) TARGET_INCLUDE_DIRECTORIES(fteqccgui PUBLIC ${Qt5Widgets_INCLUDE_DIRS} ${QSCINTILLA_INCLUDE_DIR}) SET_TARGET_PROPERTIES(fteqccgui PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES};${FTE_REVISON};${Qt5Widgets_COMPILE_DEFINITIONS}") SET_PROPERTY(TARGET fteqccgui PROPERTY POSITION_INDEPENDENT_CODE TRUE) TARGET_LINK_LIBRARIES(fteqccgui ${FTEQCC_LIBS} ${Qt5Widgets_LIBRARIES} ${QSCINTILLA_LIBRARY}) SET(INSTALLTARGS ${INSTALLTARGS} fteqccgui) ELSE() MESSAGE(WARNING "qscintilla/qt5widgets library not detected, no fteqccgui for you") ENDIF() ENDIF() ENDIF() ENDIF() IF(0) #software renderer plugin #not stable enough, and probably won't ever be ADD_LIBRARY(sw MODULE plugins/plugin.c engine/sw/sw_backend.c engine/sw/sw_image.c engine/sw/sw_rast.c #engine/sw/sw_viddos.c # engine/sw/sw_vidwin.c engine/common/mathlib.c # engine/client/in_win.c engine/sw/sw.h engine/sw/sw_spans.h ) SET_TARGET_PROPERTIES(sw PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES};SWQUAKE") TARGET_LINK_LIBRARIES(sw ${SYS_LIBS}) EMBED_PLUGIN_META(sw "Software Renderer" "Provides software rendering. Slow.") ENDIF() #Quake Injector Alike plugin SET(FTE_PLUG_QI true CACHE BOOL "Compile Quake-Injnector plugin.") IF(FTE_PLUG_QI) ADD_LIBRARY(plug_qi MODULE plugins/plugin.c plugins/qi/qi.c plugins/jabber/xml.c ) SET_TARGET_PROPERTIES(plug_qi PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_qi ${SYS_LIBS}) EMBED_PLUGIN_META(qi "Quaddicted Map Database" "Provides easy access to the quaddicted map database. Once installed you can use eg 'map qi_dopa:start' to begin playing dopa, or load it via the menus.") ENDIF() SET(FTE_PLUG_OPENSSL false CACHE BOOL "Compile OpenSSL.") IF(FTE_PLUG_OPENSSL) #the openssl license is incompatible with the GPL, so while we have code to use it distributing the binaries built with it is not a (legal) option. #note that openssl 3.0.0 upwards are apache-2 licensed, which IS gpl-3 compatible (though not gpl-2). debian has not caught up with that yet, however. #Crosscompile linux->win64: sudo ln -s ${pwd}/engine/libs-x86_64-w64-mingw32/openssl-openssl-3.0.1/ /usr/x86_64-w64-mingw32/OpenSSL SET(OPENSSL_USE_STATIC_LIBS true CACHE BOOL "Link openssl statically.") #usually as an .so though. :/) FIND_PACKAGE(OpenSSL) IF(OPENSSL_VERSION_MAJOR LESS 3) SET(FTE_PRIVATE_USE_ONLY false CACHE BOOL "Ignore license violations.") ENDIF() IF(NOT OPENSSL_FOUND) MESSAGE(WARNING "openssl library NOT available. you'll have to use some other library.") ELSEIF(OPENSSL_VERSION_MAJOR LESS 3 AND NOT FTE_PRIVATE_USE_ONLY) MESSAGE(WARNING "openssl v3 required for GPL compliance. Enable FTE_PRIVATE_USE_ONLY to compile openssl plugin.") ELSE() IF(OPENSSL_VERSION_MAJOR LESS 3) MESSAGE(WARNING "openssl library version is not 3 or above. You may not distribute plugin binaries due to license conflict.") ELSE() MESSAGE(WARNING "Using openssl. Resulting plugin must be licensed as GPLv3.") ENDIF() SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES}) if (WIN32) SET(OPENSSL_LIBRARIES ${OPENSSL_LIBRARIES} ws2_32) ENDIF() ADD_LIBRARY(plug_openssl MODULE plugins/plugin.c plugins/net_ssl_openssl.c ) TARGET_INCLUDE_DIRECTORIES(plug_openssl PRIVATE ${OPENSSL_INCLUDE_DIR}) SET_TARGET_PROPERTIES(plug_openssl PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_openssl ${SYS_LIBS} ${OPENSSL_LIBRARIES}) EMBED_PLUGIN_META(openssl "OpenSSL" "Provides OpenSSL support for dtls/tls/https support. The crypto library that is actually used is controlled via the tls_provider cvar.") ENDIF() ENDIF() #SET(FTE_PLUG_GNUTLS true CACHE BOOL "Compile GnuTLS Library.") #IF(FTE_PLUG_GNUTLS) # FIND_PACKAGE(GnuTLS) # IF(NOT GNUTLS_FOUND) # MESSAGE(WARNING "gnutls library NOT available. you'll have to use some other library.") # ELSE() # SET(FTE_LIB_DEFINES ${FTE_LIB_DEFINES}) # # ADD_LIBRARY(plug_gnutls MODULE # plugins/plugin.c # engine/common/net_ssl_gnutls.c # ) # SET_TARGET_PROPERTIES(plug_gnutls PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") # TARGET_LINK_LIBRARIES(plug_gnutls ${SYS_LIBS} ${GNUTLS_LIBRARIES}) # # EMBED_PLUGIN_META(gnutls "GnuTLS" "Provides GnuTLS support for dtls/tls/https support. The crypto library that is actually used is controlled via the tls_provider cvar.") # ENDIF() #ENDIF() #EzQuake Hud port plugin SET(FTE_PLUG_EZHUD true CACHE BOOL "Compile MoreQuakeWorld Hud plugin .") IF(FTE_PLUG_EZHUD) ADD_LIBRARY(plug_ezhud MODULE plugins/plugin.c plugins/ezhud/ezquakeisms.c plugins/ezhud/hud.c plugins/ezhud/hud_common.c plugins/ezhud/hud_editor.c ) SET_TARGET_PROPERTIES(plug_ezhud PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_ezhud ${SYS_LIBS}) EMBED_PLUGIN_META(ezhud "EzHud Plugin" "Provides compat with ezquake's hud scripts.") ENDIF() #NameMaker string generation plugin SET(FTE_PLUG_NAMEMAKER false CACHE BOOL "Compile namemaker plugin.") IF(FTE_PLUG_NAMEMAKER) ADD_LIBRARY(plug_namemaker MODULE plugins/plugin.c plugins/namemaker/namemaker.c ) SET_TARGET_PROPERTIES(plug_namemaker PROPERTIES COMPILE_DEFINITIONS "${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_namemaker ${SYS_LIBS}) EMBED_PLUGIN_META(namemaker "Name Maker Plugin" "Provides a lame UI for selecting arbitrary non-ascii glyphs as part of your nickname.") ENDIF() #Terrain Generation plugin SET(FTE_PLUG_TERRAINGEN false CACHE BOOL "Compile sample terrain generation plugin.") IF(FTE_PLUG_TERRAINGEN) ADD_LIBRARY(plug_terraingen MODULE plugins/plugin.c plugins/terrorgen/terragen.c ) SET_TARGET_PROPERTIES(plug_terraingen PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_terraingen ${SYS_LIBS}) EMBED_PLUGIN_META(terraingen "TerrainGen Plugin" "A lame example plugin for randomised terrain generation.") ENDIF() #IRC client plugin SET(FTE_PLUG_IRC true CACHE BOOL "Compile irc plugin.") IF(FTE_PLUG_IRC) ADD_LIBRARY(plug_irc MODULE plugins/plugin.c plugins/irc/ircclient.c ) SET_TARGET_PROPERTIES(plug_irc PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_REVISON};${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_irc ${SYS_LIBS}) EMBED_PLUGIN_META(irc "IRC Plugin" "Allows you to chat on IRC without tabbing out.") ENDIF() IF(ZLIB_FOUND) #mpq package format plugin (blizzard games) SET(FTE_PLUG_MPQ false CACHE BOOL "Compile mpq junk.") IF(FTE_PLUG_MPQ) ADD_LIBRARY(plug_mpq MODULE plugins/plugin.c plugins/mpq/blast.c plugins/mpq/fs_mpq.c ) SET_TARGET_PROPERTIES(plug_mpq PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_mpq ${SYS_LIBS} ${ZLIB_LIBRARIES}) EMBED_PLUGIN_META(irc "MPQ Archive Plugin" "Adds support for reading .mpq files. Not very useful...") ENDIF() ENDIF() #vpk package format plugin (halflife2) SET(FTE_PLUG_HL2 true CACHE BOOL "Compile support for hl2 file formats.") IF(FTE_PLUG_HL2) ADD_LIBRARY(plug_hl2 MODULE plugins/plugin.c plugins/hl2/hl2.c plugins/hl2/fs_vpk.c plugins/hl2/fs_vpk_vtmb.c plugins/hl2/fs_gma.c plugins/hl2/img_tth.c plugins/hl2/img_vtf.c plugins/hl2/mod_hl2.c plugins/hl2/mod_vbsp.c plugins/hl2/mat_vmt.c ) SET_TARGET_PROPERTIES(plug_hl2 PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;MULTITHREAD;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_hl2 ${SYS_LIBS} ${ZLIB_LIBRARIES}) EMBED_PLUGIN_META(hl2 "HalfLife2 Formats Plugin" "Adds support for reading various file formats used by HalfLife2. Requires mod support to be useful.") ENDIF() #model formats plugin SET(FTE_PLUG_MODELS true CACHE BOOL "Compile models formats plugin.") IF(FTE_PLUG_MODELS) ADD_LIBRARY(plug_models MODULE plugins/plugin.c plugins/models/models.c plugins/models/gltf.c ${DRACO_FILES} engine/common/json.c plugins/models/exportiqm.c ) SET_TARGET_PROPERTIES(plug_models PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${DRACO_CFLAGS};${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_models ${SYS_LIBS} ${DRACO_LIBRARY}) EMBED_PLUGIN_META(models "Models Plugin" "Kinda redundant now that the engine has gltf2 loading.") ENDIF() SET(FTE_PLUG_X11SV false CACHE BOOL "Compile x11 server plugin.") IF(FTE_PLUG_X11SV) #x11 server plugin (note: for displaying other programs) #not stable enough, and probably won't ever be ADD_LIBRARY(plug_x11sv MODULE plugins/plugin.c plugins/xsv/m_x.c plugins/xsv/x_reqs.c plugins/xsv/x_res.c engine/qclib/hash.c ) SET_TARGET_PROPERTIES(plug_x11sv PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") TARGET_LINK_LIBRARIES(plug_x11sv ${SYS_LIBS}) EMBED_PLUGIN_META(x11sv "X11 Server Plugin" "Provides a primitive X11 server in the form of a video decoder plugin.") ENDIF() #ffmpeg client plugin. no proper way to detect dependancies right now, so I've gotta try the manual way. SET(FTE_PLUG_FFMPEG true CACHE BOOL "Compile ffmpeg media plugin.") IF(FTE_PLUG_FFMPEG) FIND_PATH(AVCODEC_INCLUDE_DIR libavcodec/avcodec.h) FIND_PATH(AVFORMAT_INCLUDE_DIR libavformat/avformat.h) FIND_PATH(AVUTIL_INCLUDE_DIR libavutil/avutil.h) FIND_PATH(AVSWSCALE_INCLUDE_DIR libswscale/swscale.h) IF((AVFORMAT_INCLUDE_DIR) AND (AVSWSCALE_INCLUDE_DIR)) FIND_LIBRARY(AVCODEC_LIBRARY avcodec) FIND_LIBRARY(AVFORMAT_LIBRARY avformat) FIND_LIBRARY(AVUTIL_LIBRARY avutil) FIND_LIBRARY(AVSWSCALE_LIBRARY swscale) ADD_LIBRARY(plug_ffmpeg MODULE plugins/plugin.c plugins/avplug/avaudio.c plugins/avplug/avdecode.c plugins/avplug/avencode.c ) TARGET_INCLUDE_DIRECTORIES(plug_ffmpeg PUBLIC ${AVCODEC_INCLUDE_DIR} ${AVFORMAT_INCLUDE_DIR} ${AVUTIL_INCLUDE_DIR} ${AVSWSCALE_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(plug_ffmpeg ${SYS_LIBS} ${AVFORMAT_LIBRARY} ${AVCODEC_LIBRARY} ${AVUTIL_LIBRARY} ${AVSWSCALE_LIBRARY}) SET_TARGET_PROPERTIES(plug_ffmpeg PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") EMBED_PLUGIN_META(ffmpeg "FFMPEG Video Decoding Plugin" "Provides support for more audio formats, as well as video playback and better capture support.") ELSE() MESSAGE(WARNING "ffmpeg library NOT available. Quake shouldn't be playing fmv anyway.") ENDIF() ENDIF() SET(FTE_PLUG_TIMIDITY false CACHE BOOL "Compile timidity audio plugin.") IF(FTE_PLUG_TIMIDITY) #timidity FIND_PATH(TIMIDITY_INCLUDE_DIR timidity/timidity.h) IF(TIMIDITY_INCLUDE_DIR) FIND_LIBRARY(TIMIDITY_LIBRARY timidity) ADD_LIBRARY(plug_timidity MODULE plugins/plugin.c plugins/timidity.c ) TARGET_INCLUDE_DIRECTORIES(plug_timidity PUBLIC ${TIMIDITY_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(plug_timidity ${SYS_LIBS} ${TIMIDITY_LIBRARY}) SET_TARGET_PROPERTIES(plug_timidity PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") EMBED_PLUGIN_META(timidity "Timidity Plugin" "Provides support for playback of midi files.") ELSE() MESSAGE(WARNING "timidity library NOT available. We'll just stick to fake-cd music for hexen2.") ENDIF() ENDIF() #openxr plugin SET(FTE_PLUG_OPENXR true CACHE BOOL "Compile openxr plugin (for vr support).") IF(FTE_PLUG_OPENXR) FIND_PACKAGE(PkgConfig) IF (PKGCONFIG_FOUND) IF (NOT CMAKE_CROSSCOMPILING) #its picking up the linux headers then complaining that they're missing in mingw. also almost entirely untested so no great loss. PKG_SEARCH_MODULE(OPENXR openxr) ENDIF() IF (OPENXR_FOUND) ADD_LIBRARY(plug_openxr MODULE plugins/plugin.c plugins/openxr.c ) TARGET_INCLUDE_DIRECTORIES(plug_openxr PRIVATE ${OPENXR_INCLUDE_DIRS} ) IF (1) #dynamically link SET_TARGET_PROPERTIES(plug_openxr PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES};${FTE_DEFINES};XR_NO_PROTOTYPES") TARGET_LINK_LIBRARIES(plug_openxr ${SYS_LIBS}) ELSE() #statically link SET_TARGET_PROPERTIES(plug_openxr PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES};${FTE_DEFINES}") TARGET_LINK_LIBRARIES(plug_openxr ${SYS_LIBS} ${OPENXR_LIBRARIES}) ENDIF() EMBED_PLUGIN_META(openxr "OpenXR Plugin" "Provides support for Virtual Reality headsets and input devices.") ELSE() MESSAGE(WARNING "openxr library NOT available. Quake is already a reality anyway.") ENDIF() ENDIF() ENDIF() ##cef plugin ##libcef itself can be obtained from https://cef-builds.spotifycdn.com/index.html#linux64 (minimal builds, which still ends up with a 1,162,752,744 byte libcef.so - yes, actual size) ##(be sure to manually strip the binary of its debug info) ##to get this cmake stuff to recognise the headers etc: ## cd $FTEQW-REPO && ln -s $FOO/cef_binary_$FOO+chromium-$FOO_linux64_minimal plugins/cef/cef_linux64 ##(note that other systems use other subdir names) SET(FTE_PLUG_CEF true CACHE BOOL "Compile libcef (webbrowser) plugin.") SET(CEF_PATH ${CEF_PATH} CACHE PATH "Base location of libcef for target platform.") IF(FTE_PLUG_CEF) IF(CEF_PATH MATCHES "") UNSET(CEF_PATH CACHE) IF(WIN32) IF(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "AMD64") FIND_PATH (CEF_PATH include/cef_version.h plugins/cef/cef_windows64) ELSEIF(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86") FIND_PATH (CEF_PATH include/cef_version.h plugins/cef/cef_windows32) ELSEIF(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "ARM64") FIND_PATH (CEF_PATH include/cef_version.h plugins/cef/cef_windowsarm64) ENDIF() ELSEIF("${CMAKE_SYSTEM}" MATCHES "Linux") IF(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64") FIND_PATH (CEF_PATH include/cef_version.h plugins/cef/cef_linux64) ELSEIF(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i686") FIND_PATH (CEF_PATH include/cef_version.h plugins/cef/cef_linux32) ENDIF() ENDIF() ENDIF() #FIND_LIBRARY(CEF_LIBRARIES cef ${CEF_PATH}/Release) IF (CEF_PATH) ##statically link only for release builds. debug builds probably don't want to have to wait for ages for the debugger to finish loading debug info unless we're actually using this stuff. IF(CMAKE_BUILD_TYPE MATCHES "Release") SET(CEF_LIBRARIES "${CMAKE_BINARY_DIR}/libcef.so") ENDIF() ADD_LIBRARY(plug_cef MODULE plugins/plugin.c plugins/cef/cef.c ) TARGET_INCLUDE_DIRECTORIES(plug_cef PRIVATE ${CEF_PATH}) SET_TARGET_PROPERTIES(plug_cef PROPERTIES BUILD_RPATH_USE_ORIGIN true) if (CEF_LIBRARIES) SET_TARGET_PROPERTIES(plug_cef PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES};${FTE_DEFINES};LIBCEF_STATIC") TARGET_LINK_LIBRARIES(plug_cef ${SYS_LIBS} ${CEF_LIBRARIES} ${CMAKE_DL_LIBS}) ELSE() SET_TARGET_PROPERTIES(plug_cef PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES};${FTE_DEFINES};LIBCEF_DYNAMIC") TARGET_LINK_LIBRARIES(plug_cef ${SYS_LIBS} ${CMAKE_DL_LIBS}) ENDIF() IF(NOT ${UNIX}) ADD_CUSTOM_COMMAND( TARGET plug_cef PRE_LINK COMMAND cp ${CEF_PATH}/Release/* ${CMAKE_BINARY_DIR} COMMAND cp ${CEF_PATH}/Resources/* ${CMAKE_BINARY_DIR} ) ELSE() IF(CMAKE_BUILD_TYPE MATCHES "Release") #sigh, cef ain't stripped properly on linux. ADD_CUSTOM_COMMAND( TARGET plug_cef PRE_LINK COMMAND ln -f -s ${CEF_PATH}/Release/* ${CMAKE_BINARY_DIR} COMMAND strip ${CMAKE_BINARY_DIR}/libcef.so -o libcef.so COMMAND strip ${CMAKE_BINARY_DIR}/libEGL.so -o libEGL.so COMMAND strip ${CMAKE_BINARY_DIR}/libGLESv2.so -o libGLESv2.so COMMAND strip ${CMAKE_BINARY_DIR}/chrome-sandbox -o chrome-sandbox COMMAND ln -f -s ${CEF_PATH}/Resources/* ${CMAKE_BINARY_DIR} ) ELSE() ADD_CUSTOM_COMMAND( TARGET plug_cef PRE_LINK COMMAND ln -f -s ${CEF_PATH}/Release/* ${CMAKE_BINARY_DIR} COMMAND ln -f -s ${CEF_PATH}/Resources/* ${CMAKE_BINARY_DIR} ) ENDIF() ENDIF() EMBED_PLUGIN_META(cef "libcef(Browser) Plugin" "This plugin provides support for an in-game web browser.") ELSE() MESSAGE(WARNING "libcef library NOT available. no web browser support on walls.") ENDIF() ENDIF() SET(FTE_PLUG_XMPP true CACHE BOOL "Compile xmpp/jabber instant-messenger plugin.") IF(FTE_PLUG_XMPP) #XMPP/jabber client plugin ADD_LIBRARY(plug_xmpp MODULE plugins/plugin.c plugins/jabber/jabberclient.c plugins/jabber/xml.c plugins/jabber/jingle.c plugins/jabber/sift.c engine/common/sha1.c engine/common/sha2.c plugins/emailnot/md5.c ) SET_TARGET_PROPERTIES(plug_xmpp PROPERTIES COMPILE_DEFINITIONS "FTEPLUGIN;${FTE_LIB_DEFINES}") #for SRV lookups, so we actually get the right server from account names/etc. IF(ANDROID) #libresolv issues. ELSEIF(${WIN32}) #softlinks a dll ELSE() TARGET_LINK_LIBRARIES(plug_xmpp ${SYS_LIBS} resolv) ENDIF() EMBED_PLUGIN_META(xmpp "XMPP Plugin" "XMPP/Jabber instant messenger plugin for chatting without tabbing out.") ENDIF() INSTALL(TARGETS ${INSTALLTARGS} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/${FTE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/${FTE_INSTALL_LIBDIR}" ) IF(UNIX AND NOT APPLE) INSTALL(FILES ${CMAKE_SOURCE_DIR}/dist/linux/org.fteqw.fteqw.desktop DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications/") INSTALL(FILES ${CMAKE_SOURCE_DIR}/dist/org.fteqw.fteqw.svg DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps/") INSTALL(FILES ${CMAKE_SOURCE_DIR}/dist/linux/org.fteqw.fteqw.metainfo.xml DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo/") INSTALL(DIRECTORY ${CMAKE_SOURCE_DIR}/games DESTINATION "${CMAKE_INSTALL_PREFIX}/etc/xdg/fteqw" FILES_MATCHING PATTERN "*.fmf") ENDIF() SET(FTE_MENU_SYS true CACHE BOOL "Compile System Menu.") IF(FTE_MENU_SYS) ADD_CUSTOM_TARGET(menusys ALL VERBATIM DEPENDS fteqcc WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/quakec/menusys/" COMMAND fteqcc -srcfile "menu.src" -o "${CMAKE_CURRENT_BINARY_DIR}/menu.dat" -DREVISION="${SVNREVISION}" -DDATE="${FTE_DATE}" -DBRANCH="${FTE_BRANCH}" COMMAND /bin/echo -e "{\\n package fte_menusys\\n ver \"${SVNREVISION}\"\\n category Plugins\\n title \"Replacement Menus\"\\n gamedir \"id1\"\\n desc \"Modern menus to replace the ancient quake ones\"\\n}" | zip -j -q -9 -fz- "${CMAKE_CURRENT_BINARY_DIR}/menusys.pk3" - "${CMAKE_CURRENT_BINARY_DIR}/menu.dat" BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/menu.dat" "${CMAKE_CURRENT_BINARY_DIR}/menu.lno" "${CMAKE_CURRENT_BINARY_DIR}/menusys.pk3" SOURCES quakec/menusys/menu.src quakec/menusys/fteextensions.qc quakec/menusys/menusys/mitems.qc quakec/menusys/menusys/mitems_common.qc quakec/menusys/menusys/mitem_frame.qc quakec/menusys/menusys/mitem_desktop.qc quakec/menusys/menusys/mitem_exmenu.qc quakec/menusys/menusys/mitem_edittext.qc quakec/menusys/menusys/mitem_tabs.qc quakec/menusys/menusys/mitem_colours.qc quakec/menusys/menusys/mitem_checkbox.qc quakec/menusys/menusys/mitem_slider.qc quakec/menusys/menusys/mitem_combo.qc quakec/menusys/menusys/mitem_bind.qc quakec/menusys/menusys/mitem_spinnymodel.qc quakec/menusys/menu/loadsave.qc quakec/menusys/menu/newgame.qc quakec/menusys/menu/options_basic.qc quakec/menusys/menu/options_effects.qc quakec/menusys/menu/options_keys.qc quakec/menusys/menu/options.qc quakec/menusys/menu/presets.qc quakec/menusys/menu/servers.qc quakec/menusys/menu/main.qc quakec/menusys/menu/mods.qc quakec/menusys/menu/cvars.qc quakec/menusys/menu/updates.qc quakec/menusys/menu/options_audio.qc quakec/menusys/menu/options_configs.qc quakec/menusys/menu/options_hud.qc quakec/menusys/menu/options_particles.qc quakec/menusys/menu/options_video.qc quakec/menusys/menu/quit.qc ) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/menusys.pk3 DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/games/quake/id1/") ENDIF() SET(FTE_CSADDON true CACHE BOOL "CS Addon.") IF(FTE_CSADDON) ADD_CUSTOM_TARGET(csaddon ALL VERBATIM DEPENDS fteqcc WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/quakec/csaddon/src/" COMMAND fteqcc -srcfile "csaddon.src" -o "${CMAKE_CURRENT_BINARY_DIR}/csaddon.dat" COMMAND /bin/echo -e "{\\n package fte_csaddon\\n ver \"${SVNREVISION}\"\\n category Plugins\\n title \"${PLUGTITLE}\"\\n gamedir \"id1\"\\n desc \"${PLUGDESC}\"\\n}" | zip -j -q -9 -fz- "${CMAKE_CURRENT_BINARY_DIR}/csaddon.pk3" - "${CMAKE_CURRENT_BINARY_DIR}/csaddon.dat" BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/csaddon.dat" "${CMAKE_CURRENT_BINARY_DIR}/csaddon.lno" "${CMAKE_CURRENT_BINARY_DIR}/csaddon.pk3" SOURCES quakec/csaddon/src/csaddon.src quakec/csaddon/src/csplat.qc quakec/csaddon/src/csfixups.qc quakec/csaddon/src/editor_lights.qc quakec/csaddon/src/editor_terrain.qc quakec/csaddon/src/brush_selection.qc quakec/csaddon/src/brush_history.qc quakec/csaddon/src/brush_manip.qc quakec/csaddon/src/brush_draw.qc quakec/csaddon/src/brush_vertedit.qc quakec/csaddon/src/editor_brushes.qc quakec/csaddon/src/editor_ents.qc quakec/csaddon/src/textfield.qc quakec/csaddon/src/editor_particles.qc quakec/csaddon/src/menu.qc quakec/csaddon/src/cam.qc quakec/csaddon/src/csaddon.qc ) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/csaddon.pk3 DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/games/quake/id1/") ENDIF() ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author 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, see . Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Moe Ghoul, 1 April 1989 Moe Ghoul, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: README.md ================================================ # [FTEQW](https://fteqw.org) ![FTEQW Logo](engine/client/fte_eukara.ico) Powerful engine for playing and modding idTech based games. # What is FTEQW? FTEQW is an advanced and portable Quake engine. It supports multiple games running on idTech, plus its own set of games that developers have created. Due to the vast amount of supported formats, features, and innovations inside the engine and its very own QuakeC compiler (FTEQCC), it's very much considered the swiss-army knife of Quake engines. ### Highlights: - Single & multi-player support - Supports multiple games - Vast amount of map, model, & image formats are supported - Advanced console, with descriptions & autocompletion - Plugin support, enabling use of FFMPEG, Bullet/ODE physics & more - Extensive suite of QuakeC/entity debugging features - Deep integration with FTEQCC (fork of QuakeC created for FTEQW), which can even be executed in-game - Support for split-screen local multiplayer - Voice-chat via Opus & Speex - Support for hundreds of players on a single server - Works on Windows, Linux, OpenBSD... & more - New features are added all the time in cooperation with modders # Contributions Contributions and help is always welcomed. ### Guidelines: - Be kind and respectful - GPL2 licensed contributions are preferred, but plugins can be different but GPL-compatbile licenses - This codebase follows USA/EU/UK copyright laws - Always give credit from other codebases and make sure licenses are compatible - Test your changes and ensure nothing else has been broken (games, plugins, formats, etc) # Reporting Issues Bug reports are welcomed! :) ### Required Information: - Your system information such as your **Operating System** and **Hardware** (GPU/CPU) - If the binary is pre-built (e.g. from fteqw.org) or if it was built manually - What version of FTEQW you're using (type `version` in console) - If it is a supported game/mod/plugin/etc you're having issues with, then provide the version info for it, and tell us how it should be behaving - Make sure you have read the included documentation and ensure you have done everything right - Remember to double check the problem hasn't already been reported - Screenshots and/or video are generally desired if it is a visual malfunction **Windows Users** Please make sure you have not renamed your executable, `fteqw.exe`, to be `winquake.exe` or `glquake.exe` as Windows attempts compatability fixes that are not required for FTEQW and will cause problems. # Documentation Please see the `documentation` folder inside the repo for building, using the engine, tools, and more. The `specs` folder is for more advanced users seeking QuakeC and idTech file format related information or examples. # Contact ### Matrix https://matrix.to/#/#fte:matrix.org ### IRC **Server:** irc.quakenet.org **Channel:** #fte ### Forums **[Spike](https://forums.insideqc.com/memberlist.php?mode=viewprofile&u=26)** and **[eukara](https://forums.insideqc.com/memberlist.php?mode=viewprofile&u=949)** can be found on [insideqc.com](https://forums.insideqc.com/) ### Discord https://discord.gg/p2ag7x6Ca6 # Credits Please see the `Credits.md` file. # License Copyright (c) 2004-2025 FTE's team and its contributors Quake source (c) 1999 id Software FTEQW is supplied to you under the terms of the same license as the original Quake sources, the GNU General Public License Version 2. Please read the `LICENSE` file for details. # Download The latest source & binaries are always available at: [fteqw.org](https://fteqw.org) [fteqcc.org](https://fteqcc.org) ================================================ FILE: build_qc.sh ================================================ #!/bin/bash #FTEQCC and FTEQW must be defined. #QSS should be defined... BUILDFOLDER=~/htdocs BUILDLOGFOLDER=$BUILDFOLDER/build_logs if [ "$FTEQCC" == "" ]; then QSS=~/htdocs/qss/quakespasm-spiked-linux64 FTEQW=~/htdocs/linux_amd64/fteqw64 FTEQCC=~/htdocs/linux_amd64/fteqcc64 fi #this really should use the native cpu type... until then we use 32bit in case anyone's still using a 32bit kernel. if [ "$FTEQW" != "" ]; then echo "--- QC builds ---" echo "Making fteextensions.qc" BASEDIR=~/.fte GAMEDIR=fte mkdir -p $BASEDIR/$GAMEDIR/src #generate fte's extensions CFG=$BASEDIR/fte/minusargsaresilly.cfg echo "pr_dumpplatform -o fteextensions" > $CFG echo "pr_dumpplatform -o csqcsysdefs -Tcs" >> $CFG echo "pr_dumpplatform -o menusysdefs -Tmenu" >> $CFG $FTEQW -basedir $BASEDIR -nohome -quake -game $GAMEDIR +set snd_device none -nosound +set vid_renderer sv +exec minusargsaresilly.cfg +quit >> /dev/null #if we have fteqcc available then try to generate some symbol lists for our generic defs if [ "$FTEQCC" != "" ]; then #get QSS to spit out its defs for completeness. if [ "$QSS" != "" ] && [ -e "$BASEDIR/id1/pak1.pak" ] && [ -e "$QSS" ]; then CFG=$BASEDIR/$GAMEDIR/gen.cfg echo "pr_dumpplatform -Oqscsextensions -Tcs" > $CFG echo "pr_dumpplatform -Oqsextensions -Tss" >> $CFG echo "pr_dumpplatform -Oqsmenuextensions -Tmenu" >> $CFG $QSS -dedicated -nohome -game $GAMEDIR -basedir $BASEDIR +exec gen.cfg +quit >>/dev/null else echo "QSS not available?" fi if [ ! -e "$BASEDIR/fte/src/dpsymbols.src" ]; then ln -sr quakec/dpsymbols.src $BASEDIR/$GAMEDIR/src/ fi if [ ! -e "$BASEDIR/fte/src/dpdefs/" ]; then echo "no dpdefs subdir found in $BASEDIR/fte/src/ ... manual intervention required" fi #generate symbol tables from the various engines's defs ( cd $BASEDIR/$GAMEDIR/src $FTEQCC -Fdumpsymbols -oqsscs.dat qscsextensions.qc && sort -o qss_cs.sym qsscs.dat.sym $FTEQCC -Fdumpsymbols -oqssss.dat qsextensions.qc && sort -o qss_ss.sym qssss.dat.sym $FTEQCC -Fdumpsymbols -oqssmn.dat qsmenuextensions.qc && sort -o qss_menu.sym qssmn.dat.sym $FTEQCC -Fdumpsymbols -oftecs.dat -DCSQC fteextensions.qc && sort -o fte_cs.sym ftecs.dat.sym $FTEQCC -Fdumpsymbols -oftess.dat -DSSQC fteextensions.qc && sort -o fte_ss.sym ftess.dat.sym $FTEQCC -Fdumpsymbols -oftemn.dat -DMENU fteextensions.qc && sort -o fte_menu.sym ftemn.dat.sym $FTEQCC -Fdumpsymbols -odpcs.dat -DCSQC dpsymbols.src && sort -o dp_cs.sym dpcs.dat.sym $FTEQCC -Fdumpsymbols -odpss.dat -DSSQC dpsymbols.src && sort -o dp_ss.sym dpss.dat.sym $FTEQCC -Fdumpsymbols -odpmn.dat -DMENU dpsymbols.src && sort -o dp_menu.sym dpmn.dat.sym ) >>/dev/null #generate generic extensions CFG=$BASEDIR/fte/minusargsaresilly.cfg echo "pr_dumpplatform -Ocsqc_api -Fdepfilter -Tsimplecs" > $CFG echo "pr_dumpplatform -Ofte_csqc_api -Fdepfilter -Tcs" >> $CFG echo "pr_dumpplatform -Odp_csqc_api -Fdepfilter -Tdpcs" >> $CFG echo "pr_dumpplatform -Ossqc_api -Fdepfilter -Tnq" >> $CFG echo "pr_dumpplatform -Omenu_api -Fdepfilter -Tmenu" >> $CFG $FTEQW -basedir $BASEDIR -nohome -quake -game $GAMEDIR +set snd_device none -nosound +set vid_renderer sv +exec minusargsaresilly.cfg +quit >> /dev/null fi #fix up and copy the results somewhere useful rm $CFG mkdir -p $BUILDFOLDER/ftedefs $BUILDFOLDER/genericdefs mv $BASEDIR/$GAMEDIR/src/fteextensions.qc $BUILDFOLDER/ftedefs mv $BASEDIR/$GAMEDIR/src/csqcsysdefs.qc $BUILDFOLDER/ftedefs mv $BASEDIR/$GAMEDIR/src/menusysdefs.qc $BUILDFOLDER/ftedefs mv $BASEDIR/$GAMEDIR/src/*_api.qc $BUILDFOLDER/genericdefs fi if [ "$FTEQCC" != "" ]; then mkdir -p $BUILDFOLDER/csaddon/ ( cd quakec/csaddon/src echo -n "Making csaddon... " $FTEQCC -srcfile csaddon.src > $BUILDLOGFOLDER/csaddon.txt 2>&1 if [ $? -eq 0 ]; then echo "done" cp ../csaddon.dat $BUILDFOLDER/csaddon/ cd .. zip -q9 $BUILDFOLDER/csaddon/csaddon.pk3 csaddon.dat else echo "failed" fi ) ( cd quakec/menusys echo -n "Making menusys... " $FTEQCC -srcfile menu.src > $BUILDLOGFOLDER/menu.txt 2>&1 if [ $? -eq 0 ]; then echo "done" zip -q -q9 -o -r $BUILDFOLDER/csaddon/menusys_src.zip . cp ../menu.dat $BUILDFOLDER/csaddon/ cd .. zip -q9 $BUILDFOLDER/csaddon/menusys.pk3 menu.dat else echo "failed" fi ) else echo "Skiping csaddon + qcmenu, no compiler build" fi ================================================ FILE: build_setup.sh ================================================ #!/bin/bash #sets up dependancies for debian-jessie (8.7) #this script must be run twice. first time as root, which installs system packages #second time as a regular user (probably not your normal one), which installs 3rd-party stuff SVNROOT=$(cd "$(dirname "$BASH_SOURCE")" && pwd) FTEROOT=$(realpath $SVNROOT/..) FTEROOT=${FTEROOT:-~} FTECONFIG=$SVNROOT/build.cfg BUILDFOLDER=`echo ~`/htdocs BUILDLOGFOLDER=$BUILDFOLDER/build_logs #mac defaults OSXCROSSROOT=$FTEROOT/osxcross #emscripten defaults EMSCRIPTENROOT=$FTEROOT/emsdk-portable #android defaults ANDROIDROOT=$FTEROOT/android if [ ! -z "$(uname -o 2>&1 | grep Cygwin)" ]; then ANDROID_HOSTSYSTEM=windows-x86_64 else ANDROID_HOSTSYSTEM=linux-$(uname -m) fi ANDROIDBUILDTOOLS=25.0.0 ANDROID_ZIPALIGN=$ANDROIDROOT/build-tools/$ANDROIDBUILDTOOLS/zipalign #relative to ndk tools THREADS="-j 4" TARGETS_LINUX="qcc-rel rel dbg plugins-rel plugins-dbg" #gl-rel vk-rel TARGETS_WINDOWS="sv-rel m-rel qcc-rel qccgui-scintilla qccgui-dbg m-dbg sv-dbg plugins-dbg plugins-rel" #gl-rel vk-rel mingl-rel d3d-rel PLUGINS_DROID="qi ezhud irc hl2" PLUGINS_LINUXx86="openxr ode qi ezhud xmpp irc hl2" PLUGINS_LINUXx64="openxr ode qi ezhud xmpp irc hl2" PLUGINS_LINUXx32="qi ezhud xmpp irc hl2" PLUGINS_LINUXarmhf="qi ezhud xmpp irc hl2" PLUGINS_LINUXaarch64="qi ezhud xmpp irc hl2" if [ "$(uname -m)" != "x86_64" ]; then PLUGINS_LINUXx86="openxr ode qi ezhud xmpp irc hl2" fi if [ "$(uname -m)" == "x86_64" ]; then PLUGINS_LINUX64="openxr ode qi ezhud xmpp irc hl2" fi #windows is always cross compiled, so we don't have issues with non-native ffmpeg #windows doesn't cross compile, so no system dependancy issues #skip some dependancies if we're running on cygwin, ode is buggy. if [ "$(uname -s)" == "Linux" ]; then PLUGINS_WIN32="ode qi ezhud xmpp irc hl2" PLUGINS_WIN64="ode qi ezhud xmpp irc hl2" else PLUGINS_WIN32="qi ezhud xmpp irc hl2" PLUGINS_WIN64="qi ezhud xmpp irc hl2" fi echo echo "This is Spike's script to set up various cross compilers and dependancies." echo "This script will check dependancies. If something isn't installed you can either rerun the script as root (which will ONLY install system packages), or manually apt-get or whatever. You can then re-run the script as a regular user to finish configuring 3rd party dependancies." echo echo "You can change your choices later by just re-running this script" echo "(Your settings will be autosaved in $FTECONFIG)" echo echo "If you just want to compile a native build, just use the following command:" echo "cd $SVNROOT/engine && make gl-rel" echo "(if you're in cygwin, add FTE_TARGET=win32 to compile for native windows)" echo "(add plugins-rel qcc-rel qccgui-rel sv-rel vk-rel etc for additional targets)" echo "(or use -dbg if you want debug builds for whatever reason)" echo #always execute it if it exists, so that we preserve custom paths etc that are not prompted for here if [ -e $FTECONFIG ]; then . $FTECONFIG if [ $UID -eq 0 ]; then REUSE_CONFIG="y" #root shouldn't be writing/owning the config file. else REUSE_CONFIG="u" fi else if [ $UID -eq 0 ]; then exit #root can't create the output, as that would take ownership. else REUSE_CONFIG="n" fi fi if [ "$BUILD_CLEAN" == "n" ]; then NOUPDATE="y" fi #check args (and override config as desired) while [[ $# -gt 0 ]] do case $1 in -r) #for people that want to build a specific revision for some reason. SVN_REV_ARG="-r $2" NOUPDATE= shift ;; -j) THREADS="-j $2" shift ;; -help|--help) echo " -r VER Specifies the SVN revision to update to" echo " -j THREADS Specifies how many jobs to make with" echo " --help This text" exit 0 ;; -build|--build) #for custom build settings TARGET="FTE_CONFIG=$2" shift ;; --fast) #for people that want to live dangerously. BUILD_CLEAN="n" ;; --noupdate) #for people living privately or building old revisions... NOUPDATE="y" ;; --unattended) #don't prompt, use various defaults. UNATTENDED="y" REUSE_CONFIG="y" ;; *) echo "Unknown option $1" ;; esac shift done if [ "$REUSE_CONFIG" == "u" ]; then read -n 1 -p "Reuse previous build config? [y/N] " REUSE_CONFIG && echo REUSE_CONFIG=${REUSE_CONFIG:-n} fi if [ "$REUSE_CONFIG" != "y" ]; then #linux compiles are native-only, so don't bug out on cygwin which lacks a cross compiler. BUILD_LINUXx86=n BUILD_LINUXx64=n BUILD_LINUXx32=n BUILD_LINUXarmhf=n if [ "$(uname -s)" == "Linux" ]; then read -n 1 -p "Build for Linux x86? [Y/n] " BUILD_LINUXx86 && echo read -n 1 -p "Build for Linux x86_64? [Y/n] " BUILD_LINUXx64 && echo read -n 1 -p "Build for Linux x32? [y/N] " BUILD_LINUXx32 && echo #ubuntu's gcc-multilib-arm-foo package conflicts with gcc-multilib... #the whole point of multilib was to avoid conflicts... someone fucked up. #read -n 1 -p "Build for Linux armhf [y/N] " BUILD_LINUXarmhf && echo else echo "Skipping Linux options." fi BUILD_CYGWIN=n BUILD_MSVC=n if [ "$(uname -o)" == "Cygwin" ]; then read -n 1 -p "Build for Cygwin? [y/N] " BUILD_CYGWIN && echo read -n 1 -p "Build with MSVC? (requires windows7 sdk) [y/N] " BUILD_MSVC && echo else echo "Skipping Cygwin options." fi read -n 1 -p "Build for Windows x86? [Y/n] " BUILD_WIN32 && echo read -n 1 -p "Build for Windows x86_64? [Y/n] " BUILD_WIN64 && echo BUILD_DOS=n if [ "$(uname -o)" == "Cygwin" ]; then read -n 1 -p "Build for Dos? [y/N] " BUILD_DOS && echo fi BUILD_SDL_LINUXx86=n BUILD_SDL_LINUXx64=n BUILD_SDL_WIN32=n BUILD_SDL_WIN64=n if [ "$(uname -sm)" == "Linux i686" ]; then read -n 1 -p "Build for Linux x86 SDL? [y/N] " BUILD_SDL_LINUXx32 && echo fi if [ "$(uname -sm)" == "Linux x86_64" ]; then read -n 1 -p "Build for Linux x86_64 SDL? [y/N] " BUILD_SDL_LINUXx64 && echo fi read -n 1 -p "Build for Android? [y/N] " BUILD_ANDROID && echo read -n 1 -p "Build for Emscripten? [y/N] " BUILD_WEB && echo if [ 0 -ne 0 ]; then read -n 1 -p "Build for MacOSX? [y/N] " BUILD_MAC && echo else echo "Skipping mac option." fi fi BUILD_CLEAN=${BUILD_CLEAN:-y} BUILD_LINUXx86=${BUILD_LINUXx86:-y} BUILD_LINUXx64=${BUILD_LINUXx64:-y} BUILD_LINUXx32=${BUILD_LINUXx32:-n} BUILD_LINUXarmhf=${BUILD_LINUXarmhf:-n} BUILD_LINUXaarch64=${BUILD_LINUXaarch64:-n} BUILD_CYGWIN=${BUILD_CYGWIN:-n} BUILD_WIN32=${BUILD_WIN32:-y} BUILD_WIN64=${BUILD_WIN64:-y} BUILD_DOS=${BUILD_DOS:-n} BUILD_MSVC=${BUILD_MSVC:-n} BUILD_SDL=${BUILD_SDL:-n} BUILD_ANDROID=${BUILD_ANDROID:-n} BUILD_WEB=${BUILD_WEB:-n} BUILD_MAC=${BUILD_MAC:-n} if [ "$UID" != "0" ]; then echo "#path config for fte build scripts" >$FTECONFIG echo "THREADS=\"$THREADS\"" >>$FTECONFIG echo "BUILDFOLDER=\"$BUILDFOLDER\"" >>$FTECONFIG echo "BUILDLOGFOLDER=\"$BUILDLOGFOLDER\"" >>$FTECONFIG echo "SVNROOT=\"$SVNROOT\"" >>$FTECONFIG echo "ANDROIDROOT=\"$ANDROIDROOT\"" >>$FTECONFIG echo "export ANDROID_HOSTSYSTEM=\"$ANDROID_HOSTSYSTEM\"" >>$FTECONFIG echo "export ANDROID_ZIPALIGN=\"$ANDROID_ZIPALIGN\"" >>$FTECONFIG echo "EMSCRIPTENROOT=\"$EMSCRIPTENROOT\"" >>$FTECONFIG echo "OSXCROSSROOT=\"$OSXCROSSROOT\"" >>$FTECONFIG echo "BUILD_CLEAN=\"$BUILD_CLEAN\"" >>$FTECONFIG echo "BUILD_LINUXx86=\"$BUILD_LINUXx86\"" >>$FTECONFIG echo "BUILD_LINUXx64=\"$BUILD_LINUXx64\"" >>$FTECONFIG echo "BUILD_LINUXx32=\"$BUILD_LINUXx32\"" >>$FTECONFIG echo "BUILD_LINUXarmhf=\"$BUILD_LINUXarmhf\"" >>$FTECONFIG echo "BUILD_LINUXaarch64=\"$BUILD_LINUXaarch64\"" >>$FTECONFIG echo "BUILD_CYGWIN=\"$BUILD_CYGWIN\"" >>$FTECONFIG echo "BUILD_WIN32=\"$BUILD_WIN32\"" >>$FTECONFIG echo "BUILD_WIN64=\"$BUILD_WIN64\"" >>$FTECONFIG echo "BUILD_DOS=\"$BUILD_DOS\"" >>$FTECONFIG echo "BUILD_MSVC=\"$BUILD_MSVC\"" >>$FTECONFIG echo "BUILD_ANDROID=\"$BUILD_ANDROID\"" >>$FTECONFIG echo "BUILD_SDL=\"$BUILD_SDL\"" >>$FTECONFIG echo "BUILD_WEB=\"$BUILD_WEB\"" >>$FTECONFIG echo "BUILD_MAC=\"$BUILD_MAC\"" >>$FTECONFIG echo "TARGETS_WINDOWS=\"$TARGETS_WINDOWS\"" >>$FTECONFIG echo "TARGETS_LINUX=\"$TARGETS_LINUX\"" >>$FTECONFIG echo "PLUGINS_WIN32=\"$PLUGINS_WIN32\"" >>$FTECONFIG echo "PLUGINS_WIN64=\"$PLUGINS_WIN64\"" >>$FTECONFIG echo "PLUGINS_LINUXx86=\"$PLUGINS_LINUXx86\"" >>$FTECONFIG echo "PLUGINS_LINUXx64=\"$PLUGINS_LINUXx64\"" >>$FTECONFIG echo "PLUGINS_LINUXx32=\"$PLUGINS_LINUXx32\"" >>$FTECONFIG echo "PLUGINS_LINUXarmhf=\"$PLUGINS_LINUXarmhf\"" >>$FTECONFIG echo "PLUGINS_LINUXaarch64=\"$PLUGINS_LINUXaarch64\"" >>$FTECONFIG echo "PLUGINS_DROID=\"$PLUGINS_DROID\"" >>$FTECONFIG fi true true=$? false false=$? if [ "$(uname -s)" == "Linux" ]; then . /etc/os-release fi function debianpackages { #make sure apt-get is installed if [ -z `which apt-get 2>>/dev/null` ]; then return $false fi local ret=$true for i in "$@" do dpkg -s $i 2>&1 >> /dev/null if [ $? -eq 1 ]; then echo "Package missing: $i" ret=$false fi done if [ $ret == $false ]; then echo "Packages are not installed. Press enter to continue (or ctrl+c and install)." if [ "$UNATTENDED" != "y" ]; then read fi ret=$true fi return $ret } function jessiepackages { if [ "$PRETTY_NAME" != "Debian GNU/Linux 8 (jessie)" ]; then return $false fi debianpackages $@ return $? } #we don't really know what system we're on. assume they have any system dependancies. #fixme: args are programs findable with which function otherpackages { if [ -z "$PRETTY_NAME" ]; then return $true fi return $false } #Note: only the native linux-sdl target can be compiled, as libSDL[2]-dev doesn't support multiarch properly, and we depend upon it instead of building from source (thus ensuring it has whatever distro stuff needed... though frankly that should be inside the .so instead of the headers). #if [ $UID -eq 0 ] && [ ! -z `which apt-get` ]; then #because multiarch requires separate packages for some things, we'll need to set that up now (in case noone did that yet) # dpkg --add-architecture i386 # apt-get update #fi #generic crap. much of this is needed to set up and decompress dependancies and stuff. debianpackages git make automake libtool p7zip-full zip ca-certificates || otherpackages z7 make git || exit if [ "$BUILD_LINUXx86" == "y" ]; then #for building linux targets debianpackages gcc-multilib g++-multilib mesa-common-dev libasound2-dev libxcursor-dev || otherpackages gcc || exit jessiepackages libgnutls28-dev || debianpackages libgnutls28-dev || otherpackages gcc || exit if [[ "$PLUGINS_LINUXx86" =~ "ffmpeg" ]]; then debianpackages libswscale-dev libavcodec-dev || otherpackages || exit fi if [[ "$PLUGINS_LINUXx86" =~ "openxr" ]]; then debianpackages libopenxr-dev || otherpackages || exit fi fi if [ "$BUILD_LINUXx64" == "y" ]; then #for building linux targets debianpackages gcc-multilib g++-multilib mesa-common-dev libasound2-dev libxcursor-dev || otherpackages gcc || exit jessiepackages libgnutls28-dev || debianpackages libgnutls28-dev || otherpackages gcc || exit if [[ "$PLUGINS_LINUXx64" =~ "ffmpeg" ]]; then debianpackages libswscale-dev libavcodec-dev || otherpackages || exit fi if [[ "$PLUGINS_LINUXx64" =~ "openxr" ]]; then debianpackages libopenxr-dev || otherpackages || exit fi fi if [ "$BUILD_LINUXx32" == "y" ]; then #for building linux targets debianpackages gcc-multilib g++-multilib mesa-common-dev libasound2-dev libxcursor-dev || otherpackages gcc || exit jessiepackages libgnutls28-dev || debianpackages libgnutls28-dev || otherpackages gcc || exit fi if [ "$BUILD_LINUXarmhf" == "y" ]; then #for building linux targets debianpackages gcc-multilib-arm-linux-gnueabihf g++-multilib-arm-linux-gnueabihf mesa-common-dev libasound2-dev libxcursor-dev || otherpackages gcc || exit jessiepackages libgnutls28-dev || debianpackages libgnutls28-dev || otherpackages gcc || exit fi if [ "$BUILD_SDL" == "y" ]; then #for building SDL targets debianpackages libSDL1.2-dev libSDL2-dev libspeex-dev libspeexdsp-dev || otherpackages || exit fi if [ "$BUILD_WIN32" == "y" ] || [ "$BUILD_WIN64" == "y" ]; then #for building windows targets #tools package provides pkg-config #python is needed to configure scintilla properly. debianpackages mingw-w64 mingw-w64-tools python || otherpackages x86_64-w64-mingw32-gcc python || exit fi if [ "$BUILD_ANDROID" == "y" ]; then ( (jessiepackages openjdk-8-jdk-headless || debianpackages openjdk-8-jdk-headless ) && debianpackages ant) || otherpackages || exit fi if [ "$BUILD_WEB" == "y" ]; then ( (jessiepackages cmake || debianpackages cmake) && debianpackages git build-essential) || exit fi if [ "$BUILD_MAC" == "y" ]; then debianpackages git cmake libxml2-dev fuse || otherpackages || exit fi debianpackages subversion make build-essential || otherpackages svn make || exit echo "System Package checks complete." if [ "$UID" == "0" ]; then #avoid root taking ownership of anything. echo "Refusing to update/rebuild toolchains as root." echo "Please continue running this script as a regular user." exit fi if [ "$UNATTENDED" != "y" ]; then echo echo "(Any new toolchains will be installed to $FTEROOT)" echo "(Say no if you're certain you already set up everything)" read -n 1 -p "Rebuild/update any toolchains now? [y/N] " REBUILD_TOOLCHAINS && echo else REBUILD_TOOLCHAINS="y" fi REBUILD_TOOLCHAINS=${REBUILD_TOOLCHAINS:-n} mkdir -p $FTEROOT #dos shit if [ "$BUILD_DOS" == "y" ] && [ $UID -ne 0 ] && [ $REBUILD_TOOLCHAINS == "y" ]; then echo "You'll need to manually install djgpp for DOS builds." fi #android shit. WARNING: should come first as it spits out some EULAs that need confirming. if [ "$BUILD_ANDROID" == "y" ] && [ $UID -ne 0 ] && [ $REBUILD_TOOLCHAINS == "y" ]; then mkdir -p $ANDROIDROOT cd $ANDROIDROOT wget -N https://dl.google.com/android/repository/tools_r25.2.3-linux.zip unzip -qn tools_r25.2.3-linux.zip cd tools/bin #yes, android-8 is fucking old now. newer versions won't work on older devices. echo "downloading android build tools" ./sdkmanager "build-tools;$ANDROID_BUILDTOOLS" echo "downloading android platform tools" ./sdkmanager "platform-tools" echo "downloading android-9" ./sdkmanager "platforms;android-9" echo "downloading android ndk" ./sdkmanager "ndk-bundle" cd ~ fi #emscripten/web shit if [ "$BUILD_WEB" == "y" ] && [ $UID -ne 0 ] && [ $REBUILD_TOOLCHAINS == "y" ]; then mkdir -p $EMSCRIPTENROOT cd $EMSCRIPTENROOT/.. wget -N https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz cd $EMSCRIPTENROOT tar xzf ../emsdk-portable.tar.gz --strip-components=1 ./emsdk install latest ./emsdk activate latest cd ~ fi #osxcross, for mac crap if [ "$BUILD_MAC" == "y" ] && [ $UID -ne 0 ] && [ $REBUILD_TOOLCHAINS == "y" ] && [ "$UNATTENDED" != "y" ]; then echo "Setting up OSXCross... THIS IS TOTALLY UNTESTED" read -p "You need to download xcode first. Where did you download the .dmg file to?" XCODE git clone https://github.com/tpoechtrager/osxcross.git $OSXCROSSROOT cd $OSXCROSSROOT tools/gen_sdk_package_darling_dmg.sh $XCODE cp *.tar.xz SDK_VERSION=10.10 UNATTENDED=0 ./build.sh cd ~ fi if [ $UID -ne 0 ] && [ $REBUILD_TOOLCHAINS == "y" ]; then #initial checkout of fte's svn if [ "$NOUPDATE"!="n" ]; then if [ ! -d $SVNROOT ]; then svn checkout https://svn.code.sf.net/p/fteqw/code/trunk $SVNROOT $SVN_REV_ARG else cd $SVNROOT svn up $SVN_REV_ARG fi fi #FIXME: there may be race conditions when compiling. #so make sure we've pre-built certain targets without using -j #linux distros vary too much with various dependancies and versions and such, so we might as well pre-build our own copies of certain libraries. this really only needs to be done once, but its safe to retry anyway. cd $SVNROOT/engine if [ "$BUILD_LINUXx86" == "y" ]; then echo "Making libraries (linux x86)..." make FTE_TARGET=linux32 makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_LINUXx64" == "y" ]; then echo "Making libraries (linux x86_64)..." make FTE_TARGET=linux64 makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_LINUXx32" == "y" ]; then echo "Making libraries (linux x32)..." make FTE_TARGET=linuxx32 makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_LINUXarmhf" == "y" ]; then echo "Making libraries (linux armhf)..." make FTE_TARGET=linuxarmhf makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_LINUXaarch64" == "y" ]; then echo "Making libraries (linux aarch64)..." make FTE_TARGET=linuxaarch64 makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_WIN32" == "y" ]; then echo "Making libraries (win32)..." make FTE_TARGET=win32 makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_WIN64" == "y" ]; then echo "Making libraries (win64)..." make FTE_TARGET=win64 makelibs CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi #These plugins have external 3rd-party dependancies that are downloaded as part of building. if [ "$BUILD_WIN32" == "y" ] && [[ "$PLUGINS_WIN32" =~ "ode" ]]; then echo "Prebuilding ODE library (win32)..." make FTE_TARGET=win32 plugins-rel NATIVE_PLUGINS=ode 2>&1 >>/dev/null fi if [ "$BUILD_WIN64" == "y" ] && [[ "$PLUGINS_WIN64" =~ "ode" ]]; then echo "Prebuilding ODE library (win64)..." make FTE_TARGET=win64 plugins-rel NATIVE_PLUGINS=ode 2>&1 >>/dev/null fi if [ "$BUILD_LINUXx86" == "y" ] && [[ "$PLUGINS_LINUXx86" =~ "ode" ]]; then echo "Prebuilding ODE library (linux x86)..." make FTE_TARGET=linux32 plugins-rel NATIVE_PLUGINS=ode CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_LINUXx64" == "y" ] && [[ "$PLUGINS_LINUXx64" =~ "ode" ]]; then echo "Prebuilding ODE library (linux x86_64)..." make FTE_TARGET=linux64 plugins-rel NATIVE_PLUGINS=ode CPUOPTIMISATIONS=-fno-finite-math-only 2>&1 >>/dev/null fi if [ "$BUILD_WIN32" == "y" ] && [[ "$PLUGINS_WIN32" =~ "ffmpeg" ]]; then echo "Obtaining ffmpeg library (win32)..." make FTE_TARGET=win32 plugins-rel NATIVE_PLUGINS=ffmpeg 2>&1 >>/dev/null fi if [ "$BUILD_WIN64" == "y" ] && [[ "$PLUGINS_WIN64" =~ "ffmpeg" ]]; then echo "Obtaining ffmpeg library (win64)..." make FTE_TARGET=win64 plugins-rel NATIVE_PLUGINS=ffmpeg 2>&1 >>/dev/null fi cd ~ fi echo "Setup script complete." echo "When you run build_wip.sh output will be written to $BUILDFOLDER/*" ================================================ FILE: build_wip.sh ================================================ #!/bin/bash START=$(date +%s) SVNROOT=$(cd "$(dirname "$(readlink "$BASH_SOURCE")")" && pwd) FTECONFIG=$SVNROOT/build.cfg HOME=`echo ~` BASE=$SVNROOT/.. #set this if you want non-default branding, for customised builds. #export BRANDING=wastes #defaults, if we're not set up properly. #should be overriden in build.cfg BUILDFOLDER=$HOME/htdocs BUILDLOGFOLDER=$BUILDFOLDER/build_logs SVNROOT=$BASE/fteqw-code BUILD_LINUXx86=y BUILD_LINUXx64=y BUILD_WIN32=y BUILD_WIN64=y BUILD_ANDROID=y BUILD_WEB=y PLUGINS_LINUXx86="qi ezhud xmpp irc hl2" PLUGINS_LINUXx64="qi ezhud xmpp irc hl2" PLUGINS_LINUXx32="qi ezhud xmpp irc hl2" PLUGINS_WIN32="ffmpeg ode qi ezhud xmpp irc hl2" PLUGINS_WIN64="ffmpeg ode qi ezhud xmpp irc hl2" THREADS="-j 4" TARGETS_LINUX="qcc-rel rel dbg vk-rel plugins-rel plugins-dbg" TARGETS_WINDOWS="sv-rel m-rel qcc-rel qccgui-scintilla qccgui-dbg m-dbg sv-dbg plugins-dbg plugins-rel" TARGETS_WEB="gl-rel" if [ -e $FTECONFIG ]; then . $FTECONFIG else echo "WARNING: $FTECONFIG does not exist yet." fi if [ "$BUILD_CLEAN" == "n" ]; then NOUPDATE="y" fi #check args (and override config as desired) while [[ $# -gt 0 ]] do case $1 in -r) SVN_REV_ARG="-r $2" NOUPDATE= shift ;; -j) THREADS="-j $2" shift ;; -help|--help) echo " -r VER Specifies the SVN revision to update to" echo " -j THREADS Specifies how many jobs to make with" echo " --help This text" echo " --noupdate Don't do svn updates" echo " --unclean Don't do make clean, for faster rebuilds" echo " --web Build web target (excluding all others)" echo " --droid Build android target (excluding others)" exit 0 ;; -build|--build) TARGET="FTE_CONFIG=$2" shift ;; --noupdate) NOUPDATE="y" ;; --unclean) BUILD_CLEAN="n" ;; --web) BUILD_LINUXx86="n" BUILD_LINUXx64="n" BUILD_LINUXarmhf="n" BUILD_LINUXaarch64="n" BUILD_WIN32="n" BUILD_WIN64="n" BUILD_ANDROID="n" BUILD_WEB="y" ;; --droid) BUILD_LINUXx86="n" BUILD_LINUXx64="n" BUILD_LINUXarmhf="n" BUILD_LINUXaarch64="n" BUILD_WIN32="n" BUILD_WIN64="n" BUILD_ANDROID="y" BUILD_WEB="n" ;; *) echo "Unknown option $1" ;; esac shift done MAKEARGS="$THREADS $TARGET" ########### Emscripten / Web Stuff export EMSDK=$EMSCRIPTENROOT #export WEB_PREJS="--pre-js $HOME/prejs.js" ########### Android Stuff. so messy... #This is some android password that you should keep private. You should keep the keystore file private too, of course. Frankly, that part is more important than this small random number. KEYPASSFILE=$BASE/.fte_keypass if [ ! -e $KEYPASSFILE ]; then dd if=/dev/urandom count=9 bs=1 2>/dev/null | base64 > $KEYPASSFILE chmod 400 $KEYPASSFILE fi KEYPASS=`cat $KEYPASSFILE` export JAVA_HOME=/usr if [ ! -z "$ANDROIDROOT" ]; then export ANDROID_HOME=$ANDROIDROOT fi if [ ! -z "$ANDROIDNDKROOT" ]; then export ANDROID_NDK_ROOT=$ANDROIDNDKROOT else export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk-bundle fi export KEYTOOLARGS="-keypass $KEYPASS -storepass $KEYPASS -dname \"CN=fteqw.com, OU=ID, O=FTE, L=Unknown, S=Unknown, C=GB\"" export JARSIGNARGS="-storepass $KEYPASS" ########### Various Output etc Paths QCCBUILDFOLDER=$BUILDFOLDER/fteqcc SVNFOLDER=$SVNROOT/engine/release ARCHIVEFOLDER=$BUILDFOLDER/archive SVNDBGFOLDER=$SVNROOT/engine/debug WARNINGLEVEL="-w" FILELOCK=$BASE/.fte_buildlock #./ccache-alias.sh exec 9>$FILELOCK if ! flock -n 9 ; then echo "Build script is already running!"; exit 1 fi mkdir -p $BUILDLOGFOLDER if [ ! -d $SVNROOT ]; then #just in case... svn checkout https://svn.code.sf.net/p/fteqw/code/trunk $SVNROOT $SVN_REV_ARG fi cd $SVNROOT/ if [ "$NOUPDATE" != "y" ]; then echo "SVN Update" svn update $SVN_REV_ARG fi cd engine date > $BUILDLOGFOLDER/buildlog.txt echo "Starting build" >> $BUILDLOGFOLDER/buildlog.txt function build { BUILDSTART=$(date +%s) NAME=$1 DEST=$2 shift; shift if [ "$BUILD_CLEAN" != "n" ]; then make clean >> /dev/null fi echo -n "Making $NAME... " date > $BUILDLOGFOLDER/$DEST.txt echo BUILD: $NAME >> $BUILDLOGFOLDER/$DEST.txt echo PLUGINS: $NATIVE_PLUGINS >> $BUILDLOGFOLDER/$DEST.txt echo make $MAKEARGS $* >> $BUILDLOGFOLDER/$DEST.txt 2>&1 make $MAKEARGS $* >> $BUILDLOGFOLDER/$DEST.txt 2>&1 if [ $? -eq 0 ]; then BUILDEND=$(date +%s) BUILDTIME=$(( $BUILDEND - $BUILDSTART )) echo "$BUILDTIME seconds" echo "$NAME done, took $BUILDTIME seconds" >> $BUILDLOGFOLDER/buildlog.txt rm -rf $BUILDFOLDER/$DEST >> /dev/null 2>&1 mkdir $BUILDFOLDER/$DEST 2>> /dev/null mkdir $BUILDFOLDER/$DEST/debug 2>> /dev/null cp $SVNFOLDER/* $BUILDFOLDER/$DEST >> /dev/null 2>> /dev/null cp $SVNDBGFOLDER/* $BUILDFOLDER/$DEST/debug >> /dev/null 2>> /dev/null rm -rf $BUILDFOLDER/$DEST/*.a >> /dev/null 2>&1 rm -rf $BUILDFOLDER/$DEST/debug/*.a >> /dev/null 2>&1 rmdir $BUILDFOLDER/$DEST/debug 2>> /dev/null else echo "$NAME failed" >> $BUILDLOGFOLDER/buildlog.txt echo "failed" fi } function build_fteqcc { echo "--- no code ---" } echo "--- Engine builds ---" #the -fno-finite-math-only is to avoid a glibc dependancy if [ "$BUILD_LINUXx86" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_LINUXx86" build "Linux 32-bit" linux_x86 FTE_TARGET=linux32 CPUOPTIMIZATIONS=-fno-finite-math-only $TARGETS_LINUX fi if [ "$BUILD_LINUXx64" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_LINUXx64" build "Linux 64-bit" linux_amd64 FTE_TARGET=linux64 CPUOPTIMIZATIONS=-fno-finite-math-only $TARGETS_LINUX fi if [ "$BUILD_LINUXx32" != "n" ]; then # CFLAGS="-DNO_JPEG" NATIVE_PLUGINS="$PLUGINS_LINUXx32" build "Linux x32" linux_x32 FTE_TARGET=linux_x32 CPUOPTIMIZATIONS=-fno-finite-math-only $TARGETS_LINUX fi if [ "$BUILD_LINUXarmhf" != "n" ]; then #debian/ubuntu's armhf targets armv7. we instead target armv6, because that means we work on rpi too (but still with hard-float). It should be compatible although we likely need more ops. NATIVE_PLUGINS="$PLUGINS_LINUXarmhf" build "Linux ARMhf" linux_armhf FTE_TARGET=linux_armhf CPUOPTIMIZATIONS=-fno-finite-math-only $TARGETS_LINUX fi if [ "$BUILD_LINUXaarch64" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_LINUXaarch64" build "Linux aarch64" linux_aarch64 FTE_TARGET=linux_aarch64 CPUOPTIMIZATIONS=-fno-finite-math-only $TARGETS_LINUX fi if [ "$BUILD_CYGWIN" != "n" ]; then NATIVE_PLUGINS="qi ezhud" build "Cygwin" cygwin qcc-rel rel dbg plugins-rel plugins-dbg fi if [ "$BUILD_WIN32" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_WIN32" build "Windows 32-bit" win32 FTE_TARGET=win32 CFLAGS="$WARNINGLEVEL" $TARGETS_WINDOWS fi if [ "$BUILD_WIN64" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_WIN64" build "Windows 64-bit" win64 FTE_TARGET=win64 CFLAGS="$WARNINGLEVEL" $TARGETS_WINDOWS fi if [ "$BUILD_MSVC" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_WIN32" build "Windows MSVC 32-bit" msvc FTE_TARGET=vc BITS=32 CFLAGS="$WARNINGLEVEL" sv-rel gl-rel vk-rel mingl-rel m-rel d3d-rel qcc-rel qccgui-scintilla qccgui-dbg gl-dbg sv-dbg plugins-dbg plugins-rel NATIVE_PLUGINS="$PLUGINS_WIN64" build "Windows MSVC 64-bit" msvc FTE_TARGET=vc BITS=64 CFLAGS="$WARNINGLEVEL" sv-rel gl-rel vk-rel mingl-rel m-rel d3d-rel qcc-rel qccgui-scintilla qccgui-dbg gl-dbg sv-dbg plugins-dbg plugins-rel fi export NATIVE_PLUGINS="qi ezhud xmpp irc" if [ "$BUILD_ANDROID" != "n" ]; then NATIVE_PLUGINS="$PLUGINS_DROID" build "Android" android droid-rel fi if [ "$BUILD_DOS" == "y" ]; then #no networking makes dedicated servers useless. and only a crappy sw renderer is implemented right now. #the qcc might be useful to someone though! build "DOS" dos m-rel qcc-rel fi if [ "$BUILD_WEB" != "n" ]; then source $EMSDK/emsdk_env.sh >> /dev/null LTO= build "Emscripten" web FTE_TARGET=web $TARGETS_WEB CC=emcc fi if [ "$BUILD_SDL_LINUXx86" == "y" ]; then build "Linux 32-bit (SDL)" linux_x86_sdl FTE_TARGET=SDL2 BITS=32 $TARGETS_SDL fi if [ "$BUILD_SDL_LINUXx64" == "y" ]; then build "Linux 64-bit (SDL)" linux_amd64_sdl FTE_TARGET=SDL2 BITS=64 $TARGETS_SDL fi if [ "$BUILD_SDL_WIN32" == "y" ]; then build "Windows 32-bit (SDL)" win32_sdl FTE_TARGET=win32_SDL $TARGETS_SDL # CFLAGS="$WARNINGLEVEL -DNOLEGACY -DOMIT_QCC" build "Windows 32-bit nocompat" nocompat FTE_TARGET=win32 LTO=1 NOCOMPAT=1 BOTLIB_CFLAGS="" BOTLIB_OBJS="" $TARGETS_SDL fi if [ "$BUILD_SDL_WIN64" == "y" ]; then build "Windows 64-bit (SDL)" win64_sdl FTE_TARGET=win64_SDL $TARGETS_SDL fi ####build "MorphOS" morphos CFLAGS="-I$BASE/morphos/os-include/ -I$BASE/morphos/lib/ -L$BASE/morphos/lib/ -I$BASE/zlib/zlib-1.2.5 -L$BASE/zlib/zlib-1.2.5 -I./libs $WARNINGLEVEL" gl-rel mingl-rel sv-rel qcc-rel if [ "$BUILD_MAC" != "n" ]; then #build "MacOSX" macosx_tiger CFLAGS="-I$BASE/mac/x86/include/ -L$BASE/mac/x86/lib -I./libs" FTE_TARGET=macosx_x86 sv-rel gl-rel mingl-rel qcc-rel #FIXME: figure out how to do universal binaries or whatever they're called build "MacOSX 32-bit" osx32 CC=o32-clang CXX=o32-clang++ FTE_TARGET=osx_x86 BITS=32 sv-rel gl-rel mingl-rel qcc-rel build "MacOSX 64-bit" osx64 CC=o64-clang CXX=o64-clang++ FTE_TARGET=osx_x86_64 BITS=64 sv-rel gl-rel mingl-rel qcc-rel fi #third party stuff / misc crap if [ "$BUILD_WEB" != "n" ]; then cp $BASE/3rdparty/web/* $BUILDFOLDER/web/ fi if [ "$BUILD_WIN32" != "n" ]; then if [ -e "$BASE/3rdparty/win32/3rdparty.zip" ]; then cp $BASE/3rdparty/win32/3rdparty.zip $BUILDFOLDER/win32/3rdparty.zip else rm -f $BUILDFOLDER/win32/3rdparty.zip fi # if [ "$BUILD_SDL_WIN32" != "n" ]; then # cp $SVNROOT/engine/libs/SDL2-2.0.1/i686-w64-mingw32/bin/SDL2.dll $BUILDFOLDER/win32_sdl # fi fi if [ "$BUILD_WIN64" != "n" ]; then if [ -e "$BASE/3rdparty/win64/3rdparty.zip" ]; then cp $BASE/3rdparty/win64/3rdparty.zip $BUILDFOLDER/win64/3rdparty.zip else rm -f $BUILDFOLDER/win64/3rdparty.zip fi # if [ "$BUILD_SDL_WIN64" != "n" ]; then # cp $SVNROOT/engine/libs/SDL2-2.0.1/x86_64-w64-mingw32/bin/SDL2.dll $BUILDFOLDER/win64_sdl # fi fi if [ -e "$HOME/nocompat_readme.html" ]; then cp $HOME/nocompat_readme.html $BUILDFOLDER/nocompat/README.html fi #call out to build_qc.sh to invoke native builds as appropriate. case "$(uname -m)" in x86_64) if [ "$BUILD_LINUXx64" != "n" ]; then rm -rf $QCCBUILDFOLDER 2>&1 mkdir -p $QCCBUILDFOLDER cd $SVNROOT/ FTEQCC=$BUILDFOLDER/linux_amd64/fteqcc64 FTEQW=$BUILDFOLDER/linux_amd64/fteqw64 QSS=$BUILDFOLDER/qss/quakespasm-spiked-linux64 ./build_qc.sh fi ;; i386 | i486 | i586) if [ "$BUILD_LINUXx86" != "n" ]; then rm -rf $QCCBUILDFOLDER 2>&1 mkdir -p $QCCBUILDFOLDER cd $SVNROOT/ FTEQCC=$BUILDFOLDER/linux_x86/fteqcc32 FTEQW=$BUILDFOLDER/linux_x86/fteqw32 QSS= ./build_qc.sh fi ;; esac cd $SVNROOT/engine/ svn info > $BUILDFOLDER/version.txt if [ "$BUILD_LINUXx86" != "n" ]; then cp $BUILDFOLDER/linux_x86/fteqcc32 $QCCBUILDFOLDER/linux32-fteqcc fi if [ "$BUILD_LINUXx64" != "n" ]; then cp $BUILDFOLDER/linux_amd64/fteqcc64 $QCCBUILDFOLDER/linux64-fteqcc fi if [ "$BUILD_LINUXx32" != "n" ]; then cp $BUILDFOLDER/linux_x32/fteqccx32 $QCCBUILDFOLDER/linuxx32-fteqcc fi if [ "$BUILD_LINUXarmhf" != "n" ]; then cp $BUILDFOLDER/linux_armhf/fteqccarmhf $QCCBUILDFOLDER/linuxarmhf-fteqcc fi if [ "$BUILD_LINUXaarch64" != "n" ]; then cp $BUILDFOLDER/linux_armhf/fteqccaarch64 $QCCBUILDFOLDER/linuxaarch64-fteqcc fi if [ "$BUILD_WIN32" != "n" ]; then cp $BUILDFOLDER/win32/fteqcc.exe $QCCBUILDFOLDER/win32-fteqcc.exe cp $BUILDFOLDER/win32/fteqccgui.exe $QCCBUILDFOLDER/win32-fteqccgui.exe fi if [ "$BUILD_WIN64" != "n" ]; then cp $BUILDFOLDER/win64/fteqcc64.exe $QCCBUILDFOLDER/win64-fteqcc.exe cp $BUILDFOLDER/win64/fteqccgui64.exe $QCCBUILDFOLDER/win64-fteqccgui.exe fi #cp $BUILDFOLDER/morphos/fteqcc $QCCBUILDFOLDER/morphos-fteqcc #cp $BUILDFOLDER/macosx_tiger/fteqcc $QCCBUILDFOLDER/macosx_tiger-fteqcc cp $BUILDFOLDER/version.txt $QCCBUILDFOLDER/version.txt if [ "$BUILD_WIN32" != "n" ] && [ "$BUILD_WIN64" != "n" ]; then echo Archiving output SVNVER=$(svnversion $SVNROOT) if [ -e $ARCHIVEFOLDER ]; then cd $BUILDFOLDER/ zip -q -9 $ARCHIVEFOLDER/win_fteqw_$SVNVER.zip win32/fteglqw.exe win32/fteqwsv.exe win32/fteqccgui.exe win32/debug/fteglqw.exe win64/fteqw.exe win64/debug/fteglqw.exe fi if [ -e $BUILDFOLDER/fteqw_for_windows.zip ]; then cd $BUILDFOLDER/win32/ zip -q -j -9 $BUILDFOLDER/fteqw_for_windows.zip fteglqw.exe fteqwsv.exe fteqccgui.exe fteplug_qi_x86.dll fteplug_xmpp_x86.dll fteplug_irc_x86.dll fteplug_ezhud_x86.dll cd $HOME/3rdparty_win32/ zip -q -9 $BUILDFOLDER/fteqw_for_windows.zip ogg.dll vorbis.dll vorbisfile.dll freetype6.dll zlib1.dll mkdir -p $BASE/tmp/fte cd $BASE/tmp/ cp $BUILDFOLDER/csaddon/menu.dat fte zip -q -9 $BUILDFOLDER/fteqw_for_windows.zip fte/menu.dat fi #~/afterquake/updatemini.sh fi echo "All done" END=$(date +%s) DIFF=$(( $END - $START )) MINS=$(( $DIFF / 60 )) echo "Total Compile Time: $MINS minutes" >> $BUILDLOGFOLDER/buildlog.txt echo "Total Compile Time: $MINS minutes" cd $HOME #./errorlog.sh #cd $HOME #rm .bitchxrc #cp ./fteqw/.bitchxrc ./ #./BitchX -a irc.quakenet.org -A -c "#fte" -n A_Gorilla ================================================ FILE: dist/linux/com.riverbankcomputing.qscintilla.install.patch ================================================ diff -ur QScintilla_src-2.13.4.orig/src/qscintilla.pro QScintilla_src-2.13.4/src/qscintilla.pro --- QScintilla_src-2.13.4.orig/src/qscintilla.pro 2023-01-15 19:13:30.751242600 +0100 +++ QScintilla_src-2.13.4/src/qscintilla.pro 2023-04-18 01:42:25.794709139 +0200 @@ -70,22 +70,22 @@ # Scintilla namespace rather than pollute the global namespace. #DEFINES += SCI_NAMESPACE -target.path = $$[QT_INSTALL_LIBS] +target.path = /app/lib INSTALLS += target -header.path = $$[QT_INSTALL_HEADERS] +header.path = /app/include header.files = Qsci INSTALLS += header -trans.path = $$[QT_INSTALL_TRANSLATIONS] +trans.path = /app/share/qt5/translations trans.files = qscintilla_*.qm INSTALLS += trans -qsci.path = $$[QT_INSTALL_DATA] +qsci.path = /app/share/qt5 qsci.files = ../qsci INSTALLS += qsci -features.path = $$[QT_HOST_DATA]/mkspecs/features +features.path = /app/lib/qt5/mkspecs/features CONFIG(staticlib) { features.files = $$PWD/features_staticlib/qscintilla2.prf } else { ================================================ FILE: dist/linux/linter-exceptions.json ================================================ { "org.fteqw.fteqw": { "appstream-external-screenshot-url": "Screenshots do not need mirroring to Flathub", "runtime-is-eol-org.kde.Platform-5.15-23.08": "fteqccgui needs migrating to Qt6 and fteqw to ffmpeg 7.x and SDL3 for current runtimes", "module-fteqw-source-git-branch": "fteqw has no tagged releases" } } ================================================ FILE: dist/linux/org.fteqw.fteqw.desktop ================================================ [Desktop Entry] Version=1.0 Type=Application Name=FTEQW Game Engine Comment=Cross-platform port for the Quake game engine Exec=fteqw %u Icon=org.fteqw.fteqw Terminal=false Categories=Game; MimeType=application/x-quakeworlddemo;x-scheme-handler/quake;x-scheme-handler/qw; Actions=quake;rerel;netquake;quake2;quake3;hexen2;hexen2mp [Desktop Action quake] Name=Play Quake Exec=fteqw %u -quake [Desktop Action rerel] Name=Play Quake Rerelease Exec=fteqw %u -quake_rerel [Desktop Action netquake] Name=Play Classic Quake Exec=fteqw %u -netquake [Desktop Action quake2] Name=Play QuakeII Exec=fteqw %u -quake2 [Desktop Action quake3] Name=Play Quake III Arena Exec=fteqw %u -quake [Desktop Action hexen2] Name=Play Hexen2 Exec=fteqw %u -hexen2 [Desktop Action hexen2mp] Name=Play Hexen2 Mission Pack Exec=fteqw %u -portals ================================================ FILE: dist/linux/org.fteqw.fteqw.fixdownloads.patch ================================================ diff --git a/engine/client/m_download.c b/engine/client/m_download.c index 2d618c1b1..74a4a51b4 100644 --- a/engine/client/m_download.c +++ b/engine/client/m_download.c @@ -2205,7 +2205,9 @@ static void PM_PreparePackageList(void) { int parm; qofs_t sz = 0; - char *f = FS_MallocFile(INSTALLEDFILES, FS_ROOT, &sz); + char full_filename[256]; + snprintf(full_filename, sizeof(full_filename), "%s/%s", getenv("XDG_DATA_HOME"), INSTALLEDFILES); + char *f = FS_MallocFile(full_filename, FS_ROOT, &sz); loadedinstalled = true; if (f) { @@ -3526,11 +3528,13 @@ static void PM_WriteInstalledPackages(void) int i; char *s; package_t *p; - vfsfile_t *f = FS_OpenVFS(INSTALLEDFILES, "wbp", FS_ROOT); + char full_filename[256]; + snprintf(full_filename, sizeof(full_filename), "%s/%s", getenv("XDG_DATA_HOME"), INSTALLEDFILES); + vfsfile_t *f = FS_OpenVFS(full_filename, "wbp", FS_ROOT); qboolean v3 = false; if (!f) { - if (FS_DisplayPath(INSTALLEDFILES, FS_ROOT, buf, sizeof(buf))) + if (FS_DisplayPath(full_filename, FS_ROOT, buf, sizeof(buf))) Con_Printf("package manager: Can't write %s\n", buf); else Con_Printf("package manager: Can't update installed list\n"); ================================================ FILE: dist/linux/org.fteqw.fteqw.fixhomedir.patch ================================================ diff --git a/engine/common/fs.c b/engine/common/fs.c index 68b59d1b9..412d01bb7 100644 --- a/engine/common/fs.c +++ b/engine/common/fs.c @@ -466,7 +466,7 @@ static searchpath_t *gameonly_gamedir; char com_gamepath[MAX_OSPATH]; //c:\games\quake char com_homepath[MAX_OSPATH]; //c:\users\foo\my docs\fte\quake -qboolean com_homepathenabled; +qboolean com_homepathenabled = true; static qboolean com_homepathusable; //com_homepath is safe, even if not enabled. //char com_configdir[MAX_OSPATH]; //homedir/fte/configs @@ -2527,20 +2527,21 @@ static const char *FS_GetCleanPath(const char *pattern, qboolean silent, char *o if (*s == ':') { if (s == pattern+1 && (s[1] == '/' || s[1] == '\\')) - Con_ThrottlePrintf(&throttletimer, 0, "Error: absolute path in filename %s\n", pattern); - else + Con_ThrottlePrintf(&throttletimer, 0, "Warning: absolute path in filename %s\n", pattern); + else { Con_ThrottlePrintf(&throttletimer, 0, "Error: alternative data stream in filename %s\n", pattern); - return NULL; + return NULL; + } } else if (*s == '\\' || *s == '/' || !*s) { //end of segment if (o == seg) { - if (o == outbuf) - { - Con_ThrottlePrintf(&throttletimer, 0, "Error: absolute path in filename %s\n", pattern); - return NULL; - } + // if (o == outbuf) + // { + // Con_ThrottlePrintf(&throttletimer, 0, "Error: absolute path in filename %s\n", pattern); + // return NULL; + // } if (!*s) { *o++ = '\0'; @@ -2987,7 +2988,7 @@ vfsfile_t *QDECL FS_OpenVFS(const char *filename, const char *mode, enum fs_rela } else { - if (!try_snprintf(fullname, sizeof(fullname), "%s%s/%s", com_homepath, gamedirfile, filename)) + if (!try_snprintf(fullname, sizeof(fullname), "%s/%s", com_homepath, filename)) return NULL; if (*mode == 'w') COM_CreatePath(fullname); ================================================ FILE: dist/linux/org.fteqw.fteqw.metainfo.xml ================================================ org.fteqw.fteqw FTEQW Forethought Entertainment https://github.com/fte-team/fteqw/graphs/contributors CC0-1.0 GPL-2.0 Modern Quake source port org.fteqw.fteqw.desktop

FTEQW is a highly versatile game engine originally based on QuakeWorld with support for a number of Quake engine games.

Features:

  • Single- and multiplayer support
  • Supports multiple games: Quake, QuakeWorld, Quake II, Quake III Arena, Hexen II, ...
  • A vast amount of map, model and image formats are supported
  • Advanced console, with descriptions and autocompletion
  • Plugin support, enabling use of FFMPEG, Bullet/ODE physics and more
  • Extensive suite of QuakeC/entity debugging features
  • Deep integration with FTEQCC, which can even be executed in-game
  • Support for split-screen local multiplayer
  • Voice-chat via Opus and Speex
  • Support for hundreds of players on a single server
  • Works on Windows, Linux, OpenBSD and more!
  • New features are added all the time in cooperation with modders!
#c2a6f4 #1e191e fteqw fteqcc fteqccgui ftemaster qtv gamepad keyboard pointing retro fps shooter sourceport quake https://fteqw.org/ https://github.com/fte-team/fteqw/issues https://github.com/fte-team/fteqw Introduction level rendering in the default graphics setting via OpenGL https://www.fteqw.org/img/q1/1.png Integrated server browser with filtering capabilities https://www.fteqw.org/img/q1/2.png Graphics preset imitating a 256-colour software renderer https://www.fteqw.org/img/q1/3.png More advanced graphics setting with real-time lights, advanced particle engine and light coronas https://www.fteqw.org/img/q1/4.png EZHud plugin for user-defined HUDs https://www.fteqw.org/img/qw/4.png Optimised multiplayer HUD and player name tags https://www.fteqw.org/img/qw/3.png Action Quake II running in FTEQW https://www.fteqw.org/img/q2/4.png Quake II https://www.fteqw.org/img/q2/3.png Quake III Arena https://www.fteqw.org/img/q3/2.png HeXen II https://www.fteqw.org/img/h2/1.png HeXen II's particle system https://www.fteqw.org/img/h2/3.png HeXen II with realtime shadows and lighting https://www.fteqw.org/img/h2/4.png intense mild intense
================================================ FILE: dist/linux/org.fteqw.fteqw.sh ================================================ #!/bin/bash function check_game_data () { if [[ "$1" == "" ]]; then zenity --error --ok-label "Quit" --width=400 --title "Could not find Quake game data" \ --text "Please copy the Quake game data files (at least pak0.pak) to $XDG_DATA_HOME/id1/." exit 1 fi } function check_exit_code () { if [[ "$1" != "0" ]]; then zenity --error --ok-label "Quit" --width=400 --title "fteqw exited with an error" \ --text "For a detailed error message, please run fteqw from a terminal window using\n flatpak run $FLATPAK_ID." exit 1 fi } /app/bin/fteqw -basedir $XDG_DATA_HOME "$@" check_exit_code $? ================================================ FILE: dist/linux/org.fteqw.fteqw.yml ================================================ --- # Flatpak runtime version issues: # - 22.08 has a slightly-too-old version of SDL2 that doesn't include SDL_misc.h # - 24.08 includes ffmpeg 7.x, which fteqw fails to build against # # Options to resolve this: # - manually build ffmpeg 6.x? on the 24.08 runtime # - manually build a newer SDL2 on the 22.08 runtime # - update fteqw to use SDL3 and ffmpeg 7.x (which are the 24.08 runtime versions) app-id: org.fteqw.fteqw command: org.fteqw.fteqw.sh # runtime: org.freedesktop.Platform # runtime-version: "23.08" # sdk: org.freedesktop.Sdk # The KDE runtime includes Qt which is useful for building fteqccgui. # However, Qt 5.15 is EOL so this runtime doesn't work super well. runtime: org.kde.Platform runtime-version: "5.15-23.08" sdk: org.kde.Sdk finish-args: # Network for multiplayer - --share=network # Hardware 3D and game controllers - --device=all # GUI - --share=ipc - --socket=fallback-x11 - --socket=wayland # Audio - --socket=pulseaudio cleanup: - /include - "*.a" - /lib/cmake - /lib/pkgconfig - /share/aclocal - /share/doc - /share/man modules: - name: bullet buildsystem: cmake-ninja config-opts: - -DOpenGL_GL_PREFERENCE=GLVND - -DBUILD_SHARED_LIBS=ON - -DBULLET2_MULTITHREADING=ON - -DUSE_GRAPHICAL_BENCHMARK=OFF - -DBUILD_EGL=OFF - -DBUILD_EXTRAS=OFF - -DBUILD_CPU_DEMOS=OFF - -DBUILD_OPENGL3_DEMOS=OFF - -DBUILD_BULLET2_DEMOS=OFF - -DBUILD_UNIT_TESTS=OFF - -DINSTALL_LIBS=ON sources: - type: archive url: https://github.com/bulletphysics/bullet3/archive/3.25.tar.gz dest-filename: bullet.tar.gz sha256: c45afb6399e3f68036ddb641c6bf6f552bf332d5ab6be62f7e6c54eda05ceb77 x-checker-data: type: json url: https://api.github.com/repos/bulletphysics/bullet3/releases/latest version-query: .tag_name url-query: .tarball_url - name: ode buildsystem: cmake-ninja config-opts: - -DODE_WITH_DEMOS=OFF - -DODE_WITH_TESTS=OFF cleanup: - /bin/ode-config sources: - type: archive url: https://bitbucket.org/odedevs/ode/downloads/ode-0.16.6.tar.gz sha256: c91a28c6ff2650284784a79c726a380d6afec87ecf7a35c32a6be0c5b74513e8 x-checker-data: type: anitya project-id: 2532 url-template: https://bitbucket.org/odedevs/ode/downloads/ode-$version.tar.gz - name: openxr buildsystem: cmake-ninja sources: - type: archive url: https://github.com/KhronosGroup/OpenXR-SDK/archive/release-1.1.47.tar.gz sha256: 82c7f4e3658578a22e438b5f005ecaf22c3f724b09fe031fa0f8ffc97c30c9ba x-checker-data: type: json url: https://api.github.com/repos/KhronosGroup/OpenXR-SDK/releases/latest version-query: .tag_name | sub("release-", "") url-query: .tarball_url # libcef fails to build for reasons beyond my ability to debug # - name: libcef # buildsystem: cmake-ninja # no-make-install: true # config-opts: # - -DCMAKE_BUILD_TYPE=RelWithDbgInfo # make-args: # - libcef_dll_wrapper # build-commands: # - mkdir -p /app/cef/libcef_dll_wrapper # - cp -R ./include /app/include # - cp -R ./Release /app/lib # - cp -R ./Resources /app/cef # - cp -R ./libcef_dll_wrapper/libcef_dll_wrapper.a /app/lib/libcef_dll_wrapper.a # cleanup: # - "*.a" # - "./*" # sources: # - type: archive # url: https://cdn-fastly.obsproject.com/downloads/cef_binary_4280_linux64.tar.bz2 # sha256: d91c78349ecbfbfdfc18d5f882dc28b939028f1a631191c603b5d0d938ada972 # QScintilla is used by fteqccgui - name: qscintilla buildsystem: qmake subdir: src cleanup: - /lib/qt5 - /share/qt5 make-install-args: - DESTDIR=/app - PREFIX=/app config-opts: - CONFIG+=release sources: - type: archive url: https://www.riverbankcomputing.com/static/Downloads/QScintilla/2.14.1/QScintilla_src-2.14.1.tar.gz sha256: dfe13c6acc9d85dfcba76ccc8061e71a223957a6c02f3c343b30a9d43a4cdd4d x-checker-data: type: anitya project-id: 10082 url-template: https://www.riverbankcomputing.com/static/Downloads/QScintilla/$version/QScintilla_src-$version.tar.gz - type: patch path: com.riverbankcomputing.qscintilla.install.patch - name: fteqw buildsystem: cmake-ninja builddir: true config-opts: - -DFTE_USE_SDL=true - -DFTE_ENGINE_SERVER_ONLY=false - -DFTE_TOOL_QTV=true - -DFTE_TOOL_MASTER=true - -DFTE_TOOL_HTTPSV=false - -DFTE_TOOL_QCC=true - -DFTE_TOOL_IQM=false - -DFTE_TOOL_IMAGE=false - -DFTE_INSTALL_BINDIR=bin - -DFTE_PLUG_FFMPEG=false sources: # Launcher script - type: file path: org.fteqw.fteqw.sh - type: git url: https://github.com/fte-team/fteqw.git branch: master # In an ideal world, fteqw would be built off of tagged releases so the Flatpak infra can auto-update: # - type: archive # url: https://api.github.com/repos/fte-team/fteqw/zipball/v6202 # dest-filename: fteqw.zip # sha256: 1955a7c7eb2e6c3ba2da689d115d10f89c6e5e261577e67cd0ad176bea1b97ef # x-checker-data: # type: json # url: https://api.github.com/repos/fte-team/fteqw/releases/latest # version-query: .tag_name | sub("^v"; "") # url-query: .zipball_url # Patches to fix Flatpak sandbox compliance, currently WIP - type: patch path: org.fteqw.fteqw.fixdownloads.patch - type: patch path: org.fteqw.fteqw.fixhomedir.patch post-install: - install -Dm755 ${FLATPAK_BUILDER_BUILDDIR}/org.fteqw.fteqw.sh /app/bin/ ================================================ FILE: documentation/Building.md ================================================ > Yay, you found out the secrit location to download the sauce code from! > > Right, urm, now what? > Yeah, good question. > > Urm. # Preface Welcome to the building guide for FTEQW, as there are many systems supported, there is a number of ways to build the engine. This should cover most, if not all the supported systems and methods. ### Contents - [Repo Layout](#repo-layout) - [Compiling](#compiling) - [Easy Build Bot System (Linux)](#easy-build-bot-system-linux) - [Windows Systems (cygwin)](#windows-systems-cygwin) - [Linux/BSD Systems](#linux-bsd-system) - [Android (FTEDroid) with cygwin](#android-ftedroid-with-cygwin) - [Browser (emscripten)](#browser-emscripten) - [FTEQCC](#fteqcc) - [FTEIMG](#fteimg) - [FTEIQM](#fteiqm) - [FTEQTV](#fteqtv) - [Plugins](#plugins) # Repo Layout - `.github/workflows:` Github Actions source files. - `documentation:` General help and introduction. - `engine:` FTEQW game engine itself. Both client and dedicated server. - `engine/release:` The Makefile writes its release-build binaries here. Intermediate files are contained within a sub-directory. - `engine/debug:` The Makefile writes its debug-build binaries here. Intermediate files are contained within a sub-directory. - `fteqtv:` The QTV proxy server program. - `plugins:` several optional plugins that do various interesting things, though not so interesting. - `q3asm2:` Spike's quick hack at a QVM Assembler which is not horribly slow. Ignore it. - `quakec:` Various QuakeC mods. Some interesting, some not. - `quakec/basemod:` TimeServ's attempt to bugfix and modify vanilla Quake. - `quakec/csaddon:` In-game CSQC-controlled editors. Currently contains the camquake featureset (thanks Jogi), rtlights editor, terrain editor ui, particle editor. - `quakec/csqctest:` Spike's CSQC sample mod. Originally created as a feature testbed for the CSQC API. Useful as a reference/sample, but you perhaps don't want to use it as a base. - `specs:` Modder/Advanced documentation and samples. # Compiling Compiling FTEQW is straightforward once you have the bare minimum of build dependencies (see `Dependencies.md` for more info). For the binaries hosted here, we choose to statically link against many of the dependencies for portability reasons, while also linking against recent versions of libc for security reasons. All binaries hosted here were built inside the `engine` dir of the src tree using GNU make, aka gmake. ### Build Systems You have the choice of two build systems: - Make - CMake This guide will show commands for both. ## Easy Build Bot System (Linux) If you want to set up a Linux box that cross-compiles each target with your own private customisations, then you can run the `build_setup.sh` script to set up which targets you wish to support. You can then just run the `build_wip.sh` script any time your code changes to have it rebuild every target you previously picked. The setup script will install **android+emscripten** dependancies for you, so you're likely to find this an easier way to deal with those special targets. ### Notes - The Android SDK can be a big download, while installing emscripten may require several hours to compile clang and about **40gb** of disk space if emscripten doesn't provide prebuilt stuff for your distro. - The script can also be run from cygwin, but does not support compiling for Linux then. ## Windows Systems (cygwin) If you want to compile a Win64 build in cygwin, it should be as simple as: make makelibs FTE_TARGET=win64 make gl-rel FTE_TARGET=win64 or TODO You only should need gcc and make installed in cygwin for this. ### Notes It's currently not recommended to build using MSYS2, due to issues with zlib. ## Linux/BSD Systems It's usually as straight-forward as: make makelibs make m-rel or TODO ### Notes - You can also change `FTE_TARGET` to be `win32`, `SDL2` and on Linux systems `linux32` and `linux64`. - On BSD, you don't need to pass anything specific, but they should also compile the `linux` targets as well as the `SDL2` target fine. - Not building with `makelibs` will attempt to dynamically link against your system-level versions of dependencies. Sometimes you want this, sometimes you don't. You definitely want that if you're trying to link against the Steam runtime. ## Renders ### Vulkan make vk-rel ### OpenGL make gl-rel make glcl-rel make mingl-rel make mcl-rel ### DirectX make d3d-rel ## Android (FTEDroid) with cygwin The phone port requires the Android SDk and can be compiled with the following command: make droid-rel PATH=C:\Cygwin\bin\ DROID_SDK_PATH=/path/to/android-sdk DROID_NDK_PATH=/path/to/android-ndk-r7 ANT=/path/to/apache-ant-1.8.2/bin/ant JAVATOOL="/path/to/jdk1.7.0_02/bin/" DROID_ARCH="armeabi x86" -j4 DROID_PACKSU=/path/to/pak0.pak or -DFTE_ENGINE_FTEDROID=TRUE On Linux/Unix systems you can omit the `PATH`, `ANT`, and `JAVATOOL` parts as they should already be in the path. The `DROID_PACKSU` part is used to include the PAK file within the android package. Ideally you would use a PK3 file instead. Also you would use something that will not violate id Software's copyright. THIS IS AN EXAMPLE ONLY. You can omit the setting entirely if you require the user to provide their own packages. Finally, install the `FTEDroid.apk` file on your Android device which should be located under the `release` folder. ### Notes - There is no way to install the package with a different name at this time. - Touchscreen controls are built-in. - The APK looks for game data under: Android/data/com.fteqw/files - Configs may be located at: /fte or sdcard/fte ## Browser (emscripten) make FTE_TARGET=web web-rel or -DFTE_PLUG_CEF=TRUE ## FTEQCC make qcc-rel or -DFTE_TOOL_QCC=TRUE ## FTEQCC GUI make qccgui-rel or -DFTE_TOOL_QCCGUI=TRUE ## Standalone QCVM TODO or -DFTE_TOOL_QCVM=TRUE ## FTE Dedicated Server make sv-rel or FTE_ENGINE_SERVER_ONLY=TRUE ## FTE Master Server make master-rel or -DFTE_TOOL_MASTER=TRUE ## FTE Image Tool make imgtool-rel or -DFTE_TOOL_IMAGE=TRUE ## FTE IQM Tool make iqm-rel or -DFTE_TOOL_IQM=TRUE ## FTEQTV make qtv-rel or -DFTE_TOOL_QTV=TRUE ## Small HTTP Server make httpserver or -DFTE_TOOL_HTTPSV=TRUE ## Plugins To build all currently stable plugins, it's as simple as: make plugins-rel or TODO You can specify which plugins get compiled by passing PLUGINS_NATIVE as an example: make plugins-rel NATIVE_PLUGINS="ffmpeg bullet irc" or -DFTE_PLUG_FFMPEG=TRUE -DFTE_PLUG_BULLET=TRUE -DFTE_PLUG_IRC=TRUE The list of available plugins: > [!IMPORTANT] > Some plugins will require additional dependencies or flags on some systems, see `Dependencies.md` for more info. - Bullet Physics > Provides Rigid Body Physics. bullet -DFTE_PLUG_BULLET=TRUE - Call of Duty (1 & 2) Format Support > Provides compatability with Call Of Duty's file formats. cod -DFTE_PLUG_COD=TRUE - EzHUD > Provides compat with ezquake's hud scripts. ezhud -DFTE_PLUG_EZHUD=TRUE - FFMPEG Video Decoding & RTMP Streaming > Provides support for more audio formats, as well as video playback and better capture support. ffmpeg -DFTE_PLUG_FFMPEG=TRUE - GnuTLS > Provides GnuTLS support for dtls/tls/https support. The crypto library that is actually used is controlled via the tls_provider cvar. gnutls -DFTE_PLUG_GNUTLS=TRUE - Half-Life 2 > Adds support for reading various file formats used by Half-Life 2. hl2 -DFTE_PLUG_HL2=TRUE - IRC > Allows you to chat on IRC without tabbing out. irc -DFTE_PLUG_IRC=TRUE - libcef(Browser) Plugin >This plugin provides support for an in-game web browser. libcef -DFTE_PLUG_CEF=TRUE - Name Maker Plugin > Provides a lame UI for selecting arbitrary non-ascii glyphs as part of your nickname. namemaker -DFTE_PLUG_NAMEMAKER=TRUE - MPQ Archive Plugin > Adds support for reading .mpq files (Diablo 1 + 2, World of Warcraft). mpq -DFTE_PLUG_MPQ=TRUE - ODE Physics > Provides Rigid Body Physics behaviours. ode -DFTE_PLUG_ODE=TRUE - OpenSSL > Provides OpenSSL support for dtls/tls/https support. The crypto library that is actually used is controlled via the tls_provider cvar. openssl -DFTE_PLUG_OPENSSL=TRUE - OpenXR Support > Provides support for Virtual Reality headsets and input devices. openxr -DFTE_PLUG_OPENXR=TRUE - Quaddicted Map Database (Quake Injector) > Provides easy access to the quaddicted map database. Once installed you can use eg 'map qi_dopa:start' to begin playing dopa, or load it via the menus. qi -DFTE_PLUG_QI=TRUE - Quake 3 Game Logic and VM Support > Provides compatability with Quake3's gamecode. quake3 -DFTE_PLUG_QUAKE3=TRUE - TerrainGen Plugin > A lame example plugin for randomised terrain generation. terraingen -DFTE_PLUG_TERRAINGEN=TRUE - Timidity Plugin > Provides support for playback of midi files. timidity -DFTE_PLUG_TIMIDITY=TRUE - XMPP/Jabber Protocol Support > XMPP/Jabber instant messenger plugin for chatting without tabbing out. xmpp -DFTE_PLUG_XMPP=TRUE - X11 Display Server (Standalone) > Provides a primitive X11 server in the form of a video decoder plugin. x11server -DFTE_PLUG_X11SV=TRUE ================================================ FILE: documentation/Credits.md ================================================ # Credits ### Developers - **[Spike](https://github.com/Shpoike)** - Creator of FTEQW & FTEQCC - **[eukara](https://github.com/eukara)** - Maintainer, QuakeC guru ### Contributors - **[4LT](https://github.com/4LT)** - QC bugfixes - **Andreas Kirsch** - QC Additions & bugfixes - **[Blub](https://github.com/blubs)** - IQM & QC bugfixes - **Circlemaster** - Demo & Console additions - **[Daniel Svensson](https://github.com/dsvensson)** - Github Actions & Bugfixes - **[erysdren](https://github.com/erysdren)** - Haiku OS port - **[ewhac](https://github.com/ewhac)** - Joystick bugfixes & .gitignore - **[Fix](https://github.com/fhomolka)** - Cleanup & Bugfixes - **Hexum** - Linux Port & Fixes - **James "Ender" Brown** - Half-Life Model Support - **Jogi** - Menu Bugfixes - **[JohnNy_cz](https://github.com/johnnycz)** - Bugfixes - **KrimZon** - GLSL Additions & Bugfixes - **Lance "Moodles"** - Many Additions & Bugfixes - **Luis Gutierrez** - IQM Fixes - **Magnus** - **Mark Olsen** - Many Additions & Bugfixes - **Molgrum** - Many Additions & Bugfixes - **Theuaredead** - Extensive Win32 Bugtesting - **TimeServ** - Many Additions & Bugfixes - **[Xylemon](https://github.com/Xylemon)** - Technical support, Enthusiast ================================================ FILE: documentation/Dependencies.md ================================================ # Dependencies Here is a list of dependencies required for building FTEQW on several platforms. ## Debian / Raspbian ### Base apt-get install libgl-dev gnutls-dev ### SDL2 apt-get install libsdl2-dev ### GLX / X11 (part of libsdl2-dev) apt-get install libx11-dev libxcursor-dev libxrender-dev ### Plugin: ODE apt-get install autoconf automake libtool ### Plugin: FFMPEG apt-get install libavformat-dev libswscale-dev ## OpenBSD ### SDL2 pkg_add sdl2 ### Plugin: FFMPEG pkg_add ffmpeg ## Arch Linux ### Base pacman -S make gcc Xorg ### Plugin: ODE pacman -S zip automake autoconf ### Plugin: FFMPEG pacman -S ffmpeg4.4 You must pass these flags to compile the plugin: make plugins-rel NATIVE_PLUGINS="ffmpeg" AV_BASE=/usr/include/ffmpeg4.4/ AV_LDFLAGS="-l:libavcodec.so.58 -l:libavformat.so.58 -l:libavutil.so.56 -l:libswscale.so.5" ### SDL2 pacman -S sdl2 ## OpenSUSE ### Base zypper in make gcc gcc-c++ mesa-libGL-devel libgnutls-devel alsa-devel libopus-devel speex-devel libvorbis-devel ### SDL2 zypper in libSDL2-devel ### GLX / X11 zypper in libX11-devel libXcursor-devel libXrandr-devel ### Plugin: ODE zypper in autoconf automake libtool zip ### Plugin: FFMPEG zypper in ffmpeg-4-libavformat-devel ffmpeg-4-libswscale-devel ## Fedora dnf install make gcc gcc-c++ mesa-libGL-devel gnutls-devel alsa-devel libopus-devel speex-devel libvorbis-devel ### SDL2 dnf install SDL2-devel ### GLX / X11 (part of libsdl2-dev) dnf install libX11-devel libXcursor-devel libXrender-devel ### Plugin: ODE dnf install autoconf automake libtool zip ### Plugin: FFMPEG You need to install the RPM Fusion repo if you don't have it. We recommend reading their official guide: https://rpmfusion.org/Configuration Then you can install the required version of FFMPEG: dnf install compat-ffmpeg4-devel Finally, you must pass these flags to compile the plugin: make plugins-rel NATIVE_PLUGINS="ffmpeg" AV_BASE=/usr/include/compat-ffmpeg4 AV_LDFLAGS="-l:libavcodec.so.58 -l:libavformat.so.58 -l:libavutil.so.56 -l:libswscale.so.5" ================================================ FILE: documentation/QuickStart.md ================================================ # FTEQW README This file contains the following sections: - [0. PREFACE](#0-preface) - [1. ABOUT](#1-about) - [2. FEATURES](#2-features) - [3. INSTALLING](#3-installing) - [4. NETWORK QUICK-START](#4-network-quick-start) - [5. TWEAKS](#5-tweaks) - [6. TROUBLESHOOTING](#6-troubleshooting) - [7. FURTHER READING](#7-further-reading) - [8. CONTACT](#8-contact) - [9. LICENSE](#9-license) # 0. PREFACE Thank you for downloading FTEQW, we hope you enjoy the many advanced features and ways to play. We understand the engine can be a bit daunting at times, so we hope this will help you get up and running with little to no hasle. # 1. ABOUT FTEQW is an advanced portable Quake engine. It supports multiple games running on idTech, plus its own set of games that developers have created. Due to the vast amount of supported formats, features and innovations inside the engine and its very own QuakeC compiler (fteqcc), it's very much considered the swiss-army knife of Quake engines. # 2. FEATURES - Portable engine that runs on x86, amd64, ARM/64, PPC64LE and Web - Hybrid protocol engine that supports multiple games - Rendering API support for D3D8, D3D9, D3D11, OpenGL, Vulkan - Splitscreen support for Quake and most mods - In-game voice chat powered by either Opus or Speex - Advanced renderer features, powered by a strong material system and support for both HLSL and GLSL shader code - Multiple audio backends, from OSS to SDL_Sound and OpenAL Soft, plus API so developers can take advantage of AL EAX reverb features. And yes, DirectSound. - Integrated next-generation QuakeC compiler and debugger with support for breakpoints, real-time ingame attribute debugging and much more - Support for IPv4 and IPv6 - Video output presets, to make your games either look like the original versions, or more modern with real-time lighting and more accurate shading - Support for CD-DA/Red Book music replacement in a variety of formats, such as Vorbis, MPEG-3, WAVE and FLAC (ffmpeg plugin required) # 3. INSTALLING Put the engine binary and desired plugins for your platform into the root of the game directory. Supported games: - Quake and its Missionpacks - QuakeWorld - Quake II and its Missionpacks - Quake III Arena and Team Arena - HeXen II and its Missionpack - Half-Life 1 & 2 (via the Rad Therapy project) If you want to be explicit about the game you're starting, you can pass the command-line parameters (read below for a complete list) for the respective game. This will make sure that in a crowded universal game directory, FTE starts the right game. If you want to install music replacement files, you put them into the `music` folder with the `trackXX` naming convention, starting with `track02`. ## Important note regarding Quake II based support If you're running a 64-bit version of FTEQW, then you also need 64-bit game-logic for Quake II. We recommend getting the game .dll/.so from the Yamagi Quake II project for your respective platform. It's recommended that you do for win32 as well, as that will ensure that save games work properly and you can stop worrying about them becoming incompatible between other machines. ## Android Installation When using the Android app, you need to copy your gamedata folders in the `Android/data/com.fteqw/files` folder. An install of Quake should look like this for example: Android/data/com.fteqw/files/id1/pak0.pak Android/data/com.fteqw/files/id1/pak1.pak > [!WARNING] > FTEQW is included in other Android APKs such as idTech4a++, > please consult their documentation and help forums as we do not > provide support to these unofficial third-party ports. ## Steam FTEQW can detect and load data from the `/SteamApps/common/GAMEHERE` folders. This includes the Quake I and II Remasters. ## Linux/Unix System Games Folder FTEQW supports loading the legacy Unix game folders and will look for gamedata in the following folders: - /usr/share/games/quake - /usr/share/games/quake2 - /usr/share/games/quake3 - /usr/share/games/hexen2 - /usr/share/games/halflife - /usr/share/games/halflife2 ## 4. NETWORK QUICK-START Upon launching FTEQW, you can use the multiplayer menu's own built-in server browser to join and connect to a vast array of matches across all the different protocols. No more QuakeSpy/GameSpy 3D client required! If you want to host a game, you can either run a listen server with the ports forwarded, or host a listen server using frag-net.com's online service. Start a new multiplayer server, change the "Public" setting to "Holepunch" and players will automatically see your game in their server-browser once it's started. You can also set FTEQW to run in a terminal/command-prompt for hosting a dedicated server session. Simply pass the command-line argument like so: fteqw -dedicated This will create an interactive shell reminiscent to the console that's accessible in-game. You can host a game directly with frag-net.com without having to worry about port-forwarding and have a map up and running like so: fteqw +set sv_public 2 +set sv_playerslots 8 +map dm4 # 5. TWEAKS You can apply tweaks by opening the console (SHIFT+ESC) and entering commands into the line buffer. In there you can enter console variables (cvars) that affect how the game behaves, as well as enter console commands that trigger an action in the engine. You can always find a list of both of these with the console commands `cvarlist` and `cmdlist` respectively. ## Some example commands `bind ` Binds a command to a key, e.g. `bind F12 quit` `map ` Starts a new game on , e.g. `map dm4` `connect
` Establishes a connection to the specified IP/hostname address. `disconnect` Closes and remote or local game session. `cfg_save` Save amy unsaved configuration changes. `quit` Quits the game. ## About console variables Unlike other Quake engines, FTEs console variable system is more akin to those of later idTech engines. Console variables can be set temporarily (until the next engine restart) or permanently. `set sv_port 26000` Sets the current port to 26000 for this session `seta sv_port 26000` Sets the current port gets set to 26000 and makes sure it will get "archived", aka saved. Sometimes, you'll be able to change a cvar without entering `set` or `seta` beforehand. This is due to the cvar/cmd suggestion system. When you simply set a cvar that way, `set` is assumed. So those changes are only temporary. ## Commandline arguments You can pass cvars and commands directly to the engine binary, prefixed with a plus symbol like so: fteqw +set sv_port 26000 +map dm4 There's also other interesting commandline only arguments you can pass: `-nohome` Don't attempt to save configs, saves, screenshots in the home or user directory. `-basedir ` Specifies the root game directory path. `-basegame ` Specifies which folder to look in for main game data. `-game ` Specifies which mod folder to load over the game data `-window` Tells the renderer to run in a window `-manifest ` Specifies a game manifest to load. This is for advanced game and mod switching. `-dedicated` Run the engine in dedicated server mode, no video out. ## Changing games and mods Launching a Quake 1 mod can be done like so: fteqw -game fortress That will assume a basegame of `id1` and the mod `fortress` will be loaded on top of it. However, if you're playing a game that uses no data from Quake 1: fteqw -basegame openquartz Then it'll never even touch or peek into the folder `id1`. Of course you can pass `-game` after that, too. If you want to specify a supported game for more accurate behavior and automatic data path finding, then you can pass these arguements to load a specific game and behaviour: -quake -quake2 -quake3 -hexen2 -halflife -halflife2 > [!NOTE] > Half-Life 1 & 2 will download the Rad-Therapy project in place of their game logic, and HL2 also requires the Source formats plugin. ## Understanding manifests (advanced users) You can setup custom game configurations with FTE's manifest files. Those can be quite advanced, as they might inherit multiple directories, change the name of the window title, set binds, aliases and cvars ahead of time and much more. FTEMANIFEST 1 GAME funky NAME "Funky QW Game" BASEGAME id1 BASEGAME qw BASEGAME funky An example manifest that will load id1, qw and then funky. If you wanted, you can set default cvars and aliases in there like so: -set sv_example 1234 -seta cl_foobar 5678 -alias funky1 "impulse 416" +bind g funky1 Note the dash and plus symbols, they actually notate when to execute the command in question. '-' means before the engine loads the game config whereas '+' notates it will override anything that will usually be set by the game. You'd then save this as, for example, funky.fmf and load the manifest via the command-line: fteqw -manifest funky.fmf # 6. TROUBLESHOOTING If you're running FTEQW on an older machine with Intel GMA graphics, you probably want to try running the engine in D3D9 mode if you're encountering any graphical issues: fteqw +set vid_renderer d3d9 If you can only run OpenGL but still have graphical issues, try forcing support for the builtin GLSL off: fteqw +set gl_blacklist_debug_glsl 1 If FTEQW is seemingly not saving your settings, make sure you tell it to save your config when you quit the game. If it still does not work somehow, enter the console command `cfg_save` into the console. It should output where the file gets saved or if there's any problems writing the configuration file. If OpenAL is causing crashes at launch (happens with some distributions, that is out of our control) then try starting FTEQW with: fteqw +set s_al_disable 1 # 7. FURTHER READING Here's a small selection of some links we recommend for more info on FTEQW: - [Spirit's excellent FTEQW wiki](https://quakewiki.org/wiki/FTEQW_Wiki) - [@eukara's must read FTEQCC manual/QuakeC help](https://icculus.org/~marco/txtfiles.html) - [@Shpoike's old FTEQW website and wiki](https://fte.triptohell.info/wiki/index.php/Main_Page) # 8. CONTACT If you need more help, have suggestions or want to hang out with the developers that make FTEQW what it is, join us on a platform listed below! Bug reports are welcomed! See our [public bug tracker](https://github.com/fte-team/fteqw/issues). ### Matrix https://matrix.to/#/#fte:matrix.org ### IRC **Server:** irc.quakenet.org **Channel:** #fte ### Forums **[Spike](https://forums.insideqc.com/memberlist.php?mode=viewprofile&u=26)** and **[eukara](https://forums.insideqc.com/memberlist.php?mode=viewprofile&u=949)** can be found on [insideqc.com](https://forums.insideqc.com/) ### Discord https://discord.gg/p2ag7x6Ca6 # 9. LICENSE Copyright (c) 2004-2025 FTE's team and its contributors Quake source (c) 1999 id Software FTEQW is supplied to you under the terms of the same license as the original Quake sources, the GNU General Public License Version 2. Please read the `LICENSE` file for details. The latest source & binaries are always available at: [fteqw.org](https://fteqw.org) [fteqcc.org](https://fteqcc.org) ================================================ FILE: documentation/Tools.md ================================================ # FTE Tools README # Preface This file covers the tools include with the FTEQW source code. FTEQW features a set of tools for compiling and editing QuackeC, textures, models, packages, and other formats associated with idTech. ### Contents - [FTEQCC](#fteqcc) - [IQM Tool](#iqm-tool) - [Image Tool](#image-tool) - [License](#license) # FTEQCC FTEQCC is the defacto compiler for FTE's unique variant of QuakeC called, **FTEQC**. It is (generally) backwards compatible with standard QuakeC and is used to compile code usually found in the `src` folder. It will output a memory-safe and architecture independent `progs.dat` that is loaded in a virtual machine within the engine. Client-side progs are also able to be compiled if the code-base supports it into a `csprogs.dat` file. Another unique feature of FTEQC is the concept of "objects", similar to other C-like languages such as C++ or Obj-C. Some games (HeXen II) and the Nuclide SDK have a concept of multi-progs, meaning uniquely named progs can be arbitrarily compiled and loaded alongside the main progs. Full FTEQCC documentation can be found [here](https://fte.triptohell.info/moodles/fteqcc/README.html) > [!TIP] > You can also compile within FTEQW itself by typing `compile` in the console (must have a `src` folder in the mounted directory) ### GUI There is a GUI available for FTEQCC built on the Qt toolkit. It is incomplete and features vary by platform. ### Package Management The commandline version of FTEQCC doubles up as a PAK/PK3 creator or extractor: fteqcc -l PACKAGENAME Lists the contents of a file on the stdout. fteqcc -x PACKAGENAME Extracts all files from the named package to the working directory. You should normally use `../foo.pak` in order to avoid overwriting files unintentionally. fteqcc -x PACKAGENAME SUBDIR Extracts specific files from the named package. fteqcc -0 SUBDIR Generates a `subdir.pak` file with the contents of that subdir. Unlike most pak generators, the pak will also be openable with any zip tool for users to easily view files (they shouldn't edit them though). fteqcc -9 SUBDIR Generates a `subdir.pk3` file with the contents of that subdir. Contents will be compressed to reduce filesize, but won't work in vanilla quake. fteqcc -z SUBDIR Generate a `subdir.pk3` dictionary file, and an (additional) `subdir.pXX` file (name generated sequentially). The dictionary file will be versioned with different servers potentially using different versions. This prevents re-downloading redundant copies of the same files for each different version. The spanned data files are not versioned, so these should only be generated by authoritive developers (ie: not same-filename forks). Many engines support interpreting `foo.pk3dir` subdirs as proto-packages, which makes it easier to test packages without constant re-compression. # IQM Tool ### About: This is a commandline tool for creating iqm files from intermediate files exported from modelling programs. ### Possible Inputs: - SMD - GLTF - GLB - IQE - MD5Mesh - MD5Anim - FBX - OBJ ### Outputs: - IQM / VVM The default format. IQM supports skeletal animations and other features used by modern model formats. **VVM** is a moniker for the backwards-compatible extensions FTE supports which are not supported by other engines/tools. Think of it as a "fork" of the IQM model format. The **VVM** variant was created by Vera Visions hence the name, "Vera Visions Model". - MDL Quake's model format (idTech 2). It is vertex animation based and has many limitations, therefore it's recommend to use IQM / VVM unless you are targeting stock Quake. > [!WARNING] > While Half-Life 1 & 2 also use the `.mdl` extension, it is a different format altogether and _not yet_ supported by IQM Tool. ### Usage: IQM Tool Can be used two ways: iqm foo.cmd Uses a command script to decide which files to read, modifiers for each sequence or mesh, etc. iqm foo.iqm foo.gltf Converts the gltf file to an iqm file. Animation rate will be guessed. iqm foo.mdl foo.gltf Writes out a Quake MDL file. Framegroups will be used for each animation sequence. ### Command script: #COMMENT //COMMENT Just a comment, ignored. exec filename Invokes an external script (useful when you have multiple models with the same animations or so). modelflags BITS Sets the output model's flags. Queryable in gamecode. mesh MESHNAME ATTRIBUTELIST Overrides attributes of an imported mesh to use for the visible part of the model. Use the `import` command to actually import the geometry. Attributes are: contents BITSORNAMES Controls which traces may impact the imported surfaces. You should normally specify 0 if you are using hitboxes. Default is CONTENTS_BODY. surfaceflags BITSORNAMES Misc info eg reported by tracelines. body NUM The 'body' number which can be queried in gamecode (typically used to report eg headshots). geomset SET ID Sets this mesh to only draw when geomset SET has been configured as ID. lodrange MIN MAX This mesh will only be drawn when the screen coverage is within the specified range. Exact interpretation can vary according to engine cvars. hitbox BODYNUM BONENAME MINS MAXS Creates a cuboid mesh around the named bone (as a box in the base pose). The mesh will be given CONTENTS_BODY such that it will be hit by hitmesh traces. The bodynum arg can be queried via gamecode. bone NAME ATTRIBUTELIST rename NEWNAME The bone read from input files will be written as NEWNAME in the output file (so gamecode can manipulate things consistently). group GROUPNUM Bones can be reordered in the output according to their group numbers. Only the root bone of each group needs its group specified (children will inherit). This simplies bone range operations in gamecode import FILENAME ATTRIBUTELIST model FILENAME ATTRIBUTELIST scene FILENAME ATTRIBUTELIST animation FILENAME ATTRIBUTELIST Loads model AND animation data from an imported file. File formats must match the given filename extensions. Attributes are: Any attributes supported by the mesh command (such attributes here define the default values for imported meshes). name NEWNAME The animation will be queryable to gamecode as the NEWNAME. fps RATE Overrides the frame rate of the animation. loop Specifies that the animation must loop. clamp Specifies that the animation must NOT loop (animation will stop once it reaches the last pose). unpack Generates single-pose animations, consistent with the way vanilla QC animates. pack Generates multiple-pose animations, gamecode will be able to specify animation and time separately. nomesh Do NOT generate meshes from this file. noanim Do NOT generate animations from this file. materialprefix PATH Prefixes the imported texture with an additional path (to avoid conflicts with other files). start FRAME end FRAME Limits inputed data to the specified poses. scale SCALE Resizes the input data. rotate PITCH ROLL YAW Rotates the imported data (quake is +x=forward, +y=left, +z=up). translate X Y Z Moves the mesh+bones around a bit. event [SEQUENCE:]POSE EVCODE EVSTRING Defines a model event at the specified timestamp. output FILENAME output_iqm FILENAME output_vvm FILENAME output_qmdl FILENAME output_md16 FILENAME Specifies the file to be written. You may have one per supported output model format, each will contain roughly equivelent data (where supported). # Image Tool ### About: This is a tool for converting various image file types to more favourable ones. This includes generating wad files, cubemap images, etc. ### Wad Examples: imgtool --ext mip [--PIXFMT] [--resize W H] *.EXT Converts the input files to quake's miptex format. Input files will not be changed. If compression was specified, the resulting miptex will contain an additional high-colour alternative. If `--resize` is used, the paletted data will be resized without affecting the high-colour alternative. Supported pixel formats: rgba8,rgb8, rgb565,rgba5551,rgba4444, l8, bc1-7, etc1,etc2,etcp,etca, astc*x*, e5bgr9 > [!NOTE] > Use of alternative formats requires a qbsp which does NOT strip this information. > The vanilla qbsp will work fine for this purpose, but more advanced qbsp utils have a tendancy to strip the info (including ericw's sadly). imgtool -w WADFILE *.mip Packs the specified miptex files into the named wad file. imgtool -x --ext EXT WADORBSP Extracts the textures from the specified wad or bsp file, saving them as EXT files. ### General Examples: imgtool --help Shows a list of compressed pixel formats, and supported file formats. imgtool -i FILE Shows info about the named file(s). imgtool --ext ktx [--PIXFMT] *.EXT imgtool --ext dds [--PIXFMT] *.EXT Converts the input files to a hardware-friendly file format (with the specified pixel format). Input files will not be changed. ktx supports all recognised hardware formats. dds is more limited (and eg doesn't support etc or astc). HUD artwork should generally be astc4x4 or bc7 for best quality. models and walls can be of lower quality, astc6x6, etc2, or bc1 are good choices. for hdr pixel formats, try astc4x4_hdr or bc6. imgtool --ext png *.EXT Converts the input files to png format for viewing/editing. Input files will not be changed. imgtool --cube [--PIXFMT] -o mysky.dds mysky_*.tga Converts 6 input files to a single cubemap file. Inputs will be reordered according to quake's typical cubemap postfixes (and flipped as appropriate), otherwise be careful with wildcards. imgtool --2darray [--PIXFMT] -o foo.ktx IDX0 IDX1 IDXN Converts the input textures into a 2d texture array. Input file order matters. imgtool --3d [--PIXFMT] -o foo.ktx LAYER0 LAYER1 LAYERN Converts the input textures into a 3d texture. Input file order matters. ### Which pixel format to use: ### ASTC: - The ldr-only profile is part of the gles3.2 spec (but likely to be emulated on nvidia). - ASTC has a range of block sizes with each block being 16 bytes, thus larger block sizes yield greater compression. - This format supports multiple planes, which reduces issues with multiple gradients in a block, which means it can cope with pixel art better than eg s3tc with larger block sizes. - Bits are distributed according to usage per block, this means it can use more bits for the rgb channels where alpha is constant, giving it more useful bits than eg bc3. - ASTC is able to use a second set of weights for any single channel (not just alpha), which makes it suitable for encoding normalmaps for instance. - To encode ASTC pixel formats, you will need to install astcenc - https://github.com/ARM-software/astc-encoder/releases (or use astc-encoded source files). ### ETC2: - Part of the gles3.0/gl4.3 spec - ETC2 is a superset of ETC1, and has been somewhat obsoleted by ASTC. ### BC1/2/3: - AKA s3tc, AKA dxt, available only via optional extensions, but is mandated by d3d 9_1 feature level. - These formats all encode two 565 colours per 4x4 block with 2-bits per pixel for interpolation, which can result in discolouration. - bc2 uses an additional 64bit block to encode 4bit alpha. - bc3 also uses an additional block for alpha, but does so simiarly to its rgb values (two 8bit alpha values, with 3-way interpolation). - To encode s3tc pixel formats, you will need to install libnvtt-bin (or export from image editors as dds or ktx files). ### BC4/5: - AKA rgtc, part of the gl3.0 spec, or d3d10. - These formats reuse bc3's 'alpha' compression to encode one or two channels respectively. BC1 can be handy for heightmaps/greyscale/etc, while BC5 can be useful for 2-channel normalmaps. They are not generally useful as eg wall textures. ### BC6/7: - AKA bptc, part of the gl4.2, or d3d11. - These formats support one or two planes per block, which means they can cope with pixel art quite a bit better than bc1/3. - While they're the same size as bc3 (twice that of bc1), they have multiple modes per block that allow them to eg avoid wasting bits on unused alpha data. - The difference is that BC7 encodes RGBA ldr data, while BC6 encodes RGB hdr data. - To encode s3tc pixel formats, you will need to install libnvtt-bin (or export from image editors as dds or ktx files). ### Too Long didn't read: - Use ASTC if you're targetting modern mobile devices (astc6x6 for walls, astc4x4 for huds, or something). - Use ETC2 if you're targetting older mobile users. - Use BC1 for desktop model/wall textures, and bc3 for things with non-binary alpha channels. - Use BC7 for desktop hud textures or other textures where BC1 does a terrible job. - Use jpegs if you just want to get filesize down without caring about performance. - If the user's hardware doesn't support the used formats then FTE can software-decode, so if you're expecting both mobile+desktop users with a single set of textures then favour mobile (desktop GPUs have better memory bandwidth). # License Copyright (c) 2004-2025 FTE's team and its contributors Quake source (c) 1999 id Software FTEQW is supplied to you under the terms of the same license as the original Quake sources, the GNU General Public License Version 2. Please read the `LICENSE` file for details. The latest source & binaries are always available at: [fteqw.org](https://fteqw.org) [fteqcc.org](https://fteqcc.org) ================================================ FILE: dounifdef.sh ================================================ #!/bin/sh #this script is DANGEROUS #be sure to have committed *BEFORE* running this script. #Note: This script does not understand dead files (including botlib). #expect '-Wmisleading-indentation' warnings (that were previously muted by nearby ifdefs). #DO NOT COMMIT THE RESULTS TO FTE'S TRUNK CONFIG=wastes #must have trailing slashes SRCDIR=./ NEWDIR=/tmp/fte-$CONFIG/ echo "WARNING: This script will lock-in a build config upon your C files." echo "The resulting files will support only your choice of feature set, instead of having lots of unused code mixed in." echo "THIS IS DESTRUCTIVE SO MUST ONLY BE USED FOR FORKS." read -p "Press name the build config (or ctrl+c to abort)" CONFIG if [ "$foo" == "" ]; then echo "no config specified." exit 1 fi mkdir -p $NEWDIR cat $SRCDIR/engine/common/config_$CONFIG.h | grep "#define" | sed "s/\/\/#define/#undef/g" > $NEWDIR/unifdefrules cat $SRCDIR/engine/common/config_$CONFIG.h | grep "#undef" >> $NEWDIR/unifdefrules if [ "$SRCDIR" != "$NEWDIR" ]; then echo "Copying files to strip to $NEWDIR." cp -r $SRCDIR* $NEWDIR else echo "WARNING: WRITING FILES IN PLACE MUST ONLY BE USED FOR FORKS." read -p "Press y to confirm (or ctrl+c to abort)" foo if [ "$foo" != "y" ]; then exit 1 fi fi cd $NEWDIR for FILENAME in engine/*/*.c; do unifdef -f unifdefrules -m $FILENAME done #headers keep any defines that will be expanded in code. cat $NEWDIR/unifdefrules | grep -v FULLENGINENAME | grep -v DISTRIBUTION | grep -v ENGINEWEBSITE | grep -v MAX_SPLITS | grep GAME_SHORTNAME > $NEWDIR/unifdefhrules for FILENAME in engine/*/*.h; do unifdef -f unifdefhrules -m $FILENAME done rm $NEWDIR/unifdefrules echo "Files in $NEWDIR have now been stripped down." echo "Some things may require hand-editing to remove warnings (or just compile with CFLAGS=-Wno-misleading-indentation)." echo "You still need to set FTE_CONFIG too." read -p "Press enter to test-compile" foo cd $NEWDIR/engine && make sv-rel m-rel -j8 FTE_CONFIG=$CONFIG -k ================================================ FILE: engine/BSDmakefile ================================================ #just a stub to make things easier. #you still need gmake and gcc. all: gmake all help: gmake help clean: gmake clean gl-dbg: gmake gl-dbg sw-dbg: gmake sw-dbg m-dbg: gmake m-dbg sv-dbg: gmake sv-dbg gl-rel: gmake gl-rel sw-rel: gmake sw-rel m-rel: gmake m-rel sv-rel: gmake sv-rel ================================================ FILE: engine/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ================================================ FILE: engine/Makefile ================================================ #FTEQW Makefile #make m-rel plugins-rel sv-rel [FTE_TARGET=$ARCH] #only limited forms of cross-making is supported #only the following 3 are supported #linux->win32 (FTE_TARGET=win32) RPM Package: "mingw32-gcc", DEB Package: "mingw32" #linux->win64 (FTE_TARGET=win64) RPM Package: "mingw32-gcc", DEB Package: "mingw32" #linux->win32 (FTE_TARGET=msvc32 MSVCPATH=? WINDOWSSDKDIR=?) invokes via wine. #linux->win64 (FTE_TARGET=msvc64 MSVCPATH=? WINDOWSSDKDIR=?) invokes via wine. #linux->linux 32 (FTE_TARGET=linux32) #linux->linux 64 (FTE_TARGET=linux64) #linux->linux x32 (FTE_TARGET=linux_x32) #linux->linux armhf (FTE_TARGET=linux_armhf) #linux->linux arm64/aarch64 (FTE_TARGET=linux_arm64) #linux->linux *others* (FTE_TARGET=linux CC=other-gcc) #linux->morphos (FTE_TARGET=morphos) #linux->macosx (FTE_TARGET=macosx) or (FTE_TARGET=macosx_x86) #linux->javascript (FTE_TARGET=web) #linux->droid (make droid) #win32->droid (make droid) #if you are cross compiling, you'll need to use FTE_TARGET=mytarget #note: cross compiling will typically require 'make makelibs FTE_TARGET=mytarget', which avoids installing lots of extra system packages. #FTE_TARGET=SDL2 is also a thing. #FTE_TARGET=SDL3 is also a thing. default: help # CC?=gcc WINDRES?=windres STRIP?=strip STRIPFLAGS?=--strip-unneeded --remove-section=.comment VISIBILITY_FLAGS=-fvisibility=hidden #just assume this for sanities sake. disable only for compilers that error. CPUOPTIMIZATIONS=-Os COMPILE_SYS:=$(shell uname -o 2>&1) #canonicalize the source path. except emscripten warns about that like crazy. *sigh* ifeq ($(FTE_TARGET),web) BASE_DIR:=. else ifeq ($(findstring msvc,$(FTE_TARGET)),msvc) BASE_DIR:=. else ifeq ($(FTE_TARGET),droid) #android tools suck, but plugins need to find the engine directory. BASE_DIR:=../engine else BASE_DIR:=$(realpath .) endif ifeq ($(SVNREVISION),) #try subversion firstly... # SVN_VERSION:=$(shell test -d $(BASE_DIR)/../.svn && svnversion $(BASE_DIR)) # SVN_DATE:=$(shell test -d $(BASE_DIR)/../.svn && cd $(BASE_DIR) && svn info --show-item last-changed-date --no-newline) # ifeq (,$(SVN_VERSION)) # #grab the svn version from git-svn (assuming no other modifications). this fails when there's extra commits (probably a good thing). # SVN_VERSION=$(shell test -d $(BASE_DIR)/../.git && git svn find-rev `git rev-parse HEAD`) # SVN_DATE:=$(shell test -d $(BASE_DIR)/../.git && git log -1 --format=%cs $(BASE_DIR)) # endif ifeq (,$(SVN_VERSION)) #try to get git version info instead. this usually uses git-count-hash[-dirty] format SVN_VERSION:=$(shell test -d $(BASE_DIR)/../.git && cd $(BASE_DIR) && echo $$((`git rev-list HEAD --count` + 29))) GIT_VERSION:=$(shell test -d $(BASE_DIR)/../.git && cd $(BASE_DIR) && git describe --long --always --dirty) SVN_DATE:=$(shell test -d $(BASE_DIR)/../.git && git log -1 --format=%cs $(BASE_DIR)) ifneq (,$(SVN_VERSION)) #make sure its prefixed with something specific. we use versions for versioning, which will confuse the update mechanism if they're inconsistent - like random hashses that have no implied ordering... SVN_VERSION:=git-$(SVN_VERSION)-$(GIT_VERSION) endif endif SVNREVISION= ifneq (,$(SVN_VERSION)) SVNREVISION+=-DSVNREVISION=$(SVN_VERSION) endif ifneq (M,$(findstring M,$(SVN_VERSION))) SVNREVISION+=-DSVNDATE=$(SVN_DATE) endif endif MAKE:=$(MAKE) --no-print-directory SVNREVISION="$(SVNREVISION)" SVN_VERSION="$(SVN_VERSION)" #WHOAMI:=$(shell whoami) #update these to download+build a different version. this assumes that the url+subdirs etc contain a consistant version everywhere. JPEGVER=9c ZLIBVER=1.3.1 PNGVER=1.6.45 OGGVER=1.3.6 VORBISVER=1.3.7 VULKANVER=1.3.275.0 SDL2VER=2.30.11 SDL3VER=3.2.10 SCINTILLAVER=373 OPUSVER=1.3.1 SPEEXVER=1.2.0 SPEEXDSPVER=1.2.0 FREETYPEVER=2.10.1 BULLETVER=2.87 OPENSSLVER=3.0.1 #cygwin's make's paths confuses non-cygwin things RELEASE_DIR=$(BASE_DIR)/release DEBUG_DIR=$(BASE_DIR)/debug PROFILE_DIR=$(BASE_DIR)/profile NATIVE_ABSBASE_DIR:=$(realpath $(BASE_DIR)) ifeq ($(COMPILE_SYS),Cygwin) OUT_DIR?=. NATIVE_OUT_DIR:=$(shell cygpath -m $(OUT_DIR)) NATIVE_BASE_DIR:=$(shell cygpath -m $(BASE_DIR)) NATIVE_RELEASE_DIR:=$(shell cygpath -m $(RELEASE_DIR)) NATIVE_DEBUG_DIR:=$(shell cygpath -m $(DEBUG_DIR)) NATIVE_ABSBASE_DIR:=$(shell cygpath -m $(NATIVE_ABSBASE_DIR)) endif NATIVE_OUT_DIR?=$(OUT_DIR) NATIVE_BASE_DIR?=$(BASE_DIR) NATIVE_RELEASE_DIR?=$(RELEASE_DIR) NATIVE_DEBUG_DIR?=$(DEBUG_DIR) EXE_NAME=fteqw #include the appropriate games. ifneq (,$(BRANDING)) BRANDFLAGS+=-DBRANDING_INC=../game_$(BRANDING).h -include game_$(BRANDING).mak endif FTE_CONFIG?=fteqw ifneq ($(findstring msvc,$(FTE_TARGET)),msvc) ifeq (,$(FTE_CONFIG_EXTRA)) FTE_CONFIG_EXTRA := $(shell $(CC) -xc -E -P -DFTE_TARGET_$(shell echo $(FTE_TARGET) | tr '[:lower:]' '[:upper:]') -DCOMPILE_OPTS $(CFLAGS) common/config_$(FTE_CONFIG).h) endif endif BRANDFLAGS+=-DCONFIG_FILE_NAME=config_$(FTE_CONFIG).h $(FTE_CONFIG_EXTRA) EXE_NAME=$(FTE_CONFIG) ifeq (,$(findstring DNO_SPEEX,$(FTE_CONFIG_EXTRA))) USE_SPEEX?=1 endif ifeq (,$(findstring DNO_OPUS,$(FTE_CONFIG_EXTRA))) USE_OPUS=1 endif ifneq (,$(findstring DLINK_QUAKE3,$(FTE_CONFIG_EXTRA))) LINK_QUAKE3=1 endif ifeq ($(findstring web,$(FTE_TARGET)),) #the web target uses javascript/browser loaders for common audio+image files instead of needing to embed our own, which keeps sizes down slightly. ifeq (,$(findstring DNO_VORBISFILE,$(FTE_CONFIG_EXTRA))) USE_VORBISFILE=1 endif ifneq (,$(findstring DLINK_JPEG,$(FTE_CONFIG_EXTRA))) LINK_JPEG=1 endif ifneq (,$(findstring DLINK_PNG,$(FTE_CONFIG_EXTRA))) LINK_ZLIB=1 LINK_PNG=1 endif endif ifneq (,$(findstring DLINK_FREETYPE,$(FTE_CONFIG_EXTRA))) LINK_FREETYPE=1 LINK_ZLIB=1 LINK_PNG=1 endif ifneq (,$(findstring -Os,$(FTE_CONFIG_EXTRA))) CPUOPTIMIZATIONS+=-Os BRANDFLAGS:=$(filter-out -O%,$(BRANDFLAGS)) endif ifneq (,$(findstring DLINK_ODE,$(FTE_CONFIG_EXTRA))) #ode library will be statically linked. LINK_ODE=1 endif ifneq (,$(findstring DLINK_INTERNAL_BULLET,$(FTE_CONFIG_EXTRA))) #bullet plugin will be built into the exe itself INTERNAL_BULLET=1 endif ifneq (,$(findstring DLINK_EZHUD,$(FTE_CONFIG_EXTRA))) LINK_EZHUD=1 endif ifneq (,$(findstring DLINK_OPENSSL,$(FTE_CONFIG_EXTRA))) LINK_OPENSSL=1 endif ifeq ($(BITS),64) CC:=$(CC) -m64 CXX:=$(CXX) -m64 endif ifeq ($(BITS),32) CC:=$(CC) -m32 CXX:=$(CXX) -m32 endif #correct the gcc build when cross compiling ifneq (,$(findstring win32,$(FTE_TARGET))) ifeq ($(shell $(CC) -v 2>&1 | grep mingw),) #CC didn't state that it was mingw... so try fixing that up #old/original mingw project, headers are not very up to date. ifneq ($(shell which i586-mingw32msvc-gcc 2> /dev/null),) #yup, the alternative exists (this matches the one debian has) CC=i586-mingw32msvc-gcc CXX=i586-mingw32msvc-g++ AR=i586-mingw32msvc-ar WINDRES=i586-mingw32msvc-windres STRIP=i586-mingw32msvc-strip # BITS?=32 endif #mingw64 provides a 32bit toolchain too, which has more up to date header files than the mingw32 project. so favour that if its installed. ifneq ($(shell which i686-w64-mingw32-gcc 2> /dev/null),) #yup, the alternative exists (this matches the one debian has) CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ AR=i686-w64-mingw32-ar WINDRES=i686-w64-mingw32-windres STRIP=i686-w64-mingw32-strip # BITS?=32 endif endif endif #correct the gcc build when cross compiling ifneq (,$(findstring win64,$(FTE_TARGET))) ifeq ($(shell $(CC) -v 2>&1 | grep mingw),) #CC didn't state that it was mingw... so try fixing that up ifneq ($(shell which x86_64-w64-mingw32-gcc 2> /dev/null),) #yup, the alternative exists (this matches the one debian has) CC=x86_64-w64-mingw32-gcc -m64 CXX=x86_64-w64-mingw32-g++ -m64 AR=x86_64-w64-mingw32-ar WINDRES=x86_64-w64-mingw32-windres STRIP=x86_64-w64-mingw32-strip # BITS=64 endif ifneq ($(shell which amd64-mingw32msvc-gcc 2> /dev/null),) #yup, the alternative exists (this matches the one debian has) CC=amd64-mingw32msvc-gcc -m64 CXX=amd64-mingw32msvc-g++ -m64 AR=amd64-mingw32msvc-ar WINDRES=amd64-mingw32msvc-windres STRIP=amd64-mingw32msvc-strip # BITS=64 endif endif endif ifeq ($(FTE_TARGET),win32_sdl) FTE_TARGET=win32_SDL endif ifeq ($(FTE_TARGET),win64_sdl) FTE_TARGET=win64_SDL endif USER_TARGET:=$(FTE_TARGET) #make droid-rel doesn't get the right stuff #add a small default config file. its only small. and some other stuff, because we can. This makes it much easier to get it up and running. DROID_PACKSU?= $(BASE_DIR)/droid/fte.cfg $(BASE_DIR)/droid/default.fmf $(BASE_DIR)/droid/configs/touch.cfg ANDROID_HOME?=~/android-sdk-linux #ANDROID_NDK_ROOT?=~/android-ndk-r8e #ANDROID_NDK_ROOT?=$(ANDROID_HOME)/ndk-bundle ANDROID_NDK_ROOT=$(ANDROID_HOME)/android-ndk-r14b ANDROID_ZIPALIGN?=$(ZIPALIGN) ANDROID_ZIPALIGN?=$(ANDROID_HOME)/tools/zipalign ANT?=ant JAVA_HOME?=/usr JAVATOOL=$(JAVA_HOME)/bin/ ANDROID_SCRIPT=android DO_CMAKE=cmake -DCMAKE_C_COMPILER="$(firstword $(CC))" -DCMAKE_C_FLAGS="$(wordlist 2,99,$(CC)) $(CPUOPTIMIZATIONS)" -DCMAKE_CXX_COMPILER="$(firstword $(CXX))" -DCMAKE_CXX_FLAGS="$(wordlist 2,99,$(CXX)) $(CPUOPTIMIZATIONS)" ifeq ($(DROID_ARCH),) #armeabi armeabi-v7a arm64-v8a x86 x86_64 mips mips64 DROID_ARCH+=armeabi-v7a #old 32bit android. yucky. DROID_ARCH+=arm64-v8a #modern android devices are 64bit-only. urgh. DROID_ARCH+=x86 #mostly for testing... #DROID_ARCH+=x86_64 #starting with DROID_API_LEVEL 21 endif ifeq ($(FTE_TARGET),droid) #figure out the host system, required to find a usable compiler ifneq ($(shell uname -o 2>&1 | grep Cygwin),) # ifeq ($(shell uname -m 2>&1), i686) # ANDROID_HOSTSYSTEM?=windows # else # ANDROID_HOSTSYSTEM?=windows-$(shell uname -m) # endif ANDROID_HOSTSYSTEM?=windows-x86_64 else ANDROID_HOSTSYSTEM?=linux-$(shell uname -m) endif DROID_ABI_VER?=4.9 #omfg why the FUCK do we need all this bullshit? Why isn't there some sane way to do this that actually works regardless of ndk updates?!? #name is some random subdir that someone at google arbitrarily picked #arch is some random other name for a group of ABIs... #prefix is the 'standard' tupple that the toolchain was compiled to target (by default) #ver is whatever gcc version it is, or clang. so yeah, pretty much random. #cflags is whatever is needed to actually target that abi properly with the specific toolchain... -m64 etc. DROID_ABI_NAME___armeabi=arm-linux-androideabi DROID_ABI_PREFIX_armeabi=arm-linux-androideabi DROID_ABI_ARCH___armeabi=arm DROID_ABI_VER____armeabi?=$(DROID_ABI_VER) DROID_ABI_CFLAGS_armeabi=-march=armv5te -mtune=xscale -msoft-float DROID_ABI_NAME___armeabi-v7a=$(DROID_ABI_NAME___armeabi) DROID_ABI_PREFIX_armeabi-v7a=$(DROID_ABI_PREFIX_armeabi) DROID_ABI_ARCH___armeabi-v7a=$(DROID_ABI_ARCH___armeabi) DROID_ABI_VER____armeabi-v7a=$(DROID_ABI_VER____armeabi) DROID_ABI_CFLAGS_armeabi-v7a=-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 DROID_ABI_NAME___arm64-v8a=$(DROID_ABI_NAME___armeabi) DROID_ABI_PREFIX_arm64-v8a=$(DROID_ABI_PREFIX_armeabi) DROID_ABI_ARCH___arm64-v8a=$(DROID_ABI_ARCH___armeabi) DROID_ABI_VER____arm64-v8a=$(DROID_ABI_VER____armeabi) DROID_ABI_CFLAGS_arm64-v8a=-m64 DROID_ABI_NAME___x86=x86 DROID_ABI_PREFIX_x86=i686-linux-android DROID_ABI_ARCH___x86=x86 DROID_ABI_VER____x86?=$(DROID_ABI_VER) DROID_ABI_CFLAGS_x86=-march=i686 -mssse3 -mfpmath=sse -m32 -Os DROID_ABI_NAME___x86_64=x86_64 DROID_ABI_PREFIX_x86_64=x86_64-linux-android DROID_ABI_ARCH___x86_64=x86_64 DROID_ABI_VER____x86_64=$(DROID_ABI_VER____x86) DROID_ABI_CFLAGS_x86_64=-march=x86-64 -msse4.2 -mpopcnt -m64 -Os #DROID_ABI_NAME___mips=mipsel-linux-android #DROID_ABI_PREFIX_mips=mipsel-linux-android #DROID_ABI_ARCH___mips=mips #DROID_ABI_VER____mips?=$(DROID_ABI_VER) #DROID_ABI_CFLAGS_mips= #DROID_ABI_NAME___mips64=$(DROID_ABI_NAME___mips) #DROID_ABI_PREFIX_mips64=$(DROID_ABI_PREFIX_mips) #DROID_ABI_ARCH___mips64=$(DROID_ABI_ARCH___mips) #DROID_ABI_VER____mips64=$(DROID_ABI_VER____mips) #DROID_ABI_CFLAGS_mips64=-m64 ifeq (1,$(words [$(DROID_ARCH)])) #try and make sense of the above nonsense. DROID_ABI:=$(DROID_ABI_CFLAGS_$(DROID_ARCH)) TOOLCHAINPATH:=$(ANDROID_NDK_ROOT)/toolchains/$(DROID_ABI_NAME___$(DROID_ARCH))-$(DROID_ABI_VER____$(DROID_ARCH))/prebuilt/$(ANDROID_HOSTSYSTEM)/bin/ TOOLCHAIN:=$(TOOLCHAINPATH)$(DROID_ABI_PREFIX_$(DROID_ARCH))- #4 is the min that fte requires DROID_API_LEVEL?=9 ifeq ($(DROID_ARCH),x86) #google fecked up. anything before api_level 9 will fail to compile on x86 DROID_API_LEVEL=9 endif ifeq ($(DROID_ARCH),x86_64) #google fecked up. anything before api_level 9 will fail to compile on x86 DROID_API_LEVEL=21 endif DROID_API_NAME?=android-$(DROID_API_LEVEL) DROID_PLAT_INC=arch-$(DROID_ABI_ARCH___$(DROID_ARCH)) DROIDSYSROOT=$(realpath $(ANDROID_NDK_ROOT)/platforms/$(DROID_API_NAME)/$(DROID_PLAT_INC)) ifeq ($(DROIDSYSROOT),) #its possible that google removed whatever api we're trying to target, just switch up to the new default. BITCHANDMOAN:=$(shell echo targetting \"android-9\" instead of \"android-$(DROID_API_LEVEL)\" 1>&2) DROID_API_LEVEL=9 DROID_API_NAME=android-$(DROID_API_LEVEL) DROIDSYSROOT=$(realpath $(ANDROID_NDK_ROOT)/platforms/$(DROID_API_NAME)/$(DROID_PLAT_INC)) ifeq ($(DROIDSYSROOT),) #its possible that google removed whatever api we're trying to target, just switch up to the new default. BITCHANDMOAN:=$(shell echo $(DROID_API_NAME) not available either - $(DROID_ARCH) 1>&2) endif endif DROIDSYSROOT:=$(DROIDSYSROOT) endif #if we're running under windows, then we want to run some other binary ifeq ($(shell uname -o 2>&1 | grep Cygwin),) #set up for linux/mingw TOOLOVERRIDES=PATH="/usr/bin:$(realpath $(TOOLCHAINPATH))" CFLAGS=--sysroot="$(realpath $(ANDROID_NDK_ROOT)/platforms/$(DROID_API_NAME)/$(DROID_PLAT_INC))" CPPFLAGS=--sysroot="$(realpath $(ANDROID_NDK_ROOT)/platforms/$(DROID_API_NAME)/$(DROID_PLAT_INC))" CONFIGARGS= --with-sysroot="$(realpath $(ANDROID_NDK_ROOT)/platforms/$(DROID_API_NAME)/$(DROID_PLAT_INC))" else #we're running upon cygwin #FIXME: support mingw too... ANDROID_SCRIPT=android.bat #make can't cope with absolute win32 paths in dependancy files DEPCC= DEPCXX= ifneq ($(realpath $(TOOLCHAINPATH)),) #don't invoke cygpath when realpath returns nothing due to dodgy paths (which happens when stuff isn't set up right for the top-level makefile) #configure hates android, with its broken default sysroot and lack of path etc DROIDSYSROOT:=$(shell cygpath -m $(DROIDSYSROOT)) TOOLOVERRIDES=PATH="/usr/bin:$(shell cygpath -u $(realpath $(TOOLCHAINPATH)))" CFLAGS=--sysroot="$(DROIDSYSROOT)" CPPFLAGS=--sysroot="$(DROIDSYSROOT)" CONFIGARGS= --with-sysroot="$(shell cygpath -u $(realpath $(ANDROID_NDK_ROOT)/platforms/$(DROID_API_NAME)/$(DROID_PLAT_INC)))" endif endif CC:=$(TOOLCHAIN)gcc --sysroot="$(DROIDSYSROOT)" -DANDROID $(DROID_ABI) -fno-strict-aliasing CXX:=$(TOOLCHAIN)g++ --sysroot="$(DROIDSYSROOT)" -DANDROID $(DROID_ABI) -fno-strict-aliasing DO_LD=+$(DO_ECHO) $(CC) -Wl,-soname,libftedroid.so -shared -Wl,--no-undefined -Wl,-z,noexecstack -o $@ $(LTO_LD) $(WCFLAGS) $(BRANDFLAGS) $(CFLAGS) -llog -lc -lm -lz LD:=$(TOOLCHAIN)ld AR:=$(TOOLCHAIN)ar STRIP=$(TOOLCHAIN)strip endif #crosscompile macosx from linux, default target ppc 32bit ifeq ($(FTE_TARGET),macosx) ifeq ($(shell $(CC) -v 2>&1 | grep apple),) ifneq ($(shell which powerpc-apple-darwin8-gcc 2> /dev/null),) CC=powerpc-apple-darwin8-gcc CXX=powerpc-apple-darwin8-g++ STRIP=powerpc-apple-darwin8-strip #seems, macosx has a more limited version of strip STRIPFLAGS= BITS=32 EXTENSION=_ppc endif endif endif ifeq ($(FTE_TARGET),macosx_ppc64) ifeq ($(shell $(CC) -v 2>&1 | grep apple),) ifneq ($(shell which powerpc-apple-darwin8-gcc 2> /dev/null),) FTE_TARGET=macosx CC=powerpc-apple-darwin8-gcc -arch ppc64 CXX=powerpc-apple-darwin8-g++ -arch ppc64 STRIP=powerpc-apple-darwin8-strip #seems, macosx has a more limited version of strip STRIPFLAGS= BITS=64 EXTENSION=_ppc endif endif endif ifeq ($(FTE_TARGET),macosx_x86) ifeq ($(shell $(CC) -v 2>&1 | grep apple),) ifneq ($(shell which i686-apple-darwin8-gcc 2> /dev/null),) FTE_TARGET=macosx # i686-apple-darwin8-gcc's default target is i386, powerpc-apple-darwin8-gcc -arch i386 just invokes i686-apple-darwin8-gcc anyway CC=i686-apple-darwin8-gcc CXX=i686-apple-darwin8-g++ STRIP=i686-apple-darwin8-strip #seems, macosx has a more limited version of strip STRIPFLAGS= EXTENSION=_x86 endif endif endif #crosscompile morphos from linux ifeq ($(FTE_TARGET),morphos) ifeq ($(shell $(CC) -v 2>&1 | grep morphos),) ifneq ($(shell which ppc-morphos-gcc 2> /dev/null),) CC=ppc-morphos-gcc CXX=ppc-morphos-g++ #morphos strip has a 'feature', it strips permissions STRIP=ppc-morphos-strip endif endif endif ifeq ($(FTE_TARGET),dos) #at least from dos. CC=i586-pc-msdosdjgpp-gcc CXX=i586-pc-msdosdjgpp-g++ STRIP=i586-pc-msdosdjgpp-strip CFLAGS+=-DNO_ZLIB endif #if you have an x86, you can get gcc to build binaries using 3 different ABIs, instead of builds for just the default ABI ifeq ($(FTE_TARGET),linux32) FTE_TARGET=linux CC=gcc -m32 CXX=g++ -m32 STRIP=strip BITS=32 endif ifeq ($(FTE_TARGET),linux_x86) FTE_TARGET=linux CC=i686-linux-gnu-gcc PKGCONFIG=i686-linux-gnu-pkg-config CXX=i686-linux-gnu-g++ STRIP=i686-linux-gnu-strip BITS=32 endif ifeq ($(FTE_TARGET),linux_amd64) FTE_TARGET=linux CC=x86_64-linux-gnu-gcc PKGCONFIG=x86_64-linux-gnu-pkg-config CXX=x86_64-linux-gnu-g++ STRIP=x86_64-linux-gnu-strip BITS=64 endif ifeq ($(FTE_TARGET),linux_armhf) #debian's armhf is armv7, but armv6 works on RPI too. FTE_TARGET=linux CC=arm-linux-gnueabihf-gcc -marm -march=armv6 -mfpu=vfp -mfloat-abi=hard CXX=arm-linux-gnueabihf-g++ -marm -march=armv6 -mfpu=vfp -mfloat-abi=hard STRIP=arm-linux-gnueabihf-strip BITS=armhf endif ifeq ($(FTE_TARGET),linux_arm64) FTE_TARGET=linux CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ STRIP=aarch64-linux-gnu-strip BITS=arm64 USE_SPEEX=0 #fails to compile due to neon asm, I'm just going to disable it (will still soft-link). endif ifeq ($(FTE_TARGET),linux_aarch64) FTE_TARGET=linux CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ STRIP=aarch64-linux-gnu-strip BITS=aarch64 USE_SPEEX=0 #fails to compile due to neon asm, I'm just going to disable it (will still soft-link). endif ifeq ($(FTE_TARGET),linux_x32) #DO NOT CONFUSE WITH linux_x86. this target is amd64-with-32bit-pointers #note: the x32 abi is still not finished or something. #at the current time, you will need to edit your kernel's commandline to allow this stuff to run #try and use a proper cross-compiler if we can, otherwise fall back on multi-arch. FTE_TARGET=linux ifneq ($(shell which x86_64-linux-gnux32-gcc 2> /dev/null),) CC=x86_64-linux-gnux32-gcc CXX=x86_64-linux-gnux32-g++ STRIP=x86_64-linux-gnux32-strip else CC=gcc -mx32 CXX=g++ -mx32 STRIP=strip endif BITS=x32 endif ifeq ($(FTE_TARGET),linux64) FTE_TARGET=linux CC=gcc -m64 CXX=g++ -m64 STRIP=strip BITS=64 endif ifeq ($(FTE_TARGET),web) CC=emcc CXX=em++ AR=emar endif ifeq ($(FTE_TARGET),cygwin) FTE_TARGET=cyg endif ifeq ($(shell uname -s),OpenBSD) PKGCONFIG=pkg-config endif ifeq ($(FTE_TARGET),) #user didn't specify prefered target ifneq ($(shell uname 2>&1 | grep CYGWIN),) FTE_TARGET=cyg ANDROID_SCRIPT=android.bat endif ifneq ($(shell $(CC) -v 2>&1 | grep mingw),) FTE_TARGET=win32 endif ifeq ($(FTE_TARGET),) #still not set UNAME_SYSTEM:=$(shell uname) ifeq ($(UNAME_SYSTEM),Linux) FTE_TARGET=linux endif ifeq ($(UNAME_SYSTEM),Darwin) FTE_TARGET=macosx endif ifeq ($(UNAME_SYSTEM),FreeBSD) FTE_TARGET=bsd endif ifeq ($(UNAME_SYSTEM),NetBSD) FTE_TARGET=bsd endif ifeq ($(UNAME_SYSTEM),OpenBSD) FTE_TARGET=bsd endif ifeq ($(UNAME_SYSTEM),MorphOS) FTE_TARGET=morphos endif #else I've no idea what it is you're running endif FTE_TARGET ?= unk #so go for sdl. endif ifneq ($(shell ls|grep config.h),) HAVECONFIG=-DHAVE_CONFIG_H endif CLIENT_DIR=$(BASE_DIR)/client GL_DIR=$(BASE_DIR)/gl D3D_DIR=$(BASE_DIR)/d3d VK_DIR=$(BASE_DIR)/vk SW_DIR=$(BASE_DIR)/sw SERVER_DIR=$(BASE_DIR)/server COMMON_DIR=$(BASE_DIR)/common HTTP_DIR=$(BASE_DIR)/http #LIBS_DIR=$(BASE_DIR)/libs LIBS_DIR?=. PROGS_DIR=$(BASE_DIR)/qclib ifeq ($(NOCOMPAT),1) NCCFLAGS=-DNOLEGACY -DOMIT_QCC NCDIRPREFIX=nc endif ALL_CFLAGS=$(HAVECONFIG) $(VISIBILITY_FLAGS) $(BRANDFLAGS) $(CFLAGS) $(BASE_CFLAGS) $(WCFLAGS) $(ARCH_CFLAGS) $(NCCFLAGS) -I$(ARCHLIBS) ALL_CXXFLAGS=$(subst -Wno-pointer-sign,,$(ALL_CFLAGS)) #cheap compile-everything-in-one-unit (compile becomes preprocess only) ifneq ($(WPO),) LTO_CC= -E LTO_LD= -flto=jobserver -fwhole-program -x c LTO_END=ltoxnone LTO_START=ltoxc endif #proper/consistant link-time optimisations (requires gcc 4.5+ or so) ifneq ($(LTO),) LTO_CC=-flto=jobserver -fvisibility=hidden LTO_LD=-flto=jobserver endif #DO_ECHO=@echo $< && DO_ECHO=@ #DO_ECHO= DO_CC=$(DO_ECHO) $(CC) $(LTO_CC) $(ALL_CFLAGS) -o $@ -c $< DO_CXX=$(DO_ECHO) $(CXX) $(LTO_CC) $(ALL_CXXFLAGS) -o $@ -c $< ifeq ($(findstring msvc,$(FTE_TARGET)),msvc) BASELDFLAGS= VISIBILITY_FLAGS= endif ifeq ($(FTE_TARGET),cyg) BASELDFLAGS=-lm endif ifeq ($(FTE_TARGET),dos) BASELDFLAGS=-lm endif ifeq ($(FTE_TARGET),morphos) BASELDFLAGS=-lm endif ifneq (,$(findstring bsd,$(FTE_TARGET))) BASELDFLAGS=-lm endif BASELDFLAGS ?= -lm -ldl -lpthread ifeq (win,$(findstring cyg,$(FTE_TARGET))$(findstring win,$(FTE_TARGET))) BASELDFLAGS=-lm # MINGW_LIBS_DIR=$(LIBS_DIR)/mingw-libs # ifeq ($(shell echo $(FTE_TARGET)|grep -v win64),) # MINGW_LIBS_DIR=$(LIBS_DIR)/mingw64-libs # endif # IMAGELDFLAGS=$(MINGW_LIBS_DIR)/libpng.a $(MINGW_LIBS_DIR)/libz.a $(MINGW_LIBS_DIR)/libjpeg.a # OGGVORBISLDFLAGS=$(MINGW_LIBS_DIR)/libvorbisfile.a $(MINGW_LIBS_DIR)/libvorbis.a $(MINGW_LIBS_DIR)/libogg.a endif #BASELDFLAGS=-lm -lz XLDFLAGS=-L$(ARCHLIBS) $(IMAGELDFLAGS) #hack some other arguments based upon the toolchain ifeq ($(findstring msvc,$(FTE_TARGET)),msvc) WARNINGFLAGS?=-W3 -D_CRT_SECURE_NO_WARNINGS GNUC_FUNCS= else WARNINGFLAGS?=-Wall -Wno-pointer-sign -Wno-unknown-pragmas -Wno-format-zero-length -Wno-strict-aliasing #-Wcast-align # GNUC_FUNCS= -Dstrnicmp=strncasecmp -Dstricmp=strcasecmp endif SDL_INCLUDES= #-I$(LIBS_DIR)/sdl/include -I/usr/include/SDL -I$(LIBS_DIR)/sdl/include/SDL BASE_INCLUDES=-I$(CLIENT_DIR) -I$(SERVER_DIR) -I$(COMMON_DIR) -I$(GL_DIR) -I$(D3D_DIR) -I$(PROGS_DIR) -I. BASE_CFLAGS=$(WARNINGFLAGS) $(GNUC_FUNCS) $(BASE_INCLUDES) -I$(LIBS_DIR)/dxsdk9/include -I$(LIBS_DIR)/dxsdk7/include $(SDL_INCLUDES) $(SVNREVISION) CLIENT_ONLY_CFLAGS=-DCLIENTONLY SERVER_ONLY_CFLAGS=-DSERVERONLY JOINT_CFLAGS= DEBUG_CFLAGS?=-ggdb -g DEBUG_CFLAGS+=-DDEBUG -D_DEBUG RELEASE_CFLAGS?=$(CPUOPTIMIZATIONS) # #note: RELEASE_CFLAGS used to contain -ffast-math #however, its use resulted in the player getting stuck etc, so be warned if you try re-enabling it. # ifeq ($(findstring msvc,$(FTE_TARGET)),msvc) #msvc doesn't do -dumpmachine. #we might as well get it to reuse the mingw libraries, if only because that makes those libraries easier to compile... ifeq ($(FTE_TARGET),msvc32) BITS?=32 else ifeq ($(FTE_TARGET),msvc64) BITS?=64 endif BITS?=32 ifeq ($(BITS),64) ARCH?=x86_64-w64-mingw32 else ARCH?=i686-w64-mingw32 endif else #some idiot decided that -dumpmachine shouldn't respect -m32 etc. #at the same time, -print-multiarch is not present, buggy, or just screwed in many gcc builds (ones that target a single arch will unhelpfully just give an empty string). #so try multiarch first, and if that fails risk dumpmachine giving the wrong values. #really we want dumpmachine's more specific cpu arch included here, so lets hope that idiot burns for all eternity. or something equally melodramatic. ARCH:=$(shell $(CC) -print-multiarch 2>/dev/null) ifneq ($(words $(ARCH)),1) ARCH:=$(shell $(CC) -dumpmachine 2>/dev/null) endif #foo:=$(shell echo ARCH is $(ARCH) 1>&2 ) endif ARCHLIBS:=$(NATIVE_ABSBASE_DIR)/libs-$(ARCH) #incase our compiler doesn't support it (mingw) ifeq ($(shell LANG=c $(CC) -rdynamic 2>&1 | grep unrecognized),) DEBUG_CFLAGS+= -rdynamic endif PKGCONFIG?=$(ARCH)-pkg-config ifeq ($(shell which $(PKGCONFIG) 2> /dev/null),) FFS:=$(shell echo $(PKGCONFIG) not found 1>&2 ) PKGCONFIG=/bin/false #don't end up using eg /usr/include when cross-compiling. makelibs is a valid workaround. endif #try to statically link ifeq ($(COMPILE_SYS),Darwin) ifneq (,$(findstring SDL,$(FTE_TARGET))) IMAGELDFLAGS := $(shell $(PKGCONFIG) libpng --variable=libdir)/libpng.a $(shell $(PKGCONFIG) libjpeg --variable=libdir)/libjpeg.a OGGVORBISLDFLAGS := $(shell $(PKGCONFIG) vorbisfile --variable=libdir)/libvorbisfile.a $(shell $(PKGCONFIG) vorbis --variable=libdir)/libvorbis.a $(shell $(PKGCONFIG) ogg --variable=libdir)/libogg.a endif endif # Statically link everything but SDL/AL for the SDL target, # this assumes makelibs; but we don't really care. These are portable builds # that can only make one assumption: SDL/AL are installed. # Dependencies like libjpeg9 don't exist everywhere. ifeq ($(FTE_TARGET),SDL2) IMAGELDFLAGS:= libs-$(ARCH)/libpng.a libs-$(ARCH)/libjpeg.a OGGVORBISLDFLAGS:= libs-$(ARCH)/libvorbisfile.a libs-$(ARCH)/libvorbis.a libs-$(ARCH)/libogg.a BASE_CFLAGS+=-DLIBJPEG_STATIC -DLIBJPEG_STATIC -DLIBPNG_STATIC -DOPUS_STATIC -DSPEEX_STATIC -DFREETYPE_STATIC -DLIBVORBISFILE_STATIC endif VPATH := $(BASE_DIR) : $(CLIENT_DIR) : $(GL_DIR) : $(SW_DIR) : $(COMMON_DIR) : $(SERVER_DIR) : $(HTTP_DIR) : $(QUX_DIR) : $(PROGS_DIR) : $(D3D_DIR) : $(VK_DIR) : $(BASE_DIR)/web PROFILE_CFLAGS=-pg DX7SDK=-I./libs/dxsdk7/include/ GLCFLAGS?=-DGLQUAKE D3DCFLAGS?=-DD3D9QUAKE -DD3D11QUAKE VKCFLAGS?=-DVKQUAKE CLIENT_OBJS = \ textedit.o \ fragstats.o \ zqtp.o \ cl_demo.o \ cl_ents.o \ clq2_ents.o \ cl_input.o \ in_generic.o \ cl_main.o \ cl_parse.o \ cl_pred.o \ cl_tent.o \ cl_cam.o \ cl_screen.o \ pr_clcmd.o \ cl_ignore.o \ pr_csqc.o \ console.o \ image.o \ keys.o \ menu.o \ m_master.o \ m_multi.o \ m_items.o \ m_options.o \ m_single.o \ m_script.o \ m_native.o \ m_mp3.o \ roq_read.o \ clq2_cin.o \ r_part.o \ p_script.o \ p_null.o \ p_classic.o \ r_partset.o \ renderer.o \ renderque.o \ sbar.o \ skin.o \ snd_al.o \ snd_dma.o \ snd_mem.o \ snd_mix.o \ snd_mp3.o \ snd_ov.o \ valid.o \ vid_headless.o \ view.o \ wad.o \ \ ftpclient.o \ \ \ pr_menu.o VKQUAKE_OBJS = \ vk_init.o \ vk_backend.o GLQUAKE_OBJS = \ gl_draw.o \ gl_backend.o \ gl_rmain.o \ gl_rmisc.o \ gl_rsurf.o \ gl_screen.o \ gl_bloom.o \ gl_vidcommon.o \ $(VKQUAKE_OBJS) D3DQUAKE_OBJS = \ d3d8_backend.o \ d3d8_image.o \ vid_d3d8.o \ d3d_backend.o \ d3d_image.o \ d3d_shader.o \ vid_d3d.o \ d3d11_backend.o \ d3d11_image.o \ d3d11_shader.o \ vid_d3d11.o D3DGL_OBJS = \ gl_font.o \ gl_ngraph.o \ gl_shader.o \ gl_shadow.o \ gl_rlight.o \ gl_warp.o \ ltface.o \ r_surf.o \ r_2d.o MP3_OBJS = \ fixed.o \ bit.o \ timer.o \ stream.o \ frame.o \ synth.o \ decoder.o \ layer12.o \ layer3.o \ huffman.o \ mymad.o QCC_OBJS= \ comprout.o \ hash.o \ qcc_cmdlib.o \ qccmain.o \ qcc_pr_comp.o \ qcc_pr_lex.o \ qcd_main.o PROGS_OBJS = \ $(QCC_OBJS) \ initlib.o \ pr_bgcmd.o \ pr_skelobj.o \ pr_edict.o \ pr_exec.o \ pr_multi.o \ pr_x86.o \ qcdecomp.o SERVER_OBJS = \ pr_cmds.o \ pr_q1qvm.o \ pr_lua.o \ sv_master.o \ sv_init.o \ sv_main.o \ sv_nchan.o \ sv_ents.o \ sv_send.o \ sv_user.o \ sv_sql.o \ sv_mvd.o \ sv_ccmds.o \ sv_cluster.o \ sv_rankin.o \ sv_chat.o \ sv_demo.o \ net_preparse.o \ savegame.o \ svq2_ents.o \ svq2_game.o \ webgen.o \ ftpserver.o \ httpserver.o MASTER_OBJS= \ server/sv_master.c \ common/net_wins.c \ common/net_ice.c \ common/cvar.c \ common/cmd.c \ common/sha1.c \ common/sha2.c \ http/httpclient.c \ common/log.c \ common/fs.c \ common/fs_stdio.c \ common/common.c \ common/translate.c \ common/zone.c \ qclib/hash.c \ common/net_ssl_gnutls.c SERVERONLY_OBJS = \ sv_sys_unix.o \ sys_linux_threads.o WINDOWSSERVERONLY_OBJS = \ net_ssl_winsspi.o \ sv_sys_win.o \ sys_win_threads.o WINDOWS_OBJS = \ snd_win.o \ snd_directx.o \ snd_xaudio.o \ snd_wasapi.o \ cd_win.o \ fs_win32.o \ in_win.o \ sys_win.o \ sys_win_threads.o \ net_ssl_winsspi.o \ $(LTO_END) resources.o $(LTO_START) COMMON_OBJS = \ gl_alias.o \ gl_hlmdl.o \ gl_heightmap.o \ gl_model.o \ com_bih.o \ com_mesh.o \ common.o \ json.o \ cvar.o \ cmd.o \ crc.o \ net_ssl_gnutls.o \ net_master.o \ fs.o \ fs_stdio.o \ fs_pak.o \ fs_zip.o \ fs_dzip.o \ fs_xz.o \ m_download.o \ mathlib.o \ huff.o \ md4.o \ md5.o \ sha1.o \ sha2.o \ log.o \ net_chan.o \ net_wins.o \ net_ice.o \ httpclient.o \ zone.o \ qvm.o \ r_d3.o \ gl_q2bsp.o \ glmod_doom.o \ world.o \ sv_phys.o \ sv_move.o \ pmove.o \ pmovetst.o \ iwebiface.o \ translate.o \ plugin.o \ q1bsp.o \ q2pmove.o ifeq (1,$(LINK_QUAKE3)) VPATH := $(VPATH) : $(BASE_DIR)/../plugins/quake3 : $(BASE_DIR)/../plugins/quake3/botlib ALL_CFLAGS+=-DBOTLIB -DBOTLIB_STATIC -DSTATIC_Q3 COMMON_OBJS += \ clq3_parse.o \ clq3_ui.o \ clq3_cg.o \ svq3_game.o \ q3common.o \ be_aas_bspq3.o \ be_aas_cluster.o \ be_aas_debug.o \ be_aas_entity.o \ be_aas_file.o \ be_aas_main.o \ be_aas_move.o \ be_aas_optimize.o \ be_aas_reach.o \ be_aas_route.o \ be_aas_routealt.o \ be_aas_sample.o \ be_ai_char.o \ be_ai_chat.o \ be_ai_gen.o \ be_ai_goal.o \ be_ai_move.o \ be_ai_weap.o \ be_ai_weight.o \ be_ea.o \ be_interface.o \ l_crc.o \ l_libvar.o \ l_log.o \ l_memory.o \ l_precomp.o \ l_script.o \ l_struct.o endif COMMONLIBFLAGS?= COMMONLDDEPS?= CLIENTLIBFLAGS=$(COMMONLIBFLAGS) $(LIBOPUS_STATIC) $(LIBSPEEX_STATIC) $(OGGVORBISFILE_STATIC) SERVERLIBFLAGS=$(COMMONLIBFLAGS) CLIENTLDDEPS=$(COMMONLDDEPS) $(LIBOPUS_LDFLAGS) $(LIBSPEEX_LDFLAGS) $(OGGVORBISLDFLAGS) SERVERLDDEPS=$(COMMONLDDEPS) ifeq (1,$(USE_OPUS)) LIBOPUS_STATIC=-DOPUS_STATIC LIBOPUS_LDFLAGS=-lopus ALL_CFLAGS+=-I/usr/include/opus MAKELIBS+=libs-$(ARCH)/libopus.a endif ifeq (1,$(USE_SPEEX)) LIBSPEEX_STATIC=-DSPEEX_STATIC LIBSPEEX_LDFLAGS=-lspeex -lspeexdsp MAKELIBS+=libs-$(ARCH)/libspeex.a libs-$(ARCH)/libspeexdsp.a endif ifeq (1,$(USE_VORBISFILE)) OGGVORBISFILE_STATIC=-DLIBVORBISFILE_STATIC OGGVORBISLDFLAGS ?= -lvorbisfile -lvorbis -logg else OGGVORBISLDFLAGS= OGGVORBISFILE_STATIC= endif #freetype is annoying. ifeq (1,$(LINK_FREETYPE)) CLIENTLIBFLAGS+=-DFREETYPE_STATIC CLIENTLDDEPS+=$(FREETYPE_LDFLAGS) endif ifneq ("$(wildcard $(ARCHLIBS))","") #makelibs has been used. we know what to use here. FREETYPE_CFLAGS:= FREETYPE_LDFLAGS:=-lfreetype else #we're using system libraries... lets hope pkg-config knows it and can handle any cross compiles. FREETYPE_CFLAGS:=$(shell $(PKGCONFIG) freetype2 --cflags --silence-errors) FREETYPE_LDFLAGS:=$(shell $(PKGCONFIG) freetype2 --libs --silence-errors) ifeq (,$(FREETYPE_CFLAGS)) #system freetype headers not installed. don't try to include them! FREETYPE_CFLAGS:=-DNO_FREETYPE FREETYPE_LDFLAGS:= endif endif ALL_CFLAGS+=$(FREETYPE_CFLAGS) ifeq (1,$(LINK_PNG)) CLIENTLIBFLAGS+=-DLIBPNG_STATIC CLIENTLDDEPS+=-lpng endif ifeq (1,$(LINK_JPEG)) CLIENTLIBFLAGS+=-DLIBJPEG_STATIC CLIENTLDDEPS+=-ljpeg endif ifeq (1,$(LINK_ZLIB)) CLIENTLIBFLAGS+=-DZLIB_STATIC CLIENTLDDEPS+=-lz SERVERLDDEPS+=-lz #and deflate64, because why not. ifneq ("$(wildcard $(ARCHLIBS)/infback9.h)","") CLIENTLIBFLAGS+=-DZLIB_DEFLATE64 CLIENTLDDEPS+=-lz9 QCC_CFLAGS+=-DZLIB_DEFLATE64 QCC_LDFLAGS+=-lz9 endif endif ifeq (1,$(LINK_ODE)) ALL_CFLAGS+=$(shell $(PKGCONFIG) ode --cflags --silence-errors) -DODE_STATIC COMMONLDDEPS+=$(shell $(PKGCONFIG) ode --libs --silence-errors) endif ifeq (1,$(strip $(INTERNAL_BULLET))) COMMON_OBJS+=com_phys_bullet.o ALL_CFLAGS+=-I/usr/include/bullet -I$(ARCHLIBS)/bullet3-$(BULLETVER)/src COMMONLDDEPS+=-lBulletDynamics -lBulletCollision -lLinearMath LDCC=$(CXX) MAKELIBS+=libs-$(ARCH)/libBulletDynamics.a endif ifeq (1,$(LINK_EZHUD)) VPATH := $(VPATH) : $(BASE_DIR)/../plugins/ezhud ALL_CFLAGS+=-DSTATIC_EZHUD #let the plugins code know it needs to add the appropriate entry. CLIENT_OBJS += \ ezquakeisms.o \ hud.o \ hud_common.o \ hud_editor.o endif ifeq (1,$(LINK_OPENSSL)) ifeq (1,$(shell $(PKGCONFIG) --atleast-version 3 openssl && echo 1)) #must exist... and if its not openssl3 then its not gpl3-compatible so refuse to use it. VPATH := $(VPATH) : $(BASE_DIR)/../plugins ALL_CFLAGS+=-DSTATIC_OPENSSL #let the plugins code know it needs to add the appropriate entry. COMMON_OBJS += \ net_ssl_openssl.o ALL_DFLAGS+=$(shell $(PKGCONFIG) openssl --cflags --silence-errors) COMMONLDDEPS+=$(shell $(PKGCONFIG) openssl --libs --silence-errors) endif endif ifeq (1,$(shell $(PKGCONFIG) --exists gnutls && echo 1)) ALL_CFLAGS+=$(shell $(PKGCONFIG) gnutls --cflags --silence-errors) #ALL_CFLAGS+=-DGNUTLS_STATIC #COMMONLDDEPS+=$(shell $(PKGCONFIG) gnutls --static --libs --silence-errors) #we soft-link, so we don't need the -lgnutls stuff. else ALL_CFLAGS+=-DNO_GNUTLS endif #the defaults for sdl come first #CC_MACHINE:=$(shell $(CC) -dumpmachine) ifeq ($(FTE_TARGET),SDL2) SDLCONFIG?=sdl2-config FTE_FULLTARGET?=sdl2$(BITS) endif ifeq ($(FTE_TARGET),SDL1) SDLCONFIG?=sdl-config FTE_FULLTARGET?=sdl1$(BITS) endif ifeq ($(FTE_TARGET),SDL) FTE_FULLTARGET?=sdl$(BITS) endif SDLCONFIG?=sdl-config FTE_FULLTARGET?=sdl$(FTE_TARGET)$(BITS) GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o GL_EXE_NAME=../$(EXE_NAME)-gl$(FTE_FULLTARGET) GLCL_EXE_NAME=../$(EXE_NAME)cl-gl$(FTE_FULLTARGET) #SDLCONFIG:=libs-$(ARCH)/sdl2_mingw/$(CC_MACHINE)/bin/sdl2-config --prefix=libs-$(ARCH)/sdl2_mingw/$(CC_MACHINE) ifdef windir GL_LDFLAGS=$(GLLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` VK_LDFLAGS=$(VKLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` M_LDFLAGS=$(MLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` SV_LDFLAGS=`$(SDLCONFIG) --static-libs` else GL_LDFLAGS=$(GLLDFLAGS) $(IMAGELDFLAGS) `$(SDLCONFIG) --libs` VK_LDFLAGS=$(VKLDFLAGS) $(IMAGELDFLAGS) `$(SDLCONFIG) --libs` M_LDFLAGS=$(MLDFLAGS) $(IMAGELDFLAGS) `$(SDLCONFIG) --libs` SV_LDFLAGS=`$(SDLCONFIG) --libs` endif GL_CFLAGS=-DFTE_SDL $(GLCFLAGS) -DMULTITHREAD `$(SDLCONFIG) --cflags` GLB_DIR=gl_$(FTE_FULLTARGET) GLCL_DIR=glcl_$(FTE_FULLTARGET) SV_DIR?=sv_$(FTE_FULLTARGET) VKCL_OBJS=$(VKQUAKE_OBJS) $(D3DGL_OBJS) gl_bloom.o gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o VK_CFLAGS=-DFTE_SDL $(VKCFLAGS) -DMULTITHREAD `$(SDLCONFIG) --cflags` VKB_DIR=vk_$(FTE_FULLTARGET) VKCL_DIR=vk_$(FTE_FULLTARGET) VK_EXE_NAME=../$(EXE_NAME)-vk$(FTE_FULLTARGET) VKCL_EXE_NAME=../$(EXE_NAME)-vkcl$(FTE_FULLTARGET) SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(SERVERONLY_OBJS) SV_EXE_NAME=../$(EXE_NAME)-sv$(FTE_FULLTARGET) SV_CFLAGS=-DFTE_SDL -DMULTITHREAD `$(SDLCONFIG) --cflags` $(SERVER_ONLY_CFLAGS) MINGL_DIR=mingl_$(FTE_FULLTARGET) MINGL_EXE_NAME=../$(EXE_NAME)-mingl$(FTE_FULLTARGET) MB_DIR=m_$(FTE_FULLTARGET) MCL_DIR=mcl_$(FTE_FULLTARGET) M_EXE_NAME=../$(EXE_NAME)-$(FTE_FULLTARGET) MCL_EXE_NAME=../$(EXE_NAME)-cl$(FTE_FULLTARGET) MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o M_CFLAGS=-DFTE_SDL $(VKCFLAGS) $(GLCFLAGS) -DMULTITHREAD `$(SDLCONFIG) --cflags` MASTER_OBJECTS=$(MASTER_OBJS) server/sv_sys_unix.c common/sys_linux_threads.c QCC_DIR=qcc$(BITS) ifeq (,$(findstring NO_ZLIB,$(CFLAGS))) SV_LDFLAGS+=-lz GL_LDFLAGS+=-lz VK_LDFLAGS+=-lz M_LDFLAGS+=-lz QCC_LDFLAGS+=-L$(ARCHLIBS) -lz QTV_LDFLAGS+=-lz endif #specific targets override those defaults as needed. ifneq (,$(findstring SDL3,$(FTE_TARGET))$(findstring sdl3,$(FTE_TARGET))) FTE_FULLTARGET=$(subst SDL3,sdl3,$(BITS)-$(FTE_TARGET)) EXEPOSTFIX= DIRPOSTFIX=$(subst SDL3,sdl3,$(BITS)-$(FTE_TARGET)) SDL3_PC=$(ARCHLIBS)/lib/pkgconfig/sdl3.pc MAKELIBS+=$(SDL3_PC) ifeq (1,$(shell $(PKGCONFIG) --exists $(SDL3_PC) sdl3 && echo 1)) #use the makelibs one if its there SDL3_CFLAGS:=$(shell pkg-config --cflags $(SDL3_PC) sdl3) SDL3_LDFLAGS:=$(shell pkg-config --libs $(SDL3_PC) sdl3) else ifeq (1,$(shell $(PKGCONFIG) --exists sdl3 && echo 1)) #maybe we have a system one SDL3_CFLAGS:=$(shell pkg-config --cflags sdl3) SDL3_LDFLAGS=$(shell pkg-config --libs sdl3) else #outright failure. fail the build (without breaking makelibs). NEEDMAKELIBS=1 endif endif CLIENTLIBFLAGS+=$(SDL3_CFLAGS) GL_CFLAGS=-DFTE_SDL3 $(GLCFLAGS) -DMULTITHREAD $(CLIENTLIBFLAGS) VK_CFLAGS=-DFTE_SDL3 $(VKCFLAGS) -DMULTITHREAD $(CLIENTLIBFLAGS) M_CFLAGS=-DFTE_SDL3 $(VKCFLAGS) $(GLCFLAGS) -DMULTITHREAD $(CLIENTLIBFLAGS) SV_CFLAGS=-DFTE_SDL3 -DMULTITHREAD $(SDL3_CFLAGS) $(SERVER_ONLY_CFLAGS) ifeq ($(BITS),32) #we want ofs_t to actually be 64bit. CL_CFLAGS+=-D_LARGEFILE64_SOURCE SV_CFLAGS+=-D_LARGEFILE64_SOURCE endif GL_LDFLAGS=$(GLLDFLAGS) $(IMAGELDFLAGS) $(SDL3_LDFLAGS) VK_LDFLAGS=$(VKLDFLAGS) $(IMAGELDFLAGS) $(SDL3_LDFLAGS) M_LDFLAGS =$(MLDFLAGS) $(IMAGELDFLAGS) $(SDL3_LDFLAGS) SV_LDFLAGS=$(SDL3_LDFLAGS) #BASELDFLAGS+=-Wl,--warn-common #glibc is just buggy. ifeq (,$(findstring NO_ZLIB,$(CFLAGS))) #urgh, messy. this needs a better way BASELDFLAGS+= -lz QCC_LDFLAGS+=-L$(ARCHLIBS) -lz QTV_LDFLAGS+=-lz endif GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o VKCL_OBJS=$(VKQUAKE_OBJS) $(D3DGL_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o MCL_OBJS =$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o SV_OBJS =$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(SERVERONLY_OBJS) GL_EXE_NAME =../$(EXE_NAME)-gl$(FTE_FULLTARGET)$(EXEPOSTFIX) GLCL_EXE_NAME =../$(EXE_NAME)cl-gl$(FTE_FULLTARGET)$(EXEPOSTFIX) VK_EXE_NAME =../$(EXE_NAME)-vk$(FTE_FULLTARGET)$(EXEPOSTFIX) VKCL_EXE_NAME =../$(EXE_NAME)-vkcl$(FTE_FULLTARGET)$(EXEPOSTFIX) M_EXE_NAME =../$(EXE_NAME)$(FTE_FULLTARGET)$(EXEPOSTFIX) MCL_EXE_NAME =../$(EXE_NAME)-cl$(FTE_FULLTARGET)$(EXEPOSTFIX) MINGL_EXE_NAME=../$(EXE_NAME)-mingl$(FTE_FULLTARGET)$(EXEPOSTFIX) SV_EXE_NAME =../$(EXE_NAME)-sv$(FTE_FULLTARGET)$(EXEPOSTFIX) GLB_DIR =gl$(DIRPOSTFIX) GLCL_DIR =glcl$(DIRPOSTFIX) VKB_DIR =vk$(DIRPOSTFIX) VKCL_DIR =vkcl$(DIRPOSTFIX) MB_DIR =m$(DIRPOSTFIX) MCL_DIR =mcl$(DIRPOSTFIX) MINGL_DIR=mingl$(DIRPOSTFIX) SV_DIR =sv$(DIRPOSTFIX) QCC_DIR =qcc$(BITS) #doesn't actually use sdl ifeq (win,$(findstring win,$(FTE_TARGET))) ifneq (,$(findstring win64,$(FTE_TARGET))) BITS?=64 else BITS?=32 endif DO_CMAKE +=-DCMAKE_SYSTEM_NAME=Windows -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM="NEVER" BASELDFLAGS += -lws2_32 FTE_FULLTARGET =$(BITS)-sdl3 EXEPOSTFIX =.exe COMMON_OBJS += fs_win32.o net_ssl_winsspi.o endif $(SDL3_PC): cd $(BASE_DIR) && wget -N http://www.libsdl.org/release/SDL3-$(SDL3VER).tar.gz mkdir -p $(BASE_DIR)/libs-$(ARCH) && cd $(BASE_DIR)/libs-$(ARCH) && tar -xvzf $(BASE_DIR)/SDL3-$(SDL3VER).tar.gz cd $(BASE_DIR)/libs-$(ARCH)/SDL3-$(SDL3VER) && CFLAGS="$(CFLAGS) -Os" $(TOOLOVERRIDES) $(DO_CMAKE) -DCMAKE_INSTALL_PREFIX=$(BASE_DIR)/libs-$(ARCH) -DSDL_TESTS=OFF -DSDL_EXAMPLES=OFF -DSDL_STATIC=ON -DSDL_SHARED=OFF -DSDL_RELOCATABLE=ON . && $(TOOLOVERRIDES) $(MAKE) install endif #FTE_TARGET=win32_SDL | FTE_TARGET=win64_SDL (MinGW32 + SDL | MinGW64 + SDL) ifeq (win_SDL,$(findstring win,$(FTE_TARGET))$(findstring _SDL,$(FTE_TARGET))$(findstring _SDL3,$(FTE_TARGET))) DO_CMAKE+=-DCMAKE_SYSTEM_NAME=Windows -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM="NEVER" ifneq (,$(findstring win64,$(FTE_TARGET))) BITS=64 endif EXEPOSTFIX=.exe ARCH_PREDEP=$(BASE_DIR)/libs-$(ARCH)/SDL2-$(SDL2VER)/$(ARCH)/bin/sdl2-config SDLCONFIG=$(ARCH_PREDEP) --prefix=$(BASE_DIR)/libs-$(ARCH)/SDL2-$(SDL2VER)/$(ARCH) CLIENTLIBFLAGS+=`$(SDLCONFIG) --cflags` COMMON_OBJS += fs_win32.o net_ssl_winsspi.o #the defaults for sdl come first GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o snd_directx.o $(LTO_END) resources.o $(LTO_START) GL_EXE_NAME=../$(EXE_NAME)-sdl-gl$(BITS)$(EXEPOSTFIX) GLCL_EXE_NAME=../$(EXE_NAME)-sdl-glcl$(BITS)$(EXEPOSTFIX) ifneq ($(SDL_STATIC),0) #statically link by default. dlls suck. GL_LDFLAGS=$(GLLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` VK_LDFLAGS=$(GLLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` M_LDFLAGS=$(MLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` D3D_LDFLAGS=$(MLDFLAGS) -lmingw32 -lws2_32 `$(SDLCONFIG) --static-libs` SV_LDFLAGS=-lm -lmingw32 -lws2_32 -lwinmm `$(SDLCONFIG) --static-libs` QCC_LDFLAGS= else #or dynamically link when SDL_STATIC=0 GL_LDFLAGS=$(IMAGELDFLAGS) -lws2_32 -lmingw32 $(SDL_LDFLAGS) -mwindows -ldxguid -lwinmm -lole32 $(GLLDFLAGS) `$(SDLCONFIG) --libs` VK_LDFLAGS=$(IMAGELDFLAGS) -lws2_32 -lmingw32 $(SDL_LDFLAGS) -mwindows -ldxguid -lwinmm -lole32 $(GLLDFLAGS) `$(SDLCONFIG) --libs` M_LDFLAGS=$(IMAGELDFLAGS) -lws2_32 -lmingw32 $(SDL_LDFLAGS) -mwindows -ldxguid -lwinmm -lole32 $(MLDFLAGS) `$(SDLCONFIG) --libs` D3D_LDFLAGS=$(IMAGELDFLAGS) -lws2_32 -lmingw32 $(SDL_LDFLAGS) -mwindows -ldxguid -lwinmm -lole32 $(MLDFLAGS) `$(SDLCONFIG) --libs` SV_LDFLAGS=-lm -lmingw32 -lws2_32 -lwinmm `$(SDLCONFIG) --libs` QCC_LDFLAGS= endif GL_CFLAGS=-DFTE_SDL $(GLCFLAGS) $(CLIENTLIBFLAGS) $(DX7SDK) GLB_DIR=gl_mgw_sdl$(BITS) GLCL_DIR=glcl_mgw_sdl$(BITS) #don't use sdl at all for dedicated servers. it'd break running it as a console program etc. its not officially supported, but included anyway for ease of use. SV_DIR=sv_winsdl$(BITS) SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(WINDOWSSERVERONLY_OBJS) fs_win32.o $(LTO_END) resources.o $(LTO_START) SV_EXE_NAME=../$(EXE_NAME)-sdl-sv$(BITS)$(EXEPOSTFIX) SV_LDFLAGS=-lm -lole32 -lws2_32 -lwinmm SV_CFLAGS=$(SERVER_ONLY_CFLAGS) -mconsole #-DFTE_SDL MINGL_DIR=mingl_sdlwin$(BITS) MINGL_EXE_NAME=../$(EXE_NAME)-sdl-mingl$(BITS)$(EXEPOSTFIX) MB_DIR=m_mgw_sdl$(BITS) M_EXE_NAME=../$(EXE_NAME)-sdl$(BITS)$(EXEPOSTFIX) #with d3d... #MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) $(D3DQUAKE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o snd_directx.o $(LTO_END) resources.o $(LTO_START) #M_CFLAGS=$(D3DCFLAGS) $(VKCFLAGS) $(GLCFLAGS) -DFTE_SDL $(CLIENTLIBFLAGS) $(DX7SDK) #without d3d... MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o snd_directx.o $(LTO_END) resources.o $(LTO_START) M_CFLAGS=$(VKCFLAGS) $(GLCFLAGS) -DFTE_SDL $(CLIENTLIBFLAGS) $(DX7SDK) #unsupported, video code has winmsg/input junk in it. D3DCL_OBJS=$(D3DQUAKE_OBJS) snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o snd_directx.o $(D3DGL_OBJS) $(LTO_END) resources.o $(LTO_START) D3D_EXE_NAME=../$(EXE_NAME)-sdl-d3d$(BITS)$(EXEPOSTFIX) D3DCL_EXE_NAME=../$(EXE_NAME)-sdl-d3dcl$(BITS)$(EXEPOSTFIX) D3D_CFLAGS=$(D3DCFLAGS) -DFTE_SDL -DNO_XFLIP $(CLIENTLIBFLAGS) $(DX7SDK) D3DB_DIR=sdl_d3d_mgw$(BITS) D3DCL_DIR=sdl_d3dcl_mgw$(BITS) VKCL_OBJS=$(VKQUAKE_OBJS) gl_bloom.o gl_vidsdl.o snd_sdl.o cd_sdl.o sys_sdl.o in_sdl.o snd_directx.o $(D3DGL_OBJS) $(LTO_END) resources.o $(LTO_START) VK_EXE_NAME=../$(EXE_NAME)-sdl-vk$(BITS)$(EXEPOSTFIX) VKCL_EXE_NAME=../$(EXE_NAME)-sdl-vkcl$(BITS)$(EXEPOSTFIX) VK_CFLAGS=$(VKCFLAGS) -DFTE_SDL -DNO_XFLIP $(CLIENTLIBFLAGS) $(DX7SDK) VKB_DIR=sdl_vk_mgw$(BITS) VKCL_DIR=sdl_vkcl_mgw$(BITS) ifeq ($(shell echo $(FTE_TARGET)|grep -E -i -v "win32.*sdl"),) GL_CFLAGS+= -D_MINGW_VFPRINTF VK_CFLAGS+= -D_MINGW_VFPRINTF D3D_CFLAGS+= -D_MINGW_VFPRINTF M_CFLAGS+= -D_MINGW_VFPRINTF endif endif #FTE_TARGET=vc (Visual C) ifeq ($(findstring msvc,$(FTE_TARGET)),msvc) #assumptions... ifneq ($(shell uname -o 2>&1 | grep Cygwin),) #we're on cygwin MSVCPATH?=C:/Program Files (x86)/Microsoft Visual Studio 8/VC/ WINDOWSSDKDIR?=C:/Program Files/Microsoft SDKs/Windows/v7.1/ FIXPATH=`cygpath -m "$(1)"` INVOKE="$(1)" else #we're PROBABLY on linux (invoke it via wine) #MSVCPATH=/mnt/win/Program Files (x86)/Microsoft Visual Studio 10.0/VC/ MSVCPATH?=/mnt/win/Program Files (x86)/Microsoft Visual Studio 8/VC/ WINDOWSSDKDIR?=/mnt/win/Program Files/Microsoft SDKs/Windows/v7.1/ #functions to handle wine (or cygwin) FIXPATH=`winepath -w "$(1)"` INVOKE=WINEDEBUG=-all WINEPATH="$(WINEPATH)" wine "$(1)" endif #our msvc library naming still assumes 32bit and thus no postfix for that target (microsoft usually reuse '32' for 64bit libraries too...). ifeq ($(BITS),32) LIBBITS= else LIBBITS=$(BITS) endif #evil lameness. LIBS_DIR=./libs/ ifeq ($(BITS),64) MSVC_ARCH=amd64/ WINSDK_ARCH=x64/ WINEPATH= else MSVC_ARCH= WINSDK_ARCH= WINEPATH=$(MSVCPATH)../Common7/IDE/ endif LIBOPUS_STATIC= #-I"$(MSVCPATH)/ATLMFC/INCLUDE" MSVCINC+=-I"$(shell winepath -w "$(MSVCPATH)INCLUDE")" MSVCINC+=-I"$(shell winepath -w "$(WINDOWSSDKDIR)/Include")" #-I"$(MSVCPATH)PlatformSDK/include" #-I"$(MSVCPATH)../SDK/v2.0/include" MSVCLIB+=/libpath:"$(call FIXPATH,$(MSVCPATH)lib/$(MSVC_ARCH))" MSVCLIB+=/libpath:"$(call FIXPATH,$(WINDOWSSDKDIR)Lib/$(WINSDK_ARCH))" MSVCLIB+=/libpath:"$(call FIXPATH,$(LIBS_DIR))" ARCHLIBS:=$(call FIXPATH,$(ARCHLIBS)) MSVCINC:=$(MSVCINC) DO_WINDRES=$(DO_ECHO) $(WINDRES) /nologo $(BRANDFLAGS) $(MSVCINC) -I$(call FIXPATH,$(CLIENT_DIR)) -fo $(call FIXPATH,$@) $(call FIXPATH,$<) WINDRES=$(call INVOKE,$(WINDOWSSDKDIR)Bin/$(WINSDK_ARCH)RC.exe) LINK:=$(call INVOKE,$(MSVCPATH)/bin/$(MSVC_ARCH)link.exe) CC:=$(call INVOKE,$(MSVCPATH)/bin/$(MSVC_ARCH)cl.exe) $(MSVCINC) -DMSVCLIBSPATH=libs/ CXX=$(CC) #library stuff... we'll be breaking attempts to statically link because deps are too painful otherwise. BASELDFLAGS= CLIENTLIBFLAGS= SERVERLIBFLAGS= CLIENTLDDEPS= SERVERLDDEPS= #override various flags that expected gcc. DEBUG_CFLAGS = -Od $(CPUOPTIMIZATIONS) /fp:fast RELEASE_CFLAGS = -O2 -Gy -GS- $(CPUOPTIMIZATIONS) /fp:fast RELEASE_LDFLAGS = /LTCG #we need some command tweaks. DO_CC=$(DO_ECHO) $(CC) /nologo $(ALL_CFLAGS) -Fo"$(call FIXPATH,$@)" -c "$(call FIXPATH,$<)" DO_CXX=$(DO_ECHO) $(CXX) /nologo $(ALL_CFLAGS) -Fo"$(call FIXPATH,$@)" -c "$(call FIXPATH,$<)" DO_LD=$(DO_ECHO) PATH="$(MSVCPATH)/Common7/IDE:$(PATH)" $(LINK) /nologo /out:"$(call FIXPATH,$@)" /nodefaultlib:libc.lib /LARGEADDRESSAWARE /nodefaultlib:MSVCRT $(MSVCLIB) $(SDKLIB) /manifest:no /OPT:REF wsock32.lib user32.lib kernel32.lib advapi32.lib winmm.lib libs/zlib$(LIBBITS).lib shell32.lib STRIP=@echo SKIP: strip EXEPOSTFIX=.exe #gotta work around a whole load of paths. BASE_CFLAGS:=$(WARNINGFLAGS) $(GNUC_FUNCS) -I$(call FIXPATH,$(CLIENT_DIR)) -I$(call FIXPATH,$(SERVER_DIR)) -I$(call FIXPATH,$(COMMON_DIR)) -I$(call FIXPATH,$(GL_DIR)) -I$(call FIXPATH,$(D3D_DIR)) -I$(call FIXPATH,$(PROGS_DIR)) -I. -I$(LIBS_DIR) -I$(LIBS_DIR)/dxsdk9/include -I$(LIBS_DIR)/dxsdk7/include $(SDL_INCLUDES) $(BOTLIB_CFLAGS) $(SVNREVISION) #this stuff isn't supported. PRECOMPHEADERS= DEPCC= DEPCXX= #dedicated server stuff SV_CFLAGS=$(SERVER_ONLY_CFLAGS) $(W32_CFLAGS) -DMULTITHREAD -DMSVCLIBPATH=libs/ SV_EXE_NAME=../$(EXE_NAME)-sv$(BITS)$(EXEPOSTFIX) SV_DIR=sv_vc$(BITS) SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(WINDOWSSERVERONLY_OBJS) fs_win32.o resources.o SV_LDFLAGS=ole32.lib /subsystem:console GL_EXE_NAME=../$(EXE_NAME)-gl$(BITS)$(EXEPOSTFIX) GLCL_EXE_NAME=../$(EXE_NAME)-mingl$(BITS) GLB_DIR=gl_vc$(BITS) GLCL_DIR=glcl_vc$(BITS) GL_LDFLAGS=$(GLLDFLAGS) $(JPEGLIB) libs/libpng$(BITS).lib uuid.lib gdi32.lib ole32.lib /subsystem:windows GL_CFLAGS=$(GLCFLAGS) $(W32_CFLAGS) -DMULTITHREAD -DMSVCLIBPATH=libs/ GLCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidnt.o $(WINDOWS_OBJS) GL_OBJS= MINGL_DIR=mingl_vc$(BITS) MINGL_EXE_NAME=../$(EXE_NAME)-mingl$(BITS)$(EXEPOSTFIX) VKCL_OBJS=$(VKQUAKE_OBJS) $(D3DGL_OBJS) gl_bloom.o gl_vidnt.o $(WINDOWS_OBJS) VK_EXE_NAME=../$(EXE_NAME)-vk$(BITS)$(EXEPOSTFIX) VKCL_EXE_NAME=../$(EXE_NAME)-vkcl$(BITS)$(EXEPOSTFIX) VK_CFLAGS=$(VKCFLAGS) $(CLIENTLIBFLAGS) $(DX7SDK) -DMULTITHREAD -DMSVCLIBPATH=libs/ VK_LDFLAGS=$(JPEGLIB) libs/libpng$(BITS).lib uuid.lib gdi32.lib ole32.lib /subsystem:windows VKB_DIR=vk_vc$(BITS) VKCL_DIR=vkcl_vc$(BITS) D3DCL_OBJS=$(D3DQUAKE_OBJS) $(D3DGL_OBJS) $(WINDOWS_OBJS) D3D_EXE_NAME=../$(EXE_NAME)-d3d$(BITS)$(EXEPOSTFIX) D3DCL_EXE_NAME=../$(EXE_NAME)-d3dcl$(BITS)$(EXEPOSTFIX) D3D_LDFLAGS=$(JPEGLIB) libs/libpng$(BITS).lib uuid.lib gdi32.lib ole32.lib /subsystem:windows D3D_CFLAGS=$(D3DCFLAGS) $(W32_CFLAGS) $(DX7SDK) -DMULTITHREAD -DMSVCLIBPATH=libs/ D3DB_DIR=d3d_vc$(BITS) D3DCL_DIR=d3dcl_vc$(BITS) #merged client stuff MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) $(D3DQUAKE_OBJS) gl_vidnt.o gl_videgl.o $(WINDOWS_OBJS) M_EXE_NAME=../$(EXE_NAME)$(BITS)$(EXEPOSTFIX) MCL_EXE_NAME=../$(EXE_NAME)cl$(BITS)$(EXEPOSTFIX) M_CFLAGS=$(GLCFLAGS) $(W32_CFLAGS) $(D3DCFLAGS) $(DX7SDK) $(VKCFLAGS) -DMULTITHREAD $(CLIENTLIBFLAGS) MB_DIR=m_vc$(BITS) MCL_DIR=mcl_vc$(BITS) M_LDFLAGS=$(GLLDFLAGS) $(JPEGLIB) libs/libpng$(LIBBITS).lib uuid.lib gdi32.lib ole32.lib /subsystem:windows endif #FTE_TARGET=win32 | FTE_TARGET=win64 (MinGW32 | MinGW64) ifeq (win,$(findstring win,$(FTE_TARGET))$(findstring _SDL,$(FTE_TARGET))) # The extra object file called resources.o is specific for MinGW to link the icon in # DO_CMAKE+=-DCMAKE_SYSTEM_NAME=Windows -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM="NEVER" DO_CMAKE=cmake -DCMAKE_TOOLCHAIN_FILE=/home/spike/fteqw/fteqw-code/cmakesucks.cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_C_COMPILER="$(firstword $(CC))" -DCMAKE_C_FLAGS="$(wordlist 2,99,$(CC))" -DCMAKE_CXX_COMPILER="$(firstword $(CXX))" -DCMAKE_CXX_FLAGS="$(wordlist 2,99,$(CXX))" #cygwin's gcc requires an extra command to use mingw instead of cygwin (default paths, etc). ifneq ($(shell $(CC) -dumpmachine 2>&1 | grep cygwin),) W32_CFLAGS=-mno-cygwin endif ifeq ($(FTE_TARGET),win64) BITS=64 endif QCC_DIR=winqcc$(BITS) BASELDFLAGS= # Allow 32bit FTE to access beyond the 2GB address space ifeq ($(FTE_TARGET),win32) BASELDFLAGS=-Wl,--large-address-aware endif #Note: for deterministic builds, the following line disables timestamps for import/export tables. This is UNSAFE if there are any PE files bound to the compiled PE file. Our plugin dlls are dynamically loaded so this should not be an issue for us. BASELDFLAGS+=-Wl,--no-insert-timestamp DEBUG_LDFLAGS=-Wl,--no-dynamicbase #debug builds are useful for catching crashes. the resulting stack traces are not very useful if we don't even know what the base address should be. RELEASE_LDFLAGS=-Wl,--dynamicbase #release builds should attempt to use aslr BASELDFLAGS+=-lcomctl32 EXEPOSTFIX=.exe QTV_LDFLAGS+=-lws2_32 -lwinmm SV_EXE_NAME=../$(EXE_NAME)sv$(BITS)$(EXEPOSTFIX) SV_LDFLAGS=-lws2_32 -lwinmm -lole32 SV_DIR=sv_mingw$(BITS) SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(WINDOWSSERVERONLY_OBJS) fs_win32.o $(LTO_END) resources.o $(LTO_START) SV_CFLAGS=$(SERVER_ONLY_CFLAGS) $(W32_CFLAGS) MASTER_OBJECTS=$(MASTER_OBJS) server/sv_sys_win.c common/sys_win_threads.c common/net_ssl_winsspi.c GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidnt.o $(WINDOWS_OBJS) GL_EXE_NAME=../fteglqw$(BITS)$(EXEPOSTFIX) GLCL_EXE_NAME=../fteglqwcl$(BITS)$(EXEPOSTFIX) GL_LDFLAGS=$(GLLDFLAGS) $(IMAGELDFLAGS) -ldxguid -lws2_32 -lwinmm -lgdi32 -lole32 -Wl,--subsystem,windows GL_CFLAGS=$(GLCFLAGS) $(W32_CFLAGS) $(DX7SDK) -DMULTITHREAD $(CLIENTLIBFLAGS) GLB_DIR=gl_mgw$(BITS) GLCL_DIR=glcl_mgw$(BITS) MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) $(D3DQUAKE_OBJS) gl_vidnt.o gl_videgl.o $(WINDOWS_OBJS) M_EXE_NAME=../$(EXE_NAME)$(BITS)$(EXEPOSTFIX) MCL_EXE_NAME=../$(EXE_NAME)cl$(BITS)$(EXEPOSTFIX) M_LDFLAGS=$(GLLDFLAGS) $(IMAGELDFLAGS) -ldxguid -lws2_32 -lwinmm -lgdi32 -lole32 -Wl,--subsystem,windows M_CFLAGS=$(GLCFLAGS) $(W32_CFLAGS) $(D3DCFLAGS) $(DX7SDK) $(VKCFLAGS) -DMULTITHREAD $(CLIENTLIBFLAGS) MB_DIR=m_mgw$(BITS) MCL_DIR=mcl_mgw$(BITS) D3DCL_OBJS=$(D3DQUAKE_OBJS) $(D3DGL_OBJS) $(WINDOWS_OBJS) D3D_EXE_NAME=../fted3dqw$(BITS)$(EXEPOSTFIX) D3DCL_EXE_NAME=../fted3dclqw$(BITS)$(EXEPOSTFIX) D3D_LDFLAGS=$(IMAGELDFLAGS) -ldxguid -lws2_32 -lwinmm -lgdi32 -lole32 -Wl,--subsystem,windows D3D_CFLAGS=$(D3DCFLAGS) $(W32_CFLAGS) $(DX7SDK) -DMULTITHREAD $(CLIENTLIBFLAGS) D3DB_DIR=d3d_mgw$(BITS) D3DCL_DIR=d3dcl_mgw$(BITS) VKCL_OBJS=$(GLQUAKE_OBJS) $(D3DGL_OBJS) $(WINDOWS_OBJS) gl_vidnt.o VK_EXE_NAME=../ftevkqw$(BITS)$(EXEPOSTFIX) VKCL_EXE_NAME=../ftevkclqw$(BITS)$(EXEPOSTFIX) VK_LDFLAGS=$(IMAGELDFLAGS) -ldxguid -lws2_32 -lwinmm -lgdi32 -lole32 -Wl,--subsystem,windows VK_CFLAGS=$(VKCFLAGS) $(W32_CFLAGS) $(DX7SDK) -DMULTITHREAD $(CLIENTLIBFLAGS) VKB_DIR=vk_mgw$(BITS) VKCL_DIR=vkcl_mgw$(BITS) MINGL_EXE_NAME=../fteminglqw$(BITS)$(EXEPOSTFIX) MINGL_DIR=mingl_mgw$(BITS) ifeq (,$(findstring NO_ZLIB,$(CFLAGS))) SV_LDFLAGS+=-lz GL_LDFLAGS+=-lz VK_LDFLAGS+=-lz M_LDFLAGS+=-lz QCC_LDFLAGS+=-L$(ARCHLIBS) -lz endif ifeq ($(NOCOMPAT),1) SV_EXE_NAME=../engine-sv$(BITS)$(EXEPOSTFIX) GL_EXE_NAME=../engine-gl$(BITS)$(EXEPOSTFIX) VK_EXE_NAME=../engine-vk$(BITS)$(EXEPOSTFIX) M_EXE_NAME=../engine$(BITS)$(EXEPOSTFIX) D3D_EXE_NAME=../engine-d3d$(BITS)$(EXEPOSTFIX) MINGL_EXE_NAME=../engine-mingl$(BITS)$(EXEPOSTFIX) endif endif ifeq ($(FTE_TARGET),bsd) #mostly uses the linux stuff. #oss, X, etc. CC=cc CXX=c++ SV_DIR=sv_bsd SV_EXE_NAME=../$(EXE_NAME)-sv$(BITS) SV_LDFLAGS=-lpthread SV_CFLAGS=$(SERVER_ONLY_CFLAGS) -DMULTITHREAD GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidlinuxglx.o snd_linux.o cd_null.o sys_linux.o sys_linux_threads.o GL_EXE_NAME=../$(EXE_NAME)-gl GLCL_EXE_NAME=../$(EXE_NAME)-glcl GL_LDFLAGS= -L/usr/local/lib $(GLLDFLAGS) $(XLDFLAGS) -lpthread GL_CFLAGS=$(GLCFLAGS) -I/usr/local/include -I/usr/X11R6/include -I/usr/X11R6/include/freetype2 -DMULTITHREAD -DAUDIO_OSS GLB_DIR=gl_bsd GLCL_DIR=glcl_bsd MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidlinuxglx.o snd_linux.o cd_null.o sys_linux.o sys_linux_threads.o M_EXE_NAME=../$(EXE_NAME) MCL_EXE_NAME=../$(EXE_NAME)-cl M_LDFLAGS= -L/usr/local/lib -L/usr/X11R6/lib $(GLLDFLAGS) $(XLDFLAGS) -lpthread M_CFLAGS=$(VKCFLAGS) $(GLCFLAGS) -I/usr/local/include -I/usr/X11R6/include -I/usr/X11R6/include/freetype2 -DMULTITHREAD -DAUDIO_OSS MB_DIR=m_bsd MCL_DIR=mcl_bsd MINGL_EXE_NAME=../$(EXE_NAME)-mingl MINGL_DIR=mingl_bsd #openbsd has a special library for oss emulation. ifeq ($(shell uname -s),OpenBSD) GL_LDFLAGS+= -lossaudio -lfreetype VK_LDFLAGS+= -lossaudio -lfreetype M_LDFLAGS+= -lossaudio -lfreetype M_CFLAGS+= -DFREETYPE_STATIC VK_CFLAGS+= -DFREETYPE_STATIC GL_CFLAGS+= -DFREETYPE_STATIC endif ifeq (,$(findstring NO_ZLIB,$(CFLAGS))) SV_LDFLAGS+= -lz GL_LDFLAGS+= -lz VK_LDFLAGS+= -lz M_LDFLAGS+= -lz endif endif ifneq (,$(findstring linux,$(FTE_TARGET))) SV_DIR=sv_linux$(BITS) SV_EXE_NAME=../$(EXE_NAME)-sv$(BITS) SV_LDFLAGS= SV_CFLAGS=$(SERVER_ONLY_CFLAGS) -DMULTITHREAD ifneq ("$(wildcard /usr/include/wayland-client.h)","") HAVE_WAYLAND=-DWAYLANDQUAKE else HAVE_WAYLAND= endif ifneq ("$(wildcard /usr/include/EGL/egl.h)","") HAVE_EGL=-DUSE_EGL else HAVE_EGL= endif CL_CFLAGS=-DMULTITHREAD -DDYNAMIC_SDL $(HAVE_EGL) $(HAVE_WAYLAND) -DX11QUAKE -DAUDIO_PULSE -DAUDIO_ALSA -DAUDIO_OSS -DAUDIO_SDL BASELDFLAGS+=-Wl,--warn-common ifeq ($(BITS),32) CL_CFLAGS+=-D_LARGEFILE64_SOURCE SV_CFLAGS+=-D_LARGEFILE64_SOURCE endif QCC_DIR=linqcc$(BITS) GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidlinuxglx.o gl_vidwayland.o gl_videgl.o snd_pulse.o snd_alsa.o snd_linux.o snd_sdl.o cd_linux.o sys_linux.o sys_linux_threads.o GL_EXE_NAME=../$(EXE_NAME)-gl$(BITS) GLCL_EXE_NAME=../$(EXE_NAME)-glcl$(BITS) GL_LDFLAGS=$(GLLDFLAGS) $(XLDFLAGS) GL_CFLAGS=$(GLCFLAGS) -I/usr/X11R6/include $(CL_CFLAGS) $(CLIENTLIBFLAGS) GLB_DIR=gl_linux$(BITS) GLCL_DIR=glcl_linux$(BITS) VKCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidlinuxglx.o gl_vidwayland.o gl_videgl.o snd_pulse.o snd_alsa.o snd_linux.o snd_sdl.o cd_linux.o sys_linux.o sys_linux_threads.o VK_EXE_NAME=../$(EXE_NAME)-vk$(BITS) VKCL_EXE_NAME=../$(EXE_NAME)-vkcl$(BITS) VK_LDFLAGS=$(GLLDFLAGS) $(XLDFLAGS) VK_CFLAGS=$(VKCFLAGS) -I/usr/X11R6/include $(CL_CFLAGS) $(CLIENTLIBFLAGS) VKB_DIR=vk_linux$(BITS) VKCL_DIR=vkcl_linux$(BITS) MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidlinuxglx.o gl_vidwayland.o gl_videgl.o snd_linux.o snd_sdl.o snd_pulse.o snd_alsa.o cd_linux.o sys_linux.o sys_linux_threads.o M_EXE_NAME=../$(EXE_NAME)$(BITS) MCL_EXE_NAME=../$(EXE_NAME)-cl$(BITS) M_LDFLAGS=$(GL_LDFLAGS) M_CFLAGS=$(VKCFLAGS) $(GL_CFLAGS) $(CLIENTLIBFLAGS) MB_DIR=m_linux$(BITS) MCL_DIR=mcl_linux$(BITS) ifeq (,$(findstring NO_ZLIB,$(CFLAGS))) SV_LDFLAGS+= -lz GL_LDFLAGS+= -lz VK_LDFLAGS+= -lz M_LDFLAGS+= -lz endif MINGL_EXE_NAME=../$(EXE_NAME)-mingl$(BITS) MINGL_DIR=mingl_linux$(BITS) ifeq ($(NOCOMPAT),1) SV_EXE_NAME=../engine-sv$(BITS)$(EXEPOSTFIX) GL_EXE_NAME=../engine-gl$(BITS)$(EXEPOSTFIX) VK_EXE_NAME=../engine-vk$(BITS)$(EXEPOSTFIX) M_EXE_NAME=../engine$(BITS)$(EXEPOSTFIX) D3D_EXE_NAME=../engine-d3d$(BITS)$(EXEPOSTFIX) MINGL_EXE_NAME=../engine-mingl$(BITS)$(EXEPOSTFIX) endif endif ifneq (,$(findstring rpi,$(FTE_TARGET))) #These next two lines enable cross compiling. If you're compiling natively you can just kill the two. RPI_SYSROOT:=$(realpath $(shell echo ~)/rpi/rpi-sysroot/) CC=~/rpi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-gcc --sysroot=$(RPI_SYSROOT) CXX=~/rpi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-g++ --sysroot=$(RPI_SYSROOT) SDLCONFIG=$(RPI_SYSROOT)/usr/bin/sdl-config --prefix=$(RPI_SYSROOT)/usr GL_CFLAGS+= -I$(RPI_SYSROOT)/opt/vc/include -I$(RPI_SYSROOT)/opt/vc/include/interface/vmcs_host/linux -I$(RPI_SYSROOT)/opt/vc/include/interface/vcos/pthreads -DFTE_RPI -DUSE_EGL GL_LDFLAGS+= -L$(RPI_SYSROOT)/opt/vc/lib -Wl,--sysroot=$(RPI_SYSROOT),-rpath=/opt/vc/lib,-rpath-link=$(RPI_SYSROOT)/opt/vc/lib -lbcm_host GLCL_OBJS+=gl_vidrpi.o endif ifneq (,$(findstring fbdev,$(FTE_TARGET))) GL_CFLAGS+=-DUSE_EGL GLCL_OBJS+=gl_vidfbdev.o MCL_OBJS+=gl_vidfbdev.o endif ifneq ($(shell echo $(FTE_TARGET)|grep macosx),) SV_DIR=sv_macosx$(EXTENSION)$(BITS) GLB_DIR=gl_macosx$(EXTENSION)$(BITS) GLCL_DIR=glcl_macosx$(EXTENSION)$(BITS) MINGL_DIR=mingl_macosx$(EXTENSION)$(BITS) GL_CFLAGS=$(GLCFLAGS) -D__MACOSX__ -L/sw/lib -I/sw/include -L/opt/local/lib -I/opt/local/include -I$(LIBS_DIR) ifeq ($(FTE_TARGET),macosx_x86) GL_CFLAGS=$(GLCFLAGS) -D__MACOSX__ -L/sw/lib -I/sw/include -L/opt/local/lib -I/opt/local/include -I$(LIBS_DIR) endif GL_LDFLAGS=-framework AGL -framework OpenGL -framework Cocoa -framework AudioUnit GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidcocoa.mo gl_vidmacos.o sys_linux.o cd_null.o snd_macos.o sys_linux_threads.o GL_EXE_NAME=../$(EXE_NAME)-macosx-gl$(EXTENSION)$(BITS) GLCL_EXE_NAME=../$(EXE_NAME)cl-macosx-gl$(EXTENSION)$(BITS) M_EXE_NAME=../$(EXE_NAME)-macosx$(EXTENSION)$(BITS) MCL_EXE_NAME=../$(EXE_NAME)-macosx-cl$(EXTENSION)$(BITS) MINGL_EXE_NAME=../$(EXE_NAME)-macosx-mingl$(EXTENSION)$(BITS) MINGL_DIR=mingl_macosx$(EXTENSION)$(BITS) SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(SERVERONLY_OBJS) SV_EXE_NAME=../$(EXE_NAME)-macosx-sv$(EXTENSION)$(BITS) SV_CFLAGS=$(SERVER_ONLY_CFLAGS) SV_LDFLAGS=-lz #seems, macosx has a more limited version of strip STRIPFLAGS= endif ifeq ($(FTE_TARGET),morphos) #-Wno-pointer-sign unrecognised WARNINGFLAGS=-Wall CFLAGS+=-D__MORPHOS_SHAREDLIBS SV_DIR=sv_morphos SV_LDFLAGS=-ldl -lz GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidmorphos.o in_morphos.o snd_morphos.o cd_null.o sys_morphos.o GL_EXE_NAME=../$(EXE_NAME)-morphos-gl GLCL_EXE_NAME=../$(EXE_NAME)-morphos-glcl GL_LDFLAGS=$(GLLDFLAGS) -ldl $(IMAGELDFLAGS) -lz GL_CFLAGS=$(GLCFLAGS) -noixemul -I./ GLB_DIR=gl_morphos GLCL_DIR=glcl_morphos MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidmorphos.o vid_morphos.o in_morphos.o snd_morphos.o cd_null.o sys_morphos.o M_EXE_NAME=../$(EXE_NAME)-morphos MCL_EXE_NAME=../$(EXE_NAME)-morphos-cl M_LDFLAGS=$(GLLDFLAGS) M_CFLAGS=$(GLCFLAGS) MB_DIR=m_morphos MCL_DIR=mcl_morphos MINGL_EXE_NAME=../$(EXE_NAME)-morphos-mingl MINGL_DIR=mingl_morphos SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(SERVERONLY_OBJS) SV_EXE_NAME=../$(EXE_NAME)-morphos-sv$(BITS) SV_CFLAGS=$(SERVER_ONLY_CFLAGS) endif ifeq ($(FTE_TARGET),dos) EXEPOSTFIX=.exe SV_DIR=sv_dos GLB_DIR=gl_dos MB_DIR=m_dos MCL_DIR=mcl_dos MINGL_DIR=mingl_dos VKB_DIR=vk_dos VKCL_DIR=vkcl_dos IMAGELDFLAGS= SOFTWARE_OBJS=sw_rast.o sw_backend.o sw_image.o M_LDFLAGS= M_CFLAGS=-DSWQUAKE -DNO_ZLIB MCL_OBJS=$(SOFTWARE_OBJS) $(D3DGL_OBJS) sw_viddos.o cd_null.o sys_dos.o snd_sblaster.o M_EXE_NAME=../$(EXE_NAME)$(EXEPOSTFIX) SV_EXE_NAME=../$(EXE_NAME)sv$(BITS)$(EXEPOSTFIX) VK_EXE_NAME=../$(EXE_NAME)-vk$(BITS)$(EXEPOSTFIX) VKCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) cd_null.o sys_dos.o snd_sblaster.o endif ifeq ($(FTE_TARGET),cyg) SV_DIR=sv_cygwin SV_LDFLAGS=-lz SV_CFLAGS=$(SERVER_ONLY_CFLAGS) EXEPOSTFIX=.exe GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) gl_vidlinuxglx.o snd_linux.o cd_null.o sys_linux.o sys_linux_threads.o GL_EXE_NAME=../$(EXE_NAME)-cyg-gl$(EXEPOSTFIX) GLCL_EXE_NAME=../$(EXE_NAME)-cyg-glcl$(EXEPOSTFIX) GL_LDFLAGS=$(GLLDFLAGS) $(XLDFLAGS) -lz -lltdl GL_CFLAGS=$(GLCFLAGS) -I/usr/X11R6/include $(CLIENTLIBFLAGS) -DUSE_LIBTOOL -DAUDIO_OSS GLB_DIR=gl_cygwin GLCL_DIR=glcl_cygwin MCL_OBJS=$(D3DGL_OBJS) $(GLQUAKE_OBJS) $(SOFTWARE_OBJS) gl_vidlinuxglx.o snd_linux.o cd_null.o sys_linux.o sys_linux_threads.o M_EXE_NAME=../$(EXE_NAME)-cyg$(EXEPOSTFIX) MCL_EXE_NAME=../$(EXE_NAME)-cyg-cl$(EXEPOSTFIX) M_LDFLAGS=$(GLLDFLAGS) $(XLDFLAGS) -lz -lltdl M_CFLAGS=$(GLCFLAGS) $(CLIENTLIBFLAGS) -DUSE_LIBTOOL -DAUDIO_OSS MB_DIR=m_cygwin MCL_DIR=mcl_cygwin MINGL_EXE_NAME=../$(EXE_NAME)-cyg-mingl$(EXEPOSTFIX) MINGL_DIR=mingl_cygwin endif ifeq ($(FTE_TARGET),droid) BASELDFLAGS=-lz #erk! FIXME! CLIENTLDDEPS= SERVERLDDEPS= SYS_DROID_O=sys_droid.o sys_linux_threads.o GL_DROID_O=gl_viddroid.o $(SYS_DROID_O) SV_CFLAGS=$(SERVER_ONLY_CFLAGS) $(W32_CFLAGS) SV_LDFLAGS= SV_DIR=sv_droid-$(DROID_ARCH) SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) $(SYS_DROID_O) SV_EXE_NAME=libftedroid.so GL_CFLAGS=$(GLCFLAGS) GL_LDFLAGS=$(GLLDFLAGS) -landroid GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) $(GL_DROID_O) cd_null.o snd_droid.o GLB_DIR=gl_droid-$(DROID_ARCH) GL_EXE_NAME=libftedroid.so M_CFLAGS=$(VKCFLAGS) $(GLCFLAGS) -DMULTITHREAD M_LDFLAGS=$(GLLDFLAGS) -landroid -lEGL -lOpenSLES MCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) $(GL_DROID_O) cd_null.o snd_opensl.o #snd_droid.o MB_DIR=m_droid-$(DROID_ARCH) M_EXE_NAME=libftedroid.so endif ifeq ($(FTE_TARGET),web) COMMON_OBJS+=sys_web.o fs_web.o WEB_PREJS ?= --pre-js web/prejs.js # WEB_MEMORY?=402653184 #384mb # ASMJS_MEMORY?=16777216 #16mb # ASMJS_MEMORY?=33554432 #32mb ASMJS_MEMORY?=268435456 #256mb # ASMJS_MEMORY?=536870912 #512mb # ASMJS_MEMORY?=1073741824 #1025mb # ASMJS_MEMORY?=2147483648 #2048mb WEB_MEMORY?=$(ASMJS_MEMORY) JSLIBS=--js-library web/ftejslib.js EMCC_CFLAGS= -DFTE_TARGET_WEB EMCC_LDFLAGS=$(EMCC_CFLAGS) $(JSLIBS) $(WEB_PREJS) # EMCC_CFLAGS+= -s BINARYEN_TRAP_MODE='clamp' #fix bigfloat->int rounding crashes EMCC_LDFLAGS+= -s LEGACY_GL_EMULATION=0 #simplify the opengl wrappers. EMCC_LDFLAGS+= -s NO_FILESYSTEM=1 #we have our own. EMCC_LDFLAGS+= -s FILESYSTEM=0 #we have our own. EMCC_LDFLAGS+= -s ALLOW_MEMORY_GROWTH=1 #reduce crashes... EMCC_LDFLAGS+= -s MAX_WEBGL_VERSION=2 #make use of what we can. EMCC_LDFLAGS+= -s TOTAL_STACK=5MB #big! EMCC_LDFLAGS+=-s ERROR_ON_UNDEFINED_SYMBOLS=1 #fairly obvious. no runtime errors please. RELEASE_CFLAGS=-DOMIT_QCC -DGL_STATIC $(EMCC_CFLAGS) DEBUG_CFLAGS=-gsource-map -DOMIT_QCC -DGL_STATIC $(EMCC_CFLAGS) RELEASE_LDFLAGS=-O3 -s TOTAL_MEMORY=$(ASMJS_MEMORY) $(EMCC_LDFLAGS) # RELEASE_LDFLAGS=-O1 -s TOTAL_MEMORY=$(WEB_MEMORY) $(EMCC_LDFLAGS) DEBUG_LDFLAGS=-O0 -gsource-map -s TOTAL_MEMORY=$(WEB_MEMORY) $(EMCC_LDFLAGS) -s SAFE_HEAP=1 -s ALIASING_FUNCTION_POINTERS=0 -s ASSERTIONS=2 CC=emcc CXX=em++ AR=emar BASELDFLAGS=-lz PRECOMPHEADERS= #mostly we inherit the sdl defaults. because we can, however emscripten does not support sdl cd code. GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(BOTLIB_OBJS) $(GLQUAKE_OBJS) gl_vidweb.o cd_null.o SDL_INCLUDES= SV_DIR=sv_web SV_LDFLAGS= #SV_OBJS=$(COMMON_OBJS) $(SERVER_OBJS) $(PROGS_OBJS) SV_EXE_NAME=../libftesv.js SV_CFLAGS=$(SERVER_ONLY_CFLAGS) #SV_LDFLAGS= STRIP=@echo SKIP: strip #GLCL_OBJS=$(GL_OBJS) $(D3DGL_OBJS) $(GLQUAKE_OBJS) cd_null.o #GL_LDFLAGS=$(GLLDFLAGS) GLB_DIR=gl_web GLCL_DIR=glcl_web GL_EXE_NAME=../ftewebgl.js GLCL_EXE_NAME=../ftewebglcl.js GL_LDFLAGS=$(GLLDFLAGS) $(IMAGELDFLAGS) GL_CFLAGS=$(GLCFLAGS) $(CLIENTLIBFLAGS) IMAGELDFLAGS= SERVERLDDEPS= #generate deps properly #DEPCC= #DEPCXX= endif SV_DIR?=sv_sdl DEPCC?=$(CC) DEPCXX?=$(CXX) ARCH:=$(ARCH) ifeq ($(findstring msvc,$(FTE_TARGET)),msvc) BASELDFLAGS:=/libpath:"$(ARCHLIBS)" $(BASELDFLAGS) else BASELDFLAGS:=-L$(ARCHLIBS) $(BASELDFLAGS) endif -include Makefile_private all: rel rel: sv-rel m-rel qcc-rel dbg: sv-dbg m-dbg qcc-dbg relcl: glcl-rel mcl-rel profile: sv-profile gl-profile mingl-profile releases: #this is for releasing things from a linux box #just go through compiling absolutly everything -$(MAKE) FTE_TARGET=linux32 rel -$(MAKE) FTE_TARGET=linux64 rel -$(MAKE) FTE_TARGET=win32 rel -$(MAKE) FTE_TARGET=win64 rel -$(MAKE) FTE_TARGET=win32_SDL rel -$(MAKE) FTE_TARGET=win64_SDL rel -$(MAKE) FTE_TARGET=morphos rel -$(MAKE) FTE_TARGET=macosx rel # -$(MAKE) FTE_TARGET=linux32 relcl # -$(MAKE) FTE_TARGET=linux64 relcl # -$(MAKE) FTE_TARGET=win32 relcl -$(MAKE) droid-rel -$(MAKE) web-rel autoconfig: clean /bin/bash makeconfig.sh y config: clean /bin/bash makeconfig.sh ifneq ($(OUT_DIR),) -include $(OUT_DIR)/*.o.d endif DO_WINDRES?=$(DO_ECHO) $(WINDRES) $(BRANDFLAGS) -I$(CLIENT_DIR) -O coff $< $@ # This is for linking the FTE icon to the MinGW target $(OUT_DIR)/resources.o : winquake.rc $(DO_WINDRES) $(OUT_DIR)/fteqcc.o : fteqcc.rc $(DO_WINDRES) #$(OUT_DIR)/%.d: %.c # @set -e; rm -f $@; \ # $(CC) -MM $(ALL_CFLAGS) $< > $@.$$$$; \ # sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ # rm -f $@.$$$$ $(OUT_DIR)/%.o $(OUT_DIR)/%.d : %.c ifneq ($(DEPCC),) @-set -e; rm -f $@.d; \ $(DEPCC) -MM $(ALL_CFLAGS) $< > $@.d.$$$$; \ sed 's,\($*\)\.o[ :]*,$@ $@.d : ,g' < $@.d.$$$$ > $@.d; \ sed -e 's/.*://' -e 's/\\$$//' < $@.d.$$$$ | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $@.d; \ rm -f $@.d.$$$$ endif $(DO_CC) -I$(OUT_DIR) $(OUT_DIR)/%.o $(OUT_DIR)/%.d : %.cpp ifneq ($(DEPCXX),) @-set -e; rm -f $@.d; \ $(DEPCXX) -MM $(ALL_CXXFLAGS) $< > $@.d.$$$$; \ sed 's,\($*\)\.o[ :]*,$@ $@.d : ,g' < $@.d.$$$$ > $@.d; \ sed -e 's/.*://' -e 's/\\$$//' < $@.d.$$$$ | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $@.d; \ rm -f $@.d.$$$$ endif $(DO_CXX) -I$(OUT_DIR) $(OUT_DIR)/%.o $(OUT_DIR)/%.d : %.cxx ifneq ($(DEPCXX),) @-set -e; rm -f $@.d; \ $(DEPCXX) -MM $(ALL_CXXFLAGS) $< > $@.d.$$$$; \ sed 's,\($*\)\.o[ :]*,$@ $@.d : ,g' < $@.d.$$$$ > $@.d; \ sed -e 's/.*://' -e 's/\\$$//' < $@.d.$$$$ | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $@.d; \ rm -f $@.d.$$$$ endif $(DO_CXX) -I$(OUT_DIR) $(OUT_DIR)/%.oo $(OUT_DIR)/%.d : %.c ifneq ($(DEPCC),) @-set -e; rm -f $@.d; \ $(DEPCC) -MM $(ALL_CFLAGS) $< > $@.d.$$$$; \ sed 's,\($*\)\.oo[ :]*,$@ $@.d : ,g' < $@.d.$$$$ > $@.d; \ sed -e 's/.*://' -e 's/\\$$//' < $@.d.$$$$ | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $@.d; \ rm -f $@.d.$$$$ endif $(DO_CC) -I$(OUT_DIR) $(OUT_DIR)/%.mo $(OUT_DIR)/%.d : %.m @-set -e; rm -f $@.d; \ $(DEPCC) -MM $(ALL_CFLAGS) $< > $@.d.$$$$; \ sed 's,\($*\)\.mo[ :]*,$@ $@.d : ,g' < $@.d.$$$$ > $@.d; \ sed -e 's/.*://' -e 's/\\$$//' < $@.d.$$$$ | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $@.d; \ rm -f $@.d.$$$$ $(DO_CC) -I$(OUT_DIR) #enables use of precompiled headers in gcc 3.4 onwards. $(OUT_DIR)/quakedef.h.gch : quakedef.h $(CC) -x c-header $(ALL_CFLAGS) -o $@ -c $< PRECOMPHEADERS ?= $(OUT_DIR)/quakedef.h.gch ifneq ($(OUT_DIR),) ALL_CFLAGS+=-I$(OUT_DIR) endif #addprefix is to add the ./release/server/ part of the object name #foreach is needed as the OBJS is a list of variable names containing object lists. #which is needed as windows sucks too much for the chaining to carry a full list. #god knows how gcc loads the list properly. #or at least I hope he does. It makes no sence to mortals. LDCC ?=$(CC) DO_LD ?= +$(DO_ECHO) $(LDCC) -o $@ $(LTO_LD) $(WCFLAGS) $(BRANDFLAGS) $(CFLAGS) $(OUT_DIR)/$(EXE_NAME): $(PRECOMPHEADERS) $(foreach fn, $(CUSTOMOBJS) $(foreach ol, $(OBJS), $($(ol))),$(if $(findstring ltox,$(fn)),,$(OUT_DIR)/$(fn))) $(DO_LD) $(foreach fn, $(CUSTOMOBJS) $(foreach ol, $(OBJS) $(LTO_END), $($(ol))),$(if $(findstring ltox,$(fn)),$(subst ltox,-x ,$(fn)),$(NATIVE_OUT_DIR)/$(fn)) ) $(LDFLAGS) $(OUT_DIR)/$(EXE_NAME).db: $(PRECOMPHEADERS) $(foreach fn, $(CUSTOMOBJS) $(foreach ol, $(OBJS), $($(ol))),$(if $(findstring ltox,$(fn)),,$(OUT_DIR)/$(fn))) $(DO_LD) $(foreach fn, $(CUSTOMOBJS) $(foreach ol, $(OBJS) $(LTO_END), $($(ol))),$(if $(findstring ltox,$(fn)),$(subst ltox,-x ,$(fn)),$(NATIVE_OUT_DIR)/$(fn)) ) $(LDFLAGS) ifneq (,$(NEEDMAKELIBS)) _out-dbg: @echo Dependancies missing. Use makelibs before compiling targets @false _out-rel: @echo Dependancies missing. Use makelibs before compiling targets @false _out-profile: @echo Dependancies missing. Use makelibs before compiling targets @false else ifeq (,$(findstring SKIP,$(STRIP))) #link to a .db file #then strip its debug data to the non-.db release binary _out-rel: $(ARCH_PREDEP) @$(MAKE) $(OUT_DIR)/$(EXE_NAME).db EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(WCFLAGS) $(RELEASE_CFLAGS)" LDFLAGS="$(BASELDFLAGS) $(LDFLAGS) $(RELEASE_LDFLAGS)" OBJS="$(OBJS)" @$(STRIP) $(STRIPFLAGS) $(OUT_DIR)/$(EXE_NAME).db -o $(OUT_DIR)/$(EXE_NAME) else #STRIP macro won't work, don't do the .db thing and don't expect strip -o to work. _out-rel: $(ARCH_PREDEP) @$(MAKE) $(OUT_DIR)/$(EXE_NAME) EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(WCFLAGS) $(RELEASE_CFLAGS)" LDFLAGS="$(BASELDFLAGS) $(LDFLAGS) $(RELEASE_LDFLAGS)" OBJS="$(OBJS)" @echo not stripping $(OUT_DIR)/$(EXE_NAME) endif _out-dbg: $(ARCH_PREDEP) @$(MAKE) $(OUT_DIR)/$(EXE_NAME) EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(WCFLAGS) $(DEBUG_CFLAGS)" LDFLAGS="$(BASELDFLAGS) $(LDFLAGS) $(DEBUG_LDFLAGS)" OBJS="$(OBJS)" _out-profile: $(ARCH_PREDEP) @$(MAKE) $(OUT_DIR)/$(EXE_NAME) EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(WCFLAGS) $(PROFILE_CFLAGS)" LDFLAGS="$(BASELDFLAGS) $(LDFLAGS) $(PROFILE_LDFLAGS)" OBJS="$(OBJS)" endif _cl-rel: reldir @$(MAKE) _out-rel EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(CLIENT_ONLY_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS)" SOBJS="$(SOBJS)" OBJS="SOBJS COMMON_OBJS CLIENT_OBJS PROGS_OBJS" _cl-dbg: debugdir @$(MAKE) _out-dbg EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(CLIENT_ONLY_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS)" SOBJS="$(SOBJS)" OBJS="SOBJS COMMON_OBJS CLIENT_OBJS PROGS_OBJS" _cl-profile: reldir @$(MAKE) _out-profile EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(CLIENT_ONLY_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS)" SOBJS="$(SOBJS)" OBJS="SOBJS COMMON_OBJS CLIENT_OBJS PROGS_OBJS" _clsv-rel: reldir $(DO_ECHO) $(MAKE) _out-rel EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(JOINT_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS)" SOBJS="$(SOBJS)" OBJS="SOBJS COMMON_OBJS CLIENT_OBJS PROGS_OBJS SERVER_OBJS" _clsv-dbg: debugdir @$(MAKE) _out-dbg EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(JOINT_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS)" SOBJS="$(SOBJS)" OBJS="SOBJS COMMON_OBJS CLIENT_OBJS PROGS_OBJS SERVER_OBJS" _clsv-profile: reldir @$(MAKE) _out-profile EXE_NAME="$(EXE_NAME)" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(JOINT_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS)" SOBJS="$(SOBJS)" OBJS="SOBJS COMMON_OBJS CLIENT_OBJS PROGS_OBJS SERVER_OBJS" sv-tmp: reldir debugdir @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(SV_EXE_NAME)" WCFLAGS="$(SV_CFLAGS)" LDFLAGS="$(ARCH_LDFLAGS) $(SV_LDFLAGS) $(LDFLAGS) $(SERVERLDDEPS)" OBJS="SV_OBJS" sv-rel: @$(MAKE) sv-tmp TYPE=_out-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(SV_DIR)" sv-dbg: @$(MAKE) sv-tmp TYPE=_out-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(SV_DIR)" sv-profile: @$(MAKE) sv-tmp TYPE=_out-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(SV_DIR)" ifeq (,$(D3D_EXE_NAME)) d3dcl-tmp: @echo "D3D builds are not supported for this platform." else d3dcl-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(D3DCL_EXE_NAME)" WCFLAGS="$(D3D_CFLAGS)" LDFLAGS="$(D3D_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(D3DCL_OBJS)" endif ifeq (,$(D3D_EXE_NAME)) d3d-tmp: @echo "D3D builds are not supported for this platform." else d3d-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(D3D_EXE_NAME)" WCFLAGS="$(D3D_CFLAGS)" LDFLAGS="$(D3D_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(D3DCL_OBJS)" endif d3dcl-rel: @$(MAKE) d3dcl-tmp TYPE=_cl-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(D3DCL_DIR)" d3dcl-dbg: @$(MAKE) d3dcl-tmp TYPE=_cl-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(D3DCL_DIR)" d3dcl-profile: @$(MAKE) d3dcl-tmp TYPE=_cl-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(D3DCL_DIR)" d3d-rel: @$(MAKE) d3d-tmp TYPE=_clsv-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(D3DB_DIR)" d3d-dbg: @$(MAKE) d3d-tmp TYPE=_clsv-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(D3DB_DIR)" d3d-profile: @$(MAKE) d3d-tmp TYPE=_clsv-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(D3DB_DIR)" vkcl-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(VKCL_EXE_NAME)" WCFLAGS="$(VK_CFLAGS)" LDFLAGS="$(VK_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(VKCL_OBJS)" vk-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(VK_EXE_NAME)" WCFLAGS="$(VK_CFLAGS)" LDFLAGS="$(VK_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(VKCL_OBJS)" vkcl-rel: @$(MAKE) vkcl-tmp TYPE=_cl-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(VKCL_DIR)" vkcl-dbg: @$(MAKE) vkcl-tmp TYPE=_cl-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(VKCL_DIR)" vkcl-profile: @$(MAKE) vkcl-tmp TYPE=_cl-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(VKCL_DIR)" vk-rel: @$(MAKE) vk-tmp TYPE=_clsv-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(VKB_DIR)" vk-dbg: @$(MAKE) vk-tmp TYPE=_clsv-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(VKB_DIR)" vk-profile: @$(MAKE) vk-tmp TYPE=_clsv-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(VKB_DIR)" glcl-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(GLCL_EXE_NAME)" WCFLAGS="$(GL_CFLAGS)" LDFLAGS="$(GL_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(GLCL_OBJS)" gl-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(GL_EXE_NAME)" WCFLAGS="$(GL_CFLAGS)" LDFLAGS="$(GL_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(GLCL_OBJS)" glcl-rel: @$(MAKE) glcl-tmp TYPE=_cl-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(GLCL_DIR)" glcl-dbg: @$(MAKE) glcl-tmp TYPE=_cl-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(GLCL_DIR)" glcl-profile: @$(MAKE) glcl-tmp TYPE=_cl-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(GLCL_DIR)" gl-rel: @$(MAKE) gl-tmp TYPE=_clsv-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(GLB_DIR)" gl-dbg: @$(MAKE) gl-tmp TYPE=_clsv-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(GLB_DIR)" gl-profile: @$(MAKE) gl-tmp TYPE=_clsv-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(GLB_DIR)" mingl-tmp: reldir @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(MINGL_EXE_NAME)" WCFLAGS="$(GL_CFLAGS) -DMINIMAL" LDFLAGS="$(GL_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(GLCL_OBJS)" mingl-rel: @$(MAKE) mingl-tmp TYPE=_cl-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(MINGL_DIR)" mingl-dbg: @$(MAKE) mingl-tmp TYPE=_cl-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(MINGL_DIR)" mingl-profile: @$(MAKE) mingl-tmp TYPE=_cl-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(MINGL_DIR)" mcl-tmp: @$(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(MCL_EXE_NAME)" WCFLAGS="$(M_CFLAGS)" LDFLAGS="$(M_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(MCL_OBJS)" m-tmp: $(DO_ECHO) $(MAKE) $(TYPE) OUT_DIR="$(OUT_DIR)" EXE_NAME="$(M_EXE_NAME)" WCFLAGS="$(M_CFLAGS)" LDFLAGS="$(M_LDFLAGS) $(LDFLAGS) $(CLIENTLDDEPS)" SOBJS="$(MCL_OBJS)" mcl-rel: @$(MAKE) mcl-tmp TYPE=_cl-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(MCL_DIR)" mcl-dbg: @$(MAKE) mcl-tmp TYPE=_cl-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(MCL_DIR)" mcl-profile: @$(MAKE) mcl-tmp TYPE=_cl-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(MCL_DIR)" m-rel: $(DO_ECHO) $(MAKE) m-tmp TYPE=_clsv-rel OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(MB_DIR)" m-dbg: @$(MAKE) m-tmp TYPE=_clsv-dbg OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(MB_DIR)" m-profile: @$(MAKE) m-tmp TYPE=_clsv-profile OUT_DIR="$(PROFILE_DIR)/$(NCDIRPREFIX)$(MB_DIR)" .PHONY: m-tmp mcl-tmp mingl-tmp glcl-tmp gl-tmp sv-tmp _clsv-dbg _clsv-rel _cl-dbg _cl-rel _out-rel _out-dbg reldir debugdir makelibs wel-rel web-dbg httpserver iqmtool imgtool _qcc-tmp: $(REQDIR) @$(MAKE) $(TYPE) EXE_NAME="$(EXE_NAME)$(EXEPOSTFIX)" PRECOMPHEADERS="" OUT_DIR="$(OUT_DIR)" WCFLAGS="$(QCC_CFLAGS) $(WCFLAGS)" LDFLAGS="$(LDFLAGS) $(QCC_LDFLAGS)" OBJS="QCC_OBJS SOBJS" qcc-rel: @$(MAKE) _qcc-tmp TYPE=_out-rel REQDIR=reldir EXE_NAME="../fteqcc$(BITS)" OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(QCC_DIR)" SOBJS="qcctui.o packager.o decomp.o $(if $(findstring win,$(FTE_TARGET)),fteqcc.o)" qccgui-rel: @$(MAKE) _qcc-tmp TYPE=_out-rel REQDIR=reldir EXE_NAME="../fteqccgui$(BITS)" LTO= OUT_DIR="$(RELEASE_DIR)/$(NCDIRPREFIX)$(QCC_DIR)gui" SOBJS="qccgui.o qccguistuff.o packager.o decomp.o fteqcc.o" LDFLAGS="$(LDFLAGS) -lole32 -lcomdlg32 -lcomctl32 -lshlwapi -mwindows" qcc-dbg: @$(MAKE) _qcc-tmp TYPE=_out-dbg REQDIR=debugdir EXE_NAME="../fteqcc$(BITS)" OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(QCC_DIR)" SOBJS="qcctui.o packager.o decomp.o $(if $(findstring win,$(FTE_TARGET)),fteqcc.o)" qccgui-dbg: @$(MAKE) _qcc-tmp TYPE=_out-dbg REQDIR=debugdir EXE_NAME="../fteqccgui$(BITS)" LTO= OUT_DIR="$(DEBUG_DIR)/$(NCDIRPREFIX)$(QCC_DIR)gui" SOBJS="qccgui.o qccguistuff.o packager.o decomp.o fteqcc.o" LDFLAGS="$(LDFLAGS) -lole32 -lcomdlg32 -lcomctl32 -lshlwapi -mwindows" #scintilla is messy as fuck when building statically. but at least we can strip out the lexers we don't use this way. #note that this is only used in the 'qccgui-scintilla' target. SCINTILLA_FILES= AutoComplete.o CallTip.o CaseConvert.o CaseFolder.o CellBuffer.o CharacterCategory.o CharacterSet.o CharClassify.o ContractionState.o Decoration.o Document.o EditModel.o Editor.o EditView.o KeyMap.o Indicator.o LineMarker.o MarginView.o PerLine.o PlatWin.o PositionCache.o PropSetSimple.o RESearch.o RunStyles.o Selection.o Style.o UniConversion.o ViewStyle.o XPM.o ScintillaWin.o HanjaDic.o ScintillaBase.o Accessor.o Catalogue.o ExternalLexer.o LexerBase.o LexerModule.o LexerSimple.o StyleContext.o WordList.o LexCPP.o SCINTILLA_ROOT=$(BASE_DIR)/scintilla$(SCINTILLAVER)/scintilla SCINTILLA_DIRS=$(SCINTILLA_ROOT)/lexers:$(SCINTILLA_ROOT)/lexlib:$(SCINTILLA_ROOT)/src:$(SCINTILLA_ROOT)/win32 SCINTILLA_INC=-I$(SCINTILLA_ROOT)/include -I$(SCINTILLA_ROOT)/lexlib -I$(SCINTILLA_ROOT)/win32 -I$(SCINTILLA_ROOT)/src $(RELEASE_DIR)/scintilla$(BITS).a: $(foreach f,$(SCINTILLA_FILES),$(OUT_DIR)/$(f)) @$(AR) -r $@ $? @$(AR) -s $@ scintilla$(BITS)_static: @test -f scintilla$(SCINTILLAVER).tar.gz || wget http://prdownloads.sourceforge.net/scintilla/scintilla$(SCINTILLAVER).tgz?download -O scintilla$(SCINTILLAVER).tar.gz @-test -f $(SCINTILLA_ROOT) || (mkdir $(BASE_DIR)/scintilla$(SCINTILLAVER) && cd $(BASE_DIR)/scintilla$(SCINTILLAVER) && tar -xvzf ../scintilla$(SCINTILLAVER).tar.gz && cd scintilla && mv lexers/LexCPP.cxx . && rm lexers/Lex*.cxx && mv LexCPP.cxx lexers/ && cd scripts && python LexGen.py) @$(MAKE) reldir OUT_DIR=$(RELEASE_DIR)/$(QCC_DIR)scin @$(MAKE) $(RELEASE_DIR)/scintilla$(BITS).a VPATH="$(SCINTILLA_DIRS)" CFLAGS="$(SCINTILLA_INC) -DDISABLE_D2D -DSTATIC_BUILD -DSCI_LEXER -std=c++11" OUT_DIR=$(RELEASE_DIR)/$(QCC_DIR)scin WCFLAGS="$(WCFLAGS) -Os" WARNINGFLAGS= qccgui-scintilla: scintilla$(BITS)_static #LTO bugs out on WinMain for some reason, so try to disable it for this target. @LTO= $(MAKE) _qcc-tmp TYPE=_out-rel REQDIR=reldir EXE_NAME="../fteqccgui$(BITS)" OUT_DIR="$(RELEASE_DIR)/$(QCC_DIR)scin" SOBJS="qccgui.o qccguistuff.o packager.o decomp.o fteqcc.o" WCFLAGS="$(WCFLAGS) -DSCISTATIC" LDFLAGS="$(LDFLAGS) $(RELEASE_DIR)/scintilla$(BITS).a -static -luuid -lole32 -limm32 -lstdc++ -loleaut32 -lcomdlg32 -lcomctl32 -lshlwapi -mwindows" ifdef windir outdir: ifneq "$(OUT_DIR)" "" @-mkdir -p "$(subst /,\, $(OUT_DIR))" endif debugdir: outdir @-mkdir -p "$(subst /,\, $(RELEASE_DIR))" reldir: outdir @-mkdir -p "$(subst /,\, $(DEBUG_DIR))" else outdir: ifneq "$(OUT_DIR)" "" @-mkdir -p "$(OUT_DIR)" endif debugdir: outdir reldir: outdir @-mkdir -p "$(RELEASE_DIR)" debugdir: outdir @-mkdir -p "$(DEBUG_DIR)" endif plugins-dbg: @-mkdir -p $(DEBUG_DIR) @if test -e ../plugins/Makefile; \ then $(MAKE) native -C ../plugins OUT_DIR="$(DEBUG_DIR)" CC="$(CC) $(W32_CFLAGS) $(DEBUG_CFLAGS)" CXX="$(CXX) $(W32_CFLAGS) $(subst -Wno-pointer-sign,,$(DEBUG_CFLAGS))" PKGCONFIG="$(PKGCONFIG)" ARCH="$(ARCH)" BASE_CFLAGS="$(BASE_CFLAGS) $(BRANDFLAGS)" BASE_CXXFLAGS="$(subst -Wno-pointer-sign,,$(BASE_CFLAGS)) $(BRANDFLAGS)" FTE_TARGET="$(FTE_TARGET)"; \ else echo no plugins directory installed; \ fi plugins: plugins-rel: @-mkdir -p $(RELEASE_DIR) @if test -e ../plugins/Makefile; \ then $(MAKE) native -C ../plugins OUT_DIR="$(RELEASE_DIR)" CC="$(CC)" CXX="$(CXX)" PKGCONFIG="$(PKGCONFIG)" ARCH="$(ARCH)" BASE_CFLAGS="$(BASE_CFLAGS) $(BRANDFLAGS)" BASE_CXXFLAGS="$(subst -Wno-pointer-sign,,$(BASE_CFLAGS)) $(BRANDFLAGS)" FTE_TARGET="$(FTE_TARGET)"; \ else echo no plugins directory installed; \ fi plugins-rel: help: @-echo "Specfic targets:" @-echo "clean - removes all output (use make dirs afterwards)" @-echo "all - make all the targets possible" @-echo "rel - make the releases for the default system" @-echo "dbg - make the debug builds for the default system" @-echo "profile - make all the releases with profiling support for the default system" @-echo "" @-echo "Normal targets:" @-echo "(each of these targets must have the postfix -rel or -dbg)" @-echo "'sv-???' (Dedicated Server)" @-echo "'gl-???' (OpenGL rendering + Built-in Server)" @-echo "'m-???' (Merged client, OpenGL & D3D rendering + Dedicated server)" @-echo "'mingl-???' (Minimal featured OpenGL render)" @-echo "'d3d-???' (for windows builds)" @-echo "'mcl-???' (currently broken)" @-echo "'glcl-???' (currently broken)" @-echo "'droid-???' (cross compiles Android package)" @-echo "'web-???' (compiles javascript/emscripten page)" @-echo "" @-echo "Cross targets can be specified with FTE_TARGET=blah" @-echo "linux32, linux64 specify specific x86 archs" @-echo "SDL - Attempt to use sdl for the current target" @-echo "win32 - Mingw compile for win32" @-echo "vc - Attempts to use msvc8+ to compile. Note: uses profile guided optimisations. You must build+run the relevent profile target before a release target will compile properly. Debug doesn't care." @-echo "android targets explicitly cross compile, and should generally not be given an FTE_TARGET." clean: -rm -f -r $(RELEASE_DIR) -rm -f -r $(DEBUG_DIR) -rm -f -r $(PROFILE_DIR) -rm -f -r droid/bin -rm -f -r droid/gen -rm -f -r droid/libs -rm -f droid/default.properties -rm -f droid/local.properties -rm -f droid/proguard.cfg -rm -f droid/build.xml distclean: clean -rm -f droid/ftekeystore ################################################# #webgl helpers ifeq ($(FTE_TARGET),web) $(OUT_DIR)/$(EXE_NAME): web/ftejslib.js web/prejs.js endif ifneq ($(shell which emcc 2> /dev/null),) EMCC?=emcc else #EMCC?=/opt/emsdk_portable/emscripten/master/emcc EMCC?=emcc.bat --em-config $(shell cygpath -m $(USERPROFILE))/.emscripten endif ifeq ($(EMSDK),) #just adds some extra paths (WINDOWS HOST ONLY) #assumes you installed the emscripten 1.22.0 sdk to EMSCRIPTENROOT #if you have a different version installed, you will need to fix up the paths yourself (or just use fte_target explicitly yourself). EMSCRIPTENROOT?=C:/Games/tools/Emscripten #EMSCRIPTENPATH=$(realpath $(EMSCRIPTENROOT)):$(realpath $(EMSCRIPTENROOT)/clang/e1.22.0_64bit):$(realpath $(EMSCRIPTENROOT)/node/0.10.17_64bit):$(realpath $(EMSCRIPTENROOT)/python/2.7.5.3_64bit):$(realpath $(EMSCRIPTENROOT)/emscripten/1.22.0):$(PATH) EMSCRIPTENPATH=$(realpath $(EMSCRIPTENROOT)):$(realpath $(EMSCRIPTENROOT)/clang/e1.35.0_64bit):$(realpath $(EMSCRIPTENROOT)/node/4.1.1_64bit/bin):$(realpath $(EMSCRIPTENROOT)/python/2.7.5.3_64bit):$(realpath $(EMSCRIPTENROOT)/emscripten/1.35.0):$(PATH) else EMSCRIPTENPATH=$(PATH) endif web-rel: @PATH="$(EMSCRIPTENPATH)" $(MAKE) makelibs FTE_TARGET=web CC="$(EMCC)" && PATH="$(EMSCRIPTENPATH)" $(MAKE) gl-rel FTE_TARGET=web CC="$(EMCC)" @cp $(BASE_DIR)/web/fteshell.html $(RELEASE_DIR)/ftewebgl.html @gzip -kf $(RELEASE_DIR)/ftewebgl.html @gzip -kf $(RELEASE_DIR)/ftewebgl.js @gzip -kf $(RELEASE_DIR)/ftewebgl.wasm webcl-rel: @PATH="$(EMSCRIPTENPATH)" $(MAKE) makelibs FTE_TARGET=web CC="$(EMCC)" && PATH="$(EMSCRIPTENPATH)" $(MAKE) glcl-rel FTE_TARGET=web CC="$(EMCC)" @cp $(BASE_DIR)/web/fteshell.html $(RELEASE_DIR)/ftewebglcl.html @sed -i 's/ftewebgl.js/ftewebglcl.js/g' release/ftewebglcl.html #swap out the .js filename for the client-only one. @gzip -kf $(RELEASE_DIR)/ftewebglcl.html @gzip -kf $(RELEASE_DIR)/ftewebglcl.js @gzip -kf $(RELEASE_DIR)/ftewebglcl.wasm web-dbg: @PATH="$(EMSCRIPTENPATH)" $(MAKE) gl-dbg FTE_TARGET=web CC="$(EMCC)" @cp $(BASE_DIR)/web/fteshell.html $(DEBUG_DIR)/ftewebgl.html @gzip -kf $(DEBUG_DIR)/ftewebgl.html @gzip -kf $(DEBUG_DIR)/ftewebgl.js @gzip -kf $(DEBUG_DIR)/ftewebgl.wasm ################################################# #android #building for android will require: #download android sdk+ndk #ant installed #droid-dbg will install it on 'the current device', if you've got a device plugged in or an emulator running, it should just work. #makes an ant project for us droid/build.xml: -cd droid && PATH=$$PATH:$(realpath $(ANDROID_HOME)/tools):$(realpath $(ANDROID_NDK_ROOT)) $(ANDROID_SCRIPT) update project -t android-9 -p . -n FTEDroid #build FTE as a library, then build the java+package (release) droid/ftekeystore: ifeq ($(KEYTOOLARGS),) @echo @echo In order to build a usable APK file it must be signed. That requires a private key. @echo Creation of a private key requries various bits of info... @echo You are expected to fill that stuff in now... By the way, don\'t forget the password! @echo Note that every time you use make droid-rel, you will be required to enter a password. @echo You can use \'make droid-opt\' instead if you wish to build an optimised build without signing, @echo but such packages will require a rooted device \(or to be signed later\). @echo Just press control-c if you don\'t want to proceed. @echo Morality warning: never distribute droid/ftekeystore - always do make distclean before distributing. @echo $(JAVATOOL)keytool -genkey -v -keystore $@ -alias autogen -keyalg RSA -keysize 2048 -validity 10000 else @echo Generating keystore @$(JAVATOOL)keytool -genkey -keystore $@ -alias autogen -keyalg RSA -keysize 2048 -validity 10000 -noprompt $(KEYTOOLARGS) endif droid-rel: @$(MAKE) FTE_TARGET=droid droid/build.xml droid/ftekeystore @$(foreach a, $(DROID_ARCH), $(MAKE) FTE_TARGET=droid m-rel plugins-rel DROID_ARCH=$a NATIVE_PLUGINS="$(NATIVE_PLUGINS)"; ) @-rm -rf droid/libs @$(foreach a, $(DROID_ARCH), mkdir -p droid/libs/$a; ) -@$(foreach a, $(DROID_ARCH), cp $(RELEASE_DIR)/m_droid-$a/*.so droid/libs/$a/; ) @cd droid && $(ANT) -q release ifneq ($(DROID_PACKSU),) @echo Adding custom data files - non-compressed zip droid/bin/FTEDroid-release-unsigned.apk -0 -j $(DROID_PACKSU) endif ifneq ($(DROID_PACKSC),) @echo Adding custom data files - compressed zip droid/bin/FTEDroid-release-unsigned.apk -9 -j $(DROID_PACKSC) endif @echo Signing package... I hope you remember your password. @$(JAVATOOL)jarsigner $(JARSIGNARGS) -digestalg SHA1 -sigalg MD5withRSA -keystore droid/ftekeystore droid/bin/FTEDroid-release-unsigned.apk autogen @-rm -f $(RELEASE_DIR)/FTEDroid.apk @$(ANDROID_ZIPALIGN) 4 droid/bin/FTEDroid-release-unsigned.apk $(NATIVE_RELEASE_DIR)/FTEDroid.apk droid-opt: $(MAKE) FTE_TARGET=droid droid/build.xml droid/ftekeystore $(MAKE) FTE_TARGET=droid gl-rel mkdir -p droid/libs/armeabi @cp $(RELEASE_DIR)/libftedroid.so droid/libs/armeabi/ @cd droid && $(ANT) release cp droid/bin/FTEDroid-unsigned.apk $(RELEASE_DIR)/FTEDroid.apk #build FTE as a library, then build the java+package (release). also installs it onto the 'current' device. droid-dbg: $(MAKE) FTE_TARGET=droid droid/build.xml $(foreach a, $(DROID_ARCH), $(MAKE) FTE_TARGET=droid m-dbg plugins-dbg DROID_ARCH=$a NATIVE_PLUGINS="$(NATIVE_PLUGINS)"; ) -rm -rf droid/libs @$(foreach a, $(DROID_ARCH), mkdir -p droid/libs/$a; ) -@$(foreach a, $(DROID_ARCH), cp $(DEBUG_DIR)/m_droid-$a/*.so droid/libs/$a/; ) @cd droid && $(ANT) debug #&& $(ANT) debug install cp droid/bin/FTEDroid-debug.apk $(DEBUG_DIR)/FTEDroid.apk droid-help: @-echo "make droid-dbg - compiles engine with debug info and signs package with debug key. Attempts to install onto emulator." @-echo "make droid-opt - compiles engine with optimisations, but does not sign package. Not useful." @-echo "make droid-rel - compiles engine with optimisations, adds custom data files, signs with private key, requires password." @-echo @-echo "Android Settings:" @-echo "DROID_PACKSC: specifies additional pak or pk3 files to compress into the package, which avoids extra configuration. Only used in release builds. You probably shouldn't use this except for really small packages. Any file seeks will give really poor performance." @-echo "DROID_PACKSU: like DROID_PACKSC, but without compression. Faster loading times, but bigger. Use for anything that is already compressed (especially pk3s)." @-echo "ANDROID_HOME: path to the android sdk install path." @-echo "ANDROID_NDK_ROOT: path to the android ndk install path." @-echo "ANT: path and name of apache ant. Probably doesn't need to be set if you're on linux." @-echo "JAVA_HOME: path of your java install. Commonly already set in environment settings." @-echo "JAVATOOL: path to your java install's bin directory. Doesn't need to be set if its already in your path." @-echo "JARSIGNARGS: Additional optional arguments to java's jarsigner program. You may want to put -storepass FOO in here, but what ever you do - keep it secure. Avoid bash history snooping, etc. If its not present, you will safely be prompted as required." @-echo @-echo "Note that 'make droid-rel' will automatically generate a keystore. If you forget the password, just do a 'make dist-clean'." $(BASE_DIR)/libs-$(ARCH)/SDL2-$(SDL2VER)/i686-w64-mingw32/bin/sdl2-config: wget http://www.libsdl.org/release/SDL2-devel-$(SDL2VER)-mingw.tar.gz -O $(BASE_DIR)/sdl2.tar.gz cd $(BASE_DIR)/libs-$(ARCH) && tar -xvzf $(BASE_DIR)/sdl2.tar.gz rm $(BASE_DIR)/sdl2.tar.gz $(BASE_DIR)/libs-$(ARCH)/SDL2-$(SDL2VER)/x86_64-w64-mingw32/bin/sdl2-config: $(BASE_DIR)/libs-$(ARCH)/SDL2-$(SDL2VER)/i686-w64-mingw32/bin/sdl2-config #makes sure the configure scripts get the right idea. AR?=$(ARCH)-ar CONFIGARGS+= --host=$(ARCH) --enable-shared=no CC="$(CC)" CONFIGARGS:= $(CONFIGARGS) #--disable-silent-rules OPUSCONFIGARGS=$(CONFIGARGS) SPEEXDSPCONFIGARGS=$(CONFIGARGS) TOOLOVERRIDES+=CFLAGS="$$CFLAGS -Os" TOOLCONGIGUREOVERRIDES=$(TOOLOVERRIDES) TOOLMAKEOVERRIDES=$(TOOLOVERRIDES) ifeq (web,$(FTE_TARGET)) TOOLCONFIGUREOVERRIDES=emconfigure TOOLMAKEOVERRIDES=emmake OPUSCONFIGARGS=--disable-rtcd --disable-hardening --enable-stack-protector=no --enable-shared=no --host=none SPEEXDSPCONFIGARGS=--disable-neon --host=none CONFIGARGS=--enable-shared=no --host=none endif libs-$(ARCH)/libjpeg.a: test -f jpegsrc.v$(JPEGVER).tar.gz || wget http://www.ijg.org/files/jpegsrc.v$(JPEGVER).tar.gz test -f libs-$(ARCH)/libjpeg.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../jpegsrc.v$(JPEGVER).tar.gz && cd jpeg-$(JPEGVER) && $(CONFIGUREOVERRIDES) ./configure $(CONFIGARGS) && $(TOOLOVERRIDES) $(MAKE) && cp .libs/libjpeg.a ../ && $(TOOLOVERRIDES) $(AR) -s ../libjpeg.a && cp jconfig.h jerror.h jmorecfg.h jpeglib.h jversion.h ../ ) libs-$(ARCH)/libz.a libs-$(ARCH)/libz.pc: test -f zlib-$(ZLIBVER).tar.gz || wget http://zlib.net/fossils/zlib-$(ZLIBVER).tar.gz test -f libs-$(ARCH)/libz.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../zlib-$(ZLIBVER).tar.gz && cd zlib-$(ZLIBVER) && $(TOOLCONFIGUREOVERRIDES) ./configure --static && $(TOOLMAKEOVERRIDES) $(MAKE) libz.a CC="$(CC) $(W32_CFLAGS) -fPIC" && cp libz.a ../ && $(TOOLOVERRIDES) $(AR) -s ../libz.a && cp zlib.h zconf.h zutil.h zlib.pc ../ ) libs-$(ARCH)/libz9.a: libs-$(ARCH)/libz.a (cd libs-$(ARCH)/zlib-$(ZLIBVER) && \ $(CC) -o contrib/infback9/infback9.o -c contrib/infback9/infback9.c -I. && \ $(CC) -o contrib/infback9/inftree9.o -c contrib/infback9/inftree9.c -I. && \ cp contrib/infback9/infback9.h .. && \ $(AR) rcs ../libz9.a contrib/infback9/infback9.o contrib/infback9/inftree9.o) libs-$(ARCH)/libpng.a libs-$(ARCH)/libpng.pc: libs-$(ARCH)/libz.a libs-$(ARCH)/libz.pc test -f libpng-$(PNGVER).tar.gz || wget http://prdownloads.sourceforge.net/libpng/libpng-$(PNGVER).tar.gz?download -O libpng-$(PNGVER).tar.gz test -f libs-$(ARCH)/libpng.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../libpng-$(PNGVER).tar.gz && cd libpng-$(PNGVER) && $(TOOLOVERRIDES) ./configure CPPFLAGS=-I$(NATIVE_ABSBASE_DIR)/libs-$(ARCH)/ LDFLAGS=-L$(NATIVE_ABSBASE_DIR)/libs-$(ARCH)/ $(CONFIGARGS) --enable-static && $(TOOLOVERRIDES) $(MAKE) && cp .libs/libpng16.a ../libpng.a && cp libpng.pc png*.h ../ ) libs-$(ARCH)/libogg.a: test -f libogg-$(OGGVER).tar.gz || wget http://downloads.xiph.org/releases/ogg/libogg-$(OGGVER).tar.gz test -f libs-$(ARCH)/libogg.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../libogg-$(OGGVER).tar.gz && cd libogg-$(OGGVER) && $(TOOLOVERRIDES) ./configure $(CONFIGARGS) && $(TOOLOVERRIDES) $(MAKE) && cp src/.libs/libogg.a ../ && $(TOOLOVERRIDES) $(AR) -s ../libogg.a && mkdir ../ogg && cp include/ogg/*.h ../ogg) libs-$(ARCH)/libvorbis.a: libs-$(ARCH)/libogg.a test -f libvorbis-$(VORBISVER).tar.gz || wget http://downloads.xiph.org/releases/vorbis/libvorbis-$(VORBISVER).tar.gz test -f libs-$(ARCH)/libvorbisfile.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../libvorbis-$(VORBISVER).tar.gz && cd libvorbis-$(VORBISVER) && $(TOOLOVERRIDES) ./configure PKG_CONFIG= $(CONFIGARGS) --disable-oggtest --with-ogg-libraries=.. --with-ogg-includes=$(NATIVE_ABSBASE_DIR)/libs-$(ARCH)/libogg-$(OGGVER)/include && $(TOOLOVERRIDES) $(MAKE) && cp lib/.libs/libvorbis.a ../ && cp lib/.libs/libvorbisfile.a ../ && mkdir ../vorbis && cp include/vorbis/*.h ../vorbis) libs-$(ARCH)/libopus.a: test -f opus-$(OPUSVER).tar.gz || wget https://archive.mozilla.org/pub/opus/opus-$(OPUSVER).tar.gz test -f libs-$(ARCH)/libopus.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../opus-$(OPUSVER).tar.gz && cd opus-$(OPUSVER) && CFLAGS="-D_FORTIFY_SOURCE=0 $(CFLAGS) -Os" $(TOOLCONFIGUREOVERRIDES) ./configure $(OPUSCONFIGARGS) --disable-extra-programs && $(TOOLMAKEOVERRIDES) $(MAKE) && cp .libs/libopus.a ../ && cp include/opus*.h ../) libs-$(ARCH)/libspeex.a: test -f speex-$(SPEEXVER).tar.gz || wget http://downloads.us.xiph.org/releases/speex/speex-$(SPEEXVER).tar.gz test -f libs-$(ARCH)/libspeex.a || (mkdir -p libs-$(ARCH)/speex && cd libs-$(ARCH) && tar -xvzf ../speex-$(SPEEXVER).tar.gz && cd speex-$(SPEEXVER) && CFLAGS="$(CFLAGS) -Os" $(TOOLCONFIGUREOVERRIDES) ./configure $(CONFIGARGS) --disable-binaries && $(TOOLMAKEOVERRIDES) $(MAKE) && cp libspeex/.libs/libspeex.a ../ && cp -r include/speex/*.h ../speex/) libs-$(ARCH)/libspeexdsp.a: test -f speexdsp-$(SPEEXDSPVER).tar.gz || wget http://downloads.xiph.org/releases/speex/speexdsp-$(SPEEXDSPVER).tar.gz test -f libs-$(ARCH)/libspeexdsp.a || (mkdir -p libs-$(ARCH)/speex && cd libs-$(ARCH) && tar -xvzf ../speexdsp-$(SPEEXDSPVER).tar.gz && cd speexdsp-$(SPEEXDSPVER) && CFLAGS="$(CFLAGS) -Os" $(TOOLCONFIGUREOVERRIDES) ./configure $(SPEEXDSPCONFIGARGS) && $(TOOLMAKEOVERRIDES) $(MAKE) && cp libspeexdsp/.libs/libspeexdsp.a ../ && cp -r include/speex/*.h ../speex/) libs-$(ARCH)/libfreetype.a libs-$(ARCH)/ft2build.h: libs-$(ARCH)/libpng.a libs-$(ARCH)/libpng.pc test -f freetype-$(FREETYPEVER).tar.gz || wget https://download-mirror.savannah.gnu.org/releases/freetype/freetype-$(FREETYPEVER).tar.gz test -f libs-$(ARCH)/libfreetype.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../freetype-$(FREETYPEVER).tar.gz && cd freetype-$(FREETYPEVER) && PKG_CONFIG_LIBDIR=$(NATIVE_ABSBASE_DIR)/libs-$(ARCH) CFLAGS="$(CFLAGS) -Os" $(TOOLOVERRIDES) ./configure CPPFLAGS=-I$(NATIVE_ABSBASE_DIR)/libs-$(ARCH)/ LDFLAGS=-L$(NATIVE_ABSBASE_DIR)/libs-$(ARCH)/ $(CONFIGARGS) --with-zlib=yes --with-png=yes --with-bzip2=no --with-harfbuzz=no && $(TOOLOVERRIDES) $(MAKE) && cp objs/.libs/libfreetype.a ../ && cp -r include/* ../) libs-$(ARCH)/libBulletDynamics.a: test -f bullet3-$(BULLETVER).tar.gz || wget https://github.com/bulletphysics/bullet3/archive/$(BULLETVER).tar.gz -O bullet3-$(BULLETVER).tar.gz test -f libs-$(ARCH)/libBulletDynamics.a || (mkdir -p libs-$(ARCH) && cd libs-$(ARCH) && tar -xvzf ../bullet3-$(BULLETVER).tar.gz && cd bullet3-$(BULLETVER) && CFLAGS="$(CFLAGS) -Os" $(TOOLOVERRIDES) $(DO_CMAKE) . && $(TOOLOVERRIDES) $(MAKE) LinearMath BulletDynamics BulletCollision && cp src/LinearMath/libLinearMath.a src/BulletDynamics/libBulletDynamics.a src/BulletCollision/libBulletCollision.a src/btBulletCollisionCommon.h src/btBulletDynamicsCommon.h ..) libs-$(ARCH)/vulkan/vulkan.h: test -f vulkan-sdk-$(VULKANVER).tar.gz || wget https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/vulkan-sdk-$(VULKANVER).tar.gz cd libs-$(ARCH) && tar -xvzf ../vulkan-sdk-$(VULKANVER).tar.gz --strip-components=2 Vulkan-Headers-vulkan-sdk-$(VULKANVER)/include/ ifeq ($(FTE_TARGET),web) makelibs: libs-$(ARCH)/libz.a $(MAKELIBS) else MAKELIBS+=libs-$(ARCH)/vulkan/vulkan.h makelibs: libs-$(ARCH)/libjpeg.a libs-$(ARCH)/libz9.a libs-$(ARCH)/libz.a libs-$(ARCH)/libpng.a libs-$(ARCH)/libogg.a libs-$(ARCH)/libvorbis.a libs-$(ARCH)/libopus.a libs-$(ARCH)/libspeex.a libs-$(ARCH)/libspeexdsp.a libs-$(ARCH)/libfreetype.a $(MAKELIBS) endif HTTP_OBJECTS=http/httpserver.c http/iwebiface.c common/fs_stdio.c http/ftpserver.c $(RELEASE_DIR)/httpserver$(BITS)$(EXEPOSTFIX): $(HTTP_OBJECTS) $(CC) -o $@ -Icommon -Iclient -Iqclib -Igl -Iserver -DWEBSERVER -DWEBSVONLY -Dstricmp=strcasecmp -Dstrnicmp=strncasecmp -DNO_PNG $(HTTP_OBJECTS) httpserver: $(RELEASE_DIR)/httpserver$(BITS)$(EXEPOSTFIX) IQM_OBJECTS=../iqm/iqm.cpp ../imgtool.c client/image.c common/json.c ../plugins/models/gltf.c $(RELEASE_DIR)/iqmtool$(BITS)$(EXEPOSTFIX): $(IQM_OBJECTS) ifeq (win,$(findstring win,$(FTE_TARGET))) $(CC) -o $@ $(IQM_OBJECTS) -Icommon -Iclient -Iqclib -Igl -Iserver $(ALL_CFLAGS) $(CLIENTLIBFLAGS) -DIQMTOOL $(BASELDFLAGS) $(CLIENTLDDEPS) --static -static-libgcc -static-libstdc++ -lstdc++ -lm -Os else $(CC) -o $@ $(IQM_OBJECTS) -Icommon -Iclient -Iqclib -Igl -Iserver $(ALL_CFLAGS) -DIQMTOOL -lstdc++ -lm -ldl -Os endif iqm-rel: reldir $(RELEASE_DIR)/iqmtool$(BITS)$(EXEPOSTFIX) iqmtool-rel: reldir $(RELEASE_DIR)/iqmtool$(BITS)$(EXEPOSTFIX) iqm: iqm-rel iqmtool: iqmtool-rel IMGTOOL_OBJECTS=../imgtool.c client/image.c $(RELEASE_DIR)/imgtool$(BITS)$(EXEPOSTFIX): $(IMGTOOL_OBJECTS) $(CC) -o $@ $(IMGTOOL_OBJECTS) -lstdc++ -lm -Os $(ALL_CFLAGS) $(CLIENTLIBFLAGS) -DIMGTOOL $(BASELDFLAGS) $(CLIENTLDDEPS) imgtool-rel: $(RELEASE_DIR)/imgtool$(BITS)$(EXEPOSTFIX) imgtool: imgtool-rel ifeq (win,$(findstring win,$(FTE_TARGET))) MASTER_LDFLAGS=-lm -lz -lws2_32 -lwinmm -lole32 MASTER_OBJECTS+=common/fs_win32.c else MASTER_LDFLAGS=-lm -ldl -lz endif $(RELEASE_DIR)/ftemaster$(BITS)$(EXEPOSTFIX): $(MASTER_OBJECTS) $(CC) -o $@ $(MASTER_OBJECTS) -flto=jobserver -fvisibility=hidden -Icommon -Iclient -Iqclib -Igl -Iserver -DMASTERONLY -Dstricmp=strcasecmp -Dstrnicmp=strncasecmp $(MASTER_LDFLAGS) $(RELEASE_CFLAGS) $(RELEASE_LDFLAGS) $(ALL_CFLAGS) $(DEBUG_DIR)/ftemaster$(BITS)$(EXEPOSTFIX): $(MASTER_OBJECTS) $(CC) -o $@ $(MASTER_OBJECTS) -Icommon -Iclient -Iqclib -Igl -Iserver -DMASTERONLY -Dstricmp=strcasecmp -Dstrnicmp=strncasecmp $(MASTER_LDFLAGS) $(DEBUG_CFLAGS) $(DEBUG_LDFLAGS) master-rel: reldir $(RELEASE_DIR)/ftemaster$(BITS)$(EXEPOSTFIX) master-dbg: dbgdir $(DEBUG_DIR)/ftemaster$(BITS)$(EXEPOSTFIX) master: master-rel QTV_OBJECTS= \ netchan.c \ parse.c \ msg.c \ qw.c \ source.c \ bsp.c \ rcon.c \ mdfour.c \ md5.c \ crc.c \ control.c \ forward.c \ relay.c \ pmove.c \ menu.c \ msg.c \ httpsv.c \ sha1.c \ libqtvc/glibc_sucks.c $(RELEASE_DIR)/qtv$(BITS)$(EXEPOSTFIX): $(QTV_OBJECTS) $(CC) -o $@ $? -lstdc++ -lm $(BASE_INCLUDES) $(QTV_LDFLAGS) $(SVNREVISION) qtv-rel: @$(MAKE) $(RELEASE_DIR)/qtv$(BITS)$(EXEPOSTFIX) VPATH="$(BASE_DIR)/../fteqtv:$(VPATH)" qtv: qtv-rel utils: httpserver iqmtool imgtool master qtv-rel prefix ?= /usr/local exec_prefix ?= $(prefix) bindir ?= $(exec_prefix)/bin sbindir ?= $(exec_prefix)/sbin INSTALL ?= install INSTALL_PROGRAM ?= $(INSTALL) INSTALL_DATA ?= ${INSTALL} -m 644 install: sv-rel gl-rel mingl-rel qcc-rel $(INSTALL_PROGRAM) $(RELEASE_DIR)/$(EXE_NAME)-gl $(DESTDIR)$(bindir)/$(EXE_NAME)-gl $(INSTALL_PROGRAM) $(RELEASE_DIR)/$(EXE_NAME)-mingl $(DESTDIR)$(bindir)/$(EXE_NAME)-mingl $(INSTALL_PROGRAM) $(RELEASE_DIR)/$(EXE_NAME)-sv $(DESTDIR)$(bindir)/$(EXE_NAME)-sv $(INSTALL_PROGRAM) $(RELEASE_DIR)/fteqcc $(DESTDIR)$(bindir)/fteqcc version: @echo $(SVN_VERSION) @echo $(SVN_DATE) @echo $(FTE_CONFIG_EXTRA) ================================================ FILE: engine/README.MSVC ================================================ This code compiles against libjpeg, libpng, zlib, dx7, libogg and libvorbis. You can find the main MSVC 6 workspace in the ftequake directory. You will need to build the gas2masm project's debug build first. After that, you will have a choice of FTE builds. If you are running without libraries, you can pick the mingldebug build. For sw only builds, select the debug/release options. For dedicated builds, choose the logical one. The MDebug/MRelease builds are the merged binaries. important: msvc 6.0 standard install doesn't come with "ml.exe" which is needed to build fte, you can get it from either service pack 5 or 6. or grab it from somewhere i dunno zlib: libs/zconf.h libs/zlib.h libs/zlib.lib You will need zlib if you wish to build a version of FTE with png/zip/pk3 support. If you don't have it, you can hunt out the line '#define AVAIL_ZLIB' in bothdefs.h and disable it. Ogg Vorbis: libs/ogg/* libs/vorbis/* At the time of writing, ogg vorbis support is not fully functional and is #ifdefed out. You will not need these libraries. Lack of these files can be indicated by removing any '#define AVAIL_OGGVORBIS' line found in bothdefs.h (if they exist) lib jpeg: libs/jpeg.lib libs/jpeglib.h libs/jmorecfg.h libs/jconfig.h libs/jerror.h These files are optional and not strictly needed for anything other than screenshots and loading Quake3 textures. Hunt out and kill '#define AVAIL_JPEGLIB' from bothdefs.h to disable the requirement. URL pending. libpng: libs/libpng.lib libs/png.h libs/pngconf.h These files are for support of png textures and screenshots. Hunt out and kill '#define AVAIL_PNGLIB' from bothdefs.h to disable the requirement. URL pending. DirectX 7 SDK: libs/dxsdk7/include/* libs/dxsdk7/lib/* These are used for the d3d renderer. They are only benefitial in this way. This feature is normally enabled via an ifdef in the project file. An '#define NODIRECTX' in bothdefs.h will disable all requirements of dx. It is normally only used in the merged binaries. Without this define, it will expect to find DX5 headers and libraries in your compilers default directories. You can obtain an uptodate copy of directx from Microsoft's Website. ================================================ FILE: engine/client/anorms.h ================================================ /* Copyright (C) 1996-1997 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. */ {-0.525731, 0.000000, 0.850651}, {-0.442863, 0.238856, 0.864188}, {-0.295242, 0.000000, 0.955423}, {-0.309017, 0.500000, 0.809017}, {-0.162460, 0.262866, 0.951056}, {0.000000, 0.000000, 1.000000}, {0.000000, 0.850651, 0.525731}, {-0.147621, 0.716567, 0.681718}, {0.147621, 0.716567, 0.681718}, {0.000000, 0.525731, 0.850651}, {0.309017, 0.500000, 0.809017}, {0.525731, 0.000000, 0.850651}, {0.295242, 0.000000, 0.955423}, {0.442863, 0.238856, 0.864188}, {0.162460, 0.262866, 0.951056}, {-0.681718, 0.147621, 0.716567}, {-0.809017, 0.309017, 0.500000}, {-0.587785, 0.425325, 0.688191}, {-0.850651, 0.525731, 0.000000}, {-0.864188, 0.442863, 0.238856}, {-0.716567, 0.681718, 0.147621}, {-0.688191, 0.587785, 0.425325}, {-0.500000, 0.809017, 0.309017}, {-0.238856, 0.864188, 0.442863}, {-0.425325, 0.688191, 0.587785}, {-0.716567, 0.681718, -0.147621}, {-0.500000, 0.809017, -0.309017}, {-0.525731, 0.850651, 0.000000}, {0.000000, 0.850651, -0.525731}, {-0.238856, 0.864188, -0.442863}, {0.000000, 0.955423, -0.295242}, {-0.262866, 0.951056, -0.162460}, {0.000000, 1.000000, 0.000000}, {0.000000, 0.955423, 0.295242}, {-0.262866, 0.951056, 0.162460}, {0.238856, 0.864188, 0.442863}, {0.262866, 0.951056, 0.162460}, {0.500000, 0.809017, 0.309017}, {0.238856, 0.864188, -0.442863}, {0.262866, 0.951056, -0.162460}, {0.500000, 0.809017, -0.309017}, {0.850651, 0.525731, 0.000000}, {0.716567, 0.681718, 0.147621}, {0.716567, 0.681718, -0.147621}, {0.525731, 0.850651, 0.000000}, {0.425325, 0.688191, 0.587785}, {0.864188, 0.442863, 0.238856}, {0.688191, 0.587785, 0.425325}, {0.809017, 0.309017, 0.500000}, {0.681718, 0.147621, 0.716567}, {0.587785, 0.425325, 0.688191}, {0.955423, 0.295242, 0.000000}, {1.000000, 0.000000, 0.000000}, {0.951056, 0.162460, 0.262866}, {0.850651, -0.525731, 0.000000}, {0.955423, -0.295242, 0.000000}, {0.864188, -0.442863, 0.238856}, {0.951056, -0.162460, 0.262866}, {0.809017, -0.309017, 0.500000}, {0.681718, -0.147621, 0.716567}, {0.850651, 0.000000, 0.525731}, {0.864188, 0.442863, -0.238856}, {0.809017, 0.309017, -0.500000}, {0.951056, 0.162460, -0.262866}, {0.525731, 0.000000, -0.850651}, {0.681718, 0.147621, -0.716567}, {0.681718, -0.147621, -0.716567}, {0.850651, 0.000000, -0.525731}, {0.809017, -0.309017, -0.500000}, {0.864188, -0.442863, -0.238856}, {0.951056, -0.162460, -0.262866}, {0.147621, 0.716567, -0.681718}, {0.309017, 0.500000, -0.809017}, {0.425325, 0.688191, -0.587785}, {0.442863, 0.238856, -0.864188}, {0.587785, 0.425325, -0.688191}, {0.688191, 0.587785, -0.425325}, {-0.147621, 0.716567, -0.681718}, {-0.309017, 0.500000, -0.809017}, {0.000000, 0.525731, -0.850651}, {-0.525731, 0.000000, -0.850651}, {-0.442863, 0.238856, -0.864188}, {-0.295242, 0.000000, -0.955423}, {-0.162460, 0.262866, -0.951056}, {0.000000, 0.000000, -1.000000}, {0.295242, 0.000000, -0.955423}, {0.162460, 0.262866, -0.951056}, {-0.442863, -0.238856, -0.864188}, {-0.309017, -0.500000, -0.809017}, {-0.162460, -0.262866, -0.951056}, {0.000000, -0.850651, -0.525731}, {-0.147621, -0.716567, -0.681718}, {0.147621, -0.716567, -0.681718}, {0.000000, -0.525731, -0.850651}, {0.309017, -0.500000, -0.809017}, {0.442863, -0.238856, -0.864188}, {0.162460, -0.262866, -0.951056}, {0.238856, -0.864188, -0.442863}, {0.500000, -0.809017, -0.309017}, {0.425325, -0.688191, -0.587785}, {0.716567, -0.681718, -0.147621}, {0.688191, -0.587785, -0.425325}, {0.587785, -0.425325, -0.688191}, {0.000000, -0.955423, -0.295242}, {0.000000, -1.000000, 0.000000}, {0.262866, -0.951056, -0.162460}, {0.000000, -0.850651, 0.525731}, {0.000000, -0.955423, 0.295242}, {0.238856, -0.864188, 0.442863}, {0.262866, -0.951056, 0.162460}, {0.500000, -0.809017, 0.309017}, {0.716567, -0.681718, 0.147621}, {0.525731, -0.850651, 0.000000}, {-0.238856, -0.864188, -0.442863}, {-0.500000, -0.809017, -0.309017}, {-0.262866, -0.951056, -0.162460}, {-0.850651, -0.525731, 0.000000}, {-0.716567, -0.681718, -0.147621}, {-0.716567, -0.681718, 0.147621}, {-0.525731, -0.850651, 0.000000}, {-0.500000, -0.809017, 0.309017}, {-0.238856, -0.864188, 0.442863}, {-0.262866, -0.951056, 0.162460}, {-0.864188, -0.442863, 0.238856}, {-0.809017, -0.309017, 0.500000}, {-0.688191, -0.587785, 0.425325}, {-0.681718, -0.147621, 0.716567}, {-0.442863, -0.238856, 0.864188}, {-0.587785, -0.425325, 0.688191}, {-0.309017, -0.500000, 0.809017}, {-0.147621, -0.716567, 0.681718}, {-0.425325, -0.688191, 0.587785}, {-0.162460, -0.262866, 0.951056}, {0.442863, -0.238856, 0.864188}, {0.162460, -0.262866, 0.951056}, {0.309017, -0.500000, 0.809017}, {0.147621, -0.716567, 0.681718}, {0.000000, -0.525731, 0.850651}, {0.425325, -0.688191, 0.587785}, {0.587785, -0.425325, 0.688191}, {0.688191, -0.587785, 0.425325}, {-0.955423, 0.295242, 0.000000}, {-0.951056, 0.162460, 0.262866}, {-1.000000, 0.000000, 0.000000}, {-0.850651, 0.000000, 0.525731}, {-0.955423, -0.295242, 0.000000}, {-0.951056, -0.162460, 0.262866}, {-0.864188, 0.442863, -0.238856}, {-0.951056, 0.162460, -0.262866}, {-0.809017, 0.309017, -0.500000}, {-0.864188, -0.442863, -0.238856}, {-0.951056, -0.162460, -0.262866}, {-0.809017, -0.309017, -0.500000}, {-0.681718, 0.147621, -0.716567}, {-0.681718, -0.147621, -0.716567}, {-0.850651, 0.000000, -0.525731}, {-0.688191, 0.587785, -0.425325}, {-0.587785, 0.425325, -0.688191}, {-0.425325, 0.688191, -0.587785}, {-0.425325, -0.688191, -0.587785}, {-0.587785, -0.425325, -0.688191}, {-0.688191, -0.587785, -0.425325}, ================================================ FILE: engine/client/api_menu.h ================================================ /* * Copyright (c) 2015-2018 * Marco Cawthorne All rights reserved. * * This 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 3 of the License, or * (at your option) any later version. * This 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. If not, see . */ #define NATIVEMENU_API_VERSION_MIN 0 //will be updated any time a symbol is renamed. #define NATIVEMENU_API_VERSION_MAX 0 //bumped for any change. #ifndef NATIVEMENU_API_VERSION //so you can hold back the reported version in order to work with older engines. #define NATIVEMENU_API_VERSION NATIVEMENU_API_VERSION_MAX //version reported to the other side. #endif struct vfsfile_s; struct serverinfo_s; struct searchpathfuncs_s; struct model_s; struct font_s; struct shader_s; #ifndef __QUAKEDEF_H__ #ifdef __cplusplus typedef enum {qfalse, qtrue} qboolean;//false and true are forcivly defined. #else typedef enum {false, true} qboolean; #endif typedef float vec_t; typedef vec_t vec2_t[2]; typedef vec_t vec3_t[3]; typedef vec_t vec4_t[4]; #ifdef _MSC_VER #define QDECL __cdecl #else #define QDECL #endif #include typedef uint64_t qofs_t; #endif #if 1 //c++ or standard C #include "cl_master.h" #endif enum slist_test_e; enum hostcachekey_e; //obtained via calls to gethostcacheindexforkey enum fs_relative; enum com_tokentype_e; struct menu_inputevent_args_s { enum { MIE_KEYDOWN = 0, MIE_KEYUP = 1, MIE_MOUSEDELTA = 2, MIE_MOUSEABS = 3, MIE_JOYAXIS = 4, } eventtype; unsigned int devid; union { struct { unsigned int scancode; unsigned int charcode; } key; struct { float delta[2]; float screen[2]; //virtual coords } mouse; struct { unsigned int axis; float val; } axis; }; }; typedef enum { MI_INIT, //initial startup MI_RENDERER, //renderer restarted, any models/shaders/textures handles are no longer valid MI_RESOLUTION, //video mode changed (scale or physical size) but without any gpu resources getting destroyed. you'll want to reload fonts. } mintreason_t; typedef struct { struct model_s *model; int frame[2]; float frametime[2]; float frameweight[2]; vec4_t matrix[3]; //axis/angles+origin } menuentity_t; typedef struct { //these are in virtual coords, thus they need to be floats so that they can be rounded to ints more cleanly... yeah, scaling sucks. vec2_t pos; vec2_t size; float time; //affects shader effects vec_t fov[2]; vec4_t viewmatrix[3]; struct model_s *worldmodel; int numentities; menuentity_t *entlist; } menuscene_t; typedef struct { int api_version; //this may be higher than you expect. const char *engine_version; int (*checkextension) (const char *ext); void (QDECL *error) (const char *err, ...); void (*printf) (const char *text, ...); void (*dprintf) (const char *text, ...); void (*localcmd) (const char *cmd); float (*cvar_float) (const char *name); const char *(*cvar_string) (const char *name, qboolean effective); //NULL if it doesn't exist. return value lasts until cvar_set is called, etc, so don't cache. effective=true reports its active value, not the value that the user wanted. const char *(*cvar_default) (const char *name); void (*cvar_set) (const char *name, const char *value); void (*registercvar) (const char *name, const char *defaultvalue, unsigned int flags, const char *description); void (*registercommand) (const char *name, const char *description); char *(*parsetoken) (const char *data, char *out, int outlen, enum com_tokentype_e *toktype); int (*isserver) (void); int (*getclientstate) (char const**disconnectionreason); void (*localsound) (const char *sample, int channel, float volume); // file input / search crap struct vfsfile_s *(*fopen) (const char *filename, const char *modestring, enum fs_relative fsroot); //modestring should be one of rb,r+b,wb,w+b,ab,wbp. Mostly use a root of FS_GAMEONLY for writes, otherwise FS_GAME for reads. void (*fclose) (struct vfsfile_s *fhandle); char *(*fgets) (struct vfsfile_s *fhandle, char *out, size_t outsize); //returns output buffer, or NULL void (*fprintf) (struct vfsfile_s *fhandle, const char *s, ...); void (*enumeratefiles) (const char *match, int (QDECL *callback)(const char *fname, qofs_t fsize, time_t mtime, void *ctx, struct searchpathfuncs_s *package), void *ctx); qboolean (QDECL *nativepath)(const char *fname, enum fs_relative relativeto, char *out, int outlen); //Converts a relative path to a printable system path. All paths are considered to be utf-8. WARNING: This means that windows users will need to use _wfopen etc if they use the resulting path of this function in any system calls. WARNING: this function can and WILL fail for dodgy paths (eg blocking writes to "../engine.dll") // Drawing stuff void (*drawsetcliparea) (float x, float y, float width, float height); void (*drawresetcliparea) (void); struct shader_s *(*cachepic)(const char *name); qboolean (*drawgetimagesize)(struct shader_s *pic, int *x, int *y); void (*drawquad) (const vec2_t position[4], const vec2_t texcoords[4], struct shader_s *pic, const vec4_t rgba, unsigned int be_flags); float (*drawstring) (const vec2_t position, const char *text, struct font_s *font, float height, const vec4_t rgba, unsigned int be_flags); float (*stringwidth) (const char *text, struct font_s *font, float height); struct font_s *(*loadfont) (const char *facename, float intendedheight); //with ttf fonts, you'll probably want one for each size. void (*destroyfont) (struct font_s *font); // 3D scene stuff struct model_s *(*cachemodel)(const char *name); qboolean (*getmodelsize) (struct model_s *model, vec3_t out_mins, vec3_t out_maxs); void (*renderscene) (menuscene_t *scene); // Menu specific stuff void (*pushmenu) (void *ctx); //will have key focus. qboolean (*ismenupushed) (void *ctx); //reports if its still pushed (but not necessarily the active one!). void (*killmenu) (void *ctx); //force-removes a menu. int (*setmousecursor) (const char *cursorname, float hot_x, float hot_y, float scale); //forces absolute mouse coords whenever cursorname isn't NULL const char *(*keynumtostring) (int keynum, int modifier); int (*stringtokeynum) (const char *key, int *modifier); int (*findkeysforcommand) (int bindmap, const char *command, int *out_scancodes, int *out_modifiers, int keycount); // Server browser stuff enum hostcachekey_e (*gethostcacheindexforkey) (const char *key); struct serverinfo_s *(*getsortedhost) (int idx); char *(*gethostcachestring) (struct serverinfo_s *host, enum hostcachekey_e fld); float (*gethostcachenumber) (struct serverinfo_s *host, enum hostcachekey_e fld); void (*resethostcachemasks) (void); void (*sethostcachemaskstring) (qboolean or_, enum hostcachekey_e fld, const char *str, enum slist_test_e op); void (*sethostcachemasknumber) (qboolean or_, enum hostcachekey_e fld, int num, enum slist_test_e op); void (*sethostcachesort) (enum hostcachekey_e fld, qboolean descending); int (*resorthostcache) (void); void (*refreshhostcache) (qboolean fullreset); qboolean (*sendhostcachequeries) (void); //returns true while there are still waiting for servers. should be called each frame while you still care about the servers. } menu_import_t; typedef struct { int api_version; void (*Init) (mintreason_t reason, float vwidth, float vheight, int pwidth, int pheight); void (*Shutdown) (mintreason_t reason); void (*DrawLoading) (double frametime); //pure loading screen. void (*Toggle) (int wantmode); qboolean(*ConsoleCommand) (const char *cmdline, int argc, char const*const*argv); void (*Draw) (void *ctx, double frametime); //draws a menu. qboolean(*InputEvent) (void *ctx, struct menu_inputevent_args_s ev); //return true to prevent the engine handling it (ie: because you already did). void (*Closed) (void *ctx); //a pushed menu was closed. } menu_export_t; #ifndef NATIVEEXPORT #ifdef _WIN32 #define NATIVEEXPORTPROTO __declspec(dllexport) #define NATIVEEXPORT NATIVEEXPORTPROTO #else #define NATIVEEXPORTPROTO #define NATIVEEXPORT __attribute__((visibility("default"))) #endif #endif NATIVEEXPORTPROTO menu_export_t *QDECL GetMenuAPI (menu_import_t *import); ================================================ FILE: engine/client/bymorphed.h ================================================ /* GIMP RGBA C-Source image dump (bymorphed.c) */ static const struct { unsigned int width; unsigned int height; unsigned int bytes_per_pixel; /* 3:RGB, 4:RGBA */ unsigned char pixel_data[32 * 32 * 4 + 1]; } icon = { 32, 32, 4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\377\200\200\200\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\300\300\300\377\300\300\300\377\300\300\300" "\377\300\300\300\377\200\200\200\377\200\200\200\377\200\200\200\377\200" "\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\300\300\300" "\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\377\200\200\200\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\300\300\300\377\300\300\300\377\300\300\300" "\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200" "\377\300\300\300\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377" "\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377\200\200" "\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0" "\377\200\200\200\377\300\300\300\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\200\200\200\377\0\0\0\377\200\200\200\377\300\300\300" "\377\300\300\300\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\300\300\300\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\200\200\200\377\0\0\0\377\0\0\0\377\300\300\300\377\300\300\300\377\300" "\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0" "\0\377\200\200\200\377\200\200\200\377\200\200\200\377\300\300\300\377\300" "\300\300\377\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\200\200\377\0\0\0\377\300\300\300\377" "\300\300\300\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\200\200\200\377\300\300\300\377\300\300\300\377\300\300" "\300\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\300\300\300\377\300\300\300\377\300\300\300\377\200\200\200\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\300\300\300\377\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\377\0\0\0\377\300\300\300\377\300\300\300\377\200" "\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\300" "\300\300\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\300\300\300\377\200\200\200\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\300\300\300\377\0\0\0\0" "\0\0\0\0\0\0\0\377\0\0\0\377\300\300\300\377\200\200\200\377\0\0\0\377\0" "\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\300\300\300\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\300\300\300\377\300\300\300\377" "\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377" "\0\0\0\0\0\0\0\0\0\0\0\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0" "\0\377\0\0\0\377\0\0\0\377\300\300\300\377\300\300\300\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\300\300" "\300\377\300\300\300\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377" "\200\200\200\377\0\0\0\0\0\0\0\377\200\200\200\377\200\200\200\377\0\0\0" "\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\300\300\300\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\300\300\300\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\200\200\200\377\0\0\0\0\300\300\300\377\200\200" "\200\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\300" "\300\300\377\300\300\300\377\377\377\377\377\300\300\300\377\200\200\200" "\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200" "\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200" "\377\200\200\200\377\377\377\377\377\300\300\300\377\300\300\300\377\300" "\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\300" "\300\300\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377" "\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300" "\300\377\300\300\300\377\200\200\200\377\300\300\300\377\300\300\300\377" "\300\300\300\377\300\300\300\377\200\200\200\377\300\300\300\377\300\300" "\300\377\200\200\200\377\300\300\300\377\300\300\300\377\300\300\300\377" "\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\0\0\0\377" "\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\300\300\300\377\200" "\200\200\377\300\300\300\377\300\300\300\377\300\300\300\377\200\200\200" "\377\200\200\200\377\300\300\300\377\300\300\300\377\300\300\300\377\200" "\200\200\377\300\300\300\377\300\300\300\377\200\200\200\377\200\200\200" "\377\300\300\300\377\300\300\300\377\300\300\300\377\200\200\200\377\300" "\300\300\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377" "\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200" "\377\300\300\300\377\300\300\300\377\200\200\200\377\300\300\300\377\300" "\300\300\377\300\300\300\377\200\200\200\377\300\300\300\377\300\300\300" "\377\300\300\300\377\300\300\300\377\200\200\200\377\300\300\300\377\300" "\300\300\377\200\200\200\377\300\300\300\377\300\300\300\377\300\300\300" "\377\300\300\300\377\200\200\200\377\300\300\300\377\300\300\300\377\200" "\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\0\0" "\0\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\200\200\200\377" "\200\200\200\377\300\300\300\377\300\300\300\377\300\300\300\377\200\200" "\200\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377" "\200\200\200\377\300\300\300\377\300\300\300\377\200\200\200\377\200\200" "\200\377\200\200\200\377\300\300\300\377\300\300\300\377\200\200\200\377" "\200\200\200\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377" "\300\300\300\377\200\200\200\377\200\200\200\377\300\300\300\377\300\300" "\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377" "\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300" "\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377" "\200\200\200\377\0\0\0\377\200\200\200\377\300\300\300\377\200\200\200\377" "\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0" "\0\0\377\200\200\200\377\300\300\300\377\200\200\200\377\0\0\0\377\200\200" "\200\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377" "\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300" "\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377" "\300\300\300\377\200\200\200\377\0\0\0\377\200\200\200\377\300\300\300\377" "\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\200\200\200\377" "\0\0\0\377\200\200\200\377\300\300\300\377\300\300\300\377\300\300\300\377" "\300\300\300\377\300\300\300\377\300\300\300\377\300\300\300\377\300\300" "\300\377\300\300\300\377\300\300\300\377\300\300\300\377\200\200\200\377" "\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\200\200\200\377\300" "\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\200" "\200\200\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\200\200" "\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377" "\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200" "\200\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\200\200\200" "\377\300\300\300\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377" "\0\0\0\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200" "\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200" "\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200" "\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200" "\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377" "\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\200" "\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200" "\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\200\200" "\200\377\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\200\200\200\377" "\200\200\200\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\200" "\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377" "\300\300\300\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377" "\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200" "\377\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377" "\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377" "\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\200\200\200\377\200" "\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\300\300\300\377\0\0\0\0\200\200" "\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200" "\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200" "\200\377\200\200\200\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200" "\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200" "\377\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377" "\200\200\200\377\0\0\0\0\0\0\0\0\200\200\200\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\200\200\200\377" "\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200" "\200\377\200\200\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0" "\377\0\0\0\377\200\200\200\377\0\0\0\0\0\0\0\0\0\0\0\0\200\200\200\377\0" "\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200" "\200\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200" "\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377" "\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\300\300\300\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377" "\0\0\0\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377" "\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\200\200\200\377\200" "\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200" "\377\300\300\300\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0" "\377\300\300\300\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300" "\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200" "\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377" "\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\200\200\200\377\200" "\200\200\377\200\200\200\377\300\300\300\377\200\200\200\377\0\0\0\377\0" "\0\0\377\0\0\0\377\0\0\0\377\300\300\300\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\200\200\200" "\377\200\200\200\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377" "\200\200\200\377\300\300\300\377\200\200\200\377\200\200\200\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200" "\377\200\200\200\377\200\200\200\377\200\200\200\377\200\200\200\377\200" "\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\300\300\300\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\377\200\200" "\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0" "\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\200\200\200\377\300\300\300\377\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0\377" "\0\0\0\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300" "\377\200\200\200\377\200\200\200\377\0\0\0\377\0\0\0\377\0\0\0\377\0\0\0" "\377\0\0\0\377\0\0\0\377\200\200\200\377\200\200\200\377\300\300\300\377" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0", }; ================================================ FILE: engine/client/cd_linux.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ // Quake is a trademark of Id Software, Inc., (c) 1996 Id Software, Inc. All // rights reserved. /* * All Unix porting was primarily focused on getting this to work on FreeBSD 14+ * but hopefully will give others the ability to further improve upon it. * Most information sourced from the following documentation: * Linux CDROM ioctl: https://www.kernel.org/doc/html/latest/userspace-api/ioctl/cdrom.html * FreeBSD 14.0 cd man page: https://man.freebsd.org/cgi/man.cgi?query=cd&sektion=4&apropos=0&manpath=FreeBSD+14.0-RELEASE+and+Ports * The cdio header file found at /usr/include/sys/cdio.h (very well documented about what everything does) * - Brad */ #include "quakedef.h" #ifndef HAVE_CDPLAYER //nothing #elif defined(__CYGWIN__) #include "cd_null.c" #else #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #elif defined(__unix__) && !defined(__CYGWIN__) #include #define CDROMEJECT CDDOEJECT #define CDROMCLOSETRAY CDDOCLOSE #define CDROMPAUSE CDDOPAUSE #define CDROMRESUME CDDORESUME #define CDROMRESET CDDORESET #define CDROMSTOP CDDOSTOP #define CDROMSTART CDDOSTART #define CDROMREADTOCHDR CDREADHEADER #define CDROMREADTOCENTRY CDIOREADTOCENTRY #define CDROMSUBCHNL CDREADSUBQ #define CDROM_MSF CD_MSF_FORMAT #define CDROM_DATA_TRACK CD_SUBQ_DATA #define CDROM_AUDIO_PAUSED CD_AS_PLAY_PAUSED #define CDROM_AUDIO_PLAY CD_AS_PLAY_IN_PROGRESS #endif static int cdfile = -1; #if defined(__unix__) && !defined(__APPLE__) && !defined(__CYGWIN__) && !defined(__linux__) static char cd_dev[64] = "/dev/cd0"; #else static char cd_dev[64] = "/dev/cdrom"; #endif static qboolean playing; void CDAudio_Eject(void) { if (cdfile == -1) return; // no cd init'd if ( ioctl(cdfile, CDROMEJECT) == -1 ) Con_DPrintf("ioctl cdromeject failed\n"); } void CDAudio_CloseDoor(void) { if (cdfile == -1) return; // no cd init'd if ( ioctl(cdfile, CDROMCLOSETRAY) == -1 ) Con_DPrintf("ioctl cdromclosetray failed\n"); } int CDAudio_GetAudioDiskInfo(void) { #ifdef __linux__ struct cdrom_tochdr tochdr; #elif defined(__unix__) && !defined(__CYGWIN__) struct ioc_toc_header tochdr; struct cd_sub_channel_track_info trk; #endif if (cdfile == -1) return -1; if ( ioctl(cdfile, CDROMREADTOCHDR, &tochdr) == -1 ) { Con_DPrintf("ioctl cdromreadtochdr failed\n"); return -1; } #ifdef __linux__ if (tochdr.cdth_trk0 < 1) #elif defined(__unix__) && !defined(__CYGWIN__) if (trk.track_number < 0) // track_number may not work correctly here - Brad #endif { Con_DPrintf("CDAudio: no music tracks\n"); return -1; } #ifdef __linux__ return tochdr.cdth_trk1; #elif defined(__unix__) && !defined(__CYGWIN__) return tochdr.starting_track; #else Con_DPrintf("CDAudio: no music tracks\n"); return -1; #endif } void CDAudio_Play(int track) { #ifdef __linux__ struct cdrom_tocentry entry; struct cdrom_ti ti; #elif defined(__unix__) && !defined(__CYGWIN__) // This all may be wrong, but it should be close - Brad struct ioc_toc_header tochdr; // cd drive header info (for getting start and end track info mostly) struct ioc_read_toc_single_entry entry; // individual audio track's entry info struct cd_sub_channel_info ti; // individual audio track's subchannel info (for indexing and whatnot) struct ioc_play_track play; // cd drive audio indexing #endif if (cdfile == -1) return; // don't try to play a non-audio track #ifdef __linux__ entry.cdte_track = track; entry.cdte_format = CDROM_MSF; #elif defined(__unix__) && !defined(__CYGWIN__) entry.track = track; entry.address_format = CDROM_MSF; #endif if ( ioctl(cdfile, CDROMREADTOCENTRY, &entry) == -1 ) { Con_DPrintf("ioctl cdromreadtocentry failed\n"); return; } #ifdef __linux__ if (entry.cdte_ctrl == CDROM_DATA_TRACK) #elif defined(__unix__) && !defined(__CYGWIN__) if (entry.entry.control == CDROM_DATA_TRACK) #endif { Con_Printf("CDAudio: track %i is not audio\n", track); return; } #ifdef __linux__ ti.cdti_trk0 = track; ti.cdti_trk1 = track; ti.cdti_ind0 = 1; ti.cdti_ind1 = 99; #elif defined(__unix__) && !defined(__CYGWIN__) if (ti.what.position.track_number == 0 || ti.what.position.track_number == 1) { entry.track = track; } if (ti.what.position.index_number == 0) { play.start_track = tochdr.starting_track; } if (ti.what.position.index_number == 1) { play.end_track = tochdr.ending_track; } #define CDROMPLAYTRKIND ti.what.position.track_number #endif if ( ioctl(cdfile, CDROMPLAYTRKIND, &ti) == -1 ) { Con_DPrintf("ioctl cdromplaytrkind failed\n"); return; } if ( ioctl(cdfile, CDROMRESUME) == -1 ) Con_DPrintf("ioctl cdromresume failed\n"); playing = true; if (!bgmvolume.value || !mastervolume.value) CDAudio_Pause (); } void CDAudio_Stop(void) { if (cdfile == -1) return; if ( ioctl(cdfile, CDROMSTOP) == -1 ) Con_DPrintf("ioctl cdromstop failed (%d)\n", errno); } void CDAudio_Pause(void) { if (cdfile == -1) return; if ( ioctl(cdfile, CDROMPAUSE) == -1 ) Con_DPrintf("ioctl cdrompause failed\n"); } void CDAudio_Resume(void) { if (cdfile == -1) return; if ( ioctl(cdfile, CDROMRESUME) == -1 ) Con_DPrintf("ioctl cdromresume failed\n"); } void CDAudio_Update(void) { #ifdef __linux__ struct cdrom_subchnl subchnl; static time_t lastchk; #elif defined(__unix__) && !defined(__CYGWIN__) struct cd_sub_channel_info subchnl; struct ioc_read_subchannel cdsc; // subchn.cdsc_format workaround - Brad // Note: there doesn't seem to be a way to check how much time is left for playing // cd audio without doing some extra manual work, so I'll be omitting it from // the unix checks for the time being - Brad #endif if (playing #ifdef __linux__ && lastchk < time(NULL) #endif ) { #ifdef __linux__ lastchk = time(NULL) + 2; //two seconds between checks subchnl.cdsc_format = CDROM_MSF; #elif defined(__unix__) && !defined(__CYGWIN__) cdsc.address_format = CDROM_MSF; #endif if (ioctl(cdfile, CDROMSUBCHNL, &subchnl) == -1 ) { Con_DPrintf("ioctl cdromsubchnl failed\n"); playing = false; return; } #ifdef __linux__ if (subchnl.cdsc_audiostatus != CDROM_AUDIO_PLAY && subchnl.cdsc_audiostatus != CDROM_AUDIO_PAUSED) #elif defined(__unix__) && !defined(__CYGWIN__) if (subchnl.header.audio_status != CDROM_AUDIO_PLAY && subchnl.header.audio_status != CDROM_AUDIO_PAUSED) #endif { playing = false; Media_EndedTrack(); } } } qboolean CDAudio_Startup(void) { int i; if (cdfile != -1) return true; if ((i = COM_CheckParm("-cddev")) != 0 && i < com_argc - 1) { Q_strncpyz(cd_dev, com_argv[i + 1], sizeof(cd_dev)); cd_dev[sizeof(cd_dev) - 1] = 0; } if ((cdfile = open(cd_dev, O_RDONLY|O_NONBLOCK)) == -1) { Con_Printf("CDAudio_Init: open of \"%s\" failed (%i)\n", cd_dev, errno); cdfile = -1; return false; } return true; } void CDAudio_Init(void) { } void CDAudio_Shutdown(void) { if (cdfile == -1) return; CDAudio_Stop(); close(cdfile); cdfile = -1; } #endif ================================================ FILE: engine/client/cd_null.c ================================================ #include "quakedef.h" #ifdef HAVE_CDPLAYER #ifdef _WIN32 //not really needed, but nice none-the-less. #include "winquake.h" LONG CDAudio_MessageHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { return 1; } #endif void CDAudio_Shutdown(void) { } void CDAudio_Update(void) { } void CDAudio_Init(void) { } void CDAudio_Play(int track) { } void CDAudio_Stop(void) { } void CDAudio_Pause(void) { } void CDAudio_Resume(void) { } void CDAudio_Eject(void) { } void CDAudio_CloseDoor(void) { } int CDAudio_GetAudioDiskInfo(void) { return -2; } qboolean CDAudio_Startup(void) { return false; } #endif ================================================ FILE: engine/client/cd_sdl.c ================================================ #include "quakedef.h" #ifdef FTE_SDL3 #include #else #include #endif #ifndef HAVE_CDPLAYER //nothing #elif SDL_MAJOR_VERSION >= 2 //sdl2 has no cd support. sod off. #include "cd_null.c" #else extern cvar_t bgmvolume; static qboolean initialized = false; static SDL_CD *cddevice; void CDAudio_Eject(void) { if (SDL_CDEject(cddevice)) Con_DPrintf("SDL_CDEject failed\n"); } void CDAudio_CloseDoor(void) { Con_Printf("SDL does not support this\n"); } int CDAudio_GetAudioDiskInfo(void) { switch (SDL_CDStatus(cddevice)) { case CD_ERROR: Con_Printf("SDL_CDStatus returned error\n"); return -1; case CD_TRAYEMPTY: return 0; default: break; } return cddevice->numtracks; } void CDAudio_Play(int track) { if (SDL_CDPlayTracks(cddevice, track, 0, 1, 0)) { Con_Printf("CDAudio: track %i is not audio\n", track); return; } if (!bgmvolume.value) CDAudio_Pause (); return; } void CDAudio_Stop(void) { if (SDL_CDStop(cddevice)) Con_DPrintf("CDAudio: SDL_CDStop failed"); } void CDAudio_Pause(void) { if (SDL_CDPause(cddevice)) Con_DPrintf("CDAudio: SDL_CDPause failed"); } void CDAudio_Resume(void) { if (SDL_CDResume(cddevice)) { Con_DPrintf("CDAudio: SDL_CDResume failed\n"); return; } } void CDAudio_Update(void) { } void CDAudio_Init(void) { } qboolean CDAudio_Startup(void) { if (initialized) return !!cddevice; if (!bgmvolume.value) return false; initialized = true; SDL_InitSubSystem(SDL_INIT_CDROM|SDL_INIT_NOPARACHUTE); if(!SDL_CDNumDrives()) { Con_DPrintf("CDAudio_Init: No CD drives\n"); return false; } cddevice = SDL_CDOpen(0); if (!cddevice) { Con_Printf("CDAudio_Init: SDL_CDOpen failed\n"); return false; } return true; } void CDAudio_Shutdown(void) { if (!initialized) return; CDAudio_Stop(); SDL_CDClose(cddevice); cddevice = NULL; initialized = false; } #endif ================================================ FILE: engine/client/cd_win.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ // Quake is a trademark of Id Software, Inc., (c) 1996 Id Software, Inc. All // rights reserved. #include "quakedef.h" #include "winquake.h" #ifndef HAVE_CDPLAYER //nothing #elif defined(WINRT) #include "cd_null.c" #else #if defined(_MSC_VER) && (_MSC_VER < 1300) #define DWORD_PTR DWORD #endif extern HWND mainwindow; extern cvar_t bgmvolume; static qboolean initialized; static qboolean initializefailed; static DWORD resumeend; static qboolean pollneeded; //workaround for windows vista/7 bug, where notification simply does not work for end of tracks. static UINT wDeviceID; int CDAudio_GetAudioDiskInfo(void) { DWORD dwReturn; static MCI_STATUS_PARMS mciStatusParms; if (!CDAudio_Startup()) return -1; if (!initialized) return -1; mciStatusParms.dwItem = MCI_STATUS_READY; mciStatusParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_WAIT, (DWORD_PTR) (LPVOID) &mciStatusParms); if (dwReturn) { Con_DPrintf("CDAudio: drive ready test - get status failed\n"); return -1; } if (!mciStatusParms.dwReturn) { Con_DPrintf("CDAudio: drive not ready\n"); return -1; } mciStatusParms.dwItem = MCI_STATUS_NUMBER_OF_TRACKS; mciStatusParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_WAIT, (DWORD_PTR) (LPVOID) &mciStatusParms); if (dwReturn) { Con_DPrintf("CDAudio: get tracks - status failed\n"); return -1; } if (mciStatusParms.dwReturn < 1) { Con_DPrintf("CDAudio: no music tracks\n"); return -1; } return mciStatusParms.dwReturn; } qboolean CDAudio_Startup(void) { DWORD dwReturn; static MCI_OPEN_PARMSA mciOpenParms; static MCI_SET_PARMS mciSetParms; if (initializefailed) return false; if (initialized) return true; mciOpenParms.lpstrDeviceType = "cdaudio"; mciOpenParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_SHAREABLE, (DWORD_PTR) (LPVOID) &mciOpenParms); if (dwReturn) { Con_Printf("CDAudio_Init: MCI_OPEN failed (%i)\n", (int)dwReturn); initializefailed = true; return 0; } wDeviceID = mciOpenParms.wDeviceID; // Set the time format to frames. vista+ simply cannot come with converting to/from seconds, or something (notifies don't work, status stays playing, position stops updating at about 3 frames from the end of the track). mciSetParms.dwTimeFormat = MCI_FORMAT_MSF; mciSetParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_SET, MCI_SET_TIME_FORMAT, (DWORD_PTR)(LPVOID) &mciSetParms); if (dwReturn) { Con_Printf("MCI_SET_TIME_FORMAT failed (%i)\n", (int)dwReturn); mciSendCommand(wDeviceID, MCI_CLOSE, 0, (DWORD_PTR)NULL); initializefailed = true; return 0; } initialized = true; if (CDAudio_GetAudioDiskInfo() <= 0) { Con_Printf("CDAudio_Init: No CD in player.\n"); } return true; } void CDAudio_Shutdown(void) { if (initialized) { CDAudio_Stop(); if (mciSendCommand(wDeviceID, MCI_CLOSE, MCI_WAIT, (DWORD_PTR)NULL)) Con_DPrintf("CDAudio_Shutdown: MCI_CLOSE failed\n"); } initialized = false; } void CDAudio_Eject(void) { DWORD dwReturn; dwReturn = mciSendCommand(wDeviceID, MCI_SET, MCI_SET_DOOR_OPEN, (DWORD_PTR)NULL); if (dwReturn) Con_DPrintf("MCI_SET_DOOR_OPEN failed (%i)\n", (int)dwReturn); } void CDAudio_CloseDoor(void) { DWORD dwReturn; dwReturn = mciSendCommand(wDeviceID, MCI_SET, MCI_SET_DOOR_CLOSED, (DWORD_PTR)NULL); if (dwReturn) Con_DPrintf("MCI_SET_DOOR_CLOSED failed (%i)\n", (int)dwReturn); } //try to add time values sensibly because: //a) microsoft api SUCKS and does not directly support frames. //b) microsoft buggily stops 3 frames short of the end of the track if we use tmsf... //c) frames added together will break things //d) we can subtract an offset so we can actually detect when its reached the end of a track //e) we need to do frames so we don't break if some track is exactly a multiple of a second long DWORD MSFToFrames(DWORD base) { int m = MCI_MSF_MINUTE(base); int s = MCI_MSF_SECOND(base); int f = MCI_MSF_FRAME(base); s += m*60; f += 75*s; //75 frames per second. return f; } DWORD FramesToMSF(DWORD in) { DWORD m, s, f; f = in % 75; in /= 75; s = in % 60; in /= 60; m = in; return MCI_MAKE_MSF(m, s, f); } void CDAudio_Play(int track) { DWORD dwReturn; static MCI_PLAY_PARMS mciPlayParms; static MCI_STATUS_PARMS mciStatusParms; DWORD trackstartposition; if (track < 1) { Con_DPrintf("CDAudio: Bad track number %u.\n", track); return; } // don't try to play a non-audio track mciStatusParms.dwItem = MCI_CDA_STATUS_TYPE_TRACK; mciStatusParms.dwTrack = track; mciStatusParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK | MCI_WAIT, (DWORD_PTR) (LPVOID) &mciStatusParms); if (dwReturn) { Con_DPrintf("MCI_STATUS failed (%i)\n", (int)dwReturn); return; } if (mciStatusParms.dwReturn != MCI_CDA_TRACK_AUDIO) { Con_Printf("CDAudio: track %i is not audio\n", track); return; } // get the start of the track to be played mciStatusParms.dwItem = MCI_STATUS_POSITION; mciStatusParms.dwTrack = track; mciStatusParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK | MCI_WAIT, (DWORD_PTR) (LPVOID) &mciStatusParms); if (dwReturn) { Con_DPrintf("MCI_STATUS failed (%i)\n", (int)dwReturn); return; } trackstartposition = mciStatusParms.dwReturn; // get the length of the track to be played mciStatusParms.dwItem = MCI_STATUS_LENGTH; mciStatusParms.dwTrack = track; mciStatusParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK | MCI_WAIT, (DWORD_PTR) (LPVOID) &mciStatusParms); if (dwReturn) { Con_DPrintf("MCI_STATUS failed (%i)\n", (int)dwReturn); return; } //set up to play from start to start+length mciPlayParms.dwFrom = trackstartposition; mciPlayParms.dwTo = resumeend = FramesToMSF(MSFToFrames(trackstartposition) + MSFToFrames(mciStatusParms.dwReturn) - 8); //-8 to avoid microsoft's potential fuck ups mciPlayParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_PLAY, MCI_NOTIFY | MCI_FROM | MCI_TO, (DWORD_PTR)(LPVOID) &mciPlayParms); if (dwReturn) { Con_DPrintf("CDAudio: MCI_PLAY failed (%i)\n", (int)dwReturn); return; } pollneeded = true; if (!bgmvolume.value) CDAudio_Pause (); return; } void CDAudio_Stop(void) { DWORD dwReturn; pollneeded = false; dwReturn = mciSendCommand(wDeviceID, MCI_STOP, 0, (DWORD_PTR)NULL); if (dwReturn) Con_DPrintf("MCI_STOP failed (%i)\n", (int)dwReturn); } void CDAudio_Pause(void) { DWORD dwReturn; static MCI_GENERIC_PARMS mciGenericParms; if (!pollneeded) return; mciGenericParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_PAUSE, 0, (DWORD_PTR)(LPVOID) &mciGenericParms); if (dwReturn) Con_DPrintf("MCI_PAUSE failed (%i)\n", (int)dwReturn); pollneeded = false; } void CDAudio_Resume(void) { DWORD dwReturn; static MCI_PLAY_PARMS mciPlayParms; if (!bgmvolume.value) return; mciPlayParms.dwFrom = resumeend; mciPlayParms.dwTo = resumeend; mciPlayParms.dwCallback = (DWORD_PTR)mainwindow; dwReturn = mciSendCommand(wDeviceID, MCI_PLAY, MCI_TO | MCI_NOTIFY, (DWORD_PTR)(LPVOID) &mciPlayParms); if (dwReturn) { Con_DPrintf("CDAudio: MCI_PLAY failed (%i)\n", (int)dwReturn); return; } pollneeded = true; } LONG CDAudio_MessageHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (lParam != wDeviceID) return 1; switch (wParam) { case MCI_NOTIFY_SUCCESSFUL: pollneeded = false; Media_EndedTrack(); break; case MCI_NOTIFY_ABORTED: case MCI_NOTIFY_SUPERSEDED: break; case MCI_NOTIFY_FAILURE: Con_DPrintf("MCI_NOTIFY_FAILURE\n"); CDAudio_Stop (); CDAudio_Shutdown(); // Media_EndedTrack(); break; default: Con_DPrintf("Unexpected MM_MCINOTIFY type (%i)\n", (int)wParam); return 1; } return 0; } void CDAudio_Update(void) { //workaround for vista bug where MCI_NOTIFY does not work to signal the end of the track. if (pollneeded) { MCI_STATUS_PARMS mciStatusParms; mciStatusParms.dwCallback = (DWORD_PTR)mainwindow; mciStatusParms.dwItem = MCI_STATUS_POSITION; mciStatusParms.dwReturn = resumeend; mciStatusParms.dwTrack = 0; if (0 == mciSendCommand(wDeviceID, MCI_STATUS, MCI_WAIT|MCI_STATUS_ITEM, (DWORD_PTR)&mciStatusParms)) { unsigned int c, f; int cm = MCI_MSF_MINUTE(mciStatusParms.dwReturn); int cs = MCI_MSF_SECOND(mciStatusParms.dwReturn); int cf = MCI_MSF_FRAME(mciStatusParms.dwReturn); int fm = MCI_MSF_MINUTE(resumeend); int fs = MCI_MSF_SECOND(resumeend); int ff = MCI_MSF_FRAME(resumeend); c = cf | (cs<<8) | (cm<<16); f = ff | (fs<<8) | (fm<<16); if (c >= f) { pollneeded = false; Media_EndedTrack(); } } } } void CDAudio_Init(void) { } #endif ================================================ FILE: engine/client/cdaudio.h ================================================ /* Copyright (C) 1996-1997 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. */ #ifdef HAVE_CDPLAYER void CDAudio_Init(void); qboolean CDAudio_Startup(void); //called when the cd isn't currently valid. returns if its valid or not. int CDAudio_GetAudioDiskInfo(void);//returns number of tracks available, or 0 if the cd is not valid. void CDAudio_Play(int track); void CDAudio_Stop(void); void CDAudio_Pause(void); void CDAudio_Resume(void); void CDAudio_Eject(void); void CDAudio_CloseDoor(void); void CDAudio_Shutdown(void); void CDAudio_Update(void); #else #define CDAudio_Update() #define CDAudio_Init() #define CDAudio_Shutdown() #define CDAudio_Pause() #define CDAudio_Resume() #endif ================================================ FILE: engine/client/cl_cam.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ /* ZOID * * Player camera tracking in Spectator mode * * This takes over player controls for spectator automatic camera. * Player moves as a spectator, but the camera tracks and enemy player */ #include "quakedef.h" #include "winquake.h" #define PM_SPECTATORMAXSPEED 500 #define PM_STOPSPEED 100 #define PM_MAXSPEED 320 #define BUTTON_JUMP 2 #define BUTTON_ATTACK 1 #define MAX_ANGLE_TURN 10 char cl_spectatorgroup[] = "Spectator Tracking"; enum { TM_USER, //user hit jump and changed pov explicitly TM_HIGHTRACK, //tracking the player with the highest frags TM_KILLER, //switch to the previous person's killer. TM_MODHINTS, //using the mod's //at hints TM_STATS //parsing mvd stats and making our own choices } autotrackmode; char *autotrack_statsrule; static void QDECL CL_AutoTrackChanged(cvar_t *v, char *oldval) { Cam_AutoTrack_Update(v->string); } // track high fragger cvar_t cl_autotrack_team = CVARD("cl_autotrack_team", "", "Specifies a team name that should be auto-tracked (players on other teams will not be candidates for autotracking). Accepts * and ? wildcards for awkward chars."); cvar_t cl_autotrack = CVARCD("cl_autotrack", "auto", CL_AutoTrackChanged, "Specifies the default tracking mode at the start of the map. Use the 'autotrack' command to reset/apply an auto-tracking mode without changing the default.\nValid values are: high, ^hkiller^h, mod, user. Other values are treated as weighting scripts for mvd playback, where available."); cvar_t cl_hightrack = CVARD("cl_hightrack", "0", "Obsolete. If you want hightrack, use '[cl_]autotrack high' instead."); //cvar_t cl_camera_maxpitch = {"cl_camera_maxpitch", "10" }; //cvar_t cl_camera_maxyaw = {"cl_camera_maxyaw", "30" }; cvar_t cl_chasecam = CVAR("cl_chasecam", "1"); cvar_t cl_selfcam = CVAR("cl_selfcam", "1"); void Cam_AutoTrack_Update(const char *mode) { if (!mode) mode = cl_autotrack.string; Z_Free(autotrack_statsrule); autotrack_statsrule = NULL; if (!*mode || !Q_strcasecmp(mode, "auto")) { if (cls.demoplayback == DPB_MVD) { autotrackmode = TM_STATS; autotrack_statsrule = Z_StrDup(""); //default } else if (cl_hightrack.ival) autotrackmode = TM_HIGHTRACK; else autotrackmode = TM_MODHINTS; } else if (!Q_strcasecmp(mode, "high")) autotrackmode = TM_HIGHTRACK; else if (!Q_strcasecmp(mode, "killer")) autotrackmode = TM_KILLER; else if (!Q_strcasecmp(mode, "mod")) autotrackmode = TM_MODHINTS; else if (!Q_strcasecmp(mode, "user") || !Q_strcasecmp(mode, "off")) autotrackmode = TM_USER; else if (!Q_strcasecmp(mode, "stats")) { autotrackmode = TM_STATS; autotrack_statsrule = NULL; } else { autotrackmode = TM_STATS; autotrack_statsrule = Z_StrDup(mode); } } static void Cam_AutoTrack_f(void) { if (*Cmd_Argv(1)) Cam_AutoTrack_Update(Cmd_Argv(1)); else Cam_AutoTrack_Update(NULL); } static float CL_TrackScoreProp(player_info_t *pl, char rule, float *weights) { #ifdef QUAKESTATS float r; #endif switch(rule) { case '.': //currently being tracked. combine with currently alive or something { int i; for (i = 0; i < cl.splitclients; i++) { //not valid if an earlier view is tracking it. if (Cam_TrackNum(&cl.playerview[i]) == pl-cl.players) return 1; } return 0; } #ifdef QUAKESTATS case 'a': //armour value return pl->statsf[STAT_ARMOR]; case 'h': return pl->statsf[STAT_HEALTH]; case 'A': //armour type if (pl->stats[STAT_ITEMS] & IT_ARMOR3) r = weights[10]; else if (pl->stats[STAT_ITEMS] & IT_ARMOR2) r = weights[9]; else if (pl->stats[STAT_ITEMS] & IT_ARMOR1) r = weights[8]; else r = 0; return r; case 'W': //best weapon r = 0; if (r < weights[0] && (pl->stats[STAT_ITEMS] & IT_AXE)) r = weights[0]; if (r < weights[1] && (pl->stats[STAT_ITEMS] & IT_SHOTGUN)) r = weights[1]; if (r < weights[2] && (pl->stats[STAT_ITEMS] & IT_SUPER_SHOTGUN)) r = weights[2]; if (r < weights[3] && (pl->stats[STAT_ITEMS] & IT_NAILGUN)) r = weights[3]; if (r < weights[4] && (pl->stats[STAT_ITEMS] & IT_SUPER_NAILGUN)) r = weights[4]; if (r < weights[5] && (pl->stats[STAT_ITEMS] & IT_GRENADE_LAUNCHER)) r = weights[5]; if (r < weights[6] && (pl->stats[STAT_ITEMS] & IT_ROCKET_LAUNCHER)) r = weights[6]; if (r < weights[7] && (pl->stats[STAT_ITEMS] & IT_LIGHTNING)) r = weights[7]; return r; case 'p': //powerups held r = 0; r += (pl->stats[STAT_ITEMS] & IT_INVISIBILITY)?weights[11]:0; r += (pl->stats[STAT_ITEMS] & IT_QUAD)?weights[12]:0; r += (pl->stats[STAT_ITEMS] & IT_INVULNERABILITY)?weights[13]:0; return r; #endif case 'f': //frags return pl->frags; // case 'F': //team frags // return 0; #ifdef QUAKEHUD case 'g': //deaths return Stats_GetDeaths(pl - cl.players); #endif case 'u': //userid return pl - cl.players; case 'c': //'current run time' case 'C': //'current run frags' case 'd': //'current run teamfrags' case 'I': //ssg grabs case 'j': //ng grabs case 'J': //sng grabs case 'k': //gl grabs case 'K': //gl lost case 'l': //rl grabs case 'L': //rl lost case 'm': //lg grabs case 'M': //lg lost case 'n': //mh grabs case 'N': //ga grabs case 'o': //ya grabs case 'O': //ra grabs case 'v': //average run time case 'V': //average run frags case 'w': //average run teamfrags case 'x': //ring grabs case 'X': //ring losts case 'y': //quad grabs case 'Y': //quad losts case 'z': //pent grabs default: return 0; } } #define PRI_TOP 3 #define PRI_LOGIC 3 #define PRI_ADD 2 #define PRI_MUL 1 #define PRI_VAL 0 static float CL_TrackScore(player_info_t *pl, char **rule, float *weights, int pri) { float l, r; char *s = *rule; while (*s == ' ' || *s == '\t') s++; if (!pri) { if (*s == '!') { s++; l = !CL_TrackScore(pl, &s, weights, pri); } if (*s == '(') { l = CL_TrackScore(pl, &s, weights, PRI_TOP); while (*s == ' ' || *s == '\t') s++; if (*s == ')') s++; } else if (*s == '%') { l = CL_TrackScoreProp(pl, *++s, weights); s++; } else if (*s == '#') { int i = strtoul(s+1, &s, 0); if (i >= 0 && i < MAX_CL_STATS) l = pl->statsf[i]; else l = 0; } else l = strtod(s, &s); while (*s == ' ' || *s == '\t') s++; } else l = CL_TrackScore(pl, &s, weights, pri-1); for (;;) { if (pri == PRI_MUL) { if (*s == '*') { s++; r = CL_TrackScore(pl, &s, weights, pri-1); l *= r; continue; } else if (*s == '/') { s++; r = CL_TrackScore(pl, &s, weights, pri-1); l /= r; continue; } } else if (pri == PRI_ADD) { if (*s == '+') { s++; r = CL_TrackScore(pl, &s, weights, pri-1); l += r; continue; } else if (*s == '-') { s++; r = CL_TrackScore(pl, &s, weights, pri-1); l -= r; continue; } } else if (pri == PRI_LOGIC) { if (*s == '|' && s[1] == '|') { s+=2; r = CL_TrackScore(pl, &s, weights, pri-1); l = l||r; continue; } else if (*s == '&' && s[1] == '&') { s+=2; r = CL_TrackScore(pl, &s, weights, pri-1); l = l&&r; continue; } else if (*s == '&') { s++; r = CL_TrackScore(pl, &s, weights, pri-1); l = (int)l&(int)r; continue; } else if (*s == '|') { s++; r = CL_TrackScore(pl, &s, weights, pri-1); l = (int)l|(int)r; continue; } else if (*s == '>' && s[1] == '=') { s+=2; r = CL_TrackScore(pl, &s, weights, pri-1); l = l>=r; continue; } else if (*s == '<' && s[1] == '=') { s+=2; r = CL_TrackScore(pl, &s, weights, pri-1); l = l<=r; continue; } else if (*s == '>') { s+=1; r = CL_TrackScore(pl, &s, weights, pri-1); l = l>r; continue; } else if (*s == '<') { s+=1; r = CL_TrackScore(pl, &s, weights, pri-1); l = l>r; continue; } else if (*s == '!' && s[1] == '=') { s+=2; r = CL_TrackScore(pl, &s, weights, pri-1); l = l!=r; continue; } else if (*s == '=' && s[1] == '=') { s+=2; r = CL_TrackScore(pl, &s, weights, pri-1); l = l!=r; continue; } } break; } *rule = s; return l; } static qboolean CL_MayAutoTrack(int seat, int player) { if (player < 0) return false; if (!cl.players[player].userid || !cl.players[player].name[0] || cl.players[player].spectator) return false; if (*cl_autotrack_team.string && !wildcmp(cl_autotrack_team.string, cl.players[player].team)) return false; for (seat--; seat >= 0; seat--) { //not valid if an earlier view is tracking it. if (Cam_TrackNum(&cl.playerview[seat]) == player) return false; } return true; } //returns the player with the highest frags static int CL_FindHighTrack(int seat, char *rule) { int j = -1; int i; float max, score; player_info_t *s; float weights[14]; char *p; qboolean instant; instant = (rule && *rule == '!'); if (instant) rule++; if (rule && *rule == '[') { rule++; weights[0] = strtod(rule, &rule); //axe weights[1] = strtod(rule, &rule); //shot weights[2] = strtod(rule, &rule); //sshot weights[3] = strtod(rule, &rule); //nail weights[4] = strtod(rule, &rule); //snail weights[5] = strtod(rule, &rule); //gren weights[6] = strtod(rule, &rule); //rock weights[7] = strtod(rule, &rule); //lg weights[8] = strtod(rule, &rule); //ga weights[9] = strtod(rule, &rule); //ya weights[10] = strtod(rule, &rule); //ra weights[11] = strtod(rule, &rule); //ring weights[12] = strtod(rule, &rule); //quad weights[13] = strtod(rule, &rule); //pent if (*rule == ']') rule++; } else { weights[0] = 1; //axe weights[1] = 2; //shot weights[2] = 3; //sshot weights[3] = 2; //nail weights[4] = 3; //snail weights[5] = 3; //gren weights[6] = 8; //rock weights[7] = 8; //lg weights[8] = 1; //ga weights[9] = 2; //ya weights[10] = 3; //ra weights[11] = 500; //ring weights[12] = 900; //quad weights[13] = 1000; //pent } if (!rule || !*rule) rule = "%a * %A + 50 * %W + %p + %f"; //set a default to the currently tracked player, to reuse the current player we're tracking if someone lower equalises. j = cl.playerview[seat].cam_spec_track; if (CL_MayAutoTrack(seat, j)) { p = rule; max = CL_TrackScore(&cl.players[j], &p, weights, PRI_TOP); } else { max = -9999; j = -1; } for (i = 0; i < cl.allocated_client_slots; i++) { s = &cl.players[i]; if (j == i) //this was our default. continue; if (!CL_MayAutoTrack(seat, i)) continue; if (cl.teamplay && seat && cl.playerview[0].cam_spec_track >= 0 && strcmp(cl.players[cl.playerview[0].cam_spec_track].team, s->team)) continue; //when using multiview, keep tracking the team p = rule; score = CL_TrackScore(s, &p, weights, PRI_TOP); if (score > max) { max = score; j = i; } } if (j == -1 && cl.teamplay && seat) { //do it again, but with the teamplay check inverted //fixme: should probably reduce seat count instead, at least in demos. for (i = 0; i < cl.allocated_client_slots; i++) { s = &cl.players[i]; if (j == i) //this was our default. continue; if (!CL_MayAutoTrack(seat, i)) continue; if (!(cl.teamplay && seat && cl.playerview[0].cam_spec_track >= 0 && strcmp(cl.players[cl.playerview[0].cam_spec_track].team, s->team))) continue; //when using multiview, keep tracking the team p = rule; score = CL_TrackScore(s, &p, weights, PRI_TOP); if (score > max) { max = score; j = i; } } } if (j != -1 && CL_MayAutoTrack(seat, cl.playerview[seat].cam_spec_track) && !instant) { i = cl.playerview[seat].cam_spec_track; //extra hacks to prevent 'random' switching mid-game #ifdef QUAKESTATS if ((cl.players[j].stats[STAT_ITEMS] ^ cl.players[i].stats[STAT_ITEMS]) & (IT_INVULNERABILITY|IT_QUAD)) ; //don't block if the players have different powerups else if ((cl.players[j].stats[STAT_ITEMS] & (IT_ROCKET_LAUNCHER|IT_LIGHTNING)) && !(cl.players[i].stats[STAT_ITEMS] & (IT_ROCKET_LAUNCHER|IT_LIGHTNING))) ; //don't block the switch if the new player has a decent weapon, and the guy we're tracking does not. else if ((cl.players[j].stats[STAT_ITEMS] & (IT_ROCKET_LAUNCHER|IT_INVULNERABILITY))==(IT_ROCKET_LAUNCHER|IT_INVULNERABILITY) && (cl.players[i].stats[STAT_ITEMS] & (IT_ROCKET_LAUNCHER|IT_INVULNERABILITY)) != (IT_ROCKET_LAUNCHER|IT_INVULNERABILITY)) ; //don't block if we're switching to someone with pent+rl from someone that does not. else #endif return cl.playerview[seat].cam_spec_track; } return j; } static int CL_AutoTrack_Choose(int seat) { int best = -1; if (autotrackmode == TM_KILLER) best = cl.autotrack_killer; if (autotrackmode == TM_MODHINTS && seat == 0 && cl.autotrack_hint >= 0) best = cl.autotrack_hint; if (autotrackmode == TM_STATS && cls.demoplayback == DPB_MVD) best = CL_FindHighTrack(seat, autotrack_statsrule); if (autotrackmode == TM_HIGHTRACK || best == -1) best = CL_FindHighTrack(seat, "%f"); //TM_USER should generally avoid autotracking cl.autotrack_killer = best; //killer should continue to track whatever is currently tracked until its changed by frag message parsing return best; } static int Cam_FindSortedPlayer(int number); int selfcam=1; static float vlen(vec3_t v) { return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); } // returns true if weapon model should be drawn in camera mode qboolean Cam_DrawViewModel(playerview_t *pv) { if (pv->spectator) { if (pv->cam_state == CAM_EYECAM && cl_chasecam.ival) return true; return false; } else { if (selfcam == 1 && !r_refdef.externalview) return true; return false; } } int Cam_TrackNum(playerview_t *pv) { if (pv->spectator && CAM_ISLOCKED(pv)) return pv->cam_spec_track; return -1; } void Cam_Unlock(playerview_t *pv) { if (pv->cam_state) { CL_SendSeatClientCommand(true, pv-cl.playerview, "ptrack"); pv->cam_state = CAM_FREECAM; pv->viewentity = (cls.demoplayback)?0:(pv->playernum+1); //free floating SCR_CenterPrint(pv-cl.playerview, NULL, true); Sbar_Changed(); //flashgrens suck pv->cshifts[CSHIFT_SERVER].percent = 0; Skin_FlushPlayers(); } } void Cam_Lock(playerview_t *pv, int playernum) { pv->cam_lastviewtime = -1000; //allow the wallcam to re-snap as soon as it can CL_SendSeatClientCommand(true, pv-cl.playerview, "ptrack %i", playernum); if (pv->cam_spec_track != playernum) { //flashgrens suck pv->cshifts[CSHIFT_SERVER].percent = 0; } pv->cam_spec_track = playernum; pv->cam_state = CAM_PENDING; pv->viewentity = (cls.demoplayback)?0:(pv->playernum+1); //free floating until actually locked SCR_CenterPrint(pv-cl.playerview, NULL, true); Skin_FlushPlayers(); if (cls.demoplayback == DPB_MVD) { memcpy(&pv->stats, cl.players[playernum].stats, sizeof(pv->stats)); // pv->cam_state = CAM_; // pv->viewentity = playernum+1; /* pv->cam_state = cl_chasecam.ival?CAM_EYECAM:CAM_PENDING; //instantly lock if the player is valid. pv->viewentity = playernum+1; */ #ifdef QUAKESTATS if (cls.z_ext & Z_EXT_VIEWHEIGHT) pv->viewheight = cl.players[playernum].statsf[STAT_VIEWHEIGHT]; #endif } Sbar_Changed(); #ifdef QWSKINS { int i; for (i = 0; i < cl.allocated_client_slots; i++) CL_NewTranslation(i); } #endif } trace_t Cam_DoTrace(vec3_t vec1, vec3_t vec2) { #if 0 memset(&pmove, 0, sizeof(pmove)); pmove.numphysent = 1; memset(&pmove.physents[0], 0, sizeof(physent_t)); VectorClear (pmove.physents[0].origin); pmove.physents[0].model = cl.worldmodel; #endif VectorCopy (vec1, pmove.origin); return PM_PlayerTrace(pmove.origin, vec2, MASK_PLAYERSOLID); } // Returns distance or 9999 if invalid for some reason static float Cam_TryFlyby(vec3_t selforigin, vec3_t playerorigin, vec3_t vec, qboolean checkvis) { vec3_t v; trace_t trace; float len; pmove.player_mins[0] = pmove.player_mins[1] = -16; pmove.player_mins[2] = -24; pmove.player_maxs[0] = pmove.player_maxs[1] = 16; pmove.player_maxs[2] = 32; VectorAngles(vec, NULL, v, true); VectorCopy (v, pmove.angles); VectorNormalize(vec); VectorMA(playerorigin, 800, vec, v); // v is endpos // fake a player move trace = Cam_DoTrace(playerorigin, v); if (/*trace.inopen ||*/ trace.inwater) return 9999; VectorCopy(trace.endpos, vec); VectorSubtract(trace.endpos, playerorigin, v); len = sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); if (len < 32 || len > 800) return 9999; if (checkvis) { VectorSubtract(trace.endpos, selforigin, v); len = sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); trace = Cam_DoTrace(selforigin, vec); if (trace.fraction != 1 || trace.inwater) return 9999; } return len; } // Is player visible? static qboolean Cam_IsVisible(vec3_t playerorigin, vec3_t vec) { trace_t trace; vec3_t v; float d; VectorClear(pmove.player_mins); VectorClear(pmove.player_maxs); trace = Cam_DoTrace(playerorigin, vec); if (trace.fraction != 1 || /*trace.inopen ||*/ trace.inwater) return false; // check distance, don't let the player get too far away or too close VectorSubtract(playerorigin, vec, v); d = vlen(v); if (d < 16) return false; return true; } static qboolean InitFlyby(playerview_t *pv, vec3_t selforigin, vec3_t playerorigin, vec3_t playerviewangles, int checkvis) { vec3_t dirs[] = { {1,1,1}, {1,-1,1}, {1,1,0}, {1,-1,1}, {1,0,1}, {1,0,-1}, {-1,1,1}, {-1,-1,1}, {-1,0,0}, {1,0,0}, {0,0,-1}, {0,0,1} }; int dir; float f, max; vec3_t vec, vec2; vec3_t forward, right, up; VectorCopy(playerviewangles, vec); vec[0] = 0; AngleVectors (vec, forward, right, up); // for (i = 0; i < 3; i++) // forward[i] *= 3; max = 1000; for (dir = 0; dir < sizeof(dirs)/sizeof(dirs[0]); dir++) { VectorScale(forward, dirs[dir][0], vec2); VectorMA(vec2, dirs[dir][1], right, vec2); VectorMA(vec2, dirs[dir][2], up, vec2); if ((f = Cam_TryFlyby(selforigin, playerorigin, vec2, checkvis)) < max) { max = f; VectorCopy(vec2, vec); } } // ack, can't find him if (max >= 1000) { // Cam_Unlock(); return false; } pv->cam_state = CAM_WALLCAM; pv->viewentity = pv->playernum+1;//pv->cam_spec_track+1; VectorCopy(vec, pv->cam_desired_position); return true; } static void Cam_CheckHighTarget(playerview_t *pv) { int j; playerview_t *spv; j = CL_AutoTrack_Choose(pv - cl.playerview); if (j >= 0) { if (pv->cam_spec_track != j || pv->cam_state == CAM_FREECAM) { #ifdef QUAKEHUD if (cl.teamplay) Stats_Message("Now tracking:\n%s\n%s", cl.players[j].name, cl.players[j].team); else Stats_Message("Now tracking:\n%s", cl.players[j].name); #endif Cam_Lock(pv, j); //un-lock any higher seats watching our new target. this keeps things ordered. for (spv = pv+1; spv >= cl.playerview && spv < &cl.playerview[cl.splitclients]; spv++) { if (Cam_TrackNum(spv) == j) spv->cam_state = CAM_FREECAM; } } } else Cam_Unlock(pv); } void Cam_SelfTrack(playerview_t *pv) { vec3_t vec; if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED) return; if (selfcam == 1) { //view-from-eyes } else { if (selfcam == 2) { //fixme: vec3_t forward, right, up; trace_t tr; AngleVectors(r_refdef.viewangles, forward, right, up); VectorMA(pv->simorg, -128, forward, pv->cam_desired_position); tr = Cam_DoTrace(pv->simorg, pv->cam_desired_position); VectorCopy(tr.endpos, pv->cam_desired_position); } else { //view from a random wall if (pv->cam_state != CAM_WALLCAM || !Cam_IsVisible(pv->simorg, pv->cam_desired_position)) { if (pv->cam_state != CAM_WALLCAM || realtime - pv->cam_lastviewtime > 0.1) { if (!InitFlyby(pv, pv->cam_desired_position, pv->simorg, pv->simangles, true)) InitFlyby(pv, pv->cam_desired_position, pv->simorg, pv->simangles, false); pv->cam_lastviewtime = realtime; } } else { pv->cam_lastviewtime = realtime; } //tracking failed. if (pv->cam_state != CAM_WALLCAM) return; } // move there locally immediately VectorCopy(pv->cam_desired_position, r_refdef.vieworg); VectorSubtract(pv->simorg, pv->cam_desired_position, vec); VectorAngles(vec, NULL, r_refdef.viewangles, false); } } //player entity became visible, lock on to them (now that we know where they are etc) void Cam_NowLocked(playerview_t *pv) { pv->cam_lastviewtime = realtime; if (!cl_chasecam.ival) { if (pv->cam_state != CAM_WALLCAM || !Cam_IsVisible(pv->simorg, pv->cam_desired_position)) { if (pv->cam_state != CAM_WALLCAM || realtime - pv->cam_lastviewtime > 0.1) { if (!InitFlyby(pv, pv->cam_desired_position, pv->simorg, pv->simangles, true)) InitFlyby(pv, pv->cam_desired_position, pv->simorg, pv->simangles, false); } } } } // ZOID // // Take over the user controls and track a player. // We find a nice position to watch the player and move there void Cam_Track(playerview_t *pv, usercmd_t *cmd) { player_state_t *player, *self; inframe_t *frame; vec3_t vec; float len; if (!pv->spectator || !cl.worldmodel) //can happen when the server changes level return; if (autotrackmode != TM_USER && pv->cam_state == CAM_FREECAM) Cam_CheckHighTarget(pv); if (pv->cam_state == CAM_FREECAM || cls.state != ca_active || cl.worldmodel->loadstate != MLS_LOADED) return; if (CAM_ISLOCKED(pv) && (!cl.players[pv->cam_spec_track].name[0] || cl.players[pv->cam_spec_track].spectator)) { pv->cam_state = CAM_FREECAM; if (autotrackmode != TM_USER) Cam_CheckHighTarget(pv); else Cam_Unlock(pv); return; } frame = &cl.inframes[cl.validsequence & UPDATE_MASK]; player = frame->playerstate + pv->cam_spec_track; if (!cmd) { //this is our fancy force-wallcam mode. if (pv->cam_state != CAM_WALLCAM || !Cam_IsVisible(player->origin, pv->cam_desired_position)) { if (!InitFlyby(pv, pv->cam_desired_position, player->origin, player->viewangles, true)) InitFlyby(pv, pv->cam_desired_position, player->origin, player->viewangles, false); } return; } self = frame->playerstate + pv->playernum; if (!cl_chasecam.value && (pv->cam_state != CAM_WALLCAM || !Cam_IsVisible(player->origin, pv->cam_desired_position))) { if (pv->cam_state != CAM_WALLCAM || realtime - pv->cam_lastviewtime > 0.1) { if (!InitFlyby(pv, self->origin, player->origin, player->viewangles, true)) InitFlyby(pv, self->origin, player->origin, player->viewangles, false); pv->cam_lastviewtime = realtime; } } else if (cl_chasecam.value && pv->cam_state == CAM_WALLCAM) pv->cam_state = CAM_PENDING; else { pv->cam_lastviewtime = realtime; } //tracking failed. if (pv->cam_state == CAM_FREECAM || pv->cam_state == CAM_PENDING) return; if (cl_chasecam.value) { float *neworg; // float *newang; if (pv->nolocalplayer) neworg = cl.lerpents[pv->viewentity].origin; else neworg = player->origin; pv->cam_lastviewtime = realtime; // VectorCopy(newang, pv->viewangles); if (memcmp(neworg, &self->origin, sizeof(vec3_t)) != 0) { if (!cls.demoplayback) { MSG_WriteByte (&cls.netchan.message, clc_tmove); MSG_WriteCoord (&cls.netchan.message, neworg[0]); MSG_WriteCoord (&cls.netchan.message, neworg[1]); MSG_WriteCoord (&cls.netchan.message, neworg[2]); } // move there locally immediately VectorCopy(neworg, self->origin); } self->weaponframe = player->weaponframe; VectorCopy(player->viewangles, pv->viewangles); return; } // Ok, move to our desired position and set our angles to view // the player VectorSubtract(pv->cam_desired_position, self->origin, vec); len = vlen(vec); cmd->forwardmove = cmd->sidemove = cmd->upmove = 0; if (len > 16) { // close enough? MSG_WriteByte (&cls.netchan.message, clc_tmove); MSG_WriteCoord (&cls.netchan.message, pv->cam_desired_position[0]); MSG_WriteCoord (&cls.netchan.message, pv->cam_desired_position[1]); MSG_WriteCoord (&cls.netchan.message, pv->cam_desired_position[2]); } // move there locally immediately VectorCopy(pv->cam_desired_position, self->origin); // VectorSubtract(player->origin, pv->cam_desired_position, vec); // VectorAngles(vec, NULL, pv->viewangles); // pv->viewangles[0] = -pv->viewangles[0]; } void Cam_SetModAutoTrack(int userid) { //this is a hint from the server about who to track int slot; cl.autotrack_hint = -1; for (slot = 0; slot < cl.allocated_client_slots; slot++) { if (cl.players[slot].userid == userid) { cl.autotrack_hint = slot; return; } } Con_Printf("//at: invalid userid %i\n", userid); } /*static void Cam_TrackCrosshairedPlayer(playerview_t *pv) { inframe_t *frame; player_state_t *player; int i; float dot = 0.1, bestdot=0; int best = -1; vec3_t selforg; vec3_t dir; frame = &cl.inframes[cl.validsequence & UPDATE_MASK]; player = frame->playerstate + pv->playernum; VectorCopy(player->origin, selforg); for (i = 0; i < cl.allocated_client_slots; i++) { player = frame->playerstate + i; VectorSubtract(player->origin, selforg, dir); VectorNormalize(dir); dot = DotProduct(vpn, dir); if (dot > bestdot) { bestdot = dot; best = i; } } // Con_Printf("Track %i? %f\n", best, bestdot); if (best > .707) //did we actually get someone? { Cam_Lock(pv, best); } }*/ void Cam_FinishMove(playerview_t *pv, usercmd_t *cmd) { int i; player_info_t *s; int end; extern cvar_t cl_demospeed; if (cls.state != ca_active) return; if (!pv->spectator && cls.demoplayback != DPB_MVD) // only in spectator mode return; if (cls.demoplayback == DPB_MVD) { int nb = 0; nb |= (cmd->sidemove<0)?4:0; nb |= (cmd->sidemove>0)?8:0; nb |= (cmd->forwardmove<0)?16:0; nb |= (cmd->forwardmove>0)?32:0; nb |= (cmd->upmove<0)?64:0; nb |= (cmd->upmove>0)?128:0; if (Cam_TrackNum(pv) >= 0) { if (*cls.lastdemoname) { //changing rates or jumping doesn't make sense with mvds. if (nb & (nb ^ pv->cam_oldbuttons) & 4) Cvar_SetValue(&cl_demospeed, max(cl_demospeed.value - 0.1, 0)); if (nb & (nb ^ pv->cam_oldbuttons) & 8) Cvar_SetValue(&cl_demospeed, min(cl_demospeed.value + 0.1, 10)); if (nb & (nb ^ pv->cam_oldbuttons) & (4|8)) Con_Printf("playback speed: %g%%\n", cl_demospeed.value*100); if (nb & (nb ^ pv->cam_oldbuttons) & 16) Cbuf_AddText("demo_jump +10", RESTRICT_LOCAL); if (nb & (nb ^ pv->cam_oldbuttons) & 32) Cbuf_AddText("demo_jump -10", RESTRICT_LOCAL); if (nb & (nb ^ pv->cam_oldbuttons) & (4|8)) Con_Printf("playback speed: %g%%\n", cl_demospeed.value*100); } if (nb & (nb ^ pv->cam_oldbuttons) & 64) Cvar_SetValue(&cl_splitscreen, max(cl_splitscreen.ival - 1, 0)); if (nb & (nb ^ pv->cam_oldbuttons) & 128) Cvar_SetValue(&cl_splitscreen, min(cl_splitscreen.ival + 1, MAX_SPLITS-1)); } pv->cam_oldbuttons = (pv->cam_oldbuttons & 3) | (nb & ~3); if (cmd->impulse) { int pl = cmd->impulse; #if 1 do { Cam_Lock(pv, Cam_FindSortedPlayer(pl)); pv++; pl++; } while (pv >= &cl.playerview[0] && pv < &cl.playerview[cl.splitclients]); #else for (i = 0; ; i++) { if (i == MAX_CLIENTS) { if (pl == cmd->impulse) break; i = 0; } s = &cl.players[i]; if (s->name[0] && !s->spectator) { pl--; if (!pl) { Cam_Lock(pv, i); if (pv >= &cl.playerview[0] && pv < &cl.playerview[cl.splitclients]) { pl = 1; pv++; } else break; } } } #endif return; } } if (cmd->buttons & BUTTON_ATTACK) { if (!(pv->cam_oldbuttons & BUTTON_ATTACK)) { pv->cam_oldbuttons |= BUTTON_ATTACK; if (pv->cam_state != CAM_FREECAM) { Cam_Unlock(pv); VectorCopy(pv->viewangles, cmd->angles); autotrackmode = TM_USER; return; } } else return; } else { pv->cam_oldbuttons &= ~BUTTON_ATTACK; if (pv->cam_state == CAM_FREECAM && autotrackmode == TM_USER) { // if ((cmd->buttons & BUTTON_JUMP) && !(pv->cam_oldbuttons & BUTTON_JUMP)) // Cam_TrackCrosshairedPlayer(pv); pv->cam_oldbuttons = (pv->cam_oldbuttons&~BUTTON_JUMP) | (cmd->buttons & BUTTON_JUMP); return; } } if (autotrackmode != TM_USER) { if ((cmd->buttons & BUTTON_JUMP) && !(pv->cam_oldbuttons & BUTTON_JUMP)) autotrackmode = TM_USER; else { Cam_CheckHighTarget(pv); return; } } if (pv->cam_state != CAM_FREECAM) { if ((cmd->buttons & BUTTON_JUMP) && (pv->cam_oldbuttons & BUTTON_JUMP)) return; // don't pogo stick if (!(cmd->buttons & BUTTON_JUMP)) { pv->cam_oldbuttons &= ~BUTTON_JUMP; return; } pv->cam_oldbuttons |= BUTTON_JUMP; // don't jump again until released } // Con_Printf("Selecting track target...\n"); if (pv->cam_state != CAM_FREECAM) end = (pv->cam_spec_track + 1) % MAX_CLIENTS; else end = pv->cam_spec_track; end = max(0, end) % cl.allocated_client_slots; i = end; do { s = &cl.players[i]; //players with userid 0 are typically bots. //trying to spectate such bots just does not work (we have no idea which entity slot the bot is actually using, so don't try to track them). if (s->name[0] && !s->spectator && s->userid) { Cam_Lock(pv, i); return; } i = (i + 1) % cl.allocated_client_slots; } while (i != end); // stay on same guy? i = pv->cam_spec_track; s = &cl.players[i]; if (s->name[0] && !s->spectator && s->userid) { Cam_Lock(pv, i); return; } Con_Printf("No target found ...\n"); pv->cam_state = CAM_FREECAM; } void Cam_Reset(void) { unsigned int pnum; for (pnum = 0; pnum < MAX_SPLITS; pnum++) { playerview_t *pv = &cl.playerview[pnum]; pv->cam_state = CAM_FREECAM; pv->cam_spec_track = 0; } } static int QDECL Cam_SortPlayers(const void *p1,const void *p2) { const player_info_t *a = *(player_info_t**)p1; const player_info_t *b = *(player_info_t**)p2; if (!*a->team) return *b->team; if (!*b->team) return -*a->team; // if (a->spectator || b->spectator) // return a->spectator - b->spectator; return Q_strcasecmp(a->team, b->team); } static int Cam_FindSortedPlayer(int number) { player_info_t *playerlist[MAX_CLIENTS], *s; int i; int slots; number--; for (slots = 0, i = 0; i < cl.allocated_client_slots; i++) { s = &cl.players[i]; if (s->name[0] && !s->spectator) playerlist[slots++] = s; } if (!slots) return 0; // if (number > slot) // return cl.allocated_client_slots; //error qsort(playerlist, slots, sizeof(playerlist[0]), Cam_SortPlayers); return playerlist[number % slots] - cl.players; } void Cam_TrackPlayer(int seat, char *cmdname, char *plrarg) { playerview_t *pv = &cl.playerview[seat]; int slot; player_info_t *s; char *e; if (seat >= MAX_SPLITS) return; if (cls.state <= ca_connected) { Con_Printf("Not connected.\n"); return; } if (!pv->spectator) { Con_Printf("Not spectating.\n"); return; } if (!Q_strcasecmp(plrarg, "off")) { Cam_Unlock(pv); return; } if (*plrarg == '#' && (slot=strtoul(plrarg+1, &e, 10)) && !*e) slot = Cam_FindSortedPlayer(slot); else { // search nicks first for (slot = 0; slot < cl.allocated_client_slots; slot++) { s = &cl.players[slot]; if (s->name[0] && !s->spectator && !Q_strcasecmp(s->name, plrarg)) break; } } if (slot == cl.allocated_client_slots) { // didn't find nick, so search userids int userid; char *c; // check if given arg is in fact a number c = plrarg; while (*c) { if (*c < '0' || *c > '9') { Con_Printf("Couldn't find nick %s\n", plrarg); return; } c++; } userid = atoi(plrarg); for (slot = 0; slot < cl.allocated_client_slots; slot++) { s = &cl.players[slot]; if (s->name[0] && !s->spectator && s->userid == userid) break; } if (slot == cl.allocated_client_slots) { Con_Printf("Couldn't find userid %i\n", userid); return; } } Cam_Lock(pv, slot); } void Cam_Track_f(void) { int i, j; if (Cmd_Argc() < 2) { Con_Printf("Usage: %s userid|nick|off\n", Cmd_Argv(0)); return; } i = 1; j = Cmd_Argc() - 1; if (j > MAX_SPLITS) j = MAX_SPLITS; while (j > 0) { Cam_TrackPlayer(i - 1, Cmd_Argv(0), Cmd_Argv(i)); i++; j--; } } void Cam_Track1_f(void) { if (Cmd_Argc() < 2) { Con_Printf("Usage: %s userid|nick|off\n", Cmd_Argv(0)); return; } Cam_TrackPlayer(0, Cmd_Argv(0), Cmd_Argv(1)); } void Cam_Track2_f(void) { if (Cmd_Argc() < 2) { Con_Printf("Usage: %s userid|nick|off\n", Cmd_Argv(0)); return; } Cam_TrackPlayer(1, Cmd_Argv(0), Cmd_Argv(1)); } void Cam_Track3_f(void) { if (Cmd_Argc() < 2) { Con_Printf("Usage: %s userid|nick|off\n", Cmd_Argv(0)); return; } Cam_TrackPlayer(2, Cmd_Argv(0), Cmd_Argv(1)); } void Cam_Track4_f(void) { if (Cmd_Argc() < 2) { Con_Printf("Usage: %s userid|nick|off\n", Cmd_Argv(0)); return; } Cam_TrackPlayer(3, Cmd_Argv(0), Cmd_Argv(1)); } void CL_InitCam(void) { Cvar_Register (&cl_autotrack, cl_spectatorgroup); Cvar_Register (&cl_autotrack_team, cl_spectatorgroup); Cvar_Register (&cl_hightrack, cl_spectatorgroup); Cvar_Register (&cl_chasecam, cl_spectatorgroup); // Cvar_Register (&cl_camera_maxpitch, cl_spectatorgroup); // Cvar_Register (&cl_camera_maxyaw, cl_spectatorgroup); // Cvar_Register (&cl_selfcam, cl_spectatorgroup); Cmd_AddCommand("autotrack", Cam_AutoTrack_f); //reset it, if they jumped or something. Cmd_AddCommand("track", Cam_Track_f); Cmd_AddCommand("track1", Cam_Track1_f); Cmd_AddCommand("track2", Cam_Track2_f); Cmd_AddCommand("track3", Cam_Track3_f); Cmd_AddCommand("track4", Cam_Track4_f); } ================================================ FILE: engine/client/cl_demo.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ #include "quakedef.h" #include "fs.h" void CL_FinishTimeDemo (void); float demtime; float recdemostart; //keyed to Sys_DoubleTime int demoframe; int cls_lastto; static int cls_lasttype; void CL_PlayDemo(char *demoname, qboolean usesystempath); void CL_PlayDemoFile(vfsfile_t *f, char *demoname, qboolean issyspath); extern cvar_t qtvcl_forceversion1; extern cvar_t qtvcl_eztvextensions; extern cvar_t record_flush; static unsigned char demobuffer[1024*66]; //input buffer static int demooffset; //start offset of demo buffer static int demobuffersize; //number of valid bytes within the buffer static int demopreparsedbytes; //number of bytes within the valid buffer that has already been pre-parsed. qboolean disablepreparse; qboolean endofdemo; #define BUFFERTIME 0.5 /* ============================================================================== DEMO CODE When a demo is playing back, all NET_SendMessages are skipped, and NET_GetMessages are read from the demo file. Whenever cl.time gets past the last received message, another message is read from the demo file. ============================================================================== */ /* ============== CL_StopPlayback Called when a demo file runs out, or the user starts a game ============== */ void CL_StopPlayback (void) { if (!cls.demoplayback) return; Media_CaptureDemoEnd(); if (cls.demoinfile) VFS_CLOSE (cls.demoinfile); cls.demoinfile = NULL; cls.state = ca_disconnected; cls.demoplayback = DPB_NONE; cls.demoseeking = DEMOSEEK_NOT; //just in case cls.demotrack = -1; cls.demoeztv_ext = 0; if (cls.timedemo) CL_FinishTimeDemo (); TP_ExecTrigger("f_demoend", true); } /* ==================== CL_WriteDemoCmd Writes the player0 user cmd (demos don't support split screen) ==================== */ void CL_WriteDemoCmd (usercmd_t *pcmd) { int i; float fl; qbyte c; q1usercmd_t cmd; //nq doesn't have this info if (cls.demorecording != DPB_QUAKEWORLD) return; //Con_Printf("write: %ld bytes, %4.4f\n", msg->cursize, demtime); fl = LittleFloat(Sys_DoubleTime()-recdemostart); VFS_WRITE (cls.demooutfile, &fl, sizeof(fl)); c = dem_cmd; VFS_WRITE (cls.demooutfile, &c, sizeof(c)); // correct for byte order, bytes don't matter cmd.buttons = pcmd->buttons; cmd.impulse = pcmd->impulse; cmd.msec = pcmd->msec; for (i = 0; i < 3; i++) cmd.angles[i] = LittleShort((pcmd->angles[i]*360.0)/65535); cmd.forwardmove = LittleShort(pcmd->forwardmove); cmd.sidemove = LittleShort(pcmd->sidemove); cmd.upmove = LittleShort(pcmd->upmove); VFS_WRITE (cls.demooutfile, &cmd, sizeof(cmd)); for (i=0 ; i<3 ; i++) { fl = LittleFloat (cl.playerview[0].viewangles[i]); VFS_WRITE (cls.demooutfile, &fl, 4); } if (record_flush.ival) VFS_FLUSH (cls.demooutfile); } /* ==================== CL_WriteDemoMessage Dumps the current net message, prefixed by the length and view angles ==================== */ void CL_WriteDemoMessage (sizebuf_t *msg, int payloadoffset) { int len; int i; float fl; qbyte c; //Con_Printf("write: %ld bytes, %4.4f\n", msg->cursize, demtime); switch (cls.demorecording) { default: return; case DPB_QUAKEWORLD: //QW fl = LittleFloat(Sys_DoubleTime()-recdemostart); VFS_WRITE (cls.demooutfile, &fl, sizeof(fl)); c = dem_read; VFS_WRITE (cls.demooutfile, &c, sizeof(c)); if (*(int*)msg->data == -1 && payloadoffset==0) { //connectionless packet. len = LittleLong (msg->cursize); VFS_WRITE (cls.demooutfile, &len, 4); VFS_WRITE (cls.demooutfile, msg->data + payloadoffset, msg->cursize - payloadoffset); } else { //regenerate a legacy netchan. no fragmentation support, but whatever. this ain't udp. //the length len = LittleLong (msg->cursize - payloadoffset + 8); VFS_WRITE (cls.demooutfile, &len, 4); //hack the netchan here. i = cls.netchan.incoming_sequence; VFS_WRITE (cls.demooutfile, &i, 4); i = cls.netchan.incoming_acknowledged; VFS_WRITE (cls.demooutfile, &i, 4); //and the data VFS_WRITE (cls.demooutfile, msg->data + payloadoffset, msg->cursize - payloadoffset); } break; #ifdef Q2CLIENT case DPB_QUAKE2: len = LittleLong (net_message.cursize - payloadoffset); VFS_WRITE(cls.demooutfile, &len, sizeof(len)); VFS_WRITE(cls.demooutfile, net_message.data + payloadoffset, net_message.cursize - payloadoffset); break; #endif #ifdef NQPROT case DPB_NETQUAKE: //NQ len = LittleLong (net_message.cursize - payloadoffset); VFS_WRITE(cls.demooutfile, &len, sizeof(len)); for (i=0 ; i<3 ; i++) { float f = LittleFloat (cl.playerview[0].viewangles[i]); VFS_WRITE(cls.demooutfile, &f, sizeof(f)); } VFS_WRITE(cls.demooutfile, net_message.data + payloadoffset, net_message.cursize - payloadoffset); break; #endif } if (record_flush.ival) VFS_FLUSH (cls.demooutfile); } int demo_preparsedemo(unsigned char *buffer, int bytes) { int parsed = 0; int ofs; unsigned int length; #define dem_mask 7 if (cls.demoplayback != DPB_MVD) return bytes; //no need if its not an mvd (this simplifies it a little) while (bytes>2) { switch(buffer[1]&dem_mask) { case dem_cmd: ofs = -(int)(sizeof(q1usercmd_t)); ofs = 0; break; case dem_set: ofs = -(8); break; case dem_multiple: ofs = 6; break; default: ofs = 2; break; } if (ofs > 0) { if (ofs+4 > bytes) break; length = (buffer[ofs+0]<<0) + (buffer[ofs+1]<<8) + (buffer[ofs+2]<<16) + (buffer[ofs+3]<<24); if (length > MAX_OVERALLMSGLEN) { disablepreparse = true; Con_Printf("Error looking ahead at demo\n"); return parsed; } ofs+=4; } else { length = -ofs; ofs = 2; } //ofs is now the offset of the data if (ofs+length > bytes) { return parsed; //not got it all } if ((buffer[1]&dem_mask) == dem_all && (buffer[1] & ~dem_mask) && length < MAX_OVERALLMSGLEN) { net_message.cursize = length; memcpy(net_message.data, buffer+ofs, length); MSG_BeginReading(&net_message, cls.netchan.netprim); CLQW_ParseServerMessage(); } parsed += ofs+length; buffer += ofs+length; bytes -= ofs+length; } return parsed; } int readdemobytes(int *readpos, void *data, int len) { int i; int trybytes; if (len < 0) Host_EndGame("Corrupt demo"); //if there's not enough space in the buffer, flush it now and allow grabbing a new chunk //try to ensure it happens periodically enough for the preparsing stuff to happen early. if (demooffset+*readpos+len > demobuffersize || demooffset > sizeof(demobuffer)/2) { memmove(demobuffer, demobuffer+demooffset, demobuffersize); demooffset = 0; } if (demopreparsedbytes < 0) //won't happen in normal running, but can still happen on corrupt data... if we don't disconnect first. { Con_Printf("reset preparsed (underflow)\n"); demopreparsedbytes = 0; } if (demopreparsedbytes > demobuffersize) { Con_Printf("reset preparsed (overflow)\n"); demopreparsedbytes = 0; } trybytes = sizeof(demobuffer)-demooffset-demobuffersize; if (trybytes > 4096) i = VFS_READ(cls.demoinfile, demobuffer+demooffset+demobuffersize, trybytes); else i = 0; if (i > 0) { demobuffersize += i; if (disablepreparse) demopreparsedbytes = demobuffersize; else demopreparsedbytes += demo_preparsedemo(demobuffer+demooffset+demopreparsedbytes, demobuffersize-demopreparsedbytes); } if (*readpos+len > demobuffersize) { if (i < 0 || (i == 0 && len && cls.demoinfile->seekstyle < SS_SLOW)) { //0 means no data available yet, don't error on that (unless we can seek, in which case its not a stream and we won't get any more data later on). endofdemo = true; return 0; } // len = demobuffersize; return 0; } memcpy(data, demobuffer+demooffset+*readpos, len); *readpos += len; return len; } void demo_flushbytes(int bytes) { if (demooffset+bytes > demobuffersize) Sys_Error("demo_flushbytes: flushed too much!\n"); demooffset += bytes; demobuffersize -= bytes; if (demopreparsedbytes < bytes) demopreparsedbytes = 0; else demopreparsedbytes -= bytes; } void demo_flushcache(void) { demooffset = 0; demobuffersize = 0; demopreparsedbytes = 0; //no errors yet disablepreparse = false; } void demo_resetcache(int bytes, void *data) { endofdemo = false; demo_flushcache(); demooffset = 0; demobuffersize = bytes; demopreparsedbytes = 0; memcpy(demobuffer, data, bytes); //preparse it now bytes = 0; readdemobytes(&bytes, NULL, 0); } void CL_ProgressDemoTime(void) { extern cvar_t cl_demospeed; if (cl.parsecount && Media_PausedDemo(true)) { //console visible whilst democapturing cls.netchan.last_received = realtime; return; } if (cl.demopausedtilltime >= realtime) return; cl.demopausedtilltime = 0; cl.demonudge = 0; if (cl_demospeed.value >= 0 && cls.state == ca_active) demtime += host_frametime*cl_demospeed.value; else demtime += host_frametime; } void CL_DemoJump_f(void) { float newtime; char *s = (!strncmp(Cmd_Argv(0), "demo_jump_", 10))?Cmd_Argv(0)+10:Cmd_Argv(1); char *colon = strchr(s, ':'); if (!cls.demoplayback) { Con_Printf("not playing a demo, cannot jump.\n"); return; } if (Cmd_Argc() < 2) { Con_Printf("current time %.1f.\n", demtime); return; } if (!*cls.lastdemoname) { Con_Printf("unable to seak in qtv streams.\n"); return; //can't seek live streams... } if (!strcmp(s, "intermission") || !strcmp(s, "end")) { //seeks until we see an svc_intermission cls.demoseeking = DEMOSEEK_INTERMISSION; return; } if (!strcmp(s, "mark")) { //seeks until we see an svc_stufftext `//demomark` cls.demoseeking = DEMOSEEK_MARK; return; } if (*s == '+' || *s == '-') { if (colon) { colon++; newtime = demtime + atoi(colon) + atoi(s)*60; } else newtime = demtime + atoi(s); } else { //absolute seek time if (colon) { colon++; newtime = atoi(colon); newtime += atoi(s)*60; } else newtime = atoi(s); } if (newtime < 0) newtime = 0; if (newtime >= demtime) cls.demoseektime = newtime; else { vfsfile_t *df = cls.demoinfile; Con_Printf("Rewinding demo\n"); if (df->seekstyle != SS_UNSEEKABLE) { VFS_SEEK(df, 0); cls.demoinfile = NULL; CL_PlayDemoFile(df, cls.lastdemoname, cls.lastdemowassystempath); } else CL_PlayDemo(cls.lastdemoname, cls.lastdemowassystempath); //now fastparse it. cls.demoseektime = newtime; } cls.demoseeking = DEMOSEEK_TIME; } void CL_DemoNudge_f(void) { extern cvar_t cl_demospeed; int move = atoi(Cmd_Argv(1)); int newnudge; if (!cls.demoplayback) { Con_Printf("not playing a demo, cannot nudge.\n"); return; } if (!*cls.lastdemoname) { Con_Printf("unable to seak in qtv streams.\n"); return; //can't seek live streams... } if (!move) move = 1; newnudge = cl.demonudge + move; if (newnudge <= -(int)countof(cl.inframes)) newnudge = 1-(int)countof(cl.inframes); if (newnudge < 0) { //if we're nudging to a past frame, make sure that its actually valid. for(;-(int)countof(cl.inframes) < newnudge && newnudge < 0;) { int i = cls.netchan.incoming_sequence+newnudge; if (i < 0) break; if (cl.inframes[i&UPDATE_MASK].frameid == i && !cl.inframes[i&UPDATE_MASK].invalid) { cl.demonudge = newnudge; break; } if (move < 0) newnudge--; else newnudge++; } if (!newnudge) cl.demonudge = newnudge; } else cl.demonudge = newnudge; cl.demopausedtilltime = realtime + 3; } /* ==================== CL_GetDemoMessage FIXME... ==================== */ vec3_t demoangles; float olddemotime = 0; float nextdemotime = 0; qboolean CL_GetDemoMessage (void) { int r, i, j, tracknum; float f; float demotime; qbyte c, msecsadded=0; usercmd_t *pcmd; q1usercmd_t q1cmd; int demopos = 0; int msglength; static float throttle; static qboolean newseq; if (endofdemo) { endofdemo = false; CL_StopPlayback (); CL_NextDemo(); return 0; } #ifdef NQPROT if (cls.demoplayback == DPB_NETQUAKE #ifdef Q2CLIENT || cls.demoplayback == DPB_QUAKE2 #endif ) { //read the nq demo //if we've finished reading the connection part of the demo, but not finished loading, pause the demo if (cls.signon == 1 && !cl.worldmodel) { demtime = cl.gametime; return 0; } //if this is the starting frame of a timedemo if (cls.timedemo) if (cls.td_startframe == -1 && cls.state == ca_active) { //start the timer only once we are connected. //make sure everything is loaded, to avoid stalls Menu_PopAll(); COM_WorkerFullSync(); cls.td_starttime = Sys_DoubleTime(); cls.td_startframe = host_framecount; //force the console up, we're done loading. Key_Dest_Remove(kdm_console); scr_con_current = 0; } #ifdef Q2CLIENT //q2 uses a fixed 10fps packet rate, even demos enforce it. if (cls.demoplayback == DPB_QUAKE2) { if (cls.timedemo) { if (demoframe == host_framecount) return 0; demoframe = host_framecount; } #if 1 else if (demtime < cl.gametime && cl.gametime) { if (demtime <= cl.gametime-1) demtime = cl.gametime; return 0; } #else else if (cls.netchan.last_received == realtime || cls.netchan.last_received > realtime-0.1) return 0; #endif } else #endif if (cls.demoplayback == DPB_NETQUAKE && cls.signon == 4/*SIGNONS*/) { /*if (!demtime) { cl.gametime = 0; cl.gametimemark = demtime; olddemotime = 0; return 0; }*/ cls.netchan.last_received = realtime; if (cls.demoseeking == DEMOSEEK_TIME) { if (cl.gametime > cls.demoseektime) { cls.demoseeking = DEMOSEEK_NOT; return 0; } } else if (cl.demonudge > 0) cl.demonudge--; else if ((cls.timedemo && host_framecount == demoframe) || (!cls.timedemo && demtime < cl.gametime && cl.gametime))// > dem_lasttime+demtime) { if (demtime <= cl.gametime-1) demtime = cl.gametime; return 0; } demoframe = host_framecount; } else if (cls.signon < 4) demtime = 0; if (readdemobytes(&demopos, &msglength, 4) != 4) { return 0; } msglength = LittleLong (msglength); if (msglength == -1) { int tmppos = demopos; //q2 writes a length of -1 to mark eof. if this peek fails then it really is eof and we shouldn't fail from weird message length checks. if (readdemobytes(&tmppos, &msglength, 4) != 4) { endofdemo = true; return 0; } } if (cls.demoplayback == DPB_NETQUAKE) { for (i=0 ; i<3 ; i++) { readdemobytes(&demopos, &f, 4); demoangles[i] = LittleFloat (f); } } olddemotime = demtime; if (msglength > net_message.maxsize) { Con_Printf ("Demo message > MAX_MSGLEN\n"); CL_StopPlayback (); return 0; } if (readdemobytes(&demopos, net_message.data, msglength) != msglength) { return 0; } demo_flushbytes(demopos); NET_UpdateRates(cls.sockets, true, msglength); //keep any rate calcs sane net_message.cursize = msglength; return 1; } #endif readnext: if (demopos) { demo_flushbytes(demopos); demopos = 0; } // read the time from the packet if (cls.demoplayback == DPB_MVD) { if (demtime < 0) { readdemobytes(&demopos, NULL, 0); //keep it feeding through return 0; } if (olddemotime > demtime) olddemotime = demtime; if (demtime + 1.0 < olddemotime) demtime = olddemotime - 1.0; if (readdemobytes(&demopos, &msecsadded, sizeof(msecsadded)) != sizeof(msecsadded)) { Con_ThrottlePrintf(&throttle, 1, "Not enough buffered\n"); olddemotime = demtime+1; demotime = olddemotime; nextdemotime = demotime; return 0; } else { demotime = olddemotime + msecsadded*(1.0f/1000); nextdemotime = demotime; } } else if (cls.demoplayback == DPB_QUAKEWORLD) { if (readdemobytes(&demopos, &demotime, sizeof(demotime)) != sizeof(demotime)) { Con_ThrottlePrintf(&throttle, 1, "Not enough buffered\n"); olddemotime = demtime; //if we ran out of buffered demo, delay the demo parsing a little return 0; } demotime = LittleFloat(demotime); } else return 0; if (cl.sendprespawn) { CL_RequestNextDownload(); if (!cls.timedemo) return 0; } // decide if it is time to grab the next message if (cls.demoseeking != DEMOSEEK_NOT) { demtime = demotime; //warp if (cls.demoseeking == DEMOSEEK_TIME && demtime >= cls.demoseektime) cls.demoseeking = DEMOSEEK_NOT; } else if (cls.timedemo) { if (cls.td_lastframe < 0) cls.td_lastframe = demotime; else if (demotime > cls.td_lastframe) { cls.td_lastframe = demotime; return 0; // next packet starts after the previous one. draw something before parsing it. } if (cls.td_startframe == -1 && cls.state == ca_active) { //start the timer only once we are connected. cls.td_starttime = Sys_DoubleTime(); cls.td_startframe = host_framecount; //force the console up, we're done loading. Key_Dest_Remove(kdm_console); scr_con_current = 0; } if (cls.td_startframe == host_framecount+1) cls.td_starttime = Sys_DoubleTime(); demtime = demotime; // warp } else if (cl.demonudge > 0) { cl.demonudge--; demtime = demotime; // warp } else if (!(cl.paused&~4) && cls.state >= ca_onserver) { // always grab until fully connected if (demtime + 1.0 < demotime) { // too far back demtime = demotime - 1.0; return 0; } else if (demtime < demotime) { return 0; // don't need another message yet } } else demtime = demotime; // we're warping if (cls.demoplayback == DPB_MVD) { if ((msecsadded || cls.netchan.incoming_sequence < 2) && olddemotime != demotime) { newseq = true; cls.netchan.frame_latency = 0; cls.netchan.last_received = realtime; // just to happy timeout check } } if (cls.state < ca_demostart) Host_Error ("CL_GetDemoMessage: cls.state != ca_active"); // get the msg type if (readdemobytes (&demopos, &c, sizeof(c)) != sizeof(c)) { Con_ThrottlePrintf(&throttle, 1, "Not enough buffered\n"); olddemotime = demtime+1; return 0; } switch (c&7) { case dem_cmd : if (cls.demoplayback == DPB_MVD) { Con_Printf("mvd demos/qtv streams should not contain dem_cmd\n"); olddemotime = demtime+1; CL_StopPlayback (); return 0; } else { // user sent input i = cl.movesequence & UPDATE_MASK; pcmd = &cl.outframes[i].cmd[0]; r = readdemobytes (&demopos, &q1cmd, sizeof(q1cmd)); if (r != sizeof(q1cmd)) { Con_ThrottlePrintf(&throttle, 1, "Not enough buffered\n"); olddemotime = demtime+1; CL_StopPlayback (); return 0; } // byte order stuff for (j = 0; j < 3; j++) { q1cmd.angles[j] = LittleFloat(q1cmd.angles[j]); pcmd->angles[j] = ((int)(q1cmd.angles[j]*65536.0/360)&65535); } pcmd->forwardmove = q1cmd.forwardmove = LittleShort(q1cmd.forwardmove); pcmd->sidemove = q1cmd.sidemove = LittleShort(q1cmd.sidemove); pcmd->upmove = q1cmd.upmove = LittleShort(q1cmd.upmove); pcmd->msec = q1cmd.msec; pcmd->buttons = q1cmd.buttons; cl.outframes[i].senttime = realtime; cl.outframes[i].server_message_num = cl.validsequence; cl.outframes[i].cmd_sequence = cl.movesequence; cls.netchan.outgoing_sequence++; cl.movesequence = cls.netchan.outgoing_sequence; for (i=0 ; i<3 ; i++) { readdemobytes (&demopos, &f, 4); demoangles[i] = LittleFloat (f); cl.playerview[0].viewangles[i] = LittleFloat (f); } goto readnext; } break; case dem_read: readit: // get the next message if (readdemobytes (&demopos, &msglength, 4) != 4) { Con_ThrottlePrintf(&throttle, 1, "Not enough buffered\n"); olddemotime = demtime+1; return 0; } msglength = LittleLong (msglength); //Con_Printf("read: %ld bytes\n", msglength); if ((unsigned int)msglength > MAX_OVERALLMSGLEN) { Con_Printf ("Demo message > MAX_OVERALLMSGLEN\n"); CL_StopPlayback (); return 0; } if (readdemobytes (&demopos, net_message.data, msglength) != msglength) { Con_ThrottlePrintf(&throttle, 1, "Not enough buffered\n"); olddemotime = demtime+1; return 0; } NET_UpdateRates(cls.sockets, true, msglength); //keep any rate calcs sane net_message.cursize = msglength; if (cls.demoplayback == DPB_MVD) { int seat; cl.defaultnetsplit = 0; switch(cls_lasttype) { case dem_multiple: if (!cls_lastto && (cls.ezprotocolextensions1 & EZPEXT1_HIDDEN_MESSAGES)) { //an 'mvdsv hidden message' packet. MSG_BeginReading(&net_message, cls.netchan.netprim); CLEZ_ParseHiddenDemoMessage(); olddemotime = demotime; goto readnext; } for (seat = 0; seat < cl.splitclients; seat++) { tracknum = cl.playerview[seat].cam_spec_track; if (cl.playerview[seat].cam_state == CAM_FREECAM) tracknum = -1; if (tracknum == -1 || !(cls_lastto & (1 << tracknum))) continue; } if (seat == cl.splitclients) { olddemotime = demotime; goto readnext; } cl.defaultnetsplit = seat; break; case dem_single: { int maxseat = cl.splitclients; //only accept single messages that are directed to the first player view. //this is too problematic otherwise (apparently mvdsv doesn't use dem_multiple for team says any more). if (1) maxseat = 1; for (seat = 0; seat < maxseat; seat++) { tracknum = cl.playerview[seat].cam_spec_track; if (cl.playerview[seat].cam_state == CAM_FREECAM) tracknum = -1; if (tracknum == -1 || (cls_lastto != tracknum)) continue; break; } if (seat == maxseat) { olddemotime = demotime; goto readnext; } cl.defaultnetsplit = seat; } break; case dem_all: if (c & ~dem_mask) { olddemotime = demotime; goto readnext; } break; } } break; case dem_set: if (readdemobytes (&demopos, &j, 4) != 4) { olddemotime = demtime; return 0; } if (readdemobytes (&demopos, &i, 4) != 4) { olddemotime = demtime; return 0; } cls.demostarttime = demtime; cls.netchan.outgoing_sequence = LittleLong(j); cls.netchan.incoming_sequence = LittleLong(i); cl.movesequence = cls.netchan.outgoing_sequence; NET_UpdateRates(cls.sockets, false, demopos); //keep any rate calcs sane if (cls.demoplayback == DPB_MVD) cls.netchan.incoming_acknowledged = cls.netchan.incoming_sequence; goto readnext; case dem_multiple: if (readdemobytes (&demopos, &i, sizeof(i)) != sizeof(i)) { olddemotime = demtime; return 0; } cls_lastto = LittleLong(i); cls_lasttype = dem_multiple; goto readit; case dem_single: cls_lastto = c >> 3; cls_lasttype = dem_single; goto readit; case dem_stats: cls_lastto = c >> 3; cls_lasttype = dem_stats; goto readit; case dem_all: cls_lastto = 0; cls_lasttype = dem_all; goto readit; default : Con_Printf("Corrupted demo.\n"); CL_StopPlayback (); return 0; } demo_flushbytes(demopos); if (cls.demoplayback == DPB_MVD) { if (/*(msecsadded || cls.netchan.incoming_sequence < 2) && olddemotime != demotime ||*/ newseq) { newseq = false; if (!(cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS)) { cls.netchan.incoming_sequence++; cls.netchan.incoming_acknowledged++; } cls.netchan.frame_latency = 0; cls.netchan.last_received = realtime; // just to happy timeout check } } olddemotime = demotime; net_from.type = NA_INVALID; return 1; } /* ==================== CL_Stop_f stop recording a demo ==================== */ void CL_Stop_f (void) { if (!cls.demorecording) { #if !defined(CLIENTONLY) && defined(MVD_RECORDING) SV_MVDStop_f(); #else Con_Printf ("Not recording a demo.\n"); #endif return; } // write a disconnect message to the demo file #ifdef Q2CLIENT if (cls.demorecording == DPB_QUAKE2) { //q2 demos signify eof with a -1 size int len = ~0; VFS_WRITE(cls.demooutfile, &len, sizeof(len)); } else #endif if (cls.demorecording == DPB_QUAKEWORLD) { SZ_Clear (&net_message); MSG_WriteLong (&net_message, -1); // -1 sequence means out of band MSG_WriteByte (&net_message, svc_disconnect); MSG_WriteString (&net_message, "EndOfDemo"); CL_WriteDemoMessage (&net_message, 0); } // finish up VFS_CLOSE (cls.demooutfile); cls.demooutfile = NULL; cls.demorecording = false; Con_Printf ("Completed demo\n"); FS_FlushFSHashFull(); //FIXME: single name } void CL_WriteRecordQ2DemoMessage(sizebuf_t *msg) { //q2 is really simple, and doesn't have timings in its demo format. int len = LittleLong (msg->cursize); VFS_WRITE (cls.demooutfile, &len, 4); VFS_WRITE (cls.demooutfile, msg->data, msg->cursize); } /* ==================== CL_WriteDemoMessage Dumps the specified net message as part of initial mid-map demo writing. ==================== */ void CL_WriteRecordDemoMessage (sizebuf_t *msg, int seq) { int len; int i; float fl; qbyte c; //Con_Printf("write: %ld bytes, %4.4f\n", msg->cursize, demtime); if (!cls.demorecording) return; #ifdef NQPROT if (cls.demorecording == DPB_NETQUAKE) { len = LittleLong (msg->cursize); VFS_WRITE(cls.demooutfile, &len, sizeof(len)); for (i=0 ; i<3 ; i++) { float f = LittleFloat (cl.playerview[0].viewangles[i]); VFS_WRITE(cls.demooutfile, &f, sizeof(f)); } } else #endif { fl = LittleFloat(Sys_DoubleTime()-recdemostart); VFS_WRITE (cls.demooutfile, &fl, sizeof(fl)); c = dem_read; VFS_WRITE (cls.demooutfile, &c, sizeof(c)); len = LittleLong (msg->cursize + 8); VFS_WRITE (cls.demooutfile, &len, 4); i = LittleLong(seq); VFS_WRITE (cls.demooutfile, &i, 4); VFS_WRITE (cls.demooutfile, &i, 4); } VFS_WRITE (cls.demooutfile, msg->data, msg->cursize); SZ_Clear(msg); if (record_flush.ival) VFS_FLUSH (cls.demooutfile); } void CL_WriteSetDemoMessage (void) { int len; float fl; qbyte c; //Con_Printf("write: %ld bytes, %4.4f\n", msg->cursize, demtime); if (cls.demorecording != DPB_QUAKEWORLD) return; fl = LittleFloat(Sys_DoubleTime()-recdemostart); VFS_WRITE (cls.demooutfile, &fl, sizeof(fl)); c = dem_set; VFS_WRITE (cls.demooutfile, &c, sizeof(c)); len = LittleLong(cls.netchan.outgoing_sequence); VFS_WRITE (cls.demooutfile, &len, 4); len = LittleLong(cls.netchan.incoming_sequence); VFS_WRITE (cls.demooutfile, &len, 4); if (record_flush.ival) VFS_FLUSH (cls.demooutfile); } /* record a single player game. */ #ifndef CLIENTONLY #ifdef MVD_RECORDING mvddest_t *SV_MVD_InitRecordFile (char *name); qboolean SV_MVD_Record (mvddest_t *dest); #endif void CL_RecordMap_f (void) { char demoname[MAX_QPATH]; char mapname[MAX_QPATH]; char demoext[8]; if (Cmd_Argc() < 3) { Con_Printf("%s: demoname mapname\n", Cmd_Argv(0)); return; } Q_strncpyz(demoname, Cmd_Argv(1), sizeof(demoname)); Q_strncpyz(mapname, Cmd_Argv(2), sizeof(mapname)); CL_Disconnect_f(); SV_SpawnServer (mapname, NULL, false, false, 0); if (!sv.state) return; #ifdef MVD_RECORDING if (svs.allocated_client_slots > 1) COM_DefaultExtension(demoname, ".mvd", sizeof(demoname)); else #endif COM_DefaultExtension(demoname, ".qwd", sizeof(demoname)); COM_FileExtension(demoname, demoext, sizeof(demoext)); #if defined(AVAIL_GZDEC) && !defined(CLIENTONLY) { extern cvar_t sv_demoAutoCompress; if (sv_demoAutoCompress.ival) Q_strncatz(demoname, ".gz", sizeof(demoname)); } #endif #ifdef MVD_RECORDING if (!strcmp(demoext, "mvd")) { if (!SV_MVD_Record (SV_MVD_InitRecordFile(demoname))) CL_Disconnect_f(); // char buf[512]; // Cbuf_AddText(va("mvdrecord %s\n", COM_QuotedString(demoname, buf, sizeof(buf))), RESTRICT_LOCAL); } else #endif { #ifdef NQPROT if (!strcmp(demoext, "dem")) cls.demorecording = DPB_NETQUAKE; else #endif if (!strcmp(demoext, "qwd")) cls.demorecording = DPB_QUAKEWORLD; else { CL_Disconnect_f(); return; } cls.demooutfile = FS_OpenVFS (demoname, "wb", FS_GAME); if (!cls.demooutfile) { CL_Disconnect_f(); return; } #ifdef AVAIL_GZDEC if (!Q_strcasecmp(".gz", COM_GetFileExtension(demoname, NULL))) cls.demooutfile = FS_GZ_WriteFilter(cls.demooutfile, true, true); #endif #ifdef NQPROT if (cls.demorecording == DPB_NETQUAKE) VFS_PUTS(cls.demooutfile, "-1\n"); #endif CL_WriteSetDemoMessage(); } } #endif //qw-specific serverdata static void CLQW_RecordServerData(sizebuf_t *buf, int *seq) { extern char gamedirfile[]; unsigned int i; infosync_t sync; char serverinfostring[1024]; // send the serverdata MSG_WriteByte (buf, svc_serverdata); if (cls.fteprotocolextensions&~PEXT1_HIDEPROTOCOLS) //maintain demo compatability { MSG_WriteLong (buf, PROTOCOL_VERSION_FTE1); MSG_WriteLong (buf, cls.fteprotocolextensions&~PEXT1_HIDEPROTOCOLS); } if (cls.fteprotocolextensions2) //maintain demo compatability { MSG_WriteLong (buf, PROTOCOL_VERSION_FTE2); MSG_WriteLong (buf, cls.fteprotocolextensions2); } if (cls.ezprotocolextensions1) { MSG_WriteLong (buf, PROTOCOL_VERSION_EZQUAKE1); MSG_WriteLong (buf, cls.ezprotocolextensions1); } MSG_WriteLong (buf, PROTOCOL_VERSION_QW); MSG_WriteLong (buf, cl.servercount); MSG_WriteString (buf, gamedirfile); if (cls.fteprotocolextensions2 & PEXT2_MAXPLAYERS) { MSG_WriteByte (buf, cl.allocated_client_slots); MSG_WriteByte (buf, cl.splitclients); for (i = 0; i < cl.splitclients; i++) MSG_WriteByte (buf, cl.playerview[i].playernum); } else { for (i = 0; i < cl.splitclients; i++) { if (cl.playerview[i].spectator) MSG_WriteByte (buf, cl.playerview[i].playernum | 128); else MSG_WriteByte (buf, cl.playerview[i].playernum); } if (cls.fteprotocolextensions & PEXT_SPLITSCREEN) MSG_WriteByte (buf, 128); } // send full levelname MSG_WriteString (buf, cl.levelname); // send the movevars MSG_WriteFloat(buf, movevars.gravity); MSG_WriteFloat(buf, movevars.stopspeed); MSG_WriteFloat(buf, movevars.maxspeed); MSG_WriteFloat(buf, movevars.spectatormaxspeed); MSG_WriteFloat(buf, movevars.accelerate); MSG_WriteFloat(buf, movevars.airaccelerate); MSG_WriteFloat(buf, movevars.wateraccelerate); MSG_WriteFloat(buf, movevars.friction); MSG_WriteFloat(buf, movevars.waterfriction); MSG_WriteFloat(buf, movevars.entgravity); // send server info string memset(&sync, 0, sizeof(sync)); InfoBuf_ToString(&cl.serverinfo, serverinfostring, sizeof(serverinfostring), NULL, NULL, NULL, &sync, NULL); MSG_WriteByte (buf, svc_stufftext); MSG_WriteString (buf, va("fullserverinfo \"%s\"\n", serverinfostring)); while(sync.numkeys) { char *keyname = sync.keys[0].name; char enckey[2048], encdata[2048]; if (InfoBuf_EncodeString(keyname,strlen(keyname), enckey, sizeof(enckey))) { size_t k; if (InfoBuf_FindKey(&cl.serverinfo, keyname, &k)) { size_t offset, chunk; qboolean final; for (offset = 0 ; offset < cl.serverinfo.keys[k].size; offset += chunk) { chunk = cl.serverinfo.keys[k].size - offset; if (chunk > 1024) chunk = 1024; if (!InfoBuf_EncodeString(cl.serverinfo.keys[k].value, chunk, encdata, sizeof(encdata))) break; //shouldn't happen. if (buf->cursize > 512) CL_WriteRecordDemoMessage (buf, (*seq)++); final = (offset+chunk == cl.serverinfo.keys[k].size) && !cl.serverinfo.keys[k].partial; if (!offset && final) { //vanilla compat. we must just have a lot of data. MSG_WriteByte(buf, svc_serverinfo); MSG_WriteString(buf, enckey); MSG_WriteString(buf, encdata); } else { //awkward stuff. MSG_WriteByte(buf, svc_setinfo); MSG_WriteByte(buf, 255); //special meaning to say that this is a partial update MSG_WriteByte(buf, 255); //the serverinfo MSG_WriteLong(buf, (final?0x80000000:0)|offset); MSG_WriteString(buf, enckey); MSG_WriteString(buf, encdata); } offset += chunk; } } } InfoSync_Remove(&sync, 0); } InfoSync_Clear(&sync); } #ifdef NQPROT void CLNQ_WriteServerData(sizebuf_t *buf) //for demo recording { unsigned int protmain; unsigned int protfl = 0; unsigned int i; const char *val; //This is for compat with DP. val = InfoBuf_ValueForKey(&cl.serverinfo, "*csprogs"); if (*val) { MSG_WriteByte(buf, svc_stufftext); MSG_WriteString(buf, va("csqc_progcrc \"%s\"\n", val)); } val = InfoBuf_ValueForKey(&cl.serverinfo, "*csprogssize"); if (*val) { MSG_WriteByte(buf, svc_stufftext); MSG_WriteString(buf, va("csqc_progsize \"%s\"\n", val)); } val = InfoBuf_ValueForKey(&cl.serverinfo, "*csprogsname"); if (*val) { MSG_WriteByte(buf, svc_stufftext); MSG_WriteString(buf, va("csqc_progname \"%s\"\n", val)); } MSG_WriteByte(buf, svc_serverdata); if (cls.fteprotocolextensions&~PEXT1_HIDEPROTOCOLS) { MSG_WriteLong (buf, PROTOCOL_VERSION_FTE1); MSG_WriteLong (buf, cls.fteprotocolextensions&~PEXT1_HIDEPROTOCOLS); } if (cls.fteprotocolextensions2) { MSG_WriteLong (buf, PROTOCOL_VERSION_FTE2); MSG_WriteLong (buf, cls.fteprotocolextensions2); } if (cls.netchan.message.prim.anglesize == 2) protfl |= RMQFL_SHORTANGLE; if (cls.netchan.message.prim.anglesize == 4) protfl |= RMQFL_FLOATANGLE; switch(cls.netchan.message.prim.coordtype) { case COORDTYPE_FLOAT_32: protfl |= RMQFL_FLOATCOORD; break; case COORDTYPE_FIXED_28_4: protfl |= RMQFL_INT32COORD; break; case COORDTYPE_FIXED_16_8: protfl |= RMQFL_24BITCOORD; break; default: //err? case COORDTYPE_FIXED_13_3: break; } switch(cls.protocol_nq) { default: case CPNQ_ID: protmain = PROTOCOL_VERSION_NQ; break; case CPNQ_NEHAHRA: protmain = PROTOCOL_VERSION_NEHD; break; case CPNQ_BJP1: protmain = PROTOCOL_VERSION_BJP1; break; case CPNQ_BJP2: protmain = PROTOCOL_VERSION_BJP2; break; case CPNQ_BJP3: protmain = PROTOCOL_VERSION_BJP3; break; case CPNQ_FITZ666: protmain = protfl?PROTOCOL_VERSION_RMQ:PROTOCOL_VERSION_FITZ; break; //this might break .scale, fte doesn't care, other engines might. case CPNQ_DP5: protmain = PROTOCOL_VERSION_DP5; break; case CPNQ_DP6: protmain = PROTOCOL_VERSION_DP6; break; case CPNQ_DP7: protmain = PROTOCOL_VERSION_DP7; break; } MSG_WriteLong (buf, protmain); if (protmain == PROTOCOL_VERSION_RMQ) MSG_WriteLong (buf, protfl); if (cls.fteprotocolextensions2 & PEXT2_PREDINFO) MSG_WriteString(buf, FS_GetGamedir(true)); MSG_WriteByte (buf, cl.allocated_client_slots); MSG_WriteByte (buf, cl.deathmatch?GAME_DEATHMATCH:GAME_COOP); MSG_WriteString (buf, cl.levelname); for (i = 1; cl.model_name[i] && i < MAX_PRECACHE_MODELS; i++) MSG_WriteString (buf, cl.model_name[i]); MSG_WriteByte (buf, 0); for (i = 1; cl.sound_name[i] && i < MAX_PRECACHE_SOUNDS ; i++) MSG_WriteString (buf, cl.sound_name[i]); MSG_WriteByte (buf, 0); } #endif void CL_Record_Baseline(sizebuf_t *buf, entity_state_t *state, unsigned int fitzbits) { unsigned int j; if (fitzbits & FITZ_B_LARGEMODEL) MSG_WriteShort (buf, state->modelindex); else MSG_WriteByte (buf, state->modelindex); if (fitzbits & FITZ_B_LARGEFRAME) MSG_WriteShort (buf, state->frame); else MSG_WriteByte (buf, state->frame); MSG_WriteByte (buf, state->colormap); MSG_WriteByte (buf, state->skinnum); for (j=0 ; j<3 ; j++) { MSG_WriteCoord (buf, state->origin[j]); MSG_WriteAngle (buf, state->angles[j]); } if (fitzbits & FITZ_B_ALPHA) MSG_WriteByte(buf, state->trans); if (fitzbits & RMQFITZ_B_SCALE) MSG_WriteByte(buf, state->scale); } //nq+qw generic stuff. static int CL_Record_ParticlesStaticsBaselines(sizebuf_t *buf, int seq) { unsigned int i; entity_state_t *es; //particleeffectnum stuff for (i = 1; i < MAX_SSPARTICLESPRE; i++) { if (!cl.particle_ssname[i]) break; MSG_WriteByte(buf, svcfte_precache); MSG_WriteShort(buf, PC_PARTICLE | i); MSG_WriteString(buf, cl.particle_ssname[i]); if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); } //custom tents (needed for hexen2, if nothing else) for (i = 0; ; i++) { if (!CL_WriteCustomTEnt(buf, i)) break; if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); } // spawnstatic for (i = 0; i < cl.num_statics; i++) { es = &cl_static_entities[i].state; #ifndef CLIENTONLY //FIXME if (cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) { MSG_WriteByte(buf, svcfte_spawnstatic2); SVFTE_EmitBaseline(es, false, buf, cls.fteprotocolextensions2, cls.ezprotocolextensions1); } //else if (cls.fteprotocolextensions & PEXT_SPAWNSTATIC2) //qw deltas else #endif { unsigned int bits = 0; #ifdef NQPROT if (es->modelindex > 255) bits |= FITZ_B_LARGEMODEL; if (es->frame > 255) bits |= FITZ_B_LARGEFRAME; if (es->trans != 255) bits |= FITZ_B_ALPHA; if (es->scale != 16) bits |= RMQFITZ_B_SCALE; if (cls.protocol == CP_NETQUAKE && CPNQ_IS_BJP) { MSG_WriteByte (buf, svc_spawnstatic); bits = FITZ_B_LARGEMODEL; //bjp always uses shorts for models. } else if (cls.protocol == CP_NETQUAKE && cls.protocol_nq == CPNQ_FITZ666 && bits) { MSG_WriteByte (buf, svcfitz_spawnstatic2); MSG_WriteByte (buf, bits); } // else if (baselinetype2 >= CPNQ_DP5 && baselinetype2 <= CPNQ_DP7 && (bits & (FITZ_B_LARGEMODEL|FITZ_B_LARGEFRAME))) // { // MSG_WriteByte (buf, svcdp_spawnstatic2); // bits = FITZ_B_LARGEMODEL|FITZ_B_LARGEFRAME; //dp's baseline2 always has these (regular baseline is unmodified) // } else #endif { //classic protocol MSG_WriteByte (buf, svc_spawnstatic); bits = 0; } CL_Record_Baseline(buf, es, bits); } if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); } // FIXME: static sounds // static sounds are skipped in demos, life is hard // baselines for (i = 0; i < cl_baselines_count; i++) { es = cl_baselines + i; if (memcmp(es, &nullentitystate, sizeof(nullentitystate))) { #ifndef CLIENTONLY //FIXME if (cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) { MSG_WriteByte(buf, svcfte_spawnbaseline2); SVFTE_EmitBaseline(es, true, buf, cls.fteprotocolextensions2, cls.ezprotocolextensions1); } //else if (cls.fteprotocolextensions & PEXT_SPAWNSTATIC2) //qw deltas else #endif { unsigned int fitzbits = 0; //must take some consistent form for this to work #ifdef NQPROT if (es->modelindex > 255) fitzbits |= FITZ_B_LARGEMODEL; if (es->frame > 255) fitzbits |= FITZ_B_LARGEFRAME; if (es->trans != 255) fitzbits |= FITZ_B_ALPHA; if (es->scale != 16) fitzbits |= RMQFITZ_B_SCALE; if (cls.protocol == CP_NETQUAKE && CPNQ_IS_BJP) { MSG_WriteByte (buf, svc_spawnbaseline); fitzbits = FITZ_B_LARGEMODEL; //bjp always uses shorts for models. } else if (cls.protocol == CP_NETQUAKE && cls.protocol_nq == CPNQ_FITZ666 && fitzbits) { MSG_WriteByte (buf, svcfitz_spawnbaseline2); MSG_WriteByte (buf, fitzbits); } else if (cls.protocol == CP_NETQUAKE && CPNQ_IS_DP && (fitzbits & (FITZ_B_LARGEMODEL|FITZ_B_LARGEFRAME))) { MSG_WriteByte (buf, svcdp_spawnbaseline2); fitzbits = FITZ_B_LARGEMODEL|FITZ_B_LARGEFRAME; //dp's baseline2 always has these (regular baseline is unmodified) } else #endif { MSG_WriteByte (buf,svc_spawnbaseline); fitzbits = 0; } MSG_WriteEntity (buf, i); CL_Record_Baseline(buf, es, fitzbits); } if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); } } return seq; } static int CL_Record_Lightstyles(sizebuf_t *buf, int seq) { unsigned int i; // send all current light styles for (i=0 ; i= MAX_STANDARDLIGHTSTYLES) if (!*cl_lightstyle[i].map) continue; #ifdef PEXT_LIGHTSTYLECOL if ((cls.fteprotocolextensions & PEXT_LIGHTSTYLECOL) && (cl_lightstyle[i].colours[0]!=1||cl_lightstyle[i].colours[1]!=1||cl_lightstyle[i].colours[2]!=1) && *cl_lightstyle[i].map) { MSG_WriteByte (buf, svcfte_lightstylecol); MSG_WriteByte (buf, (unsigned char)i); MSG_WriteByte (buf, 0x87); MSG_WriteShort (buf, cl_lightstyle[i].colours[0]*1024); MSG_WriteShort (buf, cl_lightstyle[i].colours[1]*1024); MSG_WriteShort (buf, cl_lightstyle[i].colours[2]*1024); MSG_WriteString (buf, cl_lightstyle[i].map); } else #endif { MSG_WriteByte (buf, svc_lightstyle); MSG_WriteByte (buf, (unsigned char)i); MSG_WriteString (buf, cl_lightstyle[i].map); } if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); } return seq; } // send current status of all other players static int CL_RecordInitialPlayers(sizebuf_t *buf, int seq, qboolean isnq) { char info[MAX_LOCALINFO_STRING]; player_info_t *player; int i; for (i = 0; i < cl.allocated_client_slots; i++) { player = cl.players + i; if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); if (player->frags != 0) { MSG_WriteByte (buf, svc_updatefrags); MSG_WriteByte (buf, i); MSG_WriteShort(buf, player->frags); } if (isnq) { if (!*player->name) continue; MSG_WriteByte (buf, svc_updatename); MSG_WriteByte (buf, i); MSG_WriteString (buf, player->name); MSG_WriteByte (buf, svc_updatecolors); MSG_WriteByte (buf, i); MSG_WriteByte (buf, player->rtopcolor*16+player->rbottomcolor); } else { if (player->ping != 0) { MSG_WriteByte (buf, svc_updateping); MSG_WriteByte (buf, i); MSG_WriteShort (buf, player->ping); } if (player->pl != 0) { MSG_WriteByte (buf, svc_updatepl); MSG_WriteByte (buf, i); MSG_WriteByte (buf, player->pl); } if (player->userinfo.numkeys) { MSG_WriteByte (buf, svc_updateentertime); MSG_WriteByte (buf, i); MSG_WriteFloat (buf, realtime - player->realentertime); //seconds since } if (player->userinfo.numkeys) { InfoBuf_ToString(&player->userinfo, info, min(buf->maxsize-buf->cursize-6, sizeof(info)), basicuserinfos, NULL, NULL, NULL, NULL); MSG_WriteByte (buf, svc_updateuserinfo); MSG_WriteByte (buf, i); MSG_WriteLong (buf, player->userid); MSG_WriteString (buf, info); //spam svc_setinfo for all the infos that didn't fit. } } } return seq; } static int CL_RecordInitialStats(sizebuf_t *buf, int seq, qboolean isnq) { size_t seat, i; for (seat = 0; seat < cl.splitclients; seat++) { //higher stats should be 0 and thus not be sent, if not valid. for (i = 0; i < MAX_CL_STATS; i++) { if (cl.playerview[seat].stats[i] || cl.playerview[seat].statsf[i]) { double fs = cl.playerview[seat].statsf[i]; double is = cl.playerview[seat].stats[i]; if (seat) { MSG_WriteByte (buf, svcfte_choosesplitclient); MSG_WriteByte (buf, seat); } if ((int)fs == is) { MSG_WriteByte (buf, isnq?svcnq_updatestatlong:svcqw_updatestatlong); MSG_WriteByte (buf, i); MSG_WriteLong (buf, is); } else { MSG_WriteByte (buf, svcfte_updatestatfloat); MSG_WriteByte (buf, i); MSG_WriteLong (buf, fs); } } if (cl.playerview[seat].statsstr[i]) { if (seat) { MSG_WriteByte (buf, svcfte_choosesplitclient); MSG_WriteByte (buf, seat); } MSG_WriteByte (buf, svcfte_updatestatstring); MSG_WriteByte (buf, i); MSG_WriteString (buf, cl.playerview[seat].statsstr[i]); } if (buf->cursize > buf->maxsize/2) CL_WriteRecordDemoMessage (buf, seq++); } } return seq; } const char *Get_Q2ConfigString(int i); /* ==================== CL_Record_f record ==================== */ void CL_Record_f (void) { int c; char name[MAX_OSPATH]; sizebuf_t buf; qbyte buf_data[MAX_OVERALLMSGLEN]; int n, i; char *s, *p, *fname; extern char gamedirfile[]; int seq = 1; const char *defaultext; c = Cmd_Argc(); if (c > 2) { #ifndef CLIENTONLY CL_RecordMap_f(); #else Con_Printf ("record \n"); #endif return; } if (cls.state != ca_active) { Con_Printf ("You must either be connected to record, or specify a map name to load.\n"); Con_Printf ("%s: \n", Cmd_Argv(0)); return; } #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) defaultext = ".dm2"; else #endif if (cls.protocol == CP_NETQUAKE && !CPNQ_IS_DP) defaultext = ".dem"; else if (cls.protocol == CP_QUAKEWORLD) defaultext = ".qwd"; else { Con_Printf("Unable to record mid-map - try a different network protocol\n"); return; } if (cls.demorecording) CL_Stop_f(); if (c == 2) //user supplied a name { fname = Cmd_Argv(1); // See if the users supplied their own filename... s = strrchr(fname, '.'); // They did. if ( s != NULL ) { if (!Q_strcasecmp(s, defaultext)) *s = 0; //hack away that extension that they added, so that we don't get dupes. } } else { //automagically generate a name fname = TP_GenerateDemoName(); } while((p = strstr(fname, ".."))) { p[0] = '_'; p[1] = '_'; } // Make sure the filename doesn't contain illegal characters p=fname; #ifdef MVD_RECORDING if (*sv_demoDir.string && !strncmp(p, sv_demoDir.string, strlen(sv_demoDir.string)) && p[strlen(sv_demoDir.string)] == '/') p += strlen(sv_demoDir.string)+1; //allow a demos/ prefix (primarily because of autodemos) #endif for ( ; *p ; p++) { char c; *p &= 0x7F; // strip high bit c = *p; if (c<=' ' || c=='?' || c=='*' || (c!=2&&(c=='\\' || c=='/')) || c==':' || c=='<' || c=='>' || c=='"' || c=='.') *p = '_'; } Q_strncpyz(name, fname, sizeof(name)-4-strlen(defaultext)); #if defined(AVAIL_GZDEC) && !defined(CLIENTONLY) && defined(MVD_RECORDING) { extern cvar_t sv_demoAutoCompress; if (sv_demoAutoCompress.ival == 1 || !*sv_demoAutoCompress.string) defaultext = va("%s.gz", defaultext); } #endif //make a unique name (unless the user specified it). Q_strncatz(name, defaultext, sizeof(name)); if (c != 2) { vfsfile_t *f; f = FS_OpenVFS (name, "rb", FS_GAME); if (f) { //remove the extension again Q_strncpyz(name, fname, sizeof(name)-4-strlen(defaultext)); p = name + strlen(name); strcat(p, "_XX"); strcat(p, defaultext); p++; i = 0; do { VFS_CLOSE (f); p[0] = ((i/10)%10) + '0'; p[1] = (i%10) + '0'; f = FS_OpenVFS (name, "rb", FS_GAME); i++; } while (f && i < 100); } } // // open the demo file // cls.demooutfile = FS_OpenVFS (name, "wb", FS_GAMEONLY); if (!cls.demooutfile) { Con_Printf ("ERROR: couldn't open.\n"); return; } #ifdef AVAIL_GZDEC if (!Q_strcasecmp(".gz", COM_GetFileExtension(name, NULL))) cls.demooutfile = FS_GZ_WriteFilter(cls.demooutfile, true, true); #endif cls.demohadkeyframe = false; Con_Printf ("recording to %s.\n", name); recdemostart = Sys_DoubleTime(); /*-------------------------------------------------*/ memset(&buf, 0, sizeof(buf)); buf.data = buf_data; buf.maxsize = sizeof(buf_data); buf.prim = cls.netchan.netprim; switch(cls.protocol) { case CP_QUAKEWORLD: if (!cls.fteprotocolextensions && !cls.fteprotocolextensions2) buf.maxsize = MAX_QWMSGLEN; //poo compatibility... :P cls.demorecording = DPB_QUAKEWORLD; CLQW_RecordServerData(&buf, &seq); // send music Media_WriteCurrentTrack(&buf); //paknames { char buffer[1024]; FS_GetPackNames(buffer, sizeof(buffer), 2, true); /*retain extensions, or we'd have to assume pk3*/ if (*buffer) { MSG_WriteByte(&buf, svc_stufftext); SZ_Write(&buf, "//paknames ", 11); SZ_Write(&buf, buffer, strlen(buffer)); MSG_WriteString(&buf, "\n"); } } //Paks { char buffer[1024]; FS_GetPackHashes(buffer, sizeof(buffer), false); if (*buffer) { MSG_WriteByte(&buf, svc_stufftext); SZ_Write(&buf, "//paks ", 7); SZ_Write(&buf, buffer, strlen(buffer)); MSG_WriteString(&buf, "\n"); } } //FIXME: //at //FIXME: //wps //FIXME: //it MSG_WriteByte (&buf, svc_setpause); MSG_WriteByte (&buf, !!cl.paused); #ifdef PEXT_SETVIEW if (cl.playerview[0].viewentity != cl.playerview[0].playernum+1) //tell the player if we have a different view entity { MSG_WriteByte (&buf, svc_setview); MSG_WriteEntity (&buf, cl.playerview[0].viewentity); } #endif // flush packet CL_WriteRecordDemoMessage (&buf, seq++); // soundlist MSG_WriteByte (&buf, svc_soundlist); MSG_WriteByte (&buf, 0); n = 0; s = cl.sound_name[n+1]; while (s && *s) { MSG_WriteString (&buf, s); if (buf.cursize > buf.maxsize/2 && (n&0xff)) { MSG_WriteByte (&buf, 0); MSG_WriteByte (&buf, n); CL_WriteRecordDemoMessage (&buf, seq++); if (n + 1 > 0xff) { MSG_WriteByte (&buf, svcfte_soundlistshort); MSG_WriteShort (&buf, n + 1); } else { MSG_WriteByte (&buf, svc_soundlist); MSG_WriteByte (&buf, n + 1); } } n++; s = cl.sound_name[n+1]; } if (buf.cursize) { MSG_WriteByte (&buf, 0); MSG_WriteByte (&buf, 0); CL_WriteRecordDemoMessage (&buf, seq++); } //FIXME: vweps // modellist MSG_WriteByte (&buf, svc_modellist); MSG_WriteByte (&buf, 0); n = 0; s = cl.model_name[n+1]; while (s && *s) { MSG_WriteString (&buf, s); if (buf.cursize > buf.maxsize/2 && (n&0xff)) { MSG_WriteByte (&buf, 0); MSG_WriteByte (&buf, n); CL_WriteRecordDemoMessage (&buf, seq++); if (n + 1 > 0xff) { MSG_WriteByte (&buf, svcfte_modellistshort); MSG_WriteShort (&buf, n + 1); } else { MSG_WriteByte (&buf, svc_modellist); MSG_WriteByte (&buf, n + 1); } } n++; s = cl.model_name[n+1]; } if (buf.cursize) { MSG_WriteByte (&buf, 0); MSG_WriteByte (&buf, 0); CL_WriteRecordDemoMessage (&buf, seq++); } seq = CL_Record_ParticlesStaticsBaselines(&buf, seq); MSG_WriteByte (&buf, svc_stufftext); MSG_WriteString (&buf, va("cmd spawn %i\n", cl.servercount) ); if (buf.cursize) CL_WriteRecordDemoMessage (&buf, seq++); seq = CL_RecordInitialPlayers(&buf, seq, false); seq = CL_Record_Lightstyles(&buf, seq); seq = CL_RecordInitialStats(&buf, seq, false); // get the client to check and download skins // when that is completed, a begin command will be issued MSG_WriteByte (&buf, svc_stufftext); MSG_WriteString (&buf, "skins\n"); CL_WriteRecordDemoMessage (&buf, seq++); CL_WriteSetDemoMessage(); //FIXME: make sure the deltas are reset // done break; #if defined(Q2CLIENT) && defined(Q2SERVER) case CP_QUAKE2: cls.demorecording = DPB_QUAKE2; MSG_WriteByte (&buf, svcq2_serverdata); if (cls.fteprotocolextensions) //maintain demo compatability { MSG_WriteLong (&buf, PROTOCOL_VERSION_FTE1); MSG_WriteLong (&buf, cls.fteprotocolextensions); } if (cls.fteprotocolextensions2) //maintain demo compatability { MSG_WriteLong (&buf, PROTOCOL_VERSION_FTE2); MSG_WriteLong (&buf, cls.fteprotocolextensions2); } MSG_WriteLong (&buf, cls.protocol_q2); MSG_WriteLong (&buf, 0x80000000 + cl.servercount); MSG_WriteByte (&buf, 1); //attract loop if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) MSG_WriteByte (&buf, cl.q2svnetrate); //tick rate MSG_WriteString (&buf, gamedirfile); if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX && cl.playerview[0].playernum != -1 && cl.splitclients!=1) { MSG_WriteShort (&buf, -2); MSG_WriteShort (&buf, cl.splitclients); for (i = 0; i < cl.splitclients; i++) MSG_WriteShort (&buf, cl.playerview[i].playernum); } else MSG_WriteShort (&buf, cl.playerview[0].playernum); MSG_WriteString (&buf, cl.levelname); for (i = 0; i < Q2MAX_CONFIGSTRINGS; i++) { const char *cs = Get_Q2ConfigString(i); if (buf.cursize + 4 + strlen(cs) > buf.maxsize) { CL_WriteRecordQ2DemoMessage (&buf); SZ_Clear (&buf); } MSG_WriteByte (&buf, svcq2_configstring); MSG_WriteShort (&buf, i); MSG_WriteString (&buf, cs); } CLQ2_WriteDemoBaselines(&buf); MSG_WriteByte (&buf, svcq2_stufftext); MSG_WriteString (&buf, "precache\n"); CL_WriteRecordQ2DemoMessage (&buf); break; #endif #ifdef NQPROT case CP_NETQUAKE: //csqc stuff cls.demorecording = DPB_NETQUAKE; VFS_WRITE(cls.demooutfile, "-1\n", 3); //stupid lame header thing. CLNQ_WriteServerData(&buf); MSG_WriteByte (&buf, svc_setpause); MSG_WriteByte (&buf, !!cl.paused); MSG_WriteByte (&buf, svc_setview); MSG_WriteEntity (&buf, cl.playerview[0].viewentity); MSG_WriteByte (&buf, svcnq_signonnum); MSG_WriteByte (&buf, 1); CL_WriteRecordDemoMessage (&buf, seq++); seq = CL_Record_ParticlesStaticsBaselines(&buf, seq); //fixme: brushes... MSG_WriteByte (&buf, svcnq_signonnum); MSG_WriteByte (&buf, 2); CL_WriteRecordDemoMessage (&buf, seq++); seq = CL_RecordInitialPlayers(&buf, seq, true); seq = CL_Record_Lightstyles(&buf, seq); seq = CL_RecordInitialStats(&buf, seq, true); MSG_WriteByte (&buf, svcnq_signonnum); MSG_WriteByte (&buf, 3); CL_WriteRecordDemoMessage (&buf, seq++); break; #endif default: //this should have been caught earlier Con_Printf("Unable to begin demo recording with this network protocol\n"); CL_Stop_f(); break; } cl.validsequence = 0; //ask for a sequence reset. if (cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) if (cl.numackframes < sizeof(cl.ackframes)/sizeof(cl.ackframes[0])) cl.ackframes[cl.numackframes++] = -1; } static int QDECL CompleteDemoList (const char *name, qofs_t flags, time_t mtime, void *parm, searchpathfuncs_t *spath) { struct xcommandargcompletioncb_s *ctx = parm; const char *ext = NULL; ext = COM_GetFileExtension(name, ext); if (!Q_strcasecmp(ext, ".gz")) ext = COM_GetFileExtension(name, ext); if ( #ifdef NQPROT !Q_strcasecmp(ext, ".dem") || !Q_strcasecmp(ext, ".dem.gz") || #endif #ifdef Q2CLIENT !Q_strcasecmp(ext, ".dm2") || !Q_strcasecmp(ext, ".dm2.gz") || #endif !Q_strcasecmp(ext, ".qwd") || !Q_strcasecmp(ext, ".qwd.gz") || !Q_strcasecmp(ext, ".mvd") || !Q_strcasecmp(ext, ".mvd.gz")) //FIXME: enumerate .zip and .dz files too. { ctx->cb(name, NULL, NULL, ctx); } return true; } void CL_DemoList_c(int argn, const char *partial, struct xcommandargcompletioncb_s *ctx) { if (argn == 1) COM_EnumerateFiles(va("%s*", partial), CompleteDemoList, ctx); } /* ==================== CL_ReRecord_f record ==================== */ void CL_ReRecord_f (void) { int c; char name[MAX_OSPATH]; char *s; c = Cmd_Argc(); if (c != 2) { Con_Printf ("rerecord \n"); return; } if (!*cls.servername) { Con_Printf("No server to reconnect to...\n"); return; } if (cls.demorecording) CL_Stop_f(); s = Cmd_Argv(1); if (strstr(s, "..")) { Con_Printf ("Relative paths not allowed.\n"); return; } Q_snprintfz (name, sizeof(name), "%s", s); CL_Disconnect(NULL); // // open the demo file // switch (cls.protocol) { default: case CP_QUAKEWORLD: cls.demorecording = DPB_QUAKEWORLD; COM_RequireExtension (name, ".qwd", sizeof(name)); break; #ifdef NQPROT case CP_NETQUAKE: cls.demorecording = DPB_NETQUAKE; COM_RequireExtension (name, ".dem", sizeof(name)); break; #endif #ifdef Q2CLIENT case CP_QUAKE2: cls.demorecording = DPB_QUAKE2; COM_RequireExtension (name, ".dm2", sizeof(name)); break; #endif } cls.demooutfile = FS_OpenVFS (name, "wb", FS_GAME); if (!cls.demooutfile) { Con_Printf ("ERROR: couldn't open.\n"); cls.demorecording = DPB_NONE; return; } #ifdef AVAIL_GZDEC if (!Q_strcasecmp(".gz", COM_GetFileExtension(name, NULL))) cls.demooutfile = FS_GZ_WriteFilter(cls.demooutfile, true, true); #endif Con_Printf ("recording to %s.\n", name); #ifdef NQPROT if (cls.demorecording == DPB_NETQUAKE) //nq demos have some silly header. VFS_WRITE(cls.demooutfile, "-1\n", 3); #endif CL_BeginServerReconnect(); } /* ==================== CL_PlayDemo_f play [demoname] ==================== */ void CL_PlayDownloadedDemo(struct dl_download *dl); void CL_PlayDemo_f (void) { char *demoname; if (Cmd_Argc() != 2) { Con_Printf ("playdemo : plays a demo\n"); return; } if (cls.state == ca_demostart) cls.state = ca_disconnected; else cls.demonum = -1; //not via CL_NextDemo, don't confuse the user by playing random other demos. #ifdef WEBCLIENT #if 1 if (!strncmp(Cmd_Argv(1), "ftp://", 6) || !strncmp(Cmd_Argv(1), "http://", 7) || !strncmp(Cmd_Argv(1), "https://", 7)) { if (Cmd_ExecLevel == RESTRICT_LOCAL) Host_RunFile(Cmd_Argv(1), strlen(Cmd_Argv(1)), NULL); // HTTP_CL_Get(Cmd_Argv(1), COM_SkipPath(Cmd_Argv(1)), CL_PlayDownloadedDemo); return; } #endif #endif demoname = Cmd_Argv(1); if (*demoname == '#') { if (Cmd_FromGamecode()) return; CL_PlayDemo(demoname+1, true); } else CL_PlayDemo(demoname, false); } //dl is provided so that we can receive files via chunked/gziped http downloads and on systems that don't provide sockets etc. its tracked so we can cancel the download if the client aborts playback early. void CL_PlayDemoStream(vfsfile_t *file, char *filename, qboolean issyspath, int demotype, float bufferdelay, unsigned int eztv_ext) { int protocol = CP_UNKNOWN; if (demotype == DPB_NONE) { //peek etc? } switch(demotype) { case DPB_MVD: case DPB_QUAKEWORLD: protocol = CP_QUAKEWORLD; break; #ifdef Q2CLIENT case DPB_QUAKE2: protocol = CP_QUAKE2; break; #endif #ifdef NQPROT case DPB_NETQUAKE: protocol = CP_NETQUAKE; break; #endif default: break; } if (protocol == CP_UNKNOWN) { Con_Printf ("ERROR: demo format not supported: \"%s\".\n", filename); return; } // // disconnect from server // CL_Disconnect_f (); demo_flushcache(); NET_InitClient(true); // // open the demo file // cls.demoinfile = file; if (!cls.demoinfile) { Con_Printf ("ERROR: couldn't open \"%s\".\n", filename); cls.demonum = -1; // stop demo loop return; } if (filename) { Q_strncpyz (cls.lastdemoname, filename, sizeof(cls.lastdemoname)); cls.lastdemowassystempath = issyspath; Con_Printf ("Playing demo from %s.\n", filename); } cls.findtrack = (demotype == DPB_MVD); cls.demoplayback = demotype; cls.demoeztv_ext = eztv_ext; cls.protocol = protocol; cls.state = ca_demostart; net_message.packing = SZ_RAWBYTES; Netchan_Setup (NCF_CLIENT, &cls.netchan, &net_from, 0, 0); demtime = -bufferdelay; cls.demostarttime = 0; cl.gametime = -bufferdelay; cl.gametimemark = realtime;//demtime; if (demtime < -0.5) Con_Printf("Buffering for %g seconds\n", bufferdelay); cls.netchan.last_received=demtime; TP_ExecTrigger ("f_demostart", true); } vfsfile_t *CL_OpenFileInZipOrSys(char *name, qboolean usesystempath) { if (usesystempath) return VFSOS_Open(name, "rb"); else return CL_OpenFileInPackage(NULL, name); } //tries to determine the demo type void CL_PlayDemoFile(vfsfile_t *f, char *demoname, qboolean issyspath) { #if defined(Q2CLIENT) || defined(NQPROT) //figure out where we started qofs_t start = VFS_TELL(f); #endif if (!VFS_GETLEN (f)) { VFS_CLOSE(f); Con_Printf ("demo \"%s\" is empty.\n", demoname); return; } #ifdef Q2CLIENT //just assume if it has a known extension if (!Q_strcasecmp(demoname + strlen(demoname) - 3, "dm2") || !Q_strcasecmp(demoname + strlen(demoname) - 6, "dm2.gz")) { CL_PlayDemoStream(f, demoname, issyspath, DPB_QUAKE2, 0, 0); return; } #endif if (!Q_strcasecmp(demoname + strlen(demoname) - 3, "mvd") || !Q_strcasecmp(demoname + strlen(demoname) - 6, "mvd.gz")) { CL_PlayDemoStream(f, demoname, issyspath, DPB_MVD, 0, 0); return; } if (!Q_strcasecmp(demoname + strlen(demoname) - 3, "qwd") || !Q_strcasecmp(demoname + strlen(demoname) - 6, "qwd.gz")) { CL_PlayDemoStream(f, demoname, issyspath, DPB_QUAKEWORLD, 0, 0); return; } #ifdef NQPROT { int ft = 0, neg = false; char chr; //not quake2, check if its NQ //work out if the first line is a int for the track number. while ((VFS_READ(f, &chr, 1)==1) && (chr != '\n')) { if (chr == ' ') ; else if (chr == '-') neg = true; else if (chr < '0' || chr > '9') break; else ft = ft * 10 + ((int)chr - '0'); } if (neg) ft *= -1; if (chr == '\n') { if (ft > 0) cls.demotrack = ft; else cls.demotrack = -1; CL_PlayDemoStream(f, demoname, issyspath, DPB_NETQUAKE, 0, 0); return; } VFS_SEEK(f, start); } #endif #ifdef Q2CLIENT { int len; char type; int protocol; //check if its a quake2 demo. while(VFS_READ(f, &len, sizeof(len)) == sizeof(len)) { len = LittleLong(len); if (len > MAX_OVERALLMSGLEN) break; len--; VFS_READ(f, &type, sizeof(type)); while (len >= 2 && (type == svcq2_stufftext || type == svcq2_print)) { while (len > 0) { len--; VFS_READ(f, &type, sizeof(type)); if (!type) break; } if (len == 0) continue; len--; VFS_READ(f, &type, sizeof(type)); } if (len > 4 && type == svcq2_serverdata) { VFS_READ(f, &protocol, sizeof(protocol)); protocol = LittleLong(protocol); if (protocol >= PROTOCOL_VERSION_Q2_DEMO_MIN && protocol <= PROTOCOL_VERSION_Q2_DEMO_MAX) { VFS_SEEK(f, start); CL_PlayDemoStream(f, demoname, issyspath, DPB_QUAKE2, 0, 0); return; } break; } if (len) VFS_SEEK(f, VFS_TELL(f)+len); } VFS_SEEK(f, start); } #endif //it doesn't have a assumable extension, isn't q2, nor NQ. then it must be a QuakeWorld demo //could also be .qwz or .dmz or whatever that nq extension is. we don't support either. //mvd and qwd have no identifying markers, other than the extension. CL_PlayDemoStream(f, demoname, issyspath, DPB_QUAKEWORLD, 0, 0); } #ifdef WEBCLIENT void CL_PlayDownloadedDemo(struct dl_download *dl) { if (dl->status != DL_FINISHED || !dl->file) Con_Printf("Failed to download %s\n", dl->url); else { CL_PlayDemoFile(dl->file, dl->url, false); dl->file = NULL; } } #endif void CL_PlayDemo(char *demoname, qboolean usesystempath) { char name[256]; vfsfile_t *f; int i; char *exts[] = { ".qwd", ".dem", ".mvd", ".dm2", }; // // open the demo file // f = NULL; for (i = 0; i < countof(exts); i++) { Q_strncpyz (name, demoname, sizeof(name)); COM_DefaultExtension (name, exts[i], sizeof(name)); f = CL_OpenFileInZipOrSys(name, usesystempath); if (f) break; #ifdef AVAIL_GZDEC Q_strncpyz (name, demoname, sizeof(name)); COM_DefaultExtension (name, va("%s.gz", exts[i]), sizeof(name)); f = CL_OpenFileInZipOrSys(name, usesystempath); if (f) break; #endif } if (!f) { Con_Printf ("ERROR: couldn't open \"%s\".\n", demoname); cls.demonum = -1; // stop demo loop TP_ExecTrigger ("f_demoend", true); return; } Q_strncpyz (cls.lastdemoname, demoname, sizeof(cls.lastdemoname)); #ifdef AVAIL_GZDEC if (!strcmp(COM_GetFileExtension(name,NULL), ".gz")) f = FS_DecompressGZip(f, NULL); #endif CL_PlayDemoFile(f, name, usesystempath); } /*used with qtv*/ void CL_Demo_ClientCommand(char *commandtext) { unsigned char b = 1; unsigned short len = LittleShort((unsigned short)(strlen(commandtext) + 4)); #ifdef warningmsg #pragma warningmsg("this needs buffering safely") #endif if (cls.demoplayback == DPB_MVD && cls.demoeztv_ext) { VFS_WRITE(cls.demoinfile, &len, sizeof(len)); VFS_WRITE(cls.demoinfile, &b, sizeof(b)); VFS_WRITE(cls.demoinfile, commandtext, strlen(commandtext)+1); } } char *strchrrev(char *str, char chr) { const char *firstchar = str; for (str = str + strlen(str)-1; str>=firstchar; str--) if (*str == chr) return str; return NULL; } /*void CL_ParseQTVFile(vfsfile_t *f, const char *fname, qtvfile_t *result) { char buffer[2048]; char *s; memset(result, 0, sizeof(*result)); if (!f) { Con_Printf("Couldn't open QTV file: %s\n", fname); return; } if (!VFS_GETS(f, buffer, sizeof(buffer)-1)) { Con_Printf("Empty QTV file: %s\n", fname); VFS_CLOSE(f); return; } s = buffer; while (*s == ' ' || *s == '\t') s++; if (*s != '[') { Con_Printf("Bad QTV file: %s\n", fname); VFS_CLOSE(f); return; } s++; while (*s == ' ' || *s == '\t') s++; if (strnicmp(s, "QTV", 3)) { Con_Printf("Bad QTV file: %s\n", fname); VFS_CLOSE(f); return; } s+=3; while (*s == ' ' || *s == '\t') s++; if (*s != ']') { Con_Printf("Bad QTV file: %s\n", fname); VFS_CLOSE(f); return; } s++; while (*s == ' ' || *s == '\t' || *s == '\r') s++; if (*s) { Con_Printf("Bad QTV file: %s\n", fname); VFS_CLOSE(f); return; } while (VFS_GETS(f, buffer, sizeof(buffer)-1)) { s = COM_ParseToken(buffer, ":="); if (*s != '=' && *s != ':') s = ""; else s++; if (!stricmp(com_token, "stream")) { result->connectiontype = QTVCT_STREAM; s = COM_ParseOut(s, result->server, sizeof(result->server)); } else if (!stricmp(com_token, "connect")) { result->connectiontype = QTVCT_CONNECT; s = COM_ParseOut(s, result->server, sizeof(result->server)); } else if (!stricmp(com_token, "join")) { result->connectiontype = QTVCT_JOIN; s = COM_ParseOut(s, result->server, sizeof(result->server)); } else if (!stricmp(com_token, "observe")) { result->connectiontype = QTVCT_OBSERVE; s = COM_ParseOut(s, result->server, sizeof(result->server)); } else if (!stricmp(com_token, "splash")) { s = COM_ParseOut(s, result->splashscreen, sizeof(result->server)); } } VFS_CLOSE(f); }*/ void CL_ParseQTVDescriptor(vfsfile_t *f, const char *name) { //.qtv files are some sneaky way to deal with download links using file extension associations instead of special protocols. //they basically contain some directive:hostname line that tells us what to do and where from. //they should have mime type text/x-quaketvident with extension .qtv char buffer[1024]; char *s; if (!f) { Con_Printf("Couldn't open QTV file: %s\n", name); return; } while (VFS_GETS(f, buffer, sizeof(buffer)-1)) { if (!strncmp(buffer, "Stream=", 7) || !strncmp(buffer, "Stream:", 7)) { for (s = buffer + strlen(buffer)-1; s >= buffer; s--) { if (*s == '\r' || *s == '\n' || *s == ';') *s = 0; else break; } s = buffer+7; while(*s && *s <= ' ') s++; Cbuf_AddText(va("qtvplay \"%s\"\n", s), Cmd_ExecLevel); break; } if (!strncmp(buffer, "Connect=", 8) || !strncmp(buffer, "Connect:", 8)) { for (s = buffer + strlen(buffer)-1; s >= buffer; s--) { if (*s == '\r' || *s == '\n' || *s == ';') *s = 0; else break; } s = buffer+8; while(*s && *s <= ' ') s++; Cbuf_AddText(va("connect \"%s\"\n", s), Cmd_ExecLevel); break; } if (!strncmp(buffer, "Join=", 5) || !strncmp(buffer, "Join:", 5)) { for (s = buffer + strlen(buffer)-1; s >= buffer; s--) { if (*s == '\r' || *s == '\n' || *s == ';') *s = 0; else break; } s = buffer+5; while(*s && *s <= ' ') s++; Cbuf_AddText(va("join \"%s\"\n", s), Cmd_ExecLevel); break; } if (!strncmp(buffer, "Observe=", 8) || !strncmp(buffer, "Observe:", 8)) { for (s = buffer + strlen(buffer)-1; s >= buffer; s--) { if (*s == '\r' || *s == '\n' || *s == ';') *s = 0; else break; } s = buffer+8; while(*s && *s <= ' ') s++; Cbuf_AddText(va("observe \"%s\"\n", s), Cmd_ExecLevel); break; } } VFS_CLOSE(f); } #include "netinc.h" static struct pendingqtv_s { struct pendingqtv_s *next; qboolean raw; char hostname[1024]; char password[1024]; char requestbuffer[4096]; size_t requestsize; char requestcmdbuffer[4096]; int requestcmdsize; vfsfile_t *stream; char postauth[1]; } *pendingqtv; void CL_QTVPoll (void) { struct pendingqtv_s **link, *qtv; for (link = &pendingqtv; (qtv = *link); link = &qtv->next) { char *s, *e, *colon; char *tail = NULL; int len; char *streamavailable = NULL; qboolean saidheader = false; #ifndef NOBUILTINMENUS emenu_t *sourcesmenu = NULL; #endif int sourcenum = 0; int numplayers = 0; int numviewers = 0; qboolean init_numplayers = false; qboolean init_numviewers = false; qboolean iseztv = false; char srchost[256]; char auth[64]; char challenge[128]; hashfunc_t *hashfunc = NULL; //try to finish sending if (qtv->requestcmdsize) { len = VFS_WRITE(qtv->stream, qtv->requestcmdbuffer, qtv->requestcmdsize); if (len > 0) { memmove(qtv->requestcmdbuffer, qtv->requestcmdbuffer+len, qtv->requestcmdsize-len); qtv->requestcmdsize -= len; } if (len < 0) goto fail; } for(;;) { len = VFS_READ(qtv->stream, qtv->requestbuffer+qtv->requestsize, (sizeof(qtv->requestbuffer) - qtv->requestsize -1 > 0)?1:0); if (len <= 0) break; qtv->requestsize += len; } qtv->requestbuffer[qtv->requestsize] = '\0'; if (qtv->raw) { tail = qtv->requestbuffer; streamavailable = ""; } else { if (qtv->requestsize >= sizeof(qtv->requestbuffer) - 1) { //flag it as an error if the response is larger than we can handle. //this error gets ignored if the header is okay (any actual errors will get reported again by the demo code anyway), and only counts if the end of the reply header was not found. len = -1; } if (!qtv->requestsize && len == 0) continue; //still trying. //make sure it's a compleate chunk. for (s = qtv->requestbuffer; *s; s++) { if (s[0] == '\n' && s[1] == '\n') { tail = s+2; break; } if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n') { tail = s+4; break; } if (s[0] == '\r' && s[1] == '\n' && s[2] == '\n') { tail = s+3; break; } if (s[0] == '\n' && s[1] == '\r' && s[2] == '\n') { tail = s+3; break; } } } if (!tail) { if (len < 0) { if (!qtv->requestsize) Con_Printf("Connection to QTV server closed without any reply.\n"); else Con_Printf("invalid QTV handshake\n"); fail: SCR_SetLoadingStage(LS_NONE); if (qtv->stream) VFS_CLOSE(qtv->stream); qtv->stream = NULL; qtv->requestsize = 0; *link = qtv->next; Z_Free(qtv); return; } continue; } s = qtv->requestbuffer; colon = ""; *auth = *challenge = 0; for (e = s; e < tail; ) { if (*e == '\r') *e = '\0'; else if (*e == '\n') { *e = '\0'; colon = strchr(s, ':'); if (colon) { *colon++ = '\0'; if (*colon && *(unsigned char*)colon <= ' ') colon++; } else colon = ""; if (!strcmp(s, "PERROR")) { //permanent printable error Con_Printf("QTV Error:\n%s\n", colon); } else if (!strcmp(s, "PRINT")) { //printable error Con_Printf("QTV:\n%s\n", colon); } else if (!strcmp(s, "TERROR")) { //temporary printable error Con_Printf("QTV Error:\n%s\n", colon); } else if (!strcmp(s, "ADEMO")) { //printable error Con_Printf("Demo%s is available\n", colon); } else if (!strcmp(s, "AUTH")) { while (*colon && *(unsigned char*)colon <= ' ') colon++; Q_strncpyz(auth, colon, sizeof(auth)); } else if (!strcmp(s, "CHALLENGE")) { while (*colon && *(unsigned char*)colon <= ' ') colon++; Q_strncpyz(challenge, colon, sizeof(challenge)); } //generic sourcelist responce else if (!strcmp(s, "ASOURCE")) { //printable source if (!saidheader) { saidheader=true; Con_Printf("Available Sources:\n"); } Con_Printf("%s\n", colon); //we're too lazy to even try and parse this } else if (!strcmp(s, "BEGIN")) { while (*colon && *(unsigned char*)colon <= ' ') colon++; streamavailable = colon; } //eztv extensions to v1.0 else if (!strcmp(s, "QTV_EZQUAKE_EXT")) { iseztv = atoi(colon); if (iseztv & ~(EZTV_DOWNLOAD|EZTV_SETINFO|EZTV_QTVUSERLIST)) Con_Printf(CON_WARNING"Warning: unknown eztv extensions %s\n", colon); } //v1.1 sourcelist response includes SRCSRV, SRCHOST, SRCPLYRS, SRCVIEWS, SRCID else if (!strcmp(s, "SRCSRV")) { //the proxy's source string (beware of file:blah without file:blah@blah) } else if (!strcmp(s, "SRCHOST")) { //the hostname from the server the stream came from Q_strncpyz(srchost, colon, sizeof(srchost)); } else if (!strcmp(s, "SRCPLYRS")) { //number of active players actually playing on that stream numplayers = atoi(colon); init_numplayers = true; } else if (!strcmp(s, "SRCVIEWS")) { //number of people watching this stream on the proxy itself numviewers = atoi(colon); init_numviewers = true; } else if (!strcmp(s, "SRCID") && !streamavailable) { char *streamid = colon; #ifndef NOBUILTINMENUS //now put it on a menu if (!sourcesmenu) { sourcesmenu = M_CreateMenu(0); MC_AddPicture(sourcesmenu, 16, 4, 32, 144, "gfx/qplaque.lmp"); MC_AddCenterPicture(sourcesmenu, 4, 24, "gfx/p_option.lmp"); } if (init_numplayers == true && init_numviewers == true) MC_AddConsoleCommand(sourcesmenu, 42, 170, (sourcenum++)*8 + 32, va("%s (p%i, v%i)", *srchost?srchost:streamid, numplayers, numviewers), va("qtvplay %s@%s\n", streamid, qtv->hostname)); //else // FIXME: add error message here #else (void)init_numviewers; (void)numviewers; (void)init_numplayers; (void)numplayers; (void)streamid; (void)sourcenum; #endif } //end of sourcelist entry //from e to s, we have a line s = e+1; } e++; } if (streamavailable) { if (*streamavailable) Con_Printf("streaming \"%s\" via \"%s\"\n", streamavailable, qtv->hostname); else Con_Printf("qtv connection established to %s\n", qtv->hostname); CL_PlayDemoStream(qtv->stream, NULL, false, DPB_MVD, BUFFERTIME, iseztv); qtv->stream = NULL; demo_resetcache(qtv->requestsize - (tail-qtv->requestbuffer), tail); *link = qtv->next; Z_Free(qtv); return; } //something failed. if its giving us an auth type then we should be authing before sending our request... if (!strcmp(auth, "NONE")) ; // else if (!strcmp(auth, "PLAIN")) // else if (!strcmp(auth, "MD4")) else if (!strcmp(auth, "SHA1")) hashfunc = &hash_sha1; else if (!strcmp(auth, "SHA2_256")) hashfunc = &hash_sha2_256; else if (!strcmp(auth, "SHA2_512")) hashfunc = &hash_sha2_512; else if (*auth) Con_Printf("Server requires unsupported auth method: %s\n", auth); qtv->requestsize -= tail-qtv->requestbuffer; memmove(qtv->requestbuffer, tail, qtv->requestsize); if (hashfunc && *qtv->postauth) { if (*qtv->password) { char hash[DIGEST_MAXSIZE*2+1]; qbyte digest[DIGEST_MAXSIZE]; Q_snprintfz(hash, sizeof(hash), "%s%s", challenge, qtv->password); CalcHash(hashfunc, digest, sizeof(digest), hash, strlen(hash)); Base64_EncodeBlock(digest, hashfunc->digestsize, hash, sizeof(hash)); Q_snprintfz(qtv->requestcmdbuffer, sizeof(qtv->requestcmdbuffer), "QTV\n" "VERSION: 1.1\n" "AUTH: %s\n" "PASSWORD: \"%s\"\n" "%s\n", auth, hash, qtv->postauth); qtv->requestcmdsize = strlen(qtv->requestcmdbuffer); continue; } else Con_Printf("QTV server requires a password\n"); } SCR_SetLoadingStage(LS_NONE); VFS_CLOSE(qtv->stream); qtv->stream = NULL; qtv->requestsize = 0; *link = qtv->next; Z_Free(qtv); return; } } void CL_QTVPlay_Establish (const char *host, const char *password, const char *command) { struct pendingqtv_s *qtv = Z_Malloc(sizeof(*qtv) + strlen(command)); char msg[4096]; int msglen=0; // SCR_SetLoadingStage(LS_CONNECTION); qtv->stream = FS_OpenTCP(host, 27599, false); if (!qtv->stream) { SCR_SetLoadingStage(LS_NONE); Con_Printf("Couldn't connect to proxy\n"); Z_Free(qtv); return; } Q_strncpyz(qtv->hostname, host, sizeof(qtv->hostname)); Q_strncpyz(qtv->password, password, sizeof(qtv->password)); if (qtvcl_forceversion1.ival) { Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "QTV\n" "VERSION: 1.0\n"); } else { Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "QTV\n" "VERSION: 1.1\n"); } msglen += strlen(msg+msglen); if (*password) { if (qtv->raw) { //just send it directly, we can't handle any kind of response and that includes the tripple handshake for the challenge info Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "AUTH: PLAIN\n" "PASSWORD: %s\n" , password); } else { //report supported auth methods to the server. it'll pick one and send us a challenge. Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "AUTH: SHA2_512\n" "AUTH: SHA2_256\n" "AUTH: SHA1\n" // "AUTH: MD4\n" // "AUTH: CCITT\n" // "AUTH: PLAIN\n" ); } msglen += strlen(msg+msglen); strcpy(qtv->postauth, command); } else { //include supported auth methods, so server can pick one (and give suitable challenge in its response) Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "AUTH: NONE\n" ""); msglen += strlen(msg+msglen); Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "%s", command); msglen += strlen(msg+msglen); *qtv->postauth = 0; } if (qtv->raw) { //peer must either disconnect instantly, or respond with an mvd file without extra headers. Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "RAW: 1\n"); msglen += strlen(msg+msglen); } Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "\n"); msglen += strlen(msg+msglen); memcpy(qtv->requestcmdbuffer, msg, msglen); qtv->requestcmdsize = msglen; qtv->requestsize = 0; //and link it in. qtv->next = pendingqtv; pendingqtv = qtv; } void CL_QTVPlay_f (void) { char *host; const char *password; char *streamid; char msg[4096]; int msglen=0; if (Cmd_Argc() < 2) { Con_Printf("Usage: qtvplay [stream@][tls://]hostname[:port] [password]\n"); return; } streamid = Cmd_Argv(1); password = Cmd_Argv(2); host = strchrrev(streamid, '@'); if (host) *host++ = 0; else { host = streamid; streamid = NULL; } if (streamid) { if (qtvcl_eztvextensions.ival) { Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "QTV_EZQUAKE_EXT: %u\n" "USERINFO: ", EZTV_DOWNLOAD|EZTV_SETINFO|EZTV_QTVUSERLIST); msglen += strlen(msg+msglen); InfoBuf_ToString(&cls.userinfo[0], msg+msglen, sizeof(msg)-msglen-1, basicuserinfos, NULL, NULL, NULL, NULL); msglen += strlen(msg+msglen); Q_strncatz(msg+msglen, "\n", sizeof(msg)-msglen); msglen += strlen(msg+msglen); } Q_snprintfz(msg+msglen, sizeof(msg)-msglen, "SOURCE: %s\n", streamid); msglen += strlen(msg+msglen); SCR_SetLoadingStage(LS_CONNECTION); CL_QTVPlay_Establish(host, password, msg); } else { CL_QTVPlay_Establish(host, password, "SOURCELIST\n"); } } void CL_QTVList_f (void) { CL_QTVPlay_Establish(Cmd_Argv(1), Cmd_Argv(2), "SOURCELIST\n"); } void CL_QTVDemos_f (void) { CL_QTVPlay_Establish(Cmd_Argv(1), Cmd_Argv(2), "DEMOLIST\n"); } /* ==================== CL_FinishTimeDemo ==================== */ void CL_FinishTimeDemo (void) { int frames; float time; cvar_t *vw; cls.timedemo = false; // loading frames don't count if (cls.td_startframe == -1) { Con_Printf ("demo didn't finish loading\n"); frames = 0; } else frames = (host_framecount - cls.td_startframe) - 1; time = Sys_DoubleTime() - cls.td_starttime; if (!time) time = 1; Con_Printf ("%i frames %5.1f seconds %5.1f fps\n", frames, time, frames/time); cls.td_startframe = 0; TP_ExecTrigger ("f_timedemoend", true); vw = Cvar_FindVar("vid_wait"); Cvar_Set(vw, vw->string); } /* ==================== CL_TimeDemo_f timedemo [demoname] ==================== */ void CL_TimeDemo_f (void) { cvar_t *vw; if (Cmd_Argc() != 2) { Con_Printf ("timedemo : gets demo speeds\n"); return; } cls.demonum = -1; //stop the demo reel. the user will probably want to read the results. CL_PlayDemo_f (); if (cls.state != ca_demostart) return; vw = Cvar_FindVar("vid_wait"); if (vw) { char *t = vw->string; vw->string = "0"; vw->value = 0; Cvar_ForceCallback(vw); vw->string = t; } //read the initial frame so load times don't count as part of the time // CL_ReadPackets(); // cls.td_starttime will be grabbed at the second frame of the demo, so // all the loading time doesn't get counted cls.timedemo = true; cls.td_starttime = Sys_DoubleTime(); cls.td_startframe = -1; cls.td_lastframe = -1; // get a new message this frame } ================================================ FILE: engine/client/cl_ents.c ================================================ /* Copyright (C) 1996-1997 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. */ // cl_ents.c -- entity parsing and management #include "quakedef.h" #include "particles.h" #include "shader.h" #include "glquake.h" extern cvar_t cl_predict_players; extern cvar_t cl_predict_players_frac; extern cvar_t cl_predict_players_latency; extern cvar_t cl_predict_players_nudge; extern cvar_t cl_lerp_players; extern cvar_t cl_lerp_maxinterval; extern cvar_t cl_lerp_maxdistance; extern cvar_t cl_solid_players; extern cvar_t cl_item_bobbing; extern cvar_t r_rocketlight; extern cvar_t r_lightflicker; extern cvar_t r_dimlight_colour; extern cvar_t r_brightlight_colour; extern cvar_t r_redlight_colour; extern cvar_t r_greenlight_colour; extern cvar_t r_bluelight_colour; extern cvar_t cl_r2g; extern cvar_t r_powerupglow; extern cvar_t v_powerupshell; extern cvar_t cl_nolerp; extern cvar_t cl_nolerp_netquake; extern cvar_t r_torch; extern cvar_t r_shadows; extern cvar_t r_showbboxes; extern cvar_t gl_simpleitems; float r_blobshadows; extern cvar_t cl_gibfilter, cl_deadbodyfilter; extern int cl_playerindex; static qboolean cl_expandvisents; extern world_t csqc_world; static struct predicted_player { int flags; qboolean active; vec3_t origin; // predicted origin vec3_t oldo; vec3_t olda; vec3_t oldv; qboolean predict; player_state_t *oldstate; } predicted_players[MAX_CLIENTS]; static void CL_LerpNetFrameState(framestate_t *fs, lerpents_t *le); void CL_PlayerFrameUpdated(player_state_t *plstate, entity_state_t *state, int sequence); void CL_AckedInputFrame(int inseq, int outseq, qboolean worldstateokay); extern int cl_playerindex, cl_h_playerindex, cl_rocketindex, cl_grenadeindex, cl_gib1index, cl_gib2index, cl_gib3index; qboolean CL_FilterModelindex(int modelindex, int frame) { if (modelindex == cl_playerindex) { if (cl_deadbodyfilter.ival == 2) { if (frame >= 41 && frame <= 102) return true; } else if (cl_deadbodyfilter.ival) { if (frame == 49 || frame == 60 || frame == 69 || frame == 84 || frame == 93 || frame == 102) return true; } } if (cl_gibfilter.ival && ( modelindex == cl_h_playerindex || modelindex == cl_gib1index || modelindex == cl_gib2index || modelindex == cl_gib3index)) return true; return false; } static void *AllocateBoneSpace(packet_entities_t *pack, unsigned char bonecount, unsigned int *allocationpos) { size_t space = bonecount * sizeof(short)*7; void *r; if (pack->bonedatacur + space > pack->bonedatamax) { //expand the storage as needed. messy, but whatever. pack->bonedatamax = pack->bonedatacur + space; pack->bonedata = BZ_Realloc(pack->bonedata, pack->bonedatamax); } r = pack->bonedata + pack->bonedatacur; *allocationpos = pack->bonedatacur; pack->bonedatacur += space; return r; } void *GetBoneSpace(packet_entities_t *pack, unsigned int allocationpos) { if (allocationpos >= pack->bonedatacur) return NULL; return pack->bonedata + allocationpos; } //============================================================ void CL_FreeDlights(void) { #ifdef RTLIGHTS int i; if (cl_dlights) for (i = 0; i < rtlights_max; i++) { if (cl_dlights[i].customstyle) Z_Free(cl_dlights[i].customstyle); if (cl_dlights[i].worldshadowmesh) SH_FreeShadowMesh(cl_dlights[i].worldshadowmesh); #ifdef GLQUAKE if (cl_dlights[i].coronaocclusionquery) qglDeleteQueriesARB(1, &cl_dlights[i].coronaocclusionquery); #endif } #endif rtlights_max = cl_maxdlights = 0; BZ_Free(cl_dlights); cl_dlights = NULL; } void CL_InitDlights(void) { CL_FreeDlights(); rtlights_max = cl_maxdlights = RTL_FIRST; cl_dlights = BZ_Realloc(cl_dlights, sizeof(*cl_dlights)*cl_maxdlights); memset(cl_dlights, 0, sizeof(*cl_dlights)*cl_maxdlights); } void CL_CloneDlight(dlight_t *dl, dlight_t *src) { char *customstyle = dl->customstyle; void *sm = dl->worldshadowmesh; unsigned int oq = dl->coronaocclusionquery; unsigned int oqr = (dl->key == src->key)?dl->coronaocclusionresult:false; memcpy (dl, src, sizeof(*dl)); dl->coronaocclusionquery = oq; dl->coronaocclusionresult = oqr; dl->rebuildcache = true; dl->worldshadowmesh = sm; dl->customstyle = src->customstyle?Z_StrDup(src->customstyle):NULL; Z_Free(customstyle); } static void CL_ClearDlight(dlight_t *dl, int key, qboolean reused) { void *sm = dl->worldshadowmesh; unsigned int oq = dl->coronaocclusionquery; unsigned int oqr = reused?dl->coronaocclusionresult:false; Z_Free(dl->customstyle); memset (dl, 0, sizeof(*dl)); dl->coronaocclusionquery = oq; dl->coronaocclusionresult = oqr; dl->rebuildcache = true; dl->worldshadowmesh = sm; dl->axis[0][0] = 1; dl->axis[1][1] = 1; dl->axis[2][2] = 1; dl->key = key; dl->flags = LFLAG_DYNAMIC; dl->color[0] = 1; dl->color[1] = 1; dl->color[2] = 1; dl->corona = bound(0, 1 * 0.25, 1); dl->coronascale = bound(0, r_flashblendscale.value, 1); dl->style = -1; #ifdef RTLIGHTS dl->lightcolourscales[0] = r_shadow_realtime_dlight_ambient.value; dl->lightcolourscales[1] = r_shadow_realtime_dlight_diffuse.value; dl->lightcolourscales[2] = r_shadow_realtime_dlight_specular.value; #endif // if (r_shadow_realtime_dlight_shadowmap.value) // dl->flags |= LFLAG_SHADOWMAP; } dlight_t *CL_AllocSlight(void) { dlight_t *dl; int i; for (i = RTL_FIRST; i < rtlights_max; i++) { if (cl_dlights[i].radius <= 0) break; } if (i == rtlights_max) { if (rtlights_max == cl_maxdlights) { cl_maxdlights = rtlights_max+8; cl_dlights = BZ_Realloc(cl_dlights, sizeof(*cl_dlights)*cl_maxdlights); memset(&cl_dlights[rtlights_max], 0, sizeof(*cl_dlights)*(cl_maxdlights-rtlights_max)); } i = rtlights_max++; } dl = &cl_dlights[i]; CL_ClearDlight(dl, 0, false); dl->flags = LFLAG_REALTIMEMODE; dl->corona = 0; return dl; } /* =============== CL_AllocDlight =============== */ dlight_t *CL_AllocDlight (int key) { int i; dlight_t *dl; // first look for an exact key match if (key) { dl = cl_dlights+rtlights_first; for (i=rtlights_first ; ikey == key) { CL_ClearDlight(dl, key, true); return dl; } } } //default to the first dl = &cl_dlights[rtlights_first?rtlights_first-1:0]; //try and find one that is free for (i=RTL_FIRST; i > rtlights_first && i > 0; ) { i--; if (!cl_dlights[i].radius) { dl = &cl_dlights[i]; break; } } if (rtlights_first > dl - cl_dlights) rtlights_first = dl - cl_dlights; CL_ClearDlight(dl, key, false); return dl; } dlight_t *CL_AllocDlightOrg (int keyidx, vec3_t keyorg) { int i; dlight_t *dl; // first look for an exact key match dl = cl_dlights+rtlights_first; for (i=rtlights_first ; ikey == keyidx && VectorCompare(dl->origin, keyorg)) { CL_ClearDlight(dl, keyidx, true); VectorCopy(keyorg, dl->origin); return dl; } } //default to the first dl = &cl_dlights[rtlights_first?rtlights_first-1:0]; //try and find one that is free for (i=RTL_FIRST; i > rtlights_first && i > 0; ) { i--; if (!cl_dlights[i].radius) { dl = &cl_dlights[i]; break; } } if (rtlights_first > dl - cl_dlights) rtlights_first = dl - cl_dlights; CL_ClearDlight(dl, keyidx, false); VectorCopy(keyorg, dl->origin); return dl; } /* =============== CL_NewDlight =============== */ dlight_t *CL_NewDlight (int key, const vec3_t org, float radius, float time, float r, float g, float b) { dlight_t *dl; dl = CL_AllocDlight (key); VectorCopy(org, dl->origin); dl->radius = radius; dl->die = cl.time + time; dl->color[0] = r; dl->color[1] = g; dl->color[2] = b; return dl; } /* =============== CL_DecayLights =============== */ void CL_DecayLights (void) { int i; dlight_t *dl; float frametime = host_frametime; if (cl.paused) //DON'T DO IT!!! frametime = 0; dl = cl_dlights+rtlights_first; for (i=rtlights_first ; iradius) { continue; } if (!dl->die) { continue; } if (dl->die < (float)cl.time) { if (i==rtlights_first) rtlights_first++; dl->radius = 0; continue; } if (r_dynamic.ival == 2) { //don't decay quite so fast, this should aproximate winquake a bit better. dl->die -= frametime * 0.5; dl->radius -= frametime*dl->decay * 0.5; } else dl->radius -= frametime*dl->decay; if (dl->radius < 0) { if (i==rtlights_first) rtlights_first++; dl->radius = 0; continue; } if (dl->channelfade[0]) { dl->color[0] -= frametime*dl->channelfade[0]; if (dl->color[0] < 0) dl->color[0] = 0; } if (dl->channelfade[1]) { dl->color[1] -= frametime*dl->channelfade[1]; if (dl->color[1] < 0) dl->color[1] = 0; } if (dl->channelfade[2]) { dl->color[2] -= frametime*dl->channelfade[2]; if (dl->color[2] < 0) dl->color[2] = 0; } } } /* ========================================================================= PACKET ENTITY PARSING / LINKING ========================================================================= */ /* ================== CL_ParseDelta Can go from either a baseline or a previous packet_entity ================== */ //int bitcounts[32]; /// just for protocol profiling void CLQW_ParseDelta (entity_state_t *from, entity_state_t *to, int bits) { int i; #ifdef PROTOCOLEXTENSIONS int morebits=0; #endif // set everything to the state we are delta'ing from *to = *from; to->number = bits & 511; to->sequence = cls.netchan.incoming_sequence; bits &= ~511; if (bits & U_MOREBITS) { // read in the low order bits i = MSG_ReadByte (); bits |= i; } // count the bits for net profiling // for (i=0 ; i<16 ; i++) // if (bits&(1<number += 512; if ((morebits & U_ENTITYDBL2) && (cls.fteprotocolextensions & PEXT_ENTITYDBL2)) to->number += 1024; if (bits & U_MODEL) { to->modelindex = MSG_ReadByte (); if (morebits & U_MODELDBL && (cls.fteprotocolextensions & PEXT_MODELDBL)) to->modelindex += 256; } else if (morebits & U_MODELDBL && (cls.fteprotocolextensions & PEXT_MODELDBL)) to->modelindex = MSG_ReadShort(); if (bits & U_FRAME) to->frame = MSG_ReadByte (); if (bits & U_COLORMAP) to->colormap = MSG_ReadByte(); if (bits & U_SKIN) { to->skinnum = MSG_ReadByte(); if (to->skinnum >= 256-32) /*final 32 skins are taken as a content value instead*/ to->skinnum = (char)to->skinnum; } if (bits & U_EFFECTS) to->effects = (to->effects&0xff00)|MSG_ReadByte(); if (bits & U_ORIGIN1) { if (cls.ezprotocolextensions1 & EZPEXT1_FLOATENTCOORDS) to->origin[0] = MSG_ReadCoordFloat (); else to->origin[0] = MSG_ReadCoord (); } if (bits & U_ANGLE1) to->angles[0] = MSG_ReadAngle (); if (bits & U_ORIGIN2) { if (cls.ezprotocolextensions1 & EZPEXT1_FLOATENTCOORDS) to->origin[1] = MSG_ReadCoordFloat (); else to->origin[1] = MSG_ReadCoord (); } if (bits & U_ANGLE2) to->angles[1] = MSG_ReadAngle (); if (bits & U_ORIGIN3) { if (cls.ezprotocolextensions1 & EZPEXT1_FLOATENTCOORDS) to->origin[2] = MSG_ReadCoordFloat (); else to->origin[2] = MSG_ReadCoord (); } if (bits & U_ANGLE3) to->angles[2] = MSG_ReadAngle (); to->solidsize = ES_SOLID_BSP; if (bits & U_SOLID) { //doesn't mean anything in vanilla. solidity is infered instead. } #ifdef PEXT_SCALE if ((morebits & U_SCALE) && (cls.fteprotocolextensions & PEXT_SCALE)) to->scale = MSG_ReadByte(); #endif #ifdef PEXT_TRANS if ((morebits & U_TRANS) && (cls.fteprotocolextensions & PEXT_TRANS)) to->trans = MSG_ReadByte(); #endif #ifdef PEXT_FATNESS if ((morebits & U_FATNESS) && (cls.fteprotocolextensions & PEXT_FATNESS)) to->fatness = MSG_ReadChar(); #endif if ((morebits & U_DRAWFLAGS) && (cls.fteprotocolextensions & PEXT_HEXEN2)) to->hexen2flags = MSG_ReadByte(); if ((morebits & U_ABSLIGHT) && (cls.fteprotocolextensions & PEXT_HEXEN2)) to->abslight = MSG_ReadByte(); if ((morebits & U_COLOURMOD) && (cls.fteprotocolextensions & PEXT_COLOURMOD)) { to->colormod[0] = MSG_ReadByte(); to->colormod[1] = MSG_ReadByte(); to->colormod[2] = MSG_ReadByte(); } if (morebits & U_DPFLAGS)// && cls.fteprotocolextensions & PEXT_DPFLAGS) { // these are bits for the 'flags' field of the entity_state_t i = MSG_ReadByte(); to->dpflags = i; } if (!(cls.fteprotocolextensions & PEXT_DPFLAGS)) { if (to->frame) to->dpflags |= RENDER_STEP; } if (morebits & U_TAGINFO) { to->tagentity = MSG_ReadShort(); to->tagindex = MSG_ReadShort(); } if (morebits & U_LIGHT) { to->light[0] = MSG_ReadShort(); to->light[1] = MSG_ReadShort(); to->light[2] = MSG_ReadShort(); to->light[3] = MSG_ReadShort(); to->lightstyle = MSG_ReadByte(); to->lightpflags = MSG_ReadByte(); } if (morebits & U_EFFECTS16) to->effects = (to->effects&0x00ff)|(MSG_ReadByte()<<8); } /* ================= FlushEntityPacket ================= */ void FlushEntityPacket (void) { int word; entity_state_t olde, newe; Con_DPrintf ("FlushEntityPacket\n"); memset (&olde, 0, sizeof(olde)); if ((cl.validsequence&UPDATE_MASK) == (cls.netchan.incoming_sequence&UPDATE_MASK)) cl.validsequence = 0; // last-known-good sequence is becoming invalid. cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK].invalid = true; // read it all, but ignore it while (1) { word = (unsigned short)MSG_ReadShort (); if (msg_badread) { // something didn't parse right... Host_EndGame ("msg_badread in packetentities"); return; } if (!word) break; // done CLQW_ParseDelta (&olde, &newe, word); } } void CLFTE_ReadDelta(unsigned int entnum, entity_state_t *news, entity_state_t *olds, entity_state_t *baseline, packet_entities_t *newp, packet_entities_t *oldp) { unsigned int predbits = 0; unsigned int bits; bits = MSG_ReadByte(); if (bits & UF_EXTEND1) bits |= MSG_ReadByte()<<8; if (bits & UF_EXTEND2) bits |= MSG_ReadByte()<<16; if (bits & UF_EXTEND3) bits |= MSG_ReadByte()<<24; if (bits & UF_EXTEND4) Host_EndGame("ent update bit %#x\n", UF_EXTEND4); if (cl_shownet.ival >= 3) Con_Printf("%3i: Update %4i 0x%x\n", MSG_GetReadCount(), entnum, bits); if (bits & UF_RESET) { // Con_Printf("%3i: Reset %i @ %i\n", msg_readcount, entnum, cls.netchan.incoming_sequence); *news = *baseline; } else if (!olds) { /*reset got lost, probably the data will be filled in later - FIXME: we should probably ignore this entity*/ Con_DPrintf("New entity %i without reset\n", entnum); *news = nullentitystate; // *news = *baseline; } else *news = *olds; news->number = entnum; news->sequence = cls.netchan.incoming_sequence; if (bits & UF_FRAME) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->frame = MSG_ReadULEB128(); else { if (bits & UF_16BIT_LERPTIME) news->frame = MSG_ReadShort(); else news->frame = MSG_ReadByte(); } } if (cls.ezprotocolextensions1 & EZPEXT1_FLOATENTCOORDS) { if (bits & UF_ORIGINXY) { news->origin[0] = MSG_ReadFloat(); news->origin[1] = MSG_ReadFloat(); } if (bits & UF_ORIGINZ) news->origin[2] = MSG_ReadFloat(); } else { if (bits & UF_ORIGINXY) { news->origin[0] = MSG_ReadCoord(); news->origin[1] = MSG_ReadCoord(); } if (bits & UF_ORIGINZ) news->origin[2] = MSG_ReadCoord(); } if ((bits & UF_PREDINFO) && !(cls.fteprotocolextensions2 & PEXT2_PREDINFO)) { /*predicted stuff gets more precise angles*/ if (bits & UF_ANGLESXZ) { news->angles[0] = MSG_ReadAngle16(); news->angles[2] = MSG_ReadAngle16(); } if (bits & UF_ANGLESY) news->angles[1] = MSG_ReadAngle16(); } else { if (bits & UF_ANGLESXZ) { news->angles[0] = MSG_ReadAngle(); news->angles[2] = MSG_ReadAngle(); } if (bits & UF_ANGLESY) news->angles[1] = MSG_ReadAngle(); } if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) { if (bits & UF_16BIT_LERPTIME) news->lerpend = cl.gametime + MSG_ReadULEB128()*(1/1000.0); //most things will animate at 100ms, so this will usually fit a single byte, without capping out. if (bits & UF_EFFECTS) news->effects = MSG_ReadULEB128(); if (bits & UF_EFFECTS2_OLD) Host_EndGame("Received unexpected (redefined) bit %#x\n", UF_EFFECTS2_OLD); } else { if ((bits & (UF_EFFECTS | UF_EFFECTS2_OLD)) == (UF_EFFECTS | UF_EFFECTS2_OLD)) news->effects = MSG_ReadLong(); else if (bits & UF_EFFECTS2_OLD) news->effects = (unsigned short)MSG_ReadShort(); else if (bits & UF_EFFECTS) news->effects = MSG_ReadByte(); } news->u.q1.movement[0] = 0; news->u.q1.movement[1] = 0; news->u.q1.movement[2] = 0; news->u.q1.velocity[0] = 0; news->u.q1.velocity[1] = 0; news->u.q1.velocity[2] = 0; if (bits & UF_PREDINFO) { predbits = MSG_ReadByte(); if (predbits & UFP_FORWARD) news->u.q1.movement[0] = MSG_ReadShort(); else news->u.q1.movement[0] = 0; if (predbits & UFP_SIDE) news->u.q1.movement[1] = MSG_ReadShort(); else news->u.q1.movement[1] = 0; if (predbits & UFP_UP) news->u.q1.movement[2] = MSG_ReadShort(); else news->u.q1.movement[2] = 0; if (predbits & UFP_MOVETYPE) news->u.q1.pmovetype = MSG_ReadByte(); if (predbits & UFP_VELOCITYXY) { news->u.q1.velocity[0] = MSG_ReadShort(); news->u.q1.velocity[1] = MSG_ReadShort(); } else { news->u.q1.velocity[0] = 0; news->u.q1.velocity[1] = 0; } if (predbits & UFP_VELOCITYZ) news->u.q1.velocity[2] = MSG_ReadShort(); else news->u.q1.velocity[2] = 0; if (predbits & UFP_MSEC) news->u.q1.msec = MSG_ReadByte(); else news->u.q1.msec = 0; if (cls.fteprotocolextensions2 & PEXT2_PREDINFO) { if (predbits & UFP_VIEWANGLE) { if (bits & UF_ANGLESXZ) { news->u.q1.vangle[0] = MSG_ReadShort(); news->u.q1.vangle[2] = MSG_ReadShort(); } if (bits & UF_ANGLESY) news->u.q1.vangle[1] = MSG_ReadShort(); } } else { if (predbits & UFP_WEAPONFRAME_OLD) { news->u.q1.weaponframe = MSG_ReadByte(); if (news->u.q1.weaponframe & 0x80) news->u.q1.weaponframe = (news->u.q1.weaponframe & 127) | (MSG_ReadByte()<<7); } } } else { news->u.q1.msec = 0; } if (!(predbits & UFP_VIEWANGLE) || !(cls.fteprotocolextensions2 & PEXT2_PREDINFO)) { if (bits & UF_ANGLESXZ) news->u.q1.vangle[0] = ANGLE2SHORT(news->angles[0] * ((bits & UF_PREDINFO)?-3:-1)); if (bits & UF_ANGLESY) news->u.q1.vangle[1] = ANGLE2SHORT(news->angles[1]); if (bits & UF_ANGLESXZ) news->u.q1.vangle[2] = ANGLE2SHORT(news->angles[2]); } if (bits & UF_MODEL) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->modelindex = MSG_ReadULEB128(); else { if (bits & UF_16BIT_LERPTIME) news->modelindex = MSG_ReadShort(); else news->modelindex = MSG_ReadByte(); } } if (bits & UF_SKIN) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->skinnum = MSG_ReadULEB128()-64; //biased for content overrides else { if (bits & UF_16BIT_LERPTIME) news->skinnum = MSG_ReadShort(); else news->skinnum = MSG_ReadByte(); } } if (bits & UF_COLORMAP) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->colormap = MSG_ReadULEB128(); else news->colormap = MSG_ReadByte(); } if (bits & UF_SOLID) { if (cls.fteprotocolextensions2 & PEXT2_NEWSIZEENCODING) { qbyte enc = MSG_ReadByte(); if (enc == 0) news->solidsize = ES_SOLID_NOT; else if (enc == 1) news->solidsize = ES_SOLID_BSP; else if (enc == 2) news->solidsize = ES_SOLID_HULL1; else if (enc == 3) news->solidsize = ES_SOLID_HULL2; else if (enc == 16) news->solidsize = MSG_ReadSize16(&net_message); else if (enc == 32) news->solidsize = MSG_ReadLong(); else Sys_Error("Solid+Size encoding not known"); } else news->solidsize = MSG_ReadSize16(&net_message); } if (bits & UF_FLAGS) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->dpflags = MSG_ReadULEB128(); else news->dpflags = MSG_ReadByte(); } if (bits & UF_ALPHA) news->trans = MSG_ReadByte(); if (bits & UF_SCALE) news->scale = MSG_ReadByte(); if (bits & UF_BONEDATA) { unsigned char fl = MSG_ReadByte(); if (fl & 0x80) { //this is NOT finalized short *bonedata; int i; news->bonecount = MSG_ReadByte(); bonedata = AllocateBoneSpace(newp, news->bonecount, &news->boneoffset); for (i = 0; i < news->bonecount*7; i++) bonedata[i] = MSG_ReadShort(); } else news->bonecount = 0; //oo, it went away. if (fl & 0x40) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) { news->basebone = MSG_ReadULEB128(); news->baseframe = MSG_ReadULEB128(); } else { news->basebone = MSG_ReadByte(); news->baseframe = MSG_ReadShort(); } } else { news->basebone = 0; news->baseframe = 0; } //fixme: basebone, baseframe, etc. if (fl & 0x3f) Host_EndGame("unsupported entity delta info\n"); } else if (news->bonecount) { //still has bone data from the previous frame. short *bonedata = AllocateBoneSpace(newp, news->bonecount, &news->boneoffset); memcpy(bonedata, oldp->bonedata+olds->boneoffset, sizeof(short)*7*news->bonecount); } if (bits & UF_DRAWFLAGS) { news->hexen2flags = MSG_ReadByte(); if ((news->hexen2flags & MLS_MASK) >= MLS_ADDLIGHT) news->abslight = MSG_ReadByte(); else news->abslight = 0; } if (bits & UF_TAGINFO) { news->tagentity = MSGCL_ReadEntity(); if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->tagindex = MSG_ReadULEB128()-1; //biased for q3-like portals. else { news->tagindex = MSG_ReadByte(); if (news->tagindex == 0xff) news->tagindex = ~0; } } if (bits & UF_LIGHT) { news->light[0] = MSG_ReadShort(); news->light[1] = MSG_ReadShort(); news->light[2] = MSG_ReadShort(); news->light[3] = MSG_ReadShort(); if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->lightstyle = MSG_ReadULEB128(); else news->lightstyle = MSG_ReadByte(); news->lightpflags = MSG_ReadByte(); } if (bits & UF_TRAILEFFECT) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) { news->u.q1.traileffectnum = MSG_ReadULEB128(); news->u.q1.emiteffectnum = MSG_ReadULEB128(); } else { unsigned short s; s = MSG_ReadShort(); news->u.q1.traileffectnum = s & 0x3fff; if (s & 0x8000) news->u.q1.emiteffectnum = MSG_ReadShort() & 0x3fff; else news->u.q1.emiteffectnum = 0; } if (news->u.q1.traileffectnum >= countof(cl.particle_ssprecache)) news->u.q1.traileffectnum = 0; if (news->u.q1.emiteffectnum >= countof(cl.particle_ssprecache)) news->u.q1.emiteffectnum = 0; } if (bits & UF_COLORMOD) { news->colormod[0] = MSG_ReadByte(); news->colormod[1] = MSG_ReadByte(); news->colormod[2] = MSG_ReadByte(); } if (bits & UF_GLOW) { news->glowsize = MSG_ReadByte(); news->glowcolour = MSG_ReadByte(); news->glowmod[0] = MSG_ReadByte(); news->glowmod[1] = MSG_ReadByte(); news->glowmod[2] = MSG_ReadByte(); } if (bits & UF_FATNESS) news->fatness = MSG_ReadByte(); if (bits & UF_MODELINDEX2) { if (cls.fteprotocolextensions2 & PEXT2_LERPTIME) news->modelindex2 = MSG_ReadULEB128(); else { if (bits & UF_16BIT_LERPTIME) news->modelindex2 = MSG_ReadShort(); else news->modelindex2 = MSG_ReadByte(); } } if (bits & UF_GRAVITYDIR) { news->u.q1.gravitydir[0] = MSG_ReadByte(); news->u.q1.gravitydir[1] = MSG_ReadByte(); } if (bits & UF_UNUSED1) { Host_EndGame("ent update bit %#x\n", UF_UNUSED1); } } void CLFTE_ParseBaseline(entity_state_t *es, qboolean numberisimportant) { int entnum = 0; if (numberisimportant) entnum = MSGCL_ReadEntity(); CLFTE_ReadDelta(entnum, es, &nullentitystate, &nullentitystate, NULL, NULL); } void CL_PredictEntityMovement(entity_state_t *estate, float age); /* Note: strictly speaking, you don't need multiple frames, just two and flip between them. FTE retains the full 64 frames because its interpolation will go multiple packets back in time to cover packet loss. */ void CLFTE_ParseEntities(void) { int oldpacket, newpacket; packet_entities_t *oldp, *newp, nullp; entity_state_t *news, *olds; unsigned int newnum, oldnum; int oldindex; qboolean isvalid = false; qboolean removeflag; int inputframe = cls.netchan.incoming_sequence; #if defined(QUAKESTATS) || defined(NQPROT) int i; #endif // int i; // for (i = cl.validsequence+1; i < cls.netchan.incoming_sequence; i++) // { // Con_Printf("CL: Dropped %i\n", i); // } if (cls.demoplayback == DPB_MVD) { cls.netchan.incoming_sequence++; cls.netchan.incoming_acknowledged++; } #ifdef NQPROT else if (cls.protocol == CP_NETQUAKE) { cls.netchan.incoming_sequence++; cl.last_servermessage = realtime; if (cls.fteprotocolextensions2 & PEXT2_PREDINFO) { inputframe = (unsigned short)MSG_ReadShort(); inputframe = (cl.movesequence&0xffff0000) | inputframe; if (inputframe > cl.movesequence) inputframe -= 0x00010000; //err, if its in the future then cl.movesequence must have wrapped. } else inputframe = cl.movesequence; if (cl.numackframes == sizeof(cl.ackframes)/sizeof(cl.ackframes[0])) cl.numackframes--; if (!cl.validsequence) cl.ackframes[cl.numackframes++] = -1; else cl.ackframes[cl.numackframes++] = cls.netchan.incoming_unreliable; { extern vec3_t demoangles; int fr = cls.netchan.incoming_sequence&UPDATE_MASK; for (i = 0; i < MAX_SPLITS; i++) cl.inframes[fr&UPDATE_MASK].packet_entities.fixangles[i] = false; if (cls.demoplayback) { cl.inframes[fr&UPDATE_MASK].packet_entities.fixangles[0] = 2; VectorCopy(demoangles, cl.inframes[fr&UPDATE_MASK].packet_entities.fixedangles[0]); } } // if (cl.validsequence != cls.netchan.incoming_sequence-1) // Con_Printf("CLIENT: Dropped a frame\n"); } #endif newpacket = cls.netchan.incoming_sequence&UPDATE_MASK; oldpacket = cl.validsequence&UPDATE_MASK; newp = &cl.inframes[newpacket].packet_entities; oldp = &cl.inframes[oldpacket].packet_entities; cl.inframes[newpacket].invalid = true; cl.inframes[newpacket].receivedtime = realtime; cl.inframes[newpacket].frameid = cls.netchan.incoming_sequence; #ifdef QUAKESTATS for (i = 0; i < cl.splitclients; i++) { cl.inframes[newpacket&UPDATE_MASK].packet_entities.punchangle[i][0] = cl.playerview[i].statsf[STAT_PUNCHANGLE_X]; cl.inframes[newpacket&UPDATE_MASK].packet_entities.punchangle[i][1] = cl.playerview[i].statsf[STAT_PUNCHANGLE_Y]; cl.inframes[newpacket&UPDATE_MASK].packet_entities.punchangle[i][2] = cl.playerview[i].statsf[STAT_PUNCHANGLE_Z]; cl.inframes[newpacket&UPDATE_MASK].packet_entities.punchorigin[i][0] = cl.playerview[i].statsf[STAT_PUNCHVECTOR_X]; cl.inframes[newpacket&UPDATE_MASK].packet_entities.punchorigin[i][1] = cl.playerview[i].statsf[STAT_PUNCHVECTOR_Y]; cl.inframes[newpacket&UPDATE_MASK].packet_entities.punchorigin[i][2] = cl.playerview[i].statsf[STAT_PUNCHVECTOR_Z]; } #endif if (!cl.validsequence || cls.netchan.incoming_sequence-cl.validsequence >= UPDATE_BACKUP-1 || oldp == newp) { //yes, this results in a load of invalid packets for a while. //server is meant to notice and send a reset packet, which causes it to become valid again oldp = &nullp; oldp->num_entities = 0; oldp->max_entities = 0; } else isvalid = true; newp->servertime = MSG_ReadFloat(); if (cl.gametime != newp->servertime) { cl.oldgametime = cl.gametime; cl.oldgametimemark = cl.gametimemark; cl.gametime = newp->servertime; cl.gametimemark = realtime; } /*clear all entities*/ newp->num_entities = 0; newp->bonedatacur = 0; oldindex = 0; while(1) { //high bit means remove, second high bit means 22bit index newnum = (unsigned short)(short)MSG_ReadShort(); removeflag = !!(newnum & 0x8000); if (newnum & 0x4000) newnum = (newnum & 0x3fff) | (MSG_ReadByte()<<14); else newnum &= ~0x8000; if ((!newnum && !removeflag) || msg_badread) { /*reached the end, don't forget old entities*/ while(oldindex < oldp->num_entities) { if (newp->num_entities >= newp->max_entities) { newp->max_entities = newp->num_entities+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } //copy it over news = &newp->entities[newp->num_entities++]; olds = &oldp->entities[oldindex++]; *news = *olds; if (news->bonecount) { //still has bone data somehow. short *bonedata = AllocateBoneSpace(newp, news->bonecount, &news->boneoffset); memcpy(bonedata, oldp->bonedata+olds->boneoffset, sizeof(short)*7*news->bonecount); } } break; } oldnum = (oldindex >= oldp->num_entities) ? 0xffffffff : oldp->entities[oldindex].number; /*if we skipped some, then they were unchanged*/ while (newnum > oldnum) { if (newp->num_entities >= newp->max_entities) { newp->max_entities = newp->num_entities+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } //copy it over news = &newp->entities[newp->num_entities++]; olds = &oldp->entities[oldindex++]; *news = *olds; if (news->bonecount) { //still has bone data somehow. short *bonedata = AllocateBoneSpace(newp, news->bonecount, &news->boneoffset); memcpy(bonedata, oldp->bonedata+olds->boneoffset, sizeof(short)*7*news->bonecount); } oldnum = (oldindex >= oldp->num_entities) ? 0xffffffff : oldp->entities[oldindex].number; } if (removeflag) { if (cl_shownet.ival >= 3) Con_Printf("%3i: Remove %i @ %i\n", MSG_GetReadCount(), newnum, cls.netchan.incoming_sequence); if (!newnum) { /*removal of world - means forget all entities*/ if (cl_shownet.ival >= 3) Con_Printf("%3i: Reset all\n", MSG_GetReadCount()); newp->num_entities = 0; oldp = &nullp; oldp->num_entities = 0; oldp->max_entities = 0; isvalid = true; cls.demohadkeyframe = true; //we can reactivate deltas when recording now. continue; } if (oldnum == newnum) oldindex++; continue; } else { if (!CL_CheckBaselines(newnum)) Host_EndGame("CL_ParsePacketEntities: check baselines failed with size %i", newnum); if (newp->num_entities >= newp->max_entities) { newp->max_entities = newp->num_entities+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } if (oldnum == newnum) CLFTE_ReadDelta(newnum, &newp->entities[newp->num_entities++], &oldp->entities[oldindex++], cl_baselines + newnum, newp, oldp); else CLFTE_ReadDelta(newnum, &newp->entities[newp->num_entities++], NULL, cl_baselines + newnum, newp, NULL); } } if (cl.do_lerp_players) { float packetage = (realtime - cl.outframes[cl.ackedmovesequence & UPDATE_MASK].senttime) - cls.latency*cl_predict_players_latency.value + cl_predict_players_nudge.value; //predict in-place based upon calculated latencies and stuff, stuff can then be interpolated properly for (oldindex = 0; oldindex < newp->num_entities; oldindex++) { CL_PredictEntityMovement(newp->entities + oldindex, (newp->entities[oldindex].u.q1.msec / 1000.0f + packetage) *0.5); } } if (isvalid) { cl.oldvalidsequence = cl.validsequence; cl.validsequence = cls.netchan.incoming_sequence; CL_AckedInputFrame(cls.netchan.incoming_sequence, inputframe, true); cl.inframes[newpacket].invalid = false; } else { newp->num_entities = 0; cl.validsequence = 0; CL_AckedInputFrame(cls.netchan.incoming_sequence, inputframe, false); } } /* ================== CL_ParsePacketEntities An svc_packetentities has just been parsed, deal with the rest of the data stream. ================== */ void CLQW_ParsePacketEntities (qboolean delta) { int oldpacket, newpacket; packet_entities_t *oldp, *newp, dummy; int oldindex, newindex; int word, newnum, oldnum; qboolean full; int from; newpacket = cls.netchan.incoming_sequence&UPDATE_MASK; newp = &cl.inframes[newpacket].packet_entities; cl.inframes[newpacket].invalid = false; cl.inframes[newpacket].frameid = cls.netchan.incoming_sequence; cl.inframes[newpacket].receivedtime = realtime; if (cls.protocol == CP_QUAKEWORLD && cls.demoplayback == DPB_MVD) { extern float olddemotime; //time from the most recent demo packet cl.oldgametime = cl.gametime; cl.oldgametimemark = cl.gametimemark; cl.gametime = olddemotime + cl.demogametimebias; cl.gametimemark = realtime; } else if (!(cls.fteprotocolextensions & PEXT_ACCURATETIMINGS) && cls.protocol == CP_QUAKEWORLD) { extern cvar_t cl_demospeed; float scale = cls.demoplayback?cl_demospeed.value:1; cl.oldgametime = cl.gametime; cl.oldgametimemark = cl.gametimemark; if (realtime - cl.gametimemark > 0) cl.gametime += (realtime - cl.gametimemark)*scale;//cl.frames[newpacket].senttime - cl.frames[(newpacket-1)&UPDATE_MASK].senttime; cl.gametimemark = realtime; } newp->servertime = cl.gametime; if (delta) { from = MSG_ReadByte (); // Con_Printf("%i %i from %i\n", cls.netchan.outgoing_sequence, cls.netchan.incoming_sequence, from); if (cls.demoplayback == DPB_MVD) from = oldpacket = cls.netchan.incoming_sequence - 1; oldpacket = cl.inframes[from & UPDATE_MASK].frameid; if (cl.inframes[from&UPDATE_MASK].invalid || //old frame is unusable cls.netchan.outgoing_sequence - oldpacket >= UPDATE_BACKUP - 1) // we must have lost the sequence its trying to delta from (or just too old). { FlushEntityPacket (); return; } oldp = &cl.inframes[from & UPDATE_MASK].packet_entities; full = false; } else { // this is a full update that we can start delta compressing from now oldp = &dummy; dummy.num_entities = 0; full = true; } //FIXME cl.oldvalidsequence = cl.validsequence; cl.validsequence = cls.netchan.incoming_sequence; CL_AckedInputFrame(cls.netchan.incoming_sequence, cls.netchan.incoming_sequence, true); oldindex = 0; newindex = 0; newp->num_entities = 0; while (1) { word = (unsigned short)MSG_ReadShort (); if (msg_badread) { // something didn't parse right... Host_EndGame ("msg_badread in packetentities"); return; } if (!word) { while (oldindex < oldp->num_entities) { // copy all the rest of the entities from the old packet //Con_Printf ("copy %i\n", oldp->entities[oldindex].number); if (newindex >= newp->max_entities) { newp->max_entities = newindex+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } if (oldindex >= oldp->max_entities) Host_EndGame("Old packet entity too big\n"); newp->entities[newindex] = oldp->entities[oldindex]; newindex++; oldindex++; } break; } newnum = word&511; if (word & U_MOREBITS) { int oldpos = MSG_GetReadCount(); int excessive; excessive = MSG_ReadByte(); if (excessive & U_EVENMORE) { excessive = MSG_ReadByte(); if (excessive & U_ENTITYDBL) newnum += 512; if (excessive & U_ENTITYDBL2) newnum += 1024; } MSG_ReadSkip(oldpos-MSG_GetReadCount());//undo the read... } oldnum = oldindex >= oldp->num_entities ? 9999 : oldp->entities[oldindex].number; while (newnum > oldnum) { if (full) { Con_Printf ("WARNING: oldcopy on full update"); FlushEntityPacket (); return; } //Con_Printf ("copy %i\n", oldnum); // copy one of the old entities over to the new packet unchanged if (newindex >= newp->max_entities) { newp->max_entities = newindex+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } if (oldindex >= oldp->max_entities) Host_EndGame("Old packet entity too big\n"); newp->entities[newindex] = oldp->entities[oldindex]; newindex++; oldindex++; oldnum = oldindex >= oldp->num_entities ? 9999 : oldp->entities[oldindex].number; } if (newnum < oldnum) { // new from baseline //Con_Printf ("baseline %i\n", newnum); if (word & U_REMOVE) { //really read the extra entity number if required if (word & U_MOREBITS) if (MSG_ReadByte() & U_EVENMORE) MSG_ReadByte(); if (full) { cl.validsequence = 0; Con_Printf ("WARNING: U_REMOVE on full update\n"); FlushEntityPacket (); return; } continue; } if (newindex >= newp->max_entities) { newp->max_entities = newindex+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } if (!CL_CheckBaselines(newnum)) Host_EndGame("CL_ParsePacketEntities: check baselines failed with size %i", newnum); CLQW_ParseDelta (cl_baselines + newnum, &newp->entities[newindex], word); newindex++; continue; } if (newnum == oldnum) { // delta from previous if (full) { cl.validsequence = 0; Con_Printf ("WARNING: delta on full update"); } if (word & U_REMOVE) { if (word & U_MOREBITS) if (MSG_ReadByte() & U_EVENMORE) MSG_ReadByte(); oldindex++; continue; } if (newindex >= newp->max_entities) { newp->max_entities = newindex+1; newp->entities = BZ_Realloc(newp->entities, sizeof(entity_state_t)*newp->max_entities); } //Con_Printf ("delta %i\n",newnum); CLQW_ParseDelta (&oldp->entities[oldindex], &newp->entities[newindex], word); newindex++; oldindex++; } } newp->num_entities = newindex; } entity_state_t *CL_FindOldPacketEntity(int num) { int pnum; entity_state_t *s1; packet_entities_t *pack; if (!cl.validsequence) return NULL; pack = &cl.inframes[(cls.netchan.incoming_sequence-1)&UPDATE_MASK].packet_entities; for (pnum=0 ; pnumnum_entities ; pnum++) { s1 = &pack->entities[pnum]; if (num == s1->number) return s1; } return NULL; } #ifdef NQPROT void DP5_ParseDelta(entity_state_t *s, packet_entities_t *pack) { unsigned int bits; if (cl_shownet.ival >= 3) Con_Printf("%3i: Update %i", MSG_GetReadCount(), s->number); bits = MSG_ReadByte(); if (bits & E5_EXTEND1) { bits |= MSG_ReadByte() << 8; if (bits & E5_EXTEND2) { bits |= MSG_ReadByte() << 16; if (bits & E5_EXTEND3) bits |= MSG_ReadByte() << 24; } } if (cl_shownet.ival >= 3) { if (bits & E5_FULLUPDATE) Con_Printf(" full"); if (bits & E5_ORIGIN) Con_Printf(" origin"); if (bits & E5_ANGLES) Con_Printf(" angles"); if (bits & E5_MODEL) Con_Printf(" model"); if (bits & E5_FRAME) Con_Printf(" frame"); if (bits & E5_SKIN) Con_Printf(" kin"); if (bits & E5_EFFECTS) Con_Printf(" effects"); if (bits & E5_EXTEND1) Con_Printf(" extend1"); if (bits & E5_FLAGS) Con_Printf(" flags"); if (bits & E5_ALPHA) Con_Printf(" alpha"); if (bits & E5_SCALE) Con_Printf(" scale"); if (bits & E5_ORIGIN32) Con_Printf(" origin32"); if (bits & E5_ANGLES16) Con_Printf(" angles16"); if (bits & E5_MODEL16) Con_Printf(" model16"); if (bits & E5_COLORMAP) Con_Printf(" colormap"); if (bits & E5_EXTEND2) Con_Printf(" extend2"); if (bits & E5_ATTACHMENT) Con_Printf(" attachment"); if (bits & E5_LIGHT) Con_Printf(" light"); if (bits & E5_GLOW) Con_Printf(" glow"); if (bits & E5_EFFECTS16) Con_Printf(" effects16"); if (bits & E5_EFFECTS32) Con_Printf(" effects32"); if (bits & E5_FRAME16) Con_Printf(" frame16"); if (bits & E5_COLORMOD) Con_Printf(" colormod"); if (bits & E5_EXTEND3) Con_Printf(" extend3"); if (bits & E5_GLOWMOD) Con_Printf(" glowmod"); if (bits & E5_COMPLEXANIMATION) Con_Printf(" complexanimation"); if (bits & E5_TRAILEFFECTNUM) Con_Printf(" traileffectnum"); if (bits & E5_UNUSED27) Con_Printf(" unused27"); if (bits & E5_UNUSED28) Con_Printf(" unused28"); if (bits & E5_UNUSED29) Con_Printf(" unused29"); if (bits & E5_UNUSED30) Con_Printf(" unused30"); if (bits & E5_EXTEND4) Con_Printf(" extend4"); Con_Printf("\n"); } if (bits & E5_ALLUNUSED) { Host_EndGame("Detected 'unused' bits in DP5+ entity delta - %x (%x)\n", bits, (bits & E5_ALLUNUSED)); } if (bits & E5_FULLUPDATE) { int num; num = s->number; *s = nullentitystate; s->number = num; s->solidsize = ES_SOLID_BSP; // s->active = true; } if (bits & E5_FLAGS) { int i = MSG_ReadByte(); s->dpflags = i; } if (bits & E5_ORIGIN) { if (bits & E5_ORIGIN32) { s->origin[0] = MSG_ReadFloat(); s->origin[1] = MSG_ReadFloat(); s->origin[2] = MSG_ReadFloat(); } else { s->origin[0] = MSG_ReadShort()*(1/8.0f); s->origin[1] = MSG_ReadShort()*(1/8.0f); s->origin[2] = MSG_ReadShort()*(1/8.0f); } } if (bits & E5_ANGLES) { if (bits & E5_ANGLES16) { s->angles[0] = MSG_ReadAngle16(); s->angles[1] = MSG_ReadAngle16(); s->angles[2] = MSG_ReadAngle16(); } else { s->angles[0] = MSG_ReadChar() * (360.0/256); s->angles[1] = MSG_ReadChar() * (360.0/256); s->angles[2] = MSG_ReadChar() * (360.0/256); } } if (bits & E5_MODEL) { if (bits & E5_MODEL16) s->modelindex = (unsigned short) MSG_ReadShort(); else s->modelindex = MSG_ReadByte(); } if (bits & E5_FRAME) { if (bits & E5_FRAME16) s->frame = (unsigned short) MSG_ReadShort(); else s->frame = MSG_ReadByte(); } if (bits & E5_SKIN) s->skinnum = MSG_ReadByte(); if (bits & E5_EFFECTS) { if (bits & E5_EFFECTS32) s->effects = (unsigned int) MSG_ReadLong(); else if (bits & E5_EFFECTS16) s->effects = (unsigned short) MSG_ReadShort(); else s->effects = MSG_ReadByte(); } if (bits & E5_ALPHA) s->trans = MSG_ReadByte(); if (bits & E5_SCALE) s->scale = MSG_ReadByte(); if (bits & E5_COLORMAP) s->colormap = MSG_ReadByte(); if (bits & E5_ATTACHMENT) { s->tagentity = MSGCL_ReadEntity(); s->tagindex = MSG_ReadByte(); } if (bits & E5_LIGHT) { s->light[0] = MSG_ReadShort(); s->light[1] = MSG_ReadShort(); s->light[2] = MSG_ReadShort(); s->light[3] = MSG_ReadShort(); s->lightstyle = MSG_ReadByte(); s->lightpflags = MSG_ReadByte(); } if (bits & E5_GLOW) { s->glowsize = MSG_ReadByte(); s->glowcolour = MSG_ReadByte(); } if (bits & E5_COLORMOD) { s->colormod[0] = MSG_ReadByte(); s->colormod[1] = MSG_ReadByte(); s->colormod[2] = MSG_ReadByte(); } if (bits & E5_GLOWMOD) { s->glowmod[0] = MSG_ReadByte(); s->glowmod[1] = MSG_ReadByte(); s->glowmod[2] = MSG_ReadByte(); } if (bits & E5_COMPLEXANIMATION) { int type = MSG_ReadByte(); int i, numbones; if (type == 4) { short *bonedata; /*modelindex = */MSG_ReadShort(); numbones = MSG_ReadByte(); bonedata = AllocateBoneSpace(pack, numbones, &s->boneoffset); s->bonecount = numbones; for (i = 0; i < numbones*7; i++) bonedata[i] = MSG_ReadShort(); } else if (type < 4) { //n-way blends s->bonecount = 0; type++; for (i = 0; i < type; i++) /*frame = */MSG_ReadShort(); for (i = 0; i < type; i++) /*age = */MSG_ReadShort(); for (i = 0; i < type; i++) /*frac = */(type==1)?255:MSG_ReadByte(); } else Host_Error("E5_COMPLEXANIMATION: Parse error - unknown type %i\n", type); } if (bits & E5_TRAILEFFECTNUM) s->u.q1.traileffectnum = MSG_ReadShort(); } static int QDECL CLDP_SortEntities(const void *va, const void *vb) { const entity_state_t *a = va, *b = vb; if (a->inactiveflag != b->inactiveflag) return a->inactiveflag?1:-1; if (a->number != b->number) return a->number < b->number?-1:1; return 0; } void CLDP_ParseDarkPlaces5Entities(void) //the things I do.. :o( { //the incoming entities do not come in in any order. :( //well, they come in in order of priorities, but that's not useful to us. //I guess this means we'll have to go slowly. //dp deltas update in-place //this gets in the way of tracking multiple frames, and thus doesn't match fte too well packet_entities_t *oldpack, *newpack; entity_state_t *to, *from; unsigned int read; int oldi; qboolean remove; //server->client sequence if (cl.numackframes == sizeof(cl.ackframes)/sizeof(cl.ackframes[0])) cl.numackframes--; cl.ackframes[cl.numackframes++] = MSG_ReadLong(); /*server sequence to be acked*/ //client->server sequence ack if (cls.protocol_nq >= CPNQ_DP7) CL_AckedInputFrame(cls.netchan.incoming_sequence, MSG_ReadLong(), true); /*client input sequence which has been acked*/ if (cl.validsequence) oldpack = &cl.inframes[(cl.validsequence)&UPDATE_MASK].packet_entities; else oldpack = NULL; cl.validsequence = cls.netchan.incoming_sequence; cl.inframes[(cls.netchan.incoming_sequence)&UPDATE_MASK].receivedtime = realtime; cl.inframes[(cls.netchan.incoming_sequence)&UPDATE_MASK].frameid = cls.netchan.incoming_sequence; newpack = &cl.inframes[(cls.netchan.incoming_sequence)&UPDATE_MASK].packet_entities; newpack->servertime = cl.gametime; //copy old state to new state if (newpack != oldpack) { if (oldpack) { newpack->num_entities = oldpack->num_entities; newpack->max_entities = newpack->num_entities+16; //for slop for new ents, to reduce reallocs newpack->entities = BZ_Realloc(newpack->entities, sizeof(entity_state_t)*newpack->max_entities); memcpy(newpack->entities, oldpack->entities, sizeof(entity_state_t)*newpack->num_entities); } else newpack->num_entities = 0; newpack->bonedatacur = 0; //flag them all as having old bones //they'll be renewed after parsing for (oldi=0 ; oldinum_entities ; oldi++) newpack->entities[oldi].boneoffset |= 0x80000000; } for (;;) { read = MSG_ReadShort(); if (msg_badread) Host_EndGame("Corrupt entity message packet\n"); remove = !!(read&0x8000); read&=0x7fff; if (remove && !read) break; //remove world signals end of packet. if (read >= MAX_EDICTS) Host_EndGame("Too many entities.\n"); from = &nullentitystate; to = NULL; for (oldi=0 ; oldinum_entities ; oldi++) { if (read == newpack->entities[oldi].number) { from = &newpack->entities[oldi]; to = &newpack->entities[oldi]; break; } } if (!to) { //okay, so this is new if (newpack->num_entities==newpack->max_entities) { newpack->max_entities = newpack->num_entities+16; newpack->entities = BZ_Realloc(newpack->entities, sizeof(entity_state_t)*newpack->max_entities); } to = &newpack->entities[newpack->num_entities]; newpack->num_entities++; } memcpy(to, from, sizeof(*to)); to->number = read; if (remove) { //ent is meant to be removed. flag it as such. we'll strip it out later. if (cl_shownet.ival >= 3) Con_Printf("Remove %i\n", read); to->inactiveflag = 1; to->bonecount = 0; } else { if (cl_shownet.ival > 3) Con_Printf("Update %i\n", read); DP5_ParseDelta(to, newpack); to->sequence = cls.netchan.incoming_sequence; to->inactiveflag = 0; } } qsort(newpack->entities, newpack->num_entities, sizeof(entity_state_t), CLDP_SortEntities); //get rid of any removed ents (we sorted these to the end) while (newpack->num_entities) { if (newpack->entities[newpack->num_entities-1].inactiveflag) newpack->num_entities--; else break; } //make sure any bone states are refreshed for (oldi=0, to = newpack->entities; oldinum_entities ; oldi++, to++) { if (to->bonecount && (to->boneoffset & 0x80000000)) { unsigned int oldoffset = to->boneoffset & 0x7fffffff; void *dest = AllocateBoneSpace(newpack, to->bonecount, &to->boneoffset); void *src = GetBoneSpace(oldpack, oldoffset); memcpy(dest, src, to->bonecount * sizeof(short)*7); } } } #ifdef HEXEN2 #define UH2_MOREBITS (1u<<0) #define UH2_ORIGIN1 (1u<<1) #define UH2_ORIGIN2 (1u<<2) #define UH2_ORIGIN3 (1u<<3) #define UH2_ANGLE2 (1u<<4) #define UH2_STEP (1u<<5) #define UH2_FRAME (1u<<6) #define UH2_SIGNAL (1u<<7) #define UH2_ANGLE1 (1u<<8) #define UH2_ANGLE3 (1u<<9) #define UH2_MODEL (1u<<10) //#define UH2_ (1u<<11) //#define UH2_ (1u<<12) //#define UH2_ (1u<<13) #define UH2_LONGENTITY (1u<<14) #define UH2_EVENMORE (1u<<15) #define UH2_SKIN (1u<<16) #define UH2_EFFECTS (1u<<17) #define UH2_SCALE (1u<<18) #define UH2_COLORMAP (1u<<19) void CLH2_ParseEntities(void) { //h2mp apparently uses some sort of delta compression //there's three parts to this, the start, the updates, and the removes at the end. //so we can be a bit lazy and parse the 'fast updates' here and assert they end with a final clear. //entities are ordered. packet_entities_t *oldpack, *newpack; entity_state_t *to, *from; unsigned int read, bits; int oldi; unsigned int removecount; int frame = MSG_ReadByte(); int seq = MSG_ReadByte(); //not really sure what to do with this. (void)frame; (void)seq; if (cl.validsequence) oldpack = &cl.inframes[(cl.validsequence)&UPDATE_MASK].packet_entities; else oldpack = NULL; cl.validsequence = cls.netchan.incoming_sequence; cl.inframes[(cls.netchan.incoming_sequence)&UPDATE_MASK].receivedtime = realtime; cl.inframes[(cls.netchan.incoming_sequence)&UPDATE_MASK].frameid = cls.netchan.incoming_sequence; newpack = &cl.inframes[(cls.netchan.incoming_sequence)&UPDATE_MASK].packet_entities; newpack->servertime = cl.gametime; //copy old state to new state if (newpack != oldpack) { if (oldpack) { newpack->num_entities = oldpack->num_entities; newpack->max_entities = newpack->num_entities+16; //for slop for new ents, to reduce reallocs newpack->entities = BZ_Realloc(newpack->entities, sizeof(entity_state_t)*newpack->max_entities); memcpy(newpack->entities, oldpack->entities, sizeof(entity_state_t)*newpack->num_entities); } else newpack->num_entities = 0; newpack->bonedatacur = 0; //flag them all as having old bones //they'll be renewed after parsing for (oldi=0 ; oldinum_entities ; oldi++) newpack->entities[oldi].boneoffset |= 0x80000000; } for (;;) { bits = MSG_ReadByte(); if ((bits&0x80) == 0) break; //no fast-update bit! if (bits & UH2_MOREBITS) bits |= MSG_ReadByte()<<8; if (bits & UH2_EVENMORE) bits |= MSG_ReadByte()<<16; if (bits & UH2_LONGENTITY) read = MSG_ReadUInt16(); else read = MSG_ReadByte(); if (msg_badread) Host_EndGame("Corrupt entity message packet\n"); if (!read) break; //remove world signals end of packet. if (read >= MAX_EDICTS) Host_EndGame("Too many entities.\n"); if (!CL_CheckBaselines(read)) Host_EndGame("CLNQ_ParseEntity: check baselines failed with size %i", read); from = &cl_baselines[read]; to = NULL; for (oldi=0 ; oldinum_entities ; oldi++) { if (read == newpack->entities[oldi].number) { from = &newpack->entities[oldi]; to = &newpack->entities[oldi]; break; } } if (!to) { //okay, so this is new if (newpack->num_entities==newpack->max_entities) { newpack->max_entities = newpack->num_entities+16; newpack->entities = BZ_Realloc(newpack->entities, sizeof(entity_state_t)*newpack->max_entities); } to = &newpack->entities[newpack->num_entities]; newpack->num_entities++; if (cl_shownet.ival >= 3) Con_Printf("%3i: New %i %x\n", MSG_GetReadCount(), to->number, bits); } else if (cl_shownet.ival >= 3) Con_Printf("%3i: Update %i %x\n", MSG_GetReadCount(), to->number, bits); memcpy(to, from, sizeof(*to)); to->number = read; if (bits & UH2_MODEL) to->modelindex = MSG_ReadShort(); if (bits & UH2_FRAME) to->frame = MSG_ReadByte(); if (bits & UH2_COLORMAP)to->colormap = MSG_ReadByte(); if (bits & UH2_SKIN) to->skinnum = MSG_ReadByte(); if (bits & UH2_SKIN) to->hexen2flags = MSG_ReadByte(); //yes, shared with skin if (bits & UH2_EFFECTS) to->effects = MSG_ReadByte(); if (bits & UH2_ORIGIN1) to->origin[0] = MSG_ReadCoord(); if (bits & UH2_ANGLE1) to->angles[0] = MSG_ReadAngle(); if (bits & UH2_ORIGIN2) to->origin[1] = MSG_ReadCoord(); if (bits & UH2_ANGLE2) to->angles[1] = MSG_ReadAngle(); if (bits & UH2_ORIGIN3) to->origin[2] = MSG_ReadCoord(); if (bits & UH2_ANGLE3) to->angles[2] = MSG_ReadAngle(); if (bits & UH2_SCALE) to->scale = (MSG_ReadByte()/100.0)*16; if (bits & UH2_SCALE) to->abslight = MSG_ReadByte(); to->sequence = cls.netchan.incoming_sequence; to->inactiveflag = 0; } //handle the removes if (bits != 48) Host_EndGame("Corrupt entity message packet\n"); removecount = (qbyte)MSG_ReadByte(); while (removecount --> 0) { read = MSG_ReadUInt16(); for (oldi=0 ; oldinum_entities ; oldi++) { if (read == newpack->entities[oldi].number) { newpack->entities[oldi].inactiveflag = true; break; } } } //sort them, just in case. the removes will bubble to the end. qsort(newpack->entities, newpack->num_entities, sizeof(entity_state_t), CLDP_SortEntities); while (newpack->num_entities) { //pop those removes. if (newpack->entities[newpack->num_entities-1].inactiveflag) newpack->num_entities--; else break; } //make sure any bone states are refreshed for (oldi=0, to = newpack->entities; oldinum_entities ; oldi++, to++) { if (to->bonecount && (to->boneoffset & 0x80000000)) { unsigned int oldoffset = to->boneoffset & 0x7fffffff; void *dest = AllocateBoneSpace(newpack, to->bonecount, &to->boneoffset); void *src = GetBoneSpace(oldpack, oldoffset); memcpy(dest, src, to->bonecount * sizeof(short)*7); } } } #endif void CLNQ_ParseEntity(unsigned int bits) { int i; int num; entity_state_t *state;//, *from; entity_state_t *base; packet_entities_t *pack; qboolean isnehahra = CPNQ_IS_BJP||(cls.protocol_nq == CPNQ_NEHAHRA); qboolean floatcoords; if (cls.signon == 4 - 1) { // first update is the final signon stage cls.signon = 4; CLNQ_SignonReply (); } pack = &cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK].packet_entities; if (bits & NQU_MOREBITS) { i = MSG_ReadByte (); bits |= (i<<8); } if (bits & DPU_EXTEND1) { if (!isnehahra) { i = MSG_ReadByte (); bits |= (i<<16); if (bits & DPU_EXTEND2) { i = MSG_ReadByte (); bits |= (i<<24); } } } if (bits & NQU_LONGENTITY) num = MSGCL_ReadEntity (); else num = MSG_ReadByte (); // state = CL_FindPacketEntity(num); // if (!state) { // if ((int)(lasttime*100) != (int)(realtime*100)) // pack->num_entities=0; // else if (pack->num_entities==pack->max_entities) { pack->max_entities = pack->num_entities+1; pack->entities = BZ_Realloc(pack->entities, sizeof(entity_state_t)*pack->max_entities); memset(pack->entities + pack->num_entities, 0, sizeof(entity_state_t)); } state = &pack->entities[pack->num_entities++]; } // from = CL_FindOldPacketEntity(num); //this could be optimised. if (!CL_CheckBaselines(num)) Host_EndGame("CLNQ_ParseEntity: check baselines failed with size %i", num); base = cl_baselines + num; memcpy(state, base, sizeof(*state)); state->number = num; state->sequence = cls.netchan.incoming_sequence; state->solidsize = ES_SOLID_BSP; state->dpflags = 0; floatcoords = cls.qex && (bits & QE_U_FLOATCOORDS); if (bits & NQU_MODEL) { if (CPNQ_IS_BJP) state->modelindex = MSG_ReadShort (); else state->modelindex = MSG_ReadByte (); } if (bits & NQU_FRAME) state->frame = MSG_ReadByte(); if (bits & NQU_COLORMAP) state->colormap = MSG_ReadByte(); if (bits & NQU_SKIN) state->skinnum = MSG_ReadByte(); if (bits & NQU_EFFECTS) { i = MSG_ReadByte(); if (cls.qex) { unsigned fixed = i & ~(REEF_QUADLIGHT|REEF_PENTLIGHT|REEF_CANDLELIGHT); if (i & REEF_QUADLIGHT) fixed |= EF_BLUE; if (i & REEF_PENTLIGHT) fixed |= EF_RED; if (i & REEF_CANDLELIGHT) fixed |= 0; //tiny light i = fixed; } state->effects = i; } if (bits & NQU_ORIGIN1) state->origin[0] = floatcoords?MSG_ReadFloat():MSG_ReadCoord (); if (bits & NQU_ANGLE1) state->angles[0] = MSG_ReadAngle(); if (bits & NQU_ORIGIN2) state->origin[1] = floatcoords?MSG_ReadFloat():MSG_ReadCoord (); if (bits & NQU_ANGLE2) state->angles[1] = MSG_ReadAngle(); if (bits & NQU_ORIGIN3) state->origin[2] = floatcoords?MSG_ReadFloat():MSG_ReadCoord (); if (bits & NQU_ANGLE3) state->angles[2] = MSG_ReadAngle(); if (bits & NQU_NOLERP) state->dpflags |= RENDER_STEP; if (isnehahra) { if (bits & DPU_EXTEND1) //U_TRANS { float tmp = MSG_ReadFloat(); float alpha = MSG_ReadFloat(); if (tmp == 2) { if (MSG_ReadFloat() > 0.5) state->effects |= EF_FULLBRIGHT; } if (!alpha) alpha = 1; state->trans = bound(0, 255 * alpha, 255); } } else if (cls.protocol_nq == CPNQ_FITZ666) { if (bits & FITZU_ALPHA) state->trans = (MSG_ReadByte()-1)&0xff; if (bits & RMQU_SCALE) state->scale = MSG_ReadByte(); if (bits & FITZU_FRAME2) state->frame = (state->frame & 0xff) | (MSG_ReadByte() << 8); if (bits & FITZU_MODEL2) state->modelindex = (state->modelindex & 0xff) | (MSG_ReadByte() << 8); if (bits & FITZU_LERPFINISH) state->lerpend = cl.gametime + MSG_ReadByte()/255.0f; if (cls.qex) { if (bits & QE_U_SOLIDTYPE) /*state->solidsize =*/ MSG_ReadByte(); //needed for correct prediction if (bits & QE_U_ENTFLAGS) /*state->entflags = */ MSG_ReadULEB128(); //for onground/etc state if (bits & QE_U_HEALTH) /*state->health =*/ MSG_ReadSignedQEX(); //health... not really sure why, I suppose it changes player physics (they should have sent movetype instead though). if (bits & QE_U_UNKNOWN26) /*unknown =*/MSG_ReadByte(); if (bits & QE_U_UNUSED27) Con_Printf(CON_WARNING"QE_U_UNUSED27: %u\n", MSG_ReadByte()); if (bits & QE_U_UNUSED28) Con_Printf(CON_WARNING"QE_U_UNUSED28: %u\n", MSG_ReadByte()); if (bits & QE_U_UNUSED29) Con_Printf(CON_WARNING"QE_U_UNUSED29: %u\n", MSG_ReadByte()); if (bits & QE_U_UNUSED30) Con_Printf(CON_WARNING"QE_U_UNUSED30: %u\n", MSG_ReadByte()); if (bits & QE_U_UNUSED31) Con_Printf(CON_WARNING"QE_U_UNUSED31: %u\n", MSG_ReadByte()); } } else { //dp tends to leak stuff, so parse as quakedp if the normal protocol doesn't define it as something better. // if (bits & DPU_DELTA) //should delta from the previous frame. DP doesn't generate this any more, so whatever. // Host_EndGame("CLNQ_ParseEntity: DPU_DELTA not supported"); if (bits & DPU_ALPHA) state->trans = MSG_ReadByte(); if (bits & DPU_SCALE) state->scale = MSG_ReadByte(); if (bits & DPU_EFFECTS2) state->effects |= MSG_ReadByte() << 8; if (bits & DPU_GLOWSIZE) state->glowsize = MSG_ReadByte(); if (bits & DPU_GLOWCOLOR) state->glowcolour = MSG_ReadByte(); if (bits & DPU_COLORMOD) { i = MSG_ReadByte(); // follows format RRRGGGBB state->colormod[0] = (qbyte)(((i >> 5) & 7) * (32.0f / 7.0f)); state->colormod[1] = (qbyte)(((i >> 2) & 7) * (32.0f / 7.0f)); state->colormod[2] = (qbyte)((i & 3) * (32.0f / 3.0f)); } if (bits & DPU_GLOWTRAIL) state->dpflags |= RENDER_GLOWTRAIL; if (bits & DPU_FRAME2) state->frame |= MSG_ReadByte() << 8; if (bits & DPU_MODEL2) state->modelindex |= MSG_ReadByte() << 8; if (bits & DPU_VIEWMODEL) state->dpflags |= RENDER_VIEWMODEL; if (bits & DPU_EXTERIORMODEL) state->dpflags |= RENDER_EXTERIORMODEL; } } #endif #ifdef PEXT_SETVIEW entity_state_t *CL_FindPacketEntity(int num) { int pnum; entity_state_t *s1; packet_entities_t *pack = cl.currentpackentities; if (pack) for (pnum=0 ; pnumnum_entities ; pnum++) { s1 = &pack->entities[pnum]; if (num == s1->number) return s1; } return NULL; } #endif void CL_RotateAroundTag(entity_t *ent, int entnum, int parenttagent, int parenttagnum) { entity_state_t *ps; float *org=NULL, *ang=NULL; vec3_t axis[3]; float transform[12], parent[12], result[12], old[12], temp[12]; model_t *model; framestate_t fstate; if (parenttagent >= cl.maxlerpents) { Con_Printf("tag entity out of range!\n"); return; } //old is the entity's relative transform (relative to the parent entity's tag) old[0] = ent->axis[0][0]; old[1] = ent->axis[1][0]; old[2] = ent->axis[2][0]; old[3] = ent->origin[0]; old[4] = ent->axis[0][1]; old[5] = ent->axis[1][1]; old[6] = ent->axis[2][1]; old[7] = ent->origin[1]; old[8] = ent->axis[0][2]; old[9] = ent->axis[1][2]; old[10] = ent->axis[2][2]; old[11] = ent->origin[2]; memset(&fstate, 0, sizeof(fstate)); //for visibility checks ent->keynum = parenttagent; ps = CL_FindPacketEntity(parenttagent); if (ps) { if (parenttagent >= cl.maxlerpents) { org = ps->origin; ang = ps->angles; } else { lerpents_t *le = &cl.lerpents[parenttagent]; org = le->origin; ang = le->angles; } if (ps->modelindex <= countof(cl.model_precache) && cl.model_precache[ps->modelindex] && cl.model_precache[ps->modelindex]->loadstate == MLS_LOADED) model = cl.model_precache[ps->modelindex]; else model = NULL; if (model && model->type == mod_alias) AngleVectorsMesh(ang, axis[0], axis[1], axis[2]); else AngleVectors(ang, axis[0], axis[1], axis[2]); VectorInverse(axis[1]); parent[0] = axis[0][0]; parent[1] = axis[1][0]; parent[2] = axis[2][0]; parent[3] = org[0]; parent[4] = axis[0][1]; parent[5] = axis[1][1]; parent[6] = axis[2][1]; parent[7] = org[1]; parent[8] = axis[0][2]; parent[9] = axis[1][2]; parent[10] = axis[2][2]; parent[11] = org[2]; CL_LerpNetFrameState(&fstate, &cl.lerpents[parenttagent]); /*inherit certain properties from the parent entity*/ if (ps->dpflags & RENDER_VIEWMODEL) ent->flags |= RF_WEAPONMODEL|Q2RF_MINLIGHT|RF_DEPTHHACK; if ((ps->dpflags & RENDER_EXTERIORMODEL) || r_refdef.playerview->viewentity == ps->number) ent->flags |= RF_EXTERNALMODEL; //hack for xonotic. if ((ent->flags & RF_WEAPONMODEL) && ent->playerindex == -1 && ps->colormap > 0 && ps->colormap <= cl.allocated_client_slots) { ent->playerindex = ps->colormap-1; ent->topcolour = cl.players[ent->playerindex].dtopcolor; ent->bottomcolour = cl.players[ent->playerindex].dbottomcolor; } } else { extern int parsecountmod; // Con_Printf("tagent %i\n", tagent); if (parenttagent <= cl.allocated_client_slots && parenttagent > 0) { if (parenttagent == cl.playerview[0].playernum+1) { org = cl.playerview[0].simorg; ang = cl.playerview[0].simangles; } else { org = cl.inframes[parsecountmod].playerstate[parenttagent-1].origin; ang = cl.inframes[parsecountmod].playerstate[parenttagent-1].viewangles; } model = cl.model_precache[cl.inframes[parsecountmod].playerstate[parenttagent-1].modelindex]; CL_LerpNetFrameState(&fstate, &cl.lerpplayers[parenttagent-1]); } else { CL_LerpNetFrameState(&fstate, &cl.lerpents[parenttagent]); model = 0; } } { // fstate.g[FS_REG].lerpfrac = CL_EntLerpFactor(tagent); // fstate.g[FS_REG].frametime[0] = cl.time - cl.lerpents[tagent].framechange; // fstate.g[FS_REG].frametime[1] = cl.time - cl.lerpents[tagent].oldframechange; if (Mod_GetTag(model, parenttagnum, &fstate, transform)) { // parent -> transform -> old R_ConcatTransforms((void*)parent, (void*)transform, (void*)temp); R_ConcatTransforms((void*)temp, (void*)old, (void*)result); ent->axis[0][0] = result[0]; ent->axis[1][0] = result[1]; ent->axis[2][0] = result[2]; ent->origin[0] = result[3]; ent->axis[0][1] = result[4]; ent->axis[1][1] = result[5]; ent->axis[2][1] = result[6]; ent->origin[1] = result[7]; ent->axis[0][2] = result[8]; ent->axis[1][2] = result[9]; ent->axis[2][2] = result[10]; ent->origin[2] = result[11]; } else //hrm. { R_ConcatTransforms((void*)parent, (void*)old, (void*)result); ent->axis[0][0] = result[0]; ent->axis[1][0] = result[1]; ent->axis[2][0] = result[2]; ent->origin[0] = result[3]; ent->axis[0][1] = result[4]; ent->axis[1][1] = result[5]; ent->axis[2][1] = result[6]; ent->origin[1] = result[7]; ent->axis[0][2] = result[8]; ent->axis[1][2] = result[9]; ent->axis[2][2] = result[10]; ent->origin[2] = result[11]; } } if (ps && ps->tagentity) CL_RotateAroundTag(ent, entnum, ps->tagentity, ps->tagindex); } void V_AddAxisEntity(entity_t *in) { entity_t *ent; if (cl_numvisedicts == cl_maxvisedicts) { return; // object list is full } ent = &cl_visedicts[cl_numvisedicts]; cl_numvisedicts++; *ent = *in; } void V_ClearEntity(entity_t *e) { memset(e, 0, sizeof(*e)); e->pvscache.num_leafs = -1; e->playerindex = -1; e->topcolour = TOP_DEFAULT; e->bottomcolour = BOTTOM_DEFAULT; } entity_t *V_AddEntity(entity_t *in) { entity_t *ent; if (cl_numvisedicts == cl_maxvisedicts) { return NULL; // object list is full } ent = &cl_visedicts[cl_numvisedicts]; cl_numvisedicts++; *ent = *in; AngleVectorsMesh(ent->angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); return ent; } entity_t *V_AddNewEntity(void) { entity_t *ent; if (cl_numvisedicts == cl_maxvisedicts) { cl_expandvisents = true; return NULL; // object list is full } ent = &cl_visedicts[cl_numvisedicts]; cl_numvisedicts++; return ent; } /* void VQ2_AddLerpEntity(entity_t *in) //a convienience function { entity_t *ent; float fwds, back; int i; if (cl_numvisedicts == MAX_VISEDICTS) return; // object list is full ent = &cl_visedicts[cl_numvisedicts]; cl_numvisedicts++; *ent = *in; fwds = ent->framestate.g[FS_REG].lerpfrac; back = 1 - ent->framestate.g[FS_REG].lerpfrac; for (i = 0; i < 3; i++) { ent->origin[i] = in->origin[i]*fwds + in->oldorigin[i]*back; } ent->framestate.g[FS_REG].lerpfrac = back; AngleVectorsMesh(ent->angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); } */ int V_AddLight (int entsource, vec3_t org, float quant, float r, float g, float b) { return CL_NewDlight (entsource, org, quant, -0.1, r*5, g*5, b*5) - cl_dlights; } void CLQ1_AddOrientedHalfSphere(shader_t *shader, float radius, float gap, float *matrix, float r, float g, float b, float a) { //use simple algo //a series of cylinders that gets progressively narrower const int latsteps = 16; const int lngsteps = 8;//16; float cradius; int v, i, j; scenetris_t *t; vec3_t corner; float x,y; int flags = BEF_NODLIGHT|BEF_NOSHADOWS; if (!r && !g && !b) return; /*reuse the previous trigroup if its the same shader*/ if (cl_numstris && cl_stris[cl_numstris-1].shader == shader && cl_stris[cl_numstris-1].flags == flags && cl_stris[cl_numstris-1].numvert < MAX_INDICIES-(latsteps-1)*(lngsteps-1)) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader; t->numidx = 0; t->numvert = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->flags = flags; } if (cl_numstrisvert + latsteps*lngsteps > cl_maxstrisvert) cl_stris_ExpandVerts(cl_numstrisvert + latsteps*lngsteps); if (cl_maxstrisidx < cl_numstrisidx+latsteps*(lngsteps-1)*6) { cl_maxstrisidx = cl_numstrisidx+latsteps*(lngsteps-1)*6 + 64; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } for (i = 0; i < latsteps; i++) { x = sin(i * 2 * M_PI / latsteps); y = cos(i * 2 * M_PI / latsteps); for (j = 0; j < lngsteps; j++) { v = i*lngsteps + j; cradius = sin(j * 0.5 * M_PI / (lngsteps-1))*radius; corner[0] = x*cradius; corner[1] = y*cradius; corner[2] = (cos(j * 0.5 * M_PI / (lngsteps-1))*-radius) - gap; Matrix3x4_RM_Transform3(matrix, corner, cl_strisvertv[cl_numstrisvert+v]); cl_strisvertt[cl_numstrisvert+v][0] = 0; cl_strisvertt[cl_numstrisvert+v][1] = 0; cl_strisvertc[cl_numstrisvert+v][0] = r; cl_strisvertc[cl_numstrisvert+v][1] = g; cl_strisvertc[cl_numstrisvert+v][2] = b; cl_strisvertc[cl_numstrisvert+v][3] = a; } } if (radius < 0) { for (i = 0; i < lngsteps-1; i++) { v = latsteps-1; for (v = 0; v < latsteps-1; v++) { cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+lngsteps + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+lngsteps+1 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+lngsteps + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; } cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; } } else { for (i = 0; i < lngsteps-1; i++) { v = latsteps-1; for (v = 0; v < latsteps-1; v++) { cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+lngsteps + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+lngsteps + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+lngsteps+1 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; } cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + i; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*lngsteps + i; } } t->numvert += lngsteps*latsteps; t->numidx = cl_numstrisidx - t->firstidx; cl_numstrisvert += lngsteps*latsteps; } void CLQ1_AddOrientedSphere(shader_t *shader, float radius, float *matrix, float r, float g, float b, float a) { CLQ1_AddOrientedHalfSphere(shader, radius, 0, matrix, r, g, b, a); CLQ1_AddOrientedHalfSphere(shader, -radius, 0, matrix, r, g, b, a); } void CLQ1_AddOrientedCylinder(shader_t *shader, float radius, float height, qboolean capsule, float *matrix, float r, float g, float b, float a) { int sides = 16; int v; scenetris_t *t; vec3_t corner; int flags = BEF_NODLIGHT|BEF_NOSHADOWS; if (!r && !g && !b) return; radius *= 0.5; height *= 0.5; if (capsule) height -= radius; if (height > 0) { /*reuse the previous trigroup if its the same shader*/ if (cl_numstris && cl_stris[cl_numstris-1].shader == shader && cl_stris[cl_numstris-1].flags == flags) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader; t->numidx = 0; t->numvert = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->flags = flags; } if (cl_numstrisvert + sides*2 > cl_maxstrisvert) cl_stris_ExpandVerts(cl_numstrisvert + sides*2); if (cl_maxstrisidx < cl_numstrisidx+sides*6) { cl_maxstrisidx = cl_numstrisidx+sides*6 + 64; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } for (v = 0; v < sides*2; v++) { corner[0] = sin((v>>1) * 2 * M_PI / sides)*radius; corner[1] = cos((v>>1) * 2 * M_PI / sides)*radius; corner[2] = (v & 1)?height:-height; Matrix3x4_RM_Transform3(matrix, corner, cl_strisvertv[cl_numstrisvert+v]); cl_strisvertt[cl_numstrisvert+v][0] = 0; cl_strisvertt[cl_numstrisvert+v][1] = 0; cl_strisvertc[cl_numstrisvert+v][0] = r; cl_strisvertc[cl_numstrisvert+v][1] = g; cl_strisvertc[cl_numstrisvert+v][2] = b; cl_strisvertc[cl_numstrisvert+v][3] = a; } for (v = 0; v < sides-1; v++) { cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+2 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+3 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+2 + v*2; } cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1 + v*2; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0; if (!capsule) { for (v = 4; v < sides*2; v+=2) { cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+v; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+(v-2); cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+(v-2)+1; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+v+1; } } t->numvert += sides*2; t->numidx = cl_numstrisidx - t->firstidx; cl_numstrisvert += sides*2; } if (capsule) { CLQ1_AddOrientedHalfSphere(shader, radius, height, matrix, r, g, b, a); CLQ1_AddOrientedHalfSphere(shader, -radius, -height, matrix, r, g, b, a); } } void CLQ1_DrawLine(shader_t *shader, vec3_t v1, vec3_t v2, float r, float g, float b, float a) { scenetris_t *t; int flags = BEF_NODLIGHT|BEF_NOSHADOWS|BEF_LINES; if (cl_numstris && cl_stris[cl_numstris-1].shader == shader && cl_stris[cl_numstris-1].flags == flags) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader; t->numidx = 0; t->numvert = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->flags = flags; } if (cl_numstrisvert + 2 > cl_maxstrisvert) cl_stris_ExpandVerts(cl_numstrisvert + 2); if (cl_maxstrisidx < cl_numstrisidx+2) { cl_maxstrisidx = cl_numstrisidx+2; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } VectorCopy(v1, cl_strisvertv[cl_numstrisvert+0]); cl_strisvertt[cl_numstrisvert+0][0] = 0; cl_strisvertt[cl_numstrisvert+0][1] = 0; cl_strisvertc[cl_numstrisvert+0][0] = r; cl_strisvertc[cl_numstrisvert+0][1] = g; cl_strisvertc[cl_numstrisvert+0][2] = b; cl_strisvertc[cl_numstrisvert+0][3] = a; VectorCopy(v2, cl_strisvertv[cl_numstrisvert+1]); cl_strisvertt[cl_numstrisvert+1][0] = 0; cl_strisvertt[cl_numstrisvert+1][1] = 0; cl_strisvertc[cl_numstrisvert+1][0] = r; cl_strisvertc[cl_numstrisvert+1][1] = g; cl_strisvertc[cl_numstrisvert+1][2] = b; cl_strisvertc[cl_numstrisvert+1][3] = a; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+0; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert-t->firstvert+1; t->numvert += 2; t->numidx = cl_numstrisidx - t->firstidx; cl_numstrisvert += 2; } void CLQ1_AddSpriteQuad(shader_t *shader, vec3_t mid, float radius) { float r=1, g=1, b=1; scenetris_t *t; int flags = BEF_NODLIGHT|BEF_NOSHADOWS; if (cl_numstris && cl_stris[cl_numstris-1].shader == shader && cl_stris[cl_numstris-1].flags == flags && cl_stris[cl_numstris-1].numvert + 4 <= MAX_INDICIES) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris+=8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->numvert = 0; t->numidx = 0; t->flags = flags; } if (cl_numstrisidx+6 > cl_maxstrisidx) { cl_maxstrisidx=cl_numstrisidx+6 + 64; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } if (cl_numstrisvert+4 > cl_maxstrisvert) cl_stris_ExpandVerts(cl_maxstrisvert+64); { VectorMA(mid, radius, vright, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], radius, vup, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); Vector2Set(cl_strisvertt[cl_numstrisvert], 1, 1); cl_numstrisvert++; VectorMA(mid, radius, vright, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], -radius, vup, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); Vector2Set(cl_strisvertt[cl_numstrisvert], 1, 0); cl_numstrisvert++; VectorMA(mid, -radius, vright, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], -radius, vup, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); Vector2Set(cl_strisvertt[cl_numstrisvert], 0, 0); cl_numstrisvert++; VectorMA(mid, -radius, vright, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], radius, vup, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); Vector2Set(cl_strisvertt[cl_numstrisvert], 0, 1); cl_numstrisvert++; } /*build the triangles*/ cl_strisidx[cl_numstrisidx++] = t->numvert + 0; cl_strisidx[cl_numstrisidx++] = t->numvert + 1; cl_strisidx[cl_numstrisidx++] = t->numvert + 2; cl_strisidx[cl_numstrisidx++] = t->numvert + 0; cl_strisidx[cl_numstrisidx++] = t->numvert + 2; cl_strisidx[cl_numstrisidx++] = t->numvert + 3; t->numidx = cl_numstrisidx - t->firstidx; t->numvert += 4; } #include "shader.h" void CL_DrawDebugPlane(float *normal, float dist, float r, float g, float b, qboolean enqueue) { const float radius = 8192; //infinite is quite small nowadays. scenetris_t *t; if (!enqueue) cl_numstris = 0; if (cl_numstris == cl_maxstris) { cl_maxstris+=8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = R_RegisterShader("testplane", SUF_NONE, "{\n{\nmap $whiteimage\nrgbgen vertex\nalphagen vertex\nblendfunc add\nnodepth\n}\n}\n"); t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->numvert = 0; t->numidx = 0; if (cl_numstrisidx+6 > cl_maxstrisidx) { cl_maxstrisidx=cl_numstrisidx+6 + 64; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } if (cl_numstrisvert+4 > cl_maxstrisvert) cl_stris_ExpandVerts(cl_maxstrisvert+64); { vec3_t tmp = {0,0.04,0.96}; vec3_t right, forward; CrossProduct(normal, tmp, right); VectorNormalize(right); CrossProduct(normal, right, forward); VectorNormalize(forward); VectorScale( normal, dist, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], radius, right, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], radius, forward, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); cl_numstrisvert++; VectorScale( normal, dist, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], radius, right, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], -radius, forward, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); cl_numstrisvert++; VectorScale( normal, dist, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], -radius, right, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], -radius, forward, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); cl_numstrisvert++; VectorScale( normal, dist, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], -radius, right, cl_strisvertv[cl_numstrisvert]); VectorMA(cl_strisvertv[cl_numstrisvert], radius, forward, cl_strisvertv[cl_numstrisvert]); Vector4Set(cl_strisvertc[cl_numstrisvert], r, g, b, 0.2); cl_numstrisvert++; } /*build the triangles*/ cl_strisidx[cl_numstrisidx++] = t->numvert + 0; cl_strisidx[cl_numstrisidx++] = t->numvert + 1; cl_strisidx[cl_numstrisidx++] = t->numvert + 2; cl_strisidx[cl_numstrisidx++] = t->numvert + 0; cl_strisidx[cl_numstrisidx++] = t->numvert + 2; cl_strisidx[cl_numstrisidx++] = t->numvert + 3; t->numidx = cl_numstrisidx - t->firstidx; t->numvert += 4; if (!enqueue) { // int oldents = cl_numvisedicts; // cl_numvisedicts = 0; r_refdef.scenevis = NULL; BE_DrawWorld(NULL); cl_numstris = 0; // cl_numvisedicts = oldents; } } void CLQ1_AddOrientedCube(shader_t *shader, vec3_t mins, vec3_t maxs, float *matrix, float r, float g, float b, float a) { int v; scenetris_t *t; vec3_t corner; int flags = BEF_NODLIGHT|BEF_NOSHADOWS; if (!r && !g && !b) return; /*reuse the previous trigroup if its the same shader*/ if (cl_numstris && cl_stris[cl_numstris-1].shader == shader && cl_stris[cl_numstris-1].flags == flags && cl_stris[cl_numstris-1].numvert + 8 <= MAX_INDICIES) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader; t->numidx = 0; t->numvert = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->flags = flags; } if (cl_numstrisvert + 8 > cl_maxstrisvert) cl_stris_ExpandVerts(cl_numstrisvert + 8 + 1024); if (cl_maxstrisidx < cl_numstrisidx+6*6) { cl_maxstrisidx = cl_numstrisidx + 6*6 + 1024; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } for (v = 0; v < 8; v++) { corner[0] = (v & 1)?mins[0]:maxs[0]; corner[1] = (v & 2)?mins[1]:maxs[1]; corner[2] = (v & 4)?mins[2]:maxs[2]; if (matrix) Matrix3x4_RM_Transform3(matrix, corner, cl_strisvertv[cl_numstrisvert+v]); else VectorCopy(corner, cl_strisvertv[cl_numstrisvert+v]); cl_strisvertt[cl_numstrisvert+v][0] = 0; cl_strisvertt[cl_numstrisvert+v][1] = 0; cl_strisvertc[cl_numstrisvert+v][0] = r; cl_strisvertc[cl_numstrisvert+v][1] = g; cl_strisvertc[cl_numstrisvert+v][2] = b; cl_strisvertc[cl_numstrisvert+v][3] = a; } /*top*/ cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+2 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+1 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+0 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+3 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+1 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+2 - t->firstvert; /*bottom*/ cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+4 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+5 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+6 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+6 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+5 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+7 - t->firstvert; /*'left'*/ cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+5 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+4 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+0 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+1 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+5 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+0 - t->firstvert; /*right*/ cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+2 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+6 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+7 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+2 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+7 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+3 - t->firstvert; /*urm, the other way*/ cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+2 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+4 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+6 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+4 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+2 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+0 - t->firstvert; /*and its oposite*/ cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+7 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+5 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+3 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+1 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+3 - t->firstvert; cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+5 - t->firstvert; t->numvert += 8; t->numidx = cl_numstrisidx - t->firstidx; cl_numstrisvert += 8; } #include "pr_common.h" void CLQ1_AddVisibleBBoxes(void) { world_t *w; wedict_t *e; int i; shader_t *s; vec3_t min, max, size; #pragma message("Temporary Code: BBoxes calling R2D_Flush") /* * HACK(fhomolka): For some reason, bboxes like to mess with progs-drawn Polygons. * The clean way would be to understand WHY they mess with eachother, for now this must do. * TODO(fhomolka) * Comment by Spike: "qc's polys should have been flushed inside renderscene" */ if(R2D_Flush) R2D_Flush(); switch(r_showbboxes.ival & 3) { default: return; #ifndef CLIENTONLY case 1: w = &sv.world; break; #endif #ifdef CSQC_DAT case 2: { extern world_t csqc_world; w = &csqc_world; } break; #endif case 3: { inframe_t *frame; packet_entities_t *pak; entity_state_t *state; model_t *mod; s = R_RegisterShader("bboxshader", SUF_NONE, "{\n" "polygonoffset\n" "sort additive\n" "{\n" "map $whiteimage\n" "blendfunc add\n" "rgbgen vertex\n" "alphagen vertex\n" "}\n" "}\n"); frame = &cl.inframes[cl.parsecount & UPDATE_MASK]; pak = &frame->packet_entities; for (i=0 ; inum_entities ; i++) { state = &pak->entities[i]; if (state->solidsize == ES_SOLID_NOT && !state->skinnum) continue; if (state->solidsize == ES_SOLID_BSP) { /*bsp model size*/ if (state->modelindex <= 0) continue; if (!cl.model_precache[state->modelindex]) continue; /*this makes non-inline bsp objects non-solid for prediction*/ if ((cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) || ((*cl.model_precache[state->modelindex]->name == '*' || cl.model_precache[state->modelindex]->numsubmodels) && cl.model_precache[state->modelindex]->hulls[1].firstclipnode)) { mod = cl.model_precache[state->modelindex]; VectorAdd(state->origin, mod->mins, min); VectorAdd(state->origin, mod->maxs, max); CLQ1_AddOrientedCube(s, min, max, NULL, 0.1, 0, 0, 1); } } else { /*don't bother with angles*/ COM_DecodeSize(state->solidsize, min, max); VectorAdd(state->origin, min, min); VectorAdd(state->origin, max, max); CLQ1_AddOrientedCube(s, min, max, NULL, 0.1, 0, 0, 1); COM_DecodeSize(state->solidsize, min, max); VectorAdd(state->u.q1.predorg, min, min); VectorAdd(state->u.q1.predorg, max, max); CLQ1_AddOrientedCube(s, min, max, NULL, 0, 0, 0.1, 1); } } } return; } if (!w->progs) return; s = R_RegisterShader("bboxshader", SUF_NONE, "{\n" "polygonoffset\n" "sort additive\n" "{\n" "map $whiteimage\n" "blendfunc add\n" "rgbgen vertex\n" "alphagen vertex\n" "}\n" "}\n"); for (i = 1; i < w->num_edicts; i++) { e = WEDICT_NUM_PB(w->progs, i); if (ED_ISFREE(e)) continue; if (r_showbboxes.ival & 4) { //shows the hulls instead /*mins is easy*/ VectorAdd(e->v->origin, e->v->mins, min); /*maxs is weeeeird*/ VectorSubtract (e->v->maxs, e->v->mins, size); if (size[0] < 3) VectorCopy(min, max); else if (size[0] <= 32) { max[0] = min[0] + 32; max[1] = min[1] + 32; max[2] = min[2] + 56; } else { max[0] = min[0] + 64; max[1] = min[1] + 64; max[2] = min[2] + 88; } } else { if (e->v->solid == SOLID_BSP) { VectorCopy(e->v->absmin, min); VectorCopy(e->v->absmax, max); } else { VectorAdd(e->v->origin, e->v->mins, min); VectorAdd(e->v->origin, e->v->maxs, max); } } if (e->xv->geomtype == GEOMTYPE_CAPSULE) { float rad = ((e->v->maxs[0]-e->v->mins[0]) + (e->v->maxs[1]-e->v->mins[1]))/4.0; float height = (e->v->maxs[2]-e->v->mins[2])/2; float matrix[12] = {1,0,0,0,0,1,0,0,0,0,1,0}; matrix[3] = e->v->origin[0]; matrix[7] = e->v->origin[1]; matrix[11] = e->v->origin[2] + (e->v->maxs[2]-height); CLQ1_AddOrientedCylinder(s, rad*2, height*2, true, matrix, (e->v->solid || e->v->movetype)?0.1:0, (e->v->movetype == MOVETYPE_STEP || e->v->movetype == MOVETYPE_TOSS || e->v->movetype == MOVETYPE_BOUNCE)?0.1:0, ((int)e->v->flags & (FL_ONGROUND | ((e->v->movetype == MOVETYPE_STEP)?FL_FLY:0)))?0.1:0, 1); } else { if (!e->v->solid && !e->v->movetype) { vec3_t ep = {1,1,1}; VectorAdd(max, ep, max); VectorSubtract(min, ep, min); CLQ1_AddOrientedCube(s, min, max, NULL, 0, 0.1, 0, 1); } else CLQ1_AddOrientedCube(s, min, max, NULL, (e->v->solid || e->v->movetype)?0.1:0, (e->v->movetype == MOVETYPE_STEP || e->v->movetype == MOVETYPE_TOSS || e->v->movetype == MOVETYPE_BOUNCE)?0.1:0, ((int)e->v->flags & (FL_ONGROUND | ((e->v->movetype == MOVETYPE_STEP)?FL_FLY:0)))?0.1:0, 1); } } } typedef struct { scenetris_t *t; vec4_t rgbavalue; vec3_t axis[3]; float offset[3]; float scale[3]; } cl_adddecal_ctx_t; static void CL_AddDecal_Callback(void *vctx, vec3_t *fte_restrict points, size_t numtris, shader_t *shader) { cl_adddecal_ctx_t *ctx = vctx; scenetris_t *t = ctx->t; size_t numpoints = numtris*3; size_t v; if (cl_numstrisvert + numpoints > cl_maxstrisvert) cl_stris_ExpandVerts(cl_numstrisvert + numpoints); if (cl_maxstrisidx < cl_numstrisidx+numpoints) { cl_maxstrisidx = cl_numstrisidx+numpoints + 64; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } for (v = 0; v < numpoints; v++) { VectorCopy(points[v], cl_strisvertv[cl_numstrisvert+v]); cl_strisvertt[cl_numstrisvert+v][0] = 1+(DotProduct(points[v], ctx->axis[1]) - ctx->offset[1]) * ctx->scale[1]; cl_strisvertt[cl_numstrisvert+v][1] = -(DotProduct(points[v], ctx->axis[2]) - ctx->offset[2]) * ctx->scale[2]; cl_strisvertc[cl_numstrisvert+v][0] = ctx->rgbavalue[0]; cl_strisvertc[cl_numstrisvert+v][1] = ctx->rgbavalue[1]; cl_strisvertc[cl_numstrisvert+v][2] = ctx->rgbavalue[2]; cl_strisvertc[cl_numstrisvert+v][3] = ctx->rgbavalue[3] * (1-fabs(DotProduct(points[v], ctx->axis[0]) - ctx->offset[0]) * ctx->scale[0]); } for (v = 0; v < numpoints; v++) { cl_strisidx[cl_numstrisidx++] = cl_numstrisvert+v - t->firstvert; } t->numvert += numpoints; t->numidx += numpoints; cl_numstrisvert += numpoints; } void CL_AddDecal(shader_t *shader, vec3_t origin, vec3_t up, vec3_t side, vec3_t rgbvalue, float alphavalue) { scenetris_t *t; float l, s, radius, vradius; cl_adddecal_ctx_t ctx; VectorNegate(up, ctx.axis[0]); VectorCopy(side, ctx.axis[2]); s = DotProduct(ctx.axis[2], ctx.axis[2]); l = DotProduct(ctx.axis[0], ctx.axis[0]); vradius = 1/sqrt(l); radius = 1/sqrt(s); VectorScale(ctx.axis[0], vradius, ctx.axis[0]); VectorScale(ctx.axis[2], radius, ctx.axis[2]); CrossProduct(ctx.axis[0], ctx.axis[2], ctx.axis[1]); ctx.offset[2] = DotProduct(origin, ctx.axis[2]) + 0.5*radius; ctx.offset[1] = DotProduct(origin, ctx.axis[1]) + 0.5*radius; ctx.offset[0] = DotProduct(origin, ctx.axis[0]); ctx.scale[2] = 1/radius; ctx.scale[1] = 1/radius; ctx.scale[0] = 2/vradius; if (R2D_Flush) R2D_Flush(); /*reuse the previous trigroup if its the same shader*/ if (cl_numstris && cl_stris[cl_numstris-1].shader == shader && cl_stris[cl_numstris-1].flags == (BEF_NODLIGHT|BEF_NOSHADOWS)) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader; t->numidx = 0; t->numvert = 0; t->flags = BEF_NODLIGHT|BEF_NOSHADOWS; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; } ctx.t = t; VectorCopy(rgbvalue, ctx.rgbavalue); ctx.rgbavalue[3] = alphavalue; Mod_ClipDecal(cl.worldmodel, origin, ctx.axis[0], ctx.axis[1], ctx.axis[2], max(radius, vradius), 0,0, CL_AddDecal_Callback, &ctx); if (!t->numidx) cl_numstris--; } void R_AddItemTimer(vec3_t shadoworg, float yaw, float radius, float percent, vec3_t rgb) { vec3_t eang; shader_t *s; scenetris_t *t; cl_adddecal_ctx_t ctx; // if (!r_shadows.value) // return; s = R_RegisterShader("timershader", SUF_NONE, "{\n" "polygonoffset\n" "fte_program itemtimer\n" "{\n" "map $diffuse\n" "blendfunc src_alpha one\n" "rgbgen vertex\n" "alphagen vertex\n" "}\n" "}\n"); if (!s->prog) return; TEXASSIGN(s->defaulttextures->base, balltexture); eang[0] = 0; eang[1] = yaw; eang[2] = 0; AngleVectors(eang, ctx.axis[1], ctx.axis[2], ctx.axis[0]); VectorNegate(ctx.axis[0], ctx.axis[0]); ctx.offset[2] = DotProduct(shadoworg, ctx.axis[2]) + 0.5*radius; ctx.offset[1] = DotProduct(shadoworg, ctx.axis[1]) + 0.5*radius; ctx.offset[0] = DotProduct(shadoworg, ctx.axis[0]); ctx.scale[1] = 1/radius; ctx.scale[2] = 1/radius; ctx.scale[0] = 0;//.5/radius; if (R2D_Flush) R2D_Flush(); /*reuse the previous trigroup if its the same shader*/ if (cl_numstris && cl_stris[cl_numstris-1].shader == s && cl_stris[cl_numstris-1].flags == (BEF_NODLIGHT|BEF_NOSHADOWS)) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = s; t->flags = BEF_NODLIGHT|BEF_NOSHADOWS; t->numidx = 0; t->numvert = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; } ctx.t = t; Vector4Set(ctx.rgbavalue, rgb[0], rgb[1], rgb[2], percent); Mod_ClipDecal(cl.worldmodel, shadoworg, ctx.axis[0], ctx.axis[1], ctx.axis[2], radius, 0,0, CL_AddDecal_Callback, &ctx); if (!t->numidx) cl_numstris--; } void CLQ1_AddShadow(entity_t *ent) { float radius; vec3_t shadoworg; vec3_t eang; float tx, ty; shader_t *s; scenetris_t *t; cl_adddecal_ctx_t ctx; if (!r_blobshadows || !ent->model || (ent->model->type != mod_alias && ent->model->type != mod_halflife) || (ent->flags & RF_NOSHADOW)) return; s = R_RegisterShader("shadowshader", SUF_NONE, "{\n" "polygonoffset\n" "{\n" "map $diffuse\n" "blendfunc blend\n" "rgbgen vertex\n" "alphagen vertex\n" "}\n" "}\n"); TEXASSIGN(s->defaulttextures->base, balltexture); tx = ent->model->maxs[0] - ent->model->mins[0]; ty = ent->model->maxs[1] - ent->model->mins[1]; if (tx > ty) radius = tx; else radius = ty; radius/=2; shadoworg[0] = ent->origin[0]; shadoworg[1] = ent->origin[1]; shadoworg[2] = ent->origin[2] + ent->model->mins[2]; eang[0] = 0; eang[1] = ent->angles[1]; eang[2] = 0; AngleVectors(eang, ctx.axis[1], ctx.axis[2], ctx.axis[0]); VectorNegate(ctx.axis[0], ctx.axis[0]); ctx.offset[2] = DotProduct(shadoworg, ctx.axis[2]) + 0.5*radius; ctx.offset[1] = DotProduct(shadoworg, ctx.axis[1]) + 0.5*radius; ctx.offset[0] = DotProduct(shadoworg, ctx.axis[0]); ctx.scale[1] = 1/radius; ctx.scale[2] = 1/radius; ctx.scale[0] = 0.5/radius; if (R2D_Flush) R2D_Flush(); /*reuse the previous trigroup if its the same shader*/ if (cl_numstris && cl_stris[cl_numstris-1].shader == s && cl_stris[cl_numstris-1].flags == (BEF_NODLIGHT|BEF_NOSHADOWS)) t = &cl_stris[cl_numstris-1]; else { if (cl_numstris == cl_maxstris) { cl_maxstris += 8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = s; t->flags = BEF_NODLIGHT|BEF_NOSHADOWS; t->numidx = 0; t->numvert = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; } ctx.t = t; Vector4Set(ctx.rgbavalue, 0, 0, 0, r_blobshadows*((ent->flags & RF_TRANSLUCENT)?ent->shaderRGBAf[3]:1)); Mod_ClipDecal(cl.worldmodel, shadoworg, ctx.axis[0], ctx.axis[1], ctx.axis[2], radius, 0,0, CL_AddDecal_Callback, &ctx); if (!t->numidx) cl_numstris--; } void CLQ1_AddPowerupShell(entity_t *ent, qboolean viewweap, unsigned int effects) { entity_t *shell; if (!(effects & (EF_BLUE | EF_RED | EF_GREEN)) || !v_powerupshell.value || !ent) return; if (cl_numvisedicts == cl_maxvisedicts) return; // object list is full shell = &cl_visedicts[cl_numvisedicts++]; *shell = *ent; /*view weapons are much closer to the screen, the scales don't work too well, so use a different shader with a smaller expansion*/ if (viewweap) { shell->forcedshader = R_RegisterShader("powerups/shellweapon", SUF_NONE, "{\n" "program defaultpowerupshell\n" "sort additive\n" "deformVertexes wave 100 sin 0.5 0 0 0\n" "noshadows\n" "surfaceparm nodlight\n" "{\n" "map $whiteimage\n" "rgbgen entity\n" "alphagen entity\n" "blendfunc src_alpha one\n" "}\n" "}\n" ); } else { shell->forcedshader = R_RegisterShader("powerups/shell", SUF_NONE, "{\n" "program defaultpowerupshell\n" "sort additive\n" "deformVertexes wave 100 sin 3 0 0 0\n" "noshadows\n" "surfaceparm nodlight\n" "{\n" "map $whiteimage\n" "rgbgen entity\n" "alphagen entity\n" "blendfunc src_alpha one\n" "}\n" "}\n" ); } shell->shaderRGBAf[0] *= (effects & EF_RED)?1:0; shell->shaderRGBAf[1] *= (effects & EF_GREEN)?1:0; shell->shaderRGBAf[2] *= (effects & EF_BLUE)?1:0; shell->shaderRGBAf[3] *= v_powerupshell.value; /*let the shader do all the work*/ shell->flags &= ~RF_TRANSLUCENT|RF_ADDITIVE; } static void CL_LerpNetFrameState(framestate_t *fs, lerpents_t *le) { int fsanim; for (fsanim = 0; fsanim < FS_COUNT; fsanim++) { fs->g[fsanim].frame[0] = le->newframe[fsanim]; fs->g[fsanim].frame[1] = le->oldframe[fsanim]; fs->g[fsanim].frametime[0] = cl.servertime - le->newframestarttime[fsanim]; fs->g[fsanim].frametime[1] = cl.servertime - le->oldframestarttime[fsanim]; fs->g[fsanim].lerpweight[0] = (fs->g[fsanim].frametime[0]) / le->framelerpdeltatime[fsanim]; fs->g[fsanim].lerpweight[0] = bound(0, fs->g[FS_REG].lerpweight[0], 1); fs->g[fsanim].lerpweight[1] = 1 - fs->g[fsanim].lerpweight[0]; } fs->g[0].endbone = le->basebone; } static void CL_UpdateNetFrameLerpState(qboolean force, int curframe, int curbaseframe, int curbasebone, lerpents_t *le, float lerpend) { int fst, frame; if (curbasebone != le->basebone) { //FIXME: we should be able to treat 0 and 255 specially by ignoring the change and locking the respective value to the other's value. if (!curbasebone) curbaseframe = curframe; else if (curbasebone == 255) curframe = curbaseframe; le->basebone = curbasebone; } for (fst = 0; fst < FS_COUNT; fst++) { frame = (fst==FST_BASE)?curbaseframe:curframe; if (force || frame != le->newframe[fst]) { if (lerpend) le->framelerpdeltatime[fst] = bound(0, lerpend - cl.servertime, cl_lerp_maxinterval.value); //clamp to 10 tics per second else le->framelerpdeltatime[fst] = bound(0, cl.servertime - le->newframestarttime[fst], cl_lerp_maxinterval.value); //clamp to 10 tics per second if (!force) { le->oldframe[fst] = le->newframe[fst]; le->oldframestarttime[fst] = le->newframestarttime[fst]; } else { le->oldframe[fst] = frame; le->oldframestarttime[fst] = cl.servertime; } le->newframe[fst] = frame; le->newframestarttime[fst] = cl.servertime; // if (force) // { // //if its new, we need to tweak the age of the animation. looping anims won't appear any different, while non-looping ones will clamp to the last pose of the animation when its new. // le->oldframestarttime[fst] -= Mod_GetFrameDuration(le->model, 0, le->oldframe[fst]); // le->newframestarttime[fst] -= Mod_GetFrameDuration(le->model, 0, le->newframe[fst]); // } } } } void CL_ClearLerpEntsParticleState(void) { int i; for (i = 0; i < cl.maxlerpents; i++) { pe->DelinkTrailstate(&(cl.lerpents[i].trailstate)); pe->DelinkTrailstate(&(cl.lerpents[i].emitstate)); } } void CL_LinkStaticEntities(void *pvs, int *areas) { int i; entity_t *ent; model_t *clmodel; static_entity_t *stat; extern cvar_t r_drawflame, gl_part_flame; vec3_t mins, maxs; if (r_drawflame.ival < 0 || r_drawentities.ival == 0) return; if (!cl.worldmodel) return; for (i = 0; i < cl.num_statics; i++) { if (cl_numvisedicts == cl_maxvisedicts) { cl_expandvisents=true; break; } stat = &cl_static_entities[i]; clmodel = stat->ent.model; if (!clmodel) { if (stat->mdlidx < 0) { if (stat->mdlidx > -MAX_CSMODELS) clmodel = cl.model_csqcprecache[-stat->mdlidx]; } else { if (stat->mdlidx < MAX_PRECACHE_MODELS) clmodel = cl.model_precache[stat->mdlidx]; } if (!clmodel || clmodel->loadstate == MLS_LOADING) continue; if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED) continue; stat->ent.model = clmodel; //figure out the correct axis for the model if (clmodel && clmodel->type == mod_alias && (cls.protocol == CP_QUAKEWORLD || cls.protocol == CP_NETQUAKE)) { //q2 is fixed, but q1 pitches the wrong way, and hexen2 rolls the wrong way too. AngleVectorsMesh(stat->state.angles, stat->ent.axis[0], stat->ent.axis[1], stat->ent.axis[2]); } else AngleVectors(stat->state.angles, stat->ent.axis[0], stat->ent.axis[1], stat->ent.axis[2]); VectorInverse(stat->ent.axis[1]); if (clmodel) { //FIXME: wait for model to load so we know the correct size? /*FIXME: compensate for angle*/ VectorAdd(stat->state.origin, clmodel->mins, mins); VectorAdd(stat->state.origin, clmodel->maxs, maxs); } else { VectorCopy(stat->state.origin, mins); VectorCopy(stat->state.origin, maxs); } cl.worldmodel->funcs.FindTouchedLeafs(cl.worldmodel, &stat->ent.pvscache, mins, maxs); } else if (clmodel->loadstate != MLS_LOADED) { if (clmodel->loadstate == MLS_NOTLOADED) //flushed? Mod_LoadModel(clmodel, MLV_WARN); //load it, but don't otherwise care for now. continue; } /*pvs test*/ if (pvs && !cl.worldmodel->funcs.EdictInFatPVS(cl.worldmodel, &stat->ent.pvscache, pvs, areas)) continue; // emit particles for statics (we don't need to cheat check statics) if (stat->state.u.q1.emiteffectnum) P_EmitEffect (stat->ent.origin, stat->ent.axis, MDLF_EMITFORWARDS, CL_TranslateParticleFromServer(stat->state.u.q1.emiteffectnum), &(stat->emit)); else if (clmodel) { if (clmodel->particleeffect >= 0 && gl_part_flame.ival) P_EmitEffect(stat->ent.origin, stat->ent.axis, clmodel->engineflags, clmodel->particleeffect, &stat->emit); if ((!r_drawflame.ival) && (clmodel->engineflags & MDLF_FLAME)) continue; } //prepare to draw it if (!clmodel || clmodel->loadstate != MLS_LOADED) continue; ent = &cl_visedicts[cl_numvisedicts++]; *ent = stat->ent; ent->framestate.g[FS_REG].frametime[0] = cl.time; ent->framestate.g[FS_REG].frametime[1] = cl.time; // FIXME: no effects on static ents // CLQ1_AddPowerupShell(ent, false, stat->effects); } } //returns cos(angle) static float CompareAngles (const vec3_t angles1, const vec3_t angles2) { float angle; vec3_t dir1, dir2; angle = angles1[YAW] * (M_PI*2 / 360); dir1[1] = sin(angle); dir1[0] = cos(angle); if (angles1[PITCH]) { angle = angles1[PITCH] * (M_PI*2 / 360); dir1[2] = -sin(angle); angle = cos(angle); dir1[0] *= angle; dir1[1] *= angle; } else dir1[2] = 0; angle = angles2[YAW] * (M_PI*2 / 360); dir2[1] = sin(angle); dir2[0] = cos(angle); if (angles2[PITCH]) { angle = angles2[PITCH] * (M_PI*2 / 360); dir2[2] = -sin(angle); angle = cos(angle); dir2[0] *= angle; dir2[1] *= angle; } else dir2[2] = 0; return DotProduct(dir1,dir2); } /* =============== CL_LinkPacketEntities =============== */ void R_FlameTrail(vec3_t start, vec3_t end, float seperation); /* Interpolates the two packets by the given time, writes its results into the lerpentities array. */ static void CL_TransitionPacketEntities(int newsequence, packet_entities_t *newpack, packet_entities_t *oldpack, float frac, float servertime) { lerpents_t *le; entity_state_t *snew, *sold; int i; int oldpnum, newpnum; float *snew__origin; float *sold__origin; float cos_theta; int oldsequence; extern cvar_t r_nolerp; qboolean isnew; vec3_t move; float a1, a2; float maxdist = cl_lerp_maxdistance.value*cl_lerp_maxdistance.value; /* seeing as how dropped packets cannot be filled in due to the reliable networking stuff, We can simply detect changes and lerp towards them */ //we have two index-sorted lists of entities //we figure out which ones are new, //we don't care about old, as our caller will use the lerpents array we fill, and the entity numbers from the 'new' packet. oldsequence = cl.lerpentssequence; if (!oldsequence) oldsequence = -1; //something invalid, so everything is new cl.lerpentssequence = newsequence; cl.packfrac = frac; cl.currentpacktime = servertime; cl.currentpackentities = newpack; cl.previouspackentities = oldpack; oldpnum=0; for (newpnum=0 ; newpnumnum_entities ; newpnum++) { snew = &newpack->entities[newpnum]; sold = NULL; for ( ; oldpnumnum_entities ; ) { sold = &oldpack->entities[oldpnum]; if (sold->number >= snew->number) { if (sold->number > snew->number) sold = NULL; //woo, it's a new entity. else oldpnum++; break; } oldpnum++; #ifdef RAGDOLL //note: not entirely reliable le = &cl.lerpents[sold->number]; if (sold->number < cl.maxlerpents && le->skeletalobject) rag_removedeltaent(le); #endif } if (snew->number >= cl.maxlerpents) { int newmaxle = snew->number+16; cl.lerpents = BZ_Realloc(cl.lerpents, newmaxle*sizeof(lerpents_t)); memset(cl.lerpents + cl.maxlerpents, 0, sizeof(lerpents_t)*(newmaxle - cl.maxlerpents)); cl.maxlerpents = newmaxle; } if (!sold) { isnew = true; sold = snew; //don't crash if anything tries poking sold } else isnew = false; le = &cl.lerpents[snew->number]; if (le->sequence != oldsequence) isnew = true; le->sequence = newsequence; le->entstate = snew; if (snew->u.q1.pmovetype) { if (!cl.do_lerp_players) { entity_state_t *from; float age; packet_entities_t *latest; if (isnew) { /*keep trails correct*/ le->isnew = true; VectorCopy(le->origin, le->lastorigin); } CL_UpdateNetFrameLerpState(sold == snew, snew->frame, snew->baseframe, snew->basebone, le, snew->lerpend); from = sold; //eww age = servertime - oldpack->servertime; latest = &cl.inframes[cl.validsequence & UPDATE_MASK].packet_entities; for (i = 0; i < latest->num_entities; i++) { if (latest->entities[i].number == snew->number) { from = &latest->entities[i]; //use realtime instead. //also, use the sent timings instead of received as those are assumed to be more reliable age = (realtime - cl.outframes[cl.ackedmovesequence & UPDATE_MASK].senttime) - cls.latency*cl_predict_players_latency.value + cl_predict_players_nudge.value; break; } } if (age > 1) age = 1; if (cl_predict_players.ival && pmove.numphysent) { CL_PredictEntityMovement(from, age); VectorCopy(from->u.q1.predorg, le->origin); } else VectorCopy(from->origin, le->origin); VectorCopy(from->angles, le->angles); continue; } //FIXME: find a packet where this entity changed. snew__origin = snew->u.q1.predorg; sold__origin = sold->u.q1.predorg; cos_theta = 1; //don't cut off lerping when the player spins too fast. } else { snew__origin = snew->origin; sold__origin = sold->origin; cos_theta = CompareAngles(sold->angles, snew->angles); } VectorSubtract(snew__origin, sold__origin, move); if (DotProduct(move, move) > maxdist || cos_theta < 0.707 || snew->modelindex != sold->modelindex || ((sold->effects ^ snew->effects) & EF_TELEPORT_BIT)) { isnew = true; //disable lerping (and indirectly trails) // VectorClear(move); } VectorCopy(le->origin, le->lastorigin); if (isnew) { #ifdef RAGDOLL //make sure nothing gets stale if (le->skeletalobject) rag_removedeltaent(le); #endif le->newsequence = snew->sequence; //new this frame (or we noticed something changed significantly) VectorCopy(snew__origin, le->origin); VectorCopy(snew->angles, le->angles); VectorCopy(snew__origin, le->oldorigin); VectorCopy(snew->angles, le->oldangle); VectorCopy(snew__origin, le->neworigin); VectorCopy(snew->angles, le->newangle); if (snew->lerpend) le->orglerpdeltatime = bound(0.001, snew->lerpend - newpack->servertime, cl_lerp_maxinterval.value); else le->orglerpdeltatime = newpack->servertime - oldpack->servertime; le->orglerpstarttime = oldpack->servertime; le->isnew = true; VectorCopy(le->origin, le->lastorigin); } else { if ((sold->effects ^ snew->effects) & EF_RESTARTANIM_BIT) isnew = true; if (snew->dpflags & RENDER_STEP) { float lfrac; //ignore the old packet entirely, except for maybe its time. if (!VectorEquals(le->neworigin, snew__origin) || !VectorEquals(le->newangle, snew->angles)) { le->newsequence = snew->sequence; le->orglerpdeltatime = bound(0, oldpack->servertime - le->orglerpstarttime, cl_lerp_maxinterval.value); //clamp to 10 tics per second le->orglerpstarttime = oldpack->servertime; VectorCopy(le->neworigin, le->oldorigin); VectorCopy(le->newangle, le->oldangle); VectorCopy(snew__origin, le->neworigin); VectorCopy(snew->angles, le->newangle); } if (snew->lerpend) le->orglerpdeltatime = bound(0.001, snew->lerpend - le->orglerpstarttime, cl_lerp_maxinterval.value); lfrac = (servertime - le->orglerpstarttime) / le->orglerpdeltatime; lfrac = bound(0, lfrac, 1); if (r_nolerp.ival) { lfrac = 1; isnew = true; } for (i = 0; i < 3; i++) { le->origin[i] = le->oldorigin[i] + lfrac*(le->neworigin[i] - le->oldorigin[i]); a1 = le->oldangle[i]; a2 = le->newangle[i]; if (a1 - a2 > 180) a1 -= 360; if (a1 - a2 < -180) a1 += 360; le->angles[i] = a1 + lfrac * (a2 - a1); } } else { float lfrac; if (le->newsequence != snew->sequence) { le->newsequence = snew->sequence; VectorCopy(le->neworigin, le->oldorigin); VectorCopy(le->newangle, le->oldangle); VectorCopy(snew__origin, le->neworigin); VectorCopy(snew->angles, le->newangle); //fixme: should be oldservertime le->orglerpdeltatime = bound(0.001, servertime-le->orglerpstarttime, cl_lerp_maxinterval.value); le->orglerpstarttime = servertime; } if (snew->lerpend) le->orglerpdeltatime = bound(0.001, snew->lerpend - le->orglerpstarttime, cl_lerp_maxinterval.value); lfrac = (servertime - le->orglerpstarttime) / le->orglerpdeltatime; lfrac = bound(0, lfrac, 1); //lerp based purely on the packet times, for (i = 0; i < 3; i++) { le->origin[i] = le->oldorigin[i] + lfrac*(le->neworigin[i] - le->oldorigin[i]); a1 = le->oldangle[i]; a2 = le->newangle[i]; if (a1 - a2 > 180) a1 -= 360; if (a1 - a2 < -180) a1 += 360; le->angles[i] = a1 + lfrac * (a2 - a1); } } } #ifdef RAGDOLL //this preprocessor is misnamed, but oh well if (snew->bonecount) { void *newbones = GetBoneSpace(newpack, snew->boneoffset); if (sold && snew->bonecount == sold->bonecount) rag_lerpdeltaent(le, snew->bonecount, newbones, r_nolerp.ival?1:frac, GetBoneSpace(oldpack, sold->boneoffset)); else rag_lerpdeltaent(le, snew->bonecount, newbones, 1, newbones); } #endif CL_UpdateNetFrameLerpState(isnew, snew->frame, snew->baseframe, snew->basebone, le, snew->lerpend); } } static qboolean CL_ChooseInterpolationFrames(int *newf, int *oldf, float servertime) { int i; float newtime = 0; *oldf = -1; *newf = -1; //choose the two packets. //we should be picking the packet just after the server time, and the one just before for (i = cls.netchan.incoming_sequence; i >= cls.netchan.incoming_sequence-UPDATE_MASK; i--) { if (cl.inframes[i&UPDATE_MASK].frameid != i || cl.inframes[i&UPDATE_MASK].invalid) continue; //packetloss/choke, it's really only a problem for the oldframe, but... if (cl.inframes[i&UPDATE_MASK].packet_entities.servertime >= servertime) { if (cl.inframes[i&UPDATE_MASK].packet_entities.servertime) { if (!newtime || newtime != cl.inframes[i&UPDATE_MASK].packet_entities.servertime) //if it's a duplicate, pick the latest (so just-shot rockets are still present) { newtime = cl.inframes[i&UPDATE_MASK].packet_entities.servertime; *newf = i; } } } else if (newtime) { if (cl.inframes[i&UPDATE_MASK].packet_entities.servertime != newtime) { //it does actually lerp, and isn't an identical frame. *oldf = i; break; } } } if (*newf == -1) { /* This can happen if the client's predicted time is greater than the most recently received packet. This should of course not happen... */ // Con_DPrintf("Warning: No lerp-to frame packet\n"); /*just grab the most recent frame that is valid*/ for (i = cls.netchan.incoming_sequence; i >= cls.netchan.incoming_sequence-UPDATE_MASK; i--) { if (cl.inframes[i&UPDATE_MASK].frameid != i || cl.inframes[i&UPDATE_MASK].invalid) continue; //packetloss/choke, it's really only a problem for the oldframe, but... *oldf = *newf = i; return true; } return false; } else if (*oldf == -1) //can happen at map start, and really laggy games, but really shouldn't in a normal game { *oldf = *newf; } return true; } qboolean CL_MayLerp(void) { //force lerping when playing low-framerate demos. if (cls.demoplayback == DPB_MVD) return true; #ifdef NQPROT if (cls.demoplayback == DPB_NETQUAKE) return true; if (cls.protocol == CP_NETQUAKE) //this includes DP protocols. return !cl_nolerp_netquake.ival; #endif if (cl_nolerp.ival == 2 && !cls.deathmatch) return true; return !cl_nolerp.ival; } /*fills in cl.lerpents and cl.currentpackentities*/ void CL_TransitionEntities (void) { packet_entities_t *packnew, *packold; int newf, newff, oldf, i; qboolean nolerp; float servertime, frac; if (cls.protocol == CP_QUAKEWORLD && cls.demoplayback == DPB_MVD) { nolerp = false; } else { nolerp = !CL_MayLerp() && cls.demoplayback != DPB_MVD; } if (cl.demonudge < 0) { //demo playback allows nudging to earlier frames, generally only when paused though... servertime = cl.inframes[(cls.netchan.incoming_sequence+cl.demonudge)&UPDATE_MASK].packet_entities.servertime; nolerp = true; } else if (nolerp) { //force our emulated time to as late as we can, if we're not using interpolation, which has the effect of disabling all interpolation servertime = cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK].packet_entities.servertime; } else { //otherwise go for the latest frame we can. servertime = cl.servertime; } // servertime -= 0.1; /*make sure we have some info for it, on failure keep the info from the last frame (its possible that the frame data can be changed by a network packet, but mneh, but chances are if there's no info then there are NO packets at all)*/ if (!CL_ChooseInterpolationFrames(&newf, &oldf, servertime)) return; newff = newf; newf&=UPDATE_MASK; oldf&=UPDATE_MASK; /*transition the ents and stuff*/ packnew = &cl.inframes[newf].packet_entities; packold = &cl.inframes[oldf].packet_entities; if (packnew->servertime == packold->servertime) frac = 1; //lerp totally into the new (avoid any division-by-0 issues here) else frac = (servertime-packold->servertime)/(packnew->servertime-packold->servertime); // if (!cl.paused) // Con_DPrintf("%f %s%f^7 %f (%f) (%i) %f %s%f^7 %f\n", packold->servertime, (servertimeservertime||packnew->servertimeservertime, frac, newff, cl.oldgametime, (servertimepunchangle[i], frac, packnew->punchangle[i], cl.playerview[i].punchangle_sv); VectorInterpolate(packold->punchorigin[i], frac, packnew->punchorigin[i], cl.playerview[i].punchorigin); } /*and transition players too*/ { float frac, a1, a2; int i, p; vec3_t move; lerpents_t *le; player_state_t *pnew, *pold; if (!cl.do_lerp_players) { newf = newff = oldf = cl.parsecount; newf&=UPDATE_MASK; oldf&=UPDATE_MASK; } if (packnew->servertime == packold->servertime) frac = 1; //lerp totally into the new else frac = (servertime-packold->servertime)/(packnew->servertime-packold->servertime); pnew = &cl.inframes[newf].playerstate[0]; pold = &cl.inframes[oldf].playerstate[0]; for (p = 0; p < cl.allocated_client_slots; p++, pnew++, pold++) { if (pnew->messagenum != newff) { continue; } le = &cl.lerpplayers[p]; VectorSubtract(pnew->predorigin, pold->predorigin, move); if (DotProduct(move, move) > 120*120) frac = 1; //lerp based purely on the packet times, for (i = 0; i < 3; i++) { le->origin[i] = pold->predorigin[i] + frac*(move[i]); a1 = SHORT2ANGLE(pold->command.angles[i]); a2 = SHORT2ANGLE(pnew->command.angles[i]); if (a1 - a2 > 180) a1 -= 360; if (a1 - a2 < -180) a1 += 360; le->angles[i] = a1 + frac * (a2 - a1); } le->orglerpdeltatime = 0.1; le->orglerpstarttime = packold->servertime; } } } void CL_LinkPacketEntities (void) { extern cvar_t gl_part_flame; entity_t *ent; packet_entities_t *pack; entity_state_t *state; lerpents_t *le; model_t *model, *model2; vec3_t old_origin; float autorotate; int i; int newpnum; //, spnum; dlight_t *dl; vec3_t angles; static int flickertime; static int flicker; int trailef, trailidx; int modelflags; struct itemtimer_s *timer, **timerlink; float timestep = cl.time-cl.lastlinktime; extern cvar_t r_ignoreentpvs; vec3_t absmin, absmax; cl.lastlinktime = cl.time; timestep = bound(0, timestep, 0.1); pack = cl.currentpackentities; if (!pack) return; i = cl.currentpacktime*20; if (flickertime != i) { flickertime = i; flicker = rand(); } autorotate = anglemod(100*cl.currentpacktime); #ifdef CSQC_DAT CSQC_DeltaStart(cl.currentpacktime); #endif for (timerlink = &cl.itemtimers; (timer=*timerlink); ) { if (cl.time > timer->end) { *timerlink = timer->next; Z_Free(timer); } else { timerlink = &(*timerlink)->next; /* if (timer->entnum>0) { if (timer->entnum < cl.maxlerpents) { le = &cl.lerpents[timer->entnum]; if (le->sequence == cl.lerpentssequence) VectorCopy(le->origin, timer->origin); } }*/ R_AddItemTimer(timer->origin, cl.time*90 + timer->origin[0] + timer->origin[1] + timer->origin[2], timer->radius, (cl.time - timer->start) / timer->duration, timer->rgb); } } for (newpnum=0 ; newpnumnum_entities ; newpnum++) { state = &pack->entities[newpnum]; #ifdef CSQC_DAT if (CSQC_DeltaUpdate(state)) continue; #endif if (cl_numvisedicts == cl_maxvisedicts) break; if (state->number >= cl.maxlerpents) continue; le = &cl.lerpents[state->number]; ent = &cl_visedicts[cl_numvisedicts]; ent->rtype = RT_MODEL; ent->playerindex = -1; ent->customskin = 0; ent->topcolour = TOP_DEFAULT; ent->bottomcolour = BOTTOM_DEFAULT; #ifdef HEXEN2 ent->h2playerclass = 0; #endif ent->light_known = 0; ent->forcedshader = NULL; ent->shaderTime = 0; memset(&ent->framestate, 0, sizeof(ent->framestate)); VectorCopy(le->origin, ent->origin); //bots or powerup glows. items always glow, bots can be disabled if (state->modelindex != cl_playerindex || r_powerupglow.ival) if (state->effects & (EF_GREEN | EF_BLUE | EF_RED | EF_BRIGHTLIGHT | EF_DIMLIGHT)) { vec3_t colour; float radius; colour[0] = 0; colour[1] = 0; colour[2] = 0; radius = 0; if (state->effects & EF_BRIGHTLIGHT) { radius = max(radius,r_brightlight_colour.vec4[3]); if (!(state->effects & (EF_RED|EF_GREEN|EF_BLUE))) VectorAdd(colour, r_brightlight_colour.vec4, colour); } if (state->effects & EF_DIMLIGHT) { radius = max(radius,r_dimlight_colour.vec4[3]); if (!(state->effects & (EF_RED|EF_GREEN|EF_BLUE))) VectorAdd(colour, r_dimlight_colour.vec4, colour); } if (state->effects & EF_BLUE) { radius = max(radius,r_bluelight_colour.vec4[3]); VectorAdd(colour, r_bluelight_colour.vec4, colour); } if (state->effects & EF_RED) { radius = max(radius,r_redlight_colour.vec4[3]); VectorAdd(colour, r_redlight_colour.vec4, colour); } if (state->effects & EF_GREEN) { radius = max(radius,r_greenlight_colour.vec4[3]); VectorAdd(colour, r_greenlight_colour.vec4, colour); } if (radius) { radius += r_lightflicker.value?((flicker + state->number)&31):0; dl = CL_NewDlight(state->number, ent->origin, radius, 0.1, colour[0], colour[1], colour[2]); if (state->effects & EF_BRIGHTLIGHT) { //urgh. apparently correct for vanilla quake. puts the bright effect about where the firing point is. broken for hexen2, yet still consistent with the hexen2 engine... dl->origin[2] += 16; } } } if ((state->lightpflags & (PFLAGS_FULLDYNAMIC|PFLAGS_CORONA)) && ((state->lightpflags&PFLAGS_FULLDYNAMIC)||state->light[3])) { vec3_t colour; if (!state->light[0] && !state->light[1] && !state->light[2]) { colour[0] = colour[1] = colour[2] = 1; } else { colour[0] = state->light[0]/1024.0f; colour[1] = state->light[1]/1024.0f; colour[2] = state->light[2]/1024.0f; } dl = CL_NewDlight(state->number, ent->origin, state->light[3]?state->light[3]:350, 0.1, colour[0], colour[1], colour[2]); if (!(state->lightpflags & PFLAGS_FULLDYNAMIC)) //corona-only lights shouldn't do much else. { dl->flags &= ~(LFLAG_LIGHTMAP|LFLAG_FLASHBLEND); #ifdef RTLIGHTS /*make sure there's no rtlight*/ memset(dl->lightcolourscales, 0, sizeof(dl->lightcolourscales)); #endif } dl->corona = (state->lightpflags & PFLAGS_CORONA)?1:0; dl->coronascale = 0.25; dl->style = state->lightstyle; dl->flags &= ~LFLAG_FLASHBLEND; dl->flags |= (state->lightpflags & PFLAGS_NOSHADOW)?LFLAG_NOSHADOWS:0; #ifdef RTLIGHTS if (state->skinnum) { VectorCopy(le->angles, angles); //if (model && model->type == mod_alias) AngleVectorsMesh(angles, dl->axis[0], dl->axis[1], dl->axis[2]); //pflags matches alias models. //else // AngleVectors(angles, dl->axis[0], dl->axis[1], dl->axis[2]); VectorInverse(dl->axis[1]); R_LoadNumberedLightTexture(dl, state->skinnum); } #endif } if (r_torch.ival && state->number <= cl.allocated_client_slots) { dlight_t *dl; dl = CL_NewDlight(state->number, ent->origin, 300, r_torch.ival, 0.9, 0.9, 0.6); dl->flags |= LFLAG_SHADOWMAP|LFLAG_FLASHBLEND; dl->fov = 90; VectorCopy(le->angles, angles); angles[0] *= 3; // angles[1] += sin(realtime)*8; // angles[0] += cos(realtime*1.13)*5; AngleVectorsMesh(angles, dl->axis[0], dl->axis[1], dl->axis[2]); VectorInverse(dl->axis[1]); VectorMA(dl->origin, 16, dl->axis[0], dl->origin); } // if set to invisible, skip if (state->modelindex<1 || (state->effects & NQEF_NODRAW)) { if (state->tagindex == 0xffff) { if (state->tagentity) { ent->rtype = RT_PORTALCAMERA; ent->keynum = state->tagentity; } else { ent->rtype = RT_PORTALSURFACE; VectorCopy(ent->origin, ent->oldorigin); } } else continue; model = NULL; modelflags = state->effects>>24; } else { if (CL_FilterModelindex(state->modelindex, state->frame)) continue; model = cl.model_precache[state->modelindex]; if (!model) { Con_DPrintf("Bad modelindex (%i)\n", state->modelindex); continue; } if (model->loadstate != MLS_LOADED) { if (model->loadstate == MLS_NOTLOADED) Mod_LoadModel(model, MLV_WARN); continue; //still waiting for it to load, don't poke anything here } //DP extension. .modelflags (which is sent in the high parts of effects) allows to specify exactly the q1-compatible flags. //the extra bit allows for setting to 0. //note that hexen2 has additional flags which cannot be expressed. modelflags = state->effects>>24; if (!(state->effects & EF_NOMODELFLAGS)) modelflags |= model->flags; } #ifdef HAVE_LEGACY if (cl.model_precache_vwep[0] && state->modelindex2 < MAX_VWEP_MODELS) { if (state->modelindex == cl_playerindex && cl.model_precache_vwep[0]->loadstate == MLS_LOADED && state->modelindex2 && cl.model_precache_vwep[state->modelindex2] && cl.model_precache_vwep[state->modelindex2]->loadstate == MLS_LOADED) { model = cl.model_precache_vwep[0]; model2 = cl.model_precache_vwep[state->modelindex2]; } else model2 = NULL; } else #endif if (state->modelindex2 && state->modelindex2 < MAX_PRECACHE_MODELS) model2 = cl.model_precache[state->modelindex2]; else model2 = NULL; if (r_ignoreentpvs.ival || !model) { ent->pvscache.num_leafs = 0; #if defined(Q2BSPS) || defined(Q3BSPS) || defined(TERRAIN) ent->pvscache.areanum = 0; ent->pvscache.areanum2 = 0; ent->pvscache.headnode = 0; #endif } else { /*bsp model size*/ if (model->type == mod_brush && (state->angles[0]||state->angles[1]||state->angles[2])) { int i; float v; float max; //q2 method, works best with origin brushes. max = 0; for (i=0 ; i<3 ; i++) { v =fabs( model->mins[i]); if (v > max) max = v; v =fabs( model->maxs[i]); if (v > max) max = v; } for (i=0 ; i<3 ; i++) { absmin[i] = ent->origin[i] - max; absmax[i] = ent->origin[i] + max; } } else { VectorAdd(model->mins, ent->origin, absmin); VectorAdd(model->maxs, ent->origin, absmax); } cl.worldmodel->funcs.FindTouchedLeafs(cl.worldmodel, &ent->pvscache, absmin, absmax); } cl_numvisedicts++; ent->forcedshader = NULL; ent->keynum = state->number; if (cl_r2g.value && state->modelindex == cl_rocketindex && cl_rocketindex != -1 && cl_grenadeindex != -1) model = cl.model_precache[cl_grenadeindex]; ent->model = model; ent->flags = 0; if ((state->dpflags & RENDER_EXTERIORMODEL) || r_refdef.playerview->viewentity == state->number) ent->flags |= RF_EXTERNALMODEL; if (state->dpflags & RENDER_VIEWMODEL) { ent->flags |= RF_WEAPONMODEL|Q2RF_MINLIGHT|RF_DEPTHHACK; if (state->effects & DPEF_NOGUNBOB) ent->flags |= RF_WEAPONMODELNOBOB; } if (state->effects & NQEF_ADDITIVE) ent->flags |= RF_ADDITIVE; if (state->effects & EF_NODEPTHTEST) ent->flags |= RF_NODEPTHTEST; if (state->effects & EF_NOSHADOW) ent->flags |= RF_NOSHADOW; if (state->trans < 0xfe) { ent->shaderRGBAf[3] = state->trans/(float)0xfe; ent->flags |= RF_TRANSLUCENT; } else ent->shaderRGBAf[3] = 1; /* if (le->origin[2] < r_refdef.waterheight != le->lastorigin[2] < r_refdef.waterheight) { P_RunParticleEffectTypeString(le->origin, NULL, 1, "te_watertransition"); } */ // set colormap if (state->dpflags & RENDER_COLORMAPPED) { ent->topcolour = (state->colormap>>4) & 0xf; ent->bottomcolour = (state->colormap>>0) & 0xf; } else if (state->colormap > 0 && state->colormap <= cl.allocated_client_slots) { ent->playerindex = state->colormap-1; #ifdef HEXEN2 ent->h2playerclass = cl.players[ent->playerindex].h2playerclass; #endif ent->topcolour = cl.players[ent->playerindex].dtopcolor; ent->bottomcolour = cl.players[ent->playerindex].dbottomcolor; } // set skin ent->skinnum = state->skinnum; #ifdef HEXEN2 ent->abslight = state->abslight; ent->drawflags = state->hexen2flags; #endif CL_LerpNetFrameState(&ent->framestate, le); #ifdef PEXT_SCALE //set scale ent->scale = state->scale/16.0; #endif if (state->colormod[0] == 32 && state->colormod[1] == 32 && state->colormod[2] == 32) ent->shaderRGBAf[0] = ent->shaderRGBAf[1] = ent->shaderRGBAf[2] = 1; else { ent->flags |= RF_FORCECOLOURMOD; ent->shaderRGBAf[0] = (state->colormod[0]*8.0f)/256; ent->shaderRGBAf[1] = (state->colormod[1]*8.0f)/256; ent->shaderRGBAf[2] = (state->colormod[2]*8.0f)/256; } VectorScale(state->glowmod, 8.0/256.0, ent->glowmod); #ifdef PEXT_FATNESS //set trans ent->fatness = state->fatness/16.0; #endif //swap items with sprites if desired. if (gl_simpleitems.ival && ent->skinnum >= 0 && ent->skinnum < countof(model->simpleskin) && model) { if (!model->simpleskin[ent->skinnum]) { char basename[64], name[MAX_QPATH]; COM_FileBase(model->name, basename, sizeof(basename)); if (!strncmp(model->name, "maps/", 5)) Q_snprintfz(name, sizeof(name), "textures/bmodels/simple_%s_%i.tga", basename, ent->skinnum); else Q_snprintfz(name, sizeof(name), "textures/models/simple_%s_%i.tga", basename, ent->skinnum); model->simpleskin[ent->skinnum] = R_RegisterShader(name, 0, va("{\nnomipmaps\nprogram defaultsprite#MASK=0.5\nsurfaceparm noshadows\nsurfaceparm nodlight\nsort seethrough\n{\nmap \"%s\"\nalphafunc ge128\n}\n}\n", name)); } VectorCopy(le->angles, angles); if (R_GetShaderSizes(model->simpleskin[ent->skinnum], NULL, NULL, false) > 0) { float tr[2]; ent->forcedshader = model->simpleskin[ent->skinnum]; ent->rtype = RT_SPRITE; ent->scale *= 16; tr[0] = sin(le->angles[1] * M_PI / 180.0); tr[1] = cos(le->angles[1] * M_PI / 180.0); ent->origin[1] += tr[0] * (model->maxs[0] + model->mins[0])*0.5 + tr[1] * (model->maxs[1] + model->mins[1])*0.5; ent->origin[0] += tr[1] * (model->maxs[1] + model->mins[1])*0.5 - tr[0] * (model->maxs[0] + model->mins[0])*0.5; ent->origin[2] += model->mins[2]; ent->origin[2] += ent->scale; if (cl_item_bobbing.value) ent->origin[2] += 5+sin(cl.time*3+(ent->origin[0]+ent->origin[1])/8)*5.5; //don't let it into the ground } else if (modelflags & MF_ROTATE) { //surely there's a more sane way to handle this. angles[0] = 0; angles[1] = autorotate; angles[2] = 0; if (cl_item_bobbing.value) ent->origin[2] += 5+sin(cl.time*3+(state->origin[0]+state->origin[1])/8)*5.5; //don't let it into the ground } } // rotate pickup objects locally else if (modelflags & MF_ROTATE) { angles[0] = 0; angles[1] = autorotate; angles[2] = 0; if (cl_item_bobbing.value) ent->origin[2] += 5+sin(cl.time*3+(state->origin[0]+state->origin[1])/8)*5.5; //don't let it into the ground } else { for (i=0 ; i<3 ; i++) { angles[i] = le->angles[i]; } } VectorCopy(angles, ent->angles); if (model->type == mod_alias) AngleVectorsMesh(angles, ent->axis[0], ent->axis[1], ent->axis[2]); else AngleVectors(angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); /*if this entity is in a player's slot...*/ if (ent->keynum <= cl.allocated_client_slots) { if (!cl.playerview[0].nolocalplayer) ent->keynum += MAX_EDICTS; } if (state->tagindex == 0xffff) { if (state->tagentity) { ent->rtype = RT_PORTALCAMERA; ent->keynum = state->tagentity; } else { ent->rtype = RT_PORTALSURFACE; VectorCopy(ent->origin, ent->oldorigin); } } else if (state->tagentity) { //ent is attached to a tag, rotate this ent accordingly. CL_RotateAroundTag(ent, state->number, state->tagentity, state->tagindex); } #ifdef RAGDOLL if (model && (model->dollinfo || le->skeletalobject)) rag_updatedeltaent(&csqc_world, ent, le); #endif ent->framestate.g[FS_REG].frame[0] &= ~0x8000; ent->framestate.g[FS_REG].frame[1] &= ~0x8000; CLQ1_AddShadow(ent); CLQ1_AddPowerupShell(ent, false, state->effects); if (model2) CL_AddVWeapModel (ent, model2); //figure out which trail this entity is using if (model) { trailef = model->particletrail; trailidx = model->traildefaultindex; } else { trailef = P_INVALID; trailidx = P_INVALID; } if ((state->effects & EF_HASPARTICLETRAIL) || modelflags) P_DefaultTrail (state->effects, modelflags, &trailef, &trailidx); if (state->u.q1.traileffectnum) trailef = CL_TranslateParticleFromServer(state->u.q1.traileffectnum); if (state->u.q1.emiteffectnum) P_EmitEffect (ent->origin, ent->axis, MDLF_EMITFORWARDS, CL_TranslateParticleFromServer(state->u.q1.emiteffectnum), &(le->emitstate)); else if (model && model->particleeffect != P_INVALID && cls.allow_anyparticles && gl_part_flame.ival) P_EmitEffect (ent->origin, ent->axis, model->engineflags, model->particleeffect, &(le->emitstate)); // add automatic particle trails if (!model || (!(modelflags&~MF_ROTATE) && trailef < 0)) continue; if (!cls.allow_anyparticles && !(modelflags & ~MF_ROTATE)) continue; if (le->isnew) { le->isnew = false; pe->DelinkTrailstate(&(cl.lerpents[state->number].trailstate)); pe->DelinkTrailstate(&(cl.lerpents[state->number].emitstate)); continue; // not in last message } VectorCopy(le->lastorigin, old_origin); for (i=0 ; i<3 ; i++) { if ( fabs(old_origin[i] - ent->origin[i]) > 128) { // no trail if too far VectorCopy (ent->origin, old_origin); break; } } //and emit it // if (lasttime != cl.currentpacktime) { if (trailef == P_INVALID || pe->ParticleTrail (old_origin, ent->origin, trailef, timestep, ent->keynum, ent->axis, &(le->trailstate))) if (model->traildefaultindex >= 0) pe->ParticleTrailIndex(old_origin, ent->origin, P_INVALID, timestep, trailidx, 0, &(le->trailstate)); //dlights are not so customisable. if (r_rocketlight.value && (modelflags & MF_ROCKET) && !(state->lightpflags & (PFLAGS_FULLDYNAMIC|PFLAGS_CORONA))) { float rad = 0; extern cvar_t r_rocketlight_colour; rad = r_rocketlight_colour.vec4[3]; rad += r_lightflicker.value?((flicker + state->number)&31):0; dl = CL_AllocDlight (state->number); memcpy(dl->axis, ent->axis, sizeof(dl->axis)); VectorCopy (ent->origin, dl->origin); dl->die = (float)cl.time; if (modelflags & MF_ROCKET) dl->origin[2] += 1; // is this even necessary dl->radius = rad * r_rocketlight.value; VectorCopy(r_rocketlight_colour.vec4, dl->color); } } } #ifdef CSQC_DAT CSQC_DeltaEnd(); #endif CLQ1_AddVisibleBBoxes(); #ifdef RTLIGHTS R_EditLights_DrawLights(); #endif } /* ========================================================================= PROJECTILE PARSING / LINKING ========================================================================= */ typedef struct { int modelindex; vec3_t origin; vec3_t angles; } projectile_t; #define MAX_PROJECTILES 32 projectile_t cl_projectiles[MAX_PROJECTILES]; int cl_num_projectiles; extern int cl_spikeindex; void CL_ClearProjectiles (void) { cl_num_projectiles = 0; } /* ===================== CL_ParseProjectiles Nails are passed as efficient temporary entities ===================== */ void CL_ParseProjectiles (int modelindex, qboolean nails2) { int i, c, j; qbyte bits[6]; projectile_t *pr; c = MSG_ReadByte (); for (i=0 ; imodelindex = modelindex; pr->origin[0] = ( ( bits[0] + ((bits[1]&15)<<8) ) <<1) - 4096; pr->origin[1] = ( ( (bits[1]>>4) + (bits[2]<<4) ) <<1) - 4096; pr->origin[2] = ( ( bits[3] + ((bits[4]&15)<<8) ) <<1) - 4096; pr->angles[0] = 360*(((int)bits[4]>>4)/16.0f + 1/32.0f); pr->angles[1] = 360*(int)bits[5]/256.0f; } } /* ============= CL_LinkProjectiles ============= */ void CL_LinkProjectiles (void) { int i; projectile_t *pr; entity_t *ent; for (i=0, pr=cl_projectiles ; imodelindex < 1) continue; // grab an entity to fill in if (cl_numvisedicts == cl_maxvisedicts) break; // object list is full ent = &cl_visedicts[cl_numvisedicts]; cl_numvisedicts++; memset(ent, 0, sizeof(*ent)); ent->model = cl.model_precache[pr->modelindex]; ent->playerindex = -1; ent->topcolour = TOP_DEFAULT; ent->bottomcolour = BOTTOM_DEFAULT; ent->framestate.g[FS_REG].lerpweight[0] = 1; #ifdef PEXT_SCALE ent->scale = 1; #endif ent->shaderRGBAf[0] = 1; ent->shaderRGBAf[1] = 1; ent->shaderRGBAf[2] = 1; ent->shaderRGBAf[3] = 1; VectorCopy (pr->origin, ent->origin); VectorCopy (pr->angles, ent->angles); AngleVectorsMesh(ent->angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); } } //======================================== extern int cl_spikeindex, cl_playerindex, cl_flagindex, cl_rocketindex, cl_grenadeindex; entity_t *CL_NewTempEntity (void); static int MVD_TranslateFlags(int src) { int dst = 0; if (src & DF_EFFECTS) dst |= PF_EFFECTS; if (src & DF_SKINNUM) dst |= PF_SKINNUM; if (src & DF_DEAD) dst |= PF_DEAD; if (src & DF_GIB) dst |= PF_GIB; if (src & DF_WEAPONFRAME) dst |= PF_WEAPONFRAME; if (src & DF_MODEL) dst |= PF_MODEL; return dst; } /* =================== CL_ParsePlayerinfo =================== */ extern int parsecountmod, oldparsecountmod; extern double parsecounttime; void CL_ParseClientdata (void); void CL_MVDUpdateSpectator(void) { CL_ParseClientdata(); } void CLQW_ParsePlayerinfo (void) { float msec; unsigned int flags; player_info_t *info; player_state_t *state, *oldstate; unsigned int num; int i; int newf; vec3_t org, dist; if (cls.fteprotocolextensions2&PEXT2_LONGINDEXES) num = MSG_ReadUInt64 (); else num = MSG_ReadByte (); if (num >= MAX_CLIENTS) Host_EndGame ("CL_ParsePlayerinfo: bad num"); info = &cl.players[num]; oldstate = &cl.inframes[oldparsecountmod].playerstate[num]; state = &cl.inframes[parsecountmod].playerstate[num]; if (cls.demoplayback == DPB_MVD) { #ifdef QUAKESTATS int i; const char *viewmodel = NULL; static struct { const char *vmdl; const char *vwep; } vwep_mapping[] = { {"progs/v_axe.mdl", "progs/w_axe.mdl"}, {"progs/v_shot.mdl", "progs/w_shot.mdl"}, {"progs/v_shot2.mdl", "progs/w_shot2.mdl"}, {"progs/v_nail.mdl", "progs/w_nail.mdl"}, {"progs/v_nail2.mdl", "progs/w_nail2.mdl"}, {"progs/v_rock.mdl", "progs/w_rock.mdl"}, {"progs/v_rock2.mdl", "progs/w_rock2.mdl"}, {"progs/v_light.mdl", "progs/w_light.mdl"}, }; #endif player_state_t dummy; if (!cl.parsecount || info->prevcount > cl.parsecount || cl.parsecount - info->prevcount >= UPDATE_BACKUP - 1) { memset(&dummy, 0, sizeof(dummy)); oldstate = &dummy; } else { oldstate = &cl.inframes[info->prevcount & UPDATE_MASK].playerstate[num]; } memcpy(state, oldstate, sizeof(player_state_t)); info->prevcount = cl.parsecount; #ifdef QUAKESTATS if (cls.findtrack && info->stats[STAT_HEALTH] > 0) { //FIXME: is this still needed with the autotrack stuff? Cam_Lock(&cl.playerview[0], num); cls.findtrack = false; } #endif flags = MSG_ReadShort (); state->flags = MVD_TranslateFlags(flags); state->messagenum = cl.parsecount; state->command.msec = 0; state->command.impulse = 0; #ifdef QUAKESTATS i = cl.players[num].stats[STAT_WEAPONMODELI]; if (i>0&&icommand.impulse = i; break; } } break; } } } #endif state->frame = MSG_ReadByte (); state->state_time = parsecounttime; for (i = 0; i < 3; i++) { if (flags & (DF_ORIGINX << i)) { if (cls.ezprotocolextensions1 & EZPEXT1_FLOATENTCOORDS) state->origin[i] = MSG_ReadCoordFloat (); else state->origin[i] = MSG_ReadCoord (); } } VectorSubtract(state->origin, oldstate->origin, dist); VectorScale(dist, 1/(cl.inframes[parsecountmod].packet_entities.servertime - cl.inframes[oldparsecountmod].packet_entities.servertime), state->velocity); VectorCopy (state->origin, state->predorigin); for (i = 0; i < 3; i++) { if (flags & (DF_ANGLEX << i)) { state->command.angles[i] = MSG_ReadShort(); } state->viewangles[i] = state->command.angles[i] * (360.0/65536); } if (flags & DF_MODEL) state->modelindex = MSG_ReadByte (); if (flags & DF_SKINNUM) state->skinnum = MSG_ReadByte (); if (flags & DF_EFFECTS) state->effects = MSG_ReadByte (); if (flags & DF_WEAPONFRAME) state->weaponframe = MSG_ReadByte (); VectorSet(state->szmins, -16, -16, -24); VectorSet(state->szmaxs, 16, 16, 32); state->scale = 1; state->alpha = 255; state->fatness = 0; state->colourmod[0] = 32; state->colourmod[1] = 32; state->colourmod[2] = 32; state->gravitydir[0] = 0; state->gravitydir[1] = 0; state->gravitydir[2] = -1; state->pm_type = PM_NORMAL; #ifdef QUAKESTATS TP_ParsePlayerInfo(oldstate, state, info); //can't CL_SetStatInt as we don't know if its actually us or not cl.players[num].stats[STAT_WEAPONFRAME] = state->weaponframe; cl.players[num].statsf[STAT_WEAPONFRAME] = state->weaponframe; for (i = 0; i < cl.splitclients; i++) { playerview_t *pv = &cl.playerview[i]; if (pv->cam_spec_track == num) { pv->stats[STAT_WEAPONFRAME] = state->weaponframe; pv->statsf[STAT_WEAPONFRAME] = state->weaponframe; } } #endif //add a new splitscreen autotrack view if we can if (cl.splitclients < MAX_SPLITS && !cl.players[num].spectator) { if (cl.splitclients < cl_splitscreen.value+1) { for (i = 0; i < cl.splitclients; i++) { playerview_t *pv = &cl.playerview[i]; if (pv->cam_state != CAM_FREECAM && pv->cam_spec_track == num) return; } if (i == cl.splitclients) { playerview_t *pv = &cl.playerview[cl.splitclients++]; Cam_Lock(pv, num); } } } return; } flags = (unsigned short)MSG_ReadShort (); if (cls.fteprotocolextensions & (PEXT_HULLSIZE|PEXT_TRANS|PEXT_SCALE|PEXT_FATNESS)) { if (flags & PF_EXTRA_PFS) flags |= MSG_ReadByte()<<16; } else flags = (flags & 0x3fff) | ((flags & 0xc000)<<8); state->flags = flags; state->messagenum = cl.parsecount; if (cls.ezprotocolextensions1 & EZPEXT1_FLOATENTCOORDS) { org[0] = MSG_ReadCoordFloat (); org[1] = MSG_ReadCoordFloat (); org[2] = MSG_ReadCoordFloat (); } else { org[0] = MSG_ReadCoord (); org[1] = MSG_ReadCoord (); org[2] = MSG_ReadCoord (); } VectorCopy(org, state->origin); newf = MSG_ReadByte (); if (state->frame != newf) { // state->lerpstarttime = realtime; state->frame = newf; } // the other player's last move was likely some time // before the packet was sent out, so accurately track // the exact time it was valid at if (flags & PF_MSEC) { extern cvar_t cl_demospeed; msec = MSG_ReadByte (); if (cls.demoplayback) state->state_time = parsecounttime - msec*0.001 * cl_demospeed.value; else state->state_time = parsecounttime - msec*0.001; } else { msec = 0; state->state_time = parsecounttime; } if (flags & PF_COMMAND) { MSGQW_ReadDeltaUsercmd (&nullcmd, &state->command, cl.protocol_qw); state->viewangles[0] = state->command.angles[0] * (360.0/65536); state->viewangles[1] = state->command.angles[1] * (360.0/65536); state->viewangles[2] = state->command.angles[2] * (360.0/65536); if (!(cls.z_ext & Z_EXT_VWEP)) state->command.impulse = 0; } for (i=0 ; i<3 ; i++) { if (flags & (PF_VELOCITY1<velocity[i] = MSG_ReadShort(); else state->velocity[i] = 0; } if (flags & PF_MODEL) state->modelindex = MSG_ReadByte (); else state->modelindex = cl_playerindex; if (flags & PF_SKINNUM) { state->skinnum = MSG_ReadByte (); if (state->skinnum & (1<<7) && (flags & PF_MODEL)) { state->modelindex+=256; state->skinnum -= (1<<7); } } else state->skinnum = 0; if (flags & PF_EFFECTS) state->effects = MSG_ReadByte (); else state->effects = 0; if (flags & PF_WEAPONFRAME) state->weaponframe = MSG_ReadByte (); else state->weaponframe = 0; VectorSet(state->szmins, -16, -16, -24); VectorSet(state->szmaxs, 16, 16, 32); state->scale = 1; state->alpha = 255; state->fatness = 0; state->gravitydir[0] = 0; state->gravitydir[1] = 0; state->gravitydir[2] = -1; #ifdef PEXT_SCALE if ((flags & PF_SCALE) && (cls.fteprotocolextensions & PEXT_SCALE)) state->scale = MSG_ReadByte()/50.0; #endif #ifdef PEXT_TRANS if ((flags & PF_TRANS) && (cls.fteprotocolextensions & PEXT_TRANS)) state->alpha = MSG_ReadByte(); #endif #ifdef PEXT_FATNESS if ((flags & PF_FATNESS) && (cls.fteprotocolextensions & PEXT_FATNESS)) state->fatness = MSG_ReadChar(); #endif #ifdef PEXT_HULLSIZE if ((cls.fteprotocolextensions & PEXT_HULLSIZE) && (flags & PF_HULLSIZE_Z)) { int num; num = MSG_ReadByte(); if (!cl.worldmodel || cl.worldmodel->fromgame != fg_quake) { VectorScale(state->szmins, num/56.0f, state->szmins); VectorScale(state->szmaxs, num/56.0f, state->szmaxs); } else { VectorCopy(cl.worldmodel->hulls[num&(MAX_MAP_HULLSM-1)].clip_mins, state->szmins); VectorCopy(cl.worldmodel->hulls[num&(MAX_MAP_HULLSM-1)].clip_maxs, state->szmaxs); } if (num & 128) { //this hack is for hexen2. state->szmaxs[2] -= state->szmins[2]; state->szmins[2] = 0; } } //should be passed to player move func. #endif if (cls.z_ext & Z_EXT_PF_ONGROUND) state->onground = !!(flags & PF_ONGROUND); else state->onground = false; if ((cls.fteprotocolextensions & PEXT_COLOURMOD) && (flags & PF_COLOURMOD)) { state->colourmod[0] = MSG_ReadByte(); state->colourmod[1] = MSG_ReadByte(); state->colourmod[2] = MSG_ReadByte(); } else { state->colourmod[0] = 32; state->colourmod[1] = 32; state->colourmod[2] = 32; } //if we have no solidity info, guess. if (!(cls.z_ext & Z_EXT_PF_SOLID)) { if (cl.players[num].spectator || state->flags & PF_DEAD) state->flags &= ~PF_SOLID; else state->flags |= PF_SOLID; } if (cls.z_ext & Z_EXT_PM_TYPE) { int pm_code; pm_code = (flags&PF_PMC_MASK) >> PF_PMC_SHIFT; if (pm_code == PMC_NORMAL || pm_code == PMC_NORMAL_JUMP_HELD) { if (flags & PF_DEAD) state->pm_type = PM_DEAD; else { state->pm_type = PM_NORMAL; state->jump_held = (pm_code == PMC_NORMAL_JUMP_HELD); } } else if (pm_code == PMC_OLD_SPECTATOR) state->pm_type = PM_OLD_SPECTATOR; else { if (cls.z_ext & Z_EXT_PM_TYPE_NEW) { if (pm_code == PMC_SPECTATOR) state->pm_type = PM_SPECTATOR; else if (pm_code == PMC_FLY) state->pm_type = PM_FLY; else if (pm_code == PMC_NONE) state->pm_type = PM_NONE; else if (pm_code == PMC_FREEZE) state->pm_type = PM_FREEZE; else if (pm_code == PMC_WALLWALK) state->pm_type = PM_WALLWALK; else { // future extension? goto guess_pm_type; } } else { // future extension? goto guess_pm_type; } } } else { guess_pm_type: if (cl.players[num].spectator) state->pm_type = PM_OLD_SPECTATOR; else if (flags & PF_DEAD) state->pm_type = PM_DEAD; else state->pm_type = PM_NORMAL; } #ifdef QUAKESTATS TP_ParsePlayerInfo(oldstate, state, info); //can't CL_SetStatInt as we don't know if its actually us or not for (i = 0; i < cl.splitclients; i++) { playerview_t *pv = &cl.playerview[i]; if ((pv->spectator?pv->cam_spec_track:pv->playernum) == num) { pv->stats[STAT_WEAPONFRAME] = state->weaponframe; pv->statsf[STAT_WEAPONFRAME] = state->weaponframe; } } #endif if (cl.worldmodel && cl.do_lerp_players && cl_predict_players.ival) { player_state_t exact; msec -= 1000 * (cls.latency*cl_predict_players_latency.value-cl_predict_players_nudge.value); // msec = 1000*((realtime - cls.latency + 0.02) - state->state_time); // predict players movement state->command.msec = bound(0, msec, 255); //FIXME: flag these and do the pred elsewhere. CL_SetSolidEntities(); CL_SetSolidPlayers(); CL_PredictUsercmd (0, num+1, state, &exact, &state->command); //uses player 0's maxspeed/grav... VectorCopy (exact.origin, state->predorigin); } else VectorCopy (state->origin, state->predorigin); } /* void CL_ParseClientPersist(void) { player_info_t *info; int flags; flags = MSG_ReadShort(); info = &cl.players[lastplayerinfo]; if (flags & 1) info->vweapindex = MSG_ReadShort(); } */ /* ================ CL_AddFlagModels Called when the CTF flags are set ================ */ void CL_AddFlagModels (entity_t *ent, int team) { int i; float f; vec3_t v_forward, v_right, v_up; entity_t *newent; vec3_t angles; float offs = 0; if (cl_flagindex == -1) return; for (i = 0; i < FRAME_BLENDS; i++) { if (!ent->framestate.g[FS_REG].lerpweight[i]) continue; f = 14; if (ent->framestate.g[FS_REG].frame[i] >= 29 && ent->framestate.g[FS_REG].frame[i] <= 40) { if (ent->framestate.g[FS_REG].frame[i] >= 29 && ent->framestate.g[FS_REG].frame[i] <= 34) { //axpain if (ent->framestate.g[FS_REG].frame[i] == 29) f = f + 2; else if (ent->framestate.g[FS_REG].frame[i] == 30) f = f + 8; else if (ent->framestate.g[FS_REG].frame[i] == 31) f = f + 12; else if (ent->framestate.g[FS_REG].frame[i] == 32) f = f + 11; else if (ent->framestate.g[FS_REG].frame[i] == 33) f = f + 10; else if (ent->framestate.g[FS_REG].frame[i] == 34) f = f + 4; } else if (ent->framestate.g[FS_REG].frame[i] >= 35 && ent->framestate.g[FS_REG].frame[i] <= 40) { // pain if (ent->framestate.g[FS_REG].frame[i] == 35) f = f + 2; else if (ent->framestate.g[FS_REG].frame[i] == 36) f = f + 10; else if (ent->framestate.g[FS_REG].frame[i] == 37) f = f + 10; else if (ent->framestate.g[FS_REG].frame[i] == 38) f = f + 8; else if (ent->framestate.g[FS_REG].frame[i] == 39) f = f + 4; else if (ent->framestate.g[FS_REG].frame[i] == 40) f = f + 2; } } else if (ent->framestate.g[FS_REG].frame[i] >= 103 && ent->framestate.g[FS_REG].frame[i] <= 118) { if (ent->framestate.g[FS_REG].frame[i] >= 103 && ent->framestate.g[FS_REG].frame[i] <= 104) f = f + 6; //nailattack else if (ent->framestate.g[FS_REG].frame[i] >= 105 && ent->framestate.g[FS_REG].frame[i] <= 106) f = f + 6; //light else if (ent->framestate.g[FS_REG].frame[i] >= 107 && ent->framestate.g[FS_REG].frame[i] <= 112) f = f + 7; //rocketattack else if (ent->framestate.g[FS_REG].frame[i] >= 112 && ent->framestate.g[FS_REG].frame[i] <= 118) f = f + 7; //shotattack } offs += f * ent->framestate.g[FS_REG].lerpweight[i]; } newent = CL_NewTempEntity (); newent->model = cl.model_precache[cl_flagindex]; newent->skinnum = team; newent->keynum = ent->keynum; newent->flags |= ent->flags; AngleVectors (ent->angles, v_forward, v_right, v_up); v_forward[2] = -v_forward[2]; // reverse z component for (i=0 ; i<3 ; i++) newent->origin[i] = ent->origin[i] - offs*v_forward[i] + 22*v_right[i]; newent->origin[2] -= 16; VectorCopy (ent->angles, newent->angles); newent->angles[2] -= 45; VectorCopy(newent->angles, angles); AngleVectorsMesh(angles, newent->axis[0], newent->axis[1], newent->axis[2]); VectorInverse(newent->axis[1]); } void CL_AddVWeapModel(entity_t *player, model_t *model) { entity_t *newent; // vec3_t angles; if (!model) return; newent = CL_NewTempEntity (); newent->keynum = player->keynum; newent->flags |= player->flags; VectorCopy(player->origin, newent->origin); VectorCopy(player->angles, newent->angles); newent->skinnum = player->skinnum; newent->model = model; newent->framestate = player->framestate; AngleVectors(newent->angles, newent->axis[0], newent->axis[1], newent->axis[2]); VectorInverse(newent->axis[1]); } /* ============= CL_LinkPlayers Create visible entities in the correct position for all current players ============= */ vec3_t nametagorg[MAX_CLIENTS]; qboolean nametagseen[MAX_CLIENTS]; void CL_LinkPlayers (void) { int pnum; int j; player_info_t *info; player_state_t *state; player_state_t exact; double playertime; entity_t *ent; float msec; inframe_t *frame; int oldphysent; vec3_t angles; qboolean predictplayers; model_t *model; static int flickertime; static int flicker; float predictmsmult = 1000*cl_predict_players_frac.value; #ifdef HAVE_LEGACY int modelindex2; #endif extern cvar_t cl_demospeed; int displayseq; if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED) return; if (cl.paused) predictmsmult = 0; if (cls.demoplayback) predictmsmult *= cl_demospeed.value; playertime = realtime - cls.latency*cl_predict_players_latency.value + cl_predict_players_nudge.value; if (playertime > realtime) playertime = realtime; if (cl.demonudge < 0) displayseq = cl.lerpentssequence; else displayseq = cl.validsequence; frame = &cl.inframes[displayseq&UPDATE_MASK]; predictplayers = cl_predict_players.ival; if (cls.demoplayback == DPB_MVD) predictplayers = false; for (j=0, info=cl.players, state=frame->playerstate ; j < cl.allocated_client_slots ; j++, info++, state++) { nametagseen[j] = false; if (state->messagenum != displayseq) { #ifdef CSQC_DAT CSQC_DeltaPlayer(j, NULL); #endif continue; // not present this frame } CL_UpdateNetFrameLerpState(false, state->frame, 0, 0, &cl.lerpplayers[j], 0); cl.lerpplayers[j].sequence = cl.lerpentssequence; #ifdef CSQC_DAT if (CSQC_DeltaPlayer(j, state)) continue; #endif if (info->spectator || state->modelindex >= countof(cl.model_precache)) continue; //the extra modelindex check is to stop lame mods from using vweps with rings #ifdef HAVE_LEGACY if (state->command.impulse && cl.model_precache_vwep[0] && cl.model_precache_vwep[0]->type != mod_dummy && state->modelindex == cl_playerindex) { model = cl.model_precache_vwep[0]; modelindex2 = state->command.impulse; } else #endif { model = cl.model_precache[state->modelindex]; #ifdef HAVE_LEGACY modelindex2 = 0; #endif } // spawn light flashes, even ones coming from invisible objects if (r_powerupglow.value && !(r_powerupglow.value == 2 && j == cl.playerview[0].playernum) && (state->effects & (EF_BLUE|EF_RED|EF_GREEN|EF_BRIGHTLIGHT|EF_DIMLIGHT))) { vec3_t colour; float radius; colour[0] = 0; colour[1] = 0; colour[2] = 0; radius = 0; if (state->effects & EF_BRIGHTLIGHT) { radius = max(radius,r_brightlight_colour.vec4[3]); if (!(state->effects & (EF_RED|EF_GREEN|EF_BLUE))) VectorAdd(colour, r_brightlight_colour.vec4, colour); } if (state->effects & EF_DIMLIGHT) { radius = max(radius,r_dimlight_colour.vec4[3]); if (!(state->effects & (EF_RED|EF_GREEN|EF_BLUE))) VectorAdd(colour, r_dimlight_colour.vec4, colour); } if (state->effects & EF_BLUE) { radius = max(radius,r_bluelight_colour.vec4[3]); VectorAdd(colour, r_bluelight_colour.vec4, colour); } if (state->effects & EF_RED) { radius = max(radius,r_redlight_colour.vec4[3]); VectorAdd(colour, r_redlight_colour.vec4, colour); } if (state->effects & EF_GREEN) { radius = max(radius,r_greenlight_colour.vec4[3]); VectorAdd(colour, r_greenlight_colour.vec4, colour); } if (radius) { vec3_t org; VectorCopy(state->origin, org); //make the light appear at the predicted position rather than anywhere else. for (pnum = 0; pnum < cl.splitclients; pnum++) if (cl.playerview[pnum].playernum == j) VectorCopy(cl.playerview[pnum].simorg, org); if (model) { org[2] += model->mins[2]; org[2] += 32; } if (r_lightflicker.value) { pnum = realtime*20; if (flickertime != pnum) { flickertime = pnum; flicker = rand(); } radius += (flicker+j)&31; } CL_NewDlight(j+1, org, radius, 0.1, colour[0], colour[1], colour[2])->flags &= ~LFLAG_FLASHBLEND; } } if (state->modelindex < 1) continue; if (CL_FilterModelindex(state->modelindex, state->frame)) continue; /* if (!Cam_DrawPlayer(j)) continue; */ // grab an entity to fill in if (cl_numvisedicts == cl_maxvisedicts) break; // object list is full ent = &cl_visedicts[cl_numvisedicts]; cl_numvisedicts++; memset(ent, 0, sizeof(*ent)); ent->keynum = j+1; ent->model = model; ent->skinnum = state->skinnum; CL_LerpNetFrameState(&ent->framestate, &cl.lerpplayers[j]); // set colormap ent->playerindex = j; ent->topcolour = info->dtopcolor; ent->bottomcolour = info->dbottomcolor; #ifdef HEXEN2 ent->h2playerclass = info->h2playerclass; #endif #ifdef PEXT_SCALE ent->scale = state->scale; #endif ent->glowmod[0] = ent->glowmod[1] = ent->glowmod[2] = 1; ent->shaderRGBAf[0] = state->colourmod[0]/32.0f; ent->shaderRGBAf[1] = state->colourmod[1]/32.0f; ent->shaderRGBAf[2] = state->colourmod[2]/32.0f; ent->shaderRGBAf[3] = state->alpha/255.0f; if (state->alpha != 255) ent->flags |= RF_TRANSLUCENT; ent->fatness = state->fatness; // // angles // angles[PITCH] = -state->viewangles[PITCH]/3; angles[YAW] = state->viewangles[YAW]; angles[ROLL] = 0; angles[ROLL] = V_CalcRoll (angles, state->velocity)*4; if (j+1 == r_refdef.playerview->viewentity || (r_refdef.playerview->cam_state == CAM_EYECAM && r_refdef.playerview->cam_spec_track == j)) ent->flags |= RF_EXTERNALMODEL; // the player object gets added with flags | 2 for (pnum = 0; pnum < cl.splitclients; pnum++) { playerview_t *pv = &cl.playerview[pnum]; if (j == pv->playernum) { /* if (cl.spectator) { cl_numvisedicts--; continue; } */ angles[0] = -1*pv->viewangles[0] / 3; angles[1] = pv->viewangles[1]; angles[2] = pv->viewangles[2]; ent->origin[0] = pv->simorg[0]; ent->origin[1] = pv->simorg[1]; ent->origin[2] = pv->simorg[2]+pv->crouch; } } if (model && model->type == mod_alias) { angles[0]*=r_meshpitch.value; //carmack screwed up when he added alias models - they pitch the wrong way. angles[2]*=r_meshroll.value; //hexen2 screwed it up even more - they roll the wrong way. } VectorCopy(angles, ent->angles); AngleVectors(angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); // only predict half the move to minimize overruns msec = predictmsmult*(playertime - state->state_time); if (pnum < cl.splitclients) { //this is a local player } else if (cl.do_lerp_players) { lerpents_t *le = &cl.lerpplayers[j]; VectorCopy (le->origin, ent->origin); VectorCopy(le->angles, ent->angles); ent->angles[0] /= 3; AngleVectors(ent->angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); } else if (msec <= 0 || (!predictplayers)) { VectorCopy (state->origin, ent->origin); //Con_DPrintf ("nopredict\n"); } else { // predict players movement if (msec > 250) msec = 250; state->command.msec = msec; //Con_DPrintf ("predict: %i\n", msec); oldphysent = pmove.numphysent; CL_SetSolidPlayers (); CL_PredictUsercmd (0, j+1, state, &exact, &state->command); //uses player 0's maxspeed/grav... pmove.numphysent = oldphysent; VectorCopy (exact.origin, ent->origin); } VectorCopy(ent->origin, nametagorg[j]); nametagseen[j] = true; if (state->effects & QWEF_FLAG1) CL_AddFlagModels (ent, 0); else if (state->effects & QWEF_FLAG2) CL_AddFlagModels (ent, 1); #ifdef HAVE_LEGACY if (modelindex2) CL_AddVWeapModel (ent, cl.model_precache_vwep[modelindex2]); #endif CLQ1_AddShadow(ent); CLQ1_AddPowerupShell(ent, false, state->effects); if ((r_showbboxes.ival & 3) == 3) { vec3_t min, max; shader_t *s = R_RegisterShader("bboxshader", SUF_NONE, NULL); if (s) { VectorAdd(state->origin, pmove.player_mins, min); VectorAdd(state->origin, pmove.player_maxs, max); CLQ1_AddOrientedCube(s, min, max, NULL, 0.1, 0, 0, 1); VectorAdd(ent->origin, pmove.player_mins, min); VectorAdd(ent->origin, pmove.player_maxs, max); CLQ1_AddOrientedCube(s, min, max, NULL, 0, 0, 0.1, 1); } } if (r_torch.ival) { dlight_t *dl; dl = CL_NewDlight(j+1, ent->origin, 300, r_torch.ival, 0.5, 0.5, 0.2); dl->flags |= LFLAG_SHADOWMAP|LFLAG_FLASHBLEND; dl->fov = 60; angles[0] *= 3; angles[1] += sin(realtime)*8; angles[0] += cos(realtime*1.13)*5; AngleVectors(angles, dl->axis[0], dl->axis[1], dl->axis[2]); } } } void CL_LinkViewModel(void) { #ifdef QUAKESTATS extern cvar_t r_viewpreselgun; entity_t ent; unsigned int plnum; unsigned int playereffects; float alpha; playerview_t *pv = r_refdef.playerview; const char *preselectedmodelname; extern cvar_t cl_gunx, cl_guny, cl_gunz; extern cvar_t cl_gunanglex, cl_gunangley, cl_gunanglez; if (r_drawviewmodel.value <= 0 || !Cam_DrawViewModel(r_refdef.playerview)) return; #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) { //generate root matrix.. VectorCopy(pv->simorg, r_refdef.weaponmatrix[3]); AngleVectors(pv->simangles, r_refdef.weaponmatrix[0], r_refdef.weaponmatrix[1], r_refdef.weaponmatrix[2]); VectorInverse(r_refdef.weaponmatrix[1]); memcpy(r_refdef.weaponmatrix_bob, r_refdef.weaponmatrix, sizeof(r_refdef.weaponmatrix_bob)); V_ClearEntity(&ent); ent.model = pv->vm.oldmodel; ent.framestate.g[FS_REG].frame[0] = pv->vm.prevframe; ent.framestate.g[FS_REG].frame[1] = pv->vm.oldframe; ent.framestate.g[FS_REG].frametime[0] = cl.time-pv->vm.lerptime; ent.framestate.g[FS_REG].frametime[1] = cl.time-pv->vm.oldlerptime; ent.framestate.g[FS_REG].lerpweight[0] = (cl.time - pv->vm.lerptime)*10; ent.framestate.g[FS_REG].lerpweight[1] = 1 - ent.framestate.g[FS_REG].lerpweight[0]; ent.flags |= RF_WEAPONMODEL|RF_DEPTHHACK|RF_NOSHADOW; if (pv->handedness == 1) ent.flags |= RF_XFLIP; else if (pv->handedness == 2) return; ent.shaderRGBAf[0] = ent.shaderRGBAf[1] = ent.shaderRGBAf[2] = 1; ent.shaderRGBAf[3] = bound(0, r_drawviewmodel.value, 1); V_AddEntity (&ent); return; } #endif if (!r_drawentities.ival) return; if ((r_refdef.playerview->stats[STAT_ITEMS] & IT_INVISIBILITY) && r_drawviewmodelinvis.value <= 0) return; if (r_refdef.playerview->stats[STAT_HEALTH] <= 0) return; if (cl.intermissionmode != IM_NONE) return; if (pv->stats[STAT_WEAPONMODELI] <= 0 || pv->stats[STAT_WEAPONMODELI] >= MAX_PRECACHE_MODELS) return; if (r_drawviewmodel.value > 0 && r_drawviewmodel.value < 1) alpha = r_drawviewmodel.value; else alpha = 1; if ((pv->stats[STAT_ITEMS] & IT_INVISIBILITY) && r_drawviewmodelinvis.value > 0 && r_drawviewmodelinvis.value < 1) alpha *= r_drawviewmodelinvis.value; //FIXME: scale alpha by the player's alpha too if (alpha <= 0) return; V_ClearEntity(&ent); #ifdef PEXT_SCALE ent.scale = 1; #endif ent.origin[0] = cl_gunz.value; ent.origin[1] = -cl_gunx.value; ent.origin[2] = -cl_guny.value; ent.angles[0] = cl_gunanglex.value; ent.angles[1] = cl_gunangley.value; ent.angles[2] = cl_gunanglez.value; ent.glowmod[0] = ent.glowmod[1] = ent.glowmod[2] = 1; ent.shaderRGBAf[0] = ent.shaderRGBAf[1] = ent.shaderRGBAf[2] = 1; ent.shaderRGBAf[3] = alpha; if (alpha != 1) { ent.flags |= RF_TRANSLUCENT; } preselectedmodelname = r_viewpreselgun.ival?IN_GetPreselectedViewmodelName(pv-cl.playerview):NULL; if (preselectedmodelname) ent.model = Mod_ForName(preselectedmodelname, MLV_SILENT); else ent.model = NULL; if (!ent.model) ent.model = cl.model_precache[pv->stats[STAT_WEAPONMODELI]]; if (!ent.model) { pv->vm.oldmodel = NULL; return; } #ifdef HLCLIENT if (!CLHL_AnimateViewEntity(&ent)) #endif { //if the model changed, reset everything. if (ent.model != pv->vm.oldmodel) { pv->vm.oldmodel = ent.model; pv->vm.oldframe = pv->vm.prevframe = pv->stats[STAT_WEAPONFRAME]; pv->vm.oldlerptime = pv->vm.lerptime = cl.time; pv->vm.frameduration = 0.1; } //if the frame changed, update the oldframe to lerp into the new frame else if (pv->stats[STAT_WEAPONFRAME] != pv->vm.prevframe) { pv->vm.oldframe = pv->vm.prevframe; pv->vm.prevframe = pv->stats[STAT_WEAPONFRAME]; pv->vm.oldlerptime = pv->vm.lerptime; pv->vm.frameduration = (cl.time - pv->vm.lerptime); if (pv->vm.frameduration < 0.01)//no faster than 100 times a second... to avoid divide by zero pv->vm.frameduration = 0.01; if (pv->vm.frameduration > 0.2) //no slower than 5 times a second pv->vm.frameduration = 0.2; pv->vm.lerptime = cl.time; } //work out the blend fraction ent.framestate.g[FS_REG].frame[0] = pv->vm.prevframe; ent.framestate.g[FS_REG].frame[1] = pv->vm.oldframe; ent.framestate.g[FS_REG].frametime[0] = cl.time - pv->vm.lerptime; ent.framestate.g[FS_REG].frametime[1] = cl.time - pv->vm.oldlerptime; ent.framestate.g[FS_REG].lerpweight[0] = (cl.time-pv->vm.lerptime)/pv->vm.frameduration; ent.framestate.g[FS_REG].lerpweight[0] = bound(0, ent.framestate.g[FS_REG].lerpweight[0], 1); ent.framestate.g[FS_REG].lerpweight[1] = 1-ent.framestate.g[FS_REG].lerpweight[0]; } ent.flags |= RF_WEAPONMODEL|RF_DEPTHHACK|RF_NOSHADOW; plnum = -1; if (pv->spectator) plnum = Cam_TrackNum(pv); if (plnum == -1) plnum = r_refdef.playerview->playernum; playereffects = 0; if (r_refdef.playerview->nolocalplayer && plnum < cl.maxlerpents) { if (plnum+1 < cl.maxlerpents) { lerpents_t *le = &cl.lerpents[plnum+1]; if (le->entstate) { playereffects = le->entstate->effects; #ifdef HEXEN2 if (!le->entstate->modelindex || (le->entstate->hexen2flags & DRF_TRANSLUCENT)) { //urgh. ent.shaderRGBAf[3] *= .5; ent.flags |= RF_TRANSLUCENT; } #endif } } } else if (plnum < cl.allocated_client_slots) playereffects = cl.inframes[parsecountmod].playerstate[plnum].effects; if (playereffects & DPEF_NOGUNBOB) ent.flags |= RF_WEAPONMODELNOBOB; /* ent.topcolour = TOP_DEFAULT;//cl.players[plnum].ttopcolor; ent.bottomcolour = cl.players[plnum].tbottomcolor; ent.h2playerclass = cl.players[plnum].h2playerclass; */ CLQ1_AddPowerupShell(V_AddEntity(&ent), true, playereffects); //small hack to mask depth so only the front faces of the weaponmodel appear (no glitchy intra faces). if (alpha < 1 && qrenderer == QR_OPENGL) { ent.forcedshader = R_RegisterShader("viewmodeldepthmask", SUF_NONE, "{\n" "noshadows\n" "surfaceparm nodlight\n" "{\n" "map $whiteimage\n" "maskcolor\n" "depthwrite\n" "}\n" "}\n" ); ent.shaderRGBAf[3] = 1; ent.flags &= ~RF_TRANSLUCENT; V_AddEntity(&ent); ent.forcedshader = NULL; ent.shaderRGBAf[3] = alpha; ent.flags |= RF_TRANSLUCENT; } #endif } //====================================================================== /* =============== CL_SetSolid Builds all the pmove physents for the current frame =============== */ void CL_SetSolidEntities (void) { int i; inframe_t *frame; packet_entities_t *pak; entity_state_t *state; physent_t *pent; model_t *mod; VALGRIND_MAKE_MEM_UNDEFINED(&pmove, sizeof(pmove)); #ifdef CSQC_DAT pmove.world = &csqc_world; #endif memset(&pmove.physents[0], 0, sizeof(physent_t)); pmove.physents[0].model = cl.worldmodel; VectorClear (pmove.physents[0].origin); pmove.physents[0].info = 0; pmove.numphysent = 1; frame = &cl.inframes[cl.validsequence&UPDATE_MASK]; pak = &frame->packet_entities; for (i=0 ; inum_entities ; i++) { state = &pak->entities[i]; if (state->solidsize==ES_SOLID_NOT) continue; if (state->solidsize == ES_SOLID_BSP) { /*bsp model size*/ if (state->modelindex <= 0) continue; mod = cl.model_precache[state->modelindex]; if (!mod || mod->loadstate != MLS_LOADED) continue; /*vanilla protocols have no 'solid' information. all entities get assigned ES_SOLID_BSP, even if its not actually solid. so we need to make sure that item pickups are not erroneously considered solid, but doors etc are. normally, ONLY inline models are considered solid when we have no solid info. monsters will always be non-solid, too. */ if (!(cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) && mod->numsubmodels <= 1) continue; pent = &pmove.physents[pmove.numphysent]; memset(pent, 0, sizeof(physent_t)); pent->model = mod; VectorCopy (state->angles, pent->angles); pent->angles[0]*=r_meshpitch.value; pent->angles[2]*=r_meshroll.value; } else { pent = &pmove.physents[pmove.numphysent]; memset(pent, 0, sizeof(physent_t)); pent->info = state->number; /*don't bother with angles*/ COM_DecodeSize(state->solidsize, pent->mins, pent->maxs); } if (++pmove.numphysent == MAX_PHYSENTS) break; VectorCopy(state->origin, pent->origin); pent->info = state->number; switch((int)state->skinnum) { case 0: break; case Q1CONTENTS_LADDER: pent->nonsolid = true; pent->forcecontentsmask = FTECONTENTS_LADDER; break; case Q1CONTENTS_SKY: pent->nonsolid = true; pent->forcecontentsmask = FTECONTENTS_SKY; break; case Q1CONTENTS_LAVA: pent->nonsolid = true; pent->forcecontentsmask = FTECONTENTS_LAVA; break; case Q1CONTENTS_SLIME: pent->nonsolid = true; pent->forcecontentsmask = FTECONTENTS_SLIME; break; case Q1CONTENTS_WATER: pent->nonsolid = true; pent->forcecontentsmask = FTECONTENTS_WATER; break; } } } /* === Calculate the new position of players, without other player clipping We do this to set up real player prediction. Players are predicted twice, first without clipping other players, then with clipping against them. This sets up the first phase. === */ void CL_SetUpPlayerPrediction(qboolean dopred) { int j; player_state_t *state; player_state_t exact; double playertime; int msec; inframe_t *frame; struct predicted_player *pplayer; extern cvar_t cl_nopred, cl_demospeed; float predictmsmult = 1000*cl_predict_players_frac.value; int s; playertime = realtime - cls.latency*cl_predict_players_latency.value + cl_predict_players_nudge.value; if (playertime > realtime) playertime = realtime; if (cl_nopred.value || /*cls.demoplayback ||*/ cl.paused || cl.worldmodel->loadstate != MLS_LOADED) return; if (cls.demoplayback) predictmsmult *= cl_demospeed.value; frame = &cl.inframes[cl.parsecount&UPDATE_MASK]; for (j=0, pplayer = predicted_players, state=frame->playerstate; j < cl.allocated_client_slots; j++, pplayer++, state++) { pplayer->active = false; if (state->messagenum != cl.parsecount) continue; // not present this frame if (!state->modelindex) continue; pplayer->active = true; pplayer->flags = state->flags; // note that the local players are special, since they move locally // we use their last predicted postition for (s = 0; s < cl.splitclients; s++) { if (j == cl.playerview[s].playernum) { VectorCopy(cl.inframes[cls.netchan.outgoing_sequence&UPDATE_MASK].playerstate[cl.playerview[s].playernum].origin, pplayer->origin); break; } } if (s == cl.splitclients) { // only predict half the move to minimize overruns msec = predictmsmult*(playertime - state->state_time); if (msec <= 0 || !cl_predict_players.ival || !dopred) { VectorCopy (state->origin, pplayer->origin); //Con_DPrintf ("nopredict\n"); } else { // predict players movement if (msec > 250) msec = 250; state->command.msec = msec; //Con_DPrintf ("predict: %i\n", msec); CL_PredictUsercmd (0, j+1, state, &exact, &state->command); VectorCopy (exact.origin, pplayer->origin); } } } } /* =============== CL_SetSolid Builds all the pmove physents for the current frame Note that CL_SetUpPlayerPrediction() must be called first! pmove must be setup with world and solid entity hulls before calling (via CL_PredictMove) =============== */ void CL_SetSolidPlayers (void) { int j; struct predicted_player *pplayer; physent_t *pent; if (!cl_solid_players.ival) return; pent = pmove.physents + pmove.numphysent; if (pmove.numphysent == MAX_PHYSENTS) //too many. return; for (j=0, pplayer = predicted_players; j < cl.allocated_client_slots; j++, pplayer++) { if (!pplayer->active) continue; // not present this frame if (!(pplayer->flags & PF_SOLID)) continue; memset(pent, 0, sizeof(physent_t)); VectorCopy(pplayer->origin, pent->origin); pent->info = j+1; VectorCopy(pmove.player_mins, pent->mins); VectorCopy(pmove.player_maxs, pent->maxs); if (++pmove.numphysent == MAX_PHYSENTS) //we just hit 88 miles per hour. break; pent++; } } /* =============== CL_EmitEntities Builds the visedicts array for cl.time Made up of: clients, packet_entities, nails, and tents =============== */ void CL_ClearEntityLists(void) { cl_framecount++; if (cl_expandvisents || cl_numvisedicts+128 >= cl_maxvisedicts) { int newnum = cl_maxvisedicts + 256; entity_t *n = BZ_Realloc(cl_visedicts, newnum * sizeof(*n)); if (n) { cl_visedicts = n; cl_maxvisedicts = newnum; } cl_expandvisents = false; } cl_numvisedicts = 0; cl_numstrisidx = 0; cl_numstrisvert = 0; cl_numstris = 0; } void CL_FreeVisEdicts(void) { cl_framecount++; BZ_Free(cl_visedicts); cl_visedicts = NULL; cl_maxvisedicts = 0; cl_numvisedicts = 0; } /* static void CL_WaterSplashes(void) { int i; entity_t *ent; vec3_t org; static unsigned int ltime; unsigned int ntime = cl.time*1000; if (ntime - ltime < 200) return; ltime = ntime; for (i = 0; i < cl_numvisedicts; i++) { ent = &cl_visedicts[i]; if (ent->model) { if (ent->origin[2] + ent->model->mins[2] < r_refdef.waterheight && ent->origin[2] + ent->model->maxs[2] > r_refdef.waterheight) { org[0] = ent->origin[0]; org[1] = ent->origin[1]; org[2] = r_refdef.waterheight; P_RunParticleEffectTypeString(org, NULL, 1, "te_watertransition"); } } } } */ void CL_EmitEntities (void) { if (cls.state != ca_active) return; CL_DecayLights (); #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) { CL_ClearEntityLists(); CLQ2_AddEntities(); return; } #endif if (!cl.validsequence) return; CL_ClearEntityLists(); CL_LinkPlayers (); CL_LinkPacketEntities (); CL_LinkProjectiles (); CL_UpdateTEnts (); // CL_WaterSplashes(); } void CL_ClearPredict(void) { memset(predicted_players, 0, sizeof(predicted_players)); } ================================================ FILE: engine/client/cl_ignore.c ================================================ /* Copyright (C) 2001-2002 A Nourai 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 included (GNU.txt) 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. */ #include "quakedef.h" #include "cl_ignore.h" #include #define MAX_TEAMIGNORELIST 4 #define FLOODLIST_SIZE 10 int Player_IdtoSlot (int id) { int j; for (j = 0; j < cl.allocated_client_slots; j++) { if (cl.players[j].name[0] && cl.players[j].userid == id) return j; } return -1; } int Player_StringtoSlot(const char *arg) { int i, slot; for (i = 0; i < cl.allocated_client_slots; i++) { if (cl.players[i].name[0] && !strncmp(arg, cl.players[i].name, MAX_SCOREBOARDNAME - 1)) return i; } if (!arg[0]) return PLAYER_NAME_NOMATCH; for (i = 0; arg[i]; i++) { if (!isdigit(arg[i])) return PLAYER_NAME_NOMATCH; } return ((slot = Player_IdtoSlot(Q_atoi(arg))) >= 0) ? slot : PLAYER_ID_NOMATCH; } int Player_NametoSlot(const char *name) { int i; for (i = 0; i < cl.allocated_client_slots; i++) { if (cl.players[i].name[0] && !strncmp(cl.players[i].name, name, MAX_SCOREBOARDNAME - 1)) return i; } return PLAYER_NAME_NOMATCH; } int Player_SlottoId (int slot) { return (slot >= 0 && slot < cl.allocated_client_slots && cl.players[slot].name[0]) ? cl.players[slot].userid : -1; } char *Player_MyName (void) { return cl.players[cl.playerview[0].playernum].name; } cvar_t ignore_spec = CVARD("ignore_spec", "0", "0: Never ignore spectators.\n1: Ignore spectators only when playing.\n2: Always ignore spectators even when spectating."); cvar_t ignore_qizmo_spec = CVAR("ignore_qizmo_spec", "0"); cvar_t ignore_mode = CVAR("ignore_mode", "0"); cvar_t ignore_flood_duration = CVARD("ignore_flood_duration", "4", "Time limit for inbound messages to be considered duplicates."); cvar_t ignore_flood = CVARD("ignore_flood", "0", "Provides a way to reduce inbound spam from flooding out your chat (dupe messages are ignored).\n0: No inbound flood protection.\n1: Duplicate non-team messages will be filtered.\n2: ALL duplicate messages will be filtered"); cvar_t ignore_opponents = CVARD("ignore_opponents", "0", "0: Don't ignore chat from enemies.\n1: Always ignore chat from opponents (note: can also ignore f_ruleset checks).\n2: Ignore chat from opponents only during a match (requires servers that actually reports match state).\n"); char ignoreteamlist[MAX_TEAMIGNORELIST][16 + 1]; typedef struct flood_s { int playernum; char data[2048]; float time; } flood_t; static flood_t floodlist[FLOODLIST_SIZE]; static int floodindex; extern int PaddedPrint (char *s, int x); static qboolean IsIgnored(int slot) { return cl.players[slot].ignored; } static void Display_Ignorelist(void) { int i; int x; qboolean foundone; playerview_t *pv = &cl.playerview[0]; x = 0; foundone = false; for (i = 0; i < cl.allocated_client_slots; i++) { if (cl.players[i].name[0] && cl.players[i].ignored) { if (!foundone) { Con_Printf ("\x02" "User Ignore List:\n"); foundone++; } x = PaddedPrint(cl.players[i].name, x); } } if (!foundone) Con_Printf("\x02" "User Ignore List: empty\n"); else if (x) Con_Printf ("\n"); x = 0; foundone = false; for (i = 0; i < cl.allocated_client_slots; i++) { if (cl.players[i].name[0] && cl.players[i].ignored) { if (!foundone) { Con_Printf ("\x02" "User Mute List:\n"); foundone++; } x = PaddedPrint(cl.players[i].name, x); } } if (!foundone) Con_Printf("\x02" "User Mute List: empty\n"); else if (x) Con_Printf ("\n"); if (ignoreteamlist[0][0]) { x = 0; Con_Printf ("\x02" "Team Ignore List:\n"); for (i = 0; i < MAX_TEAMIGNORELIST && ignoreteamlist[i][0]; i++) x = PaddedPrint(ignoreteamlist[i], x); if (x) Con_Printf ("\n"); } if (ignore_opponents.ival) Con_Printf("\x02" "Opponents are Ignored\n"); if (ignore_spec.ival == 2 || (ignore_spec.ival == 1 && !pv->spectator)) Con_Printf ("\x02" "Spectators are Ignored\n"); if (ignore_qizmo_spec.ival) Con_Printf("\x02" "Qizmo spectators are Ignored\n"); Con_Printf("\n"); } static qboolean Ignorelist_Add(int slot) { if (IsIgnored(slot)) return false; cl.players[slot].ignored = true; cl.players[slot].vignored = true; S_Voip_Ignore(slot, true); return true; } static qboolean Ignorelist_VAdd(int slot) { if (cl.players[slot].vignored) return false; cl.players[slot].vignored = true; S_Voip_Ignore(slot, true); return true; } static qboolean Ignorelist_VDel(int slot) { if (!cl.players[slot].vignored) return false; cl.players[slot].vignored = false; S_Voip_Ignore(slot, false); return true; } static qboolean Ignorelist_Del(int slot) { if (cl.players[slot].ignored == false) return false; cl.players[slot].ignored = false; cl.players[slot].vignored = true; S_Voip_Ignore(slot, false); return true; } static void VIgnore_f(void) { int c, slot; if ((c = Cmd_Argc()) == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [userid | name]\n", Cmd_Argv(0)); return; } if ((slot = Player_StringtoSlot(Cmd_Argv(1))) == PLAYER_ID_NOMATCH) { Con_Printf("%s : no player with userid %d\n", Cmd_Argv(0), Q_atoi(Cmd_Argv(1))); } else if (slot == PLAYER_NAME_NOMATCH) { Con_Printf("%s : no player with name %s\n", Cmd_Argv(0), Cmd_Argv(1)); } else { if (Ignorelist_VAdd(slot)) Con_Printf("Added user %s to mute list\n", cl.players[slot].name); else Con_Printf ("User %s is already mute\n", cl.players[slot].name); } } static void VUnignore_f(void) { int c, slot; if ((c = Cmd_Argc()) == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [userid | name]\n", Cmd_Argv(0)); return; } if ((slot = Player_StringtoSlot(Cmd_Argv(1))) == PLAYER_ID_NOMATCH) { Con_Printf("%s : no player with userid %d\n", Cmd_Argv(0), Q_atoi(Cmd_Argv(1))); } else if (slot == PLAYER_NAME_NOMATCH) { Con_Printf("%s : no player with name %s\n", Cmd_Argv(0), Cmd_Argv(1)); } else { if (Ignorelist_VDel(slot)) Con_Printf("Removed user %s from mute list\n", cl.players[slot].name); else Con_Printf ("User %s already wasn't muted\n", cl.players[slot].name); } } static void Ignore_f(void) { int c, slot; if ((c = Cmd_Argc()) == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [userid | name]\n", Cmd_Argv(0)); return; } if ((slot = Player_StringtoSlot(Cmd_Argv(1))) == PLAYER_ID_NOMATCH) { Con_Printf("%s : no player with userid %d\n", Cmd_Argv(0), Q_atoi(Cmd_Argv(1))); } else if (slot == PLAYER_NAME_NOMATCH) { Con_Printf("%s : no player with name %s\n", Cmd_Argv(0), Cmd_Argv(1)); } else { if (Ignorelist_Add(slot)) Con_Printf("Added user %s to ignore list\n", cl.players[slot].name); else Con_Printf ("User %s is already ignored\n", cl.players[slot].name); } } static void IgnoreList_f(void) { if (Cmd_Argc() != 1) Con_Printf("%s : no arguments expected\n", Cmd_Argv(0)); else Display_Ignorelist(); } static void Ignore_ID_f(void) { int c, userid, i, slot; char *arg; if ((c = Cmd_Argc()) == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [userid]\n", Cmd_Argv(0)); return; } arg = Cmd_Argv(1); for (i = 0; arg[i]; i++) { if (!isdigit(arg[i])) { Con_Printf("Usage: %s [userid]\n", Cmd_Argv(0)); return; } } userid = Q_atoi(arg); if ((slot = Player_IdtoSlot(userid)) == PLAYER_ID_NOMATCH) { Con_Printf("%s : no player with userid %d\n", Cmd_Argv(0), userid); return; } if (Ignorelist_Add(slot)) Con_Printf("Added user %s to ignore list\n", cl.players[slot].name); else Con_Printf ("User %s is already ignored\n", cl.players[slot].name); } static void Unignore_f(void) { int c, slot; if ((c = Cmd_Argc()) == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [userid | name]\n", Cmd_Argv(0)); return; } if ((slot = Player_StringtoSlot(Cmd_Argv(1))) == PLAYER_ID_NOMATCH) { Con_Printf("%s : no player with userid %d\n", Cmd_Argv(0), Q_atoi(Cmd_Argv(1))); } else if (slot == PLAYER_NAME_NOMATCH) { Con_Printf("%s : no player with name %s\n", Cmd_Argv(0), Cmd_Argv(1)); } else { if (Ignorelist_Del(slot)) Con_Printf("Removed user %s from ignore list\n", cl.players[slot].name); else Con_Printf("User %s is not being ignored\n", cl.players[slot].name); } } static void Unignore_ID_f(void) { int c, i, userid, slot; char *arg; if ((c = Cmd_Argc()) == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [userid]\n", Cmd_Argv(0)); return; } arg = Cmd_Argv(1); for (i = 0; arg[i]; i++) { if (!isdigit(arg[i])) { Con_Printf("Usage: %s [userid]\n", Cmd_Argv(0)); return; } } userid = Q_atoi(arg); if ((slot = Player_IdtoSlot(userid)) == PLAYER_ID_NOMATCH) { Con_Printf("%s : no player with userid %d\n", Cmd_Argv(0), userid); return; } if (Ignorelist_Del(slot)) Con_Printf("Removed user %s from ignore list\n", cl.players[slot].name); else Con_Printf("User %s is not being ignored\n", cl.players[slot].name); } static void Ignoreteam_f(void) { int c, i, j; char *arg; c = Cmd_Argc(); if (c == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [team]\n", Cmd_Argv(0)); return; } arg = Cmd_Argv(1); for (i = 0; i < cl.allocated_client_slots; i++) { if (cl.players[i].name[0] && !cl.players[i].spectator && !strcmp(arg, cl.players[i].team)) { for (j = 0; j < MAX_TEAMIGNORELIST && ignoreteamlist[j][0]; j++) { if (!strncmp(arg, ignoreteamlist[j], sizeof(ignoreteamlist[j]) - 1)) { Con_Printf ("Team %s is already ignored\n", arg); return; } } if (j == MAX_TEAMIGNORELIST) Con_Printf("You cannot ignore more than %d teams\n", MAX_TEAMIGNORELIST); else { Q_strncpyz(ignoreteamlist[j], arg, sizeof(ignoreteamlist[j])); if (j + 1 < MAX_TEAMIGNORELIST) ignoreteamlist[j + 1][0] = 0; Con_Printf("Added team %s to ignore list\n", arg); } return; } } Con_Printf("%s : no team with name %s\n", Cmd_Argv(0), arg); } static void Unignoreteam_f(void) { int i, c, j; char *arg; c = Cmd_Argc(); if (c == 1) { Display_Ignorelist(); return; } else if (c != 2) { Con_Printf("Usage: %s [team]\n", Cmd_Argv(0)); return; } arg = Cmd_Argv(1); for (i = 0; i < MAX_TEAMIGNORELIST && ignoreteamlist[i][0]; i++) { if (!strncmp(arg, ignoreteamlist[i], sizeof(ignoreteamlist[i]) - 1)) { for (j = i; j < MAX_TEAMIGNORELIST && ignoreteamlist[j][0]; j++) ; if ( --j > i) Q_strncpyz(ignoreteamlist[i], ignoreteamlist[j], sizeof(ignoreteamlist[i])); ignoreteamlist[j][0] = 0; Con_Printf("Removed team %s from ignore list\n", arg); return; } } Con_Printf("Team %s is not being ignored\n", arg); } static void UnignoreAll_f (void) { int i; if (Cmd_Argc() != 1) { Con_Printf("%s : no arguments expected\n", Cmd_Argv(0)); return; } for (i = 0; i < cl.allocated_client_slots; i++) Ignorelist_Del(i); Con_Printf("User ignore list cleared\n"); } static void UnignoreteamAll_f (void) { if (Cmd_Argc() != 1) { Con_Printf("%s : no arguments expected\n", Cmd_Argv(0)); return; } ignoreteamlist[0][0] = 0; Con_Printf("Team ignore list cleared\n"); } char Ignore_Check_Flood(player_info_t *sender, const char *s, int flags) { int i; int slot; if ( !( ( (ignore_flood.value == 1 && ((flags & TPM_NORMAL) || (flags & TPM_SPECTATOR))) || (ignore_flood.value == 2 && flags != 0) ) ) ) { return NO_IGNORE_NO_ADD; } if (!sender) //don't ignore system messages. return NO_IGNORE_NO_ADD; slot = sender - cl.players; if (!cls.demoplayback && !strcmp(sender->name, Player_MyName())) { return NO_IGNORE_NO_ADD; } for (i = 0; i < FLOODLIST_SIZE; i++) { if (floodlist[i].playernum == slot && floodlist[i].data[0] && !strncmp(floodlist[i].data, s, sizeof(floodlist[i].data) - 1) && realtime - floodlist[i].time < ignore_flood_duration.value) { return IGNORE_NO_ADD; } } return NO_IGNORE_ADD; } void Ignore_Flood_Add(player_info_t *sender, const char *s) { floodlist[floodindex].playernum = sender - cl.players; floodlist[floodindex].data[0] = 0; Q_strncpyz(floodlist[floodindex].data, s, sizeof(floodlist[floodindex].data)); floodlist[floodindex].time = realtime; floodindex++; if (floodindex == FLOODLIST_SIZE) floodindex = 0; } qboolean Ignore_Message(const char *sendername, const char *s, int flags) { int slot, i; playerview_t *pv = &cl.playerview[0]; if (!ignore_mode.ival && (flags & 2)) return false; if (ignore_spec.ival == 2 && (flags == 4 || (flags == 8 && ignore_mode.ival))) return true; else if (ignore_spec.ival == 1 && (flags == 4) && !pv->spectator) return true; if (!sendername) return false; if ((slot = Player_NametoSlot(sendername)) == PLAYER_NAME_NOMATCH) return false; if (IsIgnored(slot)) return true; if (ignore_opponents.ival && ( (int) ignore_opponents.ival == 1 || (cls.state >= ca_connected && cl.matchstate == MATCH_INPROGRESS && !cls.demoplayback && !pv->spectator) // match? ) && flags == 1 && !pv->spectator && slot != pv->playernum && (!cl.teamplay || strcmp(cl.players[slot].team, cl.players[pv->playernum].team)) ) { return true; } if (!cl.teamplay) return false; if (cl.players[slot].spectator || !strcmp(Player_MyName(), sendername)) return false; for (i = 0; i < MAX_TEAMIGNORELIST && ignoreteamlist[i][0]; i++) { if (!strncmp(cl.players[slot].team, ignoreteamlist[i], sizeof(ignoreteamlist[i]) - 1)) return true; } return false; } void Ignore_ResetFloodList(void) { int i; for (i = 0; i < FLOODLIST_SIZE; i++) floodlist[i].data[0] = 0; floodindex = 0; } void Ignore_Init(void) { int i; #define IGNOREGROUP "Player Ignoring" for (i = 0; i < MAX_TEAMIGNORELIST; i++) ignoreteamlist[i][0] = 0; Ignore_ResetFloodList(); Cvar_Register (&ignore_flood_duration, IGNOREGROUP); Cvar_Register (&ignore_flood, IGNOREGROUP); Cvar_Register (&ignore_spec, IGNOREGROUP); Cvar_Register (&ignore_qizmo_spec, IGNOREGROUP); Cvar_Register (&ignore_mode, IGNOREGROUP); Cvar_Register (&ignore_opponents, IGNOREGROUP); Cmd_AddCommand ("cl_voip_mute", VIgnore_f); Cmd_AddCommand ("cl_voip_unmute", VUnignore_f); Cmd_AddCommand ("ignore", Ignore_f); Cmd_AddCommand ("ignorelist", IgnoreList_f); Cmd_AddCommand ("unignore", Unignore_f); Cmd_AddCommand ("ignore_team", Ignoreteam_f); Cmd_AddCommand ("unignore_team", Unignoreteam_f); Cmd_AddCommand ("unignoreAll", UnignoreAll_f); Cmd_AddCommand ("unignoreAll_team", UnignoreteamAll_f); Cmd_AddCommand ("unignore_id", Unignore_ID_f); Cmd_AddCommand ("ignore_id", Ignore_ID_f); } ================================================ FILE: engine/client/cl_ignore.h ================================================ /* Copyright (C) 2001-2002 A Nourai 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 included (GNU.txt) 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. */ #ifndef __IGNORE_H_ #define __IGNORE_H_ #define NO_IGNORE_NO_ADD 0 #define NO_IGNORE_ADD 1 #define IGNORE_NO_ADD 2 void Ignore_Init(void); qboolean Ignore_Message(const char *sendername, const char *s, int flags); char Ignore_Check_Flood(player_info_t *sender, const char *s, int flags); void Ignore_Flood_Add(player_info_t *sender, const char *s); void Ignore_ResetFloodList(void); #define PLAYER_ID_NOMATCH -1 #define PLAYER_NAME_NOMATCH -2 #define PLAYER_NUM_NOMATCH -3 int Player_StringtoSlot(const char *arg); #endif ================================================ FILE: engine/client/cl_input.c ================================================ /* Copyright (C) 1996-1997 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. */ // cl.input.c -- builds an intended movement command to send to the server #include "quakedef.h" #ifdef _WIN32 #include "winquake.h" //fps indep stuff. #endif float in_sensitivityscale = 1; #ifdef NQPROT static cvar_t cl_movement = CVARD("cl_movement","1", "Specifies whether to send movement sequence info over DPP7 protocols (other protocols are unaffected). Unlike cl_nopred, this can result in different serverside behaviour."); #endif cvar_t cl_nodelta = CVAR("cl_nodelta","0"); cvar_t cl_c2sdupe = CVARD("cl_c2sdupe", "0", "Send duplicate copies of packets to the server. This avoids extra latency caused by packetloss, but could also make the problem worse."); cvar_t cl_c2spps = CVARD("cl_c2spps", "0", "Reduces outgoing packet rates by dropping up to a third of outgoing packets."); cvar_t cl_c2sImpulseBackup = CVARD("cl_c2sImpulseBackup","3", "Prevents the cl_c2spps setting from dropping redundant packets that contain impulses, in an attempt to keep impulses more reliable."); static cvar_t cl_c2sMaxRedundancy = CVARD("cl_c2sMaxRedundancy","5", "This is the maximum number of input frames to send in each input packet. Values greater than 1 provide redundancy and avoid prediction misses, though you might find cl_c2sdupe provides equivelent result and at lower latency. It is locked at 3 for vanilla quakeworld, and locked at 1 for vanilla netquake."); cvar_t cl_netfps = CVARFD("cl_netfps", "150", CVAR_ARCHIVE, "Send up to this many packets to the server per second. The rate used is also limited by the server which usually forces a cap to this setting of 77. Low packet rates can result in extra extrapolation to try to hide the resulting latencies."); cvar_t cl_queueimpulses = CVARD("cl_queueimpulses", "0", "Queues unsent impulses instead of replacing them. This avoids the need for extra wait commands (and the timing issues of such commands), but potentially increases latency and can cause scripts to be desynced with regard to buttons and impulses."); cvar_t cl_smartjump = CVARD("cl_smartjump", "1", "Makes the jump button act as +moveup when in water. This is typically quieter and faster."); cvar_t cl_iDrive = CVARFD("cl_iDrive", "1", CVAR_SEMICHEAT, "Effectively releases movement keys when the opposing key is pressed. This avoids dead-time when both keys are pressed. This can be emulated with various scripts, but that's messy."); cvar_t cl_run = CVARD("cl_run", "0", "Enables autorun, inverting the state of the +speed key."); cvar_t cl_fastaccel = CVARD("cl_fastaccel", "1", "Begin moving at full speed instantly, instead of waiting a frame or so."); extern cvar_t cl_rollspeed; static cvar_t cl_sendchatstate = CVARD("cl_sendchatstate", "1", "Announce your chat state to the server in a privacy-violating kind of way. This allows other players to see your afk/at-console status."); cvar_t cl_prydoncursor = CVAR("cl_prydoncursor", ""); //for dp protocol cvar_t cl_instantrotate = CVARF("cl_instantrotate", "1", CVAR_SEMICHEAT); cvar_t in_xflip = CVAR("in_xflip", "0"); cvar_t in_vraim = CVARD("in_vraim", "1", "When set to 1, the 'view' angle sent to the server is controlled by your vr headset instead of separately. This is for fallback behaviour and blocks mouse+joy+gamepad aiming."); cvar_t prox_inmenu = CVAR("prox_inmenu", "0"); usercmd_t cl_pendingcmd[MAX_SPLITS]; /*kinda a hack...*/ unsigned int con_splitmodifier; cvar_t cl_forceseat = CVARAD("in_forceseat", "0", "in_forcesplitclient", "Overrides the device identifiers to control a specific client from any device. This can be used for debugging mods, where you only have one keyboard/mouse."); int CL_TargettedSplit(qboolean nowrap) { int mod; //explicitly targetted at some seat number from the server if (Cmd_ExecLevel >= RESTRICT_SERVER) return Cmd_ExecLevel - RESTRICT_SERVER; //locally executed command. if (nowrap) mod = MAX_SPLITS; else mod = cl.splitclients; if (mod < 1) return 0; if (con_splitmodifier > 0) return (con_splitmodifier - 1) % mod; else if (cl_forceseat.ival > 0) return (cl_forceseat.ival-1) % cl.splitclients; else return 0; } void CL_Split_f(void) { int tmp; char *c; c = Cmd_Argv(0); tmp = con_splitmodifier; if (*c == '+' || *c == '-') { con_splitmodifier = c[2]-'0'; Cmd_ExecuteString(va("%c%s", *c, Cmd_Args()), Cmd_ExecLevel); } else { con_splitmodifier = c[1]-'0'; Cmd_ExecuteString(Cmd_Args(), Cmd_ExecLevel); } con_splitmodifier = tmp; } void CL_SplitA_f(void) { int tmp; char *c, *args; c = Cmd_Argv(0); args = COM_Parse(Cmd_Args()); if (!args) return; while(*args == ' ' || *args == '\t') args++; tmp = con_splitmodifier; con_splitmodifier = atoi(com_token); if (*c == '+' || *c == '-') Cmd_ExecuteString(va("%c%s", *c, args), Cmd_ExecLevel); else Cmd_ExecuteString(args, Cmd_ExecLevel); con_splitmodifier = tmp; } /* =============================================================================== KEY BUTTONS Continuous button event tracking is complicated by the fact that two different input sources (say, mouse button 1 and the control key) can both press the same button, but the button should only be released when both of the pressing key have been released. When a key event issues a button command (+forward, +attack, etc), it appends its key number as a parameter to the command so it can be matched up with the release. state bit 0 is the current state of the key state bit 1 is edge triggered on the up to down transition state bit 2 is edge triggered on the down to up transition =============================================================================== */ kbutton_t in_mlook, in_strafe, in_speed; static kbutton_t in_klook; static kbutton_t in_left, in_right, in_forward, in_back; static kbutton_t in_lookup, in_lookdown, in_moveleft, in_moveright; static kbutton_t in_use, in_jump, in_attack; static kbutton_t in_rollleft, in_rollright, in_up, in_down; static kbutton_t in_button[19+1]; #define IN_IMPULSECACHE 32 static int in_impulse[MAX_SPLITS][IN_IMPULSECACHE]; static int in_nextimpulse[MAX_SPLITS]; static int in_impulsespending[MAX_SPLITS]; static void CL_QueueImpulse (int pnum, int newimp) { if (cl_queueimpulses.ival) { if (in_impulsespending[pnum]>=IN_IMPULSECACHE) { Con_Printf("Too many impulses, ignoring %i\n", newimp); return; } in_impulse[pnum][(in_nextimpulse[pnum]+in_impulsespending[pnum])%IN_IMPULSECACHE] = newimp; in_impulsespending[pnum]++; } else { if (in_impulsespending[pnum]) Con_DPrintf("Too many impulses, forgetting %i\n", in_impulse[pnum][(in_nextimpulse[pnum])%IN_IMPULSECACHE]); in_impulse[pnum][(in_nextimpulse[pnum])%IN_IMPULSECACHE] = newimp; in_impulsespending[pnum]=1; } } qboolean cursor_active; static qboolean KeyDown_Scan (kbutton_t *b, kbutton_t *anti, int k) { int pnum = CL_TargettedSplit(false); if (k == b->down[pnum][0] || k == b->down[pnum][1]) return false; // repeating key if (!b->down[pnum][0]) b->down[pnum][0] = k; else if (!b->down[pnum][1]) b->down[pnum][1] = k; else { Con_DPrintf ("Three keys down for a button!\n"); return false; } if (b->state[pnum] & 1) return false; // still down b->state[pnum] |= 1 + 2; // down + impulse down if (anti && (anti->state[pnum] & 1) && cl_iDrive.ival) { //anti-keys are the opposing key. so +forward can auto-release +back for slightly faster-responding keypresses. b->suppressed[pnum] = anti; anti->suppressed[pnum] = NULL; anti->state[pnum] &= ~1; // now up anti->state[pnum] |= 4; // impulse up } return true; } static void KeyDown (kbutton_t *b, kbutton_t *anti) { int k; char *c; c = Cmd_Argv(1); if (c[0]) k = atoi(c); else k = -1; // typed manually at the console for continuous down KeyDown_Scan(b, anti, k); } static qboolean KeyUp_Scan (kbutton_t *b, int k) { int pnum = CL_TargettedSplit(false); if (k < 0) { // typed manually at the console, assume for unsticking, so clear all b->suppressed[pnum] = NULL; b->down[pnum][0] = b->down[pnum][1] = 0; if (b->state[pnum] & ~4) { b->state[pnum] = 4; // impulse up return true; } return false; } if (b->down[pnum][0] == k) b->down[pnum][0] = 0; else if (b->down[pnum][1] == k) b->down[pnum][1] = 0; else return false; // key up without coresponding down (menu pass through) if (b->down[pnum][0] || b->down[pnum][1]) return false; // some other key is still holding it down if (!(b->state[pnum] & 1)) return false; // still up (this should not happen) b->state[pnum] &= ~1; // now up b->state[pnum] |= 4; // impulse up if (b->suppressed[pnum]) { if (b->suppressed[pnum]->down[pnum][0] || b->suppressed[pnum]->down[pnum][1]) b->suppressed[pnum]->state[pnum] |= 1 + 2; b->suppressed[pnum] = NULL; } return true; } static qboolean KeyUp (kbutton_t *b) { int k; char *c; c = Cmd_Argv(1); if (c[0]) k = atoi(c); else k = -1; return KeyUp_Scan(b, k); } #ifdef QUAKESTATS static cvar_t cl_weaponhide = CVARD("cl_weaponhide", "0", "HACK: Attempt to switch weapon to another in order to cheat your killer out of any possible weapon upgrades.\n0: original behaviour\n1: switch away when +attack/+fire is released\n2: switch away only in deathmatch\n"); static cvar_t cl_weaponhide_preference = CVARAD("cl_weaponhide_preference", "2 1", "cl_weaponhide_axe", "The weapon you would like to try to switch to when cl_weaponhide is active"); static cvar_t cl_weaponpreselect = CVARD("cl_weaponpreselect", "0", "HACK: Controls the interaction between the ^aweapon^a and ^a+attack^a commands (does not affect ^aimpulse^a).\n0: weapon switch happens instantly\n1: weapon switch happens on next attack\n2: instant only when already firing, otherwise delayed\n3: delay until new attack only in deathmatch 1\n4: delay until any attack only in deathmatch 1"); static cvar_t cl_weaponforgetorder = CVARD("cl_weaponforgetorder", "0", "The 'weapon' command will lock in its weapon choice, instead of choosing a different weapon between select+fire."); cvar_t r_viewpreselgun = CVARD("r_viewpreselgun", "0", "HACK: Display the preselected weaponmodel, instead of the current weaponmodel."); static int preselectedweapons[MAX_SPLITS]; static int preselectedweapon[MAX_SPLITS][32]; static kbutton_t in_wwheel; static float wwheeldir[MAX_SPLITS][2]; static size_t wwheelsel[MAX_SPLITS]; static double wwheelseltime[MAX_SPLITS]; static struct weaponinfo_s { char shortname[32]; //must not look like an impulse. int impulse; //primary key... int items_mask; int items_val; // int weapon_val; //unused... int ammostat; int ammomin; char *icons[3]; //unselected,selected,ammo char *viewmodel; } *weaponinfo; static size_t weaponinfo_count; static int IN_NameToWeaponIdx(const char *name) { size_t i; char *end; i = strtoul(name, &end, 10); if (i && !end) { //select by impulse when they specified something numeric. for (i = 0; i < weaponinfo_count; i++) { if (weaponinfo[i].impulse == i) return i; } } else { for (i = 0; i < weaponinfo_count; i++) { if (!strcmp(name, weaponinfo[i].shortname)) return i; } } return -1; } static int IN_RegisterWeapon(int impulse, const char *name, int items_mask, int items_val, int weapon_val, int ammostat, int ammomin, const char *viewmodel, const char *icon,const char *selicon, const char *ammoicon) { size_t i; char *end; if (impulse <= 0 || impulse > 255) return -1; //no. just no... if (strtol(name, &end, 10) != impulse) { if (!*end) return -1; //parsed as a number, which didn't match its impulse... don't break the impulse command. } //slots are unique/replaced by impulse for (i = 0; i < weaponinfo_count; i++) { if (weaponinfo[i].impulse == impulse) break; //replace... } if (i == weaponinfo_count) Z_ReallocElements((void**)&weaponinfo, &weaponinfo_count, i+1, sizeof(*weaponinfo)); weaponinfo[i].impulse = impulse; Q_strncpyz(weaponinfo[i].shortname, name, sizeof(weaponinfo[i].shortname)); weaponinfo[i].items_mask = items_mask; weaponinfo[i].items_val = items_val; // weaponinfo[i].weapon_val; //unused... weaponinfo[i].ammostat = ammostat; weaponinfo[i].ammomin = ammomin; #define Z_StrDupPtr2(v,s) do{Z_Free(*v),*(v) = (s&&*s)?strcpy(Z_Malloc(strlen(s)+1), s):NULL;}while(0) Z_StrDupPtr2(&weaponinfo[i].viewmodel, viewmodel); Z_StrDupPtr2(&weaponinfo[i].icons[0], icon); Z_StrDupPtr2(&weaponinfo[i].icons[1], selicon); Z_StrDupPtr2(&weaponinfo[i].icons[2], ammoicon); return i; } static void IN_RegisterWeapon_Clear(void) { while(weaponinfo_count) Z_Free(weaponinfo[--weaponinfo_count].viewmodel); Z_Free(weaponinfo); weaponinfo = NULL; } void IN_RegisterWeapon_Reset(void) { vfsfile_t *f; IN_RegisterWeapon_Clear(); f = FS_OpenVFS("wwheel.txt", "rb", FS_GAME); if (f) { //from the rerelease: /* slot N { impulse N icon "gfx/weapons/ww_foo_1.lmp" icon_sel "gfx/weapons/ww_foo_2.lmp" ammoicon "FOO" entvaroffs N weaponnum N } */ char line[1024]; char *v; for(;;) { if (!VFS_GETS(f, line, sizeof(line))) break; v = COM_Parse(line); if (!strcmp(com_token, "slot")) //slot N... just assume they're ordered. { //v = COM_Parse(line); //slot = com_token if (!VFS_GETS(f, line, sizeof(line))) break; v = COM_Parse(line); if (!strcmp(com_token, "{")) { int weaponbit=0; int impulse=0; int ammostat=-1; int ammocount=0; int field; char *icon = NULL; char *selicon = NULL; char *ammoicon = NULL; char *viewmodel = NULL; char *name = NULL; for (;;) { if (!VFS_GETS(f, line, sizeof(line))) break; v = COM_Parse(line); if (!strcmp(com_token, "}")) { //end of this weapon. IN_RegisterWeapon(impulse, name?name:va("%i", impulse), weaponbit,weaponbit, weaponbit, ammostat,ammocount, viewmodel, icon, selicon, ammoicon); break; } else if (!strcmp(com_token, "impulse")) { v = COM_Parse(v); impulse = atoi(com_token); } else if (!strcmp(com_token, "weaponnum")) { v = COM_Parse(v); weaponbit = atoi(com_token); } else if (!strcmp(com_token, "icon")) { v = COM_Parse(v); Z_StrDupPtr(&icon, com_token); } else if (!strcmp(com_token, "icon_sel")) { v = COM_Parse(v); Z_StrDupPtr(&selicon, com_token); } else if (!strcmp(com_token, "ammoicon")) { v = COM_Parse(v); if (strchr(com_token, '.') || strchr(com_token, '/')) Z_StrDupPtr(&ammoicon, com_token); else Z_StrDupPtr(&ammoicon, va("gfx/%s", com_token)); } else if (!strcmp(com_token, "entvaroffs")) { //this seems to be a host-only thing. the server can poke the qc's fields directly but other clients can't. //remap known indexes to stats - this can only work for nq's system fields. //note that rogue does some windowing thing so our client can only know either nails or lavanails, not both. either way those are NOT the normal stats and these weapons will appear to have infinite ammo as a result, sorry. //they really should have used field names here, not numbers, but hey, not my spec... use our 'ammostat' instead for fancy mods. v = COM_Parse(v); field = atoi(com_token); switch(field) { case 216: ammostat = STAT_SHELLS, ammocount = 1; break; case 220: ammostat = STAT_NAILS, ammocount = 1; break; case 224: ammostat = STAT_ROCKETS, ammocount = 1; break; case 228: ammostat = STAT_CELLS, ammocount = 1; break; default: ammostat = -1, ammocount = 0; break; } } else if (!strcmp(com_token, "ammostat")) { //non-qe v = COM_Parse(v); ammostat = atoi(com_token); if (ammocount <= 0) ammocount = 1; } else if (!strcmp(com_token, "ammomin")) { //non-qe v = COM_Parse(v); ammocount = atoi(com_token); } else if (!strcmp(com_token, "viewmodel")) { //non-qe, so preselect can show the correct model before its actually changed. v = COM_Parse(v); Z_StrDupPtr(&viewmodel, com_token); } else if (!strcmp(com_token, "shortname")) { //non-qe, for `+fire nq` v = COM_Parse(v); Z_StrDupPtr(&name, com_token); } else if (*com_token) Con_Printf("Unexpected line in wwheel.txt: %s\n", line); } Z_Free(icon); Z_Free(selicon); Z_Free(ammoicon); Z_Free(viewmodel); Z_Free(name); } else { Con_Printf("missing block, found: %s\n", line); break; } } else if (*com_token) Con_Printf("Unexpected line in wwheel.txt: %s\n", line); } VFS_CLOSE(f); } else { IN_RegisterWeapon(2, "sg", IT_SHOTGUN,IT_SHOTGUN, IT_SHOTGUN, STAT_SHELLS,1, "progs/v_shot.mdl", NULL,NULL, "gfx/sb_shells"); IN_RegisterWeapon(3, "ssg", IT_SUPER_SHOTGUN,IT_SUPER_SHOTGUN, IT_SUPER_SHOTGUN, STAT_SHELLS,1, "progs/v_shot2.mdl", NULL,NULL, "gfx/sb_shells"); IN_RegisterWeapon(4, "ng", IT_NAILGUN,IT_NAILGUN, IT_NAILGUN, STAT_NAILS,1, "progs/v_nail.mdl", NULL,NULL, "gfx/sb_nails"); IN_RegisterWeapon(5, "sng", IT_SUPER_NAILGUN,IT_SUPER_NAILGUN, IT_SUPER_NAILGUN, STAT_NAILS,1, "progs/v_nail2.mdl", NULL,NULL, "gfx/sb_nails"); IN_RegisterWeapon(6, "gl", IT_GRENADE_LAUNCHER,IT_GRENADE_LAUNCHER, IT_GRENADE_LAUNCHER, STAT_ROCKETS,1, "progs/v_rock.mdl", NULL,NULL, "gfx/sb_rocket"); IN_RegisterWeapon(7, "rl", IT_ROCKET_LAUNCHER,IT_ROCKET_LAUNCHER, IT_ROCKET_LAUNCHER, STAT_ROCKETS,1, "progs/v_rock2.mdl", NULL,NULL, "gfx/sb_rocket"); IN_RegisterWeapon(8, "lg", IT_LIGHTNING,IT_LIGHTNING, IT_LIGHTNING, STAT_CELLS,1, "progs/v_light.mdl", NULL,NULL, "gfx/sb_cells"); IN_RegisterWeapon(1, "axe", IT_AXE,IT_AXE, IT_AXE, -1,0, "progs/v_axe.mdl", NULL, NULL, NULL); } } static void IN_RegisterWeapon_f(void) { if (Cmd_Argc() <= 2) { const char *arg = Cmd_Argv(1); if (!strcmp(arg, "clear")) IN_RegisterWeapon_Clear(); else if (!strcmp(arg, "reset") || !strcmp(arg, "quake")) //'quake' for compat with dp. IN_RegisterWeapon_Reset(); else Con_Printf("Unknown arg %s\n", Cmd_Argv(1)); } else { IN_RegisterWeapon( atoi(Cmd_Argv(2)), //impulse Cmd_Argv(1), //name atoi(Cmd_Argv(3)), //itemsmask atoi(Cmd_Argv(3)), //itemsval atoi(Cmd_Argv(4)), //weaponval atoi(Cmd_Argv(5)), //ammostat atoi(Cmd_Argv(6)), //ammomin Cmd_Argv(7), //viewmodel Cmd_Argv(8), //weaponicon Cmd_Argv(9), //weaponicon_sel Cmd_Argv(10)); //ammoicon } } //hacks, because we have to guess what the mod is doing. we'll probably get it wrong, which sucks. static qboolean IN_HaveWeapon_Idx(int pnum, size_t widx) { if (widx < weaponinfo_count) if ((cl.playerview[pnum].stats[STAT_ITEMS]&weaponinfo[widx].items_mask) == weaponinfo[widx].items_val) //we have the weapon if (weaponinfo[widx].ammostat < 0 || cl.playerview[pnum].stats[weaponinfo[widx].ammostat] >= weaponinfo[widx].ammomin) //and we have enough ammo for it too. return true; return false; } static qboolean IN_HaveWeapon(int pnum, int impulse) { size_t widx; for (widx = 0; widx < weaponinfo_count; widx++) { if (weaponinfo[widx].impulse == impulse) return IN_HaveWeapon_Idx(pnum, widx); } return false; //we don't really know about it, but assume we can't because false negatives are better than false positives here. } static qboolean IN_HaveWeapon_Name(int pnum, char *name, int *impulse) { int widx = IN_NameToWeaponIdx(name); if (widx < 0) return false; if (!IN_HaveWeapon_Idx(pnum, widx)) return false; *impulse = weaponinfo[widx].impulse; return true; } static int IN_BestWeapon_Pre(unsigned int pnum); //if we're using weapon preselection, then we probably also want to show which weapon will be selected, instead of showing the shotgun the whole time. //this of course requires more hacks. const char *IN_GetPreselectedViewmodelName(unsigned int pnum) { if (r_viewpreselgun.ival && cl_weaponpreselect.ival && pnum < countof(preselectedweapons) && preselectedweapons[pnum]) { int best = IN_BestWeapon_Pre(pnum); size_t widx; for (widx = 0; widx < weaponinfo_count; widx++) { if (weaponinfo[widx].impulse == best) return weaponinfo[widx].viewmodel; } } return NULL; } static int IN_BestWeapon_Args(unsigned int pnum, int firstarg, int argcount) { //returns impulses. int i, imp; unsigned int best = 0; for (i = firstarg + argcount; --i >= firstarg; ) { if (IN_HaveWeapon_Name(pnum, Cmd_Argv(i), &imp)) best = imp; } return best; } static int IN_BestWeapon_Pre(unsigned int pnum) { int i, imp; unsigned int best = 0; for (i = preselectedweapons[pnum]; i-- > 0; ) { imp = preselectedweapon[pnum][i]; if (IN_HaveWeapon(pnum, imp)) best = imp; } return best; } static void IN_DoPostSelect(void) { int pnum = CL_TargettedSplit(false); if (cl_weaponpreselect.ival) { int best = IN_BestWeapon_Pre(pnum); if (best) CL_QueueImpulse(pnum, best); } if (in_wwheel.state[pnum]&1) { in_wwheel.state[pnum] = 0; pnum = CL_TargettedSplit(false); if (wwheelsel[pnum] < weaponinfo_count) CL_QueueImpulse(pnum, weaponinfo[wwheelsel[pnum]].impulse); } } //The weapon command autoselects a prioritised weapon like multi-arg impulse does. //however, it potentially makes the switch only on the next +attack. void IN_Weapon (void) { int newimp; int pnum = CL_TargettedSplit(false); int mode, best, i; preselectedweapons[pnum] = 0; for (i = 1; i < Cmd_Argc() && i <= countof(preselectedweapon[pnum]); i++) { best = IN_NameToWeaponIdx(Cmd_Argv(i)); if (best >= 0) best = weaponinfo[best].impulse; //a known weapon else best = atoi(Cmd_Argv(i)); //fall back if (best > 0) preselectedweapon[pnum][preselectedweapons[pnum]++] = best; } best = IN_BestWeapon_Pre(pnum); if (best) { newimp = best; if (cl_weaponforgetorder.ival) { //make sure the +attack sticks with the selected weapon. preselectedweapon[pnum][0] = best; preselectedweapons[pnum] = 1; } } else return; //no new weapon... mode = cl_weaponpreselect.ival; if (mode == 3) mode = (cl.deathmatch==1)?1:0; else if (mode == 4) mode = (cl.deathmatch==1)?2:0; if (mode == 1) return; //don't change yet. if (mode == 2 && !(in_attack.state[pnum]&3)) return; //2 changes instantly only when already firing. CL_QueueImpulse(pnum, newimp); } //+fire 8 7 [keycode] //does impulse 8 or 7 (according to held weapons) along with a +attack void IN_FireDown(void) { int pnum = CL_TargettedSplit(false); int k; int impulse; impulse = Cmd_Argc()-1; k = atoi(Cmd_Argv(impulse)); if (k >= 32) impulse--; //scancode, don't treat that arg as a weapon number else k = -1; impulse = IN_BestWeapon_Args(pnum, 1, impulse); if (impulse) CL_QueueImpulse(pnum, impulse); else IN_DoPostSelect(); KeyDown_Scan(&in_attack, NULL, k); } static void IN_DoWeaponHide(void) { if (cl_weaponhide.ival && !(cl_weaponhide.ival==2 && cl.deathmatch==1)) { int impulse, best = 0; int pnum = CL_TargettedSplit(false); char tok[64]; char *l = cl_weaponhide_preference.string; if (!strcmp(l, "0")) l = "2"; //for compat with ezquake's cl_weaponhide_axe cvar. while(l && *l) { l = COM_ParseOut(l, tok, sizeof(tok)); if (IN_HaveWeapon_Name(pnum, tok, &impulse)) best = impulse; } if (best) { //looks like we're switching away CL_QueueImpulse(pnum, best); } } } //-fire should trigger an impulse 1 or something. void IN_FireUp(void) { int k; int impulse; //any args are used in the +fire version and linger through to the -fire. //the only useful one is the keynum. impulse = Cmd_Argc()-1; k = atoi(Cmd_Argv(impulse)); if (k >= 32) impulse--; //scancode, don't treat that arg as a weapon number else k = -1; if (KeyUp_Scan(&in_attack, k)) IN_DoWeaponHide(); } qboolean IN_WeaponWheelIsShown(void) { if (!(in_wwheel.state[0]&1) || !weaponinfo_count) return false; return true; } qboolean IN_WeaponWheelAccumulate(int pnum, float x, float y, float threshhold) //either mouse or controller { if (!(in_wwheel.state[pnum]&1) || !weaponinfo_count) return false; if (x*x+y*y > threshhold*threshhold) //protects against deadzones. { wwheeldir[pnum][0] += x; wwheeldir[pnum][1] += y; } return true; } #include "shader.h" qboolean IN_DrawWeaponWheel(int pnum) { int w; float pos[2], centre[2]; const float radius = 64; float d, a; shader_t *s; if (!(in_wwheel.state[pnum]&1) || !weaponinfo_count) return false; R2D_ImageColours(1,1,1,1); centre[0] = cl.playerview[pnum].gamerect.x + cl.playerview[pnum].gamerect.width/2; centre[1] = cl.playerview[pnum].gamerect.y + cl.playerview[pnum].gamerect.height/2; d = DotProduct2(wwheeldir[pnum],wwheeldir[pnum]); a = 32; if (d > a*a && d) { wwheeldir[pnum][0] *= a/sqrt(d); wwheeldir[pnum][1] *= a/sqrt(d); } a = atan2(wwheeldir[pnum][1], wwheeldir[pnum][0]); w = (a/(2*M_PI)+1) * weaponinfo_count + 0.5; w = w % weaponinfo_count; if (w != wwheelsel[pnum]) { wwheelseltime[pnum] = realtime; wwheelsel[pnum] = w; } s = R2D_SafeCachePic("gfx/weaponwheel.lmp"); if (R_GetShaderSizes(s, NULL, NULL, false)>0) R2D_Image(centre[0]-radius*2, centre[1]-radius*2, radius*4, radius*4, 0, 0, 1, 1, s); for (w = 0; w < weaponinfo_count; w++) { pos[0] = centre[0] + cos((w*2*M_PI) / weaponinfo_count)*radius; pos[1] = centre[1] + sin((w*2*M_PI) / weaponinfo_count)*radius; if (weaponinfo[w].icons[0]) { //draw a shadow R2D_ImageColours(0,0,0,1); R2D_Image(pos[0]-24+2, pos[1]-16+2, 48, 32, 0, 0, 1, 1, R2D_SafeCachePic(weaponinfo[w].icons[0])); } //and the real icon (dark if unavailable) if (IN_HaveWeapon_Idx(pnum, w)) R2D_ImageColours(1,1,1,1); else R2D_ImageColours(0.2,0.2,0.2,1); if (w == wwheelsel[pnum]) d = 1+sin((realtime - wwheelseltime[pnum])*10); //make it bounce else d = 0; if (cl.playerview[pnum].stats[STAT_ACTIVEWEAPON] == weaponinfo[w].items_val && weaponinfo[w].icons[1]) R2D_Image(pos[0]-24-d, pos[1]-16-d, 48, 32, 0, 0, 1, 1, R2D_SafeCachePic(weaponinfo[w].icons[1])); else if (weaponinfo[w].icons[0]) R2D_Image(pos[0]-24-d, pos[1]-16-d, 48, 32, 0, 0, 1, 1, R2D_SafeCachePic(weaponinfo[w].icons[0])); else Draw_FunStringWidth(pos[0]-32-d, pos[1]-4-d, weaponinfo[w].shortname, 64, 2, cl.playerview[pnum].stats[STAT_ACTIVEWEAPON] == weaponinfo[w].items_val); } R2D_ImageColours(1,1,1,1); w = wwheelsel[pnum]; if (weaponinfo[w].icons[2]) R2D_Image(centre[0]-12, centre[1]-12, 24, 24, 0, 0, 1, 1, R2D_SafeCachePic(weaponinfo[w].icons[2])); Draw_FunStringWidth(centre[0]-32, centre[1] - 28, weaponinfo[w].shortname, 64, 2, false); if (weaponinfo[w].ammostat >= 0) Draw_FunStringWidth(centre[0]-32, centre[1] + 20, va("%s%d", (cl.playerview[pnum].stats[weaponinfo[w].ammostat]<20)?S_COLOR_RED:"", cl.playerview[pnum].stats[weaponinfo[w].ammostat]), 64, 2, false); pos[0] = centre[0] + cos(a)*radius*0.6; pos[1] = centre[1] + sin(a)*radius*0.6; Draw_FunString(pos[0], pos[1], "X"); return true; } void IN_WWheelDown (void) { int pnum = CL_TargettedSplit(false); #ifdef CSQC_DAT if (CSQC_ConsoleCommand(pnum, Cmd_Argv(0))) return; #endif if (!(in_wwheel.state[pnum]&1)) { size_t w; for (w = 0; w < weaponinfo_count; w++) { if (cl.playerview[pnum].stats[STAT_ACTIVEWEAPON] == weaponinfo[w].items_val) { //this is our active weapon. start with it highlighted. wwheelseltime[pnum] = realtime; wwheelsel[pnum] = w; wwheeldir[pnum][0] = cos((wwheelsel[pnum]*2*M_PI) / weaponinfo_count)*16; wwheeldir[pnum][1] = sin((wwheelsel[pnum]*2*M_PI) / weaponinfo_count)*16; break; } } } KeyDown(&in_wwheel, NULL); } void IN_WWheelUp (void) { int pnum = CL_TargettedSplit(false); #ifdef CSQC_DAT if (CSQC_ConsoleCommand(pnum, Cmd_Argv(0))) return; #endif if (!KeyUp(&in_wwheel)) return; if (wwheelsel[pnum] < weaponinfo_count) CL_QueueImpulse(pnum, weaponinfo[wwheelsel[pnum]].impulse); } void IN_IWheelDown (void) { } void IN_IWheelUp (void) { } #else #define IN_DoPostSelect() #define IN_DoWeaponHide() #endif //q2e compat. too lazy to use the wwheel info. let the gamecode do it the old way. void IN_WeapNext_f (void) { CL_SendClientCommand(true, "weapnext"); } void IN_WeapPrev_f (void) { CL_SendClientCommand(true, "weapprev"); } static void IN_KLookDown (void) {KeyDown(&in_klook, NULL);} static void IN_KLookUp (void) {KeyUp(&in_klook);} static void IN_MLookDown (void) {KeyDown(&in_mlook, NULL);} static void IN_MLookUp (void) { int pnum = CL_TargettedSplit(false); KeyUp(&in_mlook); if ( !(in_mlook.state[pnum]&1) && lookspring.ival) V_StartPitchDrift(&cl.playerview[pnum]); } static void IN_UpDown(void) {KeyDown(&in_up, &in_down);} static void IN_UpUp(void) {KeyUp(&in_up);} static void IN_DownDown(void) {KeyDown(&in_down, &in_up);} static void IN_DownUp(void) {KeyUp(&in_down);} static void IN_LeftDown(void) {KeyDown(&in_left, &in_right);} static void IN_LeftUp(void) {KeyUp(&in_left);} static void IN_RightDown(void) {KeyDown(&in_right, &in_left);} static void IN_RightUp(void) {KeyUp(&in_right);} static void IN_ForwardDown(void) {KeyDown(&in_forward, &in_back);} static void IN_ForwardUp(void) {KeyUp(&in_forward);} static void IN_BackDown(void) {KeyDown(&in_back, &in_forward);} static void IN_BackUp(void) {KeyUp(&in_back);} static void IN_LookupDown(void) {KeyDown(&in_lookup, &in_lookdown);} static void IN_LookupUp(void) {KeyUp(&in_lookup);} static void IN_LookdownDown(void) {KeyDown(&in_lookdown, &in_lookup);} static void IN_LookdownUp(void) {KeyUp(&in_lookdown);} static void IN_MoveleftDown(void) {KeyDown(&in_moveleft, &in_moveright);} static void IN_MoveleftUp(void) {KeyUp(&in_moveleft);} static void IN_MoverightDown(void) {KeyDown(&in_moveright, &in_moveleft);} static void IN_MoverightUp(void) {KeyUp(&in_moveright);} static void IN_RollLeftDown(void) {KeyDown(&in_rollleft, &in_rollright);} static void IN_RollLeftUp(void) {KeyUp(&in_rollleft);} static void IN_RollRightDown(void) {KeyDown(&in_rollright, &in_rollleft);} static void IN_RollRightUp(void) {KeyUp(&in_rollright);} static void IN_SpeedDown(void) {KeyDown(&in_speed, NULL);} static void IN_SpeedUp(void) {KeyUp(&in_speed);} static void IN_StrafeDown(void) {KeyDown(&in_strafe, NULL);} static void IN_StrafeUp(void) {KeyUp(&in_strafe);} static void IN_AttackDown(void) {IN_DoPostSelect(); KeyDown(&in_attack, NULL);} static void IN_AttackUp(void) {if (KeyUp(&in_attack)) IN_DoWeaponHide();} static void IN_UseDown (void) {KeyDown(&in_use, NULL);} static void IN_UseUp (void) {KeyUp(&in_use);} static void IN_JumpDown (void) { qboolean up; int pnum = CL_TargettedSplit(false); playerview_t *pv = &cl.playerview[pnum]; up = (cls.state == ca_active && cl_smartjump.ival && !prox_inmenu.ival); if (!up) up = false; #ifdef Q2CLIENT else if (cls.protocol == CP_QUAKE2) up = true; //always smartjump in q2. #endif else if (pv->spectator && pv->cam_state != CAM_FREECAM) up = false; //if we're tracking, don't confuse stuff. #ifdef QUAKESTATS else if (!pv->spectator && pv->stats[STAT_HEALTH] <= 0) up = false; //don't ever 'swim' when dead. else if (pv->pmovetype == PM_FLY || pv->pmovetype == PM_6DOF || pv->pmovetype == PM_SPECTATOR || pv->pmovetype == PM_OLD_SPECTATOR) up = true; //fling/spectating else if ((pv->pmovetype == PM_NORMAL || pv->pmovetype == PM_WALLWALK) && pv->waterlevel >= 2 && (!cl.teamfortress || !(in_forward.state[pnum] & 1))) up = true; //swimming. TF only (silently) smartjumps when NOT moving. #endif else up = false; KeyDown((up?&in_up:&in_jump), &in_down); } static void IN_JumpUp (void) { if (cl_smartjump.ival) KeyUp(&in_up); KeyUp(&in_jump); } static void IN_ButtonNDown(void) {KeyDown(&in_button[atoi(Cmd_Argv(0)+7)], NULL);} static void IN_ButtonNUp(void) {KeyUp(&in_button[atoi(Cmd_Argv(0)+7)]);} float in_rotate; static void IN_Rotate_f (void) {in_rotate += atoi(Cmd_Argv(1));} void IN_WriteButtons(vfsfile_t *f, qboolean all) { int s,b; struct { kbutton_t *button; char *name; } buttons [] = { {&in_mlook, "mlook"}, {&in_klook, "klook"}, {&in_left, "left"}, {&in_right, "right"}, {&in_forward, "forward"}, {&in_back, "back"}, {&in_lookup, "lookup"}, {&in_lookdown, "lookdown"}, {&in_moveleft, "moveleft"}, {&in_moveright, "moveright"}, {&in_strafe, "strafe"}, {&in_speed, "speed"}, {&in_use, "use"}, {&in_jump, "jump"}, {&in_attack, "attack"}, {&in_rollleft, "rollleft"}, {&in_rollright, "rollright"}, {&in_up, "up"}, {&in_down, "down"}, }; s = 0; VFS_PRINTF(f, "\n//Player 1 buttons\n"); for (b = 0; b < countof(buttons); b++) { if ((buttons[b].button->state[s]&1) && (buttons[b].button->down[s][0]==-1 || buttons[b].button->down[s][1]==-1)) VFS_PRINTF(f, "+%s\n", buttons[b].name); else if (b || all) VFS_PRINTF(f, "-%s\n", buttons[b].name); } for (b = 0; b < countof(in_button); b++) { if ((in_button[b].state[s]&1) && (in_button[b].down[s][0]==-1 || in_button[b].down[s][1]==-1)) VFS_PRINTF(f, "+button%i\n", b); else VFS_PRINTF(f, "-button%i\n", b); } for (s = 1; s < MAX_SPLITS; s++) { VFS_PRINTF(f, "\n//Player %i buttons\n", s); for (b = 0; b < countof(buttons); b++) { if ((buttons[b].button->state[s]&1) && (buttons[b].button->down[s][0]==-1 || buttons[b].button->down[s][1]==-1)) VFS_PRINTF(f, "+p%i %s\n", s, buttons[b].name); else if (b || all) VFS_PRINTF(f, "-p%i %s\n", s, buttons[b].name); } for (b = 0; b < countof(in_button); b++) { if ((in_button[b].state[s]&1) && (in_button[b].down[s][0]==-1 || in_button[b].down[s][1]==-1)) VFS_PRINTF(f, "+p%i button%i\n", s, b); else VFS_PRINTF(f, "-p%i button%i\n", s, b); } } //FIXME: save device remappings to config. } //This function incorporates Tonik's impulse 8 7 6 5 4 3 2 1 to select the prefered weapon on the basis of having it. //It also incorporates split screen input as well as impulse buffering void IN_Impulse (void) { int newimp; int pnum = CL_TargettedSplit(false); newimp = Q_atoi(Cmd_Argv(1)); #ifdef QUAKESTATS if (Cmd_Argc() > 2) { int best = IN_BestWeapon_Args(pnum, 1, Cmd_Argc() - 1); if (best) newimp = best; } #endif CL_QueueImpulse(pnum, newimp); } void IN_Restart (void) { IN_Shutdown(); IN_ReInit(); //FIXME: re-assert explicit device re-mappings } /* =============== CL_KeyState Returns 0.25 if a key was pressed and released during the frame, 0.5 if it was pressed and held 0 if held then released, and 1.0 if held for the entire time =============== */ float CL_KeyState (kbutton_t *key, int pnum, qboolean noslowstart) { float val; qboolean impulsedown, impulseup, down; noslowstart = noslowstart && cl_fastaccel.ival; impulsedown = key->state[pnum] & 2; impulseup = key->state[pnum] & 4; down = key->state[pnum] & 1; val = 0; if (impulsedown && !impulseup) { if (down) val = noslowstart?1.0:0.5; // pressed and held this frame else val = 0; // I_Error (); } if (impulseup && !impulsedown) { if (down) val = 0; // I_Error (); else val = 0; // released this frame } if (!impulsedown && !impulseup) { if (down) val = 1.0; // held the entire frame else val = 0; // up the entire frame } if (impulsedown && impulseup) { if (down) val = 0.75; // released and re-pressed this frame else val = 0.25; // pressed and released this frame } key->state[pnum] &= 1; // clear impulses return val; } void CL_ProxyMenuHook(char *command, kbutton_t *key) { if ((key->state[0] & 3) == 3) //2 is impulse down, 1 is held down { key->state[0] = 0; // clear impulses Cbuf_AddText(command, RESTRICT_DEFAULT); } } void CL_ProxyMenuHooks(void) { if (!prox_inmenu.ival) return; CL_ProxyMenuHook("say proxy:menu down\n", &in_back); CL_ProxyMenuHook("say proxy:menu up\n", &in_forward); CL_ProxyMenuHook("say proxy:menu left\n", &in_left); CL_ProxyMenuHook("say proxy:menu right\n", &in_right); CL_ProxyMenuHook("say proxy:menu left\n", &in_moveleft); CL_ProxyMenuHook("say proxy:menu right\n", &in_moveright); CL_ProxyMenuHook("say proxy:menu use\n", &in_jump); } //========================================================================== cvar_t cl_upspeed = CVARF("cl_upspeed","400", CVAR_ARCHIVE); cvar_t cl_forwardspeed = CVARF("cl_forwardspeed","400", CVAR_ARCHIVE); cvar_t cl_backspeed = CVARFD("cl_backspeed","", CVAR_ARCHIVE, "The base speed that you move backwards at. If empty, uses the value of cl_forwardspeed instead."); cvar_t cl_sidespeed = CVARF("cl_sidespeed","400", CVAR_ARCHIVE); cvar_t cl_movespeedkey = CVAR("cl_movespeedkey","2.0"); cvar_t cl_yawspeed = CVAR("cl_yawspeed","140"); cvar_t cl_pitchspeed = CVAR("cl_pitchspeed","150"); cvar_t cl_anglespeedkey = CVAR("cl_anglespeedkey","1.5"); #define GATHERBIT(bname,bit) do{if (bname.state[pnum] & 3) {bits |= (1u<<(bit));} bname.state[pnum] &= ~2;}while(0) #define UNUSEDBUTTON(bnum) do{if (in_button[bnum].state[pnum] & 3) {Con_Printf("+button%i is not supported on this protocol\n", bnum); } in_button[bnum].state[pnum] &= ~3;}while(0) void CL_GatherButtons (usercmd_t *cmd, int pnum) { unsigned int bits = 0; GATHERBIT(in_attack, 0); #ifdef Q3CLIENT if (cls.protocol==CP_QUAKE3) { //quake3's buttons are nice and simple, buttonN -> bit|=(1<buttons = bits; return; } #endif #ifdef Q2CLIENT if (cls.protocol==CP_QUAKE2 && cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) { //buttons limited to 8 bits. //GATHERBIT(in_attack, 0); //handled above GATHERBIT(in_use, 1); //GATHERBIT(in_holster, 2); //urgh //GATHERBIT(in_jump, 3); //also set by +moveup //GATHERBIT(in_crouch, 4); //also set by +movedown //GATHERBIT(in_unused, 5); //GATHERBIT(in_unused, 6); //GATHERBIT(in_any, 7); //urgh //also let +button stuff map to bits. GATHERBIT(in_button[0], 0); GATHERBIT(in_button[1], 1); GATHERBIT(in_button[2], 2); GATHERBIT(in_button[3], 3); GATHERBIT(in_button[4], 4); GATHERBIT(in_button[5], 5); GATHERBIT(in_button[6], 6); GATHERBIT(in_button[7], 7); UNUSEDBUTTON(8); UNUSEDBUTTON(9); UNUSEDBUTTON(10); UNUSEDBUTTON(11); UNUSEDBUTTON(12); UNUSEDBUTTON(13); UNUSEDBUTTON(14); UNUSEDBUTTON(15); UNUSEDBUTTON(16); UNUSEDBUTTON(17); UNUSEDBUTTON(18); UNUSEDBUTTON(19); // UNUSEDBUTTON(20); cmd->buttons |= bits; return; } #endif //quakec's numbered buttons make no sense and have no sane relation to bit numbers GATHERBIT(in_button[0], 0); UNUSEDBUTTON(1); //officially, qc's button1 field is unusable (although qw folds button3 over to it) GATHERBIT(in_button[2], 1); GATHERBIT(in_jump, 1); GATHERBIT(in_button[3], 2); GATHERBIT(in_button[4], 3); GATHERBIT(in_button[5], 4); GATHERBIT(in_button[6], 5); GATHERBIT(in_button[7], 6); GATHERBIT(in_button[8], 7); //more inconsistencies, as required for dpcompat. GATHERBIT(in_use, (cls.protocol==CP_QUAKEWORLD)?4:8); bits |= (Key_Dest_Has(~kdm_game)) ?(1u<<9):0; //'buttonchat'. game is the lowest priority, anything else will take focus away. we consider that to mean 'chat' (although it could be menus). bits |= (cursor_active) ?(1u<<10):0; //'cursor_active'. prydon cursor stuff. GATHERBIT(in_button[9], 11); GATHERBIT(in_button[10], 12); GATHERBIT(in_button[11], 13); GATHERBIT(in_button[12], 14); GATHERBIT(in_button[13], 15); GATHERBIT(in_button[14], 16); GATHERBIT(in_button[15], 17); GATHERBIT(in_button[16], 18); UNUSEDBUTTON(17); UNUSEDBUTTON(18); UNUSEDBUTTON(19); // UNUSEDBUTTON(20); //NQ protocol: //bit 30 means input_weapon field is sent. figured out at time of sending. //bit 31 means input_cursor* fields are sent. figured out at time of sending. cmd->buttons |= bits; } void CL_ClearPendingCommands(void) { size_t seat, i; memset(&cl_pendingcmd, 0, sizeof(cl_pendingcmd)); for (seat = 0; seat < countof(cl_pendingcmd); seat++) { for (i=0 ; i<3 ; i++) cl_pendingcmd[seat].angles[i] = ((int)(cl.playerview[seat].viewangles[i]*65536.0/360)&65535); } } /* ================ CL_AdjustAngles Moves the local angle positions ================ */ void CL_AdjustAngles (int pnum, double frametime) { float speed, quant; float up, down; if (in_speed.state[pnum] & 1) { if (ruleset_allow_frj.ival) speed = cl_anglespeedkey.value; else speed = bound(-2, cl_anglespeedkey.value, 2); } else speed = 1; if (in_rotate && pnum==0 && !(cl.fpd & FPD_LIMIT_YAW)) { quant = in_rotate; if (!cl_instantrotate.ival) quant *= speed*frametime; in_rotate -= quant; if (r_xflip.ival) quant *= -1; if (ruleset_allow_frj.ival) cl.playerview[pnum].viewanglechange[YAW] += quant; } if (!(in_strafe.state[pnum] & 1)) { quant = cl_yawspeed.value*speed; if ((cl.fpd & FPD_LIMIT_YAW) || !ruleset_allow_frj.ival) quant = bound(-900, quant, 900); quant *= frametime; if (r_xflip.ival) quant *= -1; cl.playerview[pnum].viewanglechange[YAW] -= quant * CL_KeyState (&in_right, pnum, false); cl.playerview[pnum].viewanglechange[YAW] += quant * CL_KeyState (&in_left, pnum, false); } if (in_klook.state[pnum] & 1) { V_StopPitchDrift (&cl.playerview[pnum]); quant = cl_pitchspeed.value*speed; if ((cl.fpd & FPD_LIMIT_PITCH) || !ruleset_allow_frj.ival) quant = bound(-700, quant, 700); quant *= frametime; cl.playerview[pnum].viewanglechange[PITCH] -= quant * CL_KeyState (&in_forward, pnum, false); cl.playerview[pnum].viewanglechange[PITCH] += quant * CL_KeyState (&in_back, pnum, false); } quant = cl_rollspeed.value*speed; quant *= frametime; cl.playerview[pnum].viewanglechange[ROLL] -= quant * CL_KeyState (&in_rollleft, pnum, false); cl.playerview[pnum].viewanglechange[ROLL] += quant * CL_KeyState (&in_rollright, pnum, false); up = CL_KeyState (&in_lookup, pnum, false); down = CL_KeyState(&in_lookdown, pnum, false); quant = cl_pitchspeed.value*speed; if ((cl.fpd & FPD_LIMIT_PITCH) || !ruleset_allow_frj.ival) quant = bound(-700, quant, 700); quant *= frametime; cl.playerview[pnum].viewanglechange[PITCH] -= quant * up; cl.playerview[pnum].viewanglechange[PITCH] += quant * down; if (up || down) V_StopPitchDrift (&cl.playerview[pnum]); } /* ================ CL_BaseMove Send the intended movement message to the server ================ */ static void CL_BaseMove (vec3_t moves, int pnum) { float fwdspeed = cl_forwardspeed.value; float sidespeed = cl_sidespeed.value; float backspeed = (*cl_backspeed.string?cl_backspeed.value:cl_forwardspeed.value); float upspeed = (*cl_backspeed.string?cl_backspeed.value:cl_forwardspeed.value); float scale = 1; // // adjust for speed key // #ifdef HEXEN2 extern qboolean sbar_hexen2; if (sbar_hexen2) { //hexen2 is a bit different. forwardspeed is treated as something of a boolean and we need to be able to cope with the boots-of-speed without forcing it always. not really sure why that's clientside instead of serverside, but oh well. evilness. scale = cl.playerview[pnum].statsf[STAT_H2_HASTED]; if (!scale) scale = 1; if (((in_speed.state[pnum] & 1) ^ (cl_run.ival || fwdspeed > 200)) && scale <= 1) //don't go super fast with speed boots. scale *= cl_movespeedkey.value; fwdspeed = backspeed = 200; sidespeed = 225; } else #endif if ((in_speed.state[pnum] & 1) ^ cl_run.ival) scale *= cl_movespeedkey.value; if (r_xflip.ival) sidespeed *= -1; moves[0] = 0; if (! (in_klook.state[pnum] & 1) ) { moves[0] += (fwdspeed * CL_KeyState (&in_forward, pnum, true) - backspeed * CL_KeyState (&in_back, pnum, true)); } moves[1] = sidespeed * (CL_KeyState (&in_moveright, pnum, true) - CL_KeyState (&in_moveleft, pnum, true)) * (in_xflip.ival?-1:1); if (in_strafe.state[pnum] & 1) moves[1] += sidespeed * (CL_KeyState (&in_right, pnum, true) - CL_KeyState (&in_left, pnum, true)) * (in_xflip.ival?-1:1); moves[2] = upspeed * (CL_KeyState (&in_up, pnum, true) - CL_KeyState (&in_down, pnum, true)); moves[0] *= scale; moves[1] *= scale; moves[2] *= scale; } void CL_ClampPitch (int pnum, float frametime) { float mat[16]; float roll; playerview_t *pv = &cl.playerview[pnum]; if (cl.intermissionmode != IM_NONE) { memset(pv->viewanglechange, 0, sizeof(pv->viewanglechange)); return; } if (pv->pmovetype == PM_6DOF) { // vec3_t impact; // vec3_t norm; float mat2[16]; // vec3_t cross; vec3_t view[4]; // float dot; AngleVectors(pv->viewangles, view[0], view[1], view[2]); Matrix4x4_RM_FromVectors(mat, view[0], view[1], view[2], vec3_origin); Matrix4_Multiply(Matrix4x4_CM_NewRotation(-pv->viewanglechange[PITCH], 0, 1, 0), mat, mat2); Matrix4_Multiply(Matrix4x4_CM_NewRotation(pv->viewanglechange[YAW], 0, 0, 1), mat2, mat); #if 1 //roll angles Matrix4_Multiply(Matrix4x4_CM_NewRotation(pv->viewanglechange[ROLL], 1, 0, 0), mat, mat2); #else //auto-roll Matrix3x4_RM_ToVectors(mat, view[0], view[1], view[2], view[3]); VectorMA(pv->simorg, -48, view[2], view[3]); if (!TraceLineN(pv->simorg, view[3], impact, norm)) { norm[0] = 0; norm[1] = 0; norm[2] = 1; } /*keep the roll relative to the 'ground'*/ CrossProduct(norm, view[2], cross); dot = DotProduct(view[0], cross); roll = timestep * 360 * -(dot); Matrix4_Multiply(Matrix4x4_CM_NewRotation(roll, 1, 0, 0), mat, mat2); #endif Matrix3x4_RM_ToVectors(mat2, view[0], view[1], view[2], view[3]); VectorAngles(view[0], view[2], pv->viewangles, false); VectorClear(pv->viewanglechange); //fixme: in_vraim stuff VectorCopy(pv->viewangles, pv->aimangles); return; } #if 1 if ((pv->gravitydir[2] != -1 || pv->viewangles[2])) { float surfm[16], invsurfm[16]; float viewm[16]; vec3_t view[4]; vec3_t surf[3]; vec3_t vang; void PerpendicularVector( vec3_t dst, const vec3_t src ); /*calc current view matrix relative to the surface*/ AngleVectors(pv->viewangles, view[0], view[1], view[2]); VectorNegate(view[1], view[1]); /*calculate the surface axis with up from the pmove code and right/forwards relative to the player's directions*/ if (!pv->gravitydir[0] && !pv->gravitydir[1] && !pv->gravitydir[2]) { VectorSet(surf[2], 0, 0, 1); } else { VectorNegate(pv->gravitydir, surf[2]); } VectorNormalize(surf[2]); PerpendicularVector(surf[1], surf[2]); VectorNormalize(surf[1]); CrossProduct(surf[2], surf[1], surf[0]); VectorNegate(surf[0], surf[0]); VectorNormalize(surf[0]); Matrix4x4_RM_FromVectors(surfm, surf[0], surf[1], surf[2], vec3_origin); Matrix3x4_InvertTo4x4_Simple(surfm, invsurfm); /*calc current view matrix relative to the surface*/ Matrix4x4_RM_FromVectors(viewm, view[0], view[1], view[2], vec3_origin); Matrix4_Multiply(viewm, invsurfm, mat); /*convert that back to angles*/ Matrix3x4_RM_ToVectors(mat, view[0], view[1], view[2], view[3]); VectorAngles(view[0], view[2], vang, false); /*edit it*/ vang[PITCH] += pv->viewanglechange[PITCH]; vang[YAW] += pv->viewanglechange[YAW]; if (vang[PITCH] <= -180) vang[PITCH] += 360; if (vang[PITCH] > 180) vang[PITCH] -= 360; if (vang[ROLL] >= 180) vang[ROLL] -= 360; if (vang[ROLL] < -180) vang[ROLL] += 360; /*keep the player looking relative to their ground (smoothlyish)*/ if (!vang[ROLL]) { if (!pv->viewanglechange[PITCH] && !pv->viewanglechange[YAW] && !pv->viewanglechange[ROLL]) { VectorCopy(pv->viewangles, pv->aimangles); return; } } else { if (fabs(vang[ROLL]) < frametime*180) vang[ROLL] = 0; else if (vang[ROLL] > 0) { // Con_Printf("Roll %f\n", vang[ROLL]); vang[ROLL] -= frametime*180; } else { // Con_Printf("Roll %f\n", vang[ROLL]); vang[ROLL] += frametime*180; } } VectorClear(pv->viewanglechange); /*clamp pitch*/ if (vang[PITCH] > cl.maxpitch) vang[PITCH] = cl.maxpitch; if (vang[PITCH] < cl.minpitch) vang[PITCH] = cl.minpitch; /*turn those angles back to a matrix*/ AngleVectors(vang, view[0], view[1], view[2]); VectorNegate(view[1], view[1]); Matrix4x4_RM_FromVectors(mat, view[0], view[1], view[2], vec3_origin); /*rotate back into world space*/ Matrix4_Multiply(mat, surfm, viewm); /*and figure out the final result*/ Matrix3x4_RM_ToVectors(viewm, view[0], view[1], view[2], view[3]); VectorAngles(view[0], view[2], cl.playerview[pnum].viewangles, false); if (pv->viewangles[ROLL] >= 360) pv->viewangles[ROLL] -= 360; if (pv->viewangles[ROLL] < 0) pv->viewangles[ROLL] += 360; if (pv->viewangles[PITCH] < -180) pv->viewangles[PITCH] += 360; //fixme: in_vraim stuff VectorCopy(pv->viewangles, pv->aimangles); return; } #endif pv->viewangles[PITCH] += pv->viewanglechange[PITCH]; pv->viewangles[YAW] += pv->viewanglechange[YAW]; pv->viewangles[ROLL] += pv->viewanglechange[ROLL]; pv->viewangles[YAW] /= 360; pv->viewangles[YAW] = pv->viewangles[YAW] - (int)pv->viewangles[YAW]; pv->viewangles[YAW] *= 360; VectorClear(pv->viewanglechange); if (in_vraim.ival && (pv->vrdev[VRDEV_HEAD].status&VRSTATUS_ANG)) { //overcomplicated code to replace the pitch+roll angles and add to the yaw angle. #if 0 matrix3x4 base, head, res; vec3_t na = {0, pv->viewangles[YAW], 0}; vec3_t f,l,u,o; Matrix3x4_RM_FromAngles(na, vec3_origin, base[0]); for (i=0 ; i<3 ; i++) na[i] = SHORT2ANGLE(pv->vrdev[VRDEV_HEAD].angles[i]); Matrix3x4_RM_FromAngles(na, pv->vrdev[VRDEV_HEAD].origin, head[0]); Matrix3x4_Multiply(head[0], base[0], res[0]); Matrix3x4_RM_ToVectors(res[0], f,l,u,o); VectorAngles(f,u,pv->aimangles,false); for (i=0 ; i<3 ; i++) cmd->angles[i] = ANGLE2SHORT(na[i]); #else pv->aimangles[PITCH] = SHORT2ANGLE(pv->vrdev[VRDEV_HEAD].angles[PITCH]); pv->aimangles[YAW] = SHORT2ANGLE(pv->vrdev[VRDEV_HEAD].angles[YAW]) + pv->viewangles[YAW]; pv->aimangles[ROLL] = SHORT2ANGLE(pv->vrdev[VRDEV_HEAD].angles[ROLL]); #endif } else VectorCopy(pv->viewangles, pv->aimangles); #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) { float pitch; pitch = SHORT2ANGLE(cl.q2frame.seat[pnum].playerstate.pmove.delta_angles[PITCH]); if (pitch > 180) pitch -= 360; if (pv->viewangles[PITCH] + pitch < -360) pv->viewangles[PITCH] += 360; // wrapped if (pv->viewangles[PITCH] + pitch > 360) pv->viewangles[PITCH] -= 360; // wrapped if (pv->viewangles[PITCH] + pitch > cl.maxpitch) pv->viewangles[PITCH] = cl.maxpitch - pitch; if (pv->viewangles[PITCH] + pitch < cl.minpitch) pv->viewangles[PITCH] = cl.minpitch - pitch; } else #endif #ifdef Q3CLIENT if (cls.protocol == CP_QUAKE3) //q3 expects the cgame to do it { //no-op } else #endif { if (pv->viewangles[PITCH] > cl.maxpitch) pv->viewangles[PITCH] = cl.maxpitch; if (pv->viewangles[PITCH] < cl.minpitch) pv->viewangles[PITCH] = cl.minpitch; if (pv->aimangles[PITCH] > cl.maxpitch) pv->aimangles[PITCH] = cl.maxpitch; if (pv->aimangles[PITCH] < cl.minpitch) pv->aimangles[PITCH] = cl.minpitch; } // if (cl.viewangles[pnum][ROLL] > 50) // cl.viewangles[pnum][ROLL] = 50; // if (cl.viewangles[pnum][ROLL] < -50) // cl.viewangles[pnum][ROLL] = -50; roll = frametime*pv->viewangles[ROLL]*30; if ((pv->viewangles[ROLL]-roll < 0) != (pv->viewangles[ROLL]<0)) pv->viewangles[ROLL] = 0; else pv->viewangles[ROLL] -= frametime*pv->viewangles[ROLL]*3; } /* ============== CL_FinishMove ============== */ static void CL_FinishMove (usercmd_t *cmd, int pnum) { int i; CL_ClampPitch(pnum, 0); // // always dump the first two message, because it may contain leftover inputs // from the last level // if (cl.movesequence <= 2) { cmd->buttons = 0; return; } // // figure button bits // CL_GatherButtons(cmd, pnum); for (i=0 ; i<3 ; i++) cmd->angles[i] = (int)(ANGLE2SHORT(cl.playerview[pnum].aimangles[i]))&65535; cmd->vr[VRDEV_LEFT] = cl.playerview[pnum].vrdev[VRDEV_LEFT]; cmd->vr[VRDEV_RIGHT] = cl.playerview[pnum].vrdev[VRDEV_RIGHT]; cmd->vr[VRDEV_HEAD] = cl.playerview[pnum].vrdev[VRDEV_HEAD]; if (in_impulsespending[pnum] && !cl.paused) { cmd->impulse = in_impulse[pnum][(in_nextimpulse[pnum])%IN_IMPULSECACHE]; in_nextimpulse[pnum]++; in_impulsespending[pnum]--; } else cmd->impulse = 0; } static void CL_AccumlateInput(int plnum, float frametime/*extra contribution*/, float framemsecs/*total accumulated*/) { usercmd_t *cmd = &cl_pendingcmd[plnum]; int i; static vec3_t mousemovements[MAX_SPLITS]; vec3_t newmoves; float nscale = framemsecs?framemsecs / (framemsecs+cmd->msec):0; float oscale = 1 - nscale; unsigned int st; CL_BaseMove (newmoves, plnum); CL_AdjustAngles (plnum, frametime); if (!cmd->msec) VectorClear(mousemovements[plnum]); IN_Move (mousemovements[plnum], newmoves, plnum, frametime); CL_ClampPitch(plnum, frametime); for (i=0 ; i<3 ; i++) cmd->angles[i] = ((int)(cl.playerview[plnum].viewangles[i]*65536.0/360)&65535); cmd->fservertime = cl.servertime; cmd->servertime = cl.time*1000; #ifdef CSQC_DAT cmd->fclienttime = realtime - cl.mapstarttime; #endif cmd->forwardmove = bound(-32768, cmd->forwardmove*oscale + newmoves[0]*nscale + mousemovements[plnum][0], 32767); cmd->sidemove = bound(-32768, cmd->sidemove*oscale + newmoves[1]*nscale + mousemovements[plnum][1], 32767); cmd->upmove = bound(-32768, cmd->upmove*oscale + newmoves[2]*nscale + mousemovements[plnum][2], 32767); if (!cmd->msec && framemsecs) { CL_GatherButtons(cmd, plnum); //buttons are from the initial state. don't blend them. CL_FinishMove(cmd, plnum); Cbuf_Waited(); //its okay to stop waiting now } cmd->msec = framemsecs; if (cl.movesequence >= 1) { //fix up the servertime value to make sure our msecs are actually correct. st = cl.outframes[(cl.movesequence-1)&UPDATE_MASK].cmd[plnum].servertime + (cmd->msec); //round it. if (abs((int)st-(int)cmd->servertime) < 50) { cmd->servertime = st; cmd->fservertime = (double)st/1000.0; } } // if we are spectator, try autocam // if (cl.spectator) Cam_Track(&cl.playerview[plnum], &cl_pendingcmd[plnum]); Cam_FinishMove(&cl.playerview[plnum], &cl_pendingcmd[plnum]); } static qboolean CLFTE_SendVRCmd (sizebuf_t *buf, unsigned int seats) { //compute the delay between receiving the frame we're acking and when we're sending the new frame unsigned int cldelay = (realtime - cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK].receivedtime)*10000; //this is to report actual network latency instead of just reporting our packet rate (framerates may still be a factor). unsigned int lost = CL_CalcNet(r_netgraph.value); //report packetloss unsigned int flags = 0; unsigned int first = cl.ackedmovesequence+1; //no point resending that which has already been acked. unsigned int last = cl.movesequence+1; //we want to ignore moveseq itself unsigned int frame, seat, count, i; const usercmd_t *from, *to; qboolean dontdrop = false; if (first > last) first = last-1; if (first < last-(countof(cl.outframes)-2)) first = last-(countof(cl.outframes)-2); if (first < 1) first = 1; if (last < first) count = 0; else count = last-first; if (count > max(1,cl_c2sMaxRedundancy.ival)) count = max(1,cl_c2sMaxRedundancy.ival); if (cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK].receivedtime<0) cldelay = 0; //erk? MSG_WriteByte (buf, clcfte_move); #ifdef NQPROT if (cls.protocol == CP_NETQUAKE) //nq uses fully separate packet+movement sequences (unlike qw). MSG_WriteShort(buf, (last-1)&0xffff); #endif if (seats!=1) flags |= VRM_SEATS; if (lost) flags |= VRM_LOSS; if (cldelay) flags |= VRM_DELAY; if (count!=3) flags |= VRM_FRAMES; if (cl.numackframes) flags |= VRM_ACKS; MSG_WriteUInt64 (buf, flags); if (flags & VRM_SEATS) MSG_WriteUInt64 (buf, seats); if (flags & VRM_FRAMES) MSG_WriteUInt64 (buf, count); if (flags & VRM_LOSS) MSG_WriteByte (buf, (qbyte)lost); if (flags & VRM_DELAY) MSG_WriteByte (buf, bound(0,cldelay,255)); //a byte should always be enough for any framerate above 40, and we don't want peole to be able to lie so easily. if (flags & VRM_ACKS) { MSG_WriteUInt64(buf, cl.numackframes); for (i = 0; i < cl.numackframes; i++) MSG_WriteLong(buf, cl.ackframes[i]); cl.numackframes = 0; } for (seat = 0; seat < seats; seat++) { from = &nullcmd; for (frame = last-count; frame < last; frame++) { to = &cl.outframes[frame&UPDATE_MASK].cmd[seat]; MSGFTE_WriteDeltaUsercmd (buf, cl.playerview[seat].baseangles, from, to); if (to->impulse && (int)(last-frame)>=cl_c2sImpulseBackup.ival) dontdrop = true; from = to; } } return dontdrop; } void CL_UpdatePrydonCursor(usercmd_t *from, int pnum) { int hit; vec3_t cursor_end; vec3_t temp; vec3_t cursor_impact_normal; cursor_active = true; if (!cl_prydoncursor.ival) { //center the cursor from->cursor_screen[0] = 0; from->cursor_screen[1] = 0; } else { from->cursor_screen[0] = mousecursor_x/(vid.width/2.0f) - 1; from->cursor_screen[1] = mousecursor_y/(vid.height/2.0f) - 1; if (from->cursor_screen[0] < -1) from->cursor_screen[0] = -1; if (from->cursor_screen[1] < -1) from->cursor_screen[1] = -1; if (from->cursor_screen[0] > 1) from->cursor_screen[0] = 1; if (from->cursor_screen[1] > 1) from->cursor_screen[1] = 1; } VectorClear(from->cursor_start); temp[0] = (from->cursor_screen[0]+1)/2; temp[1] = (-from->cursor_screen[1]+1)/2; temp[2] = 1; VectorCopy(r_origin, from->cursor_start); Matrix4x4_CM_UnProject(temp, cursor_end, cl.playerview[pnum].viewangles, from->cursor_start, r_refdef.fov_x, r_refdef.fov_y); CL_SetSolidEntities(); //don't bother with players, they don't exist in NQ... CL_TraceLine(from->cursor_start, cursor_end, from->cursor_impact, cursor_impact_normal, &hit); if (hit>0) from->cursor_entitynumber = hit; else if (hit < 0) from->cursor_entitynumber = 0; //FIXME: ask csqc for the entity's entnum else from->cursor_entitynumber = 0; // P_RunParticleEffect(cursor_impact, vec3_origin, 15, 16); } #ifdef NQPROT void CLNQ_SendMove (usercmd_t *cmd, int pnum, sizebuf_t *buf) { int i; unsigned int bits; if (cls.demoplayback!=DPB_NONE) return; //err... don't bother... :) // // always dump the first two message, because it may contain leftover inputs // from the last level // if (cl.movesequence <= 2 || cls.state == ca_connected) { MSG_WriteByte (buf, clc_nop); return; } if (cls.qex) { MSG_WriteByte (buf, clc_delta); MSG_WriteULEB128(buf, cl.movesequence); } MSG_WriteByte (buf, clc_move); if (cls.protocol_nq >= CPNQ_DP7) { if (!cl_movement.ival) MSG_WriteLong(buf, 0); else MSG_WriteLong(buf, cl.movesequence); } else if (cls.fteprotocolextensions2 & PEXT2_PREDINFO) MSG_WriteShort(buf, cl.movesequence&0xffff); MSG_WriteFloat (buf, cmd->fservertime); // use latest time. because ping reports! if (cls.qex) MSG_WriteByte(buf, 1); for (i=0 ; i<3 ; i++) { if (cls.protocol_nq == CPNQ_FITZ666 || (cls.proquake_angles_hack && buf->prim.anglesize <= 1)) { //fitz/proquake protocols are always 16bit for this angle and 8bit elsewhere. rmq is always at least 16bit //the above logic should satify everything. MSG_WriteAngle16 (buf, cl.playerview[pnum].viewangles[i]); } else MSG_WriteAngle (buf, cl.playerview[pnum].viewangles[i]); } MSG_WriteShort (buf, cmd->forwardmove); MSG_WriteShort (buf, cmd->sidemove); MSG_WriteShort (buf, cmd->upmove); bits = cmd->buttons; if (cls.fteprotocolextensions2 & PEXT2_PRYDONCURSOR) { if (cmd->cursor_screen[0] || cmd->cursor_screen[1] || cmd->cursor_start[0] || cmd->cursor_start[1] || cmd->cursor_start[2] || cmd->cursor_impact[0] || cmd->cursor_impact[1] || cmd->cursor_impact[2] || cmd->cursor_entitynumber) bits |= (1u<<31); //set it if there's actually something to send. MSG_WriteLong (buf, bits); } else if (cls.protocol_nq >= CPNQ_DP6) { MSG_WriteLong (buf, bits); bits |= (1u<<31); //unconditionally set it (without writing it) } else MSG_WriteByte (buf, cmd->buttons); MSG_WriteByte (buf, cmd->impulse); if (bits & (1u<<31)) { MSG_WriteShort (buf, cmd->cursor_screen[0] * 32767.0f); MSG_WriteShort (buf, cmd->cursor_screen[1] * 32767.0f); MSG_WriteFloat (buf, cmd->cursor_start[0]); MSG_WriteFloat (buf, cmd->cursor_start[1]); MSG_WriteFloat (buf, cmd->cursor_start[2]); MSG_WriteFloat (buf, cmd->cursor_impact[0]); MSG_WriteFloat (buf, cmd->cursor_impact[1]); MSG_WriteFloat (buf, cmd->cursor_impact[2]); MSG_WriteEntity (buf, cmd->cursor_entitynumber); } } void QDECL Name_Callback(struct cvar_s *var, char *oldvalue) { if (cls.state <= ca_connected) return; if (cls.protocol != CP_NETQUAKE) return; CL_SendClientCommand(true, "name \"%s\"\n", var->string); } void CLNQ_SendCmd(sizebuf_t *buf) { int i; int seat; usercmd_t *cmd; i = cl.movesequence & UPDATE_MASK; cl.outframes[i].senttime = realtime; cl.outframes[i].latency = -1; cl.outframes[i].server_message_num = cl.validsequence; cl.outframes[i].cmd_sequence = cl.movesequence; cl.outframes[i].sentgametime = cl.movesequence_time; for (seat = 0; seat < cl.splitclients; seat++) { cmd = &cl.outframes[i].cmd[seat]; *cmd = cl_pendingcmd[seat]; cmd->fservertime = cl.movesequence_time; // cmd->msec = (cl.time - cl.outframes[(i-1)&UPDATE_MASK].sentgametime)*1000; #ifdef CSQC_DAT CSQC_Input_Frame(seat, cmd); #endif } CL_ClearPendingCommands(); //inputs are only sent once we receive an entity. if (cls.fteprotocolextensions2 & PEXT2_VRINPUTS) CLFTE_SendVRCmd(buf, (cls.signon != 4 || cls.state == ca_connected)?0:cl.splitclients); else { if (cls.signon == 4) { for (seat = 0; seat < cl.splitclients; seat++) { // send the unreliable message // if (independantphysics[seat].impulse && !cls.netchan.message.cursize) // CLNQ_SendMove (&cl.outframes[i].cmd[seat], seat, &cls.netchan.message); // else CLNQ_SendMove (&cl.outframes[i].cmd[seat], seat, buf); } } else MSG_WriteByte (buf, clc_nop); for (i = 0; i < cl.numackframes; i++) { MSG_WriteByte(buf, clcdp_ackframe); MSG_WriteLong(buf, cl.ackframes[i]); } cl.numackframes = 0; } } #else void Name_Callback(struct cvar_s *var, char *oldvalue) { } #endif float CL_FilterTime (double time, float wantfps, float limit, qboolean ignoreserver) //now returns the extra time not taken in this slot. Note that negative 1 means uncapped. { float fps, fpscap; if (cls.timedemo) return -1; if (cls.protocol == CP_QUAKE3) ignoreserver = true; /*ignore the server if we're playing demos, sending to the server only as replies, or if its meant to be disabled (netfps depending on where its called from)*/ if (cls.demoplayback != DPB_NONE || (cls.protocol != CP_QUAKEWORLD && cls.protocol != CP_NETQUAKE) || ignoreserver) { if (!wantfps) return -1; fps = max (1.0, wantfps); } else { fpscap = cls.maxfps ? max (30.0, cls.maxfps) : 0x7fff; #ifdef IRCCONNECT if (cls.netchan.remote_address.type == NA_IRC) fps = bound (0.1, wantfps, fpscap); //if we're connected via irc, allow a greatly reduced minimum cap else #endif if (wantfps < 1) fps = fpscap; else fps = bound (6.7, wantfps, fpscap); //we actually cap ourselves to 150msecs (1000/7 = 142) } //its not time yet if (ignoreserver) { //don't try to hold to milliseconds. if (time < 1000 / fps) return 0; } else { if (time < ceil(1000 / fps)) return 0; } //clamp it if we have over 1.5 frame banked somehow if (limit && time - (1000 / fps) > (1000 / fps)*limit) return (1000 / fps) * limit; //report how much spare time the caller now has return time - (1000 / fps); } typedef struct clcmdbuf_s { struct clcmdbuf_s *next; int len; qboolean reliable; unsigned int seat; char command[4]; //this is dynamically allocated, so this is variably sized. } clcmdbuf_t; static clcmdbuf_t *clientcmdlist; void VARGS CL_SendSeatClientCommand(qboolean reliable, unsigned int seat, char *format, ...) { qboolean oldallow; va_list argptr; char string[2048]; clcmdbuf_t *buf, *prev; if (cls.demoplayback && !(cls.demoplayback == DPB_MVD && cls.demoeztv_ext)) return; //no point. va_start (argptr, format); Q_vsnprintfz (string,sizeof(string), format,argptr); va_end (argptr); #ifdef Q3CLIENT if (cls.protocol == CP_QUAKE3) { q3->cl.SendClientCommand("%s", string); return; } #endif oldallow = CL_AllowIndependantSendCmd(false); buf = Z_Malloc(sizeof(*buf)+strlen(string)); strcpy(buf->command, string); buf->len = strlen(buf->command); buf->reliable = reliable; buf->seat = seat; //add to end of the list so that the first of the list is the first to be sent. if (!clientcmdlist) clientcmdlist = buf; else { for (prev = clientcmdlist; prev->next; prev=prev->next) ; prev->next = buf; } CL_AllowIndependantSendCmd(oldallow); } void VARGS CL_SendClientCommand(qboolean reliable, char *format, ...) { va_list argptr; char string[2048]; va_start (argptr, format); Q_vsnprintfz (string,sizeof(string), format,argptr); va_end (argptr); CL_SendSeatClientCommand(reliable, 0, "%s", string); } //sometimes a server will quickly restart twice. //connected clients will then receive TWO 'new' commands - both with the same servercount value. //the connection process then tries to proceed with two sets of commands until it fails catastrophically. //by attempting to strip out dupe commands we can usually avoid the issue //note that FTE servers track progress properly, so this is not an issue for us, but in the interests of compat with mvdsv... //however, FTE servers can send a little faster, so warnings about this can be awkward. int CL_RemoveClientCommands(char *command) { clcmdbuf_t *next, *first; int removed = 0; int len = strlen(command); CL_AllowIndependantSendCmd(false); if (!clientcmdlist) return 0; while(!strncmp(clientcmdlist->command, command, len)) { next = clientcmdlist->next; Z_Free(clientcmdlist); clientcmdlist=next; removed++; if (!clientcmdlist) return removed; } first = clientcmdlist; while(first->next) { if (!strncmp(first->next->command, command, len)) { next = first->next->next; Z_Free(first->next); first->next = next; removed++; } else first = first->next; } return removed; } void CL_FlushClientCommands(void) { clcmdbuf_t *next; CL_AllowIndependantSendCmd(false); while(clientcmdlist) { Con_DPrintf("Flushed command %s\n", clientcmdlist->command); next = clientcmdlist->next; Z_Free(clientcmdlist); clientcmdlist=next; } } qboolean runningindepphys; #ifdef MULTITHREAD void *indeplock; void *indepthread; qboolean allowindepphys; qboolean CL_AllowIndependantSendCmd(qboolean allow) { qboolean ret = allowindepphys; if (!runningindepphys) return ret; if (allowindepphys != allow && runningindepphys) { if (allow) Sys_UnlockMutex(indeplock); else Sys_LockMutex(indeplock); allowindepphys = allow; } return ret; } int CL_IndepPhysicsThread(void *param) { double sleeptime; double fps; double time, lasttime; double spare; lasttime = Sys_DoubleTime(); while(runningindepphys) { time = Sys_DoubleTime(); spare = CL_FilterTime((time - lasttime)*1000, cl_netfps.value, 1.5, false); if (spare) { time -= spare/1000.0f; Sys_LockMutex(indeplock); if (cls.state) CL_SendCmd(time - lasttime, false); lasttime = time; Sys_UnlockMutex(indeplock); } fps = cl_netfps.value; if (fps < 4) fps = 4; while (fps < 100) fps*=2; sleeptime = 1/fps; Sys_Sleep(sleeptime); } return 0; } void CL_UseIndepPhysics(qboolean allow) { if (runningindepphys == allow) return; if (allow) { //enable it indeplock = Sys_CreateMutex(); runningindepphys = true; indepthread = Sys_CreateThread("indepphys", CL_IndepPhysicsThread, NULL, THREADP_HIGHEST, 8192); allowindepphys = true; } else { CL_AllowIndependantSendCmd(true); //shut it down. runningindepphys = false; //tell thread to exit gracefully Sys_WaitOnThread(indepthread); indepthread = NULL; Sys_DestroyMutex(indeplock); indeplock = NULL; } } #else qboolean CL_AllowIndependantSendCmd(qboolean allow) { return false; } void CL_UseIndepPhysics(qboolean allow) { } #endif void CL_UpdateSeats(void) { if (!cls.netchan.message.cursize && cl.allocated_client_slots > 1 && cls.state == ca_active && cl.splitclients && (cls.fteprotocolextensions & PEXT_SPLITSCREEN) && cl.worldmodel) { int targ = bound(1, cl_splitscreen.ival+1, MAX_SPLITS); if (cl.splitclients < targ) { char *ver; char buffer[2048]; char infostr[2048]; infobuf_t *info = &cls.userinfo[cl.splitclients]; //some userinfos should always have a value if (!*InfoBuf_ValueForKey(info, "name")) //$name-2 InfoBuf_SetKey(info, "name", va("%s-%i", InfoBuf_ValueForKey(&cls.userinfo[0], "name"), cl.splitclients+1)); if (cls.protocol != CP_QUAKE2) { if (!*InfoBuf_ValueForKey(info, "team")) //put players on the same team by default. this avoids team damage in coop, and if you're playing on the same computer then you probably want to be on the same team anyway. InfoBuf_SetKey(info, "team", InfoBuf_ValueForKey(&cls.userinfo[0], "team")); if (!*InfoBuf_ValueForKey(info, "bottomcolor")) //bottom colour implies team in nq InfoBuf_SetKey(info, "bottomcolor", InfoBuf_ValueForKey(&cls.userinfo[0], "bottomcolor")); if (!*InfoBuf_ValueForKey(info, "topcolor")) //should probably pick a random top colour or something InfoBuf_SetKey(info, "topcolor", InfoBuf_ValueForKey(&cls.userinfo[0], "topcolor")); } if (!*InfoBuf_ValueForKey(info, "skin")) //give players the same skin by default, because we can. q2 cares for teams. qw might as well (its not like anyone actually uses them thanks to enemy-skin forcing). InfoBuf_SetKey(info, "skin", InfoBuf_ValueForKey(&cls.userinfo[0], "skin")); InfoBuf_SetKey(info, "chat", ""); #ifdef SVNREVISION if (strcmp(STRINGIFY(SVNREVISION), "-")) ver = va("%s v%i.%02i %s", DISTRIBUTION, FTE_VER_MAJOR, FTE_VER_MINOR, STRINGIFY(SVNREVISION)); else #endif ver = va("%s v%i.%02i", DISTRIBUTION, FTE_VER_MAJOR, FTE_VER_MINOR); InfoBuf_SetStarKey(info, "*ver", ver); InfoBuf_ToString(info, infostr, sizeof(infostr), NULL, NULL, NULL, &cls.userinfosync, info); CL_SendClientCommand(true, "addseat %i %s", cl.splitclients+1, COM_QuotedString(infostr, buffer, sizeof(buffer), false)); } else if (cl.splitclients > targ && targ >= 1) CL_SendClientCommand(true, "addseat %i", targ); } } /* ================= CL_SendCmd ================= */ qboolean CL_WriteDeltas (int plnum, sizebuf_t *buf) { int i; usercmd_t *cmd, *oldcmd; qboolean dontdrop = false; i = (cls.netchan.outgoing_sequence-2) & UPDATE_MASK; cmd = &cl.outframes[i].cmd[plnum]; if (cl_c2sImpulseBackup.ival >= 2) dontdrop = dontdrop || cmd->impulse; MSGCL_WriteDeltaUsercmd (buf, &nullcmd, cmd); oldcmd = cmd; i = (cls.netchan.outgoing_sequence-1) & UPDATE_MASK; if (cl_c2sImpulseBackup.ival >= 3) dontdrop = dontdrop || cmd->impulse; cmd = &cl.outframes[i].cmd[plnum]; MSGCL_WriteDeltaUsercmd (buf, oldcmd, cmd); oldcmd = cmd; i = (cls.netchan.outgoing_sequence) & UPDATE_MASK; if (cl_c2sImpulseBackup.ival >= 1) dontdrop = dontdrop || cmd->impulse; cmd = &cl.outframes[i].cmd[plnum]; MSGCL_WriteDeltaUsercmd (buf, oldcmd, cmd); return dontdrop; } #ifdef Q2CLIENT qboolean CLQ2_SendCmd (sizebuf_t *buf) { int seq_hash; qboolean dontdrop = false; usercmd_t *cmd; int checksumIndex, i; int lightlev; int seat; cl.movesequence = cls.netchan.outgoing_sequence; //make sure its correct even over map changes. seq_hash = cl.movesequence; for (seat = 0; seat < cl.splitclients; seat++) { // send this and the previous cmds in the message, so // if the last packet was dropped, it can be recovered i = cl.movesequence & UPDATE_MASK; cmd = &cl.outframes[i].cmd[seat]; //q2admin is retarded and kicks you if you get a stall. if (cmd->msec > 100) cmd->msec = 100; if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) { //checksum byte got switched around to something that doesn't include so much sequence info. not really sure why. if (!seat) { MSG_WriteByte (buf, clcq2_move); if (!cl.q2frame.valid || cl_nodelta.ival || (cls.demorecording && !cls.demohadkeyframe)) MSG_WriteLong (buf, -1); // no compression else MSG_WriteLong (buf, cl.q2frame.serverframe); } checksumIndex = buf->cursize; MSG_WriteByte (buf, 0); //each seat has its own individual checksum for some reason (but no extra clcq2_move - player counts are not dynamic). } else if (seat) { //multi-seat still has an extra clc_move per seat //but no checksum (pointless when its opensource anyway) //no sequence (only seat 0 reports that) MSG_WriteByte (buf, clcq2_move); checksumIndex = -1; } else { MSG_WriteByte (buf, clcq2_move); // save the position for a checksum qbyte if (cls.protocol_q2 == PROTOCOL_VERSION_R1Q2 || cls.protocol_q2 == PROTOCOL_VERSION_Q2PRO) checksumIndex = -1; else { checksumIndex = buf->cursize; MSG_WriteByte (buf, 0); } if (!cl.q2frame.valid || cl_nodelta.ival || (cls.demorecording && !cls.demohadkeyframe)) MSG_WriteLong (buf, -1); // no compression else MSG_WriteLong (buf, cl.q2frame.serverframe); } lightlev = R_LightPoint(cl.playerview[seat].simorg); // msecs = msecs - (double)msecstouse; i = cls.netchan.outgoing_sequence & UPDATE_MASK; cmd = &cl.outframes[i].cmd[seat]; *cmd = cl_pendingcmd[seat]; cmd->lightlevel = (lightlev>255)?255:lightlev; cl.outframes[i].senttime = realtime; cl.outframes[i].latency = -1; if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) { if (cmd->upmove >= 100) cmd->buttons |= 1<<3; //jump else if (cmd->upmove <= -100) cmd->buttons |= 1<<4; //crouch } else { if (in_jump.state[seat]&3 && cmd->upmove==0) cmd->upmove = 200; } if (cmd->buttons) cmd->buttons |= 128; //fixme: this isn't really what's meant by the anykey. // calculate a checksum over the move commands dontdrop |= CL_WriteDeltas(seat, buf); if (checksumIndex >= 0) { buf->data[checksumIndex] = Q2COM_BlockSequenceCRCByte( buf->data + checksumIndex + 1, buf->cursize - checksumIndex - 1, seq_hash); } } CL_ClearPendingCommands(); if (cl.sendprespawn || !cls.protocol_q2) buf->cursize = 0; //tastyspleen.net is alergic. else CL_UpdateSeats(); return dontdrop; } #endif qboolean CLQW_SendCmd (sizebuf_t *buf, qboolean actuallysend) { int seq_hash; qboolean dontdrop = false; usercmd_t *cmd; int checksumIndex, firstsize, plnum; int clientcount, lost; int curframe; int st = buf->cursize; int chatstate; cl.movesequence = cls.netchan.outgoing_sequence; //make sure its correct even over map changes. curframe = cl.movesequence & UPDATE_MASK; seq_hash = cl.movesequence; cl.outframes[curframe].server_message_num = cl.validsequence; cl.outframes[curframe].cmd_sequence = cl.movesequence; cl.outframes[curframe].senttime = realtime; cl.outframes[curframe].latency = -1; // send this and the previous cmds in the message, so // if the last packet was dropped, it can be recovered clientcount = cl.splitclients; if (!clientcount) clientcount = 1; chatstate = 0; if (cl_sendchatstate.ival) { if (Key_Dest_Has(kdm_message|kdm_console|kdm_cwindows)) chatstate |= 1; //chatting else if (Key_Dest_Has(~(kdm_game|kdm_centerprint))) chatstate |= 2; //afk. ezquake sends chatting, but neither are really appropriate. if (!vid.activeapp || vid.isminimized) chatstate |= 2; //afk. //FIXME: flag as afk if no new inputs for a while. } for (plnum = 0; plnumlightlevel = 0; #ifdef CSQC_DAT if (!runningindepphys) CSQC_Input_Frame(plnum, cmd); #endif } CL_ClearPendingCommands(); if (cls.fteprotocolextensions2 & PEXT2_VRINPUTS) dontdrop = CLFTE_SendVRCmd(buf, clientcount); else { cmd = &cl.outframes[curframe].cmd[0]; if (cmd->cursor_screen[0] || cmd->cursor_screen[1] || cmd->cursor_entitynumber || cmd->cursor_start[0] || cmd->cursor_start[1] || cmd->cursor_start[2] || cmd->cursor_impact[0] || cmd->cursor_impact[1] || cmd->cursor_impact[2]) { MSG_WriteByte (buf, clcfte_prydoncursor); MSG_WriteShort(buf, cmd->cursor_screen[0] * 32767.0f); MSG_WriteShort(buf, cmd->cursor_screen[1] * 32767.0f); MSG_WriteFloat(buf, cmd->cursor_start[0]); MSG_WriteFloat(buf, cmd->cursor_start[1]); MSG_WriteFloat(buf, cmd->cursor_start[2]); MSG_WriteFloat(buf, cmd->cursor_impact[0]); MSG_WriteFloat(buf, cmd->cursor_impact[1]); MSG_WriteFloat(buf, cmd->cursor_impact[2]); MSG_WriteEntity(buf, cmd->cursor_entitynumber); } MSG_WriteByte (buf, clc_move); // save the position for a checksum qbyte checksumIndex = buf->cursize; MSG_WriteByte (buf, 0); // write our lossage percentage lost = CL_CalcNet(r_netgraph.value); MSG_WriteByte (buf, (qbyte)lost); firstsize=0; for (plnum = 0; plnumcursize; } // calculate a checksum over the move commands buf->data[checksumIndex] = COM_BlockSequenceCRCByte( buf->data + checksumIndex + 1, firstsize - checksumIndex - 1, seq_hash); } // request delta compression of entities if (cls.netchan.outgoing_sequence - cl.validsequence >= UPDATE_BACKUP-1) cl.validsequence = 0; //delta_sequence is the _expected_ previous sequences, so is set before it arrives. if (cl.validsequence && !cl_nodelta.ival && cls.state == ca_active)// && !cls.demorecording) { MSG_WriteByte (buf, clc_delta); // Con_Printf("%i\n", cl.validsequence); MSG_WriteByte (buf, cl.validsequence&255); } if (cl.sendprespawn || !actuallysend) buf->cursize = st; //don't send movement commands while we're still supposedly downloading. mvdsv does not like that. else CL_UpdateSeats(); return dontdrop; } static void CL_SendUserinfoUpdate(void) { const char *key = cls.userinfosync.keys[0].name; infobuf_t *info = cls.userinfosync.keys[0].context; size_t bloboffset = cls.userinfosync.keys[0].syncpos; unsigned int seat = info - cls.userinfo; size_t blobsize; const char *blobdata = InfoBuf_BlobForKey(info, key, &blobsize, NULL); size_t sendsize = blobsize - bloboffset; const char *s; qboolean final = true; char enckey[2048]; char encval[2048]; #ifdef Q3CLIENT if (cls.protocol == CP_QUAKE3) { //q3 sends it all in one go char userinfo[2048]; InfoSync_Strip(&cls.userinfosync, info); //can't track this stuff. all or nothing. if (info == &cls.userinfo[0]) { InfoBuf_ToString(info, userinfo, sizeof(userinfo), NULL, NULL, NULL, NULL, NULL); q3->cl.SendClientCommand("userinfo \"%s\"", userinfo); } return; } #endif #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2 && !cls.fteprotocolextensions) { char userinfo[2048]; InfoSync_Strip(&cls.userinfosync, info); //can't track this stuff. all or nothing. if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) { extern size_t Q2EX_UserInfoToString(char *infostring, size_t maxsize, const char **ignore, int seats); Q2EX_UserInfoToString(userinfo, sizeof(userinfo), NULL, cl.splitclients); MSG_WriteByte (&cls.netchan.message, clcq2_userinfo); MSG_WriteString (&cls.netchan.message, userinfo+(*userinfo=='\\'?1:0)); } else if (info == &cls.userinfo[0]) { InfoBuf_ToString(info, userinfo, sizeof(userinfo), NULL, NULL, NULL, NULL, NULL); MSG_WriteByte (&cls.netchan.message, clcq2_userinfo); MSG_WriteString (&cls.netchan.message, userinfo); } return; } #endif if (seat < max(1,cl.splitclients)) { if (sendsize > 1023) { final = false; sendsize = 1023; //should be a multiple of 3 } if (!InfoBuf_EncodeString(key, strlen(key), enckey, sizeof(enckey)) || !InfoBuf_EncodeString(blobdata+bloboffset, sendsize, encval, sizeof(encval))) { //some buffer wasn't big enough... shouldn't happen. InfoSync_Remove(&cls.userinfosync, 0); return; } if (final && !bloboffset && *encval != '\xff' && *encval != '\xff') { //vanilla-compatible info. s = va("setinfo \"%s\" \"%s\"", enckey, encval); } else if (cls.fteprotocolextensions2 & PEXT2_INFOBLOBS) { //only flood servers that actually support it. if (final) s = va("setinfo \"%s\" \"%s\" %u", enckey, encval, (unsigned int)bloboffset); else s = va("setinfo \"%s\" \"%s\" %u+", enckey, encval, (unsigned int)bloboffset); } else { //server doesn't support it, just ignore the key InfoSync_Remove(&cls.userinfosync, 0); return; } if (seat && (cls.fteprotocolextensions&PEXT_SPLITSCREEN)) { MSG_WriteByte (&cls.netchan.message, (cls.protocol == CP_QUAKE2)?clcq2_stringcmd_seat:clcfte_stringcmd_seat); MSG_WriteByte (&cls.netchan.message, seat); } else { MSG_WriteByte (&cls.netchan.message, (cls.protocol == CP_QUAKE2)?clcq2_stringcmd:clc_stringcmd); if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) MSG_WriteByte (&cls.netchan.message, 1+seat); } MSG_WriteString (&cls.netchan.message, s); } if (bloboffset+sendsize == blobsize) InfoSync_Remove(&cls.userinfosync, 0); else cls.userinfosync.keys[0].syncpos += sendsize; } void CL_SendCmd (double frametime, qboolean mainloop) { sizebuf_t buf; qbyte data[MAX_DATAGRAM*16]; int i, plnum; usercmd_t *cmd; float wantfps; int fullsend; //-1: send for sequence, with no usercmd. 0: update input frame, but don't send anything. 1: time for a new usercmd static float pps_balance = 0; static int dropcount = 0; static double msecs; static double msecsround; qboolean dontdrop=false; float usetime; //how many msecs we can use for the new frame int msecstouse; //usetime truncated to network precision (how much we'll actually eat) float framemsecs; //how long we're saying the input frame should be (differs from realtime with nq as we want to send frames reguarly, but note this might end up with funny-duration frames). qboolean xonoticworkaround; clcmdbuf_t *next; if (runningindepphys) { double curtime; static double lasttime; curtime = Sys_DoubleTime(); frametime = curtime - lasttime; lasttime = curtime; } CL_ProxyMenuHooks(); if (cls.demoplayback != DPB_NONE || cls.state <= ca_demostart) { cursor_active = false; if (!cls.state || cls.demoplayback == DPB_MVD) { extern cvar_t cl_splitscreen; cl.ackedmovesequence = cl.movesequence; i = cl.movesequence & UPDATE_MASK; cl.movesequence++; cl.outframes[i].server_message_num = cl.validsequence; cl.outframes[i].cmd_sequence = cl.movesequence; cl.outframes[i].senttime = realtime; // we haven't gotten a reply yet // cl.outframes[i].receivedtime = -1; // we haven't gotten a reply yet if (cl.splitclients > cl_splitscreen.ival+1) { cl.splitclients = cl_splitscreen.ival+1; if (cl.splitclients < 1) cl.splitclients = 1; } for (plnum = 0; plnum < cl.splitclients; plnum++) { playerview_t *pv = &cl.playerview[plnum]; cmd = &cl.outframes[i].cmd[plnum]; CL_AccumlateInput(plnum, frametime, frametime*1000); *cmd = cl_pendingcmd[plnum]; memset(&cl_pendingcmd[plnum], 0, sizeof(*cmd)); //reset the pending for the next frame. #ifdef CSQC_DAT CSQC_Input_Frame(plnum, cmd); #endif if (cls.state == ca_active) { player_state_t *from, *to; from = &cl.inframes[cl.ackedmovesequence & UPDATE_MASK].playerstate[pv->playernum]; to = &cl.inframes[cl.movesequence & UPDATE_MASK].playerstate[pv->playernum]; CL_PredictUsercmd(pv->playernum, pv->viewentity, from, to, &cl.outframes[cl.ackedmovesequence & UPDATE_MASK].cmd[plnum]); } } while (clientcmdlist) { next = clientcmdlist->next; CL_Demo_ClientCommand(clientcmdlist->command); Con_DLPrintf(2, "Sending stringcmd %s\n", clientcmdlist->command); Z_Free(clientcmdlist); clientcmdlist = next; } cls.netchan.outgoing_sequence = cl.movesequence; } IN_Move (NULL, NULL, 0, frametime); Cbuf_Waited(); //its okay to stop waiting now return; // sendcmds come from the demo } memset(&buf, 0, sizeof(buf)); buf.maxsize = sizeof(data); buf.cursize = 0; buf.data = data; buf.prim = cls.netchan.message.prim; xonoticworkaround = cls.protocol == CP_NETQUAKE && CPNQ_IS_DP && cl.time && !cl.paused; if (xonoticworkaround) { if (cl.movesequence_time > cl.time + 0.5) cl.movesequence_time = cl.time + 0.5; //shouldn't really happen if (cl.movesequence_time < cl.time - 0.5) cl.movesequence_time = cl.time - 0.5; //shouldn't really happen framemsecs = (cl.time - cl.movesequence_time)*1000; wantfps = cl_netfps.value; usetime = CL_FilterTime(framemsecs, wantfps, 5, false); if (usetime > 0) { usetime = framemsecs - usetime; fullsend = true; } else { usetime = framemsecs - usetime; fullsend = false; } msecstouse = usetime; framemsecs = msecstouse; msecs = 0; } else { msecs += frametime*1000; // Con_Printf("%f\n", msecs); wantfps = cl_netfps.value; fullsend = true; msecstouse = 0; #ifndef CLIENTONLY if (sv.state && cls.state != ca_active) { //HACK: if we're also the server, spam like a crazy person until we're on the server, for faster apparent load times. fullsend = -1; //send no movement command. msecstouse = usetime = msecs; } else #endif { // while we're not playing send a slow keepalive fullsend to stop mvdsv from screwing up if (cls.state < ca_active && !cls.download) { #ifdef IRCCONNECT //don't spam irc. if (cls.netchan.remote_address.type == NA_IRC) wantfps = 0.5; else #endif wantfps = 12.5; } if (!runningindepphys && (cl_netfps.value > 0 || !fullsend)) { float spare; spare = CL_FilterTime(msecs, wantfps, (/*cls.protocol == CP_NETQUAKE*/0?0:1.5), false); usetime = msecsround + (msecs - spare); msecstouse = (int)usetime; if (!spare) fullsend = false; else { msecsround = usetime - msecstouse; msecs = spare + msecstouse; } } else { usetime = msecsround + msecs; msecstouse = (int)usetime; msecsround = usetime - msecstouse; } } if (msecstouse > 200) // cap at 200 to avoid servers splitting movement more than four times msecstouse = 200; // align msecstouse to avoid servers wasting our msecs if (msecstouse > 100) msecstouse &= ~3; // align to 4 else if (msecstouse > 50) msecstouse &= ~1; // align to 2 if (msecstouse <= 0) //FIXME fullsend = false; if (usetime <= 0) return; //infinite frame times = weirdness. framemsecs = msecstouse; if (cls.protocol == CP_NETQUAKE) framemsecs = 1000*(cl.time - cl.movesequence_time); } #ifdef HLCLIENT if (!CLHL_BuildUserInput(msecstouse, &cl_pendingcmd[0])) #endif for (plnum = 0; plnum < (cl.splitclients?cl.splitclients:1); plnum++) CL_AccumlateInput(plnum, frametime, framemsecs); //the main loop isn't allowed to send if (runningindepphys && mainloop) return; // if (skipcmd) // return; if (!fullsend) return; // when we're actually playing we try to match netfps exactly to avoid gameplay problems // if (msecstouse > 127) // Con_Printf("%i\n", msecstouse, msecs); //HACK: 1000/77 = 12.98. nudge it just under so we never appear to be using 83fps at 77fps (which can trip cheat detection in mods that expect 72 fps when many servers are configured for 77) //so lets just never use 12. if (fullsend && cls.maxfps == 77) for (plnum = 0; plnum < (cl.splitclients?cl.splitclients:1); plnum++) if (cl_pendingcmd[plnum].msec > 12.9 && cl_pendingcmd[plnum].msec < 13) cl_pendingcmd[plnum].msec = 13; #ifdef NQPROT if (cls.protocol != CP_NETQUAKE || cls.netchan.nqreliable_allowed) #endif { CL_SendDownloadReq(&buf); //only start spamming userinfo blobs once we receive the initial serverinfo. while (cls.userinfosync.numkeys && cls.netchan.message.cursize < 512 && (cl.haveserverinfo || cls.protocol == CP_QUAKE2 || cls.protocol == CP_QUAKE3)) CL_SendUserinfoUpdate(); while (clientcmdlist) { next = clientcmdlist->next; if (clientcmdlist->reliable) { if (cls.netchan.message.cursize + 2+strlen(clientcmdlist->command)+100 > cls.netchan.message.maxsize) break; if (!strncmp(clientcmdlist->command, "spawn", 5) && cls.userinfosync.numkeys && cl.haveserverinfo) break; //HACK: don't send the spawn until all pending userinfos have been flushed. if (cls.protocol==CP_QUAKE2 && cls.protocol_q2==PROTOCOL_VERSION_Q2EX) { MSG_WriteByte (&cls.netchan.message, clcq2_stringcmd); MSG_WriteByte (&cls.netchan.message, clientcmdlist->seat+1); } else if (clientcmdlist->seat && (cls.fteprotocolextensions&PEXT_SPLITSCREEN)) { MSG_WriteByte (&cls.netchan.message, clcfte_stringcmd_seat); MSG_WriteByte (&cls.netchan.message, clientcmdlist->seat); } else MSG_WriteByte (&cls.netchan.message, clc_stringcmd); MSG_WriteString (&cls.netchan.message, clientcmdlist->command); } else { if (buf.cursize + 2+strlen(clientcmdlist->command)+100 <= buf.maxsize) { if (clientcmdlist->seat && (cls.fteprotocolextensions&PEXT_SPLITSCREEN)) { MSG_WriteByte (&cls.netchan.message, clcfte_stringcmd_seat); MSG_WriteByte (&cls.netchan.message, clientcmdlist->seat); } else MSG_WriteByte (&buf, clc_stringcmd); MSG_WriteString (&buf, clientcmdlist->command); } } Con_DLPrintf(2, "Sending stringcmd %s\n", clientcmdlist->command); Z_Free(clientcmdlist); clientcmdlist = next; } } // if we're not doing clc_moves and etc, don't continue unless we wrote something previous // or we have something on the reliable buffer (or we're loopback and don't care about flooding) if (!fullsend && cls.netchan.remote_address.type != NA_LOOPBACK && buf.cursize < 1 && cls.netchan.message.cursize < 1) return; if (fullsend) { if (!cls.state) { msecs -= (double)msecstouse; return; } cursor_active = false; for (plnum = 0; plnum < cl.splitclients; plnum++) { cmd = &cl_pendingcmd[plnum]; if (((cls.fteprotocolextensions2 & PEXT2_PRYDONCURSOR)||(cls.protocol == CP_NETQUAKE && cls.protocol_nq >= CPNQ_DP6)) && (*cl_prydoncursor.string && cl_prydoncursor.ival >= 0) && cls.state == ca_active) CL_UpdatePrydonCursor(cmd, plnum); else { Vector2Clear(cmd->cursor_screen); VectorClear(cmd->cursor_start); VectorClear(cmd->cursor_impact); cmd->cursor_entitynumber = 0; } } if (xonoticworkaround) cl.movesequence_time += msecstouse/1000.0; else cl.movesequence_time = cl.time; switch (cls.protocol) { #ifdef NQPROT case CP_NETQUAKE: msecs -= (double)msecstouse; CLNQ_SendCmd (&buf); dontdrop = true; break; #endif case CP_QUAKEWORLD: msecs -= (double)msecstouse; dontdrop = CLQW_SendCmd (&buf, fullsend == true); break; #ifdef Q2CLIENT case CP_QUAKE2: msecs -= (double)msecstouse; dontdrop = CLQ2_SendCmd (&buf); break; #endif #ifdef Q3CLIENT case CP_QUAKE3: msecs -= (double)msecstouse; i = cl.movesequence&UPDATE_MASK; memcpy(cl.outframes[i].cmd, cl_pendingcmd, sizeof(usercmd_t)*bound(1, cl.splitclients, MAX_SPLITS)); cl.outframes[i].cmd_sequence = cl.movesequence++; q3->cl.SendCmd(cls.sockets, cl.outframes[i].cmd, cl.movesequence, cl.time); cls.netchan.outgoing_sequence = cl.movesequence; CL_ClearPendingCommands(); //don't bank too much, because that results in banking speedcheats if (msecs > 200) msecs = 200; return; // Q3 does it's own thing #endif default: Host_EndGame("Invalid protocol in CL_SendCmd: %i", cls.protocol); return; } if (cls.demorecording) CL_WriteDemoCmd(&cl.outframes[cl.movesequence & UPDATE_MASK].cmd[0]); // Con_DPrintf("generated sequence %i\n", cl.movesequence); cl.movesequence++; //clear enough of the pending command for the next frame. for (plnum = 0; plnum < cl.splitclients; plnum++) { cl_pendingcmd[plnum].sequence = cl.movesequence; cl_pendingcmd[plnum].msec = 0; cl_pendingcmd[plnum].impulse = 0; // cl_pendingcmd[plnum].buttons = 0; } } #ifdef IRCCONNECT if (cls.netchan.remote_address.type == NA_IRC) { if (dropcount >= 2) { dropcount = 0; } else { // don't count this message when calculating PL cl.outframes[cls.netchan.outgoing_sequence&UPDATE_MASK].latency = -3; // drop this message cls.netchan.outgoing_sequence++; dropcount++; return; } } else #endif //shamelessly stolen from fuhquake if (cl_c2spps.ival>0) { pps_balance += frametime; // never drop more than 2 messages in a row -- that'll cause PL // and don't drop if one of the last two movemessages have an impulse if (pps_balance > 0 || dropcount >= 2 || dontdrop) { float pps; pps = cl_c2spps.ival; if (pps < 10) pps = 10; if (pps > 72) pps = 72; pps_balance -= 1 / pps; // bound pps_balance. FIXME: is there a better way? if (pps_balance > 0.1) pps_balance = 0.1; if (pps_balance < -0.1) pps_balance = -0.1; dropcount = 0; } else { // don't count this message when calculating PL cl.outframes[(cl.movesequence-1) & UPDATE_MASK].latency = -3; // drop this message cls.netchan.outgoing_sequence++; dropcount++; return; } } else { pps_balance = 0; dropcount = 0; } #ifdef VOICECHAT if (cls.protocol == CP_QUAKE2) S_Voip_Transmit(clcq2_voicechat, &buf); else S_Voip_Transmit(clcfte_voicechat, &buf); #endif // // deliver the message // cls.netchan.dupe = cl_c2sdupe.ival; Netchan_Transmit (&cls.netchan, buf.cursize, buf.data, 2500); //don't bank too much, because that results in banking speedcheats if (msecs > 200) msecs = 200; if (cls.netchan.fatal_error) { cls.netchan.fatal_error = false; cls.netchan.message.overflowed = false; cls.netchan.message.cursize = 0; } } void CL_SendCvar_f (void) { cvar_t *var; char *val; char *name = Cmd_Argv(1); var = Cvar_FindVar(name); if (!var) val = ""; else if (var->flags & CVAR_NOUNSAFEEXPAND) val = ""; else val = var->string; CL_SendSeatClientCommand(true, CL_TargettedSplit(false), "sentcvar %s \"%s\"", name, val); } /* ============ CL_InitInput ============ */ void CL_InitInput (void) { static char pcmd[MAX_SPLITS][3][6]; unsigned int sp, i; #define inputnetworkcvargroup "client networking options" cl.splitclients = 1; Cmd_AddCommand("rotate", IN_Rotate_f); Cmd_AddCommand("in_restart", IN_Restart); Cmd_AddCommand("sendcvar", CL_SendCvar_f); Cvar_Register (&cl_fastaccel, inputnetworkcvargroup); Cvar_Register (&in_xflip, inputnetworkcvargroup); Cvar_Register (&in_vraim, inputnetworkcvargroup); Cvar_Register (&cl_nodelta, inputnetworkcvargroup); Cvar_Register (&prox_inmenu, inputnetworkcvargroup); Cvar_Register (&cl_c2sdupe, inputnetworkcvargroup); Cvar_Register (&cl_c2sImpulseBackup, inputnetworkcvargroup); Cvar_Register (&cl_c2sMaxRedundancy, inputnetworkcvargroup); Cvar_Register (&cl_c2spps, inputnetworkcvargroup); Cvar_Register (&cl_queueimpulses, inputnetworkcvargroup); Cvar_Register (&cl_netfps, inputnetworkcvargroup); Cvar_Register (&cl_run, inputnetworkcvargroup); Cvar_Register (&cl_iDrive, inputnetworkcvargroup); #ifdef NQPROT Cvar_Register (&cl_movement, inputnetworkcvargroup); #endif Cvar_Register (&cl_sendchatstate, inputnetworkcvargroup); Cvar_Register (&cl_smartjump, inputnetworkcvargroup); Cvar_Register (&cl_prydoncursor, inputnetworkcvargroup); Cvar_Register (&cl_instantrotate, inputnetworkcvargroup); Cvar_Register (&cl_forceseat, inputnetworkcvargroup); for (sp = 0; sp < MAX_SPLITS; sp++) { Q_snprintfz(pcmd[sp][0], sizeof(pcmd[sp][0]), "p%i", sp+1); Q_snprintfz(pcmd[sp][1], sizeof(pcmd[sp][1]), "+p%i", sp+1); Q_snprintfz(pcmd[sp][2], sizeof(pcmd[sp][2]), "-p%i", sp+1); Cmd_AddCommand (pcmd[sp][0], CL_Split_f); Cmd_AddCommand (pcmd[sp][1], CL_Split_f); Cmd_AddCommand (pcmd[sp][2], CL_Split_f); /*default mlook to pressed, (on android we split the two sides of the screen)*/ in_mlook.state[sp] = 1; } /*then alternative arged ones*/ Cmd_AddCommand ("p", CL_SplitA_f); Cmd_AddCommand ("+p", CL_SplitA_f); Cmd_AddCommand ("-p", CL_SplitA_f); Cmd_AddCommand ("+moveup", IN_UpDown); Cmd_AddCommand ("-moveup", IN_UpUp); Cmd_AddCommand ("+movedown", IN_DownDown); Cmd_AddCommand ("-movedown", IN_DownUp); Cmd_AddCommand ("+left", IN_LeftDown); Cmd_AddCommand ("-left", IN_LeftUp); Cmd_AddCommand ("+right", IN_RightDown); Cmd_AddCommand ("-right", IN_RightUp); Cmd_AddCommand ("+forward", IN_ForwardDown); Cmd_AddCommand ("-forward", IN_ForwardUp); Cmd_AddCommand ("+back", IN_BackDown); Cmd_AddCommand ("-back", IN_BackUp); Cmd_AddCommand ("+lookup", IN_LookupDown); Cmd_AddCommand ("-lookup", IN_LookupUp); Cmd_AddCommand ("+lookdown", IN_LookdownDown); Cmd_AddCommand ("-lookdown", IN_LookdownUp); Cmd_AddCommand ("+strafe", IN_StrafeDown); Cmd_AddCommand ("-strafe", IN_StrafeUp); Cmd_AddCommand ("+moveleft", IN_MoveleftDown); Cmd_AddCommand ("-moveleft", IN_MoveleftUp); Cmd_AddCommand ("+moveright", IN_MoverightDown); Cmd_AddCommand ("-moveright", IN_MoverightUp); Cmd_AddCommand ("+rollleft", IN_RollLeftDown); Cmd_AddCommand ("-rollleft", IN_RollLeftUp); Cmd_AddCommand ("+rollright", IN_RollRightDown); Cmd_AddCommand ("-rollright", IN_RollRightUp); Cmd_AddCommand ("+speed", IN_SpeedDown); Cmd_AddCommand ("-speed", IN_SpeedUp); Cmd_AddCommand ("+attack", IN_AttackDown); Cmd_AddCommand ("-attack", IN_AttackUp); Cmd_AddCommand ("+use", IN_UseDown); Cmd_AddCommand ("-use", IN_UseUp); Cmd_AddCommand ("+jump", IN_JumpDown); Cmd_AddCommand ("-jump", IN_JumpUp); Cmd_AddCommandD("impulse", IN_Impulse, "Sends an impulse number to the server (read: weapon change)."); Cmd_AddCommand ("+klook", IN_KLookDown); Cmd_AddCommand ("-klook", IN_KLookUp); Cmd_AddCommand ("+mlook", IN_MLookDown); Cmd_AddCommand ("-mlook", IN_MLookUp); #ifdef QUAKESTATS Cmd_AddCommand ("+weaponwheel", IN_WWheelDown); Cmd_AddCommand ("-weaponwheel", IN_WWheelUp); Cmd_AddCommandD ("register_bestweapon", IN_RegisterWeapon_f, "Normally set via a mod's default.cfg file"); Cmd_AddCommandD ("bestweapon", IN_Impulse, "Works like 'impulse', for compat with other engines."); Cvar_Register (&cl_weaponhide, inputnetworkcvargroup); Cvar_Register (&cl_weaponhide_preference, inputnetworkcvargroup); Cvar_Register (&cl_weaponpreselect, inputnetworkcvargroup); Cvar_Register (&cl_weaponforgetorder, inputnetworkcvargroup); Cvar_Register (&r_viewpreselgun, inputnetworkcvargroup); #endif for (i = 0; i < countof(in_button); i++) { static char bcmd[countof(in_button)][2][10]; Q_snprintfz(bcmd[i][0], sizeof(bcmd[sp][0]), "+button%i", i); Q_snprintfz(bcmd[i][1], sizeof(bcmd[sp][1]), "-button%i", i); Cmd_AddCommandD(bcmd[i][0], IN_ButtonNDown, "This auxilliary command has mod-specific behaviour (often none)."); Cmd_AddCommand (bcmd[i][1], IN_ButtonNUp); } } ================================================ FILE: engine/client/cl_main.c ================================================ /* Copyright (C) 1996-1997 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. */ // cl_main.c -- client main loop #include "quakedef.h" #include "winquake.h" #include #include "netinc.h" #include "cl_master.h" #include "cl_ignore.h" #include "shader.h" #include "vr.h" #include // callbacks void QDECL CL_Sbar_Callback(struct cvar_s *var, char *oldvalue); #ifdef NQPROT void QDECL Name_Callback(struct cvar_s *var, char *oldvalue); #else #define Name_Callback NULL #endif void GnuTLS_Shutdown(void); static void CL_ForceStopDownload (qboolean finish); // we need to declare some mouse variables here, because the menu system // references them even when on a unix system. qboolean noclip_anglehack; // remnant from old quake int startuppending; extern int r_blockvidrestart; void Host_FinishLoading(void); cvar_t cl_crypt_rcon = CVARFD("cl_crypt_rcon", "1", CVAR_ARCHIVE|CVAR_NOTFROMSERVER, "Controls whether to send a hash instead of sending your rcon password as plain-text. Set to 1 for security, or 0 for backwards compatibility.\nYour command and any responses will still be sent as plain text.\nInstead, it is recommended to use rcon ONLY via dtls/tls/wss connections."); //CVAR_NOTFROMSERVER prevents evil servers from degrading it to send plain-text passwords. cvar_t rcon_password = CVARF("rcon_password", "", CVAR_NOUNSAFEEXPAND); cvar_t rcon_address = CVARF("rcon_address", "", CVAR_NOUNSAFEEXPAND); cvar_t cl_timeout = CVAR("cl_timeout", "60"); cvar_t cl_shownet = CVARD("cl_shownet","0", "Debugging var. 0 shows nothing. 1 shows incoming packet sizes. 2 shows individual messages. 3 shows entities too."); // can be 0, 1, or 2 cvar_t cl_disconnectreason = CVARAFD("_cl_disconnectreason", "", /*q3*/"com_errorMessage", CVAR_NOSAVE, "This cvar contains the reason for the last disconnection, so that mod menus can know why things failed."); cvar_t cl_pure = CVARD("cl_pure", "0", "0=standard quake rules.\n1=clients should prefer files within packages present on the server.\n2=clients should use *only* files within packages present on the server.\nDue to quake 1.01/1.06 differences, a setting of 2 is only reliable with total conversions.\nIf sv_pure is set, the client will prefer the highest value set."); cvar_t cl_sbar = CVARFC("cl_sbar", "0", CVAR_ARCHIVE, CL_Sbar_Callback); cvar_t cl_hudswap = CVARF("cl_hudswap", "0", CVAR_ARCHIVE); cvar_t cl_maxfps = CVARFD("cl_maxfps", "250", CVAR_ARCHIVE, "Sets the maximum allowed framerate. If you're using vsync or want to uncap framerates entirely then you should probably set this to 0. Set cl_yieldcpu 0 if you're trying to benchmark."); static cvar_t cl_maxfps_slop = CVARFD("cl_maxfps_slop", "3", CVAR_ARCHIVE, "If a frame is delayed (eg because of poor system timer precision), this is how much sooner to pretend the frame happened (in milliseconds). If it is set too low then the average framerate will drop below the target, while too high may result in excessively fast frames."); static cvar_t cl_idlefps = CVARAFD("cl_idlefps", "60", "cl_maxidlefps"/*dp*/, CVAR_ARCHIVE, "This is the maximum framerate to attain while idle/paused/unfocused."); cvar_t cl_yieldcpu = CVARFD("cl_yieldcpu", "1", CVAR_ARCHIVE, "Attempt to yield between frames. This can resolve issues with certain drivers and background software, but can mean less consistant frame times. Will reduce power consumption/heat generation so should be set on laptops or similar (over-hot/battery powered) devices."); cvar_t cl_nopext = CVARF("cl_nopext", "0", CVAR_ARCHIVE); static cvar_t cl_pext_mask = CVAR("cl_pext_mask", "0xffffffff"); cvar_t cl_nolerp = CVARD("cl_nolerp", "0", "Disables interpolation. If set, missiles/monsters will be show exactly what was last received, which will be jerky. Does not affect players. A value of 2 means 'interpolate only in single-player/coop'."); #ifdef NQPROT cvar_t cl_nolerp_netquake = CVARD("cl_nolerp_netquake", "0", "Disables interpolation when connected to an NQ server. Does affect players, even the local player. You probably don't want to set this."); static cvar_t cl_fullpitch_nq = CVARAFD("cl_fullpitch", "0", "pq_fullpitch", CVAR_SEMICHEAT, "When set, attempts to unlimit the default view pitch. Note that some servers will screw over your angles if you use this, resulting in terrible gameplay, while some may merely clamp your angle serverside. This is also considered a cheat in quakeworld, ^1so this will not function there^7. For the equivelent in quakeworld, use serverinfo minpitch+maxpitch instead, which applies to all players fairly."); #endif static cvar_t cl_vrui_force = CVARD("cl_vrui_force", "0", "Force the use of VR UIs, even with no VR headset active."); cvar_t cl_vrui_lock = CVARD("cl_vrui_lock", "1", "Controls how the UI is positioned when using VR/XR. 0: Repositioned infront of the head when the console/menus are toggled. 1: Locked infront of the reference position (which may require the user to turn around to find it)."); cvar_t *hud_tracking_show; cvar_t *hud_miniscores_show; extern cvar_t net_compress; cvar_t cl_defaultport = #ifdef GAME_DEFAULTPORT //remove the confusing port alias if we're running as a TC, as well as info about irrelevant games. CVARFD("cl_defaultport", STRINGIFY(PORT_DEFAULTSERVER), 0, "The default port used to connect to servers.") #else CVARAFD("cl_defaultport", STRINGIFY(PORT_DEFAULTSERVER), "port", 0, "The default port used to connect to servers." "\nQW: "STRINGIFY(PORT_QWSERVER) ", NQ: "STRINGIFY(PORT_NQSERVER) ", Q2: "STRINGIFY(PORT_Q2SERVER) ", Q3: "STRINGIFY(PORT_Q3SERVER) "." ) #endif ; cvar_t cfg_save_name = CVARFD("cfg_save_name", "fte", CVAR_ARCHIVE|CVAR_NOTFROMSERVER, "This is the config name that is saved by default when no argument is specified."); cvar_t cl_splitscreen = CVARD("cl_splitscreen", "0", "Enables splitscreen support. See also: allow_splitscreen, in_rawinput*, the \"p\" command."); cvar_t lookspring = CVARFD("lookspring","0", CVAR_ARCHIVE, "Recentre the camera when the mouse-look is released."); cvar_t lookstrafe = CVARFD("lookstrafe","0", CVAR_ARCHIVE, "Mouselook enables mouse strafing."); cvar_t sensitivity = CVARF("sensitivity","10", CVAR_ARCHIVE); cvar_t cl_staticsounds = CVARF("cl_staticsounds", "1", CVAR_ARCHIVE); cvar_t m_pitch = CVARF("m_pitch","0.022", CVAR_ARCHIVE); cvar_t m_yaw = CVARF("m_yaw","0.022", CVAR_ARCHIVE); cvar_t m_forward = CVARF("m_forward","1", CVAR_ARCHIVE); cvar_t m_side = CVARF("m_side","0.8", CVAR_ARCHIVE); cvar_t cl_lerp_maxinterval = CVARD("cl_lerp_maxinterval", "0.3", "Maximum interval between keyframes, in seconds. Larger values can result in entities drifting very slowly when they move sporadically."); cvar_t cl_lerp_maxdistance = CVARD("cl_lerp_maxdistance", "200", "Maximum distance that an entity may move between snapshots without being considered as having teleported."); cvar_t cl_lerp_players = CVARD("cl_lerp_players", "0", "Set this to make other players smoother, though it may increase effective latency. Affects only QuakeWorld."); cvar_t cl_predict_players = CVARD("cl_predict_players", "1", "Clear this cvar to see ents exactly how they are on the server."); cvar_t cl_predict_players_frac = CVARD("cl_predict_players_frac", "0.9", "How much of other players to predict. Values less than 1 will help minimize overruns."); cvar_t cl_predict_players_latency = CVARD("cl_predict_players_latency", "1.0", "Push the player back according to your latency, to give a smooth consistent simulation of the server."); cvar_t cl_predict_players_nudge = CVARD("cl_predict_players_nudge", "0.02", "An extra nudge of time, to cover video latency."); cvar_t cl_solid_players = CVARD("cl_solid_players", "1", "Consider other players as solid for player prediction."); cvar_t cl_noblink = CVARD("cl_noblink", "0", "Disable the ^^b text blinking feature."); cvar_t cl_servername = CVARFD("cl_servername", "", CVAR_NOSET, "The hostname of the last server you connected to"); cvar_t cl_serveraddress = CVARD("cl_serveraddress", "none", "The address of the last server you connected to"); cvar_t qtvcl_forceversion1 = CVAR("qtvcl_forceversion1", "0"); cvar_t qtvcl_eztvextensions = CVAR("qtvcl_eztvextensions", "1"); cvar_t record_flush = CVARD("record_flush", "0", "If set, explicitly flushes demo data to disk while recording. This may be inefficient, depending on how your operating system is configured."); cvar_t cl_demospeed = CVARF("cl_demospeed", "1", 0); cvar_t cl_demoreel = CVARFD("cl_demoreel", "0", CVAR_SAVE, "When enabled, the engine will begin playing a demo loop on startup."); cvar_t cl_loopbackprotocol = CVARD("cl_loopbackprotocol", "qw", "Which protocol to use for single-player/the internal client. Should be one of: qw, qwid, nqid, nq, fitz, bjp3, dp6, dp7, auto. If 'auto', will use qw protocols for qw mods, and nq protocols for nq mods."); #ifdef FTE_TARGET_WEB static cvar_t cl_verify_urischeme = CVARAFD("cl_verify_urischeme", "2", "cl_verify_qwprotocol"/*ezquake, inappropriate for misc schemes*/, CVAR_NOSAVE/*checked at startup, so its only really default.cfg that sets it*/, "0: Do nothing.\n1: Check whether our protocol scheme is registered and prompt the user to register associations.\n2: Always re-register on every startup, without prompting. Sledgehammer style."); #else static cvar_t cl_verify_urischeme = CVARAFD("cl_verify_urischeme", "0", "cl_verify_qwprotocol"/*ezquake, inappropriate for misc schemes*/, CVAR_NOSAVE/*checked at startup, so its only really default.cfg that sets it*/, "0: Do nothing.\n1: Check whether our protocol scheme is registered and prompt the user to register associations.\n2: Always re-register on every startup, without prompting. Sledgehammer style."); #endif cvar_t cl_fakeframes = CVARD("cl_fakeframes", "0", "Slow GPU? Want to see higher framerates get reported! Unleash the power of the lie to see much higher framerates! Many people said it couldn't be done, that the people wouldn't accept it, but to hell with the neighsayers and non-believers! WE WANT BIGGER NUMBERS AND WE'RE DAMN WELL GONNA GET THEM!... For best results, combine with an external tool like fluid motion frames..."); cvar_t cl_threadedphysics = CVARD("cl_threadedphysics", "0", "When set, client input frames are generated and sent on a worker thread"); #ifdef QUAKESPYAPI cvar_t localid = SCVAR("localid", ""); static qboolean allowremotecmd = true; #endif cvar_t r_drawflame = CVARD("r_drawflame", "1", "Set to -1 to disable ALL static entities. Set to 0 to disable only wall torches and standing flame. Set to 1 for everything drawn as normal."); qboolean forcesaveprompt; extern int total_loading_size, current_loading_size, loading_stage; // // info mirrors // cvar_t password = CVARAF("password", "", "pq_password", CVAR_USERINFO | CVAR_NOUNSAFEEXPAND); //this is parhaps slightly dodgy... added pq_password alias because baker seems to be using this for user accounts. cvar_t spectator = CVARF("spectator", "", CVAR_USERINFO); cvar_t name = CVARFC("name", "Player", CVAR_ARCHIVE | CVAR_USERINFO, Name_Callback); cvar_t team = CVARF("team", "", CVAR_ARCHIVE | CVAR_USERINFO); cvar_t skin = CVARAF("skin", "", "_cl_playerskin"/*dp*/, CVAR_ARCHIVE | CVAR_USERINFO); cvar_t model = CVARAF("model", "", "_cl_playermodel"/*dp*/, CVAR_ARCHIVE | CVAR_USERINFO); cvar_t topcolor = CVARF("topcolor", "13", CVAR_ARCHIVE | CVAR_USERINFO); cvar_t bottomcolor = CVARF("bottomcolor", "12", CVAR_ARCHIVE | CVAR_USERINFO); cvar_t rate = CVARAFD("rate", "30000"/*"6480"*/, "_cl_rate"/*dp*/, CVAR_ARCHIVE | CVAR_USERINFO, "A rough measure of the bandwidth to try to use while playing. Too high a value may result in 'buffer bloat'."); static cvar_t drate = CVARFD("drate", "3000000", CVAR_ARCHIVE | CVAR_USERINFO, "A rough measure of the bandwidth to try to use while downloading (in bytes per second)."); // :) static cvar_t noaim = CVARF("noaim", "", CVAR_ARCHIVE | CVAR_USERINFO); cvar_t msg = CVARFD("msg", "1", CVAR_ARCHIVE | CVAR_USERINFO, "Filter console prints/messages. Only functions on QuakeWorld servers. 0=pickup messages. 1=death messages. 2=critical messages. 3=chat."); static cvar_t b_switch = CVARF("b_switch", "", CVAR_ARCHIVE | CVAR_USERINFO); static cvar_t w_switch = CVARF("w_switch", "", CVAR_ARCHIVE | CVAR_USERINFO); #ifdef HEXEN2 cvar_t cl_playerclass=CVARF("cl_playerclass","", CVAR_ARCHIVE | CVAR_USERINFO); #endif #ifdef Q2CLIENT static cvar_t hand = CVARFD("hand", "", CVAR_ARCHIVE | CVAR_USERINFO, "For gamecode to know which hand to fire from.\n0: Right\n1: Left\n2: Chest"); #endif cvar_t cl_nofake = CVARD("cl_nofake", "2", "value 0: permits \\r chars in chat messages\nvalue 1: blocks all \\r chars\nvalue 2: allows \\r chars, but only from teammates"); cvar_t cl_chatsound = CVAR("cl_chatsound","1"); cvar_t cl_enemychatsound = CVAR("cl_enemychatsound", "misc/talk.wav"); cvar_t cl_teamchatsound = CVAR("cl_teamchatsound", "misc/talk.wav"); cvar_t r_torch = CVARFD("r_torch", "0", CVAR_CHEAT, "Generate a dynamic light at the player's position."); cvar_t r_rocketlight = CVARFC("r_rocketlight", "1", CVAR_ARCHIVE, Cvar_Limiter_ZeroToOne_Callback); cvar_t r_lightflicker = CVAR("r_lightflicker", "1"); cvar_t cl_r2g = CVARFD("cl_r2g", "0", CVAR_ARCHIVE, "Uses progs/grenade.mdl instead of progs/missile.mdl when 1."); cvar_t r_powerupglow = CVAR("r_powerupglow", "1"); cvar_t v_powerupshell = CVARF("v_powerupshell", "0", CVAR_ARCHIVE); cvar_t cl_gibfilter = CVARF("cl_gibfilter", "0", CVAR_ARCHIVE); cvar_t cl_deadbodyfilter = CVARF("cl_deadbodyfilter", "0", CVAR_ARCHIVE); cvar_t cl_gunx = CVAR("cl_gunx", "0"); cvar_t cl_guny = CVAR("cl_guny", "0"); cvar_t cl_gunz = CVAR("cl_gunz", "0"); cvar_t cl_gunanglex = CVAR("cl_gunanglex", "0"); cvar_t cl_gunangley = CVAR("cl_gunangley", "0"); cvar_t cl_gunanglez = CVAR("cl_gunanglez", "0"); cvar_t cl_proxyaddr = CVAR("cl_proxyaddr", ""); cvar_t cl_sendguid = CVARD("cl_sendguid", "", "Send a randomly generated 'globally unique' id to servers, which can be used by servers for score rankings and stuff. Different servers will see different guids. Delete the 'qkey' file in order to appear as a different user.\nIf set to 2, all servers will see the same guid. Be warned that this can show other people the guid that you're using."); cvar_t cl_downloads = CVARAFD("cl_downloads", "1", /*q3*/"cl_allowDownload", CVAR_NOTFROMSERVER|CVAR_ARCHIVE, "Allows you to block all automatic downloads."); cvar_t cl_download_csprogs = CVARFD("cl_download_csprogs", "1", CVAR_NOTFROMSERVER|CVAR_ARCHIVE, "Download updated client gamecode if available. Warning: If you clear this to avoid downloading vm code, you should also clear cl_download_packages."); cvar_t cl_download_redirection = CVARFD("cl_download_redirection", "2", CVAR_NOTFROMSERVER|CVAR_ARCHIVE, "Follow download redirection to download packages instead of individual files. Also allows the server to send nearly arbitary download commands.\n2: allows redirection only to named packages files (and demos/*.mvd), which is a bit safer."); cvar_t cl_download_packages = CVARFD("cl_download_packages", "1", CVAR_NOTFROMSERVER, "0=Do not download packages simply because the server is using them. 1=Download and load packages as needed (does not affect games which do not use this package). 2=Do download and install permanently (use with caution!)"); cvar_t requiredownloads = CVARAFD("cl_download_wait", "1", /*old*/"requiredownloads", CVAR_ARCHIVE, "0=join the game before downloads have even finished (might be laggy). 1=wait for all downloads to complete before joining."); cvar_t mod_precache = CVARD("mod_precache","1", "Controls when models are loaded.\n0: Load them only when they're actually needed.\n1: Load them upfront.\n2: Lazily load them to shorten load times at the risk of brief stuttering during only the start of the map."); cvar_t cl_muzzleflash = CVAR("cl_muzzleflash", "1"); cvar_t gl_simpleitems = CVARFD("gl_simpleitems", "0", CVAR_ARCHIVE, "Replace models with simpler sprites."); cvar_t cl_item_bobbing = CVARFD("cl_model_bobbing", "0", CVAR_ARCHIVE, "Makes rotating pickup items bob too."); cvar_t cl_countpendingpl = CVARD("cl_countpendingpl", "0", "If set to 1, packet loss percentages will show packets still in transit as lost, even if they might still be received."); cvar_t cl_standardchat = CVARFD("cl_standardchat", "0", CVAR_ARCHIVE, "Disables auto colour coding in chat messages."); cvar_t msg_filter = CVARD("msg_filter", "0", "Filter out chat messages: 0=neither. 1=broadcast chat. 2=team chat. 3=all chat."); cvar_t msg_filter_frags = CVARD("msg_filter_frags", "0", "Prevents frag messages from appearing on the console."); cvar_t msg_filter_pickups = CVARD("msg_filter_pickups", "0", "Prevents pickup messages from appearing on the console. This would normally be filtered by 'msg 1', but nq servers cannot respect that (nor nq mods running in qw servers)."); cvar_t cl_standardmsg = CVARFD("cl_standardmsg", "0", CVAR_ARCHIVE, "Disables auto colour coding in console prints."); cvar_t cl_parsewhitetext = CVARD("cl_parsewhitetext", "1", "When parsing chat messages, enable support for messages like: red{white}red"); cvar_t cl_dlemptyterminate = CVARD("cl_dlemptyterminate", "1", "Terminate downloads when reciving an empty download packet. This should help work around buggy mvdsv servers."); static void QDECL Cvar_CheckServerInfo(struct cvar_s *var, char *oldvalue) { //values depend upon the serverinfo, so reparse for overrides. CL_CheckServerInfo(); } #define RULESETADVICE " You should not normally change this cvar from its permissive default, instead impose limits on yourself only through the 'ruleset' cvar." cvar_t ruleset_allow_playercount = CVARD("ruleset_allow_playercount", "1", "Specifies whether teamplay triggers that count nearby players are allowed in the current ruleset."RULESETADVICE); cvar_t ruleset_allow_frj = CVARD("ruleset_allow_frj", "1", "Specifies whether Forward-Rocket-Jump scripts are allowed in the current ruleset. If 0, limits on yaw speed will be imposed so they cannot be scripted."RULESETADVICE); //FIXME: rename ruleset_allow_frj to allow_scripts to match ezquake - 0: block multiple commands in binds, 1: cap angle speed changes, 2: vanilla quake cvar_t ruleset_allow_semicheats = CVARD("ruleset_allow_semicheats", "1", "If 0, this blocks a range of cvars that are marked as semi-cheats. Such cvars will be locked to their empty/0 value."RULESETADVICE); cvar_t ruleset_allow_packet = CVARD("ruleset_allow_packet", "1", "If 0, network packets sent via the 'packet' command will be blocked. This makes scripting timers a little harder."RULESETADVICE); cvar_t ruleset_allow_particle_lightning = CVARD("ruleset_allow_particle_lightning", "1", "A setting of 0 blocks using the particle system to replace lightning gun trails. This prevents making the trails thinner thus preventing them from obscuring your view of your enemies."RULESETADVICE); cvar_t ruleset_allow_overlongsounds = CVARD("ruleset_allow_overlong_sounds", "1", "A setting of 0 will block the use of extra-long pickup sounds as item respawn timers."RULESETADVICE); cvar_t ruleset_allow_larger_models = CVARD("ruleset_allow_larger_models", "1", "Enforces a maximum bounds limit on models, to prevent the use of additional spikes attached to the model from being used as a kind of wallhack."RULESETADVICE); cvar_t ruleset_allow_modified_eyes = CVARD("ruleset_allow_modified_eyes", "0", "When 0, completely hides progs/eyes.mdl if it is not strictly identical to vanilla quake."RULESETADVICE); cvar_t ruleset_allow_sensitive_texture_replacements = CVARD("ruleset_allow_sensitive_texture_replacements", "1", "Allows the replacement of certain model textures (as well as the models themselves). This prevents adding extra fullbrights to make them blatently obvious."RULESETADVICE); cvar_t ruleset_allow_localvolume = CVARD("ruleset_allow_localvolume", "1", "Allows the use of the snd_playersoundvolume cvar. Muting your own sounds can make it easier to hear where your opponent is."RULESETADVICE); cvar_t ruleset_allow_shaders = CVARFD("ruleset_allow_shaders", "1", CVAR_SHADERSYSTEM, "When 0, this completely disables the use of external shader files, preventing custom shaders from being used for wallhacks."RULESETADVICE); cvar_t ruleset_allow_watervis = CVARFCD("ruleset_allow_watervis", "1", CVAR_SHADERSYSTEM, Cvar_CheckServerInfo, "When 0, this enforces ugly opaque water."RULESETADVICE); cvar_t ruleset_allow_fbmodels = CVARFD("ruleset_allow_fbmodels", "0", CVAR_SHADERSYSTEM, "When 1, allows all models to be displayed fullbright, completely ignoring the lightmaps. This feature exists only for parity with ezquake's defaults."RULESETADVICE); cvar_t ruleset_allow_triggers = CVARAD("ruleset_allow_triggers", "1", "tp_msgtriggers"/*ez*/, "When 0, blocks the use of msg_trigger checks."RULESETADVICE); extern cvar_t cl_hightrack; extern cvar_t vid_renderer; char cl_screengroup[] = "Screen options"; char cl_controlgroup[] = "client operation options"; char cl_inputgroup[] = "client input controls"; char cl_predictiongroup[] = "Client side prediction"; client_static_t cls; client_state_t cl; // alot of this should probably be dynamically allocated entity_state_t *cl_baselines; static_entity_t *cl_static_entities; unsigned int cl_max_static_entities; lightstyle_t *cl_lightstyle; size_t cl_max_lightstyles; dlight_t *cl_dlights; size_t cl_maxdlights; /*size of cl_dlights array*/ int cl_baselines_count; size_t rtlights_first, rtlights_max; // refresh list // this is double buffered so the last frame // can be scanned for oldorigins of trailing objects int cl_numvisedicts; int cl_maxvisedicts; entity_t *cl_visedicts; int cl_framecount; scenetris_t *cl_stris; vecV_t *fte_restrict cl_strisvertv; vec4_t *fte_restrict cl_strisvertc; vec2_t *fte_restrict cl_strisvertt; index_t *fte_restrict cl_strisidx; unsigned int cl_numstrisidx; unsigned int cl_maxstrisidx; unsigned int cl_numstrisvert; unsigned int cl_maxstrisvert; unsigned int cl_numstris; unsigned int cl_maxstris; static struct { qboolean trying; qboolean istransfer; //ignore the user's desired server (don't change connect.adr). qboolean resolving; int numadr; int nextadr; netadr_t adr[8]; //addresses that we're trying to transfer to, one entry per dns result, eg both ::1 AND 127.0.0.1 int protocol; //nq/qw/q2/q3. guessed based upon server replies int subprotocol; //the monkeys are trying to eat me. struct { //flags unsigned int fte1; unsigned int fte2; unsigned int ez1; int mtu; //0 for unsupported, otherwise a size. unsigned int compresscrc;//0 for unsupported, otherwise the peer's hash unsigned char guidsalt[64];//server->client (for servers that want to share guids between themselves, with noticably lower security) } ext; int qport; int challenge; //tracked as part of guesswork based upon what replies we get. int clchallenge; //generated by the client, to ensure the response wasn't spoofed/spammed. double time; //for connection retransmits qboolean clogged; //ignore time... enum coninfomode_e { CIM_DEFAULT, //sends both a qw getchallenge and nq connect (also with postfixed getchallenge so modified servers can force getchallenge) CIM_NQONLY, //disables getchallenge (so fte servers treat us as an nq client). should not be used for dpp7 servers. CIM_QEONLY, //forces dtls and uses a different nq netchan version CIM_Q2EONLY, //forces dtls and uses a different nq netchan version } mode; enum coninfospec_e { CIS_DEFAULT, //default CIS_JOIN, //force join CIS_OBSERVE, //force observe } spec; int defaultport; int tries; //increased each try, every fourth trys nq connect packets. unsigned char guid[64]; //client->server guid (so doesn't change with transfers) struct dtlspeercred_s peercred; } connectinfo; qboolean nomaster; double oldrealtime; // last frame run int host_framecount; qbyte *host_basepal; qbyte *h2playertranslations; cvar_t host_speeds = CVAR("host_speeds","0"); // set for running times int fps_count; qboolean forcesaveprompt; jmp_buf host_abort; void Master_Connect_f (void); char emodel_name[] = { 'e' ^ 0xff, 'm' ^ 0xff, 'o' ^ 0xff, 'd' ^ 0xff, 'e' ^ 0xff, 'l' ^ 0xff, 0 }; char pmodel_name[] = { 'p' ^ 0xff, 'm' ^ 0xff, 'o' ^ 0xff, 'd' ^ 0xff, 'e' ^ 0xff, 'l' ^ 0xff, 0 }; char prespawn_name[] = { 'p'^0xff, 'r'^0xff, 'e'^0xff, 's'^0xff, 'p'^0xff, 'a'^0xff, 'w'^0xff, 'n'^0xff, ' '^0xff, '%'^0xff, 'i'^0xff, ' '^0xff, '0'^0xff, ' '^0xff, '%'^0xff, 'i'^0xff, 0 }; char modellist_name[] = { 'm'^0xff, 'o'^0xff, 'd'^0xff, 'e'^0xff, 'l'^0xff, 'l'^0xff, 'i'^0xff, 's'^0xff, 't'^0xff, ' '^0xff, '%'^0xff, 'i'^0xff, ' '^0xff, '%'^0xff, 'i'^0xff, 0 }; char soundlist_name[] = { 's'^0xff, 'o'^0xff, 'u'^0xff, 'n'^0xff, 'd'^0xff, 'l'^0xff, 'i'^0xff, 's'^0xff, 't'^0xff, ' '^0xff, '%'^0xff, 'i'^0xff, ' '^0xff, '%'^0xff, 'i'^0xff, 0 }; vrui_t vrui; void VRUI_SnapAngle(void) { // VectorCopy(cl.playerview[0].viewangles, vrui.angles); vrui.angles[0] = 0; if (cl_vrui_lock.ival) //if its locked, then its locked infront of the unpitched player entity vrui.angles[1] = cl.playerview[0].viewangles[1]; else //otherwise its moved to infront of the player's head any time the menu is displayed. vrui.angles[1] = cl.playerview[0].aimangles[1]; vrui.angles[2] = 0; } void CL_UpdateWindowTitle(void) { if (VID_SetWindowCaption) { if (cl.windowtitle) { //gamecode wanted some explicit title. VID_SetWindowCaption(cl.windowtitle); return; } else { char title[2048]; switch (cls.state) { default: #ifdef HAVE_SERVER if (sv.state) Q_snprintfz(title, sizeof(title), "%s: %s", fs_gamename.string, svs.name); else #endif if (cls.demoplayback) Q_snprintfz(title, sizeof(title), "%s: %s", fs_gamename.string, cls.lastdemoname); else Q_snprintfz(title, sizeof(title), "%s: %s", fs_gamename.string, cls.servername); break; case ca_disconnected: if (CSQC_UnconnectedOkay(false)) //pure csqc mods can have a world model and yet be disconnected. we don't really know what the current map should be called though. Q_snprintfz(title, sizeof(title), "%s", fs_gamename.string); else Q_snprintfz(title, sizeof(title), "%s: disconnected", fs_gamename.string); break; } VID_SetWindowCaption(title); } } } #ifdef __GLIBC__ #include #endif void CL_MakeActive(char *gamename) { extern int fs_finds; if (fs_finds) { Con_DPrintf("%i additional FS searches\n", fs_finds); fs_finds = 0; } cl.matchgametimestart = 0; cls.state = ca_active; //this might be expensive, don't count any of this as time spent *playing* the demo. this avoids skipping the first $LOADDURATION seconds. cl.stillloading = true; //kill sounds left over from the last map. S_Purge(true); //kill models left over from the last map. Mod_Purge(MP_MAPCHANGED); //and reload shaders now if needed (this was blocked earlier) Shader_DoReload(); //and now free any textures that were not still needed. Image_Purge(); SCR_EndLoadingPlaque(); CL_UpdateWindowTitle(); #ifdef MVD_RECORDING if (sv_demoAutoRecord.ival && !sv.mvdrecording && !cls.demorecording && !cls.demoplayback && MVD_CheckSpace(false)) { //don't auto-record if we're already recording... or playing a different demo. extern cvar_t sv_demoAutoPrefix; char timestamp[64]; time_t tm = time(NULL); strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", localtime(&tm)); Cbuf_AddText(va("record %s%s%s%s_%s\n", sv_demoDir.string, *sv_demoDir.string?"/":"", sv_demoAutoPrefix.string, host_mapname.string, timestamp), RESTRICT_LOCAL); } #endif TP_ExecTrigger("f_begin", true); if (cls.demoplayback) TP_ExecTrigger("f_spawndemo", true); else TP_ExecTrigger("f_spawn", false); #ifdef __GLIBC__ malloc_trim(0); #endif } /* ================== CL_Quit_f ================== */ void CL_Quit_f (void) { if (!host_initialized) return; if (forcesaveprompt && strcmp(Cmd_Argv(1), "force")) { forcesaveprompt = false; if (Cmd_Exists("menu_quit")) { Cmd_ExecuteString("menu_quit", RESTRICT_LOCAL); return; } } TP_ExecTrigger("f_quit", true); Cbuf_Execute(); /* #ifdef HAVE_SERVER if (!isDedicated) #endif { M_Menu_Quit_f (); return; }*/ Sys_Quit (); } #ifdef NQPROT void CL_ConnectToDarkPlaces(char *challenge, netadr_t *adr) { char data[2048]; cls.fteprotocolextensions = 0; cls.fteprotocolextensions2 = 0; cls.ezprotocolextensions1 = 0; connectinfo.time = realtime; // for retransmit requests Q_snprintfz(data, sizeof(data), "%c%c%c%cconnect\\protocol\\darkplaces "STRINGIFY(NQ_NETCHAN_VERSION)"\\protocols\\DP7 DP6 DP5 RMQ FITZ NEHAHRABJP2 NEHAHRABJP NEHAHRABJP3 QUAKE\\challenge\\%s\\name\\%s", 255, 255, 255, 255, challenge, name.string); NET_SendPacket (cls.sockets, strlen(data), data, adr); cl.splitclients = 0; } #endif void CL_SupportedFTEExtensions(unsigned int *pext1, unsigned int *pext2, unsigned int *ezpext1) { unsigned int fteprotextsupported1 = Net_PextMask(PROTOCOL_VERSION_FTE1, false); unsigned int fteprotextsupported2 = Net_PextMask(PROTOCOL_VERSION_FTE2, false); unsigned int ezprotextsupported1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, false) & EZPEXT1_CLIENTADVERTISE; fteprotextsupported1 &= strtoul(cl_pext_mask.string, NULL, 16); // fteprotextsupported2 &= strtoul(cl_pext2_mask.string, NULL, 16); // ezprotextsupported1 &= strtoul(cl_ezpext1_mask.string, NULL, 16); if (cl_nopext.ival) { fteprotextsupported1 = 0; fteprotextsupported2 = 0; ezprotextsupported1 = 0; } *pext1 = fteprotextsupported1; *pext2 = fteprotextsupported2; *ezpext1 = ezprotextsupported1; } char *CL_GUIDString(netadr_t *adr) { static qbyte buf[2048]; static int buflen; qbyte digest[DIGEST_MAXSIZE]; char serveraddr[256]; void *ctx; if (!*cl_sendguid.string && *connectinfo.ext.guidsalt) { serveraddr[0] = '#'; //leading hash is to stop servers from being able to scrape from other servers (ones that don't use a custom/reproducible salt). Q_strncpyz(serveraddr+1, connectinfo.ext.guidsalt, sizeof(serveraddr)-1); } else if (cl_sendguid.ival == 2) *serveraddr = 0; else if (cl_sendguid.ival) NET_AdrToString(serveraddr, sizeof(serveraddr), adr); else return NULL; if (*connectinfo.guid && connectinfo.istransfer) return connectinfo.guid; if (!buflen) { vfsfile_t *f; f = FS_OpenVFS("qkey", "rb", FS_ROOT); if (f) { buflen = VFS_GETLEN(f); if (buflen > 2048) buflen = 2048; buflen = VFS_READ(f, buf, buflen); VFS_CLOSE(f); } if (buflen < 16) { buflen = sizeof(buf); if (!Sys_RandomBytes(buf, buflen)) { int i; srand(time(NULL)); for (i = 0; i < buflen; i++) buf[i] = rand() & 0xff; } f = FS_OpenVFS("qkey", "wb", FS_ROOT); if (f) { VFS_WRITE(f, buf, buflen); VFS_CLOSE(f); } } } ctx = alloca(hash_md4.contextsize); hash_md4.init(ctx); hash_md4.process(ctx, buf, buflen); hash_md4.process(ctx, serveraddr, strlen(serveraddr)); hash_md4.terminate(digest, ctx); Base16_EncodeBlock(digest, hash_md4.digestsize, connectinfo.guid, sizeof(connectinfo.guid)); return connectinfo.guid; } static void CL_ConnectAbort(const char *format, ...) { //stops trying to connect, doesn't affect the _current_ connection, so usable for transfers. va_list argptr; char reason[1024]; if (format) { va_start (argptr, format); Q_vsnprintfz (reason, sizeof(reason), format,argptr); va_end (argptr); Cvar_Set(&cl_disconnectreason, reason); Con_Printf (CON_ERROR"%s\n", reason); } #ifdef HAVE_DTLS while (connectinfo.numadr) NET_DTLS_Disconnect(cls.sockets, &connectinfo.adr[--connectinfo.numadr]); #endif connectinfo.numadr = 0; SCR_EndLoadingPlaque(); connectinfo.trying = false; if (format) { //try and force the menu to show again. this should force the disconnectreason to show. if (!Key_Dest_Has(kdm_console)) { #ifdef MENU_DAT if (!MP_Toggle(1)) #endif Menu_Prompt(NULL, NULL, reason, NULL, NULL, "Okay", true); } } } size_t Q2EX_UserInfoToString(char *infostring, size_t maxsize, const char **ignore, int seats) { //infoblobs are not a thing here. don't need to maintain sync objects - can't really do anything with them anyway. size_t k, r = 1, l; char *o = infostring; char *e = infostring?infostring + maxsize-1:infostring; int s; for (s = 0; s < seats; s++) { char *pf = s?va("_%i", s):""; size_t pfl = strlen(pf); infobuf_t *info = &cls.userinfo[s]; for (k = 0; k < info->numkeys; k++) { if (ignore) { for (l = 0; ignore[l]; l++) { if (!strcmp(ignore[l], info->keys[k].name)) break; else if (ignore[l][0] == '*' && !ignore[l][1] && *info->keys[k].name == '*') break; //read-only else if (ignore[l][0] == '_' && !ignore[l][1] && *info->keys[k].name == '_') break; //comment } if (ignore[l]) continue; //ignore when in the list } if (!info->keys[k].large) //lower priorities don't bother with extended blocks. be sure to prioritise them explicitly. they'd just bug stuff out. { size_t knl = strlen(info->keys[k].name); size_t kvl = info->keys[k].size; r+= 1+knl+pfl+1+kvl; if (o + 1+knl+1+kvl >= e) continue; o[0] = '\\'; memcpy(o+1, info->keys[k].name, knl); memcpy(o+1+knl, pf, pfl); o[1+knl+pfl] = '\\'; memcpy(o+1+knl+pfl+1, info->keys[k].value, kvl); o[1+knl+pfl+1+kvl] = 0; o += 1+knl+pfl+1+kvl; } } } *o = 0; return r; } /* ======================= CL_SendConnectPacket called by CL_Connect_f and CL_CheckResend ====================== */ static void CL_SendConnectPacket (netadr_t *to) { extern cvar_t qport; netadr_t addr; char data[2048]; char *info; double t1, t2; char *a; // JACK: Fixed bug where DNS lookups would cause two connects real fast // Now, adds lookup time to the connect time. // Should I add it to realtime instead?!?! if (!connectinfo.trying) return; if (cl_nopext.ival) //imagine it's an unenhanced server { connectinfo.ext.compresscrc = 0; } if (connectinfo.protocol == CP_QUAKEWORLD) { int fteprotextsupported1=0; int fteprotextsupported2=0; int ezprotextsupported1=0; CL_SupportedFTEExtensions(&fteprotextsupported1, &fteprotextsupported2, &ezprotextsupported1); connectinfo.ext.fte1 &= fteprotextsupported1; connectinfo.ext.fte2 &= fteprotextsupported2; connectinfo.ext.ez1 &= ezprotextsupported1; } #ifdef Q2CLIENT else if (connectinfo.protocol == CP_QUAKE2) { if (!(scr_fov.flags & CVAR_USERINFO)) { //q2 does server-controlled fov, so make sure the cvar is flagged properly. //FIXME: this hack needs better support, for dynamically switching between protocols without spamming too many cvars for other games. scr_fov.flags |= CVAR_USERINFO; Cvar_Set(&scr_fov, scr_fov.string); //make sure the userinfo is set properly. } connectinfo.ext.fte1 &= (PEXT_MODELDBL|PEXT_SOUNDDBL|PEXT_SPLITSCREEN); connectinfo.ext.fte2 &= PEXT2_STUNAWARE; connectinfo.ext.ez1 &= 0; } #endif else { connectinfo.ext.fte1 = 0; connectinfo.ext.fte2 = 0; connectinfo.ext.ez1 = 0; } t1 = Sys_DoubleTime (); #ifdef HAVE_DTLS if (connectinfo.peercred.hash && net_enable_dtls.ival>0) { char cert[8192]; char digest[DIGEST_MAXSIZE]; int sz = NET_GetConnectionCertificate(cls.sockets, to, QCERT_PEERCERTIFICATE, cert, sizeof(cert)); if (sz <= 0 || memcmp(connectinfo.peercred.digest, digest, CalcHash(connectinfo.peercred.hash, digest, sizeof(digest), cert, sz))) { //FIXME: we may have already pinned the bad cert, which may cause issues when reconnecting without FP info later. if (NET_GetConnectionCertificate(cls.sockets, to, QCERT_ISENCRYPTED, NULL, 0)<0) CL_ConnectAbort ("Fingerprint specified, but server did not report any certificate\n"); else CL_ConnectAbort ("Server certificate does not match specified fingerprint\n"); return; } } #endif if (!to) { to = &addr; if (!NET_StringToAdr (cls.servername, PORT_DEFAULTSERVER, to)) { CL_ConnectAbort ("CL_SendConnectPacket: Bad server address \"%s\"\n", cls.servername); return; } } NET_AdrToString(data, sizeof(data), to); Cvar_ForceSet(&cl_serveraddress, data); // Info_SetValueForStarKey (cls.userinfo, "*ip", data, MAX_INFO_STRING); if (!NET_IsClientLegal(to)) { CL_ConnectAbort("Illegal server address\n"); return; } t2 = Sys_DoubleTime (); connectinfo.time = realtime+t2-t1; // for retransmit requests //fixme: we shouldn't cycle these so much connectinfo.qport = qport.value; if (to->type != NA_LOOPBACK) Cvar_SetValue(&qport, (connectinfo.qport+1)&0xffff); if (connectinfo.protocol == CP_QUAKE2 && (connectinfo.subprotocol == PROTOCOL_VERSION_R1Q2 || connectinfo.subprotocol == PROTOCOL_VERSION_Q2PRO)) connectinfo.qport &= 0xff; #ifdef Q3CLIENT if (connectinfo.protocol == CP_QUAKE3) { //q3 requires some very strange things. //cl.splitclients = 1; if (q3) q3->cl.SendConnectPacket(cls.sockets, to, connectinfo.challenge, connectinfo.qport, cls.userinfo); else CL_ConnectAbort("Q3 plugin not loaded, cannot connect to q3 servers without it.\n"); return; } #endif if (connectinfo.subprotocol == PROTOCOL_VERSION_Q2EX) { size_t foo; int seats = bound(1, cl_splitscreen.ival+1, MAX_SPLITS), i; Q_snprintfz(data, sizeof(data), "%c%c%c%cconnect", 255, 255, 255, 255); //qport+challenge were removed. Q_strncatz(data, va(" %i %i", connectinfo.subprotocol, seats), sizeof(data)); //socials... for (i = 0; i < seats; i++) { Q_strncatz(data, va(" anon"), sizeof(data)); //also make sure they have a valid name field, Q2E kinda bugs out otherwise. if (!InfoBuf_FindKey(&cls.userinfo[i], "name", &foo)) InfoBuf_SetValueForKey(&cls.userinfo[i], "name", va("%s-%i", name.string, i+1)); if (!InfoBuf_FindKey(&cls.userinfo[i], "fov", &foo)) InfoBuf_SetValueForKey(&cls.userinfo[i], "fov", scr_fov.string); } Q_strncatz(data, " \"", sizeof(data)); //note: this specific info string should lack the leading \\ char. Q_strncatz(data, va("qport\\%i", connectinfo.qport), sizeof(data)); //just in case a server enforces it (ie: someone connecting directly raw udp without the lobby junk) Q_strncatz(data, va("\\challenge\\%i", connectinfo.challenge), sizeof(data)); //just in case a server enforces it (ie: someone connecting directly raw udp without the lobby junk) if (connectinfo.spec==CIS_OBSERVE) Q_strncatz(data, "\\spectator\\1", sizeof(data)); { const char *ignorekeys[] = {"prx", "*z_ext", (connectinfo.spec!=CIS_DEFAULT)?"spectator":NULL, NULL}; Q2EX_UserInfoToString(data+strlen(data), sizeof(data)-strlen(data), ignorekeys, seats); } Q_strncatz(data, "\"", sizeof(data)); Q_strncatz(data, "\n", sizeof(data)); connectinfo.ext.fte1 = connectinfo.ext.fte2 = connectinfo.ext.ez1 = 0; } else { Q_snprintfz(data, sizeof(data), "%c%c%c%cconnect", 255, 255, 255, 255); Q_strncatz(data, va(" %i %i %i", connectinfo.subprotocol, connectinfo.qport, connectinfo.challenge), sizeof(data)); //userinfo0 has some twiddles for extensions from other qw engines. Q_strncatz(data, " \"", sizeof(data)); //qwfwd proxy routing if ((a = strrchr(cls.servername, '@'))) { *a = 0; Q_strncatz(data, va("\\prx\\%s", cls.servername), sizeof(data)); *a = '@'; } if (connectinfo.spec==CIS_OBSERVE) Q_strncatz(data, "\\spectator\\1", sizeof(data)); //the info itself { static const char *prioritykeys[] = {"name", "password", "spectator", "lang", "rate", "team", "topcolor", "bottomcolor", "skin", "_", "*", NULL}; const char *ignorekeys[] = {"prx", "*z_ext", (connectinfo.spec!=CIS_DEFAULT)?"spectator":NULL, NULL}; InfoBuf_ToString(&cls.userinfo[0], data+strlen(data), sizeof(data)-strlen(data), prioritykeys, ignorekeys, NULL, &cls.userinfosync, &cls.userinfo[0]); } if (connectinfo.protocol == CP_QUAKEWORLD) //zquake extension info. Q_strncatz(data, va("\\*z_ext\\%i", CLIENT_SUPPORTED_Z_EXTENSIONS), sizeof(data)); Q_strncatz(data, "\"", sizeof(data)); if (connectinfo.protocol == CP_QUAKE2 && connectinfo.subprotocol == PROTOCOL_VERSION_R1Q2) Q_strncatz(data, va(" %d %d", connectinfo.ext.mtu, 1905), sizeof(data)); //mti, sub-sub-version else if (connectinfo.protocol == CP_QUAKE2 && connectinfo.subprotocol == PROTOCOL_VERSION_Q2PRO) Q_strncatz(data, va(" %d 0 0 %d", connectinfo.ext.mtu, 1021), sizeof(data)); //mtu, netchan-fragmentation, zlib, sub-sub-version Q_strncatz(data, "\n", sizeof(data)); } if (connectinfo.ext.fte1) Q_strncatz(data, va("0x%x 0x%x\n", PROTOCOL_VERSION_FTE1, connectinfo.ext.fte1), sizeof(data)); if (connectinfo.ext.fte2) Q_strncatz(data, va("0x%x 0x%x\n", PROTOCOL_VERSION_FTE2, connectinfo.ext.fte2), sizeof(data)); if (connectinfo.ext.ez1) Q_strncatz(data, va("0x%x 0x%x\n", PROTOCOL_VERSION_EZQUAKE1, connectinfo.ext.ez1), sizeof(data)); { int ourmtu; if (to->type == NA_LOOPBACK) ourmtu = MAX_UDP_PACKET; else if (*net_mtu.string) ourmtu = net_mtu.ival; else ourmtu = 1440; //a safe bet. servers have an unsafe bet by default if (ourmtu < 0) ourmtu = 0; if (connectinfo.ext.mtu > ourmtu) connectinfo.ext.mtu = ourmtu; connectinfo.ext.mtu &= ~7; if (connectinfo.ext.mtu > 0) Q_strncatz(data, va("0x%x %i\n", PROTOCOL_VERSION_FRAGMENT, connectinfo.ext.mtu), sizeof(data)); } #ifdef HUFFNETWORK if (connectinfo.ext.compresscrc && net_compress.ival && Huff_CompressionCRC(connectinfo.ext.compresscrc)) Q_strncatz(data, va("0x%x 0x%x\n", PROTOCOL_VERSION_HUFFMAN, LittleLong(connectinfo.ext.compresscrc)), sizeof(data)); else #endif connectinfo.ext.compresscrc = 0; info = CL_GUIDString(to); if (info) Q_strncatz(data, va("0x%x \"%s\"\n", PROTOCOL_INFO_GUID, info), sizeof(data)); NET_SendPacket (cls.sockets, strlen(data), data, to); } char *CL_TryingToConnect(void) { if (!connectinfo.trying) return NULL; if (connectinfo.numadr < 1) ; else if (connectinfo.adr[0].prot == NP_KEXLAN #ifdef SUPPORT_ICE || connectinfo.adr[0].type == NA_ICE #endif ) { char status[1024]; if (NET_GetConnectionCertificate(cls.sockets, &connectinfo.adr[0], QCERT_LOBBYSTATUS, status, sizeof(status))>0) return va("%s\n%s", cls.servername, status); } else if (connectinfo.adr[0].prot == NP_RTC_TCP || connectinfo.adr[0].prot == NP_RTC_TLS) return va("%s\n%s", cls.servername, "Waiting for broker connection"); return cls.servername; } #if defined(NQPROT) && defined(HAVE_SERVER) static void CL_NullReadPacket(void) { //just drop it all } #endif struct resolvectx_s { netadr_t adr[countof(connectinfo.adr)]; size_t found; char servername[1]; /*servername text*/ }; static void CL_ResolvedServer(void *vctx, void *data, size_t a, size_t b) { size_t i; struct resolvectx_s *ctx = vctx; //something screwed us over... if (strcmp(ctx->servername, cls.servername)) return; if (!ctx->found) { CL_ConnectAbort("Unable to resolve server address \"%s\"\n", ctx->servername); return; } #ifdef HAVE_DTLS for (i = 0; i < ctx->found; i++) { if (net_enable_dtls.ival>=4 || connectinfo.mode==CIM_QEONLY)// || (connectinfo.peercred.hash && net_enable_dtls.ival >= 1)) { //if we've already established a dtls connection, stick with it if (ctx->adr[i].prot == NP_DGRAM) ctx->adr[i].prot = NP_DTLS; } } #endif if (connectinfo.mode == CIM_Q2EONLY) { for (i = 0; i < ctx->found; i++) { //if we've already established a dtls connection, stick with it, otherwise 'upgrade' from udp to 'kexlan' transport layer. if (ctx->adr[i].prot == NP_DGRAM) ctx->adr[i].prot = NP_KEXLAN; } } connectinfo.numadr = ctx->found; connectinfo.nextadr = 0; connectinfo.resolving = false; memcpy(connectinfo.adr, ctx->adr, sizeof(*connectinfo.adr)*ctx->found); } static void CL_ResolveServer(void *vctx, void *data, size_t a, size_t b) { struct resolvectx_s *ctx = vctx; //stupid logic for targ@prox2@[ws[s]://]prox1 chaining. just disable it if there's weird ws:// or whatever in there. //FIXME: really shouldn't be in there const char *res = strrchr(ctx->servername, '/'); const char *host = strrchr(ctx->servername+1, '@'); if (host && (!res || res > host)) host++; else host = ctx->servername; ctx->found = NET_StringToAdr2 (host, connectinfo.defaultport, ctx->adr, countof(ctx->adr), NULL); COM_AddWork(WG_MAIN, CL_ResolvedServer, ctx, data, a, b); } qboolean CL_IsPendingServerAddress(netadr_t *adr) { size_t i; for (i = 0; i < connectinfo.numadr; i++) if (NET_CompareAdr(&connectinfo.adr[i], adr)) return true; return false; } static qboolean CL_IsPendingServerBaseAddress(netadr_t *adr) { size_t i; for (i = 0; i < connectinfo.numadr; i++) if (NET_CompareBaseAdr(&connectinfo.adr[i], adr)) return true; return false; } /* ================= CL_CheckForResend Resend a connect message if the last one has timed out ================= */ void CL_CheckForResend (void) { char data[2048]; double t1, t2; int contype = 0; qboolean keeptrying = true; netadr_t *to; #ifdef HAVE_SERVER if (!cls.state && (!connectinfo.trying || sv.state != ss_clustermode) && sv.state) { const char *lbp; #ifdef NQPROT qboolean proquakeangles = false; #endif #ifdef NETPREPARSE extern cvar_t dpcompat_nopreparse; #endif extern cvar_t sv_guidhash; if (connectinfo.time && realtime - connectinfo.time < 1.0) return; memset(&connectinfo, 0, sizeof(connectinfo)); connectinfo.time = realtime; Q_strncpyz (cls.servername, "internalserver", sizeof(cls.servername)); Cvar_ForceSet(&cl_servername, cls.servername); connectinfo.numadr = NET_StringToAdr(cls.servername, 0, &connectinfo.adr[0]); connectinfo.nextadr = 0; if (!connectinfo.numadr) return; //erk? if (*cl_disconnectreason.string) Cvar_Set(&cl_disconnectreason, ""); connectinfo.trying = true; connectinfo.istransfer = false; connectinfo.adr[0].prot = NP_DGRAM; NET_InitClient(sv.state != ss_clustermode); //netchan extensions... we skip the getchallenge part so we need to set these up still. connectinfo.ext.mtu = 8192-16; connectinfo.ext.compresscrc = 0; Q_strncpyz(connectinfo.ext.guidsalt, sv_guidhash.string, sizeof(connectinfo.ext.guidsalt)); cls.state = ca_disconnected; switch (svs.gametype) { #ifdef Q3CLIENT case GT_QUAKE3: connectinfo.protocol = CP_QUAKE3; break; #endif #ifdef Q2CLIENT case GT_QUAKE2: connectinfo.protocol = CP_QUAKE2; lbp = cl_loopbackprotocol.string; if (!strcmp(lbp, "q2")) { //vanilla connectinfo.subprotocol = PROTOCOL_VERSION_Q2; connectinfo.ext.fte1 = 0; } else if (!strcmp(lbp, "q2e") || !strcmp(lbp, STRINGIFY(PROTOCOL_VERSION_Q2EX))) { //no fte extensions //still provides big coords, bigger index sizes, and splitscreen(non-dynamic though) connectinfo.subprotocol = PROTOCOL_VERSION_Q2EX; connectinfo.ext.fte1 = 0; } //else if (!strcmp(lbp, "r1q2") || !strcmp(lbp, STRINGIFY(PROTOCOL_VERSION_R1Q2))) //else if (!strcmp(lbp, "q2pro") || !strcmp(lbp, STRINGIFY(PROTOCOL_VERSION_Q2PRO))) else { connectinfo.subprotocol = PROTOCOL_VERSION_Q2EX; connectinfo.ext.fte1 = 0;//PEXT_MODELDBL|PEXT_SOUNDDBL|PEXT_SPLITSCREEN; } connectinfo.ext.fte2 = 0; connectinfo.ext.ez1 = 0; break; #endif default: cl.movesequence = 0; lbp = cl_loopbackprotocol.string; if (!strcmp(lbp, "") || !strcmp(lbp, "qw") || progstype == PROG_H2) { //qw with all supported extensions -default //for hexen2 we always force fte's native qw protocol. other protocols won't cut it. connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; connectinfo.ext.fte1 = Net_PextMask(PROTOCOL_VERSION_FTE1, false); connectinfo.ext.fte2 = Net_PextMask(PROTOCOL_VERSION_FTE2, false); connectinfo.ext.ez1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, false) & EZPEXT1_CLIENTADVERTISE; } else if (!strcmp(lbp, "qwid") || !strcmp(lbp, "idqw")) { //for recording .qwd files in any client connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; connectinfo.ext.fte1 = 0; connectinfo.ext.fte2 = 0; connectinfo.ext.ez1 = 0; } #ifdef Q3CLIENT else if (!strcmp(lbp, "q3")) cls.protocol = CP_QUAKE3; #endif #ifdef NQPROT else if (!strcmp(lbp, "random")) { //for debugging. if (rand() & 1) { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_FITZ666; } else { connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; connectinfo.ext.fte1 = Net_PextMask(PROTOCOL_VERSION_FTE1, false); connectinfo.ext.fte2 = Net_PextMask(PROTOCOL_VERSION_FTE2, false); connectinfo.ext.ez1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, false) & EZPEXT1_CLIENTADVERTISE; } } else if (!strcmp(lbp, "fitz") || !strcmp(lbp, "rmqe") || !strcmp(lbp, "qs") || !strcmp(lbp, "666") || !strcmp(lbp, "999")) { //we don't really distinguish between fitz and rmq protocols. we just use 999 with bigcoords and 666 othewise. connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_FITZ666; } else if (!strcmp(lbp, "qe")||!strcmp(lbp, "qex")||!strcmp(lbp, "kex")) { //quake-ex has special quirks that cannot be defined by protocol numbers alone. connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_FITZ666; connectinfo.mode = CIM_QEONLY; } else if (!strcmp(lbp, "bjp1") || !strcmp(lbp, "bjp2") || //placeholders only !strcmp(lbp, "bjp3") || !strcmp(lbp, "bjp")) { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_BJP3; } else if (!strcmp(lbp, "nq")) { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_ID; proquakeangles = true; } else if (!strcmp(lbp, "nqid") || !strcmp(lbp, "idnq")) { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_ID; } else if (!strcmp(lbp, "dp1") || !strcmp(lbp, "dpp1")|| //most of these are not supported, but parsed as placeholders on the slim chance that we ever do support them !strcmp(lbp, "dp2") || !strcmp(lbp, "dpp2")|| !strcmp(lbp, "dp3") || !strcmp(lbp, "dpp3")|| !strcmp(lbp, "dp4") || !strcmp(lbp, "dpp4")|| !strcmp(lbp, "dp5") || !strcmp(lbp, "dpp5")|| //we support this serverside, but not clientside. !strcmp(lbp, "dp6") || !strcmp(lbp, "dpp6")) //this one is supported. { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_DP6; } else if (!strcmp(lbp, "dp7") || !strcmp(lbp, "dpp7") || !strcmp(lbp, "dp") || !strcmp(lbp, "xonotic")) //family name, common usage. { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_DP7; } else if (!strcmp(lbp, "qss") || (progstype != PROG_QW && progstype != PROG_H2 && sv.state!=ss_clustermode && cl_splitscreen.ival <= 0)) //h2 depends on various extensions and doesn't really match either protocol, but we go for qw because that gives us all sorts of extensions. { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_FITZ666; connectinfo.ext.fte1 = Net_PextMask(PROTOCOL_VERSION_FTE1, true); connectinfo.ext.fte2 = Net_PextMask(PROTOCOL_VERSION_FTE2, true); connectinfo.ext.ez1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, true) & EZPEXT1_CLIENTADVERTISE; } #endif else { //protocol wasn't recognised, and we didn't take the nq fallback, so that must mean we're going for qw. connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; connectinfo.ext.fte1 = Net_PextMask(PROTOCOL_VERSION_FTE1, false); connectinfo.ext.fte2 = Net_PextMask(PROTOCOL_VERSION_FTE2, false); connectinfo.ext.ez1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, false) & EZPEXT1_CLIENTADVERTISE; } #ifdef NETPREPARSE if (dpcompat_nopreparse.ival) #endif { //disabling preparsing with hexen2 is unsupported. if (progstype == PROG_H2) Con_Printf("dpcompat_nopreparse is unsupported with hexen2\n"); else if (progstype == PROG_QW && cls.protocol != CP_QUAKEWORLD) { connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; connectinfo.ext.fte1 = Net_PextMask(PROTOCOL_VERSION_FTE1, false); connectinfo.ext.fte2 = Net_PextMask(PROTOCOL_VERSION_FTE2, false); connectinfo.ext.ez1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, false) & EZPEXT1_CLIENTADVERTISE; } else if (progstype != PROG_QW && cls.protocol == CP_QUAKEWORLD) { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_DP7; //dpcompat_nopreparse is only really needed for DP mods that send unknowable svc_tempentity messages to the client. } } //make sure the protocol within demos is actually correct/sane if (cls.demorecording == DPB_QUAKEWORLD && cls.protocol != CP_QUAKEWORLD) { connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; connectinfo.ext.fte1 = Net_PextMask(PROTOCOL_VERSION_FTE1, false); connectinfo.ext.fte2 = Net_PextMask(PROTOCOL_VERSION_FTE2, false); connectinfo.ext.ez1 = Net_PextMask(PROTOCOL_VERSION_EZQUAKE1, false) & EZPEXT1_CLIENTADVERTISE; } #ifdef NQPROT else if (cls.demorecording == DPB_NETQUAKE && cls.protocol != CP_NETQUAKE) { connectinfo.protocol = CP_NETQUAKE; connectinfo.subprotocol = CPNQ_FITZ666; //FIXME: use pext. } #endif #ifdef Q2CLIENT else if (cls.demorecording == DPB_QUAKE2 && cls.protocol != CP_QUAKE2) { connectinfo.protocol = CP_QUAKE2; connectinfo.subprotocol = PROTOCOL_VERSION_Q2; connectinfo.ext.fte1 = PEXT_MODELDBL|PEXT_SOUNDDBL|PEXT_SPLITSCREEN; //FIXME: use pext. } #endif break; } CL_FlushClientCommands(); //clear away all client->server clientcommands. #ifdef NQPROT if (connectinfo.protocol == CP_NETQUAKE) { connectinfo.numadr = NET_StringToAdr2 (cls.servername, connectinfo.defaultport, connectinfo.adr, 1, NULL); connectinfo.nextadr = 0; if (!connectinfo.numadr) { CL_ConnectAbort("CL_CheckForResend: Bad server address \"%s\"\n", cls.servername); return; } NET_AdrToString(data, sizeof(data), &connectinfo.adr[connectinfo.nextadr]); /*eat up the server's packets, to clear any lingering loopback packets (like disconnect commands... yes this might cause packetloss for other clients)*/ svs.sockets->ReadGamePacket = CL_NullReadPacket; NET_ReadPackets(svs.sockets); svs.sockets->ReadGamePacket = SV_ReadPacket; net_message.packing = SZ_RAWBYTES; net_message.cursize = 0; MSG_BeginReading(&net_message, net_message.prim); if (connectinfo.mode == CIM_QEONLY) { net_from = connectinfo.adr[connectinfo.nextadr]; Cmd_TokenizeString (va("connect %i %i %i \"\\name\\unconnected\"", NQ_NETCHAN_VERSION_QEX, 0, SV_NewChallenge()), false, false); SVC_DirectConnect(0); } else if (connectinfo.subprotocol == CPNQ_ID && !proquakeangles) { net_from = connectinfo.adr[connectinfo.nextadr]; Cmd_TokenizeString (va("connect %i %i %i \"\\name\\unconnected\"", NQ_NETCHAN_VERSION, 0, SV_NewChallenge()), false, false); SVC_DirectConnect(0); } else if (connectinfo.subprotocol == CPNQ_BJP3) { net_from = connectinfo.adr[connectinfo.nextadr]; Cmd_TokenizeString (va("connect %i %i %i \"\\name\\unconnected\\mod\\%i\"", NQ_NETCHAN_VERSION, 0, SV_NewChallenge(), PROTOCOL_VERSION_BJP3), false, false); SVC_DirectConnect(0); } else if (connectinfo.subprotocol == CPNQ_FITZ666) { net_from = connectinfo.adr[connectinfo.nextadr]; Cmd_TokenizeString (va("connect %i %i %i \"\\name\\unconnected\\mod\\%i\"", NQ_NETCHAN_VERSION, 0, SV_NewChallenge(), PROTOCOL_VERSION_FITZ), false, false); SVC_DirectConnect(0); } else if (proquakeangles) { net_from = connectinfo.adr[connectinfo.nextadr]; Cmd_TokenizeString (va("connect %i %i %i \"\\name\\unconnected\\mod\\1\"", NQ_NETCHAN_VERSION, 0, SV_NewChallenge()), false, false); SVC_DirectConnect(0); } else if (1) { net_from = connectinfo.adr[connectinfo.nextadr]; Q_snprintfz(net_message.data, net_message.maxsize, "xxxxconnect\\protocol\\darkplaces "STRINGIFY(NQ_NETCHAN_VERSION)"\\protocols\\DP7 DP6 DP5 RMQ FITZ NEHAHRABJP2 NEHAHRABJP NEHAHRABJP3 QUAKE\\challenge\\0x%x\\name\\%s", SV_NewChallenge(), name.string); Cmd_TokenizeString (net_message.data+4, false, false); SVC_DirectConnect(0); } else CL_ConnectToDarkPlaces("", &connectinfo.adr[connectinfo.nextadr]); // connectinfo.trying = false; } else #endif { if (!connectinfo.challenge) connectinfo.challenge = rand(); CL_SendConnectPacket (NULL); } return; } #endif if (!connectinfo.trying) { if (*cl_servername.string) Cvar_ForceSet(&cl_servername, ""); return; } if (startuppending || r_blockvidrestart || FS_DownloadingPackage()) return; //don't send connect requests until we've actually initialised fully. this isn't a huge issue, but makes the startup prints a little more sane. if (connectinfo.time && realtime - connectinfo.time < 5.0) { if (!connectinfo.clogged) return; } else connectinfo.clogged = false; //do the prints and everything. if (!cls.sockets) //only if its needed... we don't want to keep using a new port unless we have to NET_InitClient(false); #ifdef HAVE_DTLS if (connectinfo.numadr>0 && connectinfo.adr[0].prot == NP_DTLS) { //get through the handshake first, instead of waiting for a 5-sec timeout between polls. switch(NET_SendPacket (cls.sockets, 0, NULL, &connectinfo.adr[0])) { case NETERR_CLOGGED: //temporary failure connectinfo.clogged = true; return; case NETERR_DISCONNECTED: CL_ConnectAbort("DTLS Certificate Verification Failure\n"); break; case NETERR_NOROUTE: //not an error here, just means we need to send a new handshake. break; default: break; } } #endif t1 = Sys_DoubleTime (); if (!connectinfo.istransfer) { if ((!connectinfo.numadr||connectinfo.nextadr>connectinfo.numadr*60) && !connectinfo.resolving) { struct resolvectx_s *rctx = Z_Malloc(sizeof(*rctx) + strlen(cls.servername)); strcpy(rctx->servername, cls.servername); connectinfo.resolving = true; COM_AddWork(WG_LOADER, CL_ResolveServer, rctx, NULL, 0, 0); } } CL_FlushClientCommands(); t2 = Sys_DoubleTime (); Cvar_ForceSet(&cl_servername, cls.servername); if (!connectinfo.numadr || !cls.sockets || connectinfo.resolving) return; //nothing to do yet... if (!connectinfo.clogged) connectinfo.time = realtime+t2-t1; // for retransmit requests to = &connectinfo.adr[connectinfo.nextadr%connectinfo.numadr]; if (!NET_IsClientLegal(to)) { CL_ConnectAbort ("Illegal server address\n"); return; } if (!connectinfo.clogged) { #ifdef Q3CLIENT if (q3) q3->cl.SendAuthPacket(cls.sockets, to); #endif if ((connectinfo.istransfer || connectinfo.numadr>1) && to->prot != NP_RTC_TCP && to->prot != NP_RTC_TLS #ifdef SUPPORT_ICE && to->type != NA_ICE #endif ) Con_TPrintf ("Connecting to %s" S_COLOR_GRAY "(%s)" S_COLOR_WHITE "...\n", cls.servername, NET_AdrToString(data, sizeof(data), to)); else Con_TPrintf ("Connecting to %s...\n", cls.servername); } if (connectinfo.clogged) connectinfo.clogged = false; if (connectinfo.tries == 0 && connectinfo.nextadr < connectinfo.numadr) { //stupid logic for targ@prox2@[ws[s]://]prox1 chaining. just disable it if there's weird ws:// or whatever in there. //FIXME: really shouldn't be in there const char *res = strrchr(cls.servername, '/'); const char *host = strrchr(cls.servername+1, '@'); if (host && (!res || res > host)) host++; else host = cls.servername; if (!NET_EnsureRoute(cls.sockets, "conn", &connectinfo.peercred, host, to, true)) { CL_ConnectAbort ("Unable to establish connection to %s\n", cls.servername); return; } } if (to->prot == NP_DGRAM) connectinfo.nextadr++; //cycle hosts with each ping (if we got multiple). if (connectinfo.mode==CIM_Q2EONLY) contype |= 1; //don't ever try nq packets here. else if (connectinfo.mode==CIM_QEONLY || connectinfo.mode==CIM_NQONLY) contype |= 2; else { contype |= 1; /*always try qw type connections*/ #ifdef VM_UI if (!(q3&&q3->ui.IsRunning())) //don't try to connect to nq servers when running a q3ui. I was getting annoying error messages from q3 servers due to this. #endif { COM_Parse(com_protocolname.string); if (!strcmp(com_token, "Quake2")) { if (connectinfo.nextadr>3) //don't create an extra channel until we know our preferred one has failed. contype |= 4; /*q2e's kex lan layer*/ } else contype |= 2; /*try nq connections periodically (or if its the default nq port)*/ } } /*DP, QW, Q2, Q3*/ /*NOTE: ioq3 has args. yes, a challenge to get a challenge.*/ if (contype & 1) { char tmp[256]; if (strrchr(cls.servername, '@')) //if we're apparently using qwfwd then strictly adhere to vanilla's protocol so that qwfwd does not bug out. this will pollute gamedirs if stuff starts autodownloading from the wrong type of server, and probably show up vanilla-vs-rerelease glitches everywhere.. Q_snprintfz (data, sizeof(data), "%c%c%c%cgetchallenge\n", 255, 255, 255, 255); else Q_snprintfz (data, sizeof(data), "%c%c%c%cgetchallenge %i %s\n", 255, 255, 255, 255, connectinfo.clchallenge, COM_QuotedString(com_protocolname.string, tmp, sizeof(tmp), false)); switch(NET_SendPacket (cls.sockets, strlen(data), data, to)) { case NETERR_CLOGGED: //temporary failure connectinfo.clogged = true; //inhibits the wait between sends break; case NETERR_SENT: //yay, works! break; default: keeptrying = false; break; } } /*NQ*/ #ifdef NQPROT if ((contype & 2) && !connectinfo.clogged) { char *e; int pwd; sizebuf_t sb; memset(&sb, 0, sizeof(sb)); sb.data = data; sb.maxsize = sizeof(data); MSG_WriteLong(&sb, LongSwap(NETFLAG_CTL | (strlen(NQ_NETCHAN_GAMENAME)+7))); MSG_WriteByte(&sb, CCREQ_CONNECT); MSG_WriteString(&sb, NQ_NETCHAN_GAMENAME); if (connectinfo.mode==CIM_QEONLY) MSG_WriteByte(&sb, NQ_NETCHAN_VERSION_QEX); else { MSG_WriteByte(&sb, NQ_NETCHAN_VERSION); /*NQ engines have a few extra bits on the end*/ /*proquake servers wait for us to send them a packet before anything happens, which means it corrects for our public port if our nat uses different public ports for different remote ports thus all nq engines claim to be proquake */ if (!*password.string || !strcmp(password.string, "none")) pwd = 0; else { pwd = strtol(password.string, &e, 0); if (*e) pwd = CalcHashInt(&hash_md4, password.string, strlen(password.string)); } MSG_WriteByte(&sb, MOD_PROQUAKE); /*'mod'*/ MSG_WriteByte(&sb, 34); /*'mod' version*/ MSG_WriteByte(&sb, 0); /*flags*/ MSG_WriteLong(&sb, pwd); /*password*/ /*FTE servers will detect this string and treat it as a qw challenge instead (if it allows qw clients), so protocol choice is deterministic*/ if (contype & 1) { char tmp[256]; MSG_WriteString(&sb, va("getchallenge %i %s\n", connectinfo.clchallenge, COM_QuotedString(com_protocolname.string, tmp, sizeof(tmp), false))); } } *(int*)sb.data = LongSwap(NETFLAG_CTL | sb.cursize); switch(NET_SendPacket (cls.sockets, sb.cursize, sb.data, to)) { case NETERR_CLOGGED: //temporary failure case NETERR_SENT: //yay, works! break; default: keeptrying = false; break; } } #endif #ifdef Q2CLIENT if ((contype & 4) && !connectinfo.clogged) { #define KEXLAN_SHAMELESSSELFPROMOMAGIC "\x08""CRANTIME" //hey, if you can't shove your own nick in your network protocols then you're doing it wrong. #define KEXLAN_SUBPROTOCOL "\x08""Quake II" //this should be cvar-ised at some point, if its to ever be useful for anything but q2. static char pkt[] = "\x01\x60\x80"KEXLAN_SHAMELESSSELFPROMOMAGIC KEXLAN_SUBPROTOCOL"\x01"; NET_SendPacket (cls.sockets, strlen(pkt), pkt, to); } #endif connectinfo.tries++; if (!keeptrying) { if (to->prot != NP_DGRAM && connectinfo.nextadr+1 < connectinfo.numadr) { connectinfo.nextadr++; //cycle hosts with each connection failure (if we got multiple addresses). connectinfo.tries = 0; } else { CL_ConnectAbort ("Unable to connect to %s, giving up\n", cls.servername); NET_CloseClient(); } } } static void CL_BeginServerConnect(char *chain, int port, qboolean noproxy, enum coninfomode_e mode, enum coninfospec_e spec) { const char *host = chain; const char *schemeend; char *arglist, *c; size_t presize; Q_strncpyz(cls.serverurl, chain, sizeof(cls.serverurl)); for (c = chain; *c; c++) { if (*c == '@') host=c+1; else if (*c == '/' || *c == '?') break; //stop if we find some path weirdness (like an authority). } presize = host-chain; if (presize >= sizeof(cls.servername)) { CL_ConnectAbort("server address too long"); return; //no, get lost. panic. } memcpy(cls.servername, chain, presize); schemeend = strstr(host, "://"); if (schemeend) { //"qw:tcp://host/observe" const char *schemestart = strchr(host, ':'); const struct urischeme_s *scheme; if (!schemestart || schemestart==schemeend) schemestart = host; else schemestart++; //the scheme is either a network scheme in which case we use it directly, or a game-specific scheme. scheme = NET_IsURIScheme(schemestart); if (scheme && scheme->prot == NP_INVALID) scheme = NULL; //qw:// or q3:// something that's just noise here. if (scheme && scheme->flags&URISCHEME_NEEDSRESOURCE) { Q_strncpyz (cls.servername+presize, schemestart, sizeof(cls.servername)-presize); //oh. will probably be okay then arglist = NULL; } else { //not some '/foo' name, not rtc:// either... char *sl = strchr(schemeend+3, '/'); if (sl) { if (!strncmp(sl, "/observe", 8)) { if (spec == CIS_DEFAULT) spec = CIS_OBSERVE; else if (spec != CIS_OBSERVE) Con_Printf("Ignoring 'observe'\n"); memmove(sl, sl+8, strlen(sl+8)+1); } else if (!strncmp(sl, "/join", 5)) { if (spec == CIS_DEFAULT) spec = CIS_JOIN; else if (spec != CIS_OBSERVE) Con_Printf("Ignoring 'join'\n"); memmove(sl, sl+5, strlen(sl+5)+1); } else if (!strncmp(sl, "/qtvplay", 5)) { char buf[256]; *sl = 0; Cmd_ExecuteString(va("qtvplay %s\n", COM_QuotedString(schemeend+3, buf,sizeof(buf), false)), RESTRICT_LOCAL); return; } else if (!strncmp(sl, "/", 1) && (sl[1] == 0 || sl[1]=='?')) { //current spectator mode memmove(sl, sl+1, strlen(sl+1)+1); } } if (scheme) //preserve the scheme, the netchan cares. Q_strncpyz (cls.servername+presize, schemestart, sizeof(cls.servername)-presize); //probably some game-specific mess that we don't know else Q_strncpyz (cls.servername+presize, schemeend+3, sizeof(cls.servername)-presize); //probably some game-specific mess that we don't know arglist = strchr(cls.servername, '?'); } } else { if (!strncmp(host, "localhost", 9)) noproxy = true; //FIXME: resolve the address here or something so that we don't end up using a proxy for lan addresses. if (strstr(host, "://") || *host == '/' || !*cl_proxyaddr.string || noproxy) Q_strncpyz (cls.servername+presize, host, sizeof(cls.servername)-presize); else Q_snprintfz(cls.servername+presize, sizeof(cls.servername)-presize, "%s@%s", host, cl_proxyaddr.string); arglist = strchr(cls.servername, '?'); } if (!port) port = cl_defaultport.value; CL_ConnectAbort(NULL); memset(&connectinfo, 0, sizeof(connectinfo)); if (*cl_disconnectreason.string) Cvar_Set(&cl_disconnectreason, ""); connectinfo.trying = true; connectinfo.defaultport = port; connectinfo.protocol = CP_UNKNOWN; connectinfo.mode = mode; connectinfo.spec = spec; connectinfo.clchallenge = rand()^(rand()<<16); connectinfo.peercred.name = cls.servername; if (arglist) { *arglist++ = 0; while (*arglist) { char *e = strchr(arglist, '&'); if (e) *e=0; if (!Q_strncasecmp(arglist, "fp=", 3)) { size_t l = 8*Base64_DecodeBlock(arglist+3, arglist+strlen(arglist), connectinfo.peercred.digest, sizeof(connectinfo.peercred.digest)); if (l <= 160) connectinfo.peercred.hash = &hash_sha1; else if (l <= 256) connectinfo.peercred.hash = &hash_sha2_256; else if (l <= 512) connectinfo.peercred.hash = &hash_sha2_512; else connectinfo.peercred.hash = NULL; } else Con_Printf(CON_WARNING"uri arg not known: \"%s\"\n", arglist); if (e) arglist=e+1; else break; } } SCR_SetLoadingStage(LS_CONNECTION); CL_CheckForResend(); } void CL_BeginServerReconnect(void) { #ifdef HAVE_SERVER if (isDedicated) { Con_TPrintf ("Connect ignored - dedicated. set a renderer first\n"); return; } #endif #ifdef HAVE_DTLS { int i; for (i = 0; i < connectinfo.numadr; i++) NET_DTLS_Disconnect(cls.sockets, &connectinfo.adr[i]); } #endif #ifdef SUPPORT_ICE while (connectinfo.numadr) //remove any ICE addresses. probably we'll end up with no addresses left leaving us free to re-resolve giving us the original(ish) rtc connection. { if (connectinfo.adr[connectinfo.numadr-1].type != NA_ICE) break; connectinfo.numadr--; } #endif if (*cl_disconnectreason.string) Cvar_Set(&cl_disconnectreason, ""); connectinfo.trying = true; connectinfo.istransfer = false; connectinfo.time = 0; connectinfo.tries = 0; //re-ensure routes. connectinfo.nextadr = 0; //should at least be consistent, other than packetloss. yay. connectinfo.clchallenge = rand()^(rand()<<16); NET_InitClient(false); } void CL_Transfer(netadr_t *adr) { connectinfo.adr[0] = *adr; connectinfo.numadr = 1; connectinfo.istransfer = true; CL_CheckForResend(); } void CL_Transfer_f(void) { char oldguid[64]; char *server; if (Cmd_Argc() != 2) { Con_TPrintf ("usage: cl_transfer \n"); return; } CL_ConnectAbort(NULL); server = Cmd_Argv (1); if (!*server) { //if they didn't specify a server, abort any active transfer/connection. return; } Q_strncpyz(oldguid, connectinfo.guid, sizeof(oldguid)); memset(&connectinfo, 0, sizeof(connectinfo)); connectinfo.numadr = NET_StringToAdr(server, 0, &connectinfo.adr[0]); if (connectinfo.numadr) { connectinfo.istransfer = true; Q_strncpyz(connectinfo.guid, oldguid, sizeof(oldguid)); //retain the same guid on transfers Cvar_Set(&cl_disconnectreason, "Transferring...."); connectinfo.trying = true; connectinfo.defaultport = cl_defaultport.value; connectinfo.protocol = CP_UNKNOWN; SCR_SetLoadingStage(LS_CONNECTION); CL_CheckForResend(); } else { Con_Printf("cl_transfer: bad address\n"); } } /* ================ CL_Connect_f ================ */ void CL_Connect_c(int argn, const char *partial, struct xcommandargcompletioncb_s *ctx); static void CL_Connect_f (void) { char *server; if (Cmd_Argc() != 2) { Con_TPrintf ("usage: connect \n"); return; } server = Cmd_Argv (1); server = strcpy(alloca(strlen(server)+1), server); /*#ifdef HAVE_SERVER if (sv.state == ss_clustermode) CL_Disconnect (NULL); else #endif*/ CL_Disconnect_f (); CL_BeginServerConnect(server, 0, false, CIM_DEFAULT, CIS_DEFAULT); } #if defined(CL_MASTER) && defined(HAVE_PACKET) static void CL_ConnectBestRoute_f (void) { char server[1024]; int proxies; int directcost, chainedcost; if (Cmd_Argc() != 2) { Con_TPrintf ("usage: connectbr \n"); return; } proxies = Master_FindBestRoute(Cmd_Argv(1), server, sizeof(server), &directcost, &chainedcost); if (!*server) { Con_TPrintf ("Unable to route to server\n"); return; } else if (proxies < 0) Con_TPrintf ("Routing database is not initialised, connecting directly\n"); else if (!proxies) Con_TPrintf ("Routing table favours a direct connection\n"); else if (proxies == 1) Con_TPrintf ("Routing table favours a single proxy (%ims vs %ims)\n", chainedcost, directcost); else Con_TPrintf ("Routing table favours chaining through %i proxies (%ims vs %ims)\n", proxies, chainedcost, directcost); /*#ifdef HAVE_SERVER if (sv.state == ss_clustermode) CL_Disconnect (NULL); else #endif*/ CL_Disconnect_f (); CL_BeginServerConnect(server, 0, true, CIM_DEFAULT, CIS_DEFAULT); } #endif static void CL_Join_f (void) { char *server; if (Cmd_Argc() != 2) { if (cls.state) { //Hmm. This server sucks. if ((cls.z_ext & Z_EXT_JOIN_OBSERVE) || cls.protocol != CP_QUAKEWORLD) Cmd_ForwardToServer(); else Cbuf_AddText("\nspectator 0;reconnect\n", RESTRICT_LOCAL); return; } Con_Printf ("join requires a connection or servername/ip\n"); return; } server = Cmd_Argv (1); server = strcpy(alloca(strlen(server)+1), server); CL_Disconnect_f (); CL_BeginServerConnect(server, 0, false, CIM_DEFAULT, CIS_JOIN); } void CL_Observe_f (void) { char *server; if (Cmd_Argc() != 2) { if (cls.state) { if ((cls.z_ext & Z_EXT_JOIN_OBSERVE) || cls.protocol != CP_QUAKEWORLD) Cmd_ForwardToServer(); else //Hmm. This server sucks. Cbuf_AddText("\nspectator 1;reconnect\n", RESTRICT_LOCAL); return; } Con_Printf ("observe requires a connection or servername/ip\n"); return; } server = Cmd_Argv (1); server = strcpy(alloca(strlen(server)+1), server); CL_Disconnect_f (); Cvar_Set(&spectator, "1"); CL_BeginServerConnect(server, 0, false, CIM_DEFAULT, CIS_OBSERVE); } #ifdef NQPROT void CLNQ_Connect_f (void) { char *server; enum coninfomode_e mode; int port = 26000; if (Cmd_Argc() != 2) { Con_TPrintf ("usage: connect \n"); return; } if (!strcmp(Cmd_Argv(0), "connectqe")) mode = CIM_QEONLY; else mode = CIM_NQONLY; server = Cmd_Argv (1); server = strcpy(alloca(strlen(server)+1), server); CL_Disconnect_f (); CL_BeginServerConnect(server, port, true, mode, CIS_DEFAULT/*doesn't really do spec/join stuff, but if the server asks for our info later...*/); } #endif #ifdef Q2CLIENT void CLQ2E_Connect_f (void) { char *server; if (Cmd_Argc() != 2) { Con_TPrintf ("usage: connect \n"); return; } server = Cmd_Argv (1); server = strcpy(alloca(strlen(server)+1), server); CL_Disconnect_f (); CL_BeginServerConnect(server, PORT_Q2EXSERVER/*q2e servers ignore their own port cvar, so don't use the standard q2 port number here*/, true, CIM_Q2EONLY, CIS_DEFAULT); } #endif #ifdef IRCCONNECT void CL_IRCConnect_f (void) { CL_Disconnect_f (); if (FTENET_AddToCollection(cls.sockets, "TCP", Cmd_Argv(2), NA_IP, NP_IRC, false)) { char *server; server = Cmd_Argv (1); CL_BeginServerConnect(va("irc://%s", server), 0, true); } } #endif #ifdef TCPCONNECT void CL_TCPConnect_f (void) { if (!Q_strcasecmp(Cmd_Argv(0), "tlsconnect")) Cbuf_InsertText(va("connect tls://%s", Cmd_Argv(1)), Cmd_ExecLevel, true); else Cbuf_InsertText(va("connect tcp://%s", Cmd_Argv(1)), Cmd_ExecLevel, true); } #endif /* ===================== CL_Rcon_f Send the rest of the command line over as an unconnected command. ===================== */ void CL_Rcon_f (void) { char message[1024]; char *password; int i; netadr_t to; i = 1; password = rcon_password.string; if (!*password) //FIXME: this is strange... { if (Cmd_Argc() < 3) { Con_TPrintf ("'rcon_password' is not set.\n"); Con_TPrintf("usage: rcon (password) \n"); return; } password = Cmd_Argv(1); i = 2; } else { if (Cmd_Argc() < 2) { Con_Printf("usage: rcon \n"); return; } } message[0] = (char)255; message[1] = (char)255; message[2] = (char)255; message[3] = (char)255; message[4] = 0; Q_strncatz (message, "rcon ", sizeof(message)); if (cl_crypt_rcon.ival) { char cryptpass[1024], crypttime[64]; const char *hex = "0123456789ABCDEF"; //must be upper-case for compat with mvdsv. time_t clienttime = time(NULL); size_t digestsize; unsigned char digest[64]; const unsigned char **tokens = alloca(sizeof(*tokens)*(4+Cmd_Argc()*2)); size_t *tokensizes = alloca(sizeof(*tokensizes)*(4+Cmd_Argc()*2)); int j, k; void *ctx = alloca(hash_sha1.contextsize); for (j = 0; j < sizeof(time_t); j++) { //little-endian byte order, but big-endian nibble order. just screwed. for compat with ezquake. crypttime[j*2+0] = hex[(clienttime>>(j*8+4))&0xf]; crypttime[j*2+1] = hex[(clienttime>>(j*8))&0xf]; } crypttime[j*2] = 0; tokens[0] = "rcon "; tokens[1] = password; tokens[2] = crypttime; tokens[3] = " "; for (j=0 ; j>4]; cryptpass[j*2+1] = hex[digest[j]&0xf]; } cryptpass[j*2] = 0; Q_strncatz (message, cryptpass, sizeof(message)); Q_strncatz (message, crypttime, sizeof(message)); } else Q_strncatz (message, password, sizeof(message)); Q_strncatz (message, " ", sizeof(message)); for ( ; i= ca_connected) to = cls.netchan.remote_address; else { if (!strlen(rcon_address.string)) { Con_TPrintf ("You must either be connected,\nor set the 'rcon_address' cvar\nto issue rcon commands\n"); return; } if (!NET_StringToAdr (rcon_address.string, PORT_QWSERVER, &to)) { Con_Printf("Unable to resolve target address\n"); return; } } NET_SendPacket (cls.sockets, strlen(message)+1, message, &to); } void CL_BlendFog(fogstate_t *result, fogstate_t *oldf, float time, fogstate_t *newf) { float nfrac; if (time >= newf->time) nfrac = 1; else if (time < oldf->time) nfrac = 0; else nfrac = (time - oldf->time) / (newf->time - oldf->time); FloatInterpolate(oldf->alpha, nfrac, newf->alpha, result->alpha); FloatInterpolate(oldf->depthbias, nfrac, newf->depthbias, result->depthbias); FloatInterpolate(oldf->density, nfrac, newf->density, result->density); //this should be non-linear, but that sort of maths is annoying. VectorInterpolate(oldf->colour, nfrac, newf->colour, result->colour); result->time = time; } void CL_ResetFog(int ftype) { //blend from the current state, not the old state. this means things work properly if we've not reached the new state yet. CL_BlendFog(&cl.oldfog[ftype], &cl.oldfog[ftype], realtime, &cl.fog[ftype]); //reset the new state to defaults, to be filled in by the caller. memset(&cl.fog[ftype], 0, sizeof(cl.fog[ftype])); cl.fog[ftype].time = realtime; cl.fog[ftype].density = 0; cl.fog[ftype].colour[0] = //SRGBf(0.3); cl.fog[ftype].colour[1] = //SRGBf(0.3); cl.fog[ftype].colour[2] = SRGBf(0.3); cl.fog[ftype].alpha = 1; cl.fog[ftype].depthbias = 0; /* cl.fog[ftype].end = 16384; cl.fog[ftype].height = 1<<30; cl.fog[ftype].fadedepth = 128; */ } static void CL_ReconfigureCommands(int newgame) { static int oldgame = ~0; extern void SCR_SizeUp_f (void); //cl_screen extern void SCR_SizeDown_f (void); //cl_screen #ifdef QUAKESTATS extern void IN_Weapon (void); //cl_input extern void IN_FireDown (void); //cl_input extern void IN_FireUp (void); //cl_input extern void IN_WWheelDown (void); extern void IN_WWheelUp (void); extern void IN_IWheelDown (void); extern void IN_IWheelUp (void); extern void IN_WeapNext_f (void); extern void IN_WeapPrev_f (void); #endif extern void CL_Say_f (void); extern void CL_SayTeam_f (void); extern void CL_Color_f (void); static const struct { const char *name; void (*func) (void); const char *description; unsigned int problemgames; //1<next; Z_Free(t); } if (!gamestart) { downloadlist_t *next; while(cl.downloadlist) { next = cl.downloadlist->next; Z_Free(cl.downloadlist); cl.downloadlist = next; } while(cl.faileddownloads) { next = cl.faileddownloads->next; Z_Free(cl.faileddownloads); cl.faileddownloads = next; } } pendingdownloads = cl.downloadlist; faileddownloads = cl.faileddownloads; #ifdef Q2CLIENT for (i = 0; i < countof(cl.configstring_general); i++) { if (cl.configstring_general[i]) Z_Free(cl.configstring_general[i]); } #endif for (i = 0; i < MAX_SPLITS; i++) { for (j = 0; j < MAX_CL_STATS; j++) if (cl.playerview[i].statsstr[j]) Z_Free(cl.playerview[i].statsstr[j]); } Z_Free(cl.windowtitle); Z_Free(cl.serverpacknames); Z_Free(cl.serverpackhashes); InfoBuf_Clear(&cl.serverinfo, true); for (i = 0; i < MAX_CLIENTS; i++) InfoBuf_Clear(&cl.players[i].userinfo, true); // wipe the entire cl structure memset (&cl, 0, sizeof(cl)); CL_ResetFog(FOGTYPE_AIR); CL_ResetFog(FOGTYPE_WATER); CL_ResetFog(FOGTYPE_SKYROOM); cl.mapstarttime = realtime; cl.gamespeed = 1; cl.protocol_qw = PROTOCOL_VERSION_QW; //until we get an svc_serverdata cl.allocated_client_slots = QWMAX_CLIENTS; #ifdef HAVE_SERVER //FIXME: we should just set it to 0 to make sure its set up properly elsewhere. if (sv.state) cl.allocated_client_slots = sv.allocated_client_slots; #endif // SZ_Clear (&cls.netchan.message); r_worldentity.model = NULL; // clear other arrays // memset (cl_dlights, 0, sizeof(cl_dlights)); Z_Free(cl_lightstyle); cl_lightstyle = NULL; cl_max_lightstyles = 0; rtlights_first = rtlights_max = RTL_FIRST; for (i = 0; i < MAX_SPLITS; i++) { VectorSet(cl.playerview[i].gravitydir, 0, 0, -1); cl.playerview[i].viewheight = DEFAULT_VIEWHEIGHT; cl.playerview[i].maxspeed = 320; cl.playerview[i].entgravity = 1; cl.playerview[i].chatstate = atoi(InfoBuf_ValueForKey(&cls.userinfo[i], "chat")); } #ifdef QUAKESTATS for (i = 0; i < MAX_CLIENTS; i++) //in case some server doesn't support it cl.players[i].stats[STAT_VIEWHEIGHT] = cl.players[i].statsf[STAT_VIEWHEIGHT] = DEFAULT_VIEWHEIGHT; #endif cl.minpitch = -70; cl.maxpitch = 80; cl.oldgametime = 0; cl.gametime = 0; cl.gametimemark = 0; cl.splitclients = 1; cl.autotrack_hint = -1; cl.autotrack_killer = -1; cl.downloadlist = pendingdownloads; cl.faileddownloads = faileddownloads; cl.skyautorotate = 1; if (cfg_save_auto.ival && Cvar_UnsavedArchive()) Cmd_ExecuteString("cfg_save\n", RESTRICT_LOCAL); #ifdef CL_MASTER MasterInfo_WriteServers(); #endif R_GAliasFlushSkinCache(false); } /* ===================== CL_Disconnect Sends a disconnect message to the server This is also called on Host_Error, so it shouldn't cause any errors ===================== */ void CL_Disconnect (const char *reason) { qbyte final[13]; int i; if (reason) Cvar_Set(&cl_disconnectreason, reason); connectinfo.trying = false; SCR_SetLoadingStage(0); Cvar_ApplyLatches(CVAR_SERVEROVERRIDE, true); // stop sounds (especially looping!) S_StopAllSounds (true); #ifdef VM_CG if (q3) q3->cl.Disconnect(cls.sockets); #endif #ifdef CSQC_DAT CSQC_Shutdown(); #endif // if running a local server, shut it down if (cls.demoplayback != DPB_NONE) CL_StopPlayback (); else if (cls.state != ca_disconnected) { if (cls.demorecording) CL_Stop_f (); switch(cls.protocol) { case CP_NETQUAKE: #ifdef NQPROT final[0] = clc_disconnect; final[1] = clc_stringcmd; strcpy (final+2, "drop"); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 250000); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 250000); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 250000); #endif break; case CP_PLUGIN: break; case CP_QUAKE2: #ifdef Q2CLIENT final[0] = clcq2_stringcmd; if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) { final[1] = 1; strcpy (final+2, "disconnect"); } else strcpy (final+1, "disconnect"); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 2500); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 2500); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 2500); #endif break; case CP_QUAKE3: break; case CP_QUAKEWORLD: final[0] = clc_stringcmd; strcpy (final+1, "drop"); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 2500); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 2500); Netchan_Transmit (&cls.netchan, strlen(final)+1, final, 2500); break; case CP_UNKNOWN: break; } cls.state = ca_disconnected; cls.protocol = CP_UNKNOWN; cls.demoplayback = DPB_NONE; cls.demorecording = cls.timedemo = false; #ifdef HAVE_SERVER //running a server, and it's our own if (serverrunning && !tolocalserver && sv.state != ss_clustermode) SV_UnspawnServer(); #endif } Cam_Reset(); if (cl.worldmodel) { Mod_ClearAll(); cl.worldmodel = NULL; } CL_Parse_Disconnected(); COM_FlushTempoaryPacks(); r_worldentity.model = NULL; for (i = 0; i < cl.splitclients; i++) cl.playerview[i].spectator = 0; cl.sendprespawn = false; cl.intermissionmode = IM_NONE; cl.oldgametime = 0; memset(&r_refdef, 0, sizeof(r_refdef)); #ifdef NQPROT cls.signon=0; #endif CL_StopUpload(); CL_FlushClientCommands(); #ifdef HAVE_SERVER if (!isDedicated) #endif { SCR_EndLoadingPlaque(); V_ClearCShifts(); } cl.servercount = 0; cls.findtrack = false; cls.realserverip.type = NA_INVALID; while (cls.qtvviewers) { struct qtvviewers_s *v = cls.qtvviewers; cls.qtvviewers = v->next; Z_Free(v); } #ifdef TCPCONNECT //disconnects it, without disconnecting the others. FTENET_AddToCollection(cls.sockets, "conn", NULL, NA_INVALID, NP_DGRAM); #endif Cvar_ForceSet(&cl_servername, ""); CL_ClearState(false); FS_PureMode(NULL, 0, NULL, NULL, NULL, NULL, 0); Alias_WipeStuffedAliases(); //now start up the csqc/menu module again. // (void)CSQC_UnconnectedInit(); } #undef serverrunning #undef tolocalserver void CL_Disconnect_f (void) { #ifdef HAVE_SERVER if (sv.state) SV_UnspawnServer(); #endif CL_Disconnect (NULL); CL_ConnectAbort(NULL); NET_CloseClient(); (void)CSQC_UnconnectedInit(); } void CL_Disconnect2_f (void) { char *reason = Cmd_Argv(1); if (*reason) Cvar_Set(&cl_disconnectreason, reason); CL_Disconnect_f(); } /* ==================== CL_User_f user Dump userdata / masterdata for a user ==================== */ void CL_User_f (void) { int uid; int i; qboolean found = false; #ifdef HAVE_SERVER if (sv.state) { SV_User_f(); return; } #endif if (Cmd_Argc() != 2) { Con_TPrintf ("Usage: user \n"); return; } uid = atoi(Cmd_Argv(1)); for (i=0 ; inext) { Con_Printf ("%6s %4s ^[%s^]\n", "", "-", v->name); } Con_TPrintf ("%i total users\n", c); } static struct { const char *name; unsigned int rgb; } csscolours[] = { //php-defined colours {"aliceblue", 0xf0f8ff}, {"antiquewhite", 0xfaebd7}, {"aqua", 0x00ffff}, {"aquamarine", 0x7fffd4}, {"azure", 0xf0ffff}, {"beige", 0xf5f5dc}, {"bisque", 0xffe4c4}, {"black", 0x000000}, {"blanchedalmond", 0xffebcd}, {"blue", 0x0000ff}, {"blueviolet", 0x8a2be2}, {"brown", 0xa52a2a}, {"burlywood", 0xdeb887}, {"cadetblue", 0x5f9ea0}, {"chartreuse", 0x7fff00}, {"chocolate", 0xd2691e}, {"coral", 0xff7f50}, {"cornflowerblue", 0x6495ed}, {"cornsilk", 0xfff8dc}, {"crimson", 0xdc143c}, {"cyan", 0x00ffff}, {"darkblue", 0x00008b}, {"darkcyan", 0x008b8b}, {"darkgoldenrod", 0xb8860b}, {"darkgray", 0xa9a9a9}, {"darkgreen", 0x006400}, {"darkgrey", 0xa9a9a9}, {"darkkhaki", 0xbdb76b}, {"darkmagenta", 0x8b008b}, {"darkolivegreen", 0x556b2f}, {"darkorange", 0xff8c00}, {"darkorchid", 0x9932cc}, {"darkred", 0x8b0000}, {"darksalmon", 0xe9967a}, {"darkseagreen", 0x8fbc8f}, {"darkslateblue", 0x483d8b}, {"darkslategray", 0x2f4f4f}, {"darkslategrey", 0x2f4f4f}, {"darkturquoise", 0x00ced1}, {"darkviolet", 0x9400d3}, {"deeppink", 0xff1493}, {"deepskyblue", 0x00bfff}, {"dimgray", 0x696969}, {"dimgrey", 0x696969}, {"dodgerblue", 0x1e90ff}, {"firebrick", 0xb22222}, {"floralwhite", 0xfffaf0}, {"forestgreen", 0x228b22}, {"fuchsia", 0xff00ff}, {"gainsboro", 0xdcdcdc}, {"ghostwhite", 0xf8f8ff}, {"gold", 0xffd700}, {"goldenrod", 0xdaa520}, {"gray", 0x808080}, {"green", 0x008000}, {"greenyellow", 0xadff2f}, {"grey", 0x808080}, {"honeydew", 0xf0fff0}, {"hotpink", 0xff69b4}, {"indianred", 0xcd5c5c}, {"indigo", 0x4b0082}, {"ivory", 0xfffff0}, {"khaki", 0xf0e68c}, {"lavender", 0xe6e6fa}, {"lavenderblush", 0xfff0f5}, {"lawngreen", 0x7cfc00}, {"lemonchiffon", 0xfffacd}, {"lightblue", 0xadd8e6}, {"lightcoral", 0xf08080}, {"lightcyan", 0xe0ffff}, {"lightgoldenrodyellow",0xfafad2}, {"lightgray", 0xd3d3d3}, {"lightgreen", 0x90ee90}, {"lightgrey", 0xd3d3d3}, {"lightpink", 0xffb6c1}, {"lightsalmon", 0xffa07a}, {"lightseagreen", 0x20b2aa}, {"lightskyblue", 0x87cefa}, {"lightslategray", 0x778899}, {"lightslategrey", 0x778899}, {"lightsteelblue", 0xb0c4de}, {"lightyellow", 0xffffe0}, {"lime", 0x00ff00}, {"limegreen", 0x32cd32}, {"linen", 0xfaf0e6}, {"magenta", 0xff00ff}, {"maroon", 0x800000}, {"mediumaquamarine",0x66cdaa}, {"mediumblue", 0x0000cd}, {"mediumorchid", 0xba55d3}, {"mediumpurple", 0x9370db}, {"mediumseagreen", 0x3cb371}, {"mediumslateblue", 0x7b68ee}, {"mediumspringgreen",0x00fa9a}, {"mediumturquoise", 0x48d1cc}, {"mediumvioletred", 0xc71585}, {"midnightblue", 0x191970}, {"mintcream", 0xf5fffa}, {"mistyrose", 0xffe4e1}, {"moccasin", 0xffe4b5}, {"navajowhite", 0xffdead}, {"navy", 0x000080}, {"oldlace", 0xfdf5e6}, {"olive", 0x808000}, {"olivedrab", 0x6b8e23}, {"orange", 0xffa500}, {"orangered", 0xff4500}, {"orchid", 0xda70d6}, {"palegoldenrod", 0xeee8aa}, {"palegreen", 0x98fb98}, {"paleturquoise", 0xafeeee}, {"palevioletred", 0xdb7093}, {"papayawhip", 0xffefd5}, {"peachpuff", 0xffdab9}, {"peru", 0xcd853f}, {"pink", 0xffc0cb}, {"plum", 0xdda0dd}, {"powderblue", 0xb0e0e6}, {"purple", 0x800080}, {"red", 0xff0000}, {"rosybrown", 0xbc8f8f}, {"royalblue", 0x4169e1}, {"saddlebrown", 0x8b4513}, {"salmon", 0xfa8072}, {"sandybrown", 0xf4a460}, {"seagreen", 0x2e8b57}, {"seashell", 0xfff5ee}, {"sienna", 0xa0522d}, {"silver", 0xc0c0c0}, {"skyblue", 0x87ceeb}, {"slateblue", 0x6a5acd}, {"slategray", 0x708090}, {"slategrey", 0x708090}, {"snow", 0xfffafa}, {"springgreen", 0x00ff7f}, {"steelblue", 0x4682b4}, {"tan", 0xd2b48c}, {"teal", 0x008080}, {"thistle", 0xd8bfd8}, {"tomato", 0xff6347}, {"turquoise", 0x40e0d0}, {"violet", 0xee82ee}, {"wheat", 0xf5deb3}, {"white", 0xffffff}, {"whitesmoke", 0xf5f5f5}, {"yellow", 0xffff00}, {"yellowgreen", 0x9acd32}, }; int CL_ParseColour(const char *colt) { char *e; int col; size_t i; if (!strncmp(colt, "0x", 2)) col = 0xff000000|strtoul(colt+2, NULL, 16); else { col = strtoul(colt, &e, 0); if (*e) { col = 0; for (i = 0; i < countof(csscolours); i++) if (!Q_strcasecmp(colt, csscolours[i].name)) { col = 0xff000000 | csscolours[i].rgb; break; } } else { col &= 15; if (col > 13) col = 13; } } return col; } const char *CL_ColourName(const char *colt) { int col = CL_ParseColour(colt); size_t i; if (col & 0xff000000) { col &= ~0xff000000; for (i = 0; i < countof(csscolours); i++) if (csscolours[i].rgb == col) colt = csscolours[i].name; } return colt; } void CL_Color_c(int argn, const char *partial, struct xcommandargcompletioncb_s *ctx) { int len; size_t i; if (argn == 1 || argn == 2) { len = strlen(partial); if (*partial >= '0' && *partial <= '9') ; else for (i = 0; i < countof(csscolours); i++) { if (!Q_strncasecmp(partial, csscolours[i].name, len)) ctx->cb(csscolours[i].name, va("^x%x%x%x%s", (csscolours[i].rgb>>20)&15, (csscolours[i].rgb>>12)&15, (csscolours[i].rgb>>4)&15, csscolours[i].name), NULL, ctx); } } } void CL_Color_f (void) { // just for quake compatability... int top, bottom; char num[16]; int pnum = CL_TargettedSplit(true); qboolean server_owns_colour; char *t; char *b; if (Cmd_Argc() == 1) { const char *t = InfoBuf_ValueForKey(&cls.userinfo[pnum], "topcolor"); const char *b = InfoBuf_ValueForKey(&cls.userinfo[pnum], "bottomcolor"); t = CL_ColourName(t); b = CL_ColourName(b); if (!*t) t = "0"; if (!*b) b = "0"; if (!strcmp(t, b)) Con_TPrintf ("\"color\" is \"%s\"\n", t, b); else Con_TPrintf ("\"color\" is \"%s %s\"\n", t, b); Con_TPrintf ("usage: color <0xRRGGBB> [0xRRGGBB]\n"); return; } if (Cmd_FromGamecode()) server_owns_colour = true; else server_owns_colour = false; t = Cmd_Argv(1); b = (Cmd_Argc()==2)?t:Cmd_Argv(2); if (!strcmp(t, "-1")) t = InfoBuf_ValueForKey(&cls.userinfo[pnum], "topcolor"); top = CL_ParseColour(t); if (!strcmp(b, "-1")) b = InfoBuf_ValueForKey(&cls.userinfo[pnum], "bottomcolor"); bottom = CL_ParseColour(b); Q_snprintfz (num, sizeof(num), (top&0xff000000)?"0x%06x":"%i", top & 0xffffff); if (top == 0) *num = '\0'; if (Cmd_ExecLevel>RESTRICT_SERVER) //colour command came from server for a split client Cbuf_AddText(va("p%i cmd setinfo topcolor \"%s\"\n", pnum+1, num), Cmd_ExecLevel); // else if (server_owns_colour) // Cvar_LockFromServer(&topcolor, num); else CL_SetInfo(pnum, "topcolor", num); Q_snprintfz (num, sizeof(num), (bottom&0xff000000)?"0x%06x":"%i", bottom & 0xffffff); if (bottom == 0) *num = '\0'; if (Cmd_ExecLevel>RESTRICT_SERVER) //colour command came from server for a split client Cbuf_AddText(va("p%i cmd setinfo bottomcolor \"%s\"\n", pnum+1, num), Cmd_ExecLevel); else if (server_owns_colour) Cvar_LockFromServer(&bottomcolor, num); else CL_SetInfo (pnum, "bottomcolor", num); #ifdef NQPROT if (cls.protocol == CP_NETQUAKE) Cmd_ForwardToServer(); #endif } void CL_PakDownloads(int mode) { /* mode=0 no downloads (forced to 1 for pure) mode=1 archived names so local stuff is not poluted mode=2 downloaded packages will always be present. Use With Caution. mode&4 download even packages that are not referenced. */ char local[256]; char *pname, *sep; char *s = cl.serverpackhashes; int i; if (!cl.serverpakschanged || !mode) return; Cmd_TokenizeString(cl.serverpacknames, false, false); for (i = 0; i < Cmd_Argc(); i++) { s = COM_Parse(s); pname = Cmd_Argv(i); //'*' prefix means 'referenced'. so if the server isn't using any files from it, don't bother downloading it. if (*pname == '*') pname++; else if (!(mode & 4)) continue; sep = strchr(pname, '/'); if (!sep || strchr(sep+1, '/')) continue; //don't try downloading weird ones here... paks inside paks is screwy stuff! if ((mode&3) != 2) { /*if we already have such a file, this is a no-op*/ if (CL_CheckDLFile(va("package/%s", pname))) continue; if (!FS_GenCachedPakName(pname, com_token, local, sizeof(local))) continue; } else Q_strncpyz(local, pname, sizeof(local)); CL_CheckOrEnqueDownloadFile(pname, local, DLLF_ALLOWWEB|DLLF_NONGAME); } } void CL_CheckServerPacks(void) { static int oldpure; int pure = atof(InfoBuf_ValueForKey(&cl.serverinfo, "sv_pure")); if (pure < cl_pure.ival) pure = cl_pure.ival; pure = bound(0, pure, 2); if (!cl.serverpackhashes || cls.demoplayback) pure = 0; if (pure != oldpure || cl.serverpakschanged) { CL_PakDownloads((pure && !cl_download_packages.ival)?1:cl_download_packages.ival); FS_PureMode(NULL, pure, cl.serverpacknames, cl.serverpackhashes, NULL, NULL, cls.challenge); if (pure) { /*when enabling pure, kill cached models/sounds/etc*/ Cache_Flush(); /*make sure cheating lamas can't use old shaders from a different srver*/ Shader_NeedReload(true); } } oldpure = pure; cl.serverpakschanged = false; } void CL_CheckServerInfo(void) { char *s; unsigned int allowed; #ifdef QUAKESTATS int oldstate; #endif #ifdef HAVE_SERVER extern cvar_t sv_cheats; #endif int oldteamplay = cl.teamplay; qboolean spectating = true; int i; qboolean oldwatervis = cls.allow_watervis; int oldskyboxes = cls.allow_unmaskedskyboxes; //spectator 2 = spectator-with-scores, considered to be players. this means we don't want to allow spec cheats while they're inactive, because that would be weird. for (i = 0; i < cl.splitclients; i++) if (cl.playerview[i].spectator != 1) spectating = false; cl.teamplay = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "teamplay")); cls.deathmatch = cl.deathmatch = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "deathmatch")); cls.allow_cheats = false; cls.allow_semicheats=true; cls.allow_unmaskedskyboxes=false; cls.allow_fbskins = 1; // cls.allow_fbskins = 0; // cls.allow_overbrightlight; cls.allow_csqc = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "anycsqc")) || *InfoBuf_ValueForKey(&cl.serverinfo, "*csprogs"); cl.csqcdebug = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "*csqcdebug")); s = InfoBuf_ValueForKey(&cl.serverinfo, "watervis"); if (spectating || cls.demoplayback || atoi(s) || (!*s && ruleset_allow_watervis.ival)) cls.allow_watervis=true; else cls.allow_watervis=false; s = InfoBuf_ValueForKey(&cl.serverinfo, "allow_skybox"); if (!*s) s = InfoBuf_ValueForKey(&cl.serverinfo, "allow_skyboxes"); if (!*s) cls.allow_unmaskedskyboxes = (cl.worldmodel && cl.worldmodel->fromgame != fg_quake); else cls.allow_unmaskedskyboxes = !!atoi(s); s = InfoBuf_ValueForKey(&cl.serverinfo, "fbskins"); if (*s) cls.allow_fbskins = atof(s); else if (cl.teamfortress) cls.allow_fbskins = 0; else cls.allow_fbskins = 1; s = InfoBuf_ValueForKey(&cl.serverinfo, "*cheats"); if (spectating || cls.demoplayback || !stricmp(s, "on")) cls.allow_cheats = true; #ifdef HAVE_SERVER //allow cheats in single player regardless of sv_cheats. //(also directly read the sv_cheats cvar to avoid issues with nq protocols that don't support serverinfo. if (sv.state == ss_active && (sv.allocated_client_slots == 1 || sv_cheats.ival)) cls.allow_cheats = true; #endif s = InfoBuf_ValueForKey(&cl.serverinfo, "strict"); if ((!spectating && !cls.demoplayback && *s && strcmp(s, "0")) || !ruleset_allow_semicheats.ival) { cls.allow_semicheats = false; cls.allow_cheats = false; } cls.z_ext = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "*z_ext")) & CLIENT_SUPPORTED_Z_EXTENSIONS; #ifdef NQPROT if (cls.protocol == CP_NETQUAKE && CPNQ_IS_DP) { //movevars come from stats. } else #endif { cls.maxfps = atof(InfoBuf_ValueForKey(&cl.serverinfo, "maxfps")); if (cls.maxfps < 20) cls.maxfps = 72; // movement vars for prediction cl.bunnyspeedcap = Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_bunnyspeedcap")); movevars.slidefix = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_slidefix")) != 0); movevars.slidyslopes = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_slidyslopes")) != 0); movevars.bunnyfriction = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_bunnyfriction")) != 0); movevars.airstep = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_airstep")) != 0); movevars.pground = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_pground")) != 0); movevars.stepdown = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_stepdown")) != 0); movevars.walljump = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_walljump"))); movevars.ktjump = Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_ktjump")); movevars.autobunny = (Q_atof(InfoBuf_ValueForKey(&cl.serverinfo, "pm_autobunny")) != 0); s = InfoBuf_ValueForKey(&cl.serverinfo, "pm_stepheight"); movevars.stepheight = *s?Q_atof(s):PM_DEFAULTSTEPHEIGHT; s = InfoBuf_ValueForKey(&cl.serverinfo, "pm_watersinkspeed"); movevars.watersinkspeed = *s?Q_atof(s):60; s = InfoBuf_ValueForKey(&cl.serverinfo, "pm_flyfriction"); movevars.flyfriction = *s?Q_atof(s):4; s = InfoBuf_ValueForKey(&cl.serverinfo, "pm_edgefriction"); movevars.edgefriction = *s?Q_atof(s):2; if (!(movevars.flags&MOVEFLAG_VALID)) movevars.flags = (movevars.flags&~MOVEFLAG_QWEDGEBOX) | (*s?0:MOVEFLAG_QWEDGEBOX); } movevars.coordtype = cls.netchan.netprim.coordtype; // Initialize cl.maxpitch & cl.minpitch if (cls.protocol == CP_QUAKEWORLD || cls.protocol == CP_NETQUAKE) { #ifdef NQPROT s = InfoBuf_ValueForKey(&cl.serverinfo, "maxpitch"); cl.maxpitch = *s ? Q_atof(s) : ((cl_fullpitch_nq.ival && !cl.haveserverinfo)?90.0f:80.0f); s = InfoBuf_ValueForKey(&cl.serverinfo, "minpitch"); cl.minpitch = *s ? Q_atof(s) : ((cl_fullpitch_nq.ival && !cl.haveserverinfo)?-90.0f:-70.0f); if (cls.protocol == CP_NETQUAKE) { //proquake likes spamming us with fixangles //should be about 0.5/65536, but there's some precision issues with such small numbers around 80, so we need to bias it more than we ought cl.maxpitch -= 1.0/2048; } #else s = InfoBuf_ValueForKey(&cl.serverinfo, "maxpitch"); cl.maxpitch = *s ? Q_atof(s) : 80.0f; s = InfoBuf_ValueForKey(&cl.serverinfo, "minpitch"); cl.minpitch = *s ? Q_atof(s) : -70.0f; #endif } else { cl.maxpitch = 90; cl.minpitch = -90; } //bound it, such that we never end up looking slightly more back than forwards //FIXME: we should probably tweak our movement code instead. cl.maxpitch = bound(-89.9, cl.maxpitch, 89.9); cl.minpitch = bound(-89.9, cl.minpitch, 89.9); cl.disablemouse = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "nomouse")); cl.hexen2pickups = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "sv_pupglow")); allowed = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "allow")); if (allowed & 1) cls.allow_watervis = true; // if (allowed & 2) // cls.allow_rearview = true; if (allowed & 4) cls.allow_unmaskedskyboxes = true; // if (allowed & 8) // cls.allow_mirrors = true; //16 //32 // if (allowed & 128) // cls.allow_postproc = true; // if (allowed & 256) // cls.allow_lightmapgamma = true; if (allowed & 512) cls.allow_cheats = true; if (cls.allow_semicheats) cls.allow_anyparticles = true; else cls.allow_anyparticles = false; if (spectating || cls.demoplayback) cl.fpd = 0; else cl.fpd = atoi(InfoBuf_ValueForKey(&cl.serverinfo, "fpd")); cl.gamespeed = atof(InfoBuf_ValueForKey(&cl.serverinfo, "*gamespeed"))/100.f; if (cl.gamespeed < 0.1) cl.gamespeed = 1; #ifdef QUAKESTATS s = InfoBuf_ValueForKey(&cl.serverinfo, "status"); oldstate = cl.matchstate; if (!stricmp(s, "standby")) cl.matchstate = MATCH_STANDBY; else if (!stricmp(s, "countdown")) cl.matchstate = MATCH_COUNTDOWN; else { float time = strtod(s, &s); if (!strcmp(s, " min left") || !strcmp(s, " mins left")) time *= 60; else if (!strcmp(s, " sec left") || !strcmp(s, " secs left")) time *= 1; else if (!strcmp(s, " hour left") || !strcmp(s, " hours left")) time *= 60*60; else time = -1; if (time >= 0) { //always update it. this is to try to cope with overtime. oldstate = cl.matchstate = MATCH_INPROGRESS; cl.matchgametimestart = cl.gametime + time - 60*atof(InfoBuf_ValueForKey(&cl.serverinfo, "timelimit")); } else { if (*s && cl.matchstate == MATCH_INPROGRESS) Con_DPrintf("Match state changed to unknown meaning: %s\n", s); else cl.matchstate = MATCH_DONTKNOW; //don't revert from inprogress to don't know } } if (oldstate != cl.matchstate) cl.matchgametimestart = cl.gametime; #endif CL_CheckServerPacks(); Cvar_ForceCheatVars(cls.allow_semicheats, cls.allow_cheats); if (oldteamplay != cl.teamplay) Skin_FlushPlayers(); if (oldwatervis != cls.allow_watervis || oldskyboxes != cls.allow_unmaskedskyboxes) Shader_NeedReload(false); CSQC_ServerInfoChanged(); } /* ================== CL_FullInfo_f Allow clients to change userinfo ================== */ void CL_FullInfo_f (void) { char key[512]; char value[512]; char *o; char *s; int pnum = CL_TargettedSplit(true); if (Cmd_Argc() != 2) { Con_TPrintf ("fullinfo \n"); return; } s = Cmd_Argv(1); if (*s == '\\') s++; while (*s) { o = key; while (*s && *s != '\\' && o < key + sizeof(key)) *o++ = *s++; if (o == key + sizeof(key)) { Con_Printf ("key length too long\n"); return; } *o = 0; if (!*s) { Con_Printf ("key %s has no value\n", key); return; } o = value; s++; while (*s && *s != '\\' && o < value + sizeof(value)) *o++ = *s++; if (o == value + sizeof(value)) { Con_Printf ("value length too long\n"); return; } *o = 0; if (*s) s++; if (!stricmp(key, pmodel_name) || !stricmp(key, emodel_name)) continue; InfoBuf_SetKey (&cls.userinfo[pnum], key, value); } } void CL_SetInfoBlob (int pnum, const char *key, const char *value, size_t valuesize) { cvar_t *var; if (!pnum) { var = Cvar_FindVar(key); if (var && (var->flags & CVAR_USERINFO)) { //get the cvar code to set it. the server might have locked it. Cvar_Set(var, value); return; } } else if (pnum < 0 || pnum >= MAX_SPLITS) return; InfoBuf_SetStarBlobKey(&cls.userinfo[pnum], key, value, valuesize); } void CL_SetInfo (int pnum, const char *key, const char *value) { CL_SetInfoBlob(pnum, key, value, strlen(value)); } /* ================== CL_SetInfo_f Allow clients to change userinfo ================== */ void CL_SetInfo_f (void) { char *key, *val; size_t keysize, valsize; cvar_t *var; int pnum = CL_TargettedSplit(true); if (Cmd_Argc() == 1) { InfoBuf_Print (&cls.userinfo[pnum], ""); Con_Printf("[%u]\n", (unsigned int)cls.userinfo[pnum].totalsize); return; } if (Cmd_Argc() != 3) { Con_TPrintf ("usage: setinfo [ ]\n"); return; } if (!stricmp(Cmd_Argv(1), pmodel_name) || !strcmp(Cmd_Argv(1), emodel_name)) return; if (Cmd_Argv(1)[0] == '*') { int i; if (!strcmp(Cmd_Argv(1), "*")) if (!strcmp(Cmd_Argv(2), "")) { //clear it out const char *k; for(i=0;;) { k = InfoBuf_KeyForNumber(&cls.userinfo[pnum], i); if (!k) break; //no more. else if (*k == '*') i++; //can't remove * keys else if ((var = Cvar_FindVar(k)) && (var->flags&CVAR_USERINFO)) i++; //this one is a cvar. else InfoBuf_RemoveKey(&cls.userinfo[pnum], k); //we can remove this one though, so yay. } return; } Con_Printf ("Can't set * keys\n"); return; } key = Cmd_Argv(1); val = Cmd_Argv(2); key = InfoBuf_DecodeString(key, key+strlen(key), &keysize); val = InfoBuf_DecodeString(val, val+strlen(val), &valsize); if (keysize != strlen(key)) Con_Printf ("setinfo: ignoring key name with embedded null\n"); else CL_SetInfoBlob(pnum, key, val, valsize); Z_Free(key); Z_Free(val); } #if 1//def _DEBUG void CL_SetInfoBlob_f (void) { qofs_t fsize; void *data; int pnum = CL_TargettedSplit(true); if (Cmd_Argc() == 1) { InfoBuf_Print (&cls.userinfo[pnum], ""); return; } if (Cmd_Argc() != 3) { Con_TPrintf ("usage: setinfo [ ]\n"); return; } //user isn't allowed to set pmodel, emodel, *foo as these could break stuff. if (!stricmp(Cmd_Argv(1), pmodel_name) || !strcmp(Cmd_Argv(1), emodel_name)) return; if (Cmd_Argv(1)[0] == '*') { Con_Printf ("Can't set * keys\n"); return; } data = FS_MallocFile(Cmd_Argv(2), FS_GAME, &fsize); if (!data) { Con_Printf ("Unable to read %s\n", Cmd_Argv(2)); return; } if (fsize > 64*1024*1024) Con_Printf ("File is over 64mb\n"); else CL_SetInfoBlob(pnum, Cmd_Argv(1), data, fsize); FS_FreeFile(data); } #endif void CL_SaveInfo(vfsfile_t *f) { int i; for (i = 0; i < MAX_SPLITS; i++) { VFS_WRITE(f, "\n", 1); if (i) { VFS_WRITE(f, va("p%i setinfo * \"\"\n", i+1), 16); InfoBuf_WriteToFile(f, &cls.userinfo[i], va("p%i setinfo", i+1), 0); } else { VFS_WRITE(f, "setinfo * \"\"\n", 13); InfoBuf_WriteToFile(f, &cls.userinfo[i], "setinfo", CVAR_USERINFO); } } } /* ==================== CL_Packet_f packet Contents allows \n escape character ==================== */ void CL_Packet_f (void) { #ifdef FTE_TARGET_WEB //either this creates some expensive alternative rtc connection that screws us over, or just generally fails. don't allow it. Con_Printf (CON_WARNING "Ignoring 'packet %s' request.\n", Cmd_Argv(1)); #else char send[2048]; int i, l; char *in, *out; netadr_t adr; struct dtlspeercred_s cred = {Cmd_Argv(1)}; if (Cmd_Argc() != 3) { Con_TPrintf ("usage: packet \n"); return; } if (!NET_StringToAdr (Cmd_Argv(1), PORT_DEFAULTSERVER, &adr)) { Con_Printf ("Bad address: %s\n", Cmd_Argv(1)); return; } if (Cmd_FromGamecode()) //some mvdsv servers stuffcmd a packet command which lets them know which ip the client is from. { //unfortunatly, 50% of servers are badly configured resulting in them poking local services that THEY MUST NOT HAVE ACCESS TO. const char *addrdesc; const char *realdesc; if (cls.demoplayback) { Con_DPrintf ("Not sending realip packet from demo\n"); return; } if (!NET_CompareAdr(&adr, &cls.netchan.remote_address)) { if (NET_ClassifyAddress(&adr, &addrdesc) < ASCOPE_LAN) { if (NET_ClassifyAddress(&cls.netchan.remote_address, &realdesc) < ASCOPE_LAN) { //this isn't necessarily buggy... but its still a potential exploit so we need to block it regardless. Con_Printf (CON_WARNING "Ignoring buggy %s realip request for %s server.\n", addrdesc, realdesc); } else { adr = cls.netchan.remote_address; Con_Printf (CON_WARNING "Ignoring buggy %s realip request, sending to %s server instead.\n", addrdesc, realdesc); } } } cls.realserverip = adr; Con_DPrintf ("Sending realip packet\n"); } else if (!ruleset_allow_packet.ival) { Con_Printf("Sorry, the %s command is disallowed\n", Cmd_Argv(0)); return; } cls.lastarbiatarypackettime = Sys_DoubleTime(); //prevent the packet command from causing a reconnect on badly configured mvdsv servers. in = Cmd_Argv(2); out = send+4; send[0] = send[1] = send[2] = send[3] = 0xff; l = strlen (in); for (i=0 ; istring) { Cmd_TokenizeString(cl_autodemos->string, false, false); if (!Cmd_Argc()) { //none... cls.demonum = -1; return; } if (cls.demonum >= Cmd_Argc()) cls.demonum = 0; //restart the loop if (!strcmp(Cmd_Argv(cls.demonum), "quit")) Q_snprintfz (str, sizeof(str), "quit\n"); else Q_snprintfz (str, sizeof(str), "playdemo \"demos/%s\"\n", Cmd_Argv(cls.demonum)); } else { if (!cls.demos[cls.demonum][0] || cls.demonum >= MAX_DEMOS) { cls.demonum = 0; if (!cls.demos[cls.demonum][0]) { // Con_Printf ("No demos listed with startdemos\n"); cls.demonum = -1; return; } } if (!strcmp(cls.demos[cls.demonum], "quit")) Q_snprintfz (str, sizeof(str), "quit\n"); else Q_snprintfz (str, sizeof(str), "playdemo %s\n", cls.demos[cls.demonum]); } Cbuf_InsertText (str, RESTRICT_LOCAL, false); cls.demonum++; if (!cls.state) cls.state = ca_demostart; } /* =============================================================================== DEMO LOOP CONTROL =============================================================================== */ /* ================== CL_Startdemos_f ================== */ void CL_Startdemos_f (void) { int i, c; c = Cmd_Argc() - 1; if (c > MAX_DEMOS) { Con_Printf ("Max %i demos in demoloop\n", MAX_DEMOS); c = MAX_DEMOS; } Con_DPrintf ("%i demo(s) in loop\n", c); for (i=1 ; imethod <= DL_QWPENDING) // don't change when downloading return; cls.demoseeking = DEMOSEEK_NOT; //don't seek over it if (*mapname) SCR_ImageName(mapname); else SCR_BeginLoadingPlaque(); S_StopAllSounds (true); cl.intermissionmode = IM_NONE; if (cls.state) { cls.state = ca_connected; // not active anymore, but not disconnected Con_TPrintf ("\nChanging map...\n"); } else Con_Printf("Changing while not connected\n"); #ifdef NQPROT cls.signon=0; #endif } /* ================= CL_Reconnect_f User command, or NQ protocol command (messy). ================= */ void CL_Reconnect_f (void) { if (cls.download && cls.download->method <= DL_QWPENDING) // don't change when downloading return; #ifdef NQPROT if (cls.protocol == CP_NETQUAKE && Cmd_IsInsecure()) { CL_Changing_f(); return; } #endif S_StopAllSounds (true); if (cls.state == ca_connected) { Con_TPrintf ("reconnecting...\n"); CL_SendClientCommand(true, "new"); return; } if (!*cls.servername) { Con_TPrintf ("No server to reconnect to...\n"); return; } #if defined(HAVE_SERVER) && defined(SUBSERVERS) if (sv.state == ss_clustermode) { //reconnecting while we're a cluster... o.O char oldguid[sizeof(connectinfo.guid)]; Q_strncpyz(oldguid, connectinfo.guid, sizeof(oldguid)); memset(&connectinfo, 0, sizeof(connectinfo)); connectinfo.istransfer = false; Q_strncpyz(connectinfo.guid, oldguid, sizeof(oldguid)); //retain the same guid on transfers Cvar_Set(&cl_disconnectreason, "Transferring...."); connectinfo.trying = true; connectinfo.defaultport = cl_defaultport.value; connectinfo.protocol = CP_UNKNOWN; SCR_SetLoadingStage(LS_CONNECTION); CL_CheckForResend(); return; } #endif CL_Disconnect(NULL); CL_BeginServerReconnect(); } static void CL_ConnectionlessPacket_Connection(char *tokens) { unsigned int ncflags; int qportsize = -1; if (net_from.type == NA_INVALID) return; //I've found a qizmo demo that contains one of these. its best left ignored. if (!CL_IsPendingServerAddress(&net_from)) { if (net_from.type != NA_LOOPBACK) Con_TPrintf ("ignoring connection\n"); return; } if (cls.state >= ca_connected) { if (!NET_CompareAdr(&cls.netchan.remote_address, &net_from)) { #ifdef HAVE_SERVER if (sv.state != ss_clustermode) #endif CL_Disconnect (NULL); } else { if (cls.demoplayback == DPB_NONE) Con_TPrintf ("Dup connect received. Ignored.\n"); return; } } if (net_from.type != NA_LOOPBACK) { // Con_TPrintf (S_COLOR_GRAY"connection\n"); #ifdef HAVE_SERVER if (sv.state && sv.state != ss_clustermode) SV_UnspawnServer(); #endif } #if defined(Q2CLIENT) if (tokens && connectinfo.protocol == CP_QUAKE2) { if (cls.protocol_q2 == PROTOCOL_VERSION_R1Q2 || cls.protocol_q2 == PROTOCOL_VERSION_Q2PRO) qportsize = 1; tokens = COM_Parse(tokens); //skip the client_connect bit while((tokens = COM_Parse(tokens))) { if (!strncmp(com_token, "ac=", 3)) { if (atoi(com_token+3)) { CL_ConnectAbort("Server requires anticheat support"); return; } } else if (!strncmp(com_token, "nc=", 3)) qportsize = atoi(com_token+3)?1:2; else if (!strncmp(com_token, "map=", 4)) SCR_ImageName(com_token+4); else if (!strncmp(com_token, "dlserver=", 9)) Q_strncpyz(cls.downloadurl, com_token+9, sizeof(cls.downloadurl)); else if (!strcmp(com_token, STRINGIFY(PROTOCOL_VERSION_Q2EX))) connectinfo.subprotocol = atoi(com_token); else Con_DPrintf("client_connect: Unknown token \"%s\"\n", com_token); } } #endif connectinfo.trying = false; cl.splitclients = 0; cls.protocol = connectinfo.protocol; cls.proquake_angles_hack = false; cls.fteprotocolextensions = connectinfo.ext.fte1; cls.fteprotocolextensions2 = connectinfo.ext.fte2; cls.ezprotocolextensions1 = connectinfo.ext.ez1; cls.challenge = connectinfo.challenge; ncflags = NCF_CLIENT; if (connectinfo.ext.mtu) ncflags |= NCF_FRAGABLE; if (connectinfo.ext.fte2&PEXT2_STUNAWARE) ncflags |= NCF_STUNAWARE; Netchan_Setup (ncflags, &cls.netchan, &net_from, connectinfo.qport, connectinfo.ext.mtu); cls.protocol_q2 = (cls.protocol == CP_QUAKE2)?connectinfo.subprotocol:0; if (qportsize>=0) cls.netchan.qportsize = qportsize; #ifdef HUFFNETWORK cls.netchan.compresstable = Huff_CompressionCRC(connectinfo.ext.compresscrc); #else cls.netchan.compresstable = NULL; #endif CL_ParseEstablished(); #ifdef Q3CLIENT if (cls.protocol == CP_QUAKE3) q3->cl.Established(); else #endif CL_SendClientCommand(true, "new"); cls.state = ca_connected; #ifdef QUAKESPYAPI allowremotecmd = false; // localid required now for remote cmds #endif total_loading_size = 100; current_loading_size = 0; SCR_SetLoadingStage(LS_CLIENT); Validation_Apply_Ruleset(); CL_WriteSetDemoMessage(); } /* ================= CL_ConnectionlessPacket Responses to broadcasts, etc ================= */ void CL_ConnectionlessPacket (void) { char *s; int c; char adr[MAX_ADR_SIZE]; MSG_BeginReading (&net_message, msg_nullnetprim); MSG_ReadLong (); // skip the -1 Cmd_TokenizeString(net_message.data+4, false, false); if (net_message.cursize == sizeof(net_message_buffer)) net_message.data[sizeof(net_message_buffer)-1] = '\0'; else net_message.data[net_message.cursize] = '\0'; #ifdef PLUGINS if (Plug_ConnectionlessClientPacket(net_message.data+4, net_message.cursize-4)) return; #endif c = MSG_ReadByte (); // ping from somewhere if (c == A2A_PING) { char data[256]; int len; if (cls.realserverip.type == NA_INVALID) return; //not done a realip yet if (NET_CompareBaseAdr(&cls.realserverip, &net_from) == false) return; //only reply if it came from the real server's ip. data[0] = 0xff; data[1] = 0xff; data[2] = 0xff; data[3] = 0xff; data[4] = A2A_ACK; data[5] = 0; //ack needs two parameters to work with realip properly. //firstly it needs an auth message, so it can't be spoofed. //secondly, it needs a copy of the realip ident, so you can't report a different player's client (you would need access to their ip). data[5] = ' '; Q_snprintfz(data+6, sizeof(data)-6, "%i %i", atoi(MSG_ReadString()), cls.realip_ident); len = strlen(data); NET_SendPacket (cls.sockets, len, &data, &net_from); return; } if (c == A2C_PRINT) { if (!strncmp(net_message.data+MSG_GetReadCount(), "\\chunk", 6)) { if (NET_CompareBaseAdr(&cls.netchan.remote_address, &net_from) == false) if (cls.realserverip.type == NA_INVALID || NET_CompareBaseAdr(&cls.realserverip, &net_from) == false) return; //only use it if it came from the real server's ip (this breaks on proxies). MSG_ReadLong(); MSG_ReadChar(); MSG_ReadChar(); if (CL_ParseOOBDownload()) { if (MSG_GetReadCount() != net_message.cursize) { Con_Printf ("junk on the end of the packet\n"); CL_Disconnect_f(); } cls.netchan.last_received = realtime; //in case there's some virus scanner running on the server making it stall... for instance... } return; } } if (cls.demoplayback == DPB_NONE && net_from.type != NA_LOOPBACK) Con_Printf (S_COLOR_GRAY"%s: ", NET_AdrToString (adr, sizeof(adr), &net_from)); // Con_DPrintf ("%s", net_message.data + 4); if (c == 'f') //using 'f' as a prefix so that I don't need lots of hacks { s = MSG_ReadStringLine (); if (!strcmp(s, "redir")) { netadr_t adr; char *data = MSG_ReadStringLine(); Con_TPrintf (S_COLOR_GRAY"redirect to %s\n", data); if (NET_StringToAdr(data, PORT_DEFAULTSERVER, &adr)) { if (CL_IsPendingServerAddress(&net_from)) { struct dtlspeercred_s cred = {cls.servername}; //FIXME if (!NET_EnsureRoute(cls.sockets, "redir", &cred, data, &adr, true)) Con_Printf (CON_ERROR"Unable to redirect to %s\n", data); else { connectinfo.istransfer = true; connectinfo.numadr = 1; connectinfo.adr[0] = adr; data = "\xff\xff\xff\xffgetchallenge\n"; NET_SendPacket (cls.sockets, strlen(data), data, &adr); } } } return; } else if (!strcmp(s, "reject")) { //generic rejection. stop trying. char *data = MSG_ReadStringLine(); Con_Printf ("reject\n"); if (CL_IsPendingServerAddress(&net_from)) CL_ConnectAbort("%s\n", data); return; } else if (!strcmp(s, "badname")) { //rejected purely because of player name if (CL_IsPendingServerAddress(&net_from)) CL_ConnectAbort("bad player name\n"); } else if (!strcmp(s, "badaccount")) { //rejected because username or password is wrong if (CL_IsPendingServerAddress(&net_from)) CL_ConnectAbort("invalid username or password\n"); } Con_Printf ("f%s\n", s); return; } if (c == S2C_CHALLENGE) { static unsigned int lasttime = 0xdeadbeef; static netadr_t lastadr; unsigned int curtime = Sys_Milliseconds(); #ifdef HAVE_DTLS int candtls = 0; //0=no,1=optional,2=mandatory #endif s = MSG_ReadString (); COM_Parse(s); #ifdef Q3CLIENT if (!strcmp(com_token, "onnectResponse")) { connectinfo.protocol = CP_QUAKE3; CL_ConnectionlessPacket_Connection(s); return; } #endif #ifdef Q2CLIENT if (!strcmp(com_token, "lient_connect")) { connectinfo.protocol = CP_QUAKE2; CL_ConnectionlessPacket_Connection(s); return; } #endif Con_TPrintf (S_COLOR_GRAY"challenge\n"); if (!CL_IsPendingServerAddress(&net_from)) { if (net_from.prot != NP_RTC_TCP && net_from.prot != NP_RTC_TLS) Con_Printf(CON_WARNING"Challenge from wrong server, ignoring\n"); return; } connectinfo.numadr = 1; connectinfo.adr[0] = net_from; //lock in only this specific address. if (!strcmp(com_token, "hallengeResponse")) { /*Quake3 - "\xff\xff\xff\xffchallengeResponse challenge [clchallenge protover]" (no \n)*/ #ifdef Q3CLIENT if (connectinfo.protocol == CP_QUAKE3 || connectinfo.protocol == CP_UNKNOWN) { /*throttle*/ if (curtime - lasttime < 500) return; lasttime = curtime; memset(&connectinfo.ext, 0, sizeof(connectinfo.ext)); connectinfo.protocol = CP_QUAKE3; connectinfo.challenge = atoi(s+17); CL_SendConnectPacket (&net_from); } else { Con_Printf("\nChallenge from another protocol, ignoring Q3 challenge\n"); return; } return; #else Con_Printf("\nUnable to connect to Quake3\n"); return; #endif } else if (!strcmp(com_token, "hallenge")) { /*Quake2 or Darkplaces*/ char *s2; for (s2 = s+9; *s2; s2++) { if ((*s2 < '0' || *s2 > '9') && *s2 != '-') break; } if (!strncmp(s2, "FTE", 3) || !strncmp(s2, "QW", 2)) { //hack to work around NQ+QW+DP servers that reply with both qw and dp challenge requests. //we DON'T want to treat it as a dp server. because then we end up with nq-based protocols. return; } else if (*s2 && *s2 != ' ') {//and if it's not, we're unlikly to be compatible with whatever it is that's talking at us. #ifdef NQPROT if (connectinfo.protocol == CP_NETQUAKE || connectinfo.protocol == CP_UNKNOWN) { /*throttle*/ if (curtime - lasttime < 500) return; lasttime = curtime; connectinfo.protocol = CP_NETQUAKE; CL_ConnectToDarkPlaces(s+9, &net_from); } else Con_Printf("\nChallenge from another protocol, ignoring DP challenge\n"); #else Con_Printf("\nUnable connect to DarkPlaces\n"); #endif return; } #ifdef Q2CLIENT if (connectinfo.protocol == CP_QUAKE2 || connectinfo.protocol == CP_UNKNOWN) { connectinfo.protocol = CP_QUAKE2; if (connectinfo.mode == CIM_Q2EONLY) connectinfo.subprotocol = PROTOCOL_VERSION_Q2EX; else connectinfo.subprotocol = PROTOCOL_VERSION_Q2; } else { Con_Printf("\nChallenge from another protocol, ignoring Q2 challenge\n"); return; } #else Con_Printf("\nUnable to connect to Quake2\n"); return; #endif s+=9; } /*no idea, assume a QuakeWorld challenge response ('c' packet)*/ else if (connectinfo.protocol == CP_QUAKEWORLD || connectinfo.protocol == CP_UNKNOWN) { connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; } else { Con_Printf("\nChallenge from another protocol, ignoring QW challenge\n"); return; } s = COM_Parse(s); //read the challenge. /*throttle connect requests*/ if (curtime - lasttime < 500 && NET_CompareAdr(&net_from, &lastadr) && connectinfo.challenge == atoi(com_token)) return; lasttime = curtime; lastadr = net_from; connectinfo.challenge = atoi(com_token); memset(&connectinfo.ext, 0, sizeof(connectinfo.ext)); while((s = COM_Parse(s))) { if (connectinfo.protocol == CP_QUAKE2 && !strncmp(com_token, "p=", 2)) { char *p = com_token+2; do { switch(strtoul(p, &p, 0)) { case PROTOCOL_VERSION_R1Q2: #ifdef AVAIL_ZLIB //r1q2 will typically send us compressed data, which is a problem if we can't handle that (q2pro has a way to disable it). if (connectinfo.subprotocol < PROTOCOL_VERSION_R1Q2) connectinfo.subprotocol = PROTOCOL_VERSION_R1Q2; #endif break; case PROTOCOL_VERSION_Q2PRO: if (connectinfo.subprotocol < PROTOCOL_VERSION_Q2PRO) connectinfo.subprotocol = PROTOCOL_VERSION_Q2PRO; break; case PROTOCOL_VERSION_Q2EX: if (connectinfo.subprotocol < PROTOCOL_VERSION_Q2EX) connectinfo.subprotocol = PROTOCOL_VERSION_Q2EX; break; } } while (*p++ == ','); } } //if its over q2e's lan layer then pretend there was a p=2023 hint in there... if (connectinfo.protocol == CP_QUAKE2 && net_from.prot == NP_KEXLAN) if (connectinfo.subprotocol < PROTOCOL_VERSION_Q2EX) connectinfo.subprotocol = PROTOCOL_VERSION_Q2EX; for(;;) { int cmd = MSG_ReadLong (); if (msg_badread) break; if (cmd == PROTOCOL_VERSION_VARLENGTH) { int len = MSG_ReadLong(); if (len < 0 || len > 8192) break; c = MSG_ReadLong();/*ident*/ switch(c) { case PROTOCOL_INFO_GUID: if (len > sizeof(connectinfo.ext.guidsalt)-1) { MSG_ReadData(connectinfo.ext.guidsalt, sizeof(connectinfo.ext.guidsalt)); MSG_ReadSkip(len-sizeof(connectinfo.ext.guidsalt)); len = sizeof(connectinfo.ext.guidsalt)-1; } else MSG_ReadData(connectinfo.ext.guidsalt, len); connectinfo.ext.guidsalt[len] = 0; break; default: MSG_ReadSkip(len); /*payload*/ break; } } else { unsigned int l = MSG_ReadLong(); switch(cmd) { case PROTOCOL_VERSION_FTE1: connectinfo.ext.fte1 = l; break; case PROTOCOL_VERSION_FTE2: connectinfo.ext.fte2 = l; break; case PROTOCOL_VERSION_EZQUAKE1: connectinfo.ext.ez1 = l; break; case PROTOCOL_VERSION_FRAGMENT: connectinfo.ext.mtu = l; break; #ifdef HAVE_DTLS case PROTOCOL_VERSION_DTLSUPGRADE: candtls = l; break; //0:not enabled. 1:explicit use allowed. 2:favour it. 3: require it #endif #ifdef HUFFNETWORK case PROTOCOL_VERSION_HUFFMAN: connectinfo.ext.compresscrc = l; break; #endif case PROTOCOL_INFO_GUID: Q_snprintfz(connectinfo.ext.guidsalt, sizeof(connectinfo.ext.guidsalt), "0x%x", l); break; default: break; } } } #ifdef HAVE_DTLS if ((candtls && net_enable_dtls.ival) && net_from.prot == NP_DGRAM && (connectinfo.peercred.hash || net_enable_dtls.ival>1 || candtls > 1) && !NET_IsEncrypted(&net_from)) { //c2s getchallenge //s2c c%u\0DTLS=$candtls //<> //c2s dtlsconnect %u [REALTARGET] //s2c dtlsopened //c2s DTLS(getchallenge) //DTLS(etc) //NOTE: the dtlsconnect/dtlsopened parts are redundant and the non-dtls parts are now entirely optional (and should be skipped if the client requries/knows the server supports dtls) //the challenge response includes server capabilities, so we still need the getchallenge/response part of the handshake despite dtls making the actual challenge part redundant. //getchallenge has to be done twice, with the outer one only reporting whether dtls can/should be used. //this means the actual connect packet is already over dtls, which protects the user's userinfo. //FIXME: do rcon via dtls too, but requires tracking pending rcon packets until the handshake completes. //server says it can do dtls, but will still need to ask it to allocate extra resources for us (I hadn't gotten dtls cookies working properly at that point). if (net_enable_dtls.ival>0) { char *pkt; //qwfwd proxy routing. it doesn't support it yet, but hey, if its willing to forward the dtls packets its all good. char *at; if ((at = strrchr(cls.servername, '@')) && !strchr(cls.servername, '/')) { *at = 0; pkt = va("%c%c%c%c""dtlsconnect %i %s", 255, 255, 255, 255, connectinfo.challenge, cls.servername); *at = '@'; } else pkt = va("%c%c%c%c""dtlsconnect %i", 255, 255, 255, 255, connectinfo.challenge); NET_SendPacket (cls.sockets, strlen(pkt), pkt, &net_from); return; } else if (candtls >= 3) { Cvar_Set(&cl_disconnectreason, va("DTLS is disabled, but server requires it. not connecting\n")); connectinfo.trying = false; Con_Printf("DTLS is disabled, but server requires it. Set ^[/net_enable_dtls 1^] before connecting again.\n"); return; } } if (net_enable_dtls.ival>=3 && !NET_IsEncrypted(&net_from)) { Cvar_Set(&cl_disconnectreason, va("Server does not support/allow dtls. not connecting\n")); connectinfo.trying = false; Con_Printf("Server does not support/allow dtls. not connecting.\n"); return; } #endif CL_SendConnectPacket (&net_from); return; } #ifdef Q2CLIENT if (connectinfo.protocol == CP_QUAKE2) { char *nl; MSG_ReadSkip(-1); c = MSG_GetReadCount(); s = MSG_ReadString (); nl = strchr(s, '\n'); if (nl) { MSG_ReadSkip(c + nl-s + 1 - MSG_GetReadCount()); msg_badread = false; *nl = '\0'; } COM_Parse(s); if (!strcmp(com_token, "print")) { Con_TPrintf (S_COLOR_GRAY"print\n"); s = MSG_ReadString (); if (connectinfo.trying && CL_IsPendingServerBaseAddress(&net_from) == false) Cvar_Set(&cl_disconnectreason, s); Con_Printf ("%s", s); return; } else if (!strcmp(com_token, "client_connect")) { connectinfo.protocol = CP_QUAKE2; CL_ConnectionlessPacket_Connection(s); return; } else if (!strcmp(com_token, "disconnect")) { if (NET_CompareAdr(&net_from, &cls.netchan.remote_address)) { Cvar_Set(&cl_disconnectreason, "Disconnect request from server"); Con_Printf ("disconnect\n"); CL_Disconnect_f(); return; } else { Con_Printf("Ignoring random disconnect command\n"); return; } } else { Con_TPrintf ("unknown connectionless packet for q2: %s\n", s); MSG_ReadSkip(c - MSG_GetReadCount()); c = MSG_ReadByte(); } } #endif #ifdef NQPROT if (c == 'a') { s = MSG_ReadString (); COM_Parse(s); if (!strcmp(com_token, "ccept")) { /*this is a DP server... but we don't know which version nor nq protocol*/ Con_Printf (S_COLOR_GRAY"accept\n"); if (cls.state == ca_connected) return; //we're already connected. don't do it again! if (!CL_IsPendingServerAddress(&net_from)) { //if (net_from.type != NA_LOOPBACK) Con_TPrintf ("ignoring connection\n"); return; } Validation_Apply_Ruleset(); Netchan_Setup(NCF_CLIENT, &cls.netchan, &net_from, connectinfo.qport, 0); CL_ParseEstablished(); cls.netchan.isnqprotocol = true; cls.protocol = CP_NETQUAKE; cls.protocol_nq = CPNQ_ID; //assume vanilla protocol until we know better. cls.proquake_angles_hack = false; cls.challenge = connectinfo.challenge; connectinfo.trying = false; cls.demonum = -1; // not in the demo loop now cls.state = ca_connected; SCR_BeginLoadingPlaque(); return; } } if (c == 'i') { if (!strncmp(net_message.data+4, "infoResponse\n", 13)) { Con_TPrintf (S_COLOR_GRAY"infoResponse\n"); Info_Print(net_message.data+17, ""); return; } } if (c == 'g') { if (!strncmp(net_message.data+4, "getserversResponse", 18)) { qbyte *b = net_message.data+4+18; Con_TPrintf (S_COLOR_GRAY"getserversResponse\n"); while (b+7 <= net_message.data+net_message.cursize) { if (*b == '\\') { b+=1; Con_Printf("%u.%u.%u.%u:%u\n", b[0], b[1], b[2], b[3], b[5]|(b[4]<<8)); b+=6; } else if (*b == '/') { b+=1; Con_Printf("[%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x]:%u\n", (b[0]<<8)|b[1], (b[2]<<8)|b[3], (b[4]<<8)|b[5], (b[6]<<8)|b[7], (b[8]<<8)|b[9], (b[10]<<8)|b[11], (b[12]<<8)|b[13], (b[14]<<8)|b[15], b[17]|(b[16]<<8)); b+=18; } } return; } } #endif if (c == 'd'/*M2C_MASTER_REPLY*/) { s = MSG_ReadString (); COM_Parse(s); if (!strcmp(com_token, "isconnect")) { Con_Printf("Disconnect\n"); if (CL_IsPendingServerAddress(&net_from)) { Cvar_Set(&cl_disconnectreason, "Disconnect request from server"); CL_Disconnect_f(); } } else if (!strcmp(com_token, "tlsopened")) { //server is letting us know that its now listening for a dtls handshake. #ifdef HAVE_DTLS dtlscred_t cred; Con_Printf (S_COLOR_GRAY"dtlsopened\n"); if (!CL_IsPendingServerAddress(&net_from)) return; memset(&cred, 0, sizeof(cred)); cred.peer = connectinfo.peercred; if (NET_DTLS_Create(cls.sockets, &net_from, &cred, true)) { connectinfo.numadr = 1; //fixate on this resolved address. connectinfo.adr[0] = net_from; connectinfo.adr[0].prot = NP_DTLS; connectinfo.time = 0; //send a new challenge NOW. } else CL_ConnectAbort("Unable to initialise dtls driver. You may need to adjust tls_provider or disable dtls with ^[/net_enable_dtls 0^]\n"); //this is a local issue, and not a result on remote packets. #else Con_Printf ("dtlsopened (unsupported)\n"); #endif } else if (*s != '\n') { //qw master server list response Con_Printf ("server ip list\n"); } else { Con_Printf ("disconnect\n"); if (cls.demoplayback != DPB_NONE) { Con_Printf("Disconnect\n"); CL_Disconnect_f(); } } return; } if (c == S2C_CONNECTION) { s = NULL; connectinfo.protocol = CP_QUAKEWORLD; connectinfo.subprotocol = PROTOCOL_VERSION_QW; CL_ConnectionlessPacket_Connection(NULL); return; } #ifdef QUAKESPYAPI // remote command from gui front end if (c == A2C_CLIENT_COMMAND) //man I hate this. { char cmdtext[2048]; if (net_from.type == NA_INVALID || net_from.type != net_local_cl_ipadr.type || net_from.type != NA_IP || ((*(unsigned *)net_from.address.ip != *(unsigned *)net_local_cl_ipadr.address.ip) && (*(unsigned *)net_from.address.ip != htonl(INADDR_LOOPBACK)))) { Con_TPrintf ("Command packet from remote host. Ignored.\n"); return; } #if defined(_WIN32) && !defined(WINRT) ShowWindow (mainwindow, SW_RESTORE); SetForegroundWindow (mainwindow); #endif s = MSG_ReadString (); Con_TPrintf ("client command: %s\n", s); Q_strncpyz(cmdtext, s, sizeof(cmdtext)); s = MSG_ReadString (); while (*s && isspace(*s)) s++; while (*s && isspace(s[strlen(s) - 1])) s[strlen(s) - 1] = 0; if (!allowremotecmd && (!*localid.string || strcmp(localid.string, s))) { if (!*localid.string) { Con_TPrintf ("^&C0Command packet received from local host, but no localid has been set. You may need to upgrade your server browser.\n"); return; } Con_TPrintf ("^&C0Invalid localid on command packet received from local host. \n|%s| != |%s|\nYou may need to reload your server browser and game.\n", s, localid.string); Cvar_Set(&localid, ""); return; } Cbuf_AddText (cmdtext, RESTRICT_SERVER); allowremotecmd = false; return; } #endif // print command from somewhere if (c == 'p') { if (!strncmp(net_message.data+4, "print\n", 6)) { //quake2+quake3 send rejects this way Con_TPrintf (S_COLOR_GRAY"print\n"); Con_Printf ("%s", net_message.data+10); if (connectinfo.trying && CL_IsPendingServerBaseAddress(&net_from) == false) Cvar_Set(&cl_disconnectreason, net_message.data+10); return; } } if (c == A2C_PRINT) { //closest quakeworld has to a reject message Con_TPrintf (S_COLOR_GRAY"print\n"); s = MSG_ReadString (); Con_Printf ("%s", s); if (connectinfo.trying && CL_IsPendingServerBaseAddress(&net_from) == false) Cvar_Set(&cl_disconnectreason, s); return; } if (c == 'r') { //darkplaces-style rejects s = MSG_ReadString (); Con_Printf("r%s\n", s); if (connectinfo.trying && CL_IsPendingServerBaseAddress(&net_from) == false) Cvar_Set(&cl_disconnectreason, s); return; } //happens in demos if (c == svc_disconnect && cls.demoplayback != DPB_NONE && net_from.type == NA_INVALID) { CL_NextDemo(); Host_EndGame (NULL); //end of demo. return; } Con_TPrintf ("unknown connectionless packet: %c\n", c); } #ifdef NQPROT void CLNQ_ConnectionlessPacket(void) { char *s; int length; unsigned short port; if (net_message.cursize < 5) return; //not enough size to be meaningful (qe does not include a port number) MSG_BeginReading (&net_message, msg_nullnetprim); length = LongSwap(MSG_ReadLong ()); if (!(length & NETFLAG_CTL)) return; //not an nq control packet. length &= NETFLAG_LENGTH_MASK; if (length != net_message.cursize) return; //not an nq packet. switch(MSG_ReadByte()) { case CCREP_ACCEPT: connectinfo.trying = false; if (cls.state >= ca_connected) { if (cls.demoplayback == DPB_NONE) Con_TPrintf ("Dup connect received. Ignored.\n"); return; } if (length == 5) { //QE strips the port entirely. cls.proquake_angles_hack = false; cls.protocol_nq = CPNQ_ID; Con_DPrintf("QuakeEx server...\n"); } else { port = htons((unsigned short)MSG_ReadLong()); //this is the port that we're meant to respond to... if (msg_badread) //qe has no port specified. and that's fine when its over dtls anyway. port = 0; cls.proquake_angles_hack = false; cls.protocol_nq = CPNQ_ID; if (MSG_ReadByte() == 1) //a proquake server adds a little extra info { int ver = MSG_ReadByte(); int flags = MSG_ReadByte(); Con_DPrintf("ProQuake server %i.%i\n", ver/10, ver%10); // if (ver >= 34) cls.proquake_angles_hack = true; if (flags & 1) { //its a 'pure' server. Con_Printf("pure ProQuake server\n"); return; } if (flags & 0x80) port = 0; //don't force the port. } if (port && port != net_from.port) { char buf[256]; net_from.port = port; Con_Printf("redirecting to port %s\n", NET_AdrToString(buf, sizeof(buf), &net_from)); } } Validation_Apply_Ruleset(); cls.fteprotocolextensions = connectinfo.ext.fte1; cls.fteprotocolextensions2 = connectinfo.ext.fte2; cls.ezprotocolextensions1 = connectinfo.ext.ez1; Netchan_Setup (NCF_CLIENT, &cls.netchan, &net_from, connectinfo.qport, 0); CL_ParseEstablished(); cls.netchan.isnqprotocol = true; cls.netchan.compresstable = NULL; cls.protocol = CP_NETQUAKE; cls.state = ca_connected; total_loading_size = 100; current_loading_size = 0; SCR_SetLoadingStage(LS_CLIENT); #ifdef QUAKESPYAPI allowremotecmd = false; // localid required now for remote cmds #endif if (length == 5) cls.qex = (connectinfo.mode==CIM_QEONLY); else { //send a dummy packet. //this makes our local firewall think we initialised the conversation, so that we can receive their packets. however this only works if our nat uses the same public port for private ports. Netchan_Transmit(&cls.netchan, 1, "\x01", 2500); } return; case CCREP_REJECT: s = MSG_ReadString(); Con_Printf("Connect failed\n%s\n", s); if (connectinfo.trying && CL_IsPendingServerBaseAddress(&net_from) == false) Cvar_Set(&cl_disconnectreason, s); return; } } #endif void CL_MVDUpdateSpectator (void); void CL_WriteDemoMessage (sizebuf_t *msg, int payloadoffset); void CL_ReadPacket(void) { if (!qrenderer) return; #ifdef HAVE_DTLS if (*(int *)net_message.data != -1) if (NET_DTLS_Decode(cls.sockets)) if (!net_message.cursize) return; #endif #if defined(SUPPORT_ICE) if (ICE_WasStun(cls.sockets)) return; #endif #ifdef NQPROT if (cls.demoplayback == DPB_NETQUAKE) { MSG_BeginReading (&net_message, cls.netchan.netprim); cls.netchan.last_received = realtime; CLNQ_ParseServerMessage (); return; } #endif #ifdef Q2CLIENT if (cls.demoplayback == DPB_QUAKE2) { MSG_BeginReading (&net_message, cls.netchan.netprim); cls.netchan.last_received = realtime; CLQ2_ParseServerMessage (); return; } #endif // // remote command packet // if (*(int *)net_message.data == -1) { CL_ConnectionlessPacket (); return; } if (cls.state == ca_disconnected) { //connect to nq servers, but don't get confused with sequenced packets. if (NET_WasSpecialPacket(cls.sockets)) return; #ifdef NQPROT CLNQ_ConnectionlessPacket (); #endif return; //ignore it. We arn't connected. } if (net_message.cursize < 6 && cls.demoplayback != DPB_MVD) //MVDs don't have the whole sequence header thing going on { char adr[MAX_ADR_SIZE]; if (net_message.cursize == 1 && net_message.data[0] == A2A_ACK) Con_TPrintf ("%s: Ack (Pong)\n", NET_AdrToString(adr, sizeof(adr), &net_from)); else Con_TPrintf ("%s: Runt packet (%i bytes)\n", NET_AdrToString(adr, sizeof(adr), &net_from), net_message.cursize); return; } // // packet from server // if (!cls.demoplayback && !NET_CompareAdr (&net_from, &cls.netchan.remote_address)) { char adr[MAX_ADR_SIZE]; if (NET_WasSpecialPacket(cls.sockets)) return; Con_DPrintf ("%s:sequenced packet from wrong server\n" ,NET_AdrToString(adr, sizeof(adr), &net_from)); return; } if (cls.netchan.flags&NCF_STUNAWARE) //should be safe to do this here. if (NET_WasSpecialPacket(cls.sockets)) return; switch(cls.protocol) { case CP_NETQUAKE: #ifdef NQPROT switch(NQNetChan_Process(&cls.netchan)) { case NQNC_IGNORED: break; case NQNC_ACK: case NQNC_RELIABLE: case NQNC_UNRELIABLE: MSG_ChangePrimitives(cls.netchan.netprim); CL_WriteDemoMessage (&net_message, MSG_GetReadCount()); CLNQ_ParseServerMessage (); break; } #endif break; case CP_PLUGIN: break; case CP_QUAKE2: #ifdef Q2CLIENT if (!Netchan_Process(&cls.netchan)) return; // wasn't accepted for some reason CLQ2_ParseServerMessage (); break; #endif case CP_QUAKE3: #ifdef Q3CLIENT { cactive_t newstate = q3->cl.ParseServerMessage(&net_message); if (newstate != cls.state) { cls.state = newstate; if (cls.state == ca_active) CL_MakeActive("Quake3Arena"); //became active, can flush old stuff now. } } #endif break; case CP_QUAKEWORLD: if (cls.demoplayback == DPB_MVD) { MSG_BeginReading(&net_message, cls.netchan.netprim); cls.netchan.last_received = realtime; cls.netchan.outgoing_sequence = cls.netchan.incoming_sequence; } else if (!Netchan_Process(&cls.netchan)) return; // wasn't accepted for some reason CL_WriteDemoMessage (&net_message, MSG_GetReadCount()); if (cls.netchan.incoming_sequence > cls.netchan.outgoing_sequence) { //server should not be responding to packets we have not sent yet Con_DPrintf("Server is from the future! (%i packets)\n", cls.netchan.incoming_sequence - cls.netchan.outgoing_sequence); cls.netchan.outgoing_sequence = cls.netchan.incoming_sequence; } MSG_ChangePrimitives(cls.netchan.netprim); CLQW_ParseServerMessage (); break; case CP_UNKNOWN: break; } } /* ================= CL_ReadPackets ================= */ void CL_ReadPackets (void) { if (cls.demoplayback != DPB_NONE) { while(CL_GetDemoMessage()) CL_ReadPacket(); } else NET_ReadPackets(cls.sockets); // // check timeout // if (cls.state >= ca_connected && realtime - cls.netchan.last_received > cl_timeout.value && !cls.demoplayback) { #ifdef HAVE_SERVER /*don't timeout when we're the actual server*/ if (!sv.state) #endif { Con_TPrintf ("\nServer connection timed out.\n"); CL_Disconnect ("Connection Timed Out"); return; } } if (cls.demoplayback == DPB_MVD) { CL_MVDUpdateSpectator(); } } //============================================================================= qboolean CL_AllowArbitaryDownload(const char *oldname, const char *localfile) { int allow; //never allow certain (native code) arbitary downloads. if (!Q_strncasecmp(localfile, "game", 4) || //q2-ey things !Q_strcasecmp(localfile, "progs.dat") || !Q_strcasecmp(localfile, "menu.dat") || !Q_strcasecmp(localfile, "csprogs.dat") || !Q_strcasecmp(localfile, "qwprogs.dat") || //overriding gamecode is bad (csqc should be dlcached) strstr(localfile, "\\") || strstr(localfile, "..") || strstr(localfile, "./") || strstr(localfile, ":") || strstr(localfile, "//") || //certain path patterns are just bad Q_strcasestr(localfile, ".qvm") || Q_strcasestr(localfile, ".dll") || Q_strcasestr(localfile, ".so") || Q_strcasestr(localfile, ".dylib")) //disallow any native code { //yes, I know the user can use a different progs from the one that is specified. If you leave it blank there will be no problem. (server isn't allowed to stuff progs cvar) Con_Printf("Ignoring arbitrary download to \"%s\" due to possible security risk\n", localfile); return false; } allow = cl_download_redirection.ival; if (allow == 2) { char ext[8]; COM_FileExtension(localfile, ext, sizeof(ext)); if (!strncmp(localfile, "demos/", 6) && (!Q_strcasecmp(ext, "mvd") || !Q_strcasecmp(ext, "gz"))) return true; //mvdsv popularised the server sending 'download demo/foobar.mvd' in response to 'download demonum/5' aka 'cmd dl #' else if (!strncmp(localfile, "package/", 8) && (!Q_strcasecmp(ext, "pak") || !Q_strcasecmp(ext, "pk3") || !Q_strcasecmp(ext, "pk4"))) return true; //packages, woo. //fixme: we should probably try using package/$gamedir/foo.pak if we get redirected to that. else { Con_Printf("Ignoring non-package download redirection to \"%s\"\n", localfile); return false; } } if (allow) return true; Con_Printf("Ignoring download redirection to \"%s\". This server may require you to set cl_download_redirection to 2.\n", localfile); return false; } #if defined(NQPROT) && defined(HAVE_LEGACY) //this is for DP compat. static void CL_Curl_f(void) { //curl --args url int i, argc = Cmd_Argc(); const char *arg, *gamedir, *localterse/*no dlcache*/= NULL; char localname[MAX_QPATH]; char localnametmp[MAX_QPATH]; int usage = 0; qboolean alreadyhave = false; extern char *cl_dp_packagenames; unsigned int dlflags = DLLF_VERBOSE|DLLF_ALLOWWEB; const char *ext; if (argc < 2) { Con_Printf("%s: No args\n", Cmd_Argv(0)); return; } // Con_Printf("%s %s\n", Cmd_Argv(0), Cmd_Args()); for (i = 1; i < argc; i++) { arg = Cmd_Argv(i); if (!strcmp(arg, "--info")) { Con_Printf("%s %s: not implemented\n", Cmd_Argv(0), arg); return; } else if (!strcmp(arg, "--cancel")) { Con_Printf("%s %s: not implemented\n", Cmd_Argv(0), arg); return; } else if (!strcmp(arg, "--pak")) usage |= 1; else if (!strcmp(arg, "--cachepic")) usage |= 2; else if (!strcmp(arg, "--skinframe")) usage |= 4; else if (!strcmp(arg, "--for")) { alreadyhave = true; //assume we have a package that satisfies the file name. for (i++; i < argc-1; i++) //all but the last... { arg = Cmd_Argv(i); if (!CL_CheckDLFile(arg)) { alreadyhave = false; //I guess we didn't after all. break; } } } else if (!strcmp(arg, "--forthismap")) { //'don't reconnect on failure' //though I'm guessing its better expressed as just flagging it as mandatory. dlflags |= DLLF_REQUIRED; } else if (!strcmp(arg, "--as")) { //explicit local filename localterse = Cmd_Argv(++i); } else if (!strcmp(arg, "--clear_autodownload")) { Z_Free(cl_dp_packagenames); cl_dp_packagenames = NULL; return; } else if (!strcmp(arg, "--finish_autodownload")) { //not really sure why this is needed // Con_Printf("%s %s: not implemented\n", Cmd_Argv(0), arg); return; } else if (!strcmp(arg, "--maxspeed=")) ; else if (*arg == '-') Con_Printf("%s: Unknown option %s\n", Cmd_Argv(0), arg); else ; //usually just the last arg, but may also be some parameter for an unknown arg. } arg = Cmd_Argv(argc-1); if (!localterse) { char *t; localterse = strrchr(arg, '/'); if (!localterse) localterse = arg; t = strchr(localterse, '?'); if (t) *t = 0; if (t-localterse < countof(localnametmp)) { memcpy(localnametmp, localterse, t-localterse); localnametmp[t-localterse] = 0; localterse = localnametmp; } } if (!localterse) { //for compat, we should look for the last / and truncate on a ?. Con_Printf("%s: skipping download of %s, as the local name was not explicitly given\n", Cmd_Argv(0), arg); return; } ext = COM_GetFileExtension(localterse, NULL); if (usage == 1 && (!strcmp(ext, ".pk3") || !strcmp(ext, ".pak"))) { dlflags |= DLLF_NONGAME; gamedir = FS_GetGamedir(true); FS_GenCachedPakName(va("%s/%s", gamedir, localterse), NULL, localname, sizeof(localname)); if (!alreadyhave) if (!CL_CheckOrEnqueDownloadFile(arg, localname, dlflags)) Con_Printf("Downloading %s to %s\n", arg, localname); if (cl_dp_packagenames) Z_StrCat(&cl_dp_packagenames, va("%s%s/%s", cl_dp_packagenames?" ":"", gamedir, localterse)); } else { Con_Printf("%s: %s: non-package downloads are not supported\n", Cmd_Argv(0), arg); return; } } #endif /* ===================== CL_Download_f ===================== */ void CL_Download_f (void) { // char *p, *q; char *url = Cmd_Argv(1); char *localname = Cmd_Argv(2); #ifdef WEBCLIENT if (!strnicmp(url, "http://", 7) || !strnicmp(url, "https://", 8) || !strnicmp(url, "ftp://", 6)) { if (Cmd_IsInsecure()) return; if (!*localname) { localname = strrchr(url, '/'); if (localname) localname++; else { Con_TPrintf ("no local name specified\n"); return; } } HTTP_CL_Get(url, localname, NULL);//"test.txt"); return; } #endif if (!strnicmp(url, "qw://", 5) || !strnicmp(url, "q2://", 5)) { url += 5; if (*url == '/') //a conforming url should always have a host section, an empty one is simply three slashes. url++; } if (!*localname) localname = url; if ((cls.state == ca_disconnected || cls.demoplayback) && !(cls.demoplayback == DPB_MVD && (cls.demoeztv_ext&EZTV_DOWNLOAD))) { Con_TPrintf ("Must be connected.\n"); return; } if (cls.netchan.remote_address.type == NA_LOOPBACK) { Con_TPrintf ("Must be connected.\n"); return; } if (Cmd_Argc() != 2 && Cmd_Argc() != 3) { Con_TPrintf ("Usage: download \n"); return; } if (Cmd_IsInsecure()) //mark server specified downloads. { if (cls.download && cls.download->method == DL_QWPENDING) DL_Abort(cls.download, QDL_FAILED); //don't let gamecode order us to download random junk if (!CL_AllowArbitaryDownload(NULL, localname)) return; CL_CheckOrEnqueDownloadFile(url, localname, DLLF_REQUIRED|DLLF_VERBOSE); return; } CL_EnqueDownload(url, localname, DLLF_USEREXPLICIT|DLLF_IGNOREFAILED|DLLF_REQUIRED|DLLF_OVERWRITE|DLLF_VERBOSE); } void CL_DownloadSize_f(void) { downloadlist_t *dl; char *rname; char *size; char *redirection; //if this is a demo.. urm? //ignore it. This saves any spam. if (cls.demoplayback) return; rname = Cmd_Argv(1); size = Cmd_Argv(2); if (!strcmp(size, "e")) { Con_Printf(CON_ERROR"Download of \"%s\" failed. Not found.\n", rname); CL_DownloadFailed(rname, NULL, DLFAIL_SERVERFILE); } else if (!strcmp(size, "p")) { if (cls.download && stricmp(cls.download->remotename, rname)) { Con_Printf(CON_ERROR"Download of \"%s\" failed. Not allowed.\n", rname); CL_DownloadFailed(rname, NULL, DLFAIL_SERVERCVAR); } } else if (!strcmp(size, "r")) { //'download this file instead' redirection = Cmd_Argv(3); if (!CL_AllowArbitaryDownload(rname, redirection)) return; dl = CL_DownloadFailed(rname, NULL, DLFAIL_REDIRECTED); Con_DPrintf("Download of \"%s\" redirected to \"%s\".\n", rname, redirection); if (!strncmp(redirection, "package/", 8)) { //redirected to a package, make sure we cache it in the proper place. char pkn[MAX_QPATH], pkh[32]; char localname[MAX_QPATH]; char *spn = cl.serverpacknames, *sph = cl.serverpackhashes; *pkh = 0; while(spn && sph) { spn=COM_ParseOut(spn, pkn, sizeof(pkn)); sph=COM_ParseOut(sph, pkh, sizeof(pkh)); if (!spn || !sph) break; if (!strcmp(pkn, redirection+8)) break; *pkh = 0; } if (*pkh) if (FS_GenCachedPakName(redirection+8, pkh, localname, sizeof(localname))) CL_CheckOrEnqueDownloadFile(redirection+8, localname, DLLF_NONGAME); } else CL_CheckOrEnqueDownloadFile(redirection, NULL, dl->flags); } else { for (dl = cl.downloadlist; dl; dl = dl->next) { if (!strcmp(dl->rname, rname)) { dl->size = strtoul(size, NULL, 0); dl->flags &= ~DLLF_SIZEUNKNOWN; return; } } } } void CL_FinishDownload(char *filename, char *tempname); static void CL_ForceStopDownload (qboolean finish) { qdownload_t *dl = cls.download; if (Cmd_IsInsecure()) { Con_Printf(CON_WARNING "Execution from server rejected for %s\n", Cmd_Argv(0)); return; } if (!dl) return; if (!dl->file) { if (dl->method == DL_QWPENDING) finish = false; else { Con_Printf("No files downloading by QW protocol\n"); return; } } if (finish) DL_Abort(dl, QDL_COMPLETED); else DL_Abort(dl, QDL_FAILED); // get another file if needed CL_RequestNextDownload (); } void CL_SkipDownload_f (void) { CL_ForceStopDownload(false); } void CL_FinishDownload_f (void) { CL_ForceStopDownload(true); } #if defined(_WIN32) && !defined(WINRT) && !defined(_XBOX) #include "winquake.h" /* ================= CL_Minimize_f ================= */ void CL_Windows_f (void) { if (!mainwindow) { Con_Printf("Cannot comply\n"); return; } // if (modestate == MS_WINDOWED) // ShowWindow(mainwindow, SW_MINIMIZE); // else SendMessage(mainwindow, WM_SYSKEYUP, VK_TAB, 1 | (0x0F << 16) | (1<<29)); } #endif #ifdef HAVE_SERVER void CL_ServerInfo_f(void) { if (!sv.state && cls.state && Cmd_Argc() < 2) { if (cl.haveserverinfo) { InfoBuf_Print (&cl.serverinfo, ""); Con_Printf("[%u, %s]\n", (unsigned int)cl.serverinfo.totalsize, cls.servername); } else Cmd_ForwardToServer (); } else { SV_Serverinfo_f(); //allow it to be set... (whoops) } } #endif #ifdef FTPCLIENT void CL_FTP_f(void) { FTP_Client_Command(Cmd_Args(), NULL); } #endif //fixme: make a cvar void CL_Fog_f(void) { int ftype; vec3_t rgb; if (!Q_strcasecmp(Cmd_Argv(0), "waterfog")) ftype = FOGTYPE_WATER; else if (!Q_strcasecmp(Cmd_Argv(0), "skyroomfog")) ftype = FOGTYPE_SKYROOM; else //fog ftype = FOGTYPE_AIR; if ((cl.fog_locked && !Cmd_FromGamecode() && !cls.allow_cheats) || Cmd_Argc() <= 1) { static const char *fognames[FOGTYPE_COUNT]={"fog","waterfog","skyroomfog"}; if (Cmd_ExecLevel != RESTRICT_INSECURE) Con_Printf("Current %s %f (r:%f g:%f b:%f, a:%f bias:%f)\n", fognames[ftype], cl.fog[ftype].density, cl.fog[ftype].colour[0], cl.fog[ftype].colour[1], cl.fog[ftype].colour[2], cl.fog[ftype].alpha, cl.fog[ftype].depthbias); } else { CL_ResetFog(ftype); VectorSet(rgb, 0.3,0.3,0.3); switch(Cmd_Argc()) { case 1: break; case 2: cl.fog[ftype].density = atof(Cmd_Argv(1)); break; case 3: cl.fog[ftype].density = atof(Cmd_Argv(1)); rgb[0] = rgb[1] = rgb[2] = atof(Cmd_Argv(2)); break; case 4: cl.fog[ftype].density = 0.05; //make something up for vauge compat with fitzquake, so it doesn't get the default of 0 rgb[0] = atof(Cmd_Argv(1)); rgb[1] = atof(Cmd_Argv(2)); rgb[2] = atof(Cmd_Argv(3)); break; case 5: default: cl.fog[ftype].density = atof(Cmd_Argv(1)); rgb[0] = atof(Cmd_Argv(2)); rgb[1] = atof(Cmd_Argv(3)); rgb[2] = atof(Cmd_Argv(4)); break; } if (rgb[0]>=2 || rgb[1]>=2 || rgb[2]>=2) //we allow SOME slop for hdr fog... hopefully we won't need it. this is mostly just an issue when skyfog is enabled[default .5] ('why is my sky white on map FOO') Con_Printf(CON_WARNING "Fog colour of %g %g %g exceeds standard 0-1 range\n", rgb[0], rgb[1], rgb[2]); cl.fog[ftype].colour[0] = SRGBf(rgb[0]); cl.fog[ftype].colour[1] = SRGBf(rgb[1]); cl.fog[ftype].colour[2] = SRGBf(rgb[2]); if (cls.state == ca_active) cl.fog[ftype].time += 1; //fitz: //if (Cmd_Argc() >= 6) cl.fog[ftype].time += atof(Cmd_Argv(5)); //dp: if (Cmd_Argc() >= 6) cl.fog[ftype].alpha = atof(Cmd_Argv(5)); if (Cmd_Argc() >= 7) cl.fog[ftype].depthbias = atof(Cmd_Argv(6)); //if (Cmd_Argc() >= 8) cl.fog[ftype].end = atof(Cmd_Argv(7)); //if (Cmd_Argc() >= 9) cl.fog[ftype].height = atof(Cmd_Argv(8)); //if (Cmd_Argc() >= 10) cl.fog[ftype].fadedepth = atof(Cmd_Argv(9)); if (Cmd_FromGamecode()) cl.fog_locked = !!cl.fog[ftype].density; #ifdef HAVE_LEGACY if (cl.fog[ftype].colour[0] > 1 || cl.fog[ftype].colour[1] > 1 || cl.fog[ftype].colour[2] > 1) Con_DPrintf(CON_WARNING "Fog is oversaturated. This can result in compatibility issues.\n"); #endif } } #ifdef _DEBUG void CL_FreeSpace_f(void) { char buf[32]; quint64_t freespace; const char *freepath = Cmd_Argv(1); if (Sys_GetFreeDiskSpace(freepath, &freespace)) Con_Printf("%s: %s available\n", freepath, FS_AbbreviateSize(buf,sizeof(buf),freespace)); else Con_Printf("%s: disk free not queryable\n", freepath); } #endif void CL_CrashMeEndgame_f(void) { Host_EndGame("crashme! %s", Cmd_Args()); } void CL_CrashMeError_f(void) { Sys_Error("crashme! %s", Cmd_Args()); } static char *ShowTime(unsigned int seconds) { char buf[1024]; char *b = buf; *b = 0; if (seconds > 60) { if (seconds > 60*60) { if (seconds > 24*60*60) { strcpy(b, va("%id ", seconds/(24*60*60))); b += strlen(b); seconds %= 24*60*60; } strcpy(b, va("%ih ", seconds/(60*60))); b += strlen(b); seconds %= 60*60; } strcpy(b, va("%im ", seconds/60)); b += strlen(b); seconds %= 60; } strcpy(b, va("%is", seconds)); b += strlen(b); return va("%s", buf); } void CL_Status_f(void) { #ifdef CSQC_DAT extern world_t csqc_world; #endif char adr[128]; float pi, po, bi, bo; NET_PrintAddresses(cls.sockets); NET_PrintConnectionsStatus(cls.sockets); if (NET_GetRates(cls.sockets, &pi, &po, &bi, &bo)) Con_Printf("packets,bytes/sec: in: %g %g out: %g %g\n", pi, bi, po, bo); //not relevent as a limit. if (cls.state) { char cert[8192]; qbyte fp[DIGEST_MAXSIZE+1]; char b64[(DIGEST_MAXSIZE*4)/3+1]; if (NET_GetConnectionCertificate(cls.sockets, &cls.netchan.remote_address, QCERT_ISENCRYPTED, NULL, 0)) Q_strncpyz(b64, "", sizeof(b64)); else { int sz = NET_GetConnectionCertificate(cls.sockets, &cls.netchan.remote_address, QCERT_PEERCERTIFICATE, cert, sizeof(cert)); if (sz<0) Q_strncpyz(b64, "", sizeof(b64)); else { sz = Base64_EncodeBlockURI(fp, CalcHash(&hash_certfp, fp,sizeof(fp), cert, sz), b64, sizeof(b64)); b64[sz] = 0; } } Con_Printf("Server address : %s\n", NET_AdrToString(adr, sizeof(adr), &cls.netchan.remote_address)); //not relevent as a limit. Con_Printf("Server cert fp : %s\n", b64); //not relevent as a limit. Con_Printf("Network MTU : %u (max %u) %s\n", cls.netchan.mtu_cur, cls.netchan.mtu_max, (cls.netchan.flags&NCF_FRAGABLE)?"":" (strict)"); //not relevent as a limit. switch(cls.protocol) { default: case CP_UNKNOWN: Con_Printf("Network Protocol : Unknown\n"); break; case CP_QUAKEWORLD: Con_Printf("Network Protocol : QuakeWorld\n"); break; #ifdef NQPROT case CP_NETQUAKE: switch(cls.protocol_nq) { case CPNQ_ID: if (cls.proquake_angles_hack) Con_Printf("Network Protocol : ProQuake\n"); else Con_Printf("Network Protocol : NetQuake\n"); break; case CPNQ_NEHAHRA: Con_Printf("Network Protocol : Nehahra\n"); break; case CPNQ_BJP1: Con_Printf("Network Protocol : BJP1\n"); break; case CPNQ_BJP2: Con_Printf("Network Protocol : BJP2\n"); break; case CPNQ_BJP3: Con_Printf("Network Protocol : BJP3\n"); break; case CPNQ_H2MP: Con_Printf("Network Protocol : H2MP\n"); break; case CPNQ_FITZ666: Con_Printf("Network Protocol : FitzQuake\n"); break; case CPNQ_DP5: Con_Printf("Network Protocol : DPP5\n"); break; case CPNQ_DP6: Con_Printf("Network Protocol : DPP6\n"); break; case CPNQ_DP7: Con_Printf("Network Protocol : DPP7\n"); break; } break; #endif #ifdef Q2CLIENT case CP_QUAKE2: switch (cls.protocol_q2) { case PROTOCOL_VERSION_Q2: Con_Printf("Network Protocol : Quake2\n"); break; case PROTOCOL_VERSION_R1Q2: Con_Printf("Network Protocol : R1Q2\n"); break; case PROTOCOL_VERSION_Q2PRO: Con_Printf("Network Protocol : Q2Pro\n"); break; case PROTOCOL_VERSION_Q2EXDEMO: case PROTOCOL_VERSION_Q2EX: Con_Printf("Network Protocol : Quake2Ex\n"); break; default: Con_Printf("Network Protocol : Quake2 (OLD)\n"); break; } break; #endif #ifdef Q3CLIENT case CP_QUAKE3: Con_Printf("Network Protocol : Quake3\n"); break; #endif #ifdef PLUGINS case CP_PLUGIN: Con_Printf("Network Protocol : (unknown, provided by plugin)\n"); break; #endif } //just show the more interesting extensions. if (cls.fteprotocolextensions & PEXT_FLOATCOORDS) Con_Printf("\textended coords\n"); if (cls.fteprotocolextensions & PEXT_SPLITSCREEN) Con_Printf("\tsplit screen\n"); if (cls.fteprotocolextensions & PEXT_CSQC) Con_Printf("\tcsqc info\n"); if (cls.fteprotocolextensions2 & PEXT2_VOICECHAT) Con_Printf("\tvoice chat\n"); if (cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) Con_Printf("\treplacement deltas\n"); if (cls.fteprotocolextensions2 & PEXT2_VRINPUTS) Con_Printf("\tvrinputs\n"); if (cls.fteprotocolextensions2 & PEXT2_INFOBLOBS) Con_Printf("\tinfoblobs\n"); } if (cl.worldmodel) { Con_Printf("map uptime : %s\n", ShowTime(cl.time)); COM_FileBase(cl.worldmodel->name, adr, sizeof(adr)); Con_Printf ("current map : %s (%s)\n", adr, cl.levelname); } #ifdef CSQC_DAT if (csqc_world.progs) { extern int num_sfx; int count = 0, i; edict_t *e; Con_Printf ("csqc : loaded\n"); for (i = 0; i < csqc_world.num_edicts; i++) { e = EDICT_NUM_PB(csqc_world.progs, i); if (e && e->ereftype == ER_FREE && Sys_DoubleTime() - e->freetime > 0.5) continue; //free, and older than the zombie time count++; } Con_Printf("csqc entities : %i/%i/%i (mem: %.1f%%)\n", count, csqc_world.num_edicts, csqc_world.max_edicts, 100*(float)(csqc_world.progs->stringtablesize/(double)csqc_world.progs->stringtablemaxsize)); for (count = 1; count < MAX_CSMODELS; count++) if (!*cl.model_csqcname[count]) break; Con_Printf("csqc models : %i/%i\n", count, MAX_CSMODELS); Con_Printf("client sounds : %i\n", num_sfx); //there is a limit, its just private. :( for (count = 1; count < MAX_SSPARTICLESPRE; count++) if (!cl.particle_csname[count]) break; if (count!=1) Con_Printf("csqc particles : %i/%i\n", count, MAX_CSPARTICLESPRE); if (cl.csqcdebug) Con_Printf("csqc debug : true\n"); } else Con_Printf ("csqc : not loaded\n"); #endif Con_Printf("gamedir : %s\n", FS_GetGamedir(true)); } void CL_Demo_SetSpeed_f(void) { char *s = Cmd_Argv(1); if (s) { float f = atof(s)/100; Cvar_SetValue(&cl_demospeed, f); } else Con_Printf("demo playback speed %g%%\n", cl_demospeed.value * 100); } static void CL_UserinfoChanged(void *ctx, const char *keyname) { InfoSync_Add(&cls.userinfosync, ctx, keyname); } void CL_Skygroup_f(void); void WAD_ImageList_f(void); /* ================= CL_Init ================= */ void CL_Init (void) { extern void CL_Say_f (void); extern void CL_SayMe_f (void); extern void CL_SayTeam_f (void); #ifdef QWSKINS extern cvar_t baseskin; extern cvar_t noskins; #endif char *ver; size_t seat; cls.state = ca_disconnected; cls.demotrack = -1; cls.demonum = -1; #ifdef SVNREVISION if (strcmp(STRINGIFY(SVNREVISION), "-")) ver = va("%s v%i.%02i %s", DISTRIBUTION, FTE_VER_MAJOR, FTE_VER_MINOR, STRINGIFY(SVNREVISION)); else #endif ver = va("%s v%i.%02i", DISTRIBUTION, FTE_VER_MAJOR, FTE_VER_MINOR); for (seat = 0; seat < MAX_SPLITS; seat++) { cls.userinfo[seat].ChangeCTX = &cls.userinfo[seat]; cls.userinfo[seat].ChangeCB = CL_UserinfoChanged; InfoBuf_SetStarKey (&cls.userinfo[seat], "*ver", ver); } InitValidation(); CL_InitInput (); CL_InitTEnts (); CL_InitPrediction (); CL_InitCam (); CL_InitDlights(); PM_Init (); TP_Init(); // // register our commands // CLSCR_Init(); #ifdef MENU_DAT MP_RegisterCvarsAndCmds(); #endif #ifdef CSQC_DAT CSQC_RegisterCvarsAndThings(); #endif Cvar_Register (&host_speeds, cl_controlgroup); Cvar_Register (&cfg_save_name, cl_controlgroup); Cvar_Register (&cl_disconnectreason, cl_controlgroup); Cvar_Register (&cl_proxyaddr, cl_controlgroup); Cvar_Register (&cl_sendguid, cl_controlgroup); Cvar_Register (&cl_defaultport, cl_controlgroup); Cvar_Register (&cl_servername, cl_controlgroup); Cvar_Register (&cl_serveraddress, cl_controlgroup); Cvar_Register (&cl_demospeed, "Demo playback"); Cmd_AddCommand("demo_setspeed", CL_Demo_SetSpeed_f); Cvar_Register (&cl_upspeed, cl_inputgroup); Cvar_Register (&cl_forwardspeed, cl_inputgroup); Cvar_Register (&cl_backspeed, cl_inputgroup); Cvar_Register (&cl_sidespeed, cl_inputgroup); Cvar_Register (&cl_movespeedkey, cl_inputgroup); Cvar_Register (&cl_yawspeed, cl_inputgroup); Cvar_Register (&cl_pitchspeed, cl_inputgroup); Cvar_Register (&cl_anglespeedkey, cl_inputgroup); Cvar_Register (&cl_shownet, cl_screengroup); Cvar_Register (&cl_sbar, cl_screengroup); Cvar_Register (&cl_pure, cl_screengroup); Cvar_Register (&cl_hudswap, cl_screengroup); Cvar_Register (&cl_maxfps, cl_screengroup); Cvar_Register (&cl_maxfps_slop, cl_screengroup); Cvar_Register (&cl_idlefps, cl_screengroup); Cvar_Register (&cl_yieldcpu, cl_screengroup); Cvar_Register (&cl_timeout, cl_controlgroup); Cvar_Register (&cl_vrui_force, cl_controlgroup); Cvar_Register (&cl_vrui_lock, cl_controlgroup); Cvar_Register (&lookspring, cl_inputgroup); Cvar_Register (&lookstrafe, cl_inputgroup); Cvar_Register (&sensitivity, cl_inputgroup); Cvar_Register (&m_pitch, cl_inputgroup); Cvar_Register (&m_yaw, cl_inputgroup); Cvar_Register (&m_forward, cl_inputgroup); Cvar_Register (&m_side, cl_inputgroup); Cvar_Register (&cl_crypt_rcon, cl_controlgroup); Cvar_Register (&rcon_password, cl_controlgroup); Cvar_Register (&rcon_address, cl_controlgroup); Cvar_Register (&cl_lerp_maxinterval, cl_controlgroup); Cvar_Register (&cl_lerp_maxdistance, cl_controlgroup); Cvar_Register (&cl_lerp_players, cl_controlgroup); Cvar_Register (&cl_predict_players, cl_predictiongroup); Cvar_Register (&cl_predict_players_frac, cl_predictiongroup); Cvar_Register (&cl_predict_players_latency, cl_predictiongroup); Cvar_Register (&cl_predict_players_nudge, cl_predictiongroup); Cvar_Register (&cl_solid_players, cl_predictiongroup); #ifdef QUAKESPYAPI Cvar_Register (&localid, cl_controlgroup); #endif Cvar_Register (&cl_muzzleflash, cl_controlgroup); #ifdef QWSKINS Cvar_Register (&baseskin, "Teamplay"); Cvar_Register (&noskins, "Teamplay"); #endif Cvar_Register (&cl_noblink, "Console controls"); //for lack of a better group Cvar_Register (&cl_item_bobbing, "Item effects"); Cvar_Register (&gl_simpleitems, "Item effects"); Cvar_Register (&cl_staticsounds, "Item effects"); Cvar_Register (&r_torch, "Item effects"); Cvar_Register (&r_rocketlight, "Item effects"); Cvar_Register (&r_lightflicker, "Item effects"); Cvar_Register (&cl_r2g, "Item effects"); Cvar_Register (&r_powerupglow, "Item effects"); Cvar_Register (&v_powerupshell, "Item effects"); Cvar_Register (&cl_gibfilter, "Item effects"); Cvar_Register (&cl_deadbodyfilter, "Item effects"); Cvar_Register (&cl_nolerp, "Item effects"); #ifdef NQPROT Cvar_Register (&cl_nolerp_netquake, "Item effects"); Cvar_Register (&cl_fullpitch_nq, "Cheats"); #endif Cvar_Register (&r_drawflame, "Item effects"); Cvar_Register (&cl_downloads, cl_controlgroup); Cvar_Register (&cl_download_csprogs, cl_controlgroup); Cvar_Register (&cl_download_redirection, cl_controlgroup); Cvar_Register (&cl_download_packages, cl_controlgroup); // // info mirrors // Cvar_Register (&name, cl_controlgroup); Cvar_Register (&password, cl_controlgroup); Cvar_Register (&spectator, cl_controlgroup); Cvar_Register (&skin, cl_controlgroup); Cvar_Register (&model, cl_controlgroup); Cvar_Register (&team, cl_controlgroup); Cvar_Register (&topcolor, cl_controlgroup); Cvar_Register (&bottomcolor, cl_controlgroup); Cvar_Register (&rate, cl_controlgroup); Cvar_Register (&drate, cl_controlgroup); Cvar_Register (&msg, cl_controlgroup); #ifdef Q2CLIENT Cvar_Register (&hand, cl_controlgroup); #endif Cvar_Register (&noaim, cl_controlgroup); Cvar_Register (&b_switch, cl_controlgroup); Cvar_Register (&w_switch, cl_controlgroup); #ifdef HEXEN2 Cvar_Register (&cl_playerclass, cl_controlgroup); #endif Cvar_Register (&cl_demoreel, cl_controlgroup); Cvar_Register (&record_flush, cl_controlgroup); Cvar_Register (&cl_nofake, cl_controlgroup); Cvar_Register (&cl_chatsound, cl_controlgroup); Cvar_Register (&cl_enemychatsound, cl_controlgroup); Cvar_Register (&cl_teamchatsound, cl_controlgroup); Cvar_Register (&requiredownloads, cl_controlgroup); Cvar_Register (&mod_precache, cl_controlgroup); Cvar_Register (&cl_standardchat, cl_controlgroup); Cvar_Register (&msg_filter, cl_controlgroup); Cvar_Register (&msg_filter_frags, cl_controlgroup); Cvar_Register (&cl_standardmsg, cl_controlgroup); Cvar_Register (&cl_parsewhitetext, cl_controlgroup); Cvar_Register (&cl_nopext, cl_controlgroup); Cvar_Register (&cl_pext_mask, cl_controlgroup); Cvar_Register (&cl_splitscreen, cl_controlgroup); Cvar_Register (&cl_fakeframes, cl_controlgroup); #ifndef SERVERONLY Cvar_Register (&cl_loopbackprotocol, cl_controlgroup); #endif Cvar_Register (&cl_verify_urischeme, cl_controlgroup); Cvar_Register (&cl_countpendingpl, cl_controlgroup); Cvar_Register (&cl_threadedphysics, cl_controlgroup); hud_tracking_show = Cvar_Get("hud_tracking_show", "1", 0, "statusbar"); hud_miniscores_show = Cvar_Get("hud_miniscores_show", "1", 0, "statusbar"); Cvar_Register (&cl_dlemptyterminate, cl_controlgroup); Cvar_Register (&cl_gunx, cl_controlgroup); Cvar_Register (&cl_guny, cl_controlgroup); Cvar_Register (&cl_gunz, cl_controlgroup); Cvar_Register (&cl_gunanglex, cl_controlgroup); Cvar_Register (&cl_gunangley, cl_controlgroup); Cvar_Register (&cl_gunanglez, cl_controlgroup); Cvar_Register (&ruleset_allow_playercount, cl_controlgroup); Cvar_Register (&ruleset_allow_frj, cl_controlgroup); Cvar_Register (&ruleset_allow_semicheats, cl_controlgroup); Cvar_Register (&ruleset_allow_packet, cl_controlgroup); Cvar_Register (&ruleset_allow_particle_lightning, cl_controlgroup); Cvar_Register (&ruleset_allow_overlongsounds, cl_controlgroup); Cvar_Register (&ruleset_allow_larger_models, cl_controlgroup); Cvar_Register (&ruleset_allow_modified_eyes, cl_controlgroup); Cvar_Register (&ruleset_allow_sensitive_texture_replacements, cl_controlgroup); Cvar_Register (&ruleset_allow_localvolume, cl_controlgroup); Cvar_Register (&ruleset_allow_shaders, cl_controlgroup); Cvar_Register (&ruleset_allow_watervis, cl_controlgroup); Cvar_Register (&ruleset_allow_fbmodels, cl_controlgroup); Cvar_Register (&qtvcl_forceversion1, cl_controlgroup); Cvar_Register (&qtvcl_eztvextensions, cl_controlgroup); #ifdef FTPCLIENT Cmd_AddCommand ("ftp", CL_FTP_f); #endif Cmd_AddCommandD ("changing", CL_Changing_f, "Part of network protocols. This command should not be used manually."); Cmd_AddCommand ("disconnect", CL_Disconnect_f); Cmd_AddCommandAD ("record", CL_Record_f, CL_DemoList_c, NULL); Cmd_AddCommandAD ("rerecord", CL_ReRecord_f, CL_DemoList_c, "Reconnects to the previous/current server, but starts recording a clean demo."); Cmd_AddCommandD ("stop", CL_Stop_f, "Stop demo recording."); Cmd_AddCommandAD ("playdemo", CL_PlayDemo_f, CL_DemoList_c, NULL); Cmd_AddCommand ("qtvplay", CL_QTVPlay_f); Cmd_AddCommand ("qtvlist", CL_QTVList_f); Cmd_AddCommand ("qtvdemos", CL_QTVDemos_f); Cmd_AddCommandD ("demo_jump", CL_DemoJump_f, "Jump to a specified time in a demo. Prefix with a + or - for a relative offset. Seeking backwards will restart the demo and the fast forward, which can take some time in long demos."); Cmd_AddCommandD ("demo_jump_mark", CL_DemoJump_f, "Jump to the next '//demomark' marker."); Cmd_AddCommandD ("demo_jump_end", CL_DemoJump_f, "Jump to the next intermission message."); Cmd_AddCommandD ("demo_nudge", CL_DemoNudge_f, "Nudge the demo by one frame. Argument should be +1 or -1. Nudging backwards is limited."); Cmd_AddCommandAD ("timedemo", CL_TimeDemo_f, CL_DemoList_c, NULL); #ifdef _DEBUG Cmd_AddCommand ("freespace", CL_FreeSpace_f); Cmd_AddCommand ("crashme_endgame", CL_CrashMeEndgame_f); Cmd_AddCommand ("crashme_error", CL_CrashMeError_f); #endif Cmd_AddCommandD ("showpic", SCR_ShowPic_Script_f, "showpic [width] [height] [touchcommand]\nDisplays an image onscreen, that potentially has a key binding attached to it when clicked/touched.\nzone should be one of: TL, TR, BL, BR, MM, TM, BM, ML, MR. This serves as an extra offset to move the image around the screen without any foreknowledge of the screen resolution."); Cmd_AddCommandD ("showpic_removeall", SCR_ShowPic_Remove_f, "removes any pictures inserted with the showpic command."); Cmd_AddCommandD ("startdemos", CL_Startdemos_f, "Sets the demoreel list, but does not start playing them (use the 'demos' command for that)"); Cmd_AddCommandD ("demos", CL_Demos_f, "Starts playing the demo reel."); Cmd_AddCommand ("stopdemo", CL_Stopdemo_f); Cmd_AddCommand ("skins", Skin_Skins_f); #ifdef QWSKINS Cmd_AddCommand ("allskins", Skin_AllSkins_f); #endif Cmd_AddCommand ("cl_status", CL_Status_f); Cmd_AddCommandD ("quit", CL_Quit_f, "Use this command when you get angry. Does not save any cvars. Use cfg_save to save settings, or use the menu for a prompt."); #if defined(CL_MASTER) && defined(HAVE_PACKET) Cmd_AddCommandAD ("connectbr", CL_ConnectBestRoute_f, CL_Connect_c, "connect address:port\nConnect to a qw server using the best route we can detect."); #endif Cmd_AddCommandAD("connect", CL_Connect_f, CL_Connect_c, "connect scheme://address:port\nConnect to a server. " #if defined(FTE_TARGET_WEB) "Use a scheme of rtc[s]://broker/gamename to connect via a webrtc broker." "Use a scheme of ws[s]://server to connect via websockets." #elif defined(TCPCONNECT) "Use a scheme of tcp:// or tls:// to connect via non-udp protocols." // "Use a scheme of ws[s]://server to connect via websockets." #endif #ifdef HAVE_DTLS "Use a scheme of dtls://server to connect securely." #endif #if defined(IRCCONNECT) "Use irc://network:6667/user[@channel] to connect via an irc server. Not recommended." #endif #if defined(NQPROT) || defined(Q2CLIENT) || defined(Q3CLIENT) "\nDefault port is port "STRINGIFY(PORT_DEFAULTSERVER)"." #ifndef GAME_DEFAULTPORT #ifdef NQPROT " NQ:"STRINGIFY(PORT_NQSERVER)"." #endif " QW:"STRINGIFY(PORT_QWSERVER)"." #ifdef Q2CLIENT " Q2:"STRINGIFY(PORT_Q2SERVER)"." #endif #ifdef Q3CLIENT " Q3:"STRINGIFY(PORT_Q3SERVER)"." #endif #endif #endif ); Cmd_AddCommandD ("cl_transfer", CL_Transfer_f, "Connect to a different server, disconnecting from the current server only when the new server replies."); #ifdef TCPCONNECT Cmd_AddCommandAD ("connecttcp", CL_TCPConnect_f, CL_Connect_c, "Connect to a server using the tcp:// prefix"); Cmd_AddCommandAD ("tcpconnect", CL_TCPConnect_f, CL_Connect_c, "Connect to a server using the tcp:// prefix"); #endif #ifdef IRCCONNECT Cmd_AddCommand ("connectirc", CL_IRCConnect_f); #endif #ifdef NQPROT Cmd_AddCommandD ("connectnq", CLNQ_Connect_f, "Connects to the specified server, defaulting to port "STRINGIFY(PORT_NQSERVER)". Also disables QW/Q2/Q3/DP handshakes preventing them from being favoured, so should only be used when you actually want NQ protocols specifically."); #ifdef HAVE_DTLS Cmd_AddCommandD ("connectqe", CLNQ_Connect_f, "Connects to the specified server, defaulting to port "STRINGIFY(PORT_NQSERVER)". Also forces the use of DTLS and QE-specific handshakes. You will also need to ensure the dtls_psk_* cvars are set properly or the server will refuse the connection."); #endif #endif #ifdef Q2CLIENT Cmd_AddCommandD ("connectq2e", CLQ2E_Connect_f, "Connects to the specified server, defaulting to port "STRINGIFY(PORT_Q2EXSERVER)"."); #endif #ifdef HAVE_LEGACY Cmd_AddCommandAD("qwurl", CL_Connect_f, CL_Connect_c, "For compat with ezquake."); #endif Cmd_AddCommand ("reconnect", CL_Reconnect_f); Cmd_AddCommandAD ("join", CL_Join_f, CL_Connect_c, "Switches away from spectator mode, optionally connecting to a different server."); Cmd_AddCommandAD ("observe", CL_Observe_f, CL_Connect_c, "Switches to spectator mode, optionally connecting to a different server."); Cmd_AddCommand ("rcon", CL_Rcon_f); Cmd_AddCommand ("packet", CL_Packet_f); Cmd_AddCommand ("user", CL_User_f); Cmd_AddCommand ("users", CL_Users_f); #if 1//def _DEBUG Cmd_AddCommand ("setinfoblob", CL_SetInfoBlob_f); #endif Cmd_AddCommand ("setinfo", CL_SetInfo_f); Cmd_AddCommand ("fullinfo", CL_FullInfo_f); Cmd_AddCommandAD ("color", CL_Color_f, CL_Color_c, NULL); #if defined(NQPROT) && defined(HAVE_LEGACY) Cmd_AddCommandD ("curl", CL_Curl_f, "For use by xonotic."); #endif Cmd_AddCommand ("download", CL_Download_f); Cmd_AddCommandD ("dlsize", CL_DownloadSize_f, "For internal use"); Cmd_AddCommandD ("nextul", CL_NextUpload, "For internal use"); Cmd_AddCommandD ("stopul", CL_StopUpload, "For internal use"); Cmd_AddCommand ("skipdl", CL_SkipDownload_f); Cmd_AddCommand ("finishdl", CL_FinishDownload_f); // // forward to server commands // Cmd_AddCommand ("god", NULL); //cheats Cmd_AddCommand ("give", NULL); Cmd_AddCommand ("noclip", NULL); Cmd_AddCommand ("6dof", NULL); Cmd_AddCommand ("spiderpig", NULL); Cmd_AddCommand ("fly", NULL); Cmd_AddCommand ("setpos", NULL); Cmd_AddCommand ("notarget", NULL); Cmd_AddCommand ("topten", NULL); Cmd_AddCommand ("kill", NULL); Cmd_AddCommand ("pause", NULL); Cmd_AddCommandAD ("say", CL_Say_f, Key_EmojiCompletion_c, NULL); Cmd_AddCommandAD ("me", CL_SayMe_f, Key_EmojiCompletion_c, NULL); Cmd_AddCommandAD ("sayone", CL_Say_f, Key_EmojiCompletion_c, NULL); Cmd_AddCommandAD ("say_team", CL_SayTeam_f, Key_EmojiCompletion_c, NULL); #ifdef HAVE_SERVER Cmd_AddCommand ("serverinfo", CL_ServerInfo_f); #else Cmd_AddCommand ("serverinfo", NULL); #endif Cmd_AddCommandD ("fog", CL_Fog_f, "fog "); Cmd_AddCommandD ("waterfog", CL_Fog_f, "waterfog "); Cmd_AddCommandD ("skyroomfog", CL_Fog_f, "skyroomfog "); Cmd_AddCommandD ("skygroup", CL_Skygroup_f, "Provides a way to associate a skybox name with a series of maps, so that the requested skybox will override on a per-map basis."); Cmd_AddCommandD ("r_imagelist_wad", WAD_ImageList_f, "displays the available wad images."); // // Windows commands // #if defined(_WIN32) && !defined(WINRT) && !defined(_XBOX) Cmd_AddCommand ("windows", CL_Windows_f); #endif Ignore_Init(); #ifdef QUAKEHUD Stats_Init(); #endif CL_ClearState(false); //make sure the cl.* fields are set properly if there's no ssqc or whatever. R_BumpLightstyles(1); } /* ================ Host_EndGame Call this to drop to a console without exiting the qwcl ================ */ NORETURN void VARGS Host_EndGame (const char *message, ...) { va_list argptr; char string[1024]; if (message) { va_start (argptr,message); vsnprintf (string,sizeof(string)-1, localtext(message),argptr); va_end (argptr); } else *string = 0; COM_AssertMainThread(string); SCR_EndLoadingPlaque(); if (message) { Con_TPrintf ("^&C0Host_EndGame: %s\n", string); Con_Printf ("\n"); } SCR_EndLoadingPlaque(); CL_Disconnect (string); CL_ConnectAbort(NULL); SV_UnspawnServer(); Cvar_Set(&cl_shownet, "0"); longjmp (host_abort, 1); } /* ================ Host_Error This shuts down the client and exits qwcl ================ */ void VARGS Host_Error (const char *error, ...) { va_list argptr; char string[1024]; static qboolean inerror = false; if (inerror) Sys_Error ("Host_Error: recursively entered"); inerror = true; va_start (argptr,error); vsnprintf (string,sizeof(string)-1, localtext(error),argptr); va_end (argptr); COM_AssertMainThread(string); Con_TPrintf ("Host_Error: %s\n", string); CL_Disconnect (string); cls.demonum = -1; inerror = false; // FIXME Sys_Error ("Host_Error: %s\n",string); } /* =============== Host_WriteConfiguration Writes key bindings and archived cvars to config.cfg =============== */ void Host_WriteConfiguration (void) { vfsfile_t *f; char savename[MAX_OSPATH]; char sysname[MAX_OSPATH]; if (host_initialized && cfg_save_name.string && *cfg_save_name.string) { if (strchr(cfg_save_name.string, '.')) { Con_TPrintf (CON_ERROR "Couldn't write config.cfg.\n"); return; } Q_snprintfz(savename, sizeof(savename), "%s.cfg", cfg_save_name.string); f = FS_OpenVFS(savename, "wb", FS_GAMEONLY); if (!f) { FS_DisplayPath(savename, FS_GAMEONLY, sysname, sizeof(sysname)); Con_TPrintf (CON_ERROR "Couldn't write %s.\n", sysname); return; } Key_WriteBindings (f); Cvar_WriteVariables (f, false, false); VFS_CLOSE (f); FS_DisplayPath(savename, FS_GAMEONLY, sysname, sizeof(sysname)); Con_Printf("Wrote %s\n", savename); } } //============================================================================ #if 0 /* ================== Host_SimulationTime This determines if enough time has passed to run a simulation frame ================== */ qboolean Host_SimulationTime(float time) { float fps; if (oldrealtime > realtime) oldrealtime = 0; if (cl_maxfps.value) fps = max(30.0, min(cl_maxfps.value, 72.0)); else fps = max(30.0, min(rate.value/80.0, 72.0)); if (!cls.timedemo && (realtime + time) - oldrealtime < 1.0/fps) return false; // framerate is too high return true; } #endif #include "fs.h" #define HRF_OVERWRITE (1<<0) #define HRF_NOOVERWRITE (1<<1) // (1<<2) #define HRF_ABORT (1<<3) #define HRF_OPENED (1<<4) #define HRF_DOWNLOADED (1<<5) //file was actually downloaded, and not from the local system #define HRF_WAITING (1<<6) //file looks important enough that we should wait for it to start to download or something before we try doing other stuff. #define HRF_DECOMPRESS (1<<7) //need to degzip it, which prevents streaming. #define HRF_DEMO_MVD (1<<8) #define HRF_DEMO_QWD (1<<9) #define HRF_DEMO_DM2 (1<<10) #define HRF_DEMO_DEM (1<<11) #define HRF_QTVINFO (1<<12) #define HRF_MANIFEST (1<<13) #define HRF_BSP (1<<14) #define HRF_PACKAGE (1<<15) //pak or pk3 that should be installed. #define HRF_ARCHIVE (1<<16) //zip - treated as a multiple-file 'installer' #define HRF_MODEL (1<<17) #define HRF_CONFIG (1<<18) //exec it on the console... #define HRF_ACTION (HRF_OVERWRITE|HRF_NOOVERWRITE|HRF_ABORT) #define HRF_DEMO (HRF_DEMO_MVD|HRF_DEMO_QWD|HRF_DEMO_DM2|HRF_DEMO_DEM) #define HRF_FILETYPES (HRF_DEMO|HRF_QTVINFO|HRF_MANIFEST|HRF_BSP|HRF_PACKAGE|HRF_ARCHIVE|HRF_MODEL|HRF_CONFIG) typedef struct { struct dl_download *dl; vfsfile_t *srcfile; vfsfile_t *dstfile; char *packageinfo; unsigned int flags; char fname[1]; //system path or url. } hrf_t; extern int waitingformanifest; void Host_DoRunFile(hrf_t *f); void CL_PlayDemoStream(vfsfile_t *file, char *filename, qboolean issyspath, int demotype, float bufferdelay, unsigned int eztv_ext); void CL_ParseQTVDescriptor(vfsfile_t *f, const char *name); //guesses the file type based upon its file extension. mdl/md3/iqm distinctions are not important, so we can usually get away with this in the context of quake. unsigned int Host_GuessFileType(const char *mimetype, const char *filename) { if (mimetype) { if (!strcmp(mimetype, "application/x-qtv")) //what uses this? return HRF_QTVINFO; else if (!strcmp(mimetype, "text/x-quaketvident")) return HRF_QTVINFO; else if (!strcmp(mimetype, "application/x-fteplugin")) return HRF_MANIFEST; else if (!strcmp(mimetype, "application/x-ftemanifest")) return HRF_MANIFEST; else if (!strcmp(mimetype, "application/x-multiviewdemo")) return HRF_DEMO_MVD; else if (!strcmp(mimetype, "application/zip")) return HRF_ARCHIVE; // else if (!strcmp(mimetype, "application/x-ftebsp")) // return HRF_BSP; // else if (!strcmp(mimetype, "application/x-ftepackage")) // return HRF_PACKAGE; } if (filename) { //find the query or location part of the url, so we can ignore extra stuff. struct { unsigned int type; const char *ext; } exts[] = { //demo formats {HRF_DEMO_QWD, "qwd"}, {HRF_DEMO_QWD|HRF_DECOMPRESS, "qwd.gz"}, {HRF_DEMO_MVD, "mvd"}, {HRF_DEMO_MVD|HRF_DECOMPRESS, "mvd.gz"}, {HRF_DEMO_DM2, "dm2"}, {HRF_DEMO_DM2|HRF_DECOMPRESS, "dm2.gz"}, {HRF_DEMO_DEM, "dem"}, {HRF_DEMO_DEM|HRF_DECOMPRESS, "dem.gz"}, {HRF_QTVINFO, "qtv"}, //other stuff {HRF_MANIFEST, "fmf"}, {HRF_BSP, "bsp"}, {HRF_BSP, "map"}, {HRF_CONFIG, "cfg"}, {HRF_CONFIG, "rc"}, {HRF_PACKAGE, "kpf"}, {HRF_PACKAGE, "pak"}, {HRF_PACKAGE, "pk3"}, {HRF_PACKAGE, "pk4"}, {HRF_PACKAGE, "wad"}, {HRF_ARCHIVE, "zip"}, //model formats {HRF_MODEL, "mdl"}, {HRF_MODEL, "md2"}, {HRF_MODEL, "md3"}, {HRF_MODEL, "iqm"}, {HRF_MODEL, "vvm"}, {HRF_MODEL, "psk"}, {HRF_MODEL, "zym"}, {HRF_MODEL, "dpm"}, {HRF_MODEL, "gltf"}, {HRF_MODEL, "glb"}, //sprites {HRF_MODEL, "spr"}, {HRF_MODEL, "spr2"}, //static stuff {HRF_MODEL, "obj"}, {HRF_MODEL, "lwo"}, {HRF_MODEL, "ase"}, }; size_t i; const char *ext; const char *stop = filename+strlen(filename); const char *tag = strchr(filename, '?'); if (tag && tag < stop) stop = tag; tag = strchr(filename, '#'); if (tag && tag < stop) stop = tag; ext = COM_GetFileExtension(filename, stop); if (!Q_strstopcasecmp(ext, stop, ".php")) //deal with extra extensions the easy way ext = COM_GetFileExtension(filename, stop=ext); if (!Q_strstopcasecmp(ext, stop, ".gz") || !Q_strstopcasecmp(ext, stop, ".xz")) //deal with extra extensions the easy way ext = COM_GetFileExtension(filename, ext); if (*ext == '.') ext++; for (i = 0; i < countof(exts); i++) if (!Q_strstopcasecmp(ext, stop, exts[i].ext)) return exts[i].type; } return 0; } void Host_RunFileDownloaded(struct dl_download *dl) { hrf_t *f = dl->user_ctx; if(!f) //download was previously cancelled. return; if (dl->status == DL_FAILED) { f->flags |= HRF_ABORT; f->srcfile = NULL; } else { if (f->srcfile) //erk? VFS_CLOSE(f->srcfile); f->flags |= HRF_OPENED; f->srcfile = dl->file; dl->file = NULL; } Host_DoRunFile(f); } qboolean Host_BeginFileDownload(struct dl_download *dl, char *mimetype) { qboolean result = false; //at this point the file is still downloading, so don't copy it out just yet. hrf_t *f = dl->user_ctx; if (f->flags & HRF_WAITING) { f->flags &= ~HRF_WAITING; waitingformanifest--; } if (!(f->flags & HRF_FILETYPES)) { f->flags |= Host_GuessFileType(mimetype, f->fname); if (!(f->flags & HRF_FILETYPES)) { if (mimetype) Con_Printf("mime type \"%s\" nor file extension of \"%s\" not known\n", mimetype, f->fname); else Con_Printf("file extension of \"%s\" not known\n", f->fname); //file type not guessable from extension either. f->flags |= HRF_ABORT; Host_DoRunFile(f); return false; } if ((f->flags & HRF_MANIFEST) && !(f->flags & HRF_WAITING)) { f->flags |= HRF_WAITING; waitingformanifest++; } } #ifdef AVAIL_GZDEC //seeking means we can rewind if (f->flags & HRF_DECOMPRESS) { //if its a gzip, we'll probably need to decompress it ourselves... in case the server doesn't use content-encoding:gzip //our demo playback should decompress it when its fin ally available. dl->file = VFSPIPE_Open(1, true); return true; } else #endif if (f->flags & HRF_DEMO_QWD) CL_PlayDemoStream((dl->file = VFSPIPE_Open(2, true)), f->fname, true, DPB_QUAKEWORLD, 0, 0); else if (f->flags & HRF_DEMO_MVD) CL_PlayDemoStream((dl->file = VFSPIPE_Open(2, true)), f->fname, true, DPB_MVD, 0, 0); #ifdef Q2CLIENT else if (f->flags & HRF_DEMO_DM2) CL_PlayDemoStream((dl->file = VFSPIPE_Open(2, true)), f->fname, true, DPB_QUAKE2, 0, 0); #endif #ifdef NQPROT else if (f->flags & HRF_DEMO_DEM) { //fixme: the demo code can't handle the cd track with streamed/missing-so-far writes. dl->file = VFSPIPE_Open(1, true); //make sure the reader will be seekable, so we can rewind. // CL_PlayDemoStream((dl->file = VFSPIPE_Open(2, true)), f->fname, DPB_NETQUAKE, 0, 0); } #endif else if (f->flags & (HRF_MANIFEST | HRF_QTVINFO)) { //just use a pipe instead of a temp file, working around an issue with temp files on android dl->file = VFSPIPE_Open(1, false); return true; } else if (f->flags & HRF_ARCHIVE) { char cachename[MAX_QPATH]; if (!FS_PathURLCache(f->fname, cachename, sizeof(cachename))) return false; f->srcfile = FS_OpenVFS(cachename, "rb", FS_ROOT); if (f->srcfile) { f->flags |= HRF_OPENED; Host_DoRunFile(f); return false; } FS_CreatePath(cachename, FS_ROOT); dl->file = FS_OpenVFS(cachename, "wb", FS_ROOT); if (dl->file) return true; //okay, continue downloading. } else if (f->flags & HRF_DEMO) Con_Printf("%s: format not supported\n", f->fname); //demos that are not supported in this build for one reason or another else return true; //demos stream, so we want to continue the http download, but we don't want to do anything with the result. if (f->flags & HRF_DEMO) result = true; else { f->flags |= HRF_ABORT; Host_DoRunFile(f); } return result; } void Host_RunFilePrompted(void *ctx, promptbutton_t button) { hrf_t *f = ctx; switch(button) { case PROMPT_YES: f->flags |= HRF_OVERWRITE; break; case PROMPT_NO: f->flags |= HRF_NOOVERWRITE; break; default: f->flags |= HRF_ABORT; break; } Host_DoRunFile(f); } #ifdef WEBCLIENT static qboolean isurl(char *url) { #ifdef FTE_TARGET_WEB return true; //assume EVERYTHING is a url, because the local filesystem is pointless. #endif return /*!strncmp(url, "data:", 5) || */!strncmp(url, "http://", 7) || !strncmp(url, "https://", 8); } #endif qboolean FS_FixupGamedirForExternalFile(char *input, char *filename, size_t fnamelen); void CL_PlayDemoFile(vfsfile_t *f, char *demoname, qboolean issyspath); void Host_DoRunFile(hrf_t *f) { char qname[MAX_QPATH]; char displayname[MAX_QPATH]; char loadcommand[MAX_OSPATH]; qboolean isnew = false; qboolean haschanged = false; enum fs_relative qroot = FS_GAME; if (f->flags & HRF_WAITING) { f->flags &= ~HRF_WAITING; waitingformanifest--; } if (f->flags & HRF_ABORT) { done: if (f->flags & HRF_WAITING) waitingformanifest--; if (f->packageinfo) Z_Free(f->packageinfo); if (f->srcfile) VFS_CLOSE(f->srcfile); if (f->dstfile) VFS_CLOSE(f->dstfile); Z_Free(f); return; } #ifdef WEBCLIENT if (isurl(f->fname) && !f->srcfile) { if (!(f->flags & HRF_OPENED)) { struct dl_download *dl; f->flags |= HRF_OPENED; dl = HTTP_CL_Get(f->fname, NULL, Host_RunFileDownloaded); if (dl) { f->flags |= HRF_DOWNLOADED; dl->notifystarted = Host_BeginFileDownload; dl->user_ctx = f; if (!(f->flags & HRF_WAITING)) { f->flags |= HRF_WAITING; waitingformanifest++; } return; } } } #endif if (!(f->flags & HRF_FILETYPES)) { f->flags |= Host_GuessFileType(NULL, f->fname); //if we still don't know what it is, give up. if (!(f->flags & HRF_FILETYPES)) { Con_Printf("Host_DoRunFile: unknown filetype for \"%s\"\n", f->fname); goto done; } if (f->flags & HRF_MANIFEST) { if (!(f->flags & HRF_WAITING)) { f->flags |= HRF_WAITING; waitingformanifest++; } } } if (f->flags & HRF_DEMO) { if (f->srcfile) { VFS_SEEK(f->srcfile, 0); #ifdef AVAIL_GZDEC f->srcfile = FS_DecompressGZip(f->srcfile, NULL); #endif if (f->flags & HRF_DEMO_QWD) CL_PlayDemoStream(f->srcfile, f->fname, true, DPB_QUAKEWORLD, 0, 0); #ifdef Q2CLIENT else if (f->flags & HRF_DEMO_DM2) CL_PlayDemoStream(f->srcfile, f->fname, true, DPB_QUAKE2, 0, 0); #endif #ifdef NQPROT else if (f->flags & HRF_DEMO_DEM) CL_PlayDemoFile(f->srcfile, f->fname, true); //should be able to handle the cd-track header. #endif else //if (f->flags & HRF_DEMO_MVD) CL_PlayDemoStream(f->srcfile, f->fname, true, DPB_MVD, 0, 0); f->srcfile = NULL; } else { //play directly via system path, no prompts needed FS_FixupGamedirForExternalFile(f->fname, loadcommand, sizeof(loadcommand)); Cbuf_AddText(va("playdemo \"%s\"\n", loadcommand), RESTRICT_LOCAL); } goto done; } else if (f->flags & HRF_BSP) { char shortname[MAX_QPATH]; COM_StripExtension(COM_SkipPath(f->fname), shortname, sizeof(shortname)); if (FS_FixupGamedirForExternalFile(f->fname, qname, sizeof(qname)) && !Q_strncasecmp(qname, "maps/", 5)) { COM_StripExtension(qname+5, loadcommand, sizeof(loadcommand)); Cbuf_AddText(va("map \"%s\"\n", loadcommand), RESTRICT_LOCAL); goto done; } Q_snprintfz(loadcommand, sizeof(loadcommand), "map \"%s\"\n", shortname); Q_snprintfz(displayname, sizeof(displayname), "map: %s", shortname); Q_snprintfz(qname, sizeof(qname), "maps/%s.bsp", shortname); } else if (f->flags & HRF_PACKAGE) { char *shortname; shortname = COM_SkipPath(f->fname); Q_snprintfz(qname, sizeof(qname), "%s", shortname); Q_snprintfz(loadcommand, sizeof(loadcommand), "fs_restart\n"); Q_snprintfz(displayname, sizeof(displayname), "package: %s", shortname); } else if (f->flags & HRF_MANIFEST) { if (f->flags & HRF_OPENED) { if (f->srcfile) { ftemanifest_t *man; int len = VFS_GETLEN(f->srcfile); int foo; char *fdata = BZ_Malloc(len+1); foo = VFS_READ(f->srcfile, fdata, len); fdata[len] = 0; if (foo != len || !len) { Con_Printf("Host_DoRunFile: unable to read file properly\n"); BZ_Free(fdata); } else { host_parms.manifest = Z_StrDup(fdata); man = FS_Manifest_ReadMem(NULL, NULL, fdata); if (man) { if (!man->updateurl) man->updateurl = Z_StrDup(f->fname); // if (f->flags & HRF_DOWNLOADED) man->blockupdate = true; //man->security = MANIFEST_SECURITY_DEFAULT; BZ_Free(fdata); FS_ChangeGame(man, true, true); } else { Con_Printf("Manifest file %s does not appear valid\n", f->fname); BZ_Free(fdata); } } goto done; } } } else if (f->flags & HRF_MODEL) { if (!FS_FixupGamedirForExternalFile(f->fname, loadcommand, sizeof(loadcommand))) Con_TPrintf("%s is not within the current gamedir\n", f->fname); else Cbuf_AddText(va("modelviewer \"%s\"\n", loadcommand), RESTRICT_LOCAL); goto done; } else if (f->flags & HRF_ARCHIVE) { char cachename[MAX_QPATH]; struct gamepacks packagespaths[2]; if (f->srcfile) VFS_CLOSE(f->srcfile); f->srcfile = NULL; memset(packagespaths, 0, sizeof(packagespaths)); packagespaths[0].url = f->fname; packagespaths[0].path = cachename; packagespaths[0].package = NULL; if (FS_PathURLCache(f->fname, cachename, sizeof(cachename))) { COM_Gamedir("", packagespaths); } goto done; } else if (f->flags & HRF_CONFIG) { if (!(f->flags & HRF_ACTION)) { Key_Dest_Remove(kdm_console); Menu_Prompt(Host_RunFilePrompted, f, va(localtext("Exec %s?\n"), COM_SkipPath(f->fname)), "Yes", NULL, "Cancel", true); return; } if (f->flags & HRF_OPENED) { size_t len = VFS_GETLEN(f->srcfile); char *fdata = BZ_Malloc(len+2); if (fdata) { VFS_READ(f->srcfile, fdata, len); fdata[len++] = '\n'; fdata[len] = 0; Cbuf_AddText(fdata, RESTRICT_INSECURE); BZ_Free(fdata); } goto done; } } else if (!(f->flags & HRF_QTVINFO)) { Con_Printf("Host_DoRunFile: filetype not handled\n"); goto done; } //at this point we need the file to have been opened. if (!(f->flags & HRF_OPENED)) { f->flags |= HRF_OPENED; if (!f->srcfile) { #ifdef WEBCLIENT if (isurl(f->fname)) { struct dl_download *dl = HTTP_CL_Get(f->fname, NULL, Host_RunFileDownloaded); if (dl) { dl->notifystarted = Host_BeginFileDownload; dl->user_ctx = f; return; } } #endif f->srcfile = VFSOS_Open(f->fname, "rb"); //input file is a system path, or something. } } if (!f->srcfile) { Con_TPrintf("Unable to open %s\n", f->fname); goto done; } if (f->flags & HRF_PACKAGE) { #ifdef PACKAGEMANAGER if (f->packageinfo) Z_Free(f->packageinfo); //sucks that we have to do this again, to recompute the proper qname+qroot Q_strncpyz(qname, COM_SkipPath(f->fname), sizeof(qname)); f->packageinfo = PM_GeneratePackageFromMeta(f->srcfile, qname,sizeof(qname), &qroot); if (!f->packageinfo) goto done; #endif } else if (f->flags & HRF_MANIFEST) { Host_DoRunFile(f); return; } else if (f->flags & HRF_QTVINFO) { //pass the file object to the qtv code instead of trying to install it. CL_ParseQTVDescriptor(f->srcfile, f->fname); f->srcfile = NULL; goto done; } VFS_SEEK(f->srcfile, 0); if (f->flags & HRF_OVERWRITE) ;//haschanged = isnew = true; else { f->dstfile = FS_OpenVFS(qname, "rb", (qroot==FS_GAMEONLY)?FS_GAME:qroot); if (f->dstfile) { //do a real diff. if (f->srcfile->seekstyle == SS_UNSEEKABLE || VFS_GETLEN(f->srcfile) != VFS_GETLEN(f->dstfile)) { //if we can't seek, or the sizes differ, just assume that the file is modified. haschanged = true; } else { int len = VFS_GETLEN(f->srcfile); char sbuf[8192], dbuf[8192]; if (len > sizeof(sbuf)) len = sizeof(sbuf); VFS_READ(f->srcfile, sbuf, len); VFS_READ(f->dstfile, dbuf, len); haschanged = memcmp(sbuf, dbuf, len); VFS_SEEK(f->srcfile, 0); } VFS_CLOSE(f->dstfile); f->dstfile = NULL; } else isnew = true; } if (!(f->flags & HRF_ACTION)) { Key_Dest_Remove(kdm_console); if (haschanged) { Menu_Prompt(Host_RunFilePrompted, f, va(localtext("File already exists.\nWhat would you like to do?\n%s\n"), displayname), "Overwrite", "Run old", "Cancel", true); return; } else if (isnew) { if (f->packageinfo && strstr(f->packageinfo, "\nguessed")) Menu_Prompt(Host_RunFilePrompted, f, va(localtext("File appears new.\nWould you like to install\n%s\n"CON_ERROR"File contains no metadata so will be installed to\n%s"), displayname, qname), "Install!", "", "Cancel", true); else Menu_Prompt(Host_RunFilePrompted, f, va(localtext("File appears new.\nWould you like to install\n%s\n"), displayname), "Install!", "", "Cancel", true); return; } else { Menu_Prompt(NULL, NULL, va(localtext("File is already installed\n%s\n"), displayname), NULL, NULL, "Cancel", true); f->flags |= HRF_ABORT; } } else if (f->flags & HRF_OVERWRITE) { char buffer[65536]; int len; f->dstfile = FS_OpenVFS(qname, (f->flags & HRF_PACKAGE)?"wbp":"wb", qroot); if (f->dstfile) { #ifdef FTE_TARGET_WEB VFS_SEEK(f->dstfile, VFS_GETLEN(f->srcfile)); VFS_WRITE(f->dstfile, "zomg", 0); //hack to ensure the file is there, avoiding excessive copies. VFS_SEEK(f->dstfile, 0); #endif while(1) { len = VFS_READ(f->srcfile, buffer, sizeof(buffer)); if (len <= 0) break; VFS_WRITE(f->dstfile, buffer, len); } VFS_CLOSE(f->dstfile); f->dstfile = NULL; } #ifdef PACKAGEMANAGER if (f->flags & HRF_PACKAGE) PM_FileInstalled(qname, qroot, f->packageinfo, true); #endif if (!strcmp(loadcommand, "fs_restart\n")) FS_ReloadPackFiles(); else Cbuf_AddText(loadcommand, RESTRICT_LOCAL); } goto done; } //only valid once the host has been initialised, as it needs a working filesystem. //if file is specified, takes full ownership of said file, including destruction. qboolean Host_RunFile(const char *fname, int nlen, vfsfile_t *file) { hrf_t *f; #if defined(FTE_TARGET_WEB) if (nlen >= 8 && !strncmp(fname, "file:///", 8)) { //just here so we don't get confused by the arbitrary scheme check below. } #else //file urls need special handling, if only for percent-encoding. char utf8[MAX_OSPATH*3]; if (nlen >= 5 && !strncmp(fname, "file:", 5)) { if (!Sys_ResolveFileURL(fname, nlen, utf8, sizeof(utf8))) { Con_Printf("Cannot resolve file url\n"); if(file) VFS_CLOSE(file); return false; } fname = utf8; nlen = strlen(fname); } #endif else if((nlen >= 7 && !strncmp(fname, "http://", 7)) || (nlen >= 8 && !strncmp(fname, "https://", 8))) ; //don't interpret these as our custom uri schemes else { const char *schemeend = strstr(fname, "://"); if (schemeend) { //this is also implemented by ezquake, so be careful here... //examples: // "quake2://broker:port" // "quake2:rtc://broker:port/game" // "qw://[stream@]host[:port]/COMMAND" join, spectate, qtvplay //we'll chop off any non-auth prefix, its just so we can handle multiple protocols via a single uri scheme. char *t, *cmd, *args; const char *url; char buffer[8192]; const char *schemestart = strchr(fname, ':'); int schemelen, urilen; //if its one of our explicit protocols then use the url as-is const char *netschemes[] = {"udp", "udp4", "udp6", "ipx", "tcp", "tcp4", "tcp6", "spx", "ws", "wss", "tls", "dtls", "ice", "rtc", "ices", "rtcs", "irc", "udg", "unix"}; int i; size_t slen; if (!schemestart || schemestart==schemeend) schemestart = fname; else schemestart++; schemelen = schemeend-schemestart; urilen = nlen-(schemestart-fname); for (i = 0; i < countof(netschemes); i++) { slen = strlen(netschemes[i]); if (schemelen == slen && !strncmp(schemestart, netschemes[i], slen)) { char quoted[8192]; char *t = Z_Malloc(urilen+1); memcpy(t, schemestart, urilen); t[urilen] = 0; Cbuf_AddText(va("connect %s\n", COM_QuotedString(t, quoted, sizeof(quoted), false)), RESTRICT_LOCAL); if(file) VFS_CLOSE(file); Z_Free(t); return true; } } schemelen++; if (!strncmp(schemestart+schemelen, "//", 2)) schemelen+=2; t = Z_Malloc(urilen+1); memcpy(t, schemestart, urilen); t[urilen] = 0; url = t+schemelen; *buffer = 0; for (args = t+schemelen; *args; args++) { if (*args == '?') { *args++ = 0; break; } } for (cmd = t+schemelen; *cmd; cmd++) { if (*cmd == '/') { *cmd++ = 0; break; } } //quote the url safely. url = COM_QuotedString(url, buffer, sizeof(buffer), false); //now figure out what the command actually was if (!Q_strcasecmp(cmd, "join")) Cbuf_AddText(va("join %s\n", url), RESTRICT_LOCAL); else if (!Q_strcasecmp(cmd, "spectate") || !strcmp(cmd, "observe")) Cbuf_AddText(va("observe %s\n", url), RESTRICT_LOCAL); else if (!Q_strcasecmp(cmd, "qtvplay")) Cbuf_AddText(va("qtvplay %s\n", url), RESTRICT_LOCAL); else if (!*cmd || !Q_strcasecmp(cmd, "connect")) Cbuf_AddText(va("connect %s\n", url), RESTRICT_LOCAL); else Con_Printf("Unknown url command: %s\n", cmd); if(file) VFS_CLOSE(file); Z_Free(t); return true; } } f = Z_Malloc(sizeof(*f) + nlen); memcpy(f->fname, fname, nlen); f->fname[nlen] = 0; f->srcfile = file; if (file) f->flags |= HRF_OPENED; { char dpath[MAX_OSPATH]; FS_DisplayPath(f->fname, FS_SYSTEM, dpath,sizeof(dpath)); Con_TPrintf("Opening external file: %s\n", dpath); } Host_DoRunFile(f); return true; } /* ================== Host_Frame Runs all active servers ================== */ extern cvar_t cl_netfps; void CL_StartCinematicOrMenu(void); int nopacketcount; void SNDDMA_SetUnderWater(qboolean underwater); double Host_Frame (double time) { static double time0 = 0; static double time1 = 0; static double time2 = 0; static double time3 = 0; int pass0, pass1, pass2, pass3, i; // float fps; double newrealtime, spare; float maxfps; qboolean maxfpsignoreserver; qboolean idle; static qboolean hadwork; unsigned int vrflags; qboolean mustrenderbeforeread; RSpeedLocals(); if (setjmp (host_abort) ) { return 0; // something bad happened, or the server disconnected } vrflags = vid.vr?vid.vr->SyncFrame(&time):0; //fiddle with frame timings newrealtime = Media_TweekCaptureFrameTime(realtime, time); //fiddle with time some more time = newrealtime - realtime; realtime = newrealtime; if (oldrealtime > realtime) oldrealtime = realtime; if (cl.gamespeed<0.1) cl.gamespeed = 1; time *= cl.gamespeed; #ifdef WEBCLIENT // FTP_ClientThink(); HTTP_CL_Think(NULL, NULL); #endif if (r_blockvidrestart) { Cbuf_Execute(); if (waitingformanifest) { COM_MainThreadWork(); return 0.1; } Host_FinishLoading(); return 0; } if (startuppending) CL_StartCinematicOrMenu(); if (cl.paused) cl.gametimemark += time; //if we're at a menu/console/thing // idle = !Key_Dest_Has_Higher(kdm_menu); // idle = ((cls.state == ca_disconnected) || cl.paused) && idle; //idle if we're disconnected/paused and not at a menu idle = !vid.activeapp; //always idle when tabbed out //read packets early and always, so we don't have stuff waiting for reception quite so often. //should smooth out a few things, and increase download speeds. if (!cls.timedemo) CL_ReadPackets (); if (idle && cl_idlefps.value > 0 && !(vrflags&VRF_OVERRIDEFRAMETIME)) { double idlesec = 1.0 / cl_idlefps.value; if (idlesec > 0.1) idlesec = 0.1; // limit to at least 10 fps #ifdef HAVE_MEDIA_ENCODER if (Media_Capturing()) idlesec = 0; #endif if ((realtime - oldrealtime) < idlesec) { #ifdef HAVE_SERVER if (sv.state) { RSpeedRemark(); SV_Frame(); RSpeedEnd(RSPEED_SERVER); } else MSV_PollSlaves(); #endif while(COM_DoWork(0, false)) ; return idlesec - (realtime - oldrealtime); } } #ifdef PLUGINS Plug_Tick(); #endif NET_Tick(); /* if (cl_maxfps.value) fps = cl_maxfps.value;//max(30.0, min(cl_maxfps.value, 72.0)); else fps = max(30.0, min(rate.value/80.0, 72.0)); if (!cls.timedemo && realtime - oldrealtime < 1.0/fps) return; // framerate is too high */ #ifdef RUNTIMELIGHTING RelightThink(); //think even on idle (which means small walls and a fast cpu can get more surfaces done. #endif #ifdef HAVE_SERVER if (sv.state && cls.state != ca_active) { maxfpsignoreserver = false; maxfps = 0;//cl_maxfps.ival; } else #endif if ((cl_netfps.value>0 || cls.demoplayback || runningindepphys)) { //limit the fps freely, and expect the netfps to cope. maxfpsignoreserver = true; maxfps = cl_maxfps.ival; } else { maxfpsignoreserver = false; maxfps = (cl_maxfps.ival>0||cls.protocol!=CP_QUAKEWORLD)?cl_maxfps.value:((cl_netfps.value>0)?cl_netfps.value:cls.maxfps); /*gets buggy at times longer than 250ms (and 0/negative, obviously)*/ if (maxfps < 4) maxfps = 4; } if (vid.isminimized && (maxfps <= 0 || maxfps > 10)) maxfps = 10; if (maxfps > 0 #ifdef HAVE_MEDIA_ENCODER && Media_Capturing() != 2 #endif && !(vrflags&VRF_OVERRIDEFRAMETIME)) { spare = CL_FilterTime((realtime - oldrealtime)*1000, maxfps, 1.5, maxfpsignoreserver); if (!spare) { while(COM_DoWork(0, false)) ; return (cl_yieldcpu.ival || vid.isminimized || idle)? (1.0 / maxfps - (realtime - oldrealtime)) : 0; } if (spare > cl_maxfps_slop.ival) spare = cl_maxfps_slop.ival; spare /= 1000; if (spare > 0.5/maxfps) //don't delay the next by spare = 0.5/maxfps; if (spare < 0 || cls.state < ca_onserver) spare = 0; } else spare = 0; host_frametime = (realtime-spare - oldrealtime)*cl.gamespeed; oldrealtime = realtime-spare; if (host_speeds.ival) time0 = Sys_DoubleTime (); //end-of-idle if (cls.demoplayback && !cl.stillloading) { qboolean haswork = cl.sendprespawn || COM_HasWork(); if (!hadwork && !haswork) CL_ProgressDemoTime(); hadwork = haswork; } cl.stillloading = cl.sendprespawn #ifdef LOADERTHREAD || (cls.state < ca_active && worker_flush.ival && COM_HasWork()) #endif ; COM_MainThreadWork(); // if (host_frametime > 0.2) // host_frametime = 0.2; // get new key events Sys_SendKeyEvents (); //from windowing system INS_Move(); //from things that need special polling // check what we got, and handle any click/button events IN_Commands (); // process console commands from said click/button events Cbuf_Execute (); #ifdef HAVE_SERVER if (isDedicated) //someone changed it. { if (sv.state) { float ohft = host_frametime; RSpeedRemark(); SV_Frame(); RSpeedEnd(RSPEED_SERVER); host_frametime = ohft; } else MSV_PollSlaves(); return 0; } #endif cls.framecount++; RSpeedRemark(); CL_UseIndepPhysics(cls.state != ca_disconnected && !!cl_threadedphysics.ival); //starts/stops the input frame thread. cl.do_lerp_players = cl_lerp_players.ival || cls.demoplayback==DPB_MVD || (cls.demoplayback && !cl_nolerp.ival && !cls.timedemo); CL_AllowIndependantSendCmd(false); mustrenderbeforeread = cls.protocol == CP_QUAKE2; //FIXME: quake2 MUST render a frame (or a later one) before it can read any acks from the server, otherwise its prediction screws up. I'm too lazy to rewrite that right now. // if (mustrenderbeforeread) CL_ReadPackets(); //this should be redundant. CL_RequestNextDownload(); // send intentions now // resend a connection request if necessary if (cls.state == ca_disconnected) { CL_SendCmd (host_frametime, true); // IN_Move(NULL, 0, time); CL_CheckForResend (); #ifdef VOICECHAT S_Voip_Transmit(0, NULL); #endif } else { if (connectinfo.trying) CL_CheckForResend (); CL_SendCmd (cl.gamespeed?host_frametime/cl.gamespeed:host_frametime, true); if (cls.state == ca_onserver && cl.validsequence && cl.worldmodel) { // first update is the final signon stage if (cls.protocol == CP_NETQUAKE) { //nq can send 'frames' without any entities before we're on the server, leading to short periods where the local player's position is not known. this is bad. so be more cautious with nq. this might break csqc. CL_TransitionEntities(); if (cl.currentpackentities->num_entities || cl.currentpackentities->servertime #ifdef CSQC_DAT || (cls.fteprotocolextensions & PEXT_CSQC) #endif ) CL_MakeActive("Quake"); } else CL_MakeActive("QuakeWorld"); } } CL_AllowIndependantSendCmd(true); RSpeedEnd(RSPEED_PROTOCOL); #ifdef HAVE_SERVER if (sv.state) { float ohft = host_frametime; RSpeedRemark(); SV_Frame(); RSpeedEnd(RSPEED_SERVER); host_frametime = ohft; // if (cls.protocol != CP_QUAKE3 && cls.protocol != CP_QUAKE2) // CL_ReadPackets (); //q3's cgame cannot cope with input commands with the same time as the most recent snapshot value } else MSV_PollSlaves(); #endif // fetch results from server... now that we've run it. if (!mustrenderbeforeread) { CL_AllowIndependantSendCmd(false); CL_ReadPackets (); CL_AllowIndependantSendCmd(true); } CL_CalcClientTime(); // update video if (host_speeds.ival) time1 = Sys_DoubleTime (); if (!VID_MayRefresh || VID_MayRefresh()) { if (R2D_Flush) { R2D_Flush(); Con_Printf("R2D_Flush was set outside of SCR_UpdateScreen\n"); } cl.mouseplayerview = NULL; cl.mousenewtrackplayer = -1; for (i = 0; i < MAX_SPLITS; i++) { cl.playerview[i].audio.defaulted = true; cl.playerview[i].audio.entnum = cl.playerview[i].viewentity; VectorClear(cl.playerview[i].audio.origin); VectorSet(cl.playerview[i].audio.forward, 1, 0, 0); VectorSet(cl.playerview[i].audio.right, 0, 1, 0); VectorSet(cl.playerview[i].audio.up, 0, 0, 1); cl.playerview[i].audio.reverbtype = 0; VectorClear(cl.playerview[i].audio.velocity); } if (cls.state && r_worldentity.model && r_worldentity.model->loadstate == MLS_NOTLOADED) Mod_LoadModel(cl.worldmodel, MLV_WARNSYNC); if (SCR_UpdateScreen && !vid.isminimized) { extern cvar_t r_stereo_method; r_refdef.warndraw = false; r_refdef.stereomethod = r_stereo_method.ival; { RSpeedMark(); vid.ime_allow = false; vrui.enabled |= cl_vrui_force.ival || (vrflags&VRF_UIACTIVE); if (SCR_UpdateScreen()) fps_count += 1+max(0, cl_fakeframes.ival); if (R2D_Flush) Sys_Error("update didn't flush 2d cache\n"); RSpeedEnd(RSPEED_TOTALREFRESH); } r_refdef.warndraw = true; } else fps_count++; sh_config.showbatches = false; } if (host_speeds.ival) time2 = Sys_DoubleTime (); // update audio for (i = 0 ; i < MAX_SPLITS; i++) { playerview_t *pv = &cl.playerview[cl.splitclients?i % cl.splitclients:0]; S_UpdateListener (i, pv->audio.entnum, pv->audio.origin, pv->audio.forward, pv->audio.right, pv->audio.up, pv->audio.reverbtype, pv->audio.velocity); } S_Update (); CDAudio_Update(); if (host_speeds.ival) { pass0 = (time0 - time3)*1000000; time3 = Sys_DoubleTime (); pass1 = (time1 - time0)*1000000; pass2 = (time2 - time1)*1000000; pass3 = (time3 - time2)*1000000; Con_Printf ("%4i tot %4i idle %4i server %4i gfx %4i snd\n", pass0+pass1+pass2+pass3, pass0, pass1, pass2, pass3); } // IN_Commands (); // process console commands // Cbuf_Execute (); CL_QTVPoll(); #ifdef QUAKESTATS TP_UpdateAutoStatus(); #endif host_framecount++; cl.lasttime = cl.time; return 0; } static void simple_crypt(char *buf, int len) { if (!(*buf & 128)) return; while (len--) *buf++ ^= 0xff; } void Host_FixupModelNames(void) { simple_crypt(emodel_name, sizeof(emodel_name) - 1); simple_crypt(pmodel_name, sizeof(pmodel_name) - 1); simple_crypt(prespawn_name, sizeof(prespawn_name) - 1); simple_crypt(modellist_name, sizeof(modellist_name) - 1); simple_crypt(soundlist_name, sizeof(soundlist_name) - 1); } #ifdef Q3CLIENT void CL_ReadCDKey(void) { //q3 cdkey //you don't need one, just use a server without sv_strictauth set to 0. char *buffer; buffer = COM_LoadTempFile("q3key", FSLF_IGNOREPURE, NULL); if (buffer) //a cdkey is meant to be 16 chars { char *chr; for (chr = buffer; *chr; chr++) { if (*(unsigned char*)chr < ' ') { *chr = '\0'; //don't get more than one line. break; } } Cvar_Get("cl_cdkey", buffer, CVAR_MAPLATCH|CVAR_NOUNSAFEEXPAND, "Q3 compatability"); } } #endif //============================================================================ void CL_StartCinematicOrMenu(void) { COM_MainThreadWork(); if (com_installer && FS_DownloadingPackage()) { startuppending = true; return; } if (cls.download) { startuppending = true; return; } Cmd_StuffCmds(); if (startuppending) { if (startuppending == 2) //installer finished. Cbuf_AddText("\nfs_restart\nvid_restart\n", RESTRICT_LOCAL); startuppending = false; Key_Dest_Remove(kdm_console); //make sure console doesn't stay up weirdly. } Cbuf_AddText("menu_restart\n", RESTRICT_LOCAL); Con_TPrintf ("^Ue080^Ue081^Ue081^Ue081^Ue081^Ue081^Ue081 %s %sInitialized ^Ue081^Ue081^Ue081^Ue081^Ue081^Ue081^Ue082\n", *fs_gamename.string?fs_gamename.string:"Nothing", com_installer?"Installer ":""); //there might be some console command or somesuch waiting for the renderer to begin (demos or map command or whatever all need model support). realtime+=1; Cbuf_Execute (); //server may have been waiting for the renderer Con_ClearNotify(); if (com_installer) { com_installer = false; #if 0 Key_Dest_Remove(kdm_console); //make sure console doesn't stay up weirdly. M_Menu_Installer(); return; #endif } if (!sv_state && !cls.demoinfile && !cls.state && !*cls.servername) { //this is so default.cfg can define startup commands to exec if the engine starts up with no +connect / +map etc arg TP_ExecTrigger("f_startup", true); Cbuf_Execute (); } //and any startup cinematics #ifdef HAVE_MEDIA_DECODER if (!sv_state && !cls.demoinfile && !cls.state && !*cls.servername) { int ol_depth; int idcin_depth; int idroq_depth; idcin_depth = COM_FDepthFile("video/idlog.cin", true); //q2 idroq_depth = COM_FDepthFile("video/idlogo.roq", true); //q3 ol_depth = COM_FDepthFile("video/openinglogos.roq", true); //jk2 if (ol_depth != FDEPTH_MISSING && (ol_depth <= idroq_depth || ol_depth <= idcin_depth)) Media_PlayFilm("video/openinglogos.roq", true); else if (idroq_depth != FDEPTH_MISSING && idroq_depth <= idcin_depth) Media_PlayFilm("video/idlogo.roq", true); else if (idcin_depth != FDEPTH_MISSING) Media_PlayFilm("video/idlog.cin", true); #ifdef HAVE_LEGACY //and for fun (blame spirit): if (COM_FCheckExists("data/local/video/New_Bliz640x480.bik")) Media_PlayFilm("av:data/local/video/New_Bliz640x480.bik", true); if (COM_FCheckExists("data/local/video/BlizNorth640x480.bik")) Media_PlayFilm("av:data/local/video/BlizNorth640x480.bik", true); if (COM_FCheckExists("data/local/video/eng/d2intro640x292.bik")) Media_PlayFilm("av:data/local/video/eng/d2intro640x292.bik", true); if (COM_FCheckExists("Data/Local/Video/ENG/D2x_Intro_640x292.bik")) Media_PlayFilm("av:Data/Local/Video/ENG/D2x_Intro_640x292.bik", true); #endif } #endif if (!sv_state && !cls.demoinfile && !cls.state && !*cls.servername) { if (qrenderer > QR_NONE && !Key_Dest_Has(~kdm_game)) { #ifndef NOBUILTINMENUS if (!cls.state && !Key_Dest_Has(~kdm_game) && !*FS_GetGamedir(false)) M_Menu_Mods_f(); #endif if (!cls.state && !Key_Dest_Has(~kdm_game) && cl_demoreel.ival) { cls.demonum = MAX_DEMOS; CL_NextDemo(); } if (!cls.state && !Key_Dest_Has(~kdm_game)) //if we're (now) meant to be using csqc for menus, make sure that its running. if (!CSQC_UnconnectedInit()) M_ToggleMenu_f(); } //Con_ForceActiveNow(); } } void CL_ArgumentOverrides(void) { int i; if (COM_CheckParm ("-window") || COM_CheckParm ("-startwindowed")) Cvar_Set(Cvar_FindVar("vid_fullscreen"), "0"); if (COM_CheckParm ("-fullscreen")) Cvar_Set(Cvar_FindVar("vid_fullscreen"), "1"); if ((i = COM_CheckParm ("-width"))) //width on it's own also sets height { Cvar_Set(Cvar_FindVar("vid_width"), com_argv[i+1]); Cvar_SetValue(Cvar_FindVar("vid_height"), (atoi(com_argv[i+1])/4)*3); } if ((i = COM_CheckParm ("-height"))) Cvar_Set(Cvar_FindVar("vid_height"), com_argv[i+1]); if ((i = COM_CheckParm ("-conwidth"))) //width on it's own also sets height { Cvar_Set(Cvar_FindVar("vid_conwidth"), com_argv[i+1]); Cvar_SetValue(Cvar_FindVar("vid_conheight"), (atoi(com_argv[i+1])/4)*3); } if ((i = COM_CheckParm ("-conheight"))) Cvar_Set(Cvar_FindVar("vid_conheight"), com_argv[i+1]); if ((i = COM_CheckParm ("-bpp"))) Cvar_Set(Cvar_FindVar("vid_bpp"), com_argv[i+1]); if (COM_CheckParm ("-current")) Cvar_Set(Cvar_FindVar("vid_desktopsettings"), "1"); if (COM_CheckParm("-condebug")) Cvar_Set(Cvar_FindVar("log_enable"), "1"); if ((i = COM_CheckParm ("-particles"))) Cvar_Set(Cvar_FindVar("r_part_maxparticles"), com_argv[i+1]); if (COM_CheckParm("-qmenu")) Cvar_ForceSet(Cvar_FindVar("forceqmenu"), "1"); } //note that this does NOT include commandline. void CL_ExecInitialConfigs(char *resetcommand, qboolean fullvidrestart) { #ifndef QUAKETC int qrc, hrc; #endif int def; Cbuf_Execute (); //make sure any pending console commands are done with. mostly, anyway... Cbuf_AddText("unbindall\nshowpic_removeall\n", RESTRICT_LOCAL); Cbuf_AddText("alias restart_ents \"changelevel . .\"\n",RESTRICT_LOCAL); Cbuf_AddText("alias restart map_restart\n",RESTRICT_LOCAL); Cbuf_AddText("alias startmap_sp \"map start\"\n", RESTRICT_LOCAL); #ifdef QUAKESTATS Cbuf_AddText("alias +attack2 +button3\n", RESTRICT_LOCAL); Cbuf_AddText("alias -attack2 -button3\n", RESTRICT_LOCAL); #endif Cbuf_AddText("cl_warncmd 0\n", RESTRICT_LOCAL); Cbuf_AddText("cvar_purgedefaults\n", RESTRICT_LOCAL); //reset cvar defaults to their engine-specified values. the tail end of 'exec default.cfg' will update non-cheat defaults to mod-specified values. Cbuf_AddText("cvarreset *\n", RESTRICT_LOCAL); //reset all cvars to their current (engine) defaults #ifdef HAVE_SERVER Cbuf_AddText(va("sv_gamedir \"%s\"\n", FS_GetGamedir(true)), RESTRICT_LOCAL); #endif Cbuf_AddText(resetcommand, RESTRICT_LOCAL); Cbuf_AddText("\n", RESTRICT_LOCAL); COM_ParsePlusSets(true); #ifdef QUAKESTATS Cbuf_AddText("register_bestweapon reset\n", RESTRICT_LOCAL); #endif def = COM_FDepthFile("default.cfg", true); //q2/q3/tc #ifdef QUAKETC Cbuf_AddText ("exec default.cfg\n", RESTRICT_LOCAL); if (COM_FDepthFile ("config.cfg", true) <= def) Cbuf_AddText ("exec config.cfg\n", RESTRICT_LOCAL); if (COM_FCheckExists ("autoexec.cfg")) Cbuf_AddText ("exec autoexec.cfg\n", RESTRICT_LOCAL); #else //who should we imitate? qrc = COM_FDepthFile("quake.rc", true); //q1 hrc = COM_FDepthFile("hexen.rc", true); //h2 if (qrc <= def && qrc <= hrc && qrc!=FDEPTH_MISSING) { Cbuf_AddText ("exec quake.rc\n", RESTRICT_LOCAL); def = qrc; } else if (hrc <= def && hrc!=FDEPTH_MISSING) { Cbuf_AddText ("exec hexen.rc\n", RESTRICT_LOCAL); def = hrc; } else { //they didn't give us an rc file! // int cfg = COM_FDepthFile ("config.cfg", true); int q3cfg = COM_FDepthFile ("q3config.cfg", true); // Cbuf_AddText ("bind ` toggleconsole\n", RESTRICT_LOCAL); //in case default.cfg does not exist. :( Cbuf_AddText ("exec default.cfg\n", RESTRICT_LOCAL); if (q3cfg <= def && q3cfg!=FDEPTH_MISSING) Cbuf_AddText ("exec q3config.cfg\n", RESTRICT_LOCAL); else //if (cfg <= def && cfg!=0x7fffffff) Cbuf_AddText ("exec config.cfg\n", RESTRICT_LOCAL); if (def!=FDEPTH_MISSING) Cbuf_AddText ("exec autoexec.cfg\n", RESTRICT_LOCAL); } #endif #ifdef QUAKESPYAPI if (COM_FCheckExists ("frontend.cfg")) Cbuf_AddText ("exec frontend.cfg\n", RESTRICT_LOCAL); #endif Cbuf_AddText ("cl_warncmd 1\n", RESTRICT_LOCAL); //and then it's allowed to start moaning. COM_ParsePlusSets(true); com_parseutf8.ival = com_parseutf8.value; //if the renderer is already up and running, be prepared to reload content to match the new conback/font/etc if (r_blockvidrestart) ; else if (fullvidrestart) Cbuf_AddText ("vid_restart\n", RESTRICT_LOCAL); else if (qrenderer != QR_NONE) Cbuf_AddText ("vid_reload\n", RESTRICT_LOCAL); // if (Key_Dest_Has(kdm_menu)) // Cbuf_AddText ("closemenu\ntogglemenu\n", RESTRICT_LOCAL); //make sure the menu has the right content loaded. Cbuf_Execute (); //if the server initialisation causes a problem, give it a place to abort to //assuming they didn't use any waits in their config (fools) //the configs should be fully loaded. //so convert the backwards compable commandline parameters in cvar sets. CL_ArgumentOverrides(); #ifdef HAVE_SERVER SV_ArgumentOverrides(); #endif //and disable the 'you have unsaved stuff' prompt. Cvar_Saved(); Ruleset_Scan(); } static void Host_URIPrompt(void *ctx, promptbutton_t btn) { if (btn == PROMPT_YES) Cbuf_AddText ("\nsys_register_file_associations\n", RESTRICT_LOCAL); } void Host_FinishLoading(void) { int i; extern qboolean r_forceheadless; if (r_blockvidrestart == true) { //1 means we need to init the filesystem //the filesystem has retrieved its manifest, but might still be waiting for paks to finish downloading. //make sure the filesystem has some default if no manifest was loaded. FS_ChangeGame(NULL, true, true); if (waitingformanifest) { #ifdef MULTITHREAD Sys_Sleep(0.1); #endif return; } #ifdef PLUGINS Plug_Initialise(true); #endif Con_History_Load(); r_blockvidrestart = 2; CL_ArgumentOverrides(); #ifdef HAVE_SERVER SV_ArgumentOverrides(); #endif Con_TPrintf ("\nEngine Version: %s\n", version_string()); Con_DPrintf("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." "\n" "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. " "\n" "See the GNU General Public License for more details.\n"); #if defined(_WIN32) && !defined(FTE_SDL) && !defined(_XBOX) && defined(MANIFESTDOWNLOADS) if (Sys_RunInstaller()) Sys_Quit(); #endif Menu_Download_Update(); #ifdef IPLOG IPLog_Merge_File("iplog.txt"); IPLog_Merge_File("iplog.dat"); //legacy crap, for compat with proquake #endif if (!PM_IsApplying()) Cmd_StuffCmds(); } if (PM_IsApplying() == 1) { #ifdef MULTITHREAD Sys_Sleep(0.1); #endif return; } //open any files specified on the commandline (urls, paks, models, I dunno). for (i = 1; i < com_argc; i++) { if (!com_argv[i]) continue; if (*com_argv[i] == '+' || *com_argv[i] == '-') break; Host_RunFile(com_argv[i], strlen(com_argv[i]), NULL); } //android may find that it has no renderer at various points. if (r_forceheadless) return; if (r_blockvidrestart == 2) { //2 is part of the initial startup Renderer_Start(); CL_StartCinematicOrMenu(); } else //3 flags for a renderer restart Renderer_Start(); if (fs_manifest->schemes && Cmd_IsCommand("sys_register_file_associations")) { if (cl_verify_urischeme.ival >= 2) Cbuf_AddText ("\nsys_register_file_associations\n", RESTRICT_LOCAL); else if (cl_verify_urischeme.ival) { char *scheme = Sys_URIScheme_NeedsRegistering(); if (scheme) { Menu_Prompt(Host_URIPrompt, NULL, va(localtext("The URI scheme %s:// is not configured.\nRegister now?"), scheme), "Register", NULL, "No", true); Z_Free(scheme); } } } } /* ==================== Host_Init ==================== */ void Host_Init (quakeparms_t *parms) { /*#ifdef PACKAGEMANAGER char engineupdated[MAX_OSPATH]; #endif*/ int man; com_parseutf8.ival = 1; //enable utf8 parsing even before cvars are registered. COM_InitArgv (parms->argc, parms->argv); if (setjmp (host_abort) ) Sys_Error("Host_Init: An error occured. Try the -condebug commandline parameter\n"); host_parms = *parms; Cvar_Init(); Memory_Init (); /*memory is working, its safe to printf*/ Con_Init (); Sys_Init(); COM_ParsePlusSets(false); Cbuf_Init (); Cmd_Init (); COM_Init (); /*this may be tripping some bullshit huristic in microsoft's insecurity mafia software(tbh I really don't know what they're detecting), plus causes firewall issues on updates. #ifdef PACKAGEMANAGER //we have enough of the filesystem inited now that we can read the package list and figure out which engine was last installed. if (PM_FindUpdatedEngine(engineupdated, sizeof(engineupdated))) { PM_Shutdown(); //will restart later as needed, but we need to be sure that no files are open or anything. if (Sys_EngineWasUpdated(engineupdated)) { COM_Shutdown(); Cmd_Shutdown(); Sys_Shutdown(); Con_Shutdown(); Memory_DeInit(); Cvar_Shutdown(); Sys_Quit(); return; } PM_Shutdown(); //will restart later as needed, but we need to be sure that no files are open or anything. } #endif */ V_Init (); NET_Init (); #if defined(Q2BSPS) || defined(Q3BSPS) CM_Init(); #endif #ifdef TERRAIN Terr_Init(); #endif Host_FixupModelNames(); Netchan_Init (); Renderer_Init(); Mod_Init(true); #if defined(CSQC_DAT) || defined(MENU_DAT) PF_Common_RegisterCvars(); #endif #ifdef HAVE_SERVER SV_Init(parms); #endif // W_LoadWadFile ("gfx.wad"); Key_Init (); M_Init (); IN_Init (); S_Init (); cls.state = ca_disconnected; CDAudio_Init (); Sbar_Init (); CL_Init (); #ifdef PLUGINS Plug_Initialise(false); #endif #ifdef TEXTEDITOR Editor_Init(); #endif #ifdef CL_MASTER Master_SetupSockets(); #endif #ifdef Q3CLIENT CL_ReadCDKey(); #endif // Con_Printf ("Exe: "__TIME__" "__DATE__"\n"); //Con_Printf ("%4.1f megs RAM available.\n", parms->memsize/ (1024*1024.0)); R_SetRenderer(NULL);//set the renderer stuff to unset... Cvar_ParseWatches(); host_initialized = true; forcesaveprompt = false; #ifdef PLUGINS Plug_Initialise(false); #endif Sys_SendKeyEvents(); //the engine is fully running, except the file system may be nulled out waiting for a manifest to download. man = COM_CheckParm("-manifest"); if (man && man < com_argc-1 && com_argv[man+1]) Host_RunFile(com_argv[man+1], strlen(com_argv[man+1]), NULL); } /* =============== Host_Shutdown FIXME: this is a callback from Sys_Quit and Sys_Error. It would be better to run quit through here before the final handoff to the sys code. =============== */ void Host_Shutdown(void) { size_t i; if (!host_initialized) return; host_initialized = false; CL_UseIndepPhysics(false); #ifdef WEBCLIENT HTTP_CL_Terminate(); #endif //disconnect server/client/etc CL_Disconnect_f(); M_Shutdown(true); #ifdef CSQC_DAT CSQC_Shutdown(); #endif S_Shutdown(true); CDAudio_Shutdown (); IN_Shutdown (); R_ShutdownRenderer(true); #ifdef PLUGINS Plug_Shutdown(false); #endif // Host_WriteConfiguration (); #ifdef CL_MASTER MasterInfo_Shutdown(); #endif CL_FreeDlights(); CL_FreeVisEdicts(); Mod_Shutdown(true); Wads_Flush(); Con_History_Save(); //do this outside of the console code so that the filesystem is still running at this point but still allowing the filesystem to make console prints (you might not see them, but they should be visible to sys_printf still, for debugging). #ifdef HAVE_SERVER SV_Shutdown(); #else Log_ShutDown(); NET_Shutdown (); FS_Shutdown(); #endif #ifdef QUAKEHUD Stats_Clear(); #endif Ruleset_Shutdown(); COM_DestroyWorkerThread(); P_ShutdownParticleSystem(); Cvar_Shutdown(); Validation_FlushFileList(); #ifdef HAVE_GNUTLS GnuTLS_Shutdown(); #endif Cmd_Shutdown(); #ifdef PACKAGEMANAGER PM_Shutdown(false); #endif Key_Unbindall_f(); #ifdef PLUGINS Plug_Shutdown(true); #endif for (i = 0; i < MAX_SPLITS; i++) InfoBuf_Clear(&cls.userinfo[i], true); InfoSync_Clear(&cls.userinfosync); Con_Shutdown(); COM_BiDi_Shutdown(); Memory_DeInit(); #ifdef HAVE_SERVER SV_WipeServerState(); memset(&svs, 0, sizeof(svs)); #endif Sys_Shutdown(); } #ifndef HAVE_SERVER void SV_EndRedirect (void) { } #endif ================================================ FILE: engine/client/cl_master.h ================================================ #ifndef CL_MASTER_H #define CL_MASTER_H enum masterprotocol_e { MP_UNSPECIFIED, MP_QUAKEWORLD, #if defined(Q2CLIENT) || defined(Q2SERVER) MP_QUAKE2, #endif #if defined(Q3CLIENT) || defined(Q3SERVER) MP_QUAKE3, #endif #ifdef NQPROT MP_NETQUAKE, #endif MP_DPMASTER }; #if defined(CL_MASTER) && defined(HAVE_CLIENT) #define SS_PROTOCOLMASK 0xf #define SS_UNKNOWN 0 #define SS_QUAKEWORLD 1 #define SS_NETQUAKE 2 #define SS_QUAKE2 3 #define SS_QUAKE3 4 #define SS_QEPROT 5 //needs dtls and a different ccreq version //#define SS_UNUSED 6 //#define SS_UNUSED 7 #define SS_LOCAL (1<<3u) //local servers are ones we detected without being listed on a master server (masters will report public ips, so these may appear as dupes if they're also public) #define SS_FTESERVER (1<<4u) //just highlighting differences, to give some impression of superiority. #define SS_FAVORITE (1<<5u) //filter all others. #define SS_KEEPINFO (1<<6u) #define SS_GETINFO (1<<7u) //explicitly query via getinfo #define SS_PROXY (1<<8u) //qizmo/qwfwd/qtv/eztv #define SS_RELAY (1<<9u) //supports the \prx\nexthop relay thing, and pingstatus requests for connectbr. #define PING_DEAD 0xffff //default ping value to denote servers that are not responding. #define PING_UNKNOWN 0xfffe //these servers are considered up, but we can't query them directly so can't determine the final ping from here. #define PING_MAX 0xfffd //highest 'valid' ping value. //despite not supporting nq or q2, we still load them. We just filter them. This is to make sure we properly write the listing files. enum mastertype_e { MT_BAD, //this would be an error // MT_MASTERHTTPJSON, MT_MASTERHTTP, MT_MASTERUDP, MT_BCAST, MT_SINGLE, }; typedef enum hostcachekey_e { SLKEY_PING, SLKEY_MAP, SLKEY_NAME, SLKEY_ADDRESS, SLKEY_NUMPLAYERS, SLKEY_MAXPLAYERS, SLKEY_GAMEDIR, SLKEY_FREEPLAYERS, SLKEY_BASEGAME, SLKEY_FLAGS, SLKEY_TIMELIMIT, SLKEY_FRAGLIMIT, SLKEY_MOD, SLKEY_PROTOCOL, SLKEY_NUMBOTS, //uninteresting bots that will presumably get kicked if people join. SLKEY_NUMSPECTATORS,//spectators SLKEY_NUMHUMANS, //actual players SLKEY_QCSTATUS, SLKEY_CATEGORY, //urgh, hideous shite. // SLKEY_PLAYERS, //eep! SLKEY_ISFAVORITE,//eep! SLKEY_ISLOCAL, SLKEY_ISPROXY, SLKEY_SERVERINFO, SLKEY_TOOMANY, SLKEY_PLAYER0, SLKEY_CUSTOM = SLKEY_PLAYER0+MAX_CLIENTS } hostcachekey_t; typedef enum slist_test_e { SLIST_TEST_CONTAINS, SLIST_TEST_NOTCONTAIN, SLIST_TEST_LESSEQUAL, SLIST_TEST_LESS, SLIST_TEST_EQUAL, SLIST_TEST_GREATER, SLIST_TEST_GREATEREQUAL, SLIST_TEST_NOTEQUAL, SLIST_TEST_STARTSWITH, SLIST_TEST_NOTSTARTSWITH } slist_test_t; //contains info about a server in greater detail. Could be too mem intensive. typedef struct serverdetailedinfo_s { char info[MAX_SERVERINFO_STRING]; int numplayers; struct serverdetailedplayerinfo_s { int userid; int frags; float time; int ping; char name[64]; char skin[16]; //is this even useful? char team[16]; char topc; char botc; qbyte isspec; } players[MAX_CLIENTS]; } serverdetailedinfo_t; //hold minimum info. typedef struct serverinfo_s { char name[80]; //hostname. netadr_t adr; char brokerid[64]; //'rtc[s]://adr//brokerid' short special; //flags short protocol; qbyte players; qbyte maxplayers; qbyte sends; qbyte status; #define SRVSTATUS_ALIVE 1u //server is responding to pings #define SRVSTATUS_DISPLAYED 2u //server passed all filters #define SRVSTATUS_GLOBAL 4u //server was reported by one of the master servers (ie: global and not local) qbyte numspectators; qbyte numhumans; qbyte numbots; qbyte freeslots; int qccategory; //urgh char qcstatus[128]; char modname[8+1]; char gamedir[8+1]; char map[16]; // unsigned short gameversion; unsigned short ping; short tl; short fl; float refreshtime; serverdetailedinfo_t *moreinfo; struct serverinfo_s *prevpeer; unsigned short cost; unsigned short numpeers; struct peers_s { struct serverinfo_s *peer; unsigned short ping; } *peers; struct serverinfo_s *next; } serverinfo_t; typedef struct master_s { struct master_s *next; netadr_t adr; char *address; //text based address (http servers) struct dl_download *dl; qbyte nosave; qbyte mastertype; qbyte protocoltype; int sends; /*needs to resend?*/ char name[1]; } master_t; extern struct selectedserver_s { qboolean inuse; netadr_t adr; char brokerid[64]; float refreshtime; int lastplayer; char lastrule[64]; serverdetailedinfo_t *detail; int linenum; } selectedserver; typedef struct player_s { char name[16]; int frags; int colour; char skin[8]; char team[8]; netadr_t adr; struct player_s *next; } player_t; void SListOptionChanged(serverinfo_t *newserver); extern serverinfo_t *firstserver; extern master_t *master; extern player_t *mplayers; extern qboolean sb_favouriteschanged; void Master_SetupSockets(void); qboolean CL_QueryServers(void); void Master_CheckPollSockets(void); void MasterInfo_Shutdown(void); void MasterInfo_WriteServers(void); serverinfo_t *Master_InfoForServer (netadr_t *addr, const char *brokerid); serverinfo_t *Master_InfoForNum (int num); unsigned int Master_TotalCount(void); unsigned int Master_NumPolled(void); //progress indicator unsigned int Master_NumAlive(void); void Master_SetupSockets(void); void MasterInfo_Refresh(qboolean doreset); void Master_QueryServer(serverinfo_t *server); void MasterInfo_WriteServers(void); char *Master_ServerToString (char *s, int len, serverinfo_t *a); //like NET_AdrToString, but handles more complex addresses. hostcachekey_t Master_KeyForName(const char *keyname); float Master_ReadKeyFloat(serverinfo_t *server, unsigned int keynum); char *Master_ReadKeyString(serverinfo_t *server, unsigned int keynum); int Master_SortServers(void); void Master_SetSortField(hostcachekey_t field, unsigned int sortflags); hostcachekey_t Master_GetSortField(void); qboolean Master_GetSortDescending(void); int Master_NumSorted(void); void Master_ClearMasks(void); serverinfo_t *Master_SortedServer(int idx); void Master_SetMaskString(qboolean or_, hostcachekey_t field, const char *param, slist_test_t testop); void Master_SetMaskInteger(qboolean or_, hostcachekey_t field, int param, slist_test_t testop); serverinfo_t *Master_FindRoute(netadr_t target); #endif #endif ================================================ FILE: engine/client/cl_parse.c ================================================ /* Copyright (C) 1996-1997 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. */ // cl_parse.c -- parse a message received from the server #include "quakedef.h" #include "cl_ignore.h" #include "shader.h" #include "fs.h" void CL_GetNumberedEntityInfo (int num, float *org, float *ang); void CLDP_ParseDarkPlaces5Entities(void); void CLH2_ParseEntities(void); static void CL_SetStatNumeric (int pnum, unsigned int stat, int ivalue, float fvalue); #define CL_SetStatInt(pnum,stat,ival) do{int thevalue=ival; CL_SetStatNumeric(pnum,stat,thevalue,thevalue);}while(0) #define CL_SetStatFloat(pnum,stat,fval) do{float thevalue=fval; CL_SetStatNumeric(pnum,stat,thevalue,thevalue);}while(0) static qboolean CL_CheckModelResources (char *name); #ifdef NQPROT static char *CLNQ_ParseProQuakeMessage (char *s); #endif static void DLC_Poll(qdownload_t *dl); static void CL_ProcessUserInfo (int slot, player_info_t *player); static void CL_ParseStuffCmd(char *msg, int destsplit); #define MSG_ReadBigIndex() ((cls.fteprotocolextensions2&PEXT2_LONGINDEXES)?(unsigned int)MSG_ReadUInt64():MSG_ReadByte ()) #define MSG_ReadPlayer() MSG_ReadBigIndex() #ifdef NQPROT char *cl_dp_packagenames; static char cl_dp_csqc_progsname[128]; static int cl_dp_csqc_progssize; static int cl_dp_csqc_progscrc; static int cl_dp_serverextension_download; #endif //tracks which svcs are using what data (per second) static size_t packetusage_saved[256]; static size_t packetusage_pending[256]; static double packetusageflushtime; static const double packetusage_interval=10; #ifdef AVAIL_ZLIB #ifndef ZEXPORT #define ZEXPORT VARGS #endif #include #endif static const char *svc_qwstrings[] = { "svc_bad", "svc_nop", "svc_disconnect", "svcqw_updatestatbyte", "svc_version", // [long] server version "svc_setview", // [short] entity number "svcqw_sound", // "svc_time", // [float] server time "svc_print", // [string] null terminated string "svc_stufftext", // [string] stuffed into client's console buffer // the string should be \n terminated "svc_setangle", // [vec3] set the view angle to this absolute value "svc_serverdata", // [long] version ... "svc_lightstyle", // [qbyte] [string] "svc_updatename", // [qbyte] [string] "svc_updatefrags", // [qbyte] [short] "svc_clientdata", // "svc_stopsound", // "svc_updatecolors", // [qbyte] [qbyte] "svc_particle", // [vec3] "svc_damage", // [qbyte] impact [qbyte] blood [vec3] from "svc_spawnstatic", "svcfte_spawnstatic2", "svc_spawnbaseline", "svc_temp_entity", // "svc_setpause", "svc_signonnum", "svc_centerprint", "svc_killedmonster", "svc_foundsecret", "svc_spawnstaticsound", "svc_intermission", "svc_finale", "svc_cdtrack", "svc_sellscreen", "svc_smallkick", "svc_bigkick", "svc_updateping", "svc_updateentertime", "svc_updatestatlong", "svc_muzzleflash", "svc_updateuserinfo", "svc_download", "svc_playerinfo", "svc_nails", "svc_choke", "svc_modellist", "svc_soundlist", "svc_packetentities", "svc_deltapacketentities", "svc_maxspeed", "svc_entgravity", "svc_setinfo", "svc_serverinfo", "svc_updatepl", "MVD svc_nails2", "svcfte_soundextended", "svcfte_soundlistshort", "FTE svc_lightstylecol", "FTE svc_bulletentext", // obsolete "FTE svc_lightnings", "FTE svc_modellistshort", "FTE svc_ftesetclientpersist", "FTE svc_setportalstate", "FTE svc_particle2", "FTE svc_particle3", "FTE svc_particle4", "FTE svc_spawnbaseline2", "FTE svc_customtempent", "FTE svc_choosesplitclient", "svcfte_showpic", "svcfte_hidepic", "svcfte_movepic", "svcfte_updatepic", "NEW PROTOCOL(73)", "svcfte_effect", "svcfte_effect2", "svcfte_csqcentities", "svcfte_precache", "svcfte_updatestatstring", "svcfte_updatestatfloat", "svcfte_trailparticles", "svcfte_pointparticles", "svcfte_pointparticles1", "svcfte_cgamepacket", "svcfte_voicechat", "svcfte_setangledelta", "svcfte_updateentities", "svcfte_brushedit", "svcfte_updateseats", "svcfte_setinfoblob", //89 "svcfte_cgamepacket_sized", //90 "svcfte_temp_entity_sized", //91 "svcfte_csqcentities_sized", //92 "NEW PROTOCOL(93)", "NEW PROTOCOL(94)", "NEW PROTOCOL(95)", "NEW PROTOCOL(96)", "NEW PROTOCOL(97)", "NEW PROTOCOL(98)", "NEW PROTOCOL(99)", "NEW PROTOCOL(100)", "NEW PROTOCOL(101)", "NEW PROTOCOL(102)", "NEW PROTOCOL(103)", "NEW PROTOCOL(104)", "NEW PROTOCOL(105)", "NEW PROTOCOL(106)", "NEW PROTOCOL(107)", "NEW PROTOCOL(108)", }; #ifdef NQPROT static const char *svc_nqstrings[] = { "nqsvc_bad", "nqsvc_nop", "nqsvc_disconnect", "nqsvc_updatestatlong", "nqsvc_version", // [long] server version "nqsvc_setview", // [short] entity number "nqsvc_sound", // "nqsvc_time", // [float] server time "nqsvc_print", // [string] null terminated string "nqsvc_stufftext", // [string] stuffed into client's console buffer // the string should be \n terminated "nqsvc_setangle", // [vec3] set the view angle to this absolute value "nqsvc_serverinfo", // [long] version // [string] signon string // [string]..[0]model cache [string]...[0]sounds cache // [string]..[0]item cache "nqsvc_lightstyle", // [qbyte] [string] "nqsvc_updatename", // [qbyte] [string] "nqsvc_updatefrags", // [qbyte] [short] "nqsvc_clientdata", // "nqsvc_stopsound", // "nqsvc_updatecolors", // [qbyte] [qbyte] "nqsvc_particle", // [vec3] "nqsvc_damage", // [qbyte] impact [qbyte] blood [vec3] from "nqsvc_spawnstatic", "ftenq_spawnstatic2(21)", "nqsvc_spawnbaseline", "nqsvc_temp_entity", // "nqsvc_setpause", "nqsvc_signonnum", "nqsvc_centerprint", "nqsvc_killedmonster", "nqsvc_foundsecret", "nqsvc_spawnstaticsound", "nqsvc_intermission", "nqsvc_finale", // [string] music [string] text "nqsvc_cdtrack", // [qbyte] track [qbyte] looptrack "nqsvc_sellscreen", "nqsvc_cutscene", //34 "NEW PROTOCOL", //35 "NEW PROTOCOL", //36 "fitz_skybox", //37 "NEW PROTOCOL", //38 "NEW PROTOCOL", //39 "fitz_bf", //40 "fitz_fog", //41 "fitz_spawnbaseline2", //42 "fitz_spawnstatic2", //43 "fitz_spawnstaticsound2", //44 "NEW PROTOCOL", //45 "qex_updateping", //46 "qex_updatesocial", //47 "qex_updateplinfo", //48 "qex_print", //49 "dp_downloaddata / neh_skyboxsize / qex_servervars", //50 "dp_updatestatubyte / neh_fog / qex_seq", //51 "dp_effect / qex_achievement", //52 "dp_effect2 / qex_chat", //53 "dp6_precache / dp5_sound2 / qex_levelcompleted", //54 "dp_spawnbaseline2 / qex_backtolobby", //55 "dp_spawnstatic2 / qex_localsound", //56 obsolete "dp_entities / qex_prompt", //57 "dp_csqcentities / qex_loccenterprint", //58 "dp_spawnstaticsound2", //59 "dp_trailparticles", //60 "dp_pointparticles", //61 "dp_pointparticles1", //62 "NEW PROTOCOL(63)", //63 "NEW PROTOCOL(64)", //64 "NEW PROTOCOL(65)", //65 "ftenq_spawnbaseline2", //66 "NEW PROTOCOL(67)", //67 "NEW PROTOCOL(68)", //68 "NEW PROTOCOL(69)", //69 "NEW PROTOCOL(70)", //70 "NEW PROTOCOL(71)", //71 "NEW PROTOCOL(72)", //72 "NEW PROTOCOL(73)", //73 "NEW PROTOCOL(74)", //74 "NEW PROTOCOL(75)", //75 "NEW PROTOCOL(76)", //76 "NEW PROTOCOL(77)", //77 "ftenq_updatestatstring", //78 "ftenq_updatestatfloat", //79 "NEW PROTOCOL(80)", //80 "NEW PROTOCOL(81)", //81 "NEW PROTOCOL(82)", //82 "ftenq_cgamepacket", //83 "ftenq_voicechat", //84 "ftenq_setangledelta", //85 "ftenq_updateentities", //86 "NEW PROTOCOL(87)", //87 "NEW PROTOCOL(88)", //88 "ftenq_setinfoblob", //89 "ftenq_cgamepacket_sized", //90 "ftenq_temp_entity_sized", //91 "ftenq_csqcentities_sized", //92 }; #endif extern cvar_t requiredownloads, mod_precache, snd_precache, cl_standardchat, msg_filter, msg_filter_frags, msg_filter_pickups, cl_countpendingpl, cl_download_mapsrc; int oldparsecountmod; int parsecountmod; double parsecounttime; int cl_spikeindex, cl_playerindex, cl_h_playerindex, cl_flagindex, cl_rocketindex, cl_grenadeindex, cl_gib1index, cl_gib2index, cl_gib3index; //called after disconnect, purges all memory that was allocated etc void CL_Parse_Disconnected(void) { if (cls.download) { //note: not all downloads abort when the server disconnects, as they're fully out of bounds (ie: http) if (cls.download->method <= DL_QWPENDING) DL_Abort(cls.download, QDL_DISCONNECT); } { downloadlist_t *next; while(cl.downloadlist) { next = cl.downloadlist->next; Z_Free(cl.downloadlist); cl.downloadlist = next; } while(cl.faileddownloads) { next = cl.faileddownloads->next; Z_Free(cl.faileddownloads); cl.faileddownloads = next; } } CL_ClearParseState(); } //============================================================================= float packet_latency[NET_TIMINGS]; int CL_CalcNet (float scale) { int i; outframe_t *frame; int lost = 0; int percent; int sent; // char st[80]; sent = NET_TIMINGS; for (i=cl.movesequence-UPDATE_BACKUP+1 ; i <= cl.movesequence ; i++) { frame = &cl.outframes[i&UPDATE_MASK]; if (i > cl.ackedmovesequence) { // no response yet if (cl_countpendingpl.ival) { packet_latency[i&NET_TIMINGSMASK] = 9999; lost++; } else packet_latency[i&NET_TIMINGSMASK] = 10000; } else if (frame->latency == -1) { packet_latency[i&NET_TIMINGSMASK] = 9999; // dropped lost++; } else if (frame->latency == -2) packet_latency[i&NET_TIMINGSMASK] = 10000; // choked else if (frame->latency == -3) { packet_latency[i&NET_TIMINGSMASK] = 9997; // c2spps sent--; } // else if (frame->invalid) // packet_latency[i&NET_TIMINGSMASK] = 9998; // invalid delta else packet_latency[i&NET_TIMINGSMASK] = frame->latency * 60 * scale; } if (sent < 1) percent = 100; //shouldn't ever happen. else percent = lost * 100 / sent; return percent; } void CL_CalcNet2 (float *pings, float *pings_min, float *pings_max, float *pingms_stddev, float *pingfr, int *pingfr_min, int *pingfr_max, float *dropped, float *choked, float *invalid) { int i; outframe_t *frame; int lost = 0; int pending = 0; int sent; int valid = 0; int fr; int nchoked = 0; int ninvalid = 0; // char st[80]; *pings = 0; *pings_max = 0; *pings_min = FLT_MAX; *pingfr = 0; *pingfr_max = 0; *pingfr_min = 0x7fffffff; *pingms_stddev = 0; sent = NET_TIMINGS; for (i=cl.movesequence-UPDATE_BACKUP+1 ; i <= cl.movesequence ; i++) { frame = &cl.outframes[i&UPDATE_MASK]; if (i > cl.lastackedmovesequence) { // no response yet if (cl_countpendingpl.ival) lost++; } else if (frame->latency == -1) lost++; // lost else if (frame->latency == -2) nchoked++; // choked else if (frame->latency == -3) sent--; // c2spps else if (frame->latency == -4) ninvalid++; //corrupt/wrong/dodgy/egads else { *pings += frame->latency; if (*pings_max < frame->latency) *pings_max = frame->latency; if (*pings_min > frame->latency) *pings_min = frame->latency; fr = frame->cmd_sequence-frame->server_message_num; *pingfr += fr; if (*pingfr_max < fr) *pingfr_max = fr; if (*pingfr_min > fr) *pingfr_min = fr; valid++; } } if (valid) { *pings /= valid; *pingfr /= valid; //determine stddev, in milliseconds instead of seconds. for (i=cl.movesequence-UPDATE_BACKUP+1; i <= cl.movesequence; i++) { frame = &cl.outframes[i&UPDATE_MASK]; if (i <= cl.lastackedmovesequence && frame->latency >= 0) { float dev = (frame->latency - *pings) * 1000; *pingms_stddev += dev*dev; } } *pingms_stddev = sqrt(*pingms_stddev/valid); } if (pending == sent || sent < 1) *dropped = 1; //shouldn't ever happen. else *dropped = (float)lost / sent; *choked = (float)nchoked / sent; *invalid = (float)ninvalid / sent; } void CL_AckedInputFrame(int inseq, int outseq, qboolean worldstateokay) { unsigned int i; unsigned int newmod; outframe_t *frame; //calc the latency for this frame, but only if its not a dupe ack. we want the youngest, not the oldest, so we can calculate network latency rather than simply packet frequency if (outseq != cl.lastackedmovesequence) { newmod = outseq & UPDATE_MASK; frame = &cl.outframes[newmod]; // calculate latency frame->latency = realtime - frame->senttime; if (frame->latency < 0 || frame->latency > 1.0) { // Con_Printf ("Odd latency: %5.2f\n", latency); } else { // drift the average latency towards the observed latency if (frame->latency < cls.latency) cls.latency = frame->latency; else cls.latency += 0.001; // drift up, so correction are needed } if (cls.protocol != CP_NETQUAKE && cl.inframes[inseq&UPDATE_MASK].invalid) frame->latency = -4; //and mark any missing ones as dropped for (i = (cl.lastackedmovesequence+1) & UPDATE_MASK; i != newmod; i=(i+1)&UPDATE_MASK) { //nq has no concept of choking. outbound packets that are accepted during a single frame will be erroneoulsy considered dropped. nq never had a netgraph based upon outgoing timings. // Con_Printf("Dropped moveframe %i\n", i); if (cls.protocol == CP_NETQUAKE && CPNQ_IS_DP) { //dp doesn't ack every single packet. trying to report packet loss correctly is futile, we'll just get bad-mouthed. cl.outframes[i].latency = -2; //flag as choked } else cl.outframes[i].latency = -1; //flag as dropped } } cl.inframes[inseq&UPDATE_MASK].ackframe = outseq; if (worldstateokay) cl.ackedmovesequence = outseq; cl.lastackedmovesequence = outseq; } //============================================================================= int CL_IsDownloading(const char *localname) { downloadlist_t *dl; /*check for dupes*/ for (dl = cl.downloadlist; dl; dl = dl->next) //It's already on our list. Ignore it. { if (!strcmp(dl->localname, localname)) return 2; //queued } if (cls.download) if (!strcmp(cls.download->localname, localname)) return 1; //downloading return 0; } //note: this will overwrite existing files. //returns true if the download is going to be downloaded after the call. qboolean CL_EnqueDownload(const char *filename, const char *localname, unsigned int flags) { extern cvar_t cl_downloads; downloadlist_t *dl; qboolean webdl = false; char ext[8]; if ((flags & DLLF_TRYWEB) || !strncmp(filename, "http://", 7) || !strncmp(filename, "https://", 8)) { flags |= DLLF_TRYWEB; if (!localname) return false; webdl = true; } else { if (!localname) localname = filename; if (cls.state < ca_connected) return false; if (cls.demoplayback && !(cls.demoplayback == DPB_MVD && (cls.demoeztv_ext&EZTV_DOWNLOAD))) return false; } COM_FileExtension(localname, ext, sizeof(ext)); if (!stricmp(ext, "dll") || !stricmp(ext, "so") || strchr(localname, '\\') || strchr(localname, ':') || strstr(localname, "..")) { CL_DownloadFailed(filename, NULL, DLFAIL_UNTRIED); Con_Printf("Denying download of \"%s\"\n", filename); return false; } if (!(flags & DLLF_USEREXPLICIT) && !cl_downloads.ival) { CL_DownloadFailed(filename, NULL, DLFAIL_CLIENTCVAR); if (flags & DLLF_VERBOSE) Con_Printf("cl_downloads setting prevents download of \"%s\"\n", filename); return false; } /*reject if it already failed*/ if (!(flags & DLLF_IGNOREFAILED)) { #ifdef NQPROT if (!webdl && cls.protocol == CP_NETQUAKE) if (!cl_dp_serverextension_download) { CL_DownloadFailed(filename, NULL, DLFAIL_UNSUPPORTED); return false; } #endif for (dl = cl.faileddownloads; dl; dl = dl->next) //yeah, so it failed... Ignore it. { if (!strcmp(dl->rname, filename)) { if (flags & DLLF_VERBOSE) Con_Printf("We've failed to download \"%s\" already\n", filename); return false; } } } /*check for dupes*/ switch(CL_IsDownloading(localname)) { case 2: if (flags & DLLF_VERBOSE) Con_Printf("Already waiting for \"%s\"\n", filename); return true; default: case 1: if (flags & DLLF_VERBOSE) Con_Printf("Already downloading \"%s\"\n", filename); return true; case 0: break; } if (!*filename) { Con_Printf("Download \"\"? Huh?\n"); return true; } dl = Z_Malloc(sizeof(downloadlist_t)); Q_strncpyz(dl->rname, filename, sizeof(dl->rname)); Q_strncpyz(dl->localname, localname, sizeof(dl->localname)); dl->next = cl.downloadlist; dl->size = 0; dl->flags = flags | DLLF_SIZEUNKNOWN; if (!cl.downloadlist) flags &= ~DLLF_VERBOSE; cl.downloadlist = dl; if (!webdl && (cls.fteprotocolextensions & (PEXT_CHUNKEDDOWNLOADS #ifdef PEXT_PK3DOWNLOADS | PEXT_PK3DOWNLOADS #endif )) && !(dl->flags & DLLF_TEMPORARY)) { CL_SendClientCommand(true, "dlsize \"%s\"", dl->rname); } if (flags & DLLF_VERBOSE) Con_Printf("Enqued download of \"%s\"\n", filename); return true; } void CL_GetDownloadSizes(unsigned int *filecount, qofs_t *totalsize, qboolean *somesizesunknown) { downloadlist_t *dl; qdownload_t *d; *filecount = 0; *totalsize = 0; *somesizesunknown = false; for(dl = cl.downloadlist; dl; dl = dl->next) { *filecount += 1; if (dl->flags & DLLF_SIZEUNKNOWN) *somesizesunknown = true; else *totalsize += dl->size; } d = cls.download; if (d) { if (d->sizeunknown) *somesizesunknown = true; *totalsize += d->size; } } static void CL_DisenqueDownload(char *filename) { downloadlist_t *dl, *nxt; if(cl.downloadlist) //remove from enqued download list { if (!strcmp(cl.downloadlist->rname, filename)) { dl = cl.downloadlist; cl.downloadlist = cl.downloadlist->next; Z_Free(dl); } else { for (dl = cl.downloadlist; dl->next; dl = dl->next) { if (!strcmp(dl->next->rname, filename)) { nxt = dl->next->next; Z_Free(dl->next); dl->next = nxt; break; } } } } } #ifdef WEBCLIENT static void CL_WebDownloadFinished(struct dl_download *dl) { if (dl->status == DL_FAILED) { if (dl->replycode == 404) //regular file-not-found CL_DownloadFailed(dl->url, &dl->qdownload, DLFAIL_SERVERFILE); else //other stuff is PROBABLY 403forbidden, but lets blame the server's config if its a tls issue etc. CL_DownloadFailed(dl->url, &dl->qdownload, DLFAIL_SERVERCVAR); if (dl->qdownload.flags & DLLF_ALLOWWEB) //re-enqueue it if allowed, but this time not from the web server. CL_EnqueDownload(dl->qdownload.localname, dl->qdownload.localname, dl->qdownload.flags & ~(DLLF_ALLOWWEB|DLLF_TRYWEB)); } else if (dl->status == DL_FINISHED) { if (dl->file) VFS_CLOSE(dl->file); dl->file = NULL; CL_DownloadFinished(&dl->qdownload); } } #endif static void CL_SendDownloadStartRequest(downloadlist_t *pending) { char *filename = pending->rname; char *localname = pending->localname; unsigned int flags = pending->flags; static int dlsequence; qdownload_t *dl; //don't download multiple things at once... its leaky if nothing else. if (cls.download) return; #ifdef WEBCLIENT if (flags & DLLF_TRYWEB) { struct dl_download *wdl = HTTP_CL_Get(filename, localname, CL_WebDownloadFinished); if (wdl) { if (flags & DLLF_NONGAME) { wdl->fsroot = FS_ROOT; if (!strncmp(localname, "package/", 8)) Q_strncpyz(wdl->localname, localname+8, sizeof(wdl->localname)); } if (!(flags & DLLF_TEMPORARY)) Con_TPrintf ("Downloading %s to %s...\n", wdl->url, wdl->localname); wdl->qdownload.flags = flags; CL_DisenqueDownload(filename); cls.download = &wdl->qdownload; } else CL_DownloadFailed(filename, NULL, DLFAIL_CLIENTCVAR); return; } #endif dl = Z_Malloc(sizeof(*dl)); dl->filesequence = ++dlsequence; Q_strncpyz(dl->remotename, filename, sizeof(dl->remotename)); Q_strncpyz(dl->localname, localname, sizeof(dl->localname)); if (!(flags & DLLF_TEMPORARY)) Con_TPrintf ("Downloading %s...\n", dl->localname); // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left COM_StripExtension (localname, dl->tempname, sizeof(dl->tempname)-5); Q_strncatz (dl->tempname, ".tmp", sizeof(dl->tempname)); #ifdef AVAIL_ZLIB if (cls.protocol == CP_QUAKE2 && cls.protocol_q2 == PROTOCOL_VERSION_R1Q2) CL_SendClientCommand(true, "download %s 0 udp-zlib", filename); else #endif CL_SendClientCommand(true, "download %s", filename); dl->method = DL_QWPENDING; dl->percent = 0; dl->sizeunknown = true; dl->flags = flags&DLLF_OVERWRITE; CL_DisenqueDownload(filename); cls.download = dl; } //Do any reloading for the file that just reloaded. void CL_DownloadFinished(qdownload_t *dl) { int i; char ext[8]; char filename[MAX_QPATH]; char tempname[MAX_QPATH]; Q_strncpyz(filename, dl->localname, sizeof(filename)); Q_strncpyz(tempname, dl->tempname, sizeof(tempname)); DL_Abort(dl, QDL_COMPLETED); FS_FlushFSHashWritten(filename); COM_FileExtension(filename, ext, sizeof(ext)); //should probably ask the filesytem code if its a package format instead. if (!strncmp(filename, "package/", 8) || !strncmp(ext, "pk4", 3) || !strncmp(ext, "pk3", 3) || !strncmp(ext, "pak", 3) || (dl->fsroot == FS_ROOT)) { FS_ReloadPackFiles(); CL_CheckServerInfo(); } else if (!strcmp(filename, "gfx/palette.lmp")) { Cbuf_AddText("vid_restart\n", RESTRICT_LOCAL); } else { CL_CheckModelResources(filename); Mod_FileWritten(filename); { for (i = 0; i < MAX_PRECACHE_MODELS; i++) //go and load this model now. { if (cl.model_name[i] && !strcmp(cl.model_name[i], filename)) { if (cl.model_precache[i] && cl.model_precache[i]->loadstate == MLS_FAILED) cl.model_precache[i]->loadstate = MLS_NOTLOADED; CL_CheckModelResources(cl.model_name[i]); cl.model_precache[i] = Mod_ForName(cl.model_name[i], ((i==1)?MLV_WARNSYNC:MLV_WARN)); if (i == 1) { cl.worldmodel = cl.model_precache[i]; //just in case. if (cl.model_precache[1] && cl.model_precache[1]->loadstate == MLS_LOADED) FS_LoadMapPackFile(cl.model_precache[1]->name, cl.model_precache[1]->archive); } break; } } for (i = 0; i < MAX_CSMODELS; i++) //go and load this model now. { if (!strcmp(cl.model_csqcname[i], filename)) { if (cl.model_csqcprecache[i] && cl.model_csqcprecache[i]->loadstate == MLS_FAILED) cl.model_csqcprecache[i]->loadstate = MLS_NOTLOADED; CL_CheckModelResources(cl.model_csqcname[i]); cl.model_csqcprecache[i] = Mod_ForName(cl.model_csqcname[i], MLV_WARN); break; } } #ifdef HAVE_LEGACY for (i = 0; i < MAX_VWEP_MODELS; i++) { if (cl.model_name_vwep[i] && !strcmp(cl.model_name_vwep[i], filename)) { if (cl.model_precache_vwep[i] && cl.model_precache_vwep[i]->loadstate == MLS_FAILED) cl.model_precache_vwep[i]->loadstate = MLS_NOTLOADED; CL_CheckModelResources(cl.model_name_vwep[i]); cl.model_precache_vwep[i] = Mod_ForName(cl.model_name_vwep[i], MLV_WARN); break; } } #endif } S_ResetFailedLoad(); //okay, so this can still get a little spammy in bad places... #ifdef QWSKINS //this'll do the magic for us Skin_FlushSkin(filename); #endif } } static qboolean CL_CheckFile(const char *filename) { if (strstr (filename, "..")) { Con_TPrintf ("Refusing to download a path with ..\n"); return true; } if (COM_FCheckExists (filename)) { // it exists, no need to download return true; } return false; } qboolean CL_CheckDLFile(const char *filename) { if (!strncmp(filename, "package/", 8)) { vfsfile_t *f; f = FS_OpenVFS(filename+8, "rb", FS_ROOT); if (f) { VFS_CLOSE(f); return true; } return false; } else return COM_FCheckExists(filename); } /* =============== CL_CheckOrEnqueDownloadFile Returns true if the file exists, returns false if it triggered a download. =============== */ qboolean CL_CheckOrEnqueDownloadFile (const char *filename, const char *localname, unsigned int flags) { //returns false if we don't have the file yet. COM_AssertMainThread("CL_CheckOrEnqueDownloadFile"); if (flags & DLLF_NONGAME) { /*pak/pk3 downloads have an explicit leading package/ as an internal/network marker*/ if (!strchr(filename, ':')) filename = va("package/%s", filename); localname = va("package/%s", localname); } /*files with a leading * should not be downloaded (inline models, sexed sounds, etc). also block anyone trying to explicitly download a package/ because our code (wrongly) uses that name internally*/ else if (*filename == '*' || !strncmp(filename, "package/", 8)) return true; if (!localname) localname = filename; #ifndef CLIENTONLY /*no downloading if we're the one we'd be downloading from*/ if (sv.state) return true; #endif if (!(flags & DLLF_OVERWRITE)) { if (CL_CheckDLFile(localname)) return true; } //ZOID - can't download when recording if (cls.demorecording) { Con_TPrintf ("Unable to download %s in record mode.\n", filename); #if defined(MVD_RECORDING) && defined(HAVE_SERVER) if (sv_demoAutoRecord.ival) Con_TPrintf ("Note that ^[%s\\cmd\\%s 0\\^] is enabled.\n", sv_demoAutoRecord.name, sv_demoAutoRecord.name); #endif return true; } //ZOID - can't download when playback // if (cls.demoplayback && cls.demoplayback != DPB_EZTV) // return true; SCR_EndLoadingPlaque(); //release console. if (flags & DLLF_ALLOWWEB) { const char *dlURL = InfoBuf_ValueForKey(&cl.serverinfo, "sv_dlURL"); if (!*dlURL) dlURL = cls.downloadurl; if (!*dlURL) dlURL = fs_dlURL.string; if (strncmp(dlURL, "http://", 7) && strncmp(dlURL, "https://", 8)) dlURL = ""; //only allow http+https here. just paranoid. flags &= ~(DLLF_TRYWEB|DLLF_ALLOWWEB); if (*dlURL && (flags & DLLF_NONGAME) && !strncmp(filename, "package/", 8)) { //filename is something like: package/GAMEDIR/foo.pk3 filename = va("%s%s%s", dlURL, ((dlURL[strlen(dlURL)-1]=='/')?"":"/"), filename+8); flags |= DLLF_TRYWEB|DLLF_ALLOWWEB; } else if (*dlURL) { //we don't really know which gamedir its meant to be for... #ifdef Q2CLIENT //ffs if (!strncmp(filename, "pics/../", 8)) filename += 8; #endif filename = va("%s%s%s/%s", dlURL, ((dlURL[strlen(dlURL)-1]=='/')?"":"/"), FS_GetGamedir(true), filename); flags |= DLLF_TRYWEB|DLLF_ALLOWWEB; } else if (*cl_download_mapsrc.string && !strcmp(filename, localname) && !strncmp(filename, "maps/", 5) && !strcmp(filename + strlen(filename)-4, ".bsp")) { char base[MAX_QPATH]; COM_FileBase(filename, base, sizeof(base)); #ifndef FTE_TARGET_WEB //don't care about prefixes in the web build, for site-relative uris. if (strncmp(cl_download_mapsrc.string, "http://", 7) && strncmp(cl_download_mapsrc.string, "https://", 8)) { Con_Printf("%s: Scheme not specified, assuming https.\n", cl_download_mapsrc.name); filename = va("https://%s/%s", cl_download_mapsrc.string, filename+5); } else #endif filename = va("%s%s", cl_download_mapsrc.string, filename+5); flags |= DLLF_TRYWEB|DLLF_ALLOWWEB; } } if (!CL_EnqueDownload(filename, localname, flags)) return true; /*don't stall waiting for it if it failed*/ if (!(flags & DLLF_IGNOREFAILED)) { downloadlist_t *dl; for (dl = cl.faileddownloads; dl; dl = dl->next) { if (!strcmp(dl->rname, filename)) { //if its on the failed list, don't block waiting for it to download return true; } } } return false; } static qboolean CL_CheckMD2Skins (qbyte *precache_model) { qboolean ret = false; md2_t *pheader; int skin = 1; char *str; pheader = (md2_t *)precache_model; if (LittleLong (pheader->version) != MD2ALIAS_VERSION) { //bad version. return false; } pheader = (md2_t *)precache_model; for (skin = 0; skin < LittleLong(pheader->num_skins); skin++) { str = (char *)precache_model + LittleLong(pheader->ofs_skins) + skin*MD2MAX_SKINNAME; COM_CleanUpPath(str); if (!CL_CheckOrEnqueDownloadFile(str, str, 0)) ret = true; } return ret; } static qboolean CL_CheckHLBspWads(char *file) { lump_t lump; dheader_t *dh; char *s; char *w; char key[256]; char wads[4096]; dh = (dheader_t *)file; lump.fileofs = LittleLong(dh->lumps[LUMP_ENTITIES].fileofs); lump.filelen = LittleLong(dh->lumps[LUMP_ENTITIES].filelen); s = file + lump.fileofs; s = COM_Parse(s); if (strcmp(com_token, "{")) return false; while (*s) { s = COM_ParseOut(s, key, sizeof(key)); if (!strcmp(key, "}")) break; s = COM_ParseOut(s, wads, sizeof(wads)); if (!strcmp(key, "wad")) { s = wads; while ((s = COM_ParseToken(s, ";"))) { if (!strcmp(com_token, ";")) continue; while ((w = strchr(com_token, '\\'))) *w = '/'; w = COM_SkipPath(com_token); if (!CL_CheckFile(w)) { Con_Printf("missing wad: %s\n", w); CL_CheckOrEnqueDownloadFile(va("textures/%s", w), NULL, DLLF_REQUIRED); } } return false; } } return false; } static qboolean CL_CheckQ2BspWals(char *file) { qboolean gotone = false; #ifdef Q2BSPS q2dheader_t *dh; lump_t lump; q2texinfo_t *tinf; unsigned int i, j, count; dh = (q2dheader_t*)file; if (LittleLong(dh->version) != BSPVERSION_Q2) { //quake3? unknown? return false; } lump.fileofs = LittleLong(dh->lumps[Q2LUMP_TEXINFO].fileofs); lump.filelen = LittleLong(dh->lumps[Q2LUMP_TEXINFO].filelen); count = lump.filelen / sizeof(*tinf); if (lump.filelen != count*sizeof(*tinf)) return false; //grab the appropriate palette, just in case... but only if this won't confuse anything. if (CL_CheckDLFile("gfx/palette.lmp")) if (!CL_CheckOrEnqueDownloadFile("pics/colormap.pcx", NULL, 0)) gotone = true; tinf = (q2texinfo_t*)(file + lump.fileofs); for (i = 0; i < count; i++) { //ignore duplicate files (to save filesystem hits) for (j = 0; j < i; j++) if (!strcmp(tinf[i].texture, tinf[j].texture)) break; if (i == j) { //note: we do support formats other than .wal but we still need the .wal to figure out the correct scaling. //we make a special exception for .tga-without-.wal because other q2 engines already expect that, with pre-scaled textures (and thus lightmaps too). if (!CL_CheckDLFile(va("textures/%s.wal", tinf[i].texture))) if (!CL_CheckDLFile(va("textures/%s.tga", tinf[i].texture))) if (!CL_CheckOrEnqueDownloadFile(va("textures/%s.wal", tinf[i].texture), NULL, DLLF_ALLOWWEB)) gotone = true; } } //FIXME: parse entity lump for sky name. #endif return gotone; } static qboolean CL_CheckModelResources (char *name) { //returns true if we triggered a download qboolean ret; qbyte *file; if (!(strstr(name, ".md2") || strstr(name, ".bsp"))) return false; // checking for skins in the model FS_LoadFile(name, (void **)&file); if (!file) { return false; // couldn't load it } if (!memcmp(file, MD2IDALIASHEADER)) ret = CL_CheckMD2Skins(file); else if (!memcmp(file, BSPVERSIONHL)) ret = CL_CheckHLBspWads(file); else if (!memcmp(file, IDBSPHEADER)) ret = CL_CheckQ2BspWals(file); else ret = false; FS_FreeFile(file); return ret; } /* ================= Model_NextDownload ================= */ static void Model_CheckDownloads (void) { char *s; int i; char ext[8]; // Con_TPrintf (TLC_CHECKINGMODELS); #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) { for (i = 0; i < Q2MAX_IMAGES; i++) { char picname[256]; if (!cl.image_name[i] || !*cl.image_name[i]) continue; #if defined(HAVE_LEGACY) Q_snprintfz(picname, sizeof(picname), "pics/%s.png", cl.image_name[i]); if (COM_FCheckExists(picname)) return; #endif Q_snprintfz(picname, sizeof(picname), "pics/%s.pcx", cl.image_name[i]); if (!strncmp(cl.image_name[i], "../", 3)) //some servers are just awkward. CL_CheckOrEnqueDownloadFile(picname, picname+8, DLLF_ALLOWWEB); else CL_CheckOrEnqueDownloadFile(picname, picname, DLLF_ALLOWWEB); } if (!CLQ2_RegisterTEntModels()) return; } #endif for (i = 1; i < countof(cl.model_name) && cl.model_name[i]; i++) { s = cl.model_name[i]; if (s[0] == '*') continue; // inline brush model if (!stricmp(COM_FileExtension(s, ext, sizeof(ext)), "dsp")) //doom sprites are weird, and not really downloadable via this system continue; #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2 && s[0] == '#') //this is a vweap continue; #endif CL_CheckOrEnqueDownloadFile(s, s, ((i==1)?DLLF_REQUIRED:0)|DLLF_ALLOWWEB); //world is required to be loaded. CL_CheckModelResources(s); } #ifdef HAVE_LEGACY for (i = 0; i < MAX_VWEP_MODELS; i++) { s = cl.model_name_vwep[i]; if (!s) continue; if (!stricmp(COM_FileExtension(s, ext, sizeof(ext)), "dsp")) //doom sprites are weird, and not really downloadable via this system continue; CL_CheckOrEnqueDownloadFile(s, s, DLLF_ALLOWWEB); CL_CheckModelResources(s); } #endif } static int CL_LoadModels(int stage, qboolean dontactuallyload) { int i; float giveuptime = Sys_DoubleTime()+1; //small things get padded into a single frame #define atstage() ((cl.contentstage == stage++ && !dontactuallyload)?true:false) #define endstage() ++cl.contentstage;if (!cls.timedemo && giveuptimetype == mod_dummy) { if (!cl.model_name[1]) Host_EndGame("Worldmodel name wasn't sent\n"); // else // return stage; // Host_EndGame("Worldmodel wasn't loaded\n"); } //the worldmodel can take a while to load, so be sure to wait. if (cl.worldmodel && cl.worldmodel->loadstate == MLS_LOADING) return -1; FS_LoadMapPackFile(cl.worldmodel->name, cl.worldmodel->archive); SCR_SetLoadingFile("csprogs world"); endstage(); } for (i=1 ; iloadstate == MLS_LOADING) return -1; if (cl.worldmodel && cl.worldmodel->loadstate == MLS_LOADING) COM_WorkerPartialSync(cl.worldmodel, &cl.worldmodel->loadstate, MLS_LOADING); Mod_ParseInfoFromEntityLump(cl.worldmodel); Wad_NextDownload(); endstage(); } if (atstage()) { SCR_SetLoadingFile("external textures"); if (cl.worldmodel && cl.worldmodel->loadstate == MLS_LOADING) COM_WorkerPartialSync(cl.worldmodel, &cl.worldmodel->loadstate, MLS_LOADING); CL_CheckServerInfo(); //some serverinfo rules can change with map type, so make sure they're updated now we're sure we know it properly. if (cl.worldmodel && cl.worldmodel->loadstate == MLS_LOADED) Mod_NowLoadExternal(cl.worldmodel); /*#ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2 && cl.worldmodel && !cls.demoplayback && cl.worldmodel->checksum != cl.q2mapchecksum && cl.worldmodel->checksum2 != cl.q2mapchecksum) Host_EndGame("Local map version differs from server: %i != '%i'\n", cl.worldmodel->checksum2, cl.q2mapchecksum); #endif*/ endstage(); } // all done if (atstage()) { SCR_SetLoadingFile("newmap"); // if (!cl.worldmodel || cl.worldmodel->type == mod_dummy) // Host_EndGame("No worldmodel was loaded\n"); Surf_NewMap (cl.worldmodel); pmove.physents[0].model = cl.worldmodel; endstage(); } #ifdef CSQC_DAT if (atstage()) { SCR_SetLoadingFile("csqc init"); CSQC_WorldLoaded(); if (CSQC_Inited()) { if (cls.fteprotocolextensions & PEXT_CSQC) CL_SendClientCommand(true, "enablecsqc"); } else { if (cls.fteprotocolextensions & PEXT_CSQC) CL_SendClientCommand(true, "disablecsqc"); } endstage(); } #endif return stage; } static int CL_LoadSounds(int stage, qboolean dontactuallyload) { int i; float giveuptime = Sys_DoubleTime()+0.1; //small things get padded into a single frame //#define atstage() ((cl.contentstage == stage++)?++cl.contentstage:false) //#define endstage() if (giveuptimenext) { if (dl->flags & DLLF_NONGAME) break; } if (!dl) { for (dl = cl.downloadlist; dl; dl = dl->next) { if (dl->flags & DLLF_REQUIRED) break; } if (!dl) dl = cl.downloadlist; } /*if we don't require downloads don't queue requests until we're actually on the server, slightly more deterministic*/ if (cls.state == ca_active || (requiredownloads.value && !(cls.demoplayback && !(dl->flags&DLLF_TRYWEB))) || (dl->flags & DLLF_REQUIRED)) { if ((dl->flags & DLLF_OVERWRITE) || !CL_CheckFile (dl->localname)) { CL_SendDownloadStartRequest(dl); return; } else { //we already got this file somehow? must have come from a pak or something. don't spam. Con_DPrintf("Already have %s\n", dl->localname); CL_DisenqueDownload(dl->rname); //recurse a bit. CL_RequestNextDownload(); return; } } } else if (cls.download && requiredownloads.value) return; } if (cl.sendprespawn) { // get next signon phase extern int total_loading_size, current_loading_size; if (!cl.contentstage) { int pure; stage = 0; stage = CL_LoadModels(stage, true); stage = CL_LoadSounds(stage, true); total_loading_size = stage; cl.contentstage = 0; //might be safer to do it later, but kinder to do it before wasting time. pure = FS_PureOkay(); if (pure < 0 || (pure==0 && (cls.download || cl.downloadlist))) return; //we're downloading something and may still be able to satisfy it. if (pure == 0 && !cls.demoplayback) { //failure! Con_Printf(CON_ERROR"You are missing pure packages, and they could not be autodownloaded.\nYou may need to purchase an update.\n"); #ifdef HAVE_MEDIA_ENCODER if (cls.demoplayback && Media_Capturing()) { Con_Printf(CON_ERROR "Aborting capture\n"); CL_StopPlayback(); } #endif SCR_SetLoadingStage(LS_NONE); CL_Disconnect("Game Content differs from server"); return; } } stage = 0; stage = CL_LoadModels(stage, false); current_loading_size = cl.contentstage; if (stage < 0) return; //not yet stage = CL_LoadSounds(stage, false); current_loading_size = cl.contentstage; if (stage < 0) return; //if (cls.userinfosync.numkeys) // return; //don't prespawn until we've actually sent all our initial userinfo. if (requiredownloads.ival && COM_HasWork()) { SCR_SetLoadingFile("loading content"); return; } SCR_SetLoadingFile("receiving game state"); cl.sendprespawn = false; if (cl_splitscreen.ival) { if (cls.fteprotocolextensions & PEXT_SPLITSCREEN) ; else if (cls.protocol == CP_QUAKE2 && cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) ; else Con_TPrintf(CON_WARNING "Splitscreen requested but not available on this server.\n"); } if (cl.worldmodel && cl.worldmodel->loadstate == MLS_LOADING) COM_WorkerPartialSync(cl.worldmodel, &cl.worldmodel->loadstate, MLS_LOADING); if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED) { downloadlist_t *dl = NULL; const char *worldname = cl.worldmodel?cl.worldmodel->name:"unknown"; if (cl.worldmodel) for (dl = cl.faileddownloads; dl; dl = dl->next) //yeah, so it failed... Ignore it. if (!strcmp(dl->rname, cl.worldmodel->name)) break; Con_Printf("\n\n-------------\n"); switch (dl?dl->failreason:DLFAIL_UNTRIED) { case DLFAIL_UNSUPPORTED: Con_Printf(CON_ERROR "Download of \"%s\" not supported on this server - cannot fully connect\n", worldname); break; case DLFAIL_CORRUPTED: Con_Printf(CON_ERROR "Download of \"%s\" corrupt/failed - cannot fully connect\n", worldname); break; case DLFAIL_CLIENTCVAR: Con_Printf(CON_ERROR "Downloading of \"%s\" blocked by clientside cvars - tweak cl_download* before retrying\n", worldname); break; case DLFAIL_CLIENTFILE: Con_Printf(CON_ERROR "Disk error downloading \"%s\" - cannot fully connect\n", worldname); break; case DLFAIL_SERVERCVAR: Con_Printf(CON_ERROR "Download of \"%s\" denied by server - cannot fully connect\n", worldname); break; case DLFAIL_SERVERFILE: Con_Printf(CON_ERROR "Download of \"%s\" unavailable - cannot fully connect\n", worldname); break; case DLFAIL_REDIRECTED: Con_Printf(CON_ERROR "Redirection failure downloading \"%s\" - cannot fully connect\n", worldname); break; case DLFAIL_UNTRIED: if (COM_FCheckExists(worldname)) { if (!cl.worldmodel) Con_Printf(CON_ERROR "Couldn't load \"%s\" - worldmodel not set - cannot fully connect\n", worldname); else if (cl.worldmodel->loadstate == MLS_FAILED) Con_Printf(CON_ERROR "Couldn't load \"%s\" - corrupt? - cannot fully connect\n", worldname); else if (cl.worldmodel->loadstate == MLS_LOADING) Con_Printf(CON_ERROR "Couldn't load \"%s\" - still loading - cannot fully connect\n", worldname); else if (cl.worldmodel->loadstate == MLS_NOTLOADED) Con_Printf(CON_ERROR "Couldn't load \"%s\" - worldmodel not loaded - cannot fully connect\n", worldname); else Con_Printf(CON_ERROR "Couldn't load \"%s\" - corrupt? - cannot fully connect\n", worldname); } else Con_Printf(CON_ERROR "Couldn't find \"%s\" - cannot fully connect\n", worldname); break; } #ifdef HAVE_MEDIA_ENCODER if (cls.demoplayback && Media_Capturing()) { Con_Printf(CON_ERROR "Aborting capture\n"); CL_StopPlayback(); } #endif //else should probably force the demo speed really fast or something SCR_SetLoadingStage(LS_NONE); return; } Cvar_ForceCallback(Cvar_FindVar("r_particlesdesc")); #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) { if (cls.protocol_q2 == PROTOCOL_VERSION_R1Q2) { //fixme: make dynamic... // MSG_WriteByte (&cls.netchan.message, clcr1q2_setting); // MSG_WriteShort (&cls.netchan.message, R1Q2_CLSET_NOGUN); // MSG_WriteShort (&cls.netchan.message, r_drawviewmodel.value <= 0); // MSG_WriteByte (&cls.netchan.message, clcr1q2_setting); // MSG_WriteShort (&cls.netchan.message, R1Q2_CLSET_PLAYERUPDATES); // MSG_WriteShort (&cls.netchan.message, 1); // MSG_WriteByte (&cls.netchan.message, clcr1q2_setting); // MSG_WriteShort (&cls.netchan.message, R1Q2_CLSET_FPS); // MSG_WriteShort (&cls.netchan.message, 30); } Skin_NextDownload(); SCR_SetLoadingStage(LS_NONE); CL_SendClientCommand(true, "begin %i\n", cl.servercount); } else #endif { if (cls.demoplayback == DPB_MVD && cls.demoeztv_ext) { if (CL_RemoveClientCommands("qtvspawn")) Con_DPrintf("Multiple prespawns\n"); CL_SendClientCommand(true, "qtvspawn %i 0 %i", cl.servercount, COM_RemapMapChecksum(cl.worldmodel, LittleLong(cl.worldmodel->checksum2))); SCR_SetLoadingStage(LS_NONE); } else { // done with modellist, request first of static signon messages if (CL_RemoveClientCommands("prespawn")) Con_DPrintf("Multiple prespawns\n"); if (cls.protocol == CP_NETQUAKE) CL_SendClientCommand(true, "prespawn"); else { // CL_SendClientCommand("prespawn %i 0 %i", cl.servercount, cl.worldmodel->checksum2); CL_SendClientCommand(true, prespawn_name, cl.servercount, COM_RemapMapChecksum(cl.worldmodel, LittleLong(cl.worldmodel->checksum2))); } } } if (mod_precache.ival >= 2) { int i; for (i=1 ; iloadstate == MLS_NOTLOADED) Mod_LoadModel(cl.model_precache[i], MLV_WARN); } } if (snd_precache.ival >= 2) { int i; for (i=1 ; iloadstate == SLS_NOTLOADED) S_LoadSound(cl.sound_precache[i], false); } } } } int CL_RequestADownloadChunk(void); void CL_SendDownloadReq(sizebuf_t *msg) { if (cls.demoplayback == DPB_MVD) return; //tcp connection, so no need to constantly ask if (!cls.download) { if (cl.downloadlist) CL_RequestNextDownload(); return; } #ifdef PEXT_CHUNKEDDOWNLOADS if (cls.download->method == DL_QWCHUNKS) DLC_Poll(cls.download); #endif } #ifdef PEXT_ZLIBDL #include static char *ZLibDownloadDecode(int *messagesize, char *input, int finalsize) { char *outbuf = Hunk_TempAlloc(finalsize); z_stream zs; *messagesize = (*(short*)input); input+=2; if (!*messagesize) { *messagesize = finalsize+2; return input; } memset(&zs, 0, sizeof(zs)); zs.next_in = input; zs.avail_in = *messagesize; //tell it that it has a lot. Possibly a bad idea. zs.total_in = 0; zs.next_out = outbuf; zs.avail_out = finalsize; //this is the limiter. zs.total_out = 0; zs.data_type = Z_BINARY; inflateInit(&zs); inflate(&zs, Z_FINISH); //decompress it in one go. inflateEnd(&zs); *messagesize = zs.total_in+2; return outbuf; } #endif downloadlist_t *CL_DownloadFailed(const char *name, qdownload_t *qdl, enum dlfailreason_e failreason) { //add this to our failed list. (so we don't try downloading it again...) downloadlist_t *failed, **link, *dl; failed = Z_Malloc(sizeof(downloadlist_t)); failed->next = cl.faileddownloads; cl.faileddownloads = failed; Q_strncpyz(failed->rname, name, sizeof(failed->rname)); failed->failreason = failreason; //if this is what we're currently downloading, close it up now. //don't do this if we're just marking the file as unavailable for download. if (qdl && (!stricmp(qdl->remotename, name) || !*name)) { DL_Abort(qdl, QDL_FAILED); } link = &cl.downloadlist; while(*link) { dl = *link; if (!strcmp(dl->rname, name)) { *link = dl->next; failed->flags |= dl->flags; Z_Free(dl); } else link = &(*link)->next; } return failed; } #ifdef PEXT_CHUNKEDDOWNLOADS int CL_DownloadRate(void) { qdownload_t *dl = cls.download; if (dl) { double curtime = Sys_DoubleTime(); if (!dl->ratetime) { dl->ratetime = curtime; return dl->completedbytes/(Sys_DoubleTime() - dl->starttime); } if (curtime - dl->ratetime > 1) { dl->rate = dl->ratebytes / (curtime - dl->ratetime); dl->ratetime = curtime; dl->ratebytes = 0; } return dl->rate; } return 0; } //called when the server acks the download. opens the local file and stuff. returns false on failure qboolean DL_Begun(qdownload_t *dl) { //figure out where the file is meant to be going. dl->prefixbytes = 0; if (!strncmp(dl->tempname, "package/", 8)) { dl->prefixbytes = 8; //ignore the package/ part dl->fsroot = FS_ROOT; //and put it in the root dir (-basedir), and hope the name includes a gamedir part } else if (!strncmp(dl->tempname,"skins/",6)) dl->fsroot = FS_PUBBASEGAMEONLY; //shared between gamedirs, so only use the basegame. else dl->fsroot = FS_PUBGAMEONLY;//FS_GAMEONLY; //other files are relative to the active gamedir. Q_snprintfz(dl->dclname, sizeof(dl->dclname), "%s.dcl", dl->tempname); if (dl->method == DL_QWCHUNKS) { qboolean error = false; char partline[256]; char partterm[128]; char *p, t; qofs_t lastend = 0; qofs_t start, end; struct dlblock_s **link = &dl->dlblocks; vfsfile_t *parts = FS_OpenVFS(dl->dclname+dl->prefixbytes, "rb", dl->fsroot); if (!parts) error = true; while(!error && VFS_GETS(parts, partline, sizeof(partline))) { p = COM_ParseOut(partline, partterm, sizeof(partterm)); t = *partterm; p = COM_ParseOut(p, partterm, sizeof(partterm)); start = strtoull(partterm, NULL, 0); p = COM_ParseOut(p, partterm, sizeof(partterm)); end = strtoull(partterm, NULL, 0); (*link) = Z_Malloc(sizeof(**link)); (*link)->start = start; (*link)->end = end; (*link)->state = (t == 'c')?DLB_RECEIVED:DLB_MISSING; link = &(*link)->next; if (t == 'c') dl->completedbytes += end - start; if (start != lastend) error = true; lastend = end; } if (lastend != dl->size) error = true; if (parts) VFS_CLOSE(parts); if (!error) dl->file = FS_OpenVFS(dl->tempname+dl->prefixbytes, "w+b", dl->fsroot); } if (!dl->file) { struct dlblock_s *b; //make sure we don't get confused if someone end-tasks us before the download is complete. FS_Remove(dl->dclname+dl->prefixbytes, dl->fsroot); dl->completedbytes = 0; while (dl->dlblocks) { b = dl->dlblocks; dl->dlblocks = b->next; Z_Free(b); } FS_CreatePath(dl->tempname+dl->prefixbytes, dl->fsroot); dl->file = FS_OpenVFS(dl->tempname+dl->prefixbytes, "wb", dl->fsroot); } if (!dl->file) { char displaypath[MAX_OSPATH]; FS_DisplayPath(dl->tempname+dl->prefixbytes, dl->fsroot, displaypath, sizeof(displaypath)); Con_TPrintf("Unable to open \"%s\"\n", displaypath); return false; } if (dl->method == DL_QWPENDING) Con_TPrintf("method is still 'pending'\n"); if (dl->method == DL_QWCHUNKS && !dl->dlblocks) { dl->dlblocks = Z_Malloc(sizeof(*dl->dlblocks)); dl->dlblocks->start = 0; dl->dlblocks->end = dl->size; dl->dlblocks->state = DLB_MISSING; } dl->flags |= DLLF_BEGUN; dl->starttime = Sys_DoubleTime(); return true; } static void DL_Completed(qdownload_t *dl, qofs_t start, qofs_t end) { struct dlblock_s *prev = NULL, *b, *n, *e; if (end <= start) return; //ignore invalid ranges. for (b = dl->dlblocks; b; ) { if (b->state == DLB_RECEIVED) { //nothing to be done. dupe. somehow. or simply a different range. } else if (b->start >= start && b->end <= end) { //whole block // Con_Printf("Whole block\n"); b->state = DLB_RECEIVED; dl->completedbytes += b->end - b->start; dl->ratebytes += b->end - b->start; } else if (start > b->start && end < b->end) { // Con_Printf("chop out middle\n"); //in the middle, no need to merge n = Z_Malloc(sizeof(*n)); e = Z_Malloc(sizeof(*e)); e->next = b->next; n->next = e; b->next = n; e->state = b->state; e->sequence = b->sequence; n->state = DLB_RECEIVED; e->end = b->end; b->end = start; n->start = start; n->end = end; e->start = end; dl->completedbytes += n->end - n->start; dl->ratebytes += n->end - n->start; } //data overlaps the start (data end must be smaller than block end) else if (start <= b->start && end > b->start) { // Con_Printf("complete start\n"); //split it. new(non-complete) block is second. n = Z_Malloc(sizeof(*n)); n->next = b->next; b->next = n; //second block keeps original block's state. first block gets completed n->state = b->state; n->sequence = b->sequence; b->state = DLB_RECEIVED; n->start = end; n->end = b->end; b->end = end; dl->completedbytes += b->end - b->start; dl->ratebytes += b->end - b->start; } //new data overlaps the end else if (start > b->start && start < b->end) { // Con_Printf("complete end\n"); //split it. new(completed) block is second. n = Z_Malloc(sizeof(*n)); n->next = b->next; b->next = n; //second block keeps original block's state. first block gets completed n->state = DLB_RECEIVED; n->sequence = 0; n->start = end; n->end = b->end; b->end = end; dl->completedbytes += n->end - n->start; dl->ratebytes += n->end - n->start; prev = b; b = n; } else {//don't bother merging, as nothing changed prev = b; b = b->next; continue; } //merge with next block if (b->next && b->next->state == DLB_RECEIVED) { n = b->next; b->next = n->next; b->end = n->end; Z_Free(n); } //merge with previous block if possible if (prev && prev->state == DLB_RECEIVED) { n = b; prev->end = b->end; prev->next = b->next; Z_Free(b); b = prev->next; continue;//careful here } prev = b; b = b->next; } } static float chunkrate; static int CL_CountQueuedDownloads(void); static void CL_ParseChunkedDownload(qdownload_t *dl) { qbyte *svname; int flag; qofs_t filesize; qofs_t chunknum; char data[DLBLOCKSIZE]; chunknum = MSG_ReadLong(); if (chunknum == -1) { flag = MSG_ReadLong(); if (flag == 0x80000000) { //really big files need special handling here. flag = MSG_ReadLong(); filesize = qofs_Make(flag, MSG_ReadLong()); flag = 0; } else filesize = flag; svname = MSG_ReadString(); if (cls.demoplayback) { //downloading in demos is allowed ONLY for csprogs.dat extern cvar_t cl_downloads, cl_download_csprogs; if (!cls.download && !dl && !strcmp(svname, "csprogs.dat") && filesize && filesize == strtoul(InfoBuf_ValueForKey(&cl.serverinfo, "*csprogssize"), NULL, 0) && cl_downloads.ival && cl_download_csprogs.ival) { //FIXME: should probably save this to memory instead of bloating it on disk. dl = Z_Malloc(sizeof(*dl)); Q_strncpyz(dl->remotename, svname, sizeof(dl->remotename)); Q_strncpyz(dl->localname, va("csprogsvers/%x.dat", (unsigned int)strtoul(InfoBuf_ValueForKey(&cl.serverinfo, "*csprogs"), NULL, 0)), sizeof(dl->localname)); // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left COM_StripExtension (dl->localname, dl->tempname, sizeof(dl->tempname)-5); Q_strncatz (dl->tempname, ".tmp", sizeof(dl->tempname)); dl->method = DL_QWPENDING; dl->percent = 0; dl->sizeunknown = true; dl->flags = DLLF_OVERWRITE; if (COM_FCheckExists(dl->localname)) { Con_DPrintf("Demo embeds redundant %s\n", dl->localname); Z_Free(dl); return; } cls.download = dl; Con_Printf("Saving recorded file %s (%lu bytes)\n", dl->localname, (unsigned long)filesize); } else return; } if (!*svname) { //stupid mvdsv. /*if (totalsize < 0) svname = cls.downloadname; else*/ { Con_Printf("ignoring nameless download\n"); return; } } if (flag < 0) { enum dlfailreason_e failreason; if (flag == DLERR_REDIRECTFILE) { failreason = DLFAIL_REDIRECTED; if (CL_AllowArbitaryDownload(dl->remotename, svname)) { Con_Printf("Download of \"%s\" redirected to \"%s\"\n", dl->remotename, svname); if (!strncmp(svname, "package/", 8)) { int i, c; char *pn; Cmd_TokenizeString(cl.serverpacknames, false, false); c = Cmd_Argc(); for (i = 0; i < c; i++) { pn = Cmd_Argv(i); if (*pn == '*') pn++; //'required'... so shouldn't really be missing. if (!strcmp(pn, svname+8)) break; } if (i == c) Con_Printf("However, package \"%s\" is unknown.\n", svname+8); else { char localname[MAX_OSPATH]; char *hash; Cmd_TokenizeString(cl.serverpackhashes, false, false); hash = Cmd_Argv(i); if (FS_GenCachedPakName(svname+8, hash, localname, sizeof(localname))) { if (CL_CheckOrEnqueDownloadFile(svname+8, localname, DLLF_NONGAME)) if (!CL_CheckDLFile(dl->localname)) Con_Printf("However, \"%s\" already exists. You may need to delete it.\n", svname); } else Con_Printf("However, package \"%s\" is invalid.\n", svname+8); } } else if (CL_CheckOrEnqueDownloadFile(svname, NULL, 0)) { Con_Printf("However, \"%s\" already exists. You may need to delete it.\n", svname); failreason = DLFAIL_CLIENTFILE; } } svname = dl->remotename; } else if (flag == DLERR_UNKNOWN) { Con_Printf("Server reported an error when downloading file \"%s\"\n", svname); failreason = DLFAIL_CORRUPTED; } else if (flag == DLERR_PERMISSIONS) { Con_Printf("Server permissions deny downloading file \"%s\"\n", svname); failreason = DLFAIL_SERVERCVAR; } else //if (flag == DLERR_FILENOTFOUND) { Con_Printf("Couldn't find file \"%s\" on the server\n", svname); failreason = DLFAIL_SERVERFILE; } if (dl) { CL_DownloadFailed(svname, dl, failreason); CL_RequestNextDownload(); } return; } if (!dl) { Con_Printf("ignoring download start. we're not meant to be downloading\n"); return; } if (dl->method == DL_QWCHUNKS) Host_EndGame("Received second download - \"%s\"\n", svname); if (stricmp(dl->remotename, svname)) { //fixme: we should allow extension changes, in the case of ogg/mp3/wav, or tga/png/jpg/pcx, or the addition of .gz or whatever Host_EndGame("Server sent the wrong download - \"%s\" instead of \"%s\"\n", svname, dl->remotename); } //start the new download dl->method = DL_QWCHUNKS; dl->percent = 0; dl->size = filesize; dl->sizeunknown = false; dl->starttime = Sys_DoubleTime(); /* strcpy(cls.downloadname, svname); COM_StripExtension(svname, cls.downloadtempname); COM_DefaultExtension(cls.downloadtempname, ".tmp"); */ if (!DL_Begun(dl)) { CL_DownloadFailed(svname, dl, DLFAIL_CLIENTFILE); return; } return; } // Con_Printf("Received dl block %i: ", chunknum); MSG_ReadData(data, DLBLOCKSIZE); if (!dl) { if (!cls.demoplayback) //mute it in demos. Con_Printf("ignoring download data packet\n"); return; } if (chunknum*DLBLOCKSIZE > dl->size+DLBLOCKSIZE) return; if (!dl->file) return; VFS_SEEK(dl->file, chunknum*DLBLOCKSIZE); if (dl->size - chunknum*DLBLOCKSIZE < DLBLOCKSIZE) //final block is actually meant to be smaller than we recieve. VFS_WRITE(dl->file, data, dl->size - chunknum*DLBLOCKSIZE); else VFS_WRITE(dl->file, data, DLBLOCKSIZE); DL_Completed(dl, chunknum*DLBLOCKSIZE, (chunknum+1)*DLBLOCKSIZE); dl->percent = dl->completedbytes/(float)dl->size*100; chunkrate += 1; if (dl->completedbytes == dl->size) CL_DownloadFinished(dl); } static int CL_CountQueuedDownloads(void) { int count = 0; downloadlist_t *dl; for (dl = cl.downloadlist; dl; dl = dl->next) count++; return count; } static void DLC_RequestDownloadChunks(qdownload_t *dl, float frametime) { char *cmd; qofs_t chunk; struct dlblock_s *b, *n; qboolean stillpending = false; qboolean haveloss = false; int chunks, chunksremaining; static float slop; //try to keep things as integers // int cmds = 20; if (frametime < 0) frametime = 0; if (frametime > 0.1) frametime = 0.1; //erg? if (chunkrate < 0) chunkrate = 0; slop += chunkrate*frametime; chunksremaining = slop; slop -= chunksremaining; if (chunksremaining < 1) { if (chunkrate < 30) chunksremaining = 1; else return; /* if (!chunkrate) chunkrate = 72; else chunkrate+=frametime; return; */ } if (chunksremaining > 100) { //we're going to need some sanity limit, for cpu resources. chunkrate -= (chunksremaining-100); chunksremaining = 100; } //Con_DPrintf("%i\n", chunksremaining); for (b = dl->dlblocks; b; b = b->next) { //packetloss reverts blocks to missing. if (b->state == DLB_PENDING) { if (b->sequence < cls.netchan.incoming_sequence-10) { haveloss = true; b->state = DLB_MISSING; for (;;) //merge it with the next if they're all invalid { n = b->next; if (!n) break; if (n->state == DLB_MISSING || (n->state == DLB_PENDING && n->sequence < cls.netchan.incoming_sequence-10)) { b->next = n->next; b->end = n->end; Z_Free(n); continue; } break; } } else stillpending = true; } if (b->state == DLB_MISSING && chunksremaining) { chunk = b->start / DLBLOCKSIZE; chunks = 1;//((b->end+DLBLOCKSIZE-1)/DLBLOCKSIZE) - (b->start / DLBLOCKSIZE); if (chunks > chunksremaining) chunks = chunksremaining; //if this block is bigger than a chunk, split the two blocks. if (b->end - b->start > DLBLOCKSIZE*chunks) { n = Z_Malloc(sizeof(*n)); n->next = b->next; n->start = (chunk+chunks)*DLBLOCKSIZE; n->end = b->end; b->end = n->start; n->state = DLB_MISSING; b->next = n; } b->state = DLB_PENDING; b->sequence = cls.netchan.outgoing_sequence; stillpending = true; if (chunks > 1) cmd = va("nextdl %u %3g %i %i\n", (unsigned int)chunk, dl->percent, dl->filesequence, chunks); else cmd = va("nextdl %u %3g %i\n", (unsigned int)chunk, dl->percent, dl->filesequence); CL_RemoveClientCommands(cmd); CL_SendClientCommand(false, "%s", cmd); chunksremaining -= chunks; if (chunksremaining <= 0) break; /*if (--cmds <= 0) { chunkrate -= chunksremaining; // haveloss = true; break; }*/ } } if (haveloss) { chunkrate *= 0.98; } if (!stillpending) { //when there's nothing still pending, the download is complete. Con_DPrintf("Download took %i seconds (%i more)\n", (int)(Sys_DoubleTime() - dl->starttime), CL_CountQueuedDownloads()); CL_DownloadFinished(dl); } } static void DLC_Poll(qdownload_t *dl) { static float lasttime; DLC_RequestDownloadChunks(dl, realtime - lasttime); lasttime = realtime; } #endif void DL_Abort(qdownload_t *dl, enum qdlabort aborttype) { struct dlblock_s *b, *n; if (dl->file) { VFS_CLOSE(dl->file); dl->file = NULL; } if (dl->flags & DLLF_BEGUN) { dl->flags &= ~DLLF_BEGUN; if (aborttype == QDL_COMPLETED) { //this file isn't needed now the download has finished. FS_Remove(dl->dclname+dl->prefixbytes, dl->fsroot); if (dl->flags & DLLF_TEMPORARY) { #ifdef TERRAIN if (!Terr_DownloadedSection(dl->tempname+dl->prefixbytes)) #endif Con_Printf("Downloaded unusable temporary file\n"); FS_Remove(dl->tempname+dl->prefixbytes, dl->fsroot); } else if (Q_strcasecmp(dl->tempname, dl->localname)) { if (dl->flags & DLLF_OVERWRITE) FS_Remove(dl->localname+dl->prefixbytes, dl->fsroot); if (!FS_Rename(dl->tempname+dl->prefixbytes, dl->localname+dl->prefixbytes, dl->fsroot)) { char displaytmp[MAX_OSPATH], displayfinal[MAX_OSPATH]; FS_DisplayPath(dl->tempname+dl->prefixbytes, dl->fsroot, displaytmp, sizeof(displaytmp)); FS_DisplayPath(dl->localname+dl->prefixbytes, dl->fsroot, displayfinal, sizeof(displayfinal)); Con_Printf("Couldn't rename %s to %s\n", displaytmp, displayfinal); } } #ifdef PACKAGEMANAGER if (!strncmp(dl->localname, "package/", 8) && dl->fsroot == FS_ROOT) PM_FileInstalled(dl->localname+8, dl->fsroot, NULL, false); #endif } else { //file was aborted half way through... if (dl->dlblocks) { //save the list of valid chunks so we don't have to redownload those. vfsfile_t *parts; parts = FS_OpenVFS(dl->dclname+dl->prefixbytes, "wb", dl->fsroot); if (parts) { for (b = dl->dlblocks; b; b = n) { if (b->state == DLB_RECEIVED) VFS_PRINTF(parts, "c %"PRIx64" %"PRIx64"\n", (quint64_t)b->start, (quint64_t)b->end); else { for(;;) { n = b->next; if (n && n->state != DLB_RECEIVED) { b->end = n->end; b->next = n->next; Z_Free(n); continue; } break; } VFS_PRINTF(parts, "m %"PRIx64" %"PRIx64"\n", (quint64_t)b->start, (quint64_t)b->end); } n = b->next; Z_Free(b); } dl->dlblocks = NULL; VFS_CLOSE(parts); } else FS_Remove(dl->tempname + dl->prefixbytes, dl->fsroot); } else { //download looks like it was non-resumable. just delete it. FS_Remove(dl->tempname + dl->prefixbytes, dl->fsroot); } } if (aborttype != QDL_DISCONNECT) { switch(dl->method) { default: break; #ifdef Q3CLIENT case DL_Q3: q3->cl.SendClientCommand("stopdl"); break; #endif case DL_QW: break; case DL_DARKPLACES: CL_SendClientCommand(true, "stopdownload"); break; case DL_QWCHUNKS: { //char *serverversion = InfoBuf_ValueForKey(&cl.serverinfo, "*version"); //if (!strncmp(serverversion , "MVDSV ", 6)) //mvdsv will spam if we use stopdownload. and it'll misreport packetloss if we send nothing. grr. CL_SendClientCommand(true, "nextdl -1 100 %i", dl->filesequence); //else // CL_SendClientCommand(true, "stopdownload"); } break; } } } for (b = dl->dlblocks; b; b = n) { n = b->next; Z_Free(b); } dl->dlblocks = NULL; if (dl->method != DL_HTTP) Z_Free(dl); if (cls.download == dl) cls.download = NULL; } /* ===================== CL_ParseDownload A download message has been received from the server ===================== */ static void CL_ParseDownload (qboolean zlib) { extern cvar_t cl_dlemptyterminate; int size, percent; qbyte name[1024]; qdownload_t *dl = cls.download; #ifdef PEXT_CHUNKEDDOWNLOADS if (cls.fteprotocolextensions & PEXT_CHUNKEDDOWNLOADS) { if (cls.demoplayback == DPB_MVD && cls.demoeztv_ext) Host_EndGame("CL_ParseDownload: chunked download on qtv proxy."); CL_ParseChunkedDownload(dl); return; } #endif // read the data size = MSG_ReadShort (); percent = MSG_ReadByte (); if (size == -2) { /*quakeforge*/ MSG_ReadString(); return; } if (size == -3) { char *requestedname; Q_strncpyz(name, MSG_ReadString(), sizeof(name)); requestedname = MSG_ReadString(); Con_DPrintf("Download for %s redirected to %s\n", requestedname, name); /*quakeforge http download redirection*/ if (dl) CL_DownloadFailed(dl->remotename, dl, DLFAIL_REDIRECTED); //FIXME: find some safe way to do this and actually test it. we should already know the local name, but we might have gained a .gz or something (this is quakeforge after all). // CL_CheckOrEnqueDownloadFile(name, localname, DLLF_IGNOREFAILED); return; } if (cls.demoplayback && !cls.demoeztv_ext) { if (size > 0) MSG_ReadSkip(size); return; // not in demo playback, we don't know the name of the file. } if (!dl) { //download packet without file requested. if (size > 0) MSG_ReadSkip(size); return; // not in demo playback } if (size < 0) { Con_TPrintf ("File not found.\n"); if (dl) CL_DownloadFailed(dl->remotename, dl, DLFAIL_SERVERFILE); return; } // open the file if not opened yet if (dl->method == DL_QWPENDING) { dl->method = DL_QW; if (!DL_Begun(dl)) { MSG_ReadSkip(size); Con_TPrintf ("Failed to open %s\n", dl->tempname); CL_DownloadFailed(dl->remotename, dl, DLFAIL_CLIENTFILE); CL_RequestNextDownload (); return; } SCR_EndLoadingPlaque(); } if (zlib) { #if defined(AVAIL_ZLIB) && defined(Q2CLIENT) z_stream s; unsigned short clen = size; unsigned short ulen = MSG_ReadShort(); char cdata[8192]; unsigned int done = 0; memset(&s, 0, sizeof(s)); s.next_in = net_message.data + MSG_GetReadCount(); s.avail_in = clen; if (inflateInit2(&s, -15) != Z_OK) Host_EndGame ("CL_ParseZDownload: unable to initialise zlib"); for(;;) { int zerr; s.next_out = cdata; s.avail_out = sizeof(cdata); zerr = inflate(&s, Z_FULL_FLUSH); VFS_WRITE (dl->file, cdata, s.total_out - done); done = s.total_out; if (zerr == Z_STREAM_END) break; else if (zerr == Z_OK) continue; else Host_EndGame ("CL_ParseZDownload: stream truncated"); } if (inflateEnd(&s) != Z_OK) Host_EndGame ("CL_ParseZDownload: stream truncated"); VFS_WRITE (dl->file, cdata, s.total_out - done); done = s.total_out; if (s.total_out != ulen || s.total_in != clen) Host_EndGame ("CL_ParseZDownload: stream truncated"); #else Host_EndGame("Unable to handle zlib downloads, zlib is not supported in this build"); #endif MSG_ReadSkip(size); } else #ifdef PEXT_ZLIBDL if (percent >= 101 && percent <= 201)// && cls.fteprotocolextensions & PEXT_ZLIBDL) { int compsize; percent = percent - 101; VFS_WRITE (cls.download, ZLibDownloadDecode(&compsize, net_message.data + MSG_GetReadCount(), size), size); MSG_ReadSkip(compsize); } else #endif { VFS_WRITE (dl->file, net_message.data + MSG_GetReadCount(), size); MSG_ReadSkip(size); } dl->completedbytes += size; dl->ratebytes += size; if (dl->percent != percent) //try and guess the size (its most acurate when the percent value changes) dl->size = ((float)dl->completedbytes*100)/percent; if (percent != 100 && size == 0 && cl_dlemptyterminate.ival) { Con_Printf(CON_WARNING "WARNING: Client received empty svc_download, assuming EOF.\n"); percent = 100; } if (percent != 100) { // request next block dl->percent = percent; CL_SendClientCommand(true, "nextdl"); } else { Con_DPrintf("Download took %i seconds\n", (int)(Sys_DoubleTime() - dl->starttime)); CL_DownloadFinished(dl); // get another file if needed CL_RequestNextDownload (); } } qboolean CL_ParseOOBDownload(void) { qdownload_t *dl = cls.download; if (!dl) return false; if (MSG_ReadLong() != dl->filesequence) return false; if (MSG_ReadChar() != svc_download) return false; CL_ParseDownload(false); return true; } #ifdef NQPROT static void CLDP_ParseDownloadData(void) { qdownload_t *dl = cls.download; unsigned char buffer[1<<16]; qofs_t start; int size; start = MSG_ReadLong(); size = (unsigned short)MSG_ReadShort(); MSG_ReadData(buffer, size); if (!dl) return; if (dl->file) { if (start > dl->completedbytes) { //this protocol cannot deal with gaps. we might as well wait until its repeated later. //don't ack values ahead of what we completed, we won't get good results if we do that. servers are dumb. start = dl->completedbytes; size = 0; } else if (start+size < dl->completedbytes) ; //already completed this data else { int offset = dl->completedbytes-start; //we may already have completed some chunk already VFS_WRITE(dl->file, buffer+offset, size-offset); dl->completedbytes += size-offset; dl->ratebytes += size-offset; //for download rate calcs } dl->percent = (dl->completedbytes) / (float)dl->size * 100; } //we need to ack in order. //the server doesn't actually track packets, only position, however there's no way to tell it that we already have a chunk //we could send the acks unreliably, but any cl->sv loss would involve a sv->cl resend (because we can't dupe). MSG_WriteByte(&cls.netchan.message, clcdp_ackdownloaddata); MSG_WriteLong(&cls.netchan.message, start); MSG_WriteShort(&cls.netchan.message, size); } static void CLDP_ParseDownloadBegin(char *s) { qdownload_t *dl = cls.download; char buffer[8192]; qofs_t size, pos, chunk; char *fname; Cmd_TokenizeString(s, false, false); size = (qofs_t)strtoull(Cmd_Argv(1), NULL, 0); fname = Cmd_Argv(2); if (!dl || strcmp(fname, dl->remotename)) { #ifdef CSQC_DAT if (cls.demoplayback && !dl && cl_dp_csqc_progssize && size == cl_dp_csqc_progssize && !strcmp(fname, cl_dp_csqc_progsname)) { //its somewhat common for demos to contain a copy of the csprogs, so that the same version is available when trying to play the demo back. extern cvar_t cl_download_csprogs, cl_nocsqc; if (!cl_nocsqc.ival && cl_download_csprogs.ival) { fname = va("csprogsvers/%x.dat", cl_dp_csqc_progscrc); if (CL_CheckDLFile(fname)) return; //we already have this version //Begin downloading it... } else return; //silently ignore it } else #endif { Con_Printf("Warning: server started sending a file we did not request. Ignoring.\n"); return; } } if (!dl) { dl = Z_Malloc(sizeof(*dl)); dl->filesequence = 0; Q_strncpyz(dl->remotename, fname, sizeof(dl->remotename)); Q_strncpyz(dl->localname, fname, sizeof(dl->localname)); Con_TPrintf ("Downloading %s...\n", dl->localname); // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left COM_StripExtension (dl->localname, dl->tempname, sizeof(dl->tempname)-5); Q_strncatz (dl->tempname, ".tmp", sizeof(dl->tempname)); dl->method = DL_DARKPLACES; dl->percent = 0; dl->sizeunknown = true; dl->flags = DLLF_REQUIRED; cls.download = dl; } if (dl->method == DL_QWPENDING) dl->method = DL_DARKPLACES; if (dl->method != DL_DARKPLACES) { Con_Printf("Warning: download method isn't right.\n"); return; } dl->sizeunknown = false; dl->size = size; if (!DL_Begun(dl)) { CL_DownloadFailed(dl->remotename, dl, DLFAIL_CLIENTFILE); return; } CL_SendClientCommand(true, "sv_startdownload"); //fill the file with 0 bytes, for some reason memset(buffer, 0, sizeof(buffer)); for (pos = 0, chunk = 1; chunk; pos += chunk) { chunk = size - pos; if (chunk > sizeof(buffer)) chunk = sizeof(buffer); VFS_WRITE(dl->file, buffer, chunk); } VFS_SEEK(dl->file, 0); } static void CLDP_ParseDownloadFinished(char *s) { qdownload_t *dl = cls.download; unsigned int runningcrc = 0; const hashfunc_t *hfunc = &hash_crc16; char buffer[8192]; qofs_t size, pos, chunk; if (!dl || !dl->file) return; Cmd_TokenizeString(s, false, false); VFS_CLOSE (dl->file); dl->file = FS_OpenVFS (dl->tempname+dl->prefixbytes, "rb", dl->fsroot); if (dl->file) { void *hashctx = alloca(hfunc->contextsize); size = dl->size; hfunc->init(hashctx); for (pos = 0; pos < size; pos += chunk) { chunk = min(sizeof(buffer), size - pos); if (chunk != VFS_READ(dl->file, buffer, chunk)) break; hfunc->process(hashctx, buffer, chunk); } VFS_CLOSE (dl->file); dl->file = NULL; runningcrc = hashfunc_terminate_uint(hfunc, hashctx); } else { Con_Printf("Download failed: unable to check CRC of download\n"); CL_DownloadFailed(dl->remotename, dl, DLFAIL_CLIENTFILE); return; } if (size != atoi(Cmd_Argv(1))) { Con_Printf("Download failed: wrong file size\n"); CL_DownloadFailed(dl->remotename, dl, DLFAIL_CORRUPTED); return; } if (runningcrc != atoi(Cmd_Argv(2))) { Con_Printf("Download failed: wrong crc\n"); CL_DownloadFailed(dl->remotename, dl, DLFAIL_CORRUPTED); return; } Con_DPrintf("Download took %i seconds\n", (int)(Sys_DoubleTime() - dl->starttime)); CL_DownloadFinished(dl); // get another file if needed CL_RequestNextDownload (); } #endif static vfsfile_t *upload_file; static qbyte *upload_data; static int upload_pos; static int upload_size; void CL_NextUpload(void) { qbyte buffer[1024]; int r; int percent; int size; r = upload_size - upload_pos; if (r > 768) r = 768; if (upload_data) { memcpy(buffer, upload_data + upload_pos, r); } else if (upload_file) { r = VFS_READ(upload_file, buffer, r); if (r == 0) { CL_StopUpload(); return; } } else return; MSG_WriteByte (&cls.netchan.message, clc_upload); MSG_WriteShort (&cls.netchan.message, r); upload_pos += r; size = upload_size; if (!size) size = 1; percent = upload_pos*100/size; MSG_WriteByte (&cls.netchan.message, percent); SZ_Write (&cls.netchan.message, buffer, r); Con_DPrintf ("UPLOAD: %6d: %d written\n", upload_pos - r, r); if (upload_pos != upload_size) return; Con_TPrintf ("Upload completed\n"); CL_StopUpload(); } void CL_StartUpload (qbyte *data, int size) { if (cls.state < ca_onserver) return; // gotta be connected // override CL_StopUpload(); Con_DPrintf("Upload starting of %d...\n", size); upload_data = BZ_Malloc(size); memcpy(upload_data, data, size); upload_size = size; upload_pos = 0; CL_NextUpload(); } qboolean CL_IsUploading(void) { if (upload_data || upload_file) return true; return false; } void CL_StopUpload(void) { if (upload_data) BZ_Free(upload_data); if (upload_file) VFS_CLOSE(upload_file); upload_file = NULL; upload_data = NULL; upload_pos = upload_size = 0; } #if 0 //in case we ever want to add any uploads other than snaps static qboolean CL_StartUploadFile(char *filename) { if (!COM_CheckParm("-fileul")) { Con_Printf("You must currently use the -fileul commandline parameter in order to use this functionality\n"); return false; } if (cls.state < ca_onserver) { Con_Printf("not connected\n"); return false; // gotta be connected } CL_StopUpload(); upload_file = FS_OpenVFS(filename, "rb", FS_ROOT); upload_size = VFS_GETLEN(upload_file); upload_pos = 0; if (upload_file) { CL_NextUpload(); return true; } return false; } #endif /* ===================================================================== SERVER CONNECTING MESSAGES ===================================================================== */ void CL_ClearParseState(void) { // done with sounds, request models now memset (cl.model_precache, 0, sizeof(cl.model_precache)); cl_playerindex = -1; cl_h_playerindex = -1; cl_spikeindex = -1; cl_flagindex = -1; cl_rocketindex = -1; cl_grenadeindex = -1; cl_gib1index = -1; cl_gib2index = -1; cl_gib3index = -1; if (cl_baselines) { BZ_Free(cl_baselines); cl_baselines = NULL; } cl_baselines_count = 0; cl_max_static_entities = 0; if (cl_static_entities) { BZ_Free(cl_static_entities); cl_static_entities = NULL; } } /* ================== CL_ParseServerData ================== */ static void CLQW_ParseServerData (void) { int pnum; int clnum; char *str; int protover, svcnt; float maxspeed, entgrav; if (cls.download && cls.download->method == DL_QWPENDING) { //if we didn't actually start downloading it yet, cancel the current download. //this is to avoid qizmo not responding to the download command, resulting in hanging downloads that cause the client to then be unable to connect anywhere simply because someone's skin was set. CL_DownloadFailed(cls.download->remotename, cls.download, DLFAIL_CORRUPTED); } Con_DPrintf ("Serverdata packet %s.\n", cls.demoplayback?"read":"received"); // // wipe the client_state_t struct // SCR_SetLoadingStage(LS_CLIENT); SCR_BeginLoadingPlaque(); // parse protocol version number // allow 2.2 and 2.29 demos to play cls.fteprotocolextensions = 0; cls.fteprotocolextensions2 = 0; cls.ezprotocolextensions1 = 0; for(;;) { protover = MSG_ReadLong (); if (protover == PROTOCOL_VERSION_FTE1) { cls.fteprotocolextensions = MSG_ReadLong(); continue; } if (protover == PROTOCOL_VERSION_FTE2) { cls.fteprotocolextensions2 = MSG_ReadLong(); continue; } if (protover == PROTOCOL_VERSION_EZQUAKE1) { cls.ezprotocolextensions1 = MSG_ReadLong(); continue; } if (protover == PROTOCOL_VERSION_VARLENGTH) { int ident; int len; char data[1024]; ident = MSG_ReadLong(); len = MSG_ReadLong(); if (len <= sizeof(data)) { MSG_ReadData(data, len); switch(ident) { default: break; } continue; } } if (protover == PROTOCOL_VERSION_QW) //this ends the version info break; if (cls.demoplayback && (protover >= 24 && protover <= 28)) //older versions, maintain demo compatability. break; Host_EndGame ("Server returned version %i, not %i\n", protover, PROTOCOL_VERSION_QW); } if (developer.ival || cl_shownet.ival) { if (cls.fteprotocolextensions2||cls.fteprotocolextensions||cls.ezprotocolextensions1) Con_TPrintf ("Using FTE extensions 0x%x%08x %#x\n", cls.fteprotocolextensions2, cls.fteprotocolextensions, cls.ezprotocolextensions1); } if (cls.fteprotocolextensions & ~PEXT_CLIENTSUPPORT) Con_TPrintf (CON_WARNING"Using unknown fte-pext1 extensions (%#x)\n", cls.fteprotocolextensions&~PEXT_CLIENTSUPPORT); if (cls.fteprotocolextensions2 & ~PEXT2_CLIENTSUPPORT) Con_TPrintf (CON_WARNING"Using unknown fte-pext2 extensions (%#x)\n", cls.fteprotocolextensions2&~PEXT2_CLIENTSUPPORT); if (cls.ezprotocolextensions1 & ~EZPEXT1_CLIENTSUPPORT) Con_TPrintf (CON_WARNING"Using unknown ezquake extensions (%#x)\n", cls.ezprotocolextensions1&~EZPEXT1_CLIENTSUPPORT); if ((cls.ezprotocolextensions1 & EZPEXT1_HIDDEN_MESSAGES) && !cls.demoplayback) Con_TPrintf (CON_WARNING"Server's EZPEXT1_HIDDEN_MESSAGES extension does not make sense outside of demos\n"); //we do not request this at all. its safe to blame the server. if (cls.fteprotocolextensions & PEXT_FLOATCOORDS) { cls.netchan.netprim.coordtype = COORDTYPE_FLOAT_32; cls.netchan.netprim.anglesize = 2; } else { cls.netchan.netprim.coordtype = COORDTYPE_FIXED_13_3; cls.netchan.netprim.anglesize = 1; } cls.netchan.message.prim = cls.netchan.netprim; MSG_ChangePrimitives(cls.netchan.netprim); svcnt = MSG_ReadLong (); // game directory str = MSG_ReadString (); Con_DPrintf("Server is using gamedir \"%s\"\n", str); if (!*str) str = "qw"; //FIXME: query active manifest's basegamedir #ifndef CLIENTONLY if (!sv.state) #endif COM_Gamedir(str, NULL); CL_ClearState (true); #ifdef QUAKEHUD Stats_NewMap(); #endif cl.servercount = svcnt; cl.protocol_qw = protover; Cvar_ForceCallback(Cvar_FindVar("r_particlesdesc")); cl.teamfortress = !!Q_strcasestr(str, "fortress"); if (cl.gamedirchanged) { cl.gamedirchanged = false; #ifndef CLIENTONLY if (!sv.state) #endif Wads_Flush(); } /*mvds have different parsing*/ if (cls.demoplayback == DPB_MVD) { extern float olddemotime; int i,j; if (cls.fteprotocolextensions2 & PEXT2_MAXPLAYERS) { cl.allocated_client_slots = MSG_ReadPlayer(); if (cl.allocated_client_slots > MAX_CLIENTS) { Con_Printf(CON_ERROR"Server has too many client slots (%u > %u)\n", cl.allocated_client_slots, MAX_CLIENTS); cl.allocated_client_slots = MAX_CLIENTS; } } cl.gametime = MSG_ReadFloat(); cl.gametimemark = realtime; cl.oldgametime = cl.gametime; cl.oldgametimemark = realtime; cl.demogametimebias = cl.gametime - olddemotime; for (j = 0; j < MAX_SPLITS; j++) { cl.playerview[j].playernum = cl.allocated_client_slots + j; cl.playerview[j].viewentity = 0; //free floating. cl.playerview[j].spectator = true; for (i = 0; i < UPDATE_BACKUP; i++) { cl.inframes[i].playerstate[cl.playerview[j].playernum].pm_type = PM_SPECTATOR; cl.inframes[i].playerstate[cl.playerview[j].playernum].messagenum = 1; } } cl.splitclients = 1; } else if (cls.fteprotocolextensions2 & PEXT2_MAXPLAYERS) { // qboolean spec = false; cl.allocated_client_slots = MSG_ReadPlayer(); if (cl.allocated_client_slots > MAX_CLIENTS) { Con_Printf(CON_ERROR"Server has too many client slots (%u > %u)\n", cl.allocated_client_slots, MAX_CLIENTS); cl.allocated_client_slots = MAX_CLIENTS; } /*parsing here is slightly different to allow us 255 max players instead of 127*/ cl.splitclients = (qbyte)MSG_ReadByte(); if (cls.fteprotocolextensions2 & PEXT2_VRINPUTS) ; else if (cl.splitclients & 128) { // spec = true; cl.splitclients &= ~128; } if (cl.splitclients > MAX_SPLITS) Host_EndGame("Server sent us too many seats (%u > %u)\n", cl.splitclients, MAX_SPLITS); for (pnum = 0; pnum < cl.splitclients; pnum++) { cl.playerview[pnum].spectator = true; #ifdef QUAKESTATS if (cls.z_ext & Z_EXT_VIEWHEIGHT) cl.playerview[pnum].viewheight = cl.playerview[pnum].statsf[STAT_VIEWHEIGHT]; #endif cl.playerview[pnum].playernum = (qbyte)MSG_ReadByte(); if (cl.playerview[pnum].playernum >= cl.allocated_client_slots) Host_EndGame("unsupported local player slot\n"); cl.playerview[pnum].viewentity = cl.playerview[pnum].playernum+1; } } else { // parse player slot, high bit means spectator pnum = MSG_ReadByte (); for (clnum = 0; ; clnum++) { if (clnum == MAX_SPLITS) Host_EndGame("Server sent us over %u seats\n", MAX_SPLITS); #ifdef QUAKESTATS if (cls.z_ext & Z_EXT_VIEWHEIGHT) cl.playerview[pnum].viewheight = cl.playerview[pnum].statsf[STAT_VIEWHEIGHT]; #endif cl.playerview[clnum].playernum = pnum; if (cl.playerview[clnum].playernum & 128) { cl.playerview[clnum].spectator = true; cl.playerview[clnum].playernum &= ~128; } else cl.playerview[clnum].spectator = false; if (cl.playerview[clnum].playernum >= cl.allocated_client_slots) Host_EndGame("unsupported local player slot\n"); cl.playerview[clnum].viewentity = cl.playerview[clnum].playernum+1; if (!(cls.fteprotocolextensions & PEXT_SPLITSCREEN)) break; pnum = MSG_ReadByte (); if (pnum == 128) break; } cl.splitclients = clnum+1; } // get the full level name str = MSG_ReadString (); Q_strncpyz (cl.levelname, str, sizeof(cl.levelname)); if (cl.protocol_qw >= 25) { // get the movevars movevars.gravity = MSG_ReadFloat(); movevars.stopspeed = MSG_ReadFloat(); maxspeed = MSG_ReadFloat(); movevars.spectatormaxspeed = MSG_ReadFloat(); movevars.accelerate = MSG_ReadFloat(); movevars.airaccelerate = MSG_ReadFloat(); movevars.wateraccelerate = MSG_ReadFloat(); movevars.friction = MSG_ReadFloat(); movevars.waterfriction = MSG_ReadFloat(); entgrav = MSG_ReadFloat(); } else { movevars.gravity = 800; movevars.stopspeed = 100; maxspeed = 320; movevars.spectatormaxspeed = 500; movevars.accelerate = 10; movevars.airaccelerate = 0.7f; movevars.wateraccelerate = 10; movevars.friction = 6.0f; movevars.waterfriction = 1; entgrav = 1; } movevars.flags = MOVEFLAG_QWCOMPAT; for (clnum = 0; clnum < cl.splitclients; clnum++) { cl.playerview[clnum].maxspeed = maxspeed; cl.playerview[clnum].entgravity = entgrav; } // seperate the printfs so the server message can have a color #if 1 Con_Printf ("\n\n"); Con_Printf ("^Ue01d^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01f"); Con_Printf ("\n\n"); Con_Printf ("\1%s\n", str); #else Con_TPrintf ("\n\n^Ue01d^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01f\n\n"); Con_Printf ("%c%s\n", 2, str); #endif if (CL_RemoveClientCommands("new")) //mvdsv is really appaling some times. { // Con_Printf("Multiple 'new' commands?!?!? This server needs reinstalling!\n"); } memset(cl.sound_name, 0, sizeof(cl.sound_name)); if (cls.demoplayback == DPB_MVD && (cls.demoeztv_ext&EZTV_DOWNLOAD)) { if (CL_RemoveClientCommands("qtvsoundlist")) Con_DPrintf("Multiple soundlists\n"); CL_SendClientCommand (true, "qtvsoundlist %i 0", cl.servercount); } else { if (CL_RemoveClientCommands("soundlist") && !(cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS)) Con_DPrintf("Multiple soundlists\n"); // ask for the sound list next // CL_SendClientCommand ("soundlist %i 0", cl.servercount); CL_SendClientCommand (true, soundlist_name, cl.servercount, 0); } // now waiting for downloads, etc cls.state = ca_onserver; Cam_AutoTrack_Update(NULL); cl.sendprespawn = false; #ifdef VOICECHAT S_Voip_MapChange(); #endif #ifdef CSQC_DAT CSQC_Shutdown(); //revive it when we get the serverinfo saying the checksum. #endif } #ifdef Q2CLIENT static void CLQ2_ParseServerData (void) { char *str; int i; int svcnt; int rate; // int cflag; memset(&cls.netchan.netprim, 0, sizeof(cls.netchan.netprim)); cls.netchan.netprim.coordtype = COORDTYPE_FIXED_13_3; cls.netchan.netprim.anglesize = 1; cls.fteprotocolextensions = 0; cls.fteprotocolextensions2 = 0; cls.ezprotocolextensions1 = 0; cls.demohadkeyframe = true; //assume that it did, so this stuff all gets recorded. Con_DPrintf ("Serverdata packet %s.\n", cls.demoplayback?"read":"received"); // // wipe the client_state_t struct // SCR_SetLoadingStage(LS_CLIENT); SCR_BeginLoadingPlaque(); // CL_ClearState (); cls.state = ca_onserver; // parse protocol version number i = MSG_ReadLong (); if (i == PROTOCOL_VERSION_FTE1) { cls.fteprotocolextensions = i = MSG_ReadLong(); if (i & PEXT_FLOATCOORDS) i -= PEXT_FLOATCOORDS; if (i & PEXT_SOUNDDBL) i -= PEXT_SOUNDDBL; if (i & PEXT_MODELDBL) i -= PEXT_MODELDBL; if (i & PEXT_SPLITSCREEN) i -= PEXT_SPLITSCREEN; if (i) Host_EndGame ("Unsupported q2 protocol extensions: %x", i); i = MSG_ReadLong (); if (cls.fteprotocolextensions & PEXT_FLOATCOORDS) { cls.netchan.netprim.coordtype = COORDTYPE_FLOAT_32; cls.netchan.netprim.anglesize = 2; } } if (i == PROTOCOL_VERSION_R1Q2) Con_DPrintf("Using R1Q2 protocol\n"); else if (i == PROTOCOL_VERSION_Q2PRO) Con_DPrintf("Using Q2PRO protocol\n"); else if (i == PROTOCOL_VERSION_Q2EXDEMO) { i = PROTOCOL_VERSION_Q2EX; //close enough, don't distinguish. only real difference is 16bit coords for WritePos etc. Con_DPrintf("Using Q2EXDemo protocol\n"); } else if (i == PROTOCOL_VERSION_Q2EX) { cls.netchan.netprim.coordtype = COORDTYPE_FLOAT_32; cls.netchan.netprim.anglesize = 2; Con_DPrintf("Using Q2EX protocol\n"); } else if (i > PROTOCOL_VERSION_Q2 || i < (cls.demoplayback?PROTOCOL_VERSION_Q2_DEMO_MIN:PROTOCOL_VERSION_Q2_MIN)) Host_EndGame ("Q2 Server returned version %i, not %i", i, PROTOCOL_VERSION_Q2); cls.protocol_q2 = i; svcnt = MSG_ReadLong (); /*cl.attractloop =*/ MSG_ReadByte (); if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX) rate = MSG_ReadByte(); else rate = 10; //fixed to a poopy 10hz. // game directory str = MSG_ReadString (); // set gamedir if (!*str) COM_Gamedir("baseq2", NULL); else COM_Gamedir(str, NULL); Cvar_Get("timescale", "1", 0, "Q2Admin hacks"); //Q2Admin will kick players who have a timescale set to something other than 1 //FTE doesn't actually have a timescale cvar, so create one to 'fool' q2admin. //I can't really blame q2admin for rejecting engines that don't have this cvar, as it could have been renamed via a hex-edit. CL_ClearState (true); CLQ2_ClearState (); cl.minpitch = -89; cl.maxpitch = 89; cl.servercount = svcnt; Cam_AutoTrack_Update(NULL); #ifdef QUAKEHUD Stats_NewMap(); #endif // parse player entity number cl.playerview[0].playernum = MSG_ReadShort (); cl.playerview[0].viewentity = cl.playerview[0].playernum+1; cl.playerview[0].spectator = false; cl.splitclients = 1; if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX && cl.playerview[0].playernum==-2) { i = MSG_ReadShort(); if (i > MAX_SPLITS) Host_EndGame ("server's splitscreen count too high (%u > %u)\n", i, MAX_SPLITS); cl.splitclients = i; for (i = 0; i < cl.splitclients; i++) { cl.playerview[i].playernum = MSG_ReadShort (); cl.playerview[i].viewentity = cl.playerview[i].playernum+1; cl.playerview[i].spectator = false; } } cl.numq2visibleweapons = 1; //give it a default. cl.q2visibleweapons[0] = "weapon.md2"; cl.q2svnetrate = rate; if (cl.q2svnetrate < 1) //wut?... cl.q2svnetrate = 10; // get the full level name str = MSG_ReadString (); Q_strncpyz (cl.levelname, str, sizeof(cl.levelname)); if (cls.protocol_q2 == PROTOCOL_VERSION_R1Q2) { unsigned short r1q2ver; qboolean isenhanced = MSG_ReadByte(); if (isenhanced) Host_EndGame ("R1Q2 server is running an unsupported mod"); r1q2ver = MSG_ReadShort(); //protocol version... limit... yeah, buggy. if (r1q2ver > 1905) Host_EndGame ("R1Q2 server version %i not supported", r1q2ver); if (r1q2ver >= 1903) { MSG_ReadByte(); //'used to be advanced deltas' MSG_ReadByte(); //strafejump hack } if (r1q2ver >= 1904) cls.netchan.netprim.flags |= NPQ2_R1Q2_UCMD; if (r1q2ver >= 1905) cls.netchan.netprim.flags |= NPQ2_SOLID32; } else if (cls.protocol_q2 == PROTOCOL_VERSION_Q2PRO) { unsigned short q2prover = MSG_ReadShort(); //q2pro protocol version if (q2prover < 1011 || q2prover > 1021) Host_EndGame ("Q2PRO server version %i not supported", q2prover); MSG_ReadByte(); //server state (ie: demo playback vs actual game) MSG_ReadByte(); //strafejump hack MSG_ReadByte(); //q2pro qw-mode. kinda silly for us tbh. if (q2prover >= 1014) cls.netchan.netprim.flags |= NPQ2_SOLID32; if (q2prover >= 1018) cls.netchan.netprim.flags |= NPQ2_ANG16; if (q2prover >= 1015) MSG_ReadByte(); //some kind of waterjump hack enable } cls.netchan.message.prim = cls.netchan.netprim; MSG_ChangePrimitives(cls.netchan.netprim); if (cl.playerview[0].playernum == -1) { // playing a cinematic or showing a pic, not a level SCR_EndLoadingPlaque(); CL_MakeActive("Quake2"); if (!COM_FCheckExists(str) && !COM_FCheckExists(va("video/%s", str))) { int i; char basename[64], *t; char *exts[] = {".ogv", ".roq", ".cin"}; COM_StripExtension(COM_SkipPath(str), basename, sizeof(basename)); for(i = 0; i < countof(exts); i++) { t = va("video/%s%s", basename, exts[i]); if (COM_FCheckExists(t)) { str = t; break; } } } if (!Media_PlayFilm(str, false)) { CL_SendClientCommand(true, "nextserver %i", cl.servercount); } } else { // seperate the printfs so the server message can have a color Con_TPrintf ("\n\n^Ue01d^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01f\n\n"); Con_Printf ("%c%s\n", 2, str); Media_StopFilm(true); // need to prep refresh at next oportunity //cl.refresh_prepped = false; } Cvar_ForceCallback(Cvar_FindVar("r_particlesdesc")); Surf_PreNewMap(); CL_CheckServerInfo(); } #endif void CL_ParseEstablished(void) { #ifdef NQPROT cls.qex = false; Z_Free(cl_dp_packagenames); cl_dp_packagenames = NULL; cl_dp_serverextension_download = false; *cl_dp_csqc_progsname = 0; cl_dp_csqc_progscrc = 0; cl_dp_csqc_progssize = 0; #endif if (cls.netchan.remote_address.type != NA_LOOPBACK) { char fp[DIGEST_MAXSIZE*3+1+4]; char dig[DIGEST_MAXSIZE+1]; char cert[8192]; const char *security; switch(cls.protocol) { case CP_QUAKEWORLD: Con_DPrintf(S_COLOR_GRAY"QW "); break; case CP_NETQUAKE: Con_Printf (S_COLOR_GRAY"NQ "); break; case CP_QUAKE2: Con_Printf (S_COLOR_GRAY"Q2 "); break; case CP_QUAKE3: Con_Printf (S_COLOR_GRAY"Q3 "); break; default: break; } *fp = 0; if (NET_IsEncrypted(&cls.netchan.remote_address)) { if (cls.netchan.remote_address.prot == NP_DTLS) { int sz = NET_GetConnectionCertificate(cls.sockets, &cls.netchan.remote_address, QCERT_PEERCERTIFICATE, cert,sizeof(cert)); if (sz >= 0) { Q_strncpyz(fp, "?fp=",sizeof(fp)); sz = 4+Base64_EncodeBlockURI(dig, CalcHash(&hash_certfp, dig,sizeof(dig), cert, sz), fp+4,sizeof(fp)-4); fp[sz] = 0; } } security = localtext("^["S_COLOR_GREEN"encrypted\\tip\\Any passwords will be sent securely, but will still be readable by the server admin\n^]"); } else security = localtext("^["S_COLOR_RED"plain-text\\tip\\"CON_WARNING"Do not type passwords as they can potentially be seen by network sniffers^]"); Con_Printf ("\r"); Con_TPrintf ("Connected to ^["S_COLOR_BLUE"%s"S_COLOR_GRAY"%s\\type\\connect %s%s^] (%s).\n", cls.servername, fp, cls.servername, fp, security); } } #ifdef NQPROT static void CLNQ_ParseProtoVersion(void) { int protover; struct netprim_s netprim; cls.fteprotocolextensions = 0; cls.fteprotocolextensions2 = 0; cls.ezprotocolextensions1 = 0; for(;;) { protover = MSG_ReadLong (); switch(protover) { case PROTOCOL_VERSION_FTE1: cls.fteprotocolextensions = MSG_ReadLong(); continue; case PROTOCOL_VERSION_FTE2: cls.fteprotocolextensions2 = MSG_ReadLong(); continue; default: break; } break; } netprim.coordtype = COORDTYPE_FIXED_13_3; netprim.anglesize = 1; cls.protocol_nq = CPNQ_ID; cls.z_ext = 0; #ifdef HAVE_LEGACY if (protover == PROTOCOL_VERSION_NQ && cls.demoplayback) { if (!Q_strcasecmp(FS_GetGamedir(true), "nehahra")) protover = PROTOCOL_VERSION_NEHD; //if we're using the nehahra gamedir, pretend that it was the nehahra protocol version. clients should otherwise be using a different protocol. } #endif if (protover == PROTOCOL_VERSION_NEHD) { cls.protocol_nq = CPNQ_NEHAHRA; Con_DPrintf("Nehahra demo net protocol\n"); } else if (protover == PROTOCOL_VERSION_FITZ) { //fitzquake 0.85 cls.protocol_nq = CPNQ_FITZ666; Con_DPrintf("FitzQuake 666 protocol\n"); } else if (protover == PROTOCOL_VERSION_RMQ) { int fl; cls.protocol_nq = CPNQ_FITZ666; Con_DPrintf("RMQ extensions to FitzQuake's protocol\n"); fl = MSG_ReadLong(); if (((fl & RMQFL_SHORTANGLE) && (fl & RMQFL_FLOATANGLE)) || ((fl & RMQFL_24BITCOORD) && (fl & RMQFL_INT32COORD)) || ((fl & RMQFL_24BITCOORD) && (fl & RMQFL_FLOATCOORD)) || ((fl & RMQFL_INT32COORD) && (fl & RMQFL_FLOATCOORD)) ) Host_EndGame("Server is using conflicting RMQ protocol bits - %#x\n", fl); if (fl & RMQFL_SHORTANGLE) netprim.anglesize = 2; if (fl & RMQFL_FLOATANGLE) netprim.anglesize = 4; if (fl & RMQFL_24BITCOORD) netprim.coordtype = COORDTYPE_FIXED_16_8; if (fl & RMQFL_INT32COORD) netprim.coordtype = COORDTYPE_FIXED_28_4; if (fl & RMQFL_FLOATCOORD) netprim.coordtype = COORDTYPE_FLOAT_32; fl &= ~(RMQFL_SHORTANGLE|RMQFL_FLOATANGLE|RMQFL_24BITCOORD|RMQFL_INT32COORD|RMQFL_FLOATCOORD|RMQFL_EDICTSCALE); if (fl) Host_EndGame("Server is using unsupported RMQ extensions - %#x\n", fl); } else if (protover == PROTOCOL_VERSION_DP5) { //darkplaces5 cls.protocol_nq = CPNQ_DP5; netprim.coordtype = COORDTYPE_FLOAT_32; netprim.anglesize = 2; Con_DPrintf("DP5 protocols\n"); } else if (protover == PROTOCOL_VERSION_DP6) { //darkplaces6 (it's a small difference from dp5) cls.protocol_nq = CPNQ_DP6; netprim.coordtype = COORDTYPE_FLOAT_32; netprim.anglesize = 2; cls.z_ext = Z_EXT_VIEWHEIGHT; Con_DPrintf("DP6 protocols\n"); } else if (protover == PROTOCOL_VERSION_DP7) { //darkplaces7 (it's a small difference from dp5) cls.protocol_nq = CPNQ_DP7; netprim.coordtype = COORDTYPE_FLOAT_32; netprim.anglesize = 2; cls.z_ext = Z_EXT_VIEWHEIGHT; Con_DPrintf("DP7 protocols\n"); } else if (protover == PROTOCOL_VERSION_H2) { if (cls.demoplayback) cls.protocol_nq = CPNQ_H2MP; else Host_EndGame ("\nUnable to connect to standard Hexen2 servers. Host the game with "DISTRIBUTION"\n"); } else if (protover == PROTOCOL_VERSION_BJP1) { cls.protocol_nq = CPNQ_BJP1; Con_DPrintf("bjp1 %i protocol\n", PROTOCOL_VERSION_BJP1); } else if (protover == PROTOCOL_VERSION_BJP2) { cls.protocol_nq = CPNQ_BJP2; Con_DPrintf("bjp2 %i protocol\n", PROTOCOL_VERSION_BJP2); } else if (protover == PROTOCOL_VERSION_BJP3) { cls.protocol_nq = CPNQ_BJP3; Con_DPrintf("bjp3 %i protocol\n", PROTOCOL_VERSION_BJP3); } else if (protover == PROTOCOL_VERSION_NQ) Con_DPrintf("Standard NQ protocols\n"); else Host_EndGame ("Server is using protocol version %i, which is not supported by this version of " FULLENGINENAME ".", protover); if (cls.fteprotocolextensions & PEXT_FLOATCOORDS) { if (netprim.anglesize < 2) netprim.anglesize = 2; if (netprim.coordtype < COORDTYPE_FLOAT_32) netprim.coordtype = COORDTYPE_FLOAT_32; } cls.netchan.message.prim = cls.netchan.netprim = netprim; MSG_ChangePrimitives(netprim); } static int CL_Darkplaces_Particle_Precache(const char *pname) { int i; for (i = 1; i < MAX_SSPARTICLESPRE; i++) { if (!cl.particle_ssname[i]) { cl.particle_ssname[i] = strdup(pname); cl.particle_ssprecache[i] = P_FindParticleType(pname); cl.particle_ssprecaches = true; return i; } if (!strcmp(cl.particle_ssname[i], pname)) return i; } return 0; //failed } //FIXME: move to header void CL_KeepaliveMessage(void){} static void CLNQ_ParseServerData(void) //Doesn't change gamedir - use with caution. { int nummodels, numsounds; char *str = NULL; int gametype; Con_DPrintf ("Serverdata packet %s.\n", cls.demoplayback?"read":"received"); SCR_SetLoadingStage(LS_CLIENT); CL_ClearState (true); #ifdef QUAKEHUD Stats_NewMap(); #endif Cvar_ForceCallback(Cvar_FindVar("r_particlesdesc")); CLNQ_ParseProtoVersion(); if (cls.qex) { cl.allocated_client_slots = MSG_ReadPlayer(); str = MSG_ReadString(); } else { if (cls.fteprotocolextensions2 & PEXT2_PREDINFO) str = MSG_ReadString(); cl.allocated_client_slots = MSG_ReadPlayer(); } if (str) { #ifndef CLIENTONLY if (!sv.state) #endif COM_Gamedir(str, NULL); } if (cl.allocated_client_slots > MAX_CLIENTS) { cl.allocated_client_slots = MAX_CLIENTS; Con_Printf ("\nWarning, this server supports more than %i clients, additional clients will do bad things\n", MAX_CLIENTS); } cl.splitclients = 1; gametype = MSG_ReadByte (); str = MSG_ReadString (); Q_strncpyz (cl.levelname, str, sizeof(cl.levelname)); // seperate the printfs so the server message can have a color #if 1 Con_Printf ("\n\n"); Con_Printf ("^Ue01d^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01f"); Con_Printf ("\n\n"); Con_Printf ("\1%s\n", str); #else Con_TPrintf ("\n\n^Ue01d^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01e^Ue01f\n\n"); Con_Printf ("%c%s\n", 2, str); #endif SCR_BeginLoadingPlaque(); Surf_PreNewMap(); for (nummodels=0; nummodels < countof(cl.model_name); nummodels++) { if (cl.model_name[nummodels]) { Z_Free(cl.model_name[nummodels]); cl.model_name[nummodels] = NULL; } } for (nummodels=1 ; ; nummodels++) { str = MSG_ReadString (); if (!str[0]) break; if (nummodels==MAX_PRECACHE_MODELS) { Con_TPrintf ("Server sent too many model precaches\n"); return; } Z_StrDupPtr(&cl.model_name[nummodels], str); if (*str != '*' && strcmp(str, "null")) //not inline models! CL_CheckOrEnqueDownloadFile(str, NULL, ((nummodels==1)?DLLF_REQUIRED|DLLF_ALLOWWEB:0)); //qw has a special network protocol for spikes. if (!strcmp(cl.model_name[nummodels],"progs/spike.mdl")) cl_spikeindex = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/player.mdl")) cl_playerindex = nummodels; #ifdef HAVE_LEGACY if (cl.model_name_vwep[0] && !strcmp(cl.model_name[nummodels],cl.model_name_vwep[0]) && cl_playerindex == -1) cl_playerindex = nummodels; #endif if (!strcmp(cl.model_name[nummodels],"progs/h_player.mdl")) cl_h_playerindex = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/flag.mdl")) cl_flagindex = nummodels; //rocket to grenade if (!strcmp(cl.model_name[nummodels],"progs/missile.mdl")) cl_rocketindex = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/grenade.mdl")) cl_grenadeindex = nummodels; //cl_gibfilter if (!strcmp(cl.model_name[nummodels],"progs/gib1.mdl")) cl_gib1index = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/gib2.mdl")) cl_gib2index = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/gib3.mdl")) cl_gib3index = nummodels; Mod_TouchModel (str); } for (numsounds=0; numsounds < countof(cl.sound_name); numsounds++) { if (cl.sound_name[numsounds]) { Z_Free(cl.sound_name[numsounds]); cl.sound_name[numsounds] = NULL; } } for (numsounds=1 ; ; numsounds++) { str = MSG_ReadString (); if (!str[0]) break; if (numsounds==MAX_PRECACHE_SOUNDS) { Con_TPrintf ("Server sent too many sound precaches\n"); return; } Z_StrDupPtr(&cl.sound_name[numsounds], str); cl.sound_precache[numsounds] = S_FindName(cl.sound_name[numsounds], true, false); Sound_CheckDownload(str); } cls.signon = 0; cls.state = ca_onserver; Cam_AutoTrack_Update(NULL); //fill in the csqc stuff if (!cl_dp_csqc_progscrc) { InfoBuf_RemoveKey(&cl.serverinfo, "*csprogs"); InfoBuf_RemoveKey(&cl.serverinfo, "*csprogssize"); InfoBuf_RemoveKey(&cl.serverinfo, "*csprogsname"); } else { InfoBuf_SetStarKey(&cl.serverinfo, "*csprogs", va("%i", cl_dp_csqc_progscrc)); InfoBuf_SetStarKey(&cl.serverinfo, "*csprogssize", va("%i", cl_dp_csqc_progssize)); InfoBuf_SetStarKey(&cl.serverinfo, "*csprogsname", va("%s", cl_dp_csqc_progsname)); } if (cl_dp_packagenames) { char *in = cl_dp_packagenames; if (cl.serverpacknames) Z_StrCat(&cl.serverpacknames, " "); Z_StrCat(&cl.serverpacknames, in); while ((in = COM_Parse(in))) { Z_StrCat(&cl.serverpackhashes, cl.serverpackhashes?" -":"-"); //no hash info. cl.serverpakschanged = true; } } //update gamemode if (gametype != GAME_COOP) InfoBuf_SetKey(&cl.serverinfo, "deathmatch", "1"); else InfoBuf_SetKey(&cl.serverinfo, "deathmatch", "0"); InfoBuf_SetKey(&cl.serverinfo, "teamplay", "0"); //allow some things by default that quakeworld bans by default InfoBuf_SetKey(&cl.serverinfo, "watervis", "1"); //prohibit some things that QW/FTE has enabled by default, which would be frowned upon in NQ InfoBuf_SetKey(&cl.serverinfo, "fbskins", "0"); //pretend it came from the server, and update cheat/permissions/etc CL_CheckServerInfo(); #if _MSC_VER > 1200 Sys_RecentServer("+connectnq", cls.servername, cls.servername, "Join NQ Server"); #endif if (CPNQ_IS_DP) //DP's protocol requires client+server to have exactly the same data files. this is shit, but in the interests of compatibility... COM_Effectinfo_Enumerate(CL_Darkplaces_Particle_Precache); #ifdef VOICECHAT S_Voip_MapChange(); #endif #ifdef CSQC_DAT CSQC_Shutdown(); #endif } static void CLQEX_ParseServerVars(void) { unsigned int bits = MSG_ReadULEB128(); if (bits & QEX_GV_DEATHMATCH) InfoBuf_SetStarKey(&cl.serverinfo, "deathmatch", va("%i", MSG_ReadByte ())); if (bits & QEX_GV_IDEALPITCHSCALE) MSG_ReadFloat (); if (bits & QEX_GV_FRICTION) movevars.friction = MSG_ReadFloat (); if (bits & QEX_GV_EDGEFRICTION) InfoBuf_SetStarKey(&cl.serverinfo, "pm_edgefriction", va("%g", MSG_ReadFloat ())); if (bits & QEX_GV_STOPSPEED) movevars.stopspeed = MSG_ReadFloat (); if (bits & QEX_GV_MAXVELOCITY) /*movevars.maxvelocity =*/ MSG_ReadFloat (); if (bits & QEX_GV_GRAVITY) movevars.gravity = MSG_ReadFloat (); if (bits & QEX_GV_NOSTEP) /*movevars.nostep =*/ MSG_ReadByte (); if (bits & QEX_GV_MAXSPEED) movevars.maxspeed = MSG_ReadFloat (); if (bits & QEX_GV_ACCELERATE) movevars.accelerate = MSG_ReadFloat (); if (bits & QEX_GV_CONTROLLERONLY) InfoBuf_SetStarKey(&cl.serverinfo, "nomouse", va("%i", MSG_ReadByte ())); if (bits & QEX_GV_TIMELIMIT) InfoBuf_SetStarKey(&cl.serverinfo, "timelimit", va("%g", MSG_ReadFloat ())); if (bits & QEX_GV_FRAGLIMIT) InfoBuf_SetStarKey(&cl.serverinfo, "fraglimit", va("%g", MSG_ReadFloat ())); if (bits & QEX_GV_TEAMPLAY) InfoBuf_SetStarKey(&cl.serverinfo, "teamplay", va("%i", MSG_ReadByte ())); if (bits & ~QEX_GV_ALL) Con_Printf("CLQEX_ParseServerVars: Unknown bits %#x\n", bits & ~QEX_GV_ALL); CL_CheckServerInfo(); } static void CLQEX_ParsePrompt(void) { int a, count = MSG_ReadByte(), imp; const char *s; char message[65536]; size_t ofs = 0; if (count == 0) { SCR_CenterPrint(0, NULL, true); return; } *message = 0; s = MSG_ReadString(); Q_strncatz(message+ofs, "/S/C/.", sizeof(message)-ofs); ofs += strlen(message+ofs); TL_Reformat(com_language, message+ofs, sizeof(message)-ofs, 1, &s); ofs += strlen(message+ofs); Q_strncatz(message+ofs, "\n", sizeof(message)-ofs); ofs += strlen(message+ofs); for (a = 0; a < count; a++) { s = MSG_ReadString(); imp = MSG_ReadByte(); Q_strncatz(message+ofs, "^[[", sizeof(message)-ofs); ofs += strlen(message+ofs); TL_Reformat(com_language, message+ofs, sizeof(message)-ofs, 1, &s); ofs += strlen(message+ofs); Q_strncatz(message+ofs, va("]\\impulse\\%i^]\n", imp), sizeof(message)-ofs); ofs += strlen(message+ofs); } SCR_CenterPrint(0, message, true); } static char *CLQEX_ReadStrings(void) { unsigned short count = MSG_ReadShort(), a; const char *arg[256]; static char formatted[8192]; char inputs[65536]; size_t ofs = 0; for (a = 0; a < count && a < countof(arg); ) { arg[a++] = MSG_ReadStringBuffer(inputs+ofs, sizeof(inputs)-ofs-1); ofs += strlen(inputs+ofs)+1; if (ofs >= sizeof(inputs)) break; } for (; a < count; a++) MSG_ReadString(); //don't lose space, though we can't buffer it. TL_Reformat(com_language, formatted, sizeof(formatted), a, arg); return formatted; } static void CLNQ_SendInitialUserInfo(void *ctx, const char *key, const char *value) { InfoSync_Add(&cls.userinfosync, ctx, key); } void CLNQ_SignonReply (void) { extern cvar_t topcolor; extern cvar_t bottomcolor; extern cvar_t rate; extern cvar_t model; extern cvar_t skin; Con_DPrintf ("CL_SignonReply: %i\n", cls.signon); switch (cls.signon) { case 1: cl.sendprespawn = true; SCR_SetLoadingFile("loading data"); CL_RequestNextDownload(); //this sucks, but sometimes mods send csqc-specific messages to us before things are properly inited. if we start doing stuff now then we can minimize the chances of dodgy mods screwing with us. FIXME: warn about receiving csqc messages before begin. break; case 2: CL_SendClientCommand(true, "name \"%s\"\n", name.string); CL_SendClientCommand(true, "color %i %i\n", topcolor.ival, bottomcolor.ival); if (cl.haveserverinfo) InfoBuf_Enumerate(&cls.userinfo[0], &cls.userinfo[0], CLNQ_SendInitialUserInfo); else if (CPNQ_IS_DP) { //dp needs a couple of extras to work properly in certain cases. don't send them on other servers because that generally results in error messages. CL_SendClientCommand(true, "rate %s", rate.string); CL_SendClientCommand(true, "playermodel %s", model.string); CL_SendClientCommand(true, "playerskin %s", skin.string); } CL_SendClientCommand(true, "spawn %s", ""); break; case 3: CL_SendClientCommand(true, "begin"); break; case 4: SCR_EndLoadingPlaque (); // allow normal screen updates SCR_SetLoadingStage(LS_NONE); break; } } #define DEFAULT_VIEWHEIGHT 22 static void CLNQ_ParseClientdata (void) { int i; const int seat = 0; player_state_t *pl = &cl.inframes[cl.validsequence&UPDATE_MASK].playerstate[cl.playerview[seat].playernum]; unsigned int bits; bits = (unsigned short)MSG_ReadShort(); if (bits & SU_EXTEND1) bits |= (MSG_ReadByte() << 16); if (bits & SU_EXTEND2) bits |= (MSG_ReadByte() << 24); if (bits & SU_VIEWHEIGHT) CL_SetStatInt(0, STAT_VIEWHEIGHT, MSG_ReadChar ()); else if ((!CPNQ_IS_DP || cls.protocol_nq <= CPNQ_DP5) && cls.protocol_nq != CPNQ_H2MP) CL_SetStatInt(0, STAT_VIEWHEIGHT, DEFAULT_VIEWHEIGHT); if (bits & SU_IDEALPITCH) CL_SetStatInt(0, STAT_IDEALPITCH, MSG_ReadChar ()); else if (cls.protocol_nq != CPNQ_H2MP) CL_SetStatInt(0, STAT_IDEALPITCH, 0); if (cls.protocol_nq == CPNQ_H2MP && (bits & (1<<8)/*SU_IDEALROLL*/)) MSG_ReadChar (); for (i=0 ; i<3 ; i++) { if (bits & (SU_PUNCH1<velocity[i] = MSG_ReadFloat(); else pl->velocity[i] = MSG_ReadChar()*16; } else if (cls.protocol_nq != CPNQ_H2MP) pl->velocity[i] = 0; } if ((bits & SU_ITEMS) || cls.protocol_nq == CPNQ_ID) //hipnotic bug - hipnotic demos don't always have SU_ITEMS set, yet they update STAT_ITEMS anyway. CL_SetStatInt(0, STAT_ITEMS, MSG_ReadLong()); pl->onground = (bits & SU_ONGROUND) != 0; if (bits & SU_INWATER) pl->flags |= PF_INWATER; //mostly just means smartjump should be used. else pl->flags &= ~PF_INWATER; if (cls.protocol_nq == CPNQ_DP5) { CL_SetStatInt(0, STAT_WEAPONFRAME, (bits & SU_WEAPONFRAME)?(unsigned short)MSG_ReadShort():0); CL_SetStatInt(0, STAT_ARMOR, (bits & SU_ARMOR)?MSG_ReadShort():0); CL_SetStatInt(0, STAT_WEAPONMODELI, (bits & SU_WEAPONMODEL)?MSG_ReadShort():0); CL_SetStatInt(0, STAT_HEALTH, MSG_ReadShort()); CL_SetStatInt(0, STAT_AMMO, MSG_ReadShort()); CL_SetStatInt(0, STAT_SHELLS, MSG_ReadShort()); CL_SetStatInt(0, STAT_NAILS, MSG_ReadShort()); CL_SetStatInt(0, STAT_ROCKETS, MSG_ReadShort()); CL_SetStatInt(0, STAT_CELLS, MSG_ReadShort()); CL_SetStatInt(0, STAT_ACTIVEWEAPON, (unsigned short)MSG_ReadShort()); } else if (CPNQ_IS_DP && cls.protocol_nq > CPNQ_DP5) { /*nothing in dp6+*/ } else if (cls.protocol_nq == CPNQ_H2MP) { //only changed stuff if (bits & SU_WEAPONFRAME) CL_SetStatInt(0, STAT_WEAPONFRAME, MSG_ReadByte()); if (bits & SU_ARMOR) CL_SetStatInt(0, STAT_ARMOR, MSG_ReadByte()); if (bits & SU_WEAPONMODEL) CL_SetStatInt(0, STAT_WEAPONMODELI, MSG_ReadUInt16()); } //nothing else. else { int weaponmodel = 0, armour = 0, weaponframe = 0, health = 0, currentammo = 0, shells = 0, nails = 0, rockets = 0, cells = 0, activeweapon = 0; if (bits & SU_WEAPONFRAME) weaponframe |= (unsigned char)MSG_ReadByte(); if (bits & SU_ARMOR) armour |= (unsigned char)MSG_ReadByte(); if (bits & SU_WEAPONMODEL) { if (CPNQ_IS_BJP) weaponmodel |= (unsigned short)MSG_ReadShort(); else weaponmodel |= (unsigned char)MSG_ReadByte(); } health |= MSG_ReadShort(); currentammo |= MSG_ReadByte(); shells |= MSG_ReadByte(); nails |= MSG_ReadByte(); rockets |= MSG_ReadByte(); cells |= MSG_ReadByte(); activeweapon |= MSG_ReadByte(); if (cls.protocol_nq == CPNQ_FITZ666) { if (bits & FITZSU_WEAPONMODEL2) weaponmodel |= MSG_ReadByte() << 8; if (bits & FITZSU_ARMOR2) armour |= MSG_ReadByte() << 8; if (bits & FITZSU_AMMO2) currentammo |= MSG_ReadByte() << 8; if (bits & FITZSU_SHELLS2) shells |= MSG_ReadByte() << 8; if (bits & FITZSU_NAILS2) nails |= MSG_ReadByte() << 8; if (bits & FITZSU_ROCKETS2) rockets |= MSG_ReadByte() << 8; if (bits & FITZSU_CELLS2) cells |= MSG_ReadByte() << 8; if (bits & FITZSU_WEAPONFRAME2) weaponframe |= MSG_ReadByte() << 8; if (bits & FITZSU_WEAPONALPHA) MSG_ReadByte(); if (cls.qex) { if (bits & QEX_SU_ENTFLAGS) /*entflags =*/ MSG_ReadULEB128(); } } CL_SetStatInt(0, STAT_WEAPONFRAME, weaponframe); CL_SetStatInt(0, STAT_ARMOR, armour); CL_SetStatInt(0, STAT_WEAPONMODELI, weaponmodel); CL_SetStatInt(0, STAT_HEALTH, health); CL_SetStatInt(0, STAT_AMMO, currentammo); CL_SetStatInt(0, STAT_SHELLS, shells); CL_SetStatInt(0, STAT_NAILS, nails); CL_SetStatInt(0, STAT_ROCKETS, rockets); CL_SetStatInt(0, STAT_CELLS, cells); CL_SetStatInt(0, STAT_ACTIVEWEAPON, activeweapon); } if (CPNQ_IS_DP) { if (bits & DPSU_VIEWZOOM) { if (cls.protocol_nq >= CPNQ_DP5) i = (unsigned short) MSG_ReadShort(); else i = MSG_ReadByte(); if (i < 2) i = 2; CL_SetStatFloat(0, STAT_VIEWZOOM, i*(STAT_VIEWZOOM_SCALE/255.0)); } else CL_SetStatFloat(0, STAT_VIEWZOOM, STAT_VIEWZOOM_SCALE); } } #endif /* ================== CL_ParseSoundlist ================== */ static void CL_ParseSoundlist (qboolean lots) { int numsounds; char *str; int n; // precache sounds // memset (cl.sound_precache, 0, sizeof(cl.sound_precache)); if (lots) numsounds = MSG_ReadShort(); else numsounds = (cl.protocol_qw>=26)?MSG_ReadByte():0; for (;;) { str = MSG_ReadString (); if (!str[0]) break; numsounds++; if (numsounds >= MAX_PRECACHE_SOUNDS) Host_EndGame ("Server sent too many sound_precache"); // if (strlen(str)>4) // if (!strcmp(str+strlen(str)-4, ".mp3")) //don't let the server send us a specific mp3. convert it to wav and this way we know not to look outside the quake path for it. // strcpy(str+strlen(str)-4, ".wav"); Z_StrDupPtr(&cl.sound_name[numsounds], str); } n = (cl.protocol_qw>=26)?MSG_ReadByte():0; if (n) { if (cls.demoplayback == DPB_MVD && (cls.demoeztv_ext&EZTV_DOWNLOAD)) ; else { if (CL_RemoveClientCommands("soundlist") && !(cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS)) Con_DPrintf("Multiple soundlists\n"); // CL_SendClientCommand("soundlist %i %i", cl.servercount, n); CL_SendClientCommand(true, soundlist_name, cl.servercount, (numsounds&0xff00) + n); } return; } #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2) { CL_AllowIndependantSendCmd(false); //stop it now, the indep stuff *could* require model tracing. cl.sendprespawn = true; SCR_SetLoadingFile("loading data"); } else #endif { if (cls.demoplayback == DPB_MVD && cls.demoeztv_ext) { if (CL_RemoveClientCommands("qtvmodellist")) Con_DPrintf("Multiple modellists\n"); CL_SendClientCommand (true, "qtvmodellist %i 0", cl.servercount); } else { if (CL_RemoveClientCommands("modellist") && !(cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS)) Con_DPrintf("Multiple modellists\n"); // CL_SendClientCommand ("modellist %i 0", cl.servercount); CL_SendClientCommand (true, modellist_name, cl.servercount, 0); } } } /* ================== CL_ParseModellist ================== */ static void CL_ParseModellist (qboolean lots) { int nummodels; char *str; int n; // precache models and note certain default indexes if (lots) nummodels = MSG_ReadShort(); else nummodels = (cl.protocol_qw>=26)?MSG_ReadByte():0; for (;;) { str = MSG_ReadString (); if (!str[0]) break; nummodels++; if (nummodels>=MAX_PRECACHE_MODELS) Host_EndGame ("Server sent too many model_precache"); Z_StrDupPtr(&cl.model_name[nummodels], str); if (nummodels==1) SCR_ImageName(cl.model_name[nummodels]); //qw has a special network protocol for spikes. if (!strcmp(cl.model_name[nummodels],"progs/spike.mdl")) cl_spikeindex = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/player.mdl")) cl_playerindex = nummodels; #ifdef HAVE_LEGACY if (cl.model_name_vwep[0] && !strcmp(cl.model_name[nummodels],cl.model_name_vwep[0]) && cl_playerindex == -1) cl_playerindex = nummodels; #endif if (!strcmp(cl.model_name[nummodels],"progs/h_player.mdl")) cl_h_playerindex = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/flag.mdl")) cl_flagindex = nummodels; //rocket to grenade if (!strcmp(cl.model_name[nummodels],"progs/missile.mdl")) cl_rocketindex = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/grenade.mdl")) cl_grenadeindex = nummodels; //cl_gibfilter if (!strcmp(cl.model_name[nummodels],"progs/gib1.mdl")) cl_gib1index = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/gib2.mdl")) cl_gib2index = nummodels; if (!strcmp(cl.model_name[nummodels],"progs/gib3.mdl")) cl_gib3index = nummodels; //we have the names, we might as well START loading them now. if (COM_HasWorkers(WG_LOADER)) Mod_ForName (cl.model_name[nummodels], MLV_SILENT); } n = (cl.protocol_qw>=26)?MSG_ReadByte():0; if (n) { if (cls.demoplayback == DPB_MVD && cls.demoeztv_ext) ; else { if (CL_RemoveClientCommands("modellist") && !(cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS)) Con_DPrintf("Multiple modellists\n"); // CL_SendClientCommand("modellist %i %i", cl.servercount, n); CL_SendClientCommand(true, modellist_name, cl.servercount, (nummodels&0xff00) + n); } return; } #ifdef QUAKESTATS if (cls.demoplayback == DPB_MVD && !cl.model_name_vwep[0] && !(cls.fteprotocolextensions2&PEXT2_REPLACEMENTDELTAS)) CL_ParseStuffCmd("//vwep vwplayer w_axe w_shot w_shot2 w_nail w_nail2 w_rock w_rock2 w_light\n", 0); #endif SCR_SetLoadingFile("loading data"); //we need to try to load it now if we can, so any embedded archive will be loaded *before* we start looking for other content... cl.model_precache[1] = cl.model_name[1]?Mod_ForName (cl.model_name[1], MLV_SILENTSYNC):NULL; if (cl.model_precache[1] && cl.model_precache[1]->loadstate == MLS_LOADED) FS_LoadMapPackFile(cl.model_precache[1]->name, cl.model_precache[1]->archive); Sound_CheckDownloads(); Model_CheckDownloads(); CL_AllowIndependantSendCmd(false); //stop it now, the indep stuff *could* require model tracing. //set the flag to load models and send prespawn cl.sendprespawn = true; } #ifdef Q2CLIENT static void CLQ2_ParseClientinfo(int i, char *s) { char *model, *name; player_info_t *player; //s contains "name\model/skin" //q2 doesn't really do much with userinfos. if (i >= MAX_CLIENTS) return; player = &cl.players[i]; InfoBuf_Clear(&player->userinfo, true); cl.players[i].userinfovalid = true; model = strchr(s, '\\'); if (model) { *model = '\0'; model++; name = s; } else { name = "Unnammed"; model = "male"; } #if 0 skin = strchr(model, '/'); if (skin) { *skin = '\0'; skin++; } else skin = ""; InfoBuf_SetValueForKey(&player->userinfo, "model", model); InfoBuf_SetValueForKey(&player->userinfo, "skin", skin); #else InfoBuf_SetValueForKey(&player->userinfo, "skin", model); #endif InfoBuf_SetValueForKey(&player->userinfo, "name", name); cl.players[i].userid = i; cl.players[i].rbottomcolor = 1; cl.players[i].rtopcolor = 1; CL_ProcessUserInfo (i, player); } void CLQ2EX_ParseLightConfigString(int i, const char *s); static void CLQ2_UpdateConfigString (unsigned int i, char *s) { if (i >= 0x8000 && i < 0x8000+MAX_PRECACHE_MODELS) { i -= 0x8000; goto parsemodelindex; } else if (i >= 0xc000 && i < 0xc000+MAX_PRECACHE_SOUNDS) { i -= 0xc000; goto parsesoundindex; } if (cls.protocol_q2 != PROTOCOL_VERSION_Q2EX) { //remap from vanilla to q2e #define PASTE(a,b) (a##b) #define REMAPR(n,l) if (i >= Q2CS_##n && i < Q2CS_##n+Q2MAX_##l) i = i-Q2CS_##n+Q2EXCS_##n; else #define REMAPS(n) if (i == PASTE(Q2CS_,n)) i = i-PASTE(Q2CS_,n)+PASTE(Q2EXCS_,n); else #define Q2MAX_STATUSBAR (Q2CS_AIRACCEL-Q2CS_STATUSBAR) REMAPS(NAME) REMAPS(CDTRACK) REMAPS(SKY) REMAPS(SKYAXIS) REMAPS(SKYROTATE) REMAPR(STATUSBAR, STATUSBAR) REMAPS(AIRACCEL) REMAPS(MAXCLIENTS) REMAPS(MAPCHECKSUM) REMAPR(MODELS, MODELS) REMAPR(SOUNDS, SOUNDS) REMAPR(IMAGES, IMAGES) REMAPR(LIGHTS, LIGHTSTYLES) REMAPR(ITEMS, ITEMS) REMAPR(PLAYERSKINS, CLIENTS) REMAPR(GENERAL, GENERAL) Host_EndGame ("configstring %i > Q2MAX_CONFIGSTRINGS", i); } if ((unsigned int)i >= Q2EXMAX_CONFIGSTRINGS) Host_EndGame ("configstring %i > Q2EXMAX_CONFIGSTRINGS", i); // strncpy (olds, cl.configstrings[i], sizeof(olds)); // olds[sizeof(olds) - 1] = 0; // strcpy (cl.configstrings[i], s); // do something apropriate if (i == Q2EXCS_NAME) { Q_strncpyz (cl.levelname, s, sizeof(cl.levelname)); } else if (i == Q2EXCS_SKY) R_SetSky(s); else if (i == Q2EXCS_SKYAXIS || i == Q2EXCS_SKYROTATE) { if (i == Q2EXCS_SKYROTATE) { s = COM_Parse(s); cl.skyrotate = atof(com_token); s = COM_Parse(s); if (*com_token) cl.skyautorotate = atoi(com_token); } else { s = COM_Parse(s); if (s) { cl.skyaxis[0] = atof(com_token); s = COM_Parse(s); if (s) { cl.skyaxis[1] = atof(com_token); s = COM_Parse(s); if (s) cl.skyaxis[2] = atof(com_token); } } } if (cl.skyrotate) { if (cl.skyaxis[0]||cl.skyaxis[1]||cl.skyaxis[2]) Cvar_LockFromServer(&r_skybox_orientation, va("%g %g %g %g", cl.skyaxis[0], cl.skyaxis[1], cl.skyaxis[2], cl.skyrotate)); else Cvar_LockFromServer(&r_skybox_orientation, va("0 0 1 %g", cl.skyrotate)); } else Cvar_LockFromServer(&r_skybox_orientation, ""); Cvar_LockFromServer(&r_skybox_autorotate, va("%i", cl.skyautorotate)); } else if (i == Q2EXCS_STATUSBAR) { Q_strncpyz(cl.q2statusbar, s, sizeof(cl.q2statusbar)); } else if (i > Q2EXCS_STATUSBAR && i < Q2EXCS_AIRACCEL) ; //trailing statusbar else if (i == Q2EXCS_MAXCLIENTS) { i = atoi(s); if (i > 1) cl.allocated_client_slots = i; } else if (i >= Q2EXCS_LIGHTS && i < Q2EXCS_LIGHTS+Q2EXMAX_LIGHTSTYLES) { R_UpdateLightStyle(i-Q2EXCS_LIGHTS, s, 1, 1, 1); } else if (i >= Q2EXCS_RTLIGHTS && i < Q2EXCS_RTLIGHTS+Q2EXMAX_RTLIGHTS) { i -= Q2EXCS_RTLIGHTS; CLQ2EX_ParseLightConfigString(i, s); } else if (i == Q2EXCS_CDTRACK) { Media_NamedTrack (s, NULL); } else if (i == Q2EXCS_AIRACCEL) Q_strncpyz(cl.q2airaccel, s, sizeof(cl.q2airaccel)); else if (i >= Q2EXCS_MODELS && i < Q2EXCS_MODELS+Q2EXMAX_MODELS) { i-= Q2EXCS_MODELS; parsemodelindex: if ((unsigned int)i >= countof(cl.model_name)) return; if (*s == '/') s++; //*sigh* if (i == 255) s = "*playermodel"; //something special. Z_StrDupPtr(&cl.model_name[i], s); if (cl.model_name[i][0] == '#') { if (cl.numq2visibleweapons < Q2MAX_VISIBLE_WEAPONS) { cl.q2visibleweapons[cl.numq2visibleweapons] = cl.model_name[i]+1; cl.numq2visibleweapons++; } cl.model_precache[i] = NULL; } else if (cl.contentstage) cl.model_precache[i] = Mod_ForName (cl.model_name[i], MLV_WARN); } else if (i >= Q2EXCS_SOUNDS && i < Q2EXCS_SOUNDS+Q2MAX_SOUNDS) { i-= Q2EXCS_SOUNDS; parsesoundindex: if ((unsigned int)i >= countof(cl.sound_name)) return; if (*s == '/') s++; //*sigh* Z_StrDupPtr(&cl.sound_name[i], s); if (cl.contentstage) cl.sound_precache[i] = S_PrecacheSound (s); } else if (i >= Q2EXCS_IMAGES && i < Q2EXCS_IMAGES+Q2MAX_IMAGES) { i -= Q2EXCS_IMAGES; if ((unsigned int)i >= countof(cl.image_name)) return; Z_StrDupPtr(&cl.image_name[i], s); } else if (i >= Q2EXCS_ITEMS && i < Q2EXCS_ITEMS+Q2MAX_ITEMS) { i -= Q2EXCS_ITEMS; if ((unsigned int)i >= countof(cl.item_name)) return; Z_StrDupPtr(&cl.item_name[i], s); } else if (i >= Q2EXCS_GENERAL && i < Q2EXCS_GENERAL+Q2EXMAX_GENERAL) { i -= Q2EXCS_GENERAL; if ((unsigned int)i >= Q2MAX_CLIENTS)//countof(cl.configstring_general)) return; Z_StrDupPtr(&cl.configstring_general[i], s); } else if (i >= Q2EXCS_PLAYERSKINS && i < Q2EXCS_PLAYERSKINS+Q2EXMAX_CLIENTS) { i -= Q2EXCS_GENERAL; i += Q2EXMAX_CLIENTS; if ((unsigned int)i >= countof(cl.configstring_general)) return; Z_StrDupPtr(&cl.configstring_general[i], s); CLQ2_ParseClientinfo (i, s); } else if (i == Q2EXCS_MAPCHECKSUM) { int serverchecksum = (int)strtol(s, NULL, 10); if (cl.worldmodel) { if (cl.worldmodel->loadstate == MLS_LOADING) COM_WorkerPartialSync(cl.worldmodel, &cl.worldmodel->loadstate, MLS_LOADING); // the Q2 client normally exits here, however for our purposes we might as well ignore it if (cl.worldmodel->checksum != serverchecksum && cl.worldmodel->checksum2!= serverchecksum) Con_Printf(CON_WARNING "WARNING: Client checksum does not match server checksum (%i != %i)", cl.worldmodel->checksum2, serverchecksum); } cl.q2mapchecksum = serverchecksum; } else if (i >= Q2ECS_WHEEL_WEAPONS && i < Q2ECS_WHEEL_WEAPONS+Q2EXMAX_WWHEEL) ; else if (i >= Q2ECS_WHEEL_AMMO && i < Q2ECS_WHEEL_AMMO+Q2EXMAX_WWHEEL) ; else if (i >= Q2ECS_WHEEL_POWERUPS && i < Q2ECS_WHEEL_POWERUPS+Q2EXMAX_WWHEEL) ; else if (i == Q2ECS_CD_LOOP_COUNT) ; else if (i == Q2ECS_GAME_STYLE) ; #define Q2EXCS_SOUNDS (Q2EXCS_MODELS +Q2EXMAX_MODELS) #define Q2EXCS_IMAGES (Q2EXCS_SOUNDS +Q2EXMAX_SOUNDS) #define Q2EXCS_LIGHTS (Q2EXCS_IMAGES +Q2EXMAX_IMAGES) #define Q2EXCS_RTLIGHTS (Q2EXCS_LIGHTS +Q2EXMAX_LIGHTSTYLES) else Con_Printf(CON_WARNING"Config string %i unsupported\n", i); } static void CLQ2_ParseConfigString (void) { unsigned int i = MSG_ReadUInt16(); char *s = MSG_ReadString(); CLQ2_UpdateConfigString(i, s); } #endif qboolean CL_CheckBaselines (int size) { int i; if (size < 0) return false; if (size > MAX_EDICTS) return false; size = (size + 64) & ~63; // round up to next 64 if (size <= cl_baselines_count) return true; cl_baselines = BZ_Realloc(cl_baselines, sizeof(*cl_baselines)*size); for (i = cl_baselines_count; i < size; i++) { memcpy(cl_baselines + i, &nullentitystate, sizeof(*cl_baselines)); if (cls.protocol == CP_NETQUAKE && cls.protocol_nq == CPNQ_H2MP) cl_baselines[i].hexen2flags = 0; } cl_baselines_count = size; return true; } /* ================== CL_ParseBaseline ================== */ static void CL_ParseBaseline (entity_state_t *es, int baselinetype2) { int i; unsigned int bits; memcpy(es, &nullentitystate, sizeof(entity_state_t)); if (baselinetype2 == CPNQ_FITZ666) bits = MSG_ReadByte(); //fitzquake has actual flags. yay extensibility. just a shame they're not the same as other entity updates. else if (baselinetype2 >= CPNQ_DP5 && baselinetype2 <= CPNQ_DP7) bits = FITZ_B_LARGEMODEL|FITZ_B_LARGEFRAME; //dp's baseline2 always has these (regular baseline is unmodified) else if (cls.protocol == CP_NETQUAKE && CPNQ_IS_BJP) bits = FITZ_B_LARGEMODEL; //bjp always uses shorts for models. else if (cls.protocol == CP_NETQUAKE && cls.protocol_nq == CPNQ_H2MP) bits = FITZ_B_LARGEMODEL; //urgh else bits = 0; //vanilla nq or qw es->modelindex = (bits & FITZ_B_LARGEMODEL) ? (unsigned short)MSG_ReadShort() : MSG_ReadByte(); es->frame = (bits & FITZ_B_LARGEFRAME) ? (unsigned short)MSG_ReadShort() : MSG_ReadByte(); es->colormap = MSG_ReadByte(); es->skinnum = MSG_ReadByte(); if (cls.protocol == CP_NETQUAKE && cls.protocol_nq == CPNQ_H2MP) { es->scale = (MSG_ReadByte()/100.0)*16; es->hexen2flags = MSG_ReadByte(); es->abslight = MSG_ReadByte(); } for (i=0 ; i<3 ; i++) { es->origin[i] = MSG_ReadCoord (); es->angles[i] = MSG_ReadAngle (); } es->trans = (bits & FITZ_B_ALPHA) ? MSG_ReadByte() : 255; #ifdef NQPROT if (cls.qex) { if (bits & QEX_B_SOLID) /*es->solidtype =*/ MSG_ReadByte(); if (bits & QEX_B_UNKNOWN4) Con_Printf(CON_WARNING"QEX_B_UNKNOWN4: %x\n", MSG_ReadByte()); if (bits & QEX_B_UNKNOWN5) Con_Printf(CON_WARNING"QEX_B_UNKNOWN5: %x\n", MSG_ReadByte()); if (bits & QEX_B_UNKNOWN6) Con_DPrintf(CON_WARNING"QEX_B_UNKNOWN6: %x\n", MSG_ReadByte()); if (bits & QEX_B_UNKNOWN7) Con_Printf(CON_WARNING"QEX_B_UNKNOWN7: %x\n", MSG_ReadByte()); } else #endif es->scale = (bits & RMQFITZ_B_SCALE) ? MSG_ReadByte() : 16; } static void CL_ParseBaselineDelta (void) { entity_state_t es; if (cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) CLFTE_ParseBaseline(&es, true); else CLQW_ParseDelta(&nullentitystate, &es, (unsigned short)MSG_ReadShort()); if (!CL_CheckBaselines(es.number)) Host_EndGame("CL_ParseBaselineDelta: check baselines failed with size %i", es.number); memcpy(cl_baselines + es.number, &es, sizeof(es)); } #ifdef Q2CLIENT static void CLQ2_Precache_f (void) { Model_CheckDownloads(); Sound_CheckDownloads(); cl.contentstage = 0; cl.sendprespawn = true; SCR_SetLoadingFile("loading data"); } #endif /* ===================== CL_ParseStatic Static entities are non-interactive world objects like torches ===================== */ void R_StaticEntityToRTLight(int i); static void CL_ParseStaticProt (int baselinetype) { entity_t *ent; int i; entity_state_t es; vec3_t mins,maxs; if (baselinetype >= 0) { CL_ParseBaseline(&es, baselinetype); i = cl.num_statics; cl.num_statics++; } else { //new deltaed style ('full' extension support) if (cls.fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) CLFTE_ParseBaseline(&es, false); else CLQW_ParseDelta(&nullentitystate, &es, (unsigned short)MSG_ReadShort()); if (!es.number) i = cl.num_statics++; else { es.number+=MAX_EDICTS; for (i = 0; i < cl.num_statics; i++) if (cl_static_entities[i].ent.keynum == es.number) { pe->DelinkTrailstate (&cl_static_entities[i].emit); break; } if (i == cl.num_statics) cl.num_statics++; } } if (i == cl_max_static_entities) { cl_max_static_entities += 16; cl_static_entities = BZ_Realloc(cl_static_entities, sizeof(*cl_static_entities)*cl_max_static_entities); } cl_static_entities[i].mdlidx = es.modelindex; cl_static_entities[i].emit = trailkey_null; cl_static_entities[i].state = es; ent = &cl_static_entities[i].ent; V_ClearEntity(ent); memset(&cl_static_entities[i].ent.pvscache, 0, sizeof(cl_static_entities[i].ent.pvscache)); ent->keynum = es.number; // copy it to the current state ent->model = cl.model_precache[es.modelindex]; memset(&ent->framestate, 0, sizeof(ent->framestate)); ent->framestate.g[FS_REG].frame[0] = ent->framestate.g[FS_REG].frame[1] = es.frame; ent->framestate.g[FS_REG].lerpweight[0] = 1; ent->skinnum = es.skinnum; #ifdef HEXEN2 ent->drawflags = es.hexen2flags; ent->abslight = es.abslight; #endif #ifdef PEXT_SCALE ent->scale = es.scale/16.0; #endif ent->glowmod[0] = (8.0f/256.0f)*es.glowmod[0]; ent->glowmod[1] = (8.0f/256.0f)*es.glowmod[1]; ent->glowmod[2] = (8.0f/256.0f)*es.glowmod[2]; ent->shaderRGBAf[0] = (8.0f/256.0f)*es.colormod[0]; ent->shaderRGBAf[1] = (8.0f/256.0f)*es.colormod[1]; ent->shaderRGBAf[2] = (8.0f/256.0f)*es.colormod[2]; ent->fatness = es.fatness/16.0; ent->flags = 0; if (es.dpflags & RENDER_VIEWMODEL) ent->flags |= RF_WEAPONMODEL|Q2RF_MINLIGHT|RF_DEPTHHACK; if (es.dpflags & RENDER_EXTERIORMODEL) ent->flags |= RF_EXTERNALMODEL; if (es.effects & NQEF_ADDITIVE) ent->flags |= RF_ADDITIVE; if (es.effects & EF_NODEPTHTEST) ent->flags |= RF_NODEPTHTEST; if (es.effects & EF_NOSHADOW) ent->flags |= RF_NOSHADOW; if (es.trans < 0xfe) { ent->shaderRGBAf[3] = es.trans/(float)0xfe; ent->flags |= RF_TRANSLUCENT; } else ent->shaderRGBAf[3] = 1.0; VectorCopy (es.origin, ent->origin); VectorCopy (es.angles, ent->angles); if (ent->model && ent->model->type == mod_alias) AngleVectorsMesh(es.angles, ent->axis[0], ent->axis[1], ent->axis[2]); else AngleVectors(es.angles, ent->axis[0], ent->axis[1], ent->axis[2]); VectorInverse(ent->axis[1]); if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED) return; if (ent->model) { //FIXME: wait for model to load so we know the correct size? /*FIXME: compensate for angle*/ VectorAdd(es.origin, ent->model->mins, mins); VectorAdd(es.origin, ent->model->maxs, maxs); } else { VectorCopy(es.origin, mins); VectorCopy(es.origin, maxs); } cl.worldmodel->funcs.FindTouchedLeafs(cl.worldmodel, &cl_static_entities[i].ent.pvscache, mins, maxs); #ifdef RTLIGHTS //and now handle any rtlight fields on it R_StaticEntityToRTLight(i); #endif } /* =================== CL_ParseStaticSound =================== */ static void CL_ParseStaticSound (unsigned int flags) { extern cvar_t cl_staticsounds; vec3_t org; size_t sound_num; float vol, atten; int i; if (flags & ~(1)) Host_EndGame("CL_ParseStaticSound: unsupported flags & %x\n", flags&~(1)); for (i=0 ; i<3 ; i++) org[i] = MSG_ReadCoord (); if (flags || (cls.protocol == CP_NETQUAKE && (cls.protocol_nq == CPNQ_BJP2 || cls.protocol_nq == CPNQ_H2MP))) { if (cls.fteprotocolextensions2&PEXT2_LERPTIME) sound_num = (unsigned short)MSG_ReadULEB128(); else sound_num = (unsigned short)MSG_ReadShort(); } else sound_num = MSG_ReadByte (); vol = MSG_ReadByte ()/255.0; atten = MSG_ReadByte ()/64.0; if (sound_num >= countof(cl.sound_precache)) return; //no crashing, please. vol *= cl_staticsounds.value; if (vol < 0) return; S_StaticSound (cl.sound_precache[sound_num], org, vol, atten); } /* ===================================================================== ACTION MESSAGES ===================================================================== */ /* ================== CL_ParseStartSoundPacket ================== */ static void CLQW_ParseStartSoundPacket(void) { vec3_t pos; int channel, ent; int sound_num; int volume; float attenuation; int i; channel = MSG_ReadShort(); if (channel & QWSND_VOLUME) volume = MSG_ReadByte (); else volume = DEFAULT_SOUND_PACKET_VOLUME; if (channel & QWSND_ATTENUATION) attenuation = MSG_ReadByte () / 64.0; else attenuation = DEFAULT_SOUND_PACKET_ATTENUATION; sound_num = MSG_ReadByte (); for (i=0 ; i<3 ; i++) pos[i] = MSG_ReadCoord (); ent = (channel>>3)&1023; channel &= 7; if (ent > MAX_EDICTS) Host_EndGame ("CL_ParseStartSoundPacket: ent = %i", ent); #ifdef CSQC_DAT if (!CSQC_StartSound(ent, channel, cl.sound_name[sound_num], pos, volume/255.0, attenuation, 1, 0, 0)) #endif { if (!sound_num) S_StopSound(ent, channel); else S_StartSound (ent, channel, cl.sound_precache[sound_num], pos, NULL, volume/255.0, attenuation, 0, 0, 0); } #ifdef QUAKESTATS for (i = 0; i < cl.splitclients; i++) { if (ent == cl.playerview[i].playernum+1) { TP_CheckPickupSound(cl.sound_name[sound_num], pos, i); return; } } TP_CheckPickupSound(cl.sound_name[sound_num], pos, -1); #endif } #ifdef Q2CLIENT static void CLQ2_ParseStartSoundPacket(void) { vec3_t pos; int channel, ent; int sound_num; float volume; float attenuation; int flags; float ofs; sfx_t *sfx; flags = MSG_ReadByte (); if (flags & Q2SND_EXTRABITS) flags |= MSG_ReadByte ()<<8; if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX || ((flags & Q2SNDFTE_LARGEIDX) && (cls.fteprotocolextensions & PEXT_SOUNDDBL))) sound_num = MSG_ReadUInt16(); else sound_num = MSG_ReadByte (); if (flags & Q2SND_VOLUME) volume = MSG_ReadByte () / 255.0; else volume = Q2DEFAULT_SOUND_PACKET_VOLUME; if (flags & Q2SND_ATTENUATION) attenuation = MSG_ReadByte () / 64.0; else attenuation = Q2DEFAULT_SOUND_PACKET_ATTENUATION; if (flags & Q2SND_OFFSET) ofs = MSG_ReadByte () / 1000.0; else ofs = 0; if (flags & Q2SND_ENT) { // entity reletive if (cls.protocol_q2 == PROTOCOL_VERSION_Q2EX && (flags & Q2SNDEX_LARGEENT)) channel = MSG_ReadLong(); else channel = MSG_ReadShort(); ent = channel>>3; if (ent > MAX_EDICTS) Host_EndGame ("CL_ParseStartSoundPacket: ent = %i", ent); channel &= 7; } else { ent = 0; channel = 0; } if (flags & Q2SND_POS) { // positioned in space MSG_ReadPos (pos); //FIXME // if (!(flags & Q2SNDEX_EXPLICITPOS)) // CL_GetNumberedEntityInfo(ent, pos, NULL); } else // use entity number { CL_GetNumberedEntityInfo(ent, pos, NULL); } if (!cl.sound_precache[sound_num]) return; sfx = cl.sound_precache[sound_num]; if (sfx->name[0] == '*') { //a 'sexed' sound if (ent > 0 && ent <= MAX_CLIENTS) { char *model = InfoBuf_ValueForKey(&cl.players[ent-1].userinfo, "skin"); char *skin; skin = strchr(model, '/'); if (skin) *skin = '\0'; if (*model) sfx = S_PrecacheSound(va("players/%s/%s", model, cl.sound_precache[sound_num]->name+1)); } //fall back to male if it failed to load. //note: threaded loading can still make it silent the first time we hear it. if (sfx->loadstate == SLS_FAILED) sfx = S_PrecacheSound(va("players/male/%s", cl.sound_precache[sound_num]->name+1)); } S_StartSound (ent, channel, sfx, pos, NULL, volume, attenuation, ofs, 0, 0); } #endif //returns the player if they're not spectating. static int CL_TryTrackNum(playerview_t *pv) { if (pv->spectator && pv->cam_state != CAM_FREECAM && pv->cam_spec_track >= 0) return pv->cam_spec_track; return pv->playernum; } #if defined(NQPROT) || defined(PEXT_SOUNDDBL) static void CLNQ_ParseStartSoundPacket(void) { vec3_t pos, vel; int channel, ent; unsigned int sound_num; int volume; int field_mask; float attenuation; int i; float pitchadj; float timeofs; unsigned int flags; field_mask = MSG_ReadByte(); if (field_mask & FTESND_MOREFLAGS) field_mask |= MSG_ReadUInt64()<<8; if (field_mask & NQSND_VOLUME) volume = MSG_ReadByte (); else volume = DEFAULT_SOUND_PACKET_VOLUME; if (field_mask & NQSND_ATTENUATION) attenuation = MSG_ReadByte () / 64.0; else attenuation = DEFAULT_SOUND_PACKET_ATTENUATION; if (field_mask & FTESND_PITCHADJ) pitchadj = MSG_ReadByte()/100.0; else pitchadj = 1; if (field_mask & FTESND_TIMEOFS) timeofs = MSG_ReadShort() / 1000.0; else timeofs = 0; if (field_mask & FTESND_VELOCITY) { vel[0] = MSG_ReadShort()/8.0; vel[1] = MSG_ReadShort()/8.0; vel[2] = MSG_ReadShort()/8.0; } else VectorClear(vel); if (field_mask & DPSND_SPEEDUSHORT4000) pitchadj = (unsigned short)MSG_ReadShort() / 4000.0; flags = field_mask>>8; flags &= CF_NETWORKED; if (field_mask & NQSND_LARGEENTITY) { ent = MSGCL_ReadEntity(); channel = MSG_ReadByte(); } else { //regular channel = MSG_ReadShort (); ent = channel >> 3; channel &= 7; } //channel = (channel & 7) | ((channel & 0x0f1) << 1); //this line undoes the reliable=(channel&8) gap from qwssqc... but frankly just pass the flags arg properly. csqc's builtin doesn't use it, so don't give an inconsistent gap at all here. if ((field_mask & NQSND_LARGESOUND) || (cls.protocol == CP_NETQUAKE && (cls.protocol_nq == CPNQ_BJP2 || cls.protocol_nq == CPNQ_BJP3))) //bjp kinda sucks sound_num = (unsigned short)MSG_ReadShort(); else sound_num = (unsigned char)MSG_ReadByte (); for (i=0 ; i<3 ; i++) pos[i] = MSG_ReadCoord (); if (ent > MAX_EDICTS) Host_EndGame ("CL_ParseStartSoundPacket: ent = %i", ent); if (sound_num >= MAX_PRECACHE_SOUNDS) Host_EndGame ("CL_ParseStartSoundPacket: sndidx = %i", sound_num); if (!cl.sound_name[sound_num]) return; //nope, not precached yet... silly raqces. #ifdef CSQC_DAT if (!CSQC_StartSound(ent, channel, cl.sound_name[sound_num], pos, volume/255.0, attenuation, pitchadj, timeofs, flags)) #endif { if (!sound_num) S_StopSound(ent, channel); else S_StartSound (ent, channel, cl.sound_precache[sound_num], pos, vel, volume/255.0, attenuation, timeofs, pitchadj, flags); } #ifdef QUAKESTATS for (i = 0; i < cl.splitclients; i++) { if (ent == cl.playerview[i].playernum+1) { TP_CheckPickupSound(cl.sound_name[sound_num], pos, i); return; } } TP_CheckPickupSound(cl.sound_name[sound_num], pos, -1); #endif } #endif /* ================== CL_ParseClientdata Server information pertaining to this client only, sent every frame ================== */ void CL_ParseClientdata (void) { int i; // calculate simulated time of message oldparsecountmod = parsecountmod; i = cls.netchan.incoming_acknowledged; #ifdef NQPROT if (cls.demoplayback == DPB_NETQUAKE) { i = cls.netchan.incoming_sequence-1; cl.oldparsecount = i - 1; oldparsecountmod = cl.oldparsecount & UPDATE_MASK; } else #endif if (cls.demoplayback == DPB_MVD) { cl.oldparsecount = i - 1; oldparsecountmod = cl.oldparsecount & UPDATE_MASK; } cl.parsecount = i; parsecountmod = i&UPDATE_MASK; parsecounttime = realtime;//cl.outframes[i].senttime; if (cls.protocol == CP_QUAKEWORLD) CL_AckedInputFrame(cls.netchan.incoming_sequence, cl.parsecount, false); } #ifdef QWSKINS static qboolean CLQ2_PlayerSkinIsOkay(skinid_t id) { skinfile_t *sk = Mod_LookupSkin(id); if (!sk) //err... return false; if (sk->nummappings != 1 || *sk->mappings[0].surface) return true; //looks like its a custom skin, ignore it. return R_GetShaderSizes(sk->mappings[0].shader, NULL, NULL, true) > 0; } static int QDECL CLQ2_EnumeratedSkin(const char *name, qofs_t size, time_t mtime, void *ptr, searchpathfuncs_t *spath) { //follows the form of players/$MODELNAME/$SKINNAME_i.$EXT player_info_t *player = ptr; if (!player->skinid) { char *e; e = strstr(name, "_i."); if (e) { *e = 0; player->skinid = Mod_ReadSkinFile(va("%s.skin", name), va("replace \"\" \"%s.pcx\"", name)); } } return true; } /* ===================== CL_NewTranslation ===================== */ void CL_NewTranslation (int slot) { int top, bottom; int local; qboolean mayforce; char *s; player_info_t *player; if (slot >= MAX_CLIENTS) Host_Error ("CL_NewTranslation: slot > MAX_CLIENTS"); player = &cl.players[slot]; if (cls.protocol == CP_QUAKE2) { char *mod, *skin, *dogtag; player->qwskin = NULL; player->skinid = 0; player->model = NULL; player->ttopcolor = TOP_DEFAULT; player->tbottomcolor = BOTTOM_DEFAULT; mod = InfoBuf_ValueForKey(&player->userinfo, "skin"); skin = strchr(mod, '/'); if (skin) { *skin++ = 0; dogtag = strchr(skin, '\\'); if (dogtag) *dogtag++ = 0; } if (!mod || !*mod) mod = "male"; if (!skin || !*skin || !COM_FCheckExists(va("players/%s/%s.pcx", mod, skin))) skin = "grunt"; player->model = Mod_ForName(va("players/%s/tris.md2", mod), MLV_WARNSYNC); if (player->model->loadstate == MLS_FAILED && strcmp(mod, "male")) { //fall back on male if the model doesn't exist. yes, sexist, but also statistically most likely to represent the actual player. mod = "male"; player->model = Mod_ForName(va("players/%s/tris.md2", mod), 0); } player->skinid = Mod_RegisterSkinFile(va("players/%s/%s.skin", mod,skin)); if (!player->skinid) player->skinid = Mod_ReadSkinFile(va("players/%s/%s.skin", mod,skin), va("replace \"\" \"players/%s/%s.pcx\"", mod,skin)); if (!CLQ2_PlayerSkinIsOkay(player->skinid)) { player->skinid = 0; COM_EnumerateFiles(va("players/%s/*_i.*", mod), CLQ2_EnumeratedSkin, player); } return; } mayforce = !(cl.fpd & FPD_NO_FORCE_COLOR); #if MAX_SPLITS > 1 if (mayforce && cl.splitclients > 1 && cl.teamplay) { //if we're using splitscreen, only allow team/enemy forcing if all split clients are on the same team char *needteam; int i; needteam = cl.players[CL_TryTrackNum(&cl.playerview[0])].team; for (i = 1; i < cl.splitclients; i++) { if (strcmp(needteam, cl.players[CL_TryTrackNum(&cl.playerview[i])].team)) { mayforce = false; break; } } } #endif s = Skin_FindName (player); COM_StripExtension(s, s, MAX_QPATH); if (player->qwskin && stricmp(s, player->qwskin->name)) player->qwskin = NULL; player->skinid = 0; player->model = NULL; top = player->rtopcolor; bottom = player->rbottomcolor; if (mayforce) { local = CL_TryTrackNum(&cl.playerview[0]); if ((cl.teamplay || cls.protocol == CP_NETQUAKE) && !strcmp(player->team, cl.players[local].team)) { if (cl_teamtopcolor != ~0) top = cl_teamtopcolor; if (cl_teambottomcolor != ~0) bottom = cl_teambottomcolor; if (player->colourised) { if (player->colourised->topcolour != ~0) top = player->colourised->topcolour; if (player->colourised->bottomcolour != ~0) bottom = player->colourised->bottomcolour; } } else { if (cl_enemytopcolor != ~0) top = cl_enemytopcolor; if (cl_enemybottomcolor != ~0) bottom = cl_enemybottomcolor; } } /* if (top > 13 || top < 0) top = 13; if (bottom > 13 || bottom < 0) bottom = 13; */ //other renderers still need the team stuff set, but that's all player->ttopcolor = top; player->tbottomcolor = bottom; } #endif /* ============== CL_UpdateUserinfo ============== */ static void CL_ProcessUserInfo (int slot, player_info_t *player) { int i; char *col; int ospec = player->spectator; if (cls.protocol == CP_NETQUAKE) player->userid = slot; Q_strncpyz (player->name, InfoBuf_ValueForKey (&player->userinfo, "name"), sizeof(player->name)); Q_strncpyz (player->team, InfoBuf_ValueForKey (&player->userinfo, "team"), sizeof(player->team)); Ruleset_Check(InfoBuf_ValueForKey (&player->userinfo, RULESET_USERINFO), player->ruleset, sizeof(player->ruleset)); col = InfoBuf_ValueForKey (&player->userinfo, "topcolor"); if (!strncmp(col, "0x", 2)) player->rtopcolor = 0xff000000|strtoul(col+2, NULL, 16); else player->rtopcolor = atoi(col); col = InfoBuf_ValueForKey (&player->userinfo, "bottomcolor"); if (!strncmp(col, "0x", 2)) player->rbottomcolor = 0xff000000|strtoul(col+2, NULL, 16); else player->rbottomcolor = atoi(col); i = atoi(InfoBuf_ValueForKey (&player->userinfo, "*spectator")); if (i == 2) player->spectator = 2; else if (i) player->spectator = true; else player->spectator = false; /* if (player->rtopcolor > 13) player->rtopcolor = 13; if (player->rbottomcolor > 13) player->rbottomcolor = 13; */ player->chatstate = atoi(InfoBuf_ValueForKey (&player->userinfo, "chat")); #ifdef HEXEN2 /*if we're running hexen2, they have to be some class...*/ player->h2playerclass = atoi(InfoBuf_ValueForKey (&player->userinfo, "cl_playerclass")); if (player->h2playerclass > 5) player->h2playerclass = 5; if (player->h2playerclass < 1) player->h2playerclass = 1; #endif #ifdef QWSKINS player->model = NULL; player->colourised = TP_FindColours(player->name); #endif // If it's us for (i = 0; i < cl.splitclients; i++) if (slot == cl.playerview[i].playernum) break; if (i < cl.splitclients && player->name[0]) { if (cl.playerview[i].spectator != player->spectator) { cl.playerview[i].spectator = player->spectator; for (i = 0; i < cl.splitclients; i++) { Cam_Unlock(&cl.playerview[i]); } CL_CheckServerInfo(); } // Update the rules since spectators can bypass everything but players can't else if (ospec != player->spectator) CL_CheckServerInfo(); Skin_FlushPlayers(); } #ifdef QWSKINS else if (cl.teamplay && cl.playerview[0].spectator && slot == Cam_TrackNum(&cl.playerview[0])) //skin forcing cares about the team of the guy we're tracking. Skin_FlushPlayers(); else if (cls.state >= ca_onserver) Skin_Find (player); CL_NewTranslation (slot); #endif Sbar_Changed (); CSQC_PlayerInfoChanged(slot); } /* ============== CL_UpdateUserinfo ============== */ static void CL_UpdateUserinfo (void) { unsigned int slot; player_info_t *player; slot = MSG_ReadPlayer(); if (slot >= MAX_CLIENTS) Host_EndGame ("CL_ParseServerMessage: svc_updateuserinfo > MAX_SCOREBOARD"); player = &cl.players[slot]; player->userid = MSG_ReadLong (); InfoBuf_FromString(&player->userinfo, MSG_ReadString(), false); player->userinfovalid = true; CL_ProcessUserInfo (slot, player); if (slot == cl.playerview[0].playernum && player->name[0]) { char *qz; qz = InfoBuf_ValueForKey(&player->userinfo, "Qizmo"); if (*qz) TP_ExecTrigger("f_qizmoconnect", false); } } static void CL_ParseSetInfoBlob (void) { unsigned int slot = MSG_ReadPlayer(); char *key = MSG_ReadString(); size_t keysize; unsigned int offset = MSG_ReadLong(); qboolean final = !!(offset & 0x80000000); unsigned short valsize = MSG_ReadShort(); char *val = BZ_Malloc(valsize); MSG_ReadData(val, valsize); offset &= ~0x80000000; key = InfoBuf_DecodeString(key, key+strlen(key), &keysize); if (slot-- == 0) { InfoBuf_SyncReceive(&cl.serverinfo, key, keysize, val, valsize, offset, final); if (final) CL_CheckServerInfo(); } else if (slot >= MAX_CLIENTS) Con_Printf("INVALID SETINFO %i: %s=%s\n", slot, key, val); else { player_info_t *player = &cl.players[slot]; if (offset) Con_DLPrintf(2,"SETINFO %s: %s+=%s\n", player->name, key, val); else Con_DLPrintf(strcmp(key, "chat")?1:2,"SETINFO %s: %s=%s\n", player->name, key, val); InfoBuf_SyncReceive(&player->userinfo, key, keysize, val, valsize, offset, final); player->userinfovalid = true; if (final) CL_ProcessUserInfo (slot, player); } Z_Free(key); Z_Free(val); } /* ============== CL_SetInfo ============== */ static void CL_ParseSetInfo (void) { unsigned int slot; player_info_t *player; char *val; char key[512]; slot = MSG_ReadPlayer (); MSG_ReadStringBuffer(key, sizeof(key)); val = MSG_ReadString(); if (slot >= MAX_CLIENTS) Con_Printf("INVALID SETINFO %i: %s=%s\n", slot, key, val); else { player = &cl.players[slot]; if (cl_shownet.value == 3) Con_Printf("\t%i(%s): %s=\"%s\"\n", slot, player->name, key, val); else Con_DLPrintf(strcmp(key, "chat")?1:2,"SETINFO %s: %s=%s\n", player->name, key, val); InfoBuf_SetStarKey(&player->userinfo, key, val); player->userinfovalid = true; CL_ProcessUserInfo (slot, player); } } /* ============== CL_ServerInfo ============== */ static void CL_ServerInfo (void) { // int slot; // player_info_t *player; char key[MAX_QWMSGLEN]; char value[MAX_QWMSGLEN]; Q_strncpyz (key, MSG_ReadString(), sizeof(key)); Q_strncpyz (value, MSG_ReadString(), sizeof(value)); if (cl_shownet.value == 3) Con_Printf("\t%s=%s\n", key, value); else Con_DPrintf("SERVERINFO: %s=%s\n", key, value); InfoBuf_SetStarKey(&cl.serverinfo, key, value); CL_CheckServerInfo(); } /* ===================== CL_SetStat ===================== */ static void CL_SetStat_Internal (int pnum, int stat, int ivalue, float fvalue) { if (cl.playerview[pnum].stats[stat] != ivalue) Sbar_Changed (); #ifdef QUAKESTATS if (stat == STAT_ITEMS) { // set flash times int j; for (j=0 ; j<32 ; j++) if ( (ivalue & (1<= MAX_CL_STATS) return; // Host_EndGame ("CL_SetStat: %i is invalid", stat); #ifdef QUAKESTATS if (stat == STAT_TIME && (cls.fteprotocolextensions & PEXT_ACCURATETIMINGS)) { cl.oldgametime = cl.gametime; cl.oldgametimemark = cl.gametimemark; cl.gametime = fvalue * 0.001; cl.gametimemark = realtime; } #endif if (cls.demoplayback == DPB_MVD) { extern int cls_lastto; cl.players[cls_lastto].stats[stat]=ivalue; cl.players[cls_lastto].statsf[stat]=fvalue; if (cl_shownet.value == 3) Con_Printf("\t%i: %i=%g\n", cls_lastto, stat, fvalue); for (pnum = 0; pnum < cl.splitclients; pnum++) if (cl.playerview[pnum].cam_spec_track == cls_lastto && cl.playerview[pnum].cam_state != CAM_FREECAM) CL_SetStat_Internal(pnum, stat, ivalue, fvalue); } else { unsigned int pl = cl.playerview[pnum].playernum; if (pl < MAX_CLIENTS) { cl.players[pl].stats[stat]=ivalue; cl.players[pl].statsf[stat]=fvalue; } if (cl_shownet.value == 3) Con_Printf("\t%i(%i): %i=%g\n", pnum, pl, stat, fvalue); CL_SetStat_Internal(pnum, stat, ivalue, fvalue); } #ifdef QUAKESTATS if (stat == STAT_VIEWHEIGHT && ((cls.z_ext & Z_EXT_VIEWHEIGHT) || cls.protocol == CP_NETQUAKE)) cl.playerview[pnum].viewheight = fvalue; #endif #ifdef NQPROT if (cls.protocol == CP_NETQUAKE && (CPNQ_IS_DP || (cls.fteprotocolextensions2 & PEXT2_PREDINFO))) { if (cls.fteprotocolextensions2 & PEXT2_PREDINFO) CL_SetStatMovevar(pnum, stat, ivalue, fvalue); else CL_SetStatMovevar(pnum, stat, ivalue, *(float*)&ivalue); //DP sucks. } #endif } static void CL_SetStatString (int pnum, int stat, const char *value) { if (stat < 0 || stat >= MAX_CL_STATS) return; // Host_EndGame ("CL_SetStat: %i is invalid", stat); if (cls.demoplayback == DPB_MVD) { extern int cls_lastto; //Z_Free(cl.players[cls_lastto].statsstr[stat]); //cl.players[cls_lastto].statsstr[stat]=Z_StrDup(value); for (pnum = 0; pnum < cl.splitclients; pnum++) if (cl.playerview[pnum].cam_spec_track == cls_lastto && cl.playerview[pnum].cam_state != CAM_FREECAM) { if (cl.playerview[pnum].statsstr[stat]) Z_Free(cl.playerview[pnum].statsstr[stat]); cl.playerview[pnum].statsstr[stat] = Z_StrDup(value); } } else { if (cl.playerview[pnum].statsstr[stat]) Z_Free(cl.playerview[pnum].statsstr[stat]); cl.playerview[pnum].statsstr[stat] = Z_StrDup(value); } } /* //if we're going to 'spend' another byte for longer indexes, we might as well spend an extra 4 bits on the type too, allowing for 64bit types etc. static void CL_ParseExtendedStat(int destsplit) { //float/double/sint/uint //string quint64_t id = MSG_ReadUInt64(); unsigned int type; type = id&0xf; id>>=4; //we're never going to have that many stats. switch(type) { case ev_void: //might as well. CL_SetStatNumeric(destsplit, id, 0, 0); break; case ev_string: CL_SetStatString(destsplit, id, MSG_ReadString()); break; case ev_float: { float f = MSG_ReadFloat(); CL_SetStatNumeric(destsplit, id, f, f); } break; case ev_vector: { float f; f = MSG_ReadFloat();CL_SetStatNumeric(destsplit, id+0, f, f); f = MSG_ReadFloat();CL_SetStatNumeric(destsplit, id+1, f, f); f = MSG_ReadFloat();CL_SetStatNumeric(destsplit, id+2, f, f); } break; case ev_entity: { unsigned int i = MSGCL_ReadEntity(); CL_SetStatNumeric(destsplit, id, i, i); } break; // case ev_field: // case ev_function: // case ev_pointer: case ev_integer: { signed int i = MSG_ReadLong(); CL_SetStatNumeric(destsplit, id, i, i); } break; case ev_uint: { unsigned int i = MSG_ReadLong(); CL_SetStatNumeric(destsplit, id, i, i); } break; case ev_int64: { qint64_t i = MSG_ReadInt64(); CL_SetStatNumeric(destsplit, id, i, i); } break; case ev_uint64: { quint64_t i = MSG_ReadUInt64(); CL_SetStatNumeric(destsplit, id, i, i); } break; case ev_double: { double f = MSG_ReadDouble(); CL_SetStatNumeric(destsplit, id, f, f); } break; default: Host_EndGame("CL_ParseExtendedStat: type %i is unsupported", type); break; } }*/ /* ============== CL_MuzzleFlash ============== */ static void CL_MuzzleFlash (int entnum) { dlight_t *dl; player_state_t *pl; packet_entities_t *pack; entity_state_t *s1; int pnum; vec3_t org = {0,0,0}; vec3_t axis[3] = {{0,0,0}}; int dlightkey = 0; extern int pt_muzzleflash; extern cvar_t cl_muzzleflash; //was it us? if (!cl_muzzleflash.ival) // remove all muzzleflashes return; if (cl_muzzleflash.value == 2) { //muzzleflash 2 removes muzzleflashes on us for (pnum = 0; pnum < cl.splitclients; pnum++) if (entnum-1 == cl.playerview[pnum].playernum) return; } if (!dlightkey) { pack = &cl.inframes[cl.validsequence&UPDATE_MASK].packet_entities; for (pnum=0 ; pnumnum_entities ; pnum++) //try looking for an entity with that id first { s1 = &pack->entities[pnum]; if (s1->number == entnum) { dlightkey = entnum; VectorCopy(s1->origin, org); AngleVectors(s1->angles, axis[0], axis[1], axis[2]); break; } } } if (!dlightkey) { //that ent number doesn't exist, go for a player with that number if ((unsigned)(entnum) <= cl.allocated_client_slots && entnum > 0) { pl = &cl.inframes[cl.validsequence&UPDATE_MASK].playerstate[entnum-1]; if (pl->messagenum == cl.validsequence) { dlightkey = -entnum; VectorCopy(pl->origin, org); AngleVectors(pl->viewangles, axis[0], axis[1], axis[2]); if (pl->szmins[2] == 0) /*hull is 0-based, so origin is bottom of model, move the light up slightly*/ org[2] += pl->szmaxs[2]/2; } } } if (!dlightkey) return; if (P_RunParticleEffectType(org, axis[0], 1, pt_muzzleflash)) { extern cvar_t r_muzzleflash_colour; extern cvar_t r_muzzleflash_fade; dl = CL_AllocDlight (dlightkey); VectorMA (org, 15, axis[0], dl->origin); memcpy(dl->axis, axis, sizeof(dl->axis)); dl->minlight = 32; dl->die = cl.time + 0.1; VectorCopy(r_muzzleflash_colour.vec4, dl->color); dl->radius = r_muzzleflash_colour.vec4[3] + (rand()&31); VectorCopy(r_muzzleflash_fade.vec4, dl->channelfade); dl->decay = r_muzzleflash_fade.vec4[3]; #ifdef RTLIGHTS dl->lightcolourscales[2] = 4; #endif } } //return if we want to print the message. static char *CL_ParseChat(char *text, player_info_t **player, int *msgflags) { extern cvar_t cl_chatsound, cl_nofake, cl_teamchatsound, cl_enemychatsound; int flags; int offset=0; qboolean suppress_talksound; char *p; char *s; int check_flood; flags = TP_CategorizeMessage (text, &offset, player); *msgflags = flags; s = text + offset; if (flags) { if (!cls.demoplayback) Sys_ServerActivity(); //chat always flashes the screen.. if (*player && Ignore_Message((*player)->name, s, flags)) return NULL; //check f_ stuff if (*player && (!strncmp(s, "f_", 2)|| !strncmp(s, "q_", 2))) { Validation_Auto_Response(*player - cl.players, s); return s; } Validation_CheckIfResponse(text); #ifdef PLUGINS if (!Plug_ChatMessage(text + offset, *player ? (int)(*player - cl.players) : -1, flags)) return NULL; #endif if (flags & (TPM_TEAM|TPM_OBSERVEDTEAM) && !TP_FilterMessage(text + offset)) return NULL; #ifdef QUAKEHUD if (flags & (TPM_TEAM|TPM_OBSERVEDTEAM) && Sbar_UpdateTeamStatus(*player, text+offset)) return NULL; #endif if ((int)msg_filter.value & flags) return NULL; //filter chat check_flood = Ignore_Check_Flood(*player, s, flags); if (check_flood == IGNORE_NO_ADD) return NULL; else if (check_flood == NO_IGNORE_ADD) Ignore_Flood_Add(*player, s); } #ifdef PLUGINS else { if (!Plug_ServerMessage(text + offset, PRINT_CHAT)) return NULL; } #endif suppress_talksound = false; if (flags == 2 || (!cl.teamplay && flags)) suppress_talksound = TP_CheckSoundTrigger (text + offset); if (cls.demoseeking || !cl_chatsound.value || // no sound at all (cl_chatsound.value == 2 && flags != 2)) // only play sound in mm2 suppress_talksound = true; if (!suppress_talksound) { if (flags & (TPM_OBSERVEDTEAM|TPM_TEAM) && cl.teamplay) S_LocalSound (cl_teamchatsound.string); else S_LocalSound (cl_enemychatsound.string); } if (flags) { if (cl_nofake.value == 1 || (cl_nofake.value == 2 && !(flags & (TPM_OBSERVEDTEAM | TPM_TEAM)))) { for (p = s; *p; p++) if (*p == 13 || (*p == 10 && p[1])) *p = ' '; } } return s; } // CL_PlayerColor: returns color and mask for player_info_t static int CL_PlayerColor(player_info_t *plr, qboolean *name_coloured) { char *t; unsigned int c; *name_coloured = false; if (cl.teamfortress) //override based on team { //damn spies if (!Q_strcasecmp(plr->team, "red")) c = 1; else if (!Q_strcasecmp(plr->team, "blue")) c = 5; else // TODO: needs some work switch (plr->rbottomcolor) { //translate q1 skin colours to console colours case 10: case 1: *name_coloured = true; case 4: //red c = 1; break; case 11: *name_coloured = true; case 3: // green c = 2; break; case 5: *name_coloured = true; case 12: c = 3; break; case 6: case 7: *name_coloured = true; case 8: case 9: c = 6; break; case 2: // light blue *name_coloured = true; case 13: //blue case 14: //blue c = 5; break; default: *name_coloured = true; case 0: // white c = 7; break; } } else if (cl.teamplay) { // team name hacks if (!strcmp(plr->team, "red")) c = 1; else if (!strcmp(plr->team, "blue")) c = 5; else { char *t; t = plr->team; c = 0; for (t = plr->team; *t; t++) { c >>= 1; c ^= *t; // TODO: very weak hash, replace } if ((c / 7) & 1) *name_coloured = true; c = 1 + (c % 7); } } else { // override chat color with tc infokey // 0-6 is standard colors (red to white) // 7-13 is using secondard charactermask // 14 and afterwards repeats t = InfoBuf_ValueForKey(&plr->userinfo, "tc"); if (*t) c = atoi(t); else c = plr->userid; // Quake2 can start from 0 if ((c / 7) & 1) *name_coloured = true; c = 1 + (c % 7); } return c; } void TTS_SayChatString(char **stringtosay); // CL_PrintChat: takes chat strings and performs name coloring and cl_parsewhitetext parsing // NOTE: text in rawmsg/msg is assumed destroyable and should not be used afterwards void CL_PrintChat(player_info_t *plr, char *msg, int plrflags) { extern cvar_t con_separatechat; char *name = NULL; int c; qboolean name_coloured = false; extern cvar_t cl_parsewhitetext; qboolean memessage = false; char fullchatmessage[2048]; fullchatmessage[0] = 0; /*if (plrflags & TPM_FAKED) { name = rawmsg; // use rawmsg pointer and msg modification to generate null-terminated string if (msg) *(msg - 2) = 0; // it's assumed that msg has 2 chars before it due to strstr }*/ if (0)//*msg == '\r') { name = msg; msg = strstr(msg, ": "); if (msg) { name++; *msg = 0; msg+=2; plrflags &= ~TPM_TEAM|TPM_OBSERVEDTEAM; } else { msg = name; name = NULL; } } if (msg[0] == '/' && msg[1] == 'm' && msg[2] == 'e' && msg[3] == ' ') { msg += 4; memessage = true; // special /me formatting } if (plr && !name) // use special formatting with a real chat message name = plr->name; // use player's name if (cl_standardchat.ival) { name_coloured = true; c = 7; } else { if (plrflags & TPM_SPECTATOR) // is an observer { // TODO: we don't even check for this yet... if (plrflags & (TPM_TEAM | TPM_OBSERVEDTEAM)) // is on team c = 0; // blacken () on observers else { name_coloured = true; c = 7; } } else if (plr) c = CL_PlayerColor(plr, &name_coloured); else { // defaults for fake clients name_coloured = true; c = 7; } } c = '0' + c; if (plrflags & TPM_QTV) Q_strncatz(fullchatmessage, "QTV ^m", sizeof(fullchatmessage)); else if (name) { if (memessage) { if (!cl_standardchat.value && (plrflags & TPM_SPECTATOR)) Q_strncatz(fullchatmessage, "^0*^7 ", sizeof(fullchatmessage)); else Q_strncatz(fullchatmessage, "* ", sizeof(fullchatmessage)); } else Q_strncatz(fullchatmessage, "\1", sizeof(fullchatmessage)); #if defined(HAVE_SPEECHTOTEXT) TTS_SayChatString(&msg); #endif if (plrflags & (TPM_TEAM|TPM_OBSERVEDTEAM)) // for team chat don't highlight the name, just the brackets { Q_strncatz(fullchatmessage, va("(^[^7%s%s^d\\player\\%i^])", name_coloured?"^m":"", name, (int)(plr-cl.players)), sizeof(fullchatmessage)); } else if (cl_standardchat.ival) { Q_strncatz(fullchatmessage, va("^[^7%s%s^d\\player\\%i^]", name_coloured?"^m":"", name, (int)(plr-cl.players)), sizeof(fullchatmessage)); } else { Q_strncatz(fullchatmessage, va("^[^7%s^%c%s^d\\player\\%i^]", name_coloured?"^m":"", c, name, (int)(plr-cl.players)), sizeof(fullchatmessage)); } if (!memessage) { // only print seperator with an actual player name if (!cl_standardchat.value && (plrflags & TPM_SPECTATOR)) Q_strncatz(fullchatmessage, "^0: ^d", sizeof(fullchatmessage)); else Q_strncatz(fullchatmessage, ": ", sizeof(fullchatmessage)); } else Q_strncatz(fullchatmessage, " ", sizeof(fullchatmessage)); } else Q_strncatz(fullchatmessage, "\1", sizeof(fullchatmessage)); // print message if (cl_parsewhitetext.value && (cl_parsewhitetext.value == 1 || (plrflags & (TPM_TEAM|TPM_OBSERVEDTEAM)))) { char *t, *u; while ((t = strchr(msg, '{'))) { int c; if (t > msg && t[-1] == '^') { for (c = 1; t-c > msg; c++) { if (t[-c] == '^') break; } if (c & 1) { *t = '\0'; Q_strncatz(fullchatmessage, va("%s{", msg), sizeof(fullchatmessage)); msg = t+1; continue; } } u = strchr(t, '}'); if (u) { *t = 0; *u = 0; Q_strncatz(fullchatmessage, va("%s", msg), sizeof(fullchatmessage)); Q_strncatz(fullchatmessage, va("^m%s^m", t+1), sizeof(fullchatmessage)); msg = u+1; } else break; } Q_strncatz(fullchatmessage, va("%s", msg), sizeof(fullchatmessage)); } else { Q_strncatz(fullchatmessage, va("%s", msg), sizeof(fullchatmessage)); } #ifdef CSQC_DAT if (CSQC_ParsePrint(fullchatmessage, PRINT_CHAT)) return; #endif if (con_separatechat.ival) { if (!con_chat) con_chat = Con_Create("chat", CONF_HIDDEN|CONF_NOTIFY|CONF_NOTIFY_BOTTOM); if (con_chat) { Con_PrintCon(con_chat, fullchatmessage, con_chat->parseflags); if (con_separatechat.ival == 1) { console_t *c = Con_GetMain(); Con_PrintCon(c, fullchatmessage, c->parseflags|PFS_NONOTIFY); return; } } } Con_Printf("%s", fullchatmessage); } // CL_PrintStandardMessage: takes non-chat net messages and performs name coloring // NOTE: msg is considered destroyable static char acceptedchars[] = {'.', '?', '!', '\'', ',', ':', ' ', '\0'}; static void CL_PrintStandardMessage(char *msgtext, int printlevel) { int i; player_info_t *p, *foundp = NULL; extern cvar_t cl_standardmsg, msg; char *begin = msgtext; char fullmessage[2048]; char *found; if (printlevel < msg.ival) return; fullmessage[0] = 0; while(*msgtext) { found = NULL; // search for player names in message for (i = 0, p = cl.players; i < cl.allocated_client_slots; p++, i++) { char *v; char *name; int len; name = p->name; if (!(*name)) continue; len = strlen(name); v = strstr(msgtext, name); while (v) { // name parsing rules if (v != begin && *(v-1) != ' ') // must be space before name { v = strstr(v+len, name); continue; } { int i; char aftername = *(v + len); // search for accepted chars in char after name in msg for (i = 0; i < sizeof(acceptedchars); i++) { if (acceptedchars[i] == aftername) break; } if (sizeof(acceptedchars) == i) { v = strstr(v+len, name); continue; // no accepted char found } } if (!found || v < found) { found = v; foundp = p; } break; } } if (found) { qboolean coloured; char c; int len = strlen(foundp->name); // print msg chunk *found = 0; // cut off message Q_strncatz(fullmessage, msgtext, sizeof(fullmessage)); msgtext = found + len; // update search point // get name color if (foundp->spectator || cl_standardmsg.ival) { coloured = false; c = '7'; } else c = '0' + CL_PlayerColor(foundp, &coloured); // print name Q_strncatz(fullmessage, va("^[%s^%c%s^d\\player\\%i^]", coloured?"^m":"", c, foundp->name, (int)(foundp - cl.players)), sizeof(fullmessage)); } else break; //nope, can't find anyone in there... } // print final chunk Q_strncatz(fullmessage, msgtext, sizeof(fullmessage)); #ifdef HAVE_LEGACY if (scr_usekfont.ival) Con_PrintFlags(fullmessage, PFS_FORCEUTF8, 0); else #endif Con_Printf("%s", fullmessage); } static char printtext[4096]; static void CL_ParsePrint(const char *msg, int level) { char n, *e; if (strlen(printtext) + strlen(msg)+2 >= sizeof(printtext)) { Con_Printf("%s", printtext); Q_strncpyz(printtext, msg, sizeof(printtext)); } else strcat(printtext, msg); //safe due to size on if. #ifdef HAVE_LEGACY //eztv is fucking nasty. if (level == PRINT_CHAT && *printtext == '#' && printtext[1] >= '0' && printtext[1] <= '9' && !strchr(printtext, '\n')) strcat(printtext, "\n"); #endif while((e = strchr(printtext, '\n')) || (e = strchr(printtext, '\r'))) { n = e[1]; e[1] = 0; if (!cls.demoseeking) { if (level == PRINT_CHAT) { char *body; int msgflags; player_info_t *plr = NULL; if (!TP_SuppressMessage(printtext)) { body = CL_ParseChat(printtext, &plr, &msgflags); if (body) CL_PrintChat(plr, body, msgflags); } } else { #ifdef CSQC_DAT if (!CSQC_ParsePrint(printtext, level)) #endif #ifdef PLUGINS if (Plug_ServerMessage(printtext, level)) #endif #ifdef QUAKEHUD if (!Stats_ParsePickups(printtext) || !msg_filter_pickups.ival) if (!Stats_ParsePrintLine(printtext) || !msg_filter_frags.ival) #else if (!msg_filter_pickups.ival) if (!msg_filter_frags.ival) #endif CL_PrintStandardMessage(printtext, level); } } TP_SearchForMsgTriggers(printtext, level); e[1] = n; e++; memmove(printtext, e, strlen(e)+1); } } static void CL_ParseWeaponStats(void) { #ifdef QUAKEHUD int pl = atoi(Cmd_Argv(0)); char *wname = Cmd_Argv(1); unsigned int total = strtoul(Cmd_Argv(2), NULL, 0); unsigned int hit = strtoul(Cmd_Argv(3), NULL, 0); unsigned int idx; if (pl >= cl.allocated_client_slots) return; for (idx = 0; idx < countof(cl.players[pl].weaponstats); idx++) { if (!strcmp(cl.players[pl].weaponstats[idx].wname, wname) || !*cl.players[pl].weaponstats[idx].wname) { Q_strncpyz(cl.players[pl].weaponstats[idx].wname, wname, sizeof(cl.players[pl].weaponstats[idx].wname)); cl.players[pl].weaponstats[idx].total = total; cl.players[pl].weaponstats[idx].hit = hit; return; } } #endif } static void CL_ParseItemTimer(void) { //it [cur/]duration x y z radius 0xRRGGBB "timername" owningent float timeout;// = atof(Cmd_Argv(0)); vec3_t org = { atof(Cmd_Argv(1)), atof(Cmd_Argv(2)), atof(Cmd_Argv(3))}; float radius = atof(Cmd_Argv(4)); unsigned int rgb = (Cmd_Argc() > 5)?strtoul(Cmd_Argv(5), NULL, 16):0x202020; // char *timername = Cmd_Argv(6); unsigned int entnum = strtoul(Cmd_Argv(7), NULL, 0); struct itemtimer_s *timer; float start = cl.time; char *e; timeout = strtod(Cmd_Argv(0), &e); if (*e == '/') { start += timeout; timeout = atof(e+1); start -= timeout; } if (!timeout) timeout = FLT_MAX; if (!radius) radius = 32; for (timer = cl.itemtimers; timer; timer = timer->next) { if (entnum) { if (timer->entnum == entnum) break; } else if (VectorCompare(timer->origin, org)) break; } if (!timer) { //didn't find it. timer = Z_Malloc(sizeof(*timer)); timer->next = cl.itemtimers; cl.itemtimers = timer; } VectorCopy(org, timer->origin); timer->radius = radius; timer->duration = timeout; timer->entnum = entnum; timer->start = start; timer->end = start + timer->duration; timer->rgb[0] = ((rgb>>16)&0xff)/255.0; timer->rgb[1] = ((rgb>> 8)&0xff)/255.0; timer->rgb[2] = ((rgb )&0xff)/255.0; } #ifdef PLUGINS static void CL_ParseTeamInfo(void) { unsigned int pidx = atoi(Cmd_Argv(1)); vec3_t org = { atof(Cmd_Argv(2)), atof(Cmd_Argv(3)), atof(Cmd_Argv(4)) }; float health = atof(Cmd_Argv(5)); float armour = atof(Cmd_Argv(6)); unsigned int items = strtoul(Cmd_Argv(7), NULL, 0); char *nick = Cmd_Argv(8); if (pidx < cl.allocated_client_slots) { player_info_t *pl = &cl.players[pidx]; pl->tinfo.time = cl.time+5; pl->tinfo.health = health; pl->tinfo.armour = armour; pl->tinfo.items = items; VectorCopy(org, pl->tinfo.org); Q_strncpyz(pl->tinfo.nick, nick, sizeof(pl->tinfo.nick)); } } #endif static char stufftext[4096]; static void CL_ParseStuffCmd(char *msg, int destsplit) //this protects stuffcmds from network segregation. { int cbuflevel; #ifdef NQPROT if (!*stufftext && *msg == 1) { if (developer.ival) { Con_DPrintf("Proquake Message:\n"); Con_HexDump(msg, strlen(msg), 1, 16); } msg = CLNQ_ParseProQuakeMessage(msg); } #endif Q_strncatz(stufftext, msg, sizeof(stufftext)-1); while((msg = strchr(stufftext, '\n'))) { *msg = '\0'; cbuflevel = RESTRICT_SERVERSEAT(destsplit); Con_DLPrintf((cls.state==ca_active)?1:2, "stufftext%i: %s\n", destsplit, stufftext); if (!strncmp(stufftext, "fullserverinfo ", 15) || !strncmp(stufftext, "//fullserverinfo ", 17)) { Cmd_TokenizeString(stufftext+2, false, false); if (Cmd_Argc() == 2) { cl.haveserverinfo = true; InfoBuf_FromString(&cl.serverinfo, Cmd_Argv(1), false); CL_CheckServerInfo(); } #if _MSC_VER > 1200 if (cls.netchan.remote_address.type != NA_LOOPBACK) Sys_RecentServer("+connect", cls.servername, va("%s (%s)", InfoBuf_ValueForKey(&cl.serverinfo, "hostname"), cls.servername), "Join QW Server"); #endif } else if (!strncmp(stufftext, "//svi ", 6)) //for serverinfo over NQ protocols { Cmd_TokenizeString(stufftext+2, false, false); Con_DPrintf("SERVERINFO: %s=%s\n", Cmd_Argv(1), Cmd_Argv(2)); InfoBuf_SetStarKey(&cl.serverinfo, Cmd_Argv(1), Cmd_Argv(2)); CL_CheckServerInfo(); } else if (!strncmp(stufftext, "//ls ", 5)) //for extended lightstyles { vec3_t rgb; Cmd_TokenizeString(stufftext+2, false, false); rgb[0] = ((Cmd_Argc()>3)?atof(Cmd_Argv(3)):1); rgb[1] = ((Cmd_Argc()>5)?atof(Cmd_Argv(4)):rgb[0]); rgb[2] = ((Cmd_Argc()>5)?atof(Cmd_Argv(5)):rgb[0]); R_UpdateLightStyle(atoi(Cmd_Argv(1)), Cmd_Argv(2), rgb[0], rgb[1], rgb[2]); } #ifdef NQPROT //DP's download protocol else if (cls.protocol == CP_NETQUAKE && !strncmp(stufftext, "cl_serverextension_download ", 28)) //. server lets us know that it supports it. cl_dp_serverextension_download = true; //warning, this is sent BEFORE svc_serverdata, so cannot use cl.foo else if (cls.protocol == CP_NETQUAKE && !strncmp(stufftext, "cl_downloadbegin ", 17)) // . server [reliably] lets us know that its going to start sending data. CLDP_ParseDownloadBegin(stufftext); else if (cls.protocol == CP_NETQUAKE && !strncmp(stufftext, "cl_downloadfinished ", 20)) // . server [reliably] lets us know that we acked the entire thing CLDP_ParseDownloadFinished(stufftext); else if (cls.protocol == CP_NETQUAKE && !strcmp(stufftext, "stopdownload")) //download command reported failure. safe to request the next. { if (cls.download) CL_DownloadFailed(cls.download->remotename, cls.download, DLFAIL_CORRUPTED); } //DP servers use these to report the correct csprogs.dat file+version to use. //WARNING: these are sent BEFORE svc_serverdata, so we cannot store this state into cl.foo //we poke the data into cl.serverinfo once we get the following svc_serverdata. //we then clobber it from a fullserverinfo message if its an fte server running dpp7, but hey. else if (cls.protocol == CP_NETQUAKE && !strncmp(stufftext, "csqc_progname ", 14)) COM_ParseOut(stufftext+14, cl_dp_csqc_progsname, sizeof(cl_dp_csqc_progsname)); else if (cls.protocol == CP_NETQUAKE && !strncmp(stufftext, "csqc_progsize ", 14)) cl_dp_csqc_progssize = atoi(stufftext+14); else if (cls.protocol == CP_NETQUAKE && !strncmp(stufftext, "csqc_progcrc ", 13)) cl_dp_csqc_progscrc = atoi(stufftext+13); //NQ servers/mods like spamming this. Its annoying, but we might as well use it if we can, while also muting it. else if (!strncmp(stufftext, "cl_fullpitch ", 13) || !strncmp(stufftext, "pq_fullpitch ", 13)) { if (!cl.haveserverinfo) { InfoBuf_SetKey(&cl.serverinfo, "maxpitch", (atoi(stufftext+13))? "90":""); InfoBuf_SetKey(&cl.serverinfo, "minpitch", (atoi(stufftext+13))?"-90":""); CL_CheckServerInfo(); } } #endif else if (!strncmp(stufftext, "//paknames ", 11)) //so that the client knows what to download... { //there's a couple of prefixes involved etc Z_StrCat(&cl.serverpacknames, stufftext+(cl.serverpackhashes?11:10)); cl.serverpakschanged = true; } else if (!strncmp(stufftext, "//paks ", 7)) //gives the client a list of hashes to match against { //the client can re-order for cl_pure support, or download dupes to avoid version mismatches Z_StrCat(&cl.serverpackhashes, stufftext+(cl.serverpackhashes?7:6)); cl.serverpakschanged = true; CL_CheckServerPacks(); } #ifdef HAVE_LEGACY else if (!strncmp(stufftext, "//vwep ", 7)) //list of vwep model indexes, because using the normal model precaches wasn't cool enough { //(from zquake/ezquake) int i; char *mname; Cmd_TokenizeString(stufftext+7, false, false); for (i = 0; i < Cmd_Argc(); i++) { mname = Cmd_Argv(i); if (strcmp(mname, "-")) { mname = va("progs/%s.mdl", Cmd_Argv(i)); Z_StrDupPtr(&cl.model_name_vwep[i], mname); if (cls.state == ca_active) { CL_CheckOrEnqueDownloadFile(cl.model_name_vwep[i], NULL, 0); cl.model_precache_vwep[i] = Mod_ForName(cl.model_name_vwep[i], MLV_WARN); } } else { Z_Free(cl.model_name_vwep[i]); cl.model_name_vwep[i] = NULL; } } } #endif else if (cls.demoplayback && !strncmp(stufftext, "playdemo ", 9)) { //some demos (like speed-demos-archive's marathon runs) chain multiple demos with playdemo commands //these should still chain properly even when the demo is in some archive(like .dz) or subdir char newdemo[MAX_OSPATH], temp[MAX_OSPATH], *s; Cmd_TokenizeString(stufftext, false, false); s = Cmd_Argv(1); if (strchr(s, ':') || strchr(s, '/') || strchr(s, '\\')) Q_strncpyz(newdemo, s, sizeof(newdemo)); else { newdemo[0] = 0; if (cls.lastdemowassystempath) Q_strncatz(newdemo, "#", sizeof(newdemo)); Q_strncatz(newdemo, cls.lastdemoname, sizeof(newdemo)); *COM_SkipPath(newdemo) = 0; Q_strncatz(newdemo, Cmd_Argv(1), sizeof(newdemo)); } Cbuf_AddText ("playdemo ", cbuflevel); Cbuf_AddText (COM_QuotedString(newdemo, temp, sizeof(temp), false), cbuflevel); Cbuf_AddText ("\n", cbuflevel); } #ifdef CSQC_DAT else if (CSQC_StuffCmd(destsplit, stufftext, msg)) { } #endif else if (!strncmp(stufftext, "//querycmd ", 11)) //for servers to check if a command exists or not. { COM_Parse(stufftext + 11); if (Cmd_Exists(com_token)) { Cbuf_AddText ("cmd cmdsupported ", cbuflevel); Cbuf_AddText (com_token, cbuflevel); Cbuf_AddText ("\n", cbuflevel); } } else if (!strncmp(stufftext, "//exectrigger ", 14)) //so that mods can add whatever 'alias grabbedarmour' or whatever triggers that users might want to script responses for, without errors about unknown commands { COM_Parse(stufftext + 14); if (Cmd_AliasExist(com_token, cbuflevel)) Cmd_ExecuteString(com_token, cbuflevel); //do this NOW so that it's done before any models or anything are loaded } else if (!strncmp(stufftext, "//set ", 6)) //equivelent to regular set, except non-spammy if it doesn't exist, and happens instantly without extra latency. { Cmd_ExecuteString(stufftext+2, cbuflevel); //do this NOW so that it's done before any models or anything are loaded } else if (!strncmp(stufftext, "//at ", 5)) //ktx autotrack hints { Cam_SetModAutoTrack(atoi(stufftext+5)); } else if (!strncmp(stufftext, "//wps ", 5)) //ktx weapon statistics { Cmd_TokenizeString(stufftext+5, false, false); CL_ParseWeaponStats(); } else if (!strncmp(stufftext, "//kickfile ", 11)) //FTE sends this to give a more friendly error about modified BSP files, although it could be used for more stuff. { flocation_t loc; Cmd_TokenizeString(stufftext+2, false, false); if (FS_FLocateFile(Cmd_Argv(1), FSLF_IFFOUND, &loc)) { if (!*loc.rawname) Con_Printf("You have been kicked due to the file "U8("%s")" being modified, inside "U8("%s")"\n", Cmd_Argv(1), loc.search->logicalpath); else Con_Printf("You have been kicked due to the file "U8("%s")" being modfied, located at "U8("%s")"\n", Cmd_Argv(1), loc.rawname); } } else if (!strncmp(stufftext, "//it ", 5)) //it { Cmd_TokenizeString(stufftext+5, false, false); CL_ParseItemTimer(); } else if (!strncmp(stufftext, "//fui ", 6)) //ui . Full user info updates. { unsigned int slot; const char *value; Cmd_TokenizeString(stufftext+6, false, false); slot = atoi(Cmd_Argv(0)); value = Cmd_Argv(1); if (slot < MAX_CLIENTS) { player_info_t *player = &cl.players[slot]; Con_DPrintf("SETINFO %s: %s\n", player->name, value); InfoBuf_FromString(&player->userinfo, value, false); player->userinfovalid = true; CL_ProcessUserInfo (slot, player); } } else if (!strncmp(stufftext, "//ui ", 5)) //ui . Partial user info updates. { unsigned int slot; const char *key, *value; Cmd_TokenizeString(stufftext+5, false, false); slot = atoi(Cmd_Argv(0)); key = Cmd_Argv(1); value = Cmd_Argv(2); if (slot < MAX_CLIENTS) { player_info_t *player = &cl.players[slot]; Con_DPrintf("SETINFO %s: %s=%s\n", player->name, key, value); InfoBuf_SetValueForStarKey (&player->userinfo, key, value); CL_ProcessUserInfo (slot, player); } } else if (!strncmp(stufftext, "//qul ", 6)) //qtv user list { unsigned int cmd, id; const char *name; struct qtvviewers_s **link, *v; Cmd_TokenizeString(stufftext+5, false, false); cmd = atoi(Cmd_Argv(0)); id = atoi(Cmd_Argv(1)); name = Cmd_Argv(2); for (link = &cls.qtvviewers; (v=*link); link = &v->next) { if (v->userid == id) break; } switch(cmd) { case 3://del if (v) { *link = v->next; Z_Free(v); } break; case 2://change case 1://add if (!v) { //new... v = Z_Malloc(sizeof(*v)); v->next = cls.qtvviewers; cls.qtvviewers = v; v->userid = id; } Q_strncpyz(v->name, name, sizeof(v->name)); break; } } #ifdef PLUGINS else if (!strncmp(stufftext, "//tinfo ", 8)) //ktx-team-info { Cmd_TokenizeString(stufftext+2, false, false); CL_ParseTeamInfo(); Plug_Command_f(); //FIXME: deprecate this call } else if (!strncmp(stufftext, "//sn ", 5)) { Cmd_TokenizeString(stufftext+2, false, false); Plug_Command_f(); } #endif else if (!strncmp(stufftext, "//demomark", 10) && (stufftext[10]==0||stufftext[10]==' ') && cls.demoseeking == DEMOSEEK_MARK) { //found the next marker. we're done seeking. cls.demoseeking = DEMOSEEK_NOT; //FIXME: pause it. } else { if (!strncmp(stufftext, "cmd ", 4)) Cbuf_AddText (va("p%i ", destsplit+1), cbuflevel); //without this, in_forceseat can break directed cmds. Cbuf_AddText (stufftext, cbuflevel); Cbuf_AddText ("\n", cbuflevel); } msg++; memmove(stufftext, msg, strlen(msg)+1); } } static void CL_ParsePrecache(void) { int i, code = (unsigned short)MSG_ReadShort(); char *s = MSG_ReadString(); i = code & ~PC_TYPE; switch(code & PC_TYPE) { case PC_MODEL: if (i >= 1 && i < MAX_PRECACHE_MODELS) { model_t *model; Z_StrDupPtr(&cl.model_name[i], s); CL_CheckOrEnqueDownloadFile(s, s, DLLF_ALLOWWEB); model = Mod_ForName(Mod_FixName(s, cl.model_name[1]), (i == 1&&!cl.sendprespawn)?MLV_ERROR:MLV_WARN); // if (!model) // Con_Printf("svc_precache: Mod_ForName(\"%s\") failed\n", s); cl.model_precache[i] = model; } else Con_Printf("svc_precache: model index %i outside range %i...%i\n", i, 1, MAX_PRECACHE_MODELS); break; case PC_UNUSED: break; case PC_SOUND: if (i >= 1 && i < MAX_PRECACHE_SOUNDS) { sfx_t *sfx; if (S_HaveOutput()) CL_CheckOrEnqueDownloadFile(va("sound/%s", s), NULL, DLLF_ALLOWWEB); sfx = S_PrecacheSound (s); // if (!sfx) // Con_Printf("svc_precache: S_PrecacheSound(\"%s\") failed\n", s); cl.sound_precache[i] = sfx; Z_StrDupPtr(&cl.sound_name[i], s); } else Con_Printf("svc_precache: sound index %i outside range %i...%i\n", i, 1, MAX_PRECACHE_SOUNDS); break; case PC_PARTICLE: if (i >= 1 && i < MAX_SSPARTICLESPRE) { Z_StrDupPtr(&cl.particle_ssname[i], s); cl.particle_ssprecache[i] = P_FindParticleType(s); cl.particle_ssprecaches = true; } else Con_Printf("svc_precache: particle index %i outside range %i...%i\n", i, 1, MAX_SSPARTICLESPRE); break; } } void Con_HexDump(qbyte *packet, size_t len, size_t badoffset, size_t stride) { int i; int pos; pos = 0; while(pos < len) { Con_Printf("%5i ", pos); for (i = 0; i < stride; i++) { if (pos >= len) Con_Printf(" - "); else if (pos == badoffset) Con_Printf("^b^1%2x ", packet[pos]); else Con_Printf("%2x ", packet[pos]); pos++; } pos-=stride; for (i = 0; i < stride; i++) { if (pos >= len) Con_Printf("X"); else if (packet[pos] == 0 || packet[pos] == '\t' || packet[pos] == '\r' || packet[pos] == '\n') { if (pos == badoffset) Con_Printf("^b^1."); else Con_Printf("."); } else { if (pos == badoffset) Con_Printf("^b^1%c", packet[pos]); else Con_Printf("%c", packet[pos]); } pos++; } Con_Printf("\n"); } } void CL_DumpPacket(void) { Con_HexDump(net_message.data, net_message.cursize, MSG_GetReadCount()-1, 16); } static void CL_ParsePortalState(void) { int mode = MSG_ReadByte(); int p = -1, a1 = -1, a2 = -1, state = -1; #define PS_NEW (1<<7) #define PS_AREANUMS (1<<6) //q3 style #define PS_PORTALNUM (1<<5) //q2 style #define PS_LARGE (1<<1) #define PS_OPEN (1<<0) if (mode & PS_NEW) { state = mode&1; if (!(mode & PS_AREANUMS) && !(mode & PS_PORTALNUM)) mode |= PS_PORTALNUM; //legacy crap if (mode & PS_PORTALNUM) { //q2 style if (mode&PS_LARGE) p = MSG_ReadShort(); else p = MSG_ReadByte(); } if (mode & PS_AREANUMS) { //q3 style if (mode&PS_LARGE) { a1 = MSG_ReadShort(); a2 = MSG_ReadShort(); } else { a1 = MSG_ReadByte(); a2 = MSG_ReadByte(); } } } else { //legacy crap Con_Printf(CON_WARNING"svc_setportalstate: legacy mode\n"); mode |= MSG_ReadByte()<<8; p = (mode & 0x7fff); state = !!(mode & 0x8000); } #ifdef HAVE_SERVER //reduce race conditions when we're both client+server. if (sv.active) return; #endif if (cl.worldmodel && cl.worldmodel->loadstate==MLS_LOADED && cl.worldmodel->funcs.SetAreaPortalState) cl.worldmodel->funcs.SetAreaPortalState(cl.worldmodel, p, a1, a2, state); } static void CL_ParseBaseAngle(int seat) { int i; short diff[3]; short newbase[3]; vec3_t newang; inframe_t *inf = &cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK]; qbyte fl = MSG_ReadByte(); //pitch yaw roll lock for (i=0 ; i<3 ; i++) { if (fl & (1u<packet_entities.fixangles[seat] = true; VectorCopy (newang, inf->packet_entities.fixedangles[seat]); } VectorCopy (newang, cl.playerview[seat].viewangles); } VectorCopy (newang, cl.playerview[seat].intermissionangles); if (fl & 8) VRUI_SnapAngle(); } void CLEZ_ParseHiddenDemoMessage(void) { for(;;) { static float throttle; int size = MSG_ReadLong(); unsigned int cmd; if (size == -1 || msg_badread) break; cmd = MSG_ReadUInt16(); switch(cmd) { case 0xFFFF://mvdhidden_extended return; //can't handle it... protocol is stupid. case 0x0003://mvdhidden_demoinfo MSG_ReadUInt16(); //'more' MSG_ReadSkip(size-2); //probably json break; case 0x0007://mvdhidden_dmgdone { lerpents_t *le; unsigned short typeandflags = MSG_ReadUInt16(); unsigned short attacker = MSG_ReadUInt16(); unsigned short targ = MSG_ReadUInt16(); short dmg = MSG_ReadShort(); unsigned short issplash = !!(typeandflags&0x8000); unsigned short isteamdamage = (attacker==targ) || (cl.teamplay && attacker-1origin[0], le->origin[1], le->origin[2], typeandflags, dmg, issplash, isteamdamage), cmd); } } break; /* case 0x0000://mvdhidden_antilag_position case 0x0001://mvdhidden_usercmd // case 0x0002://mvdhidden_usercmd_weapons // case 0x0004://mvdhidden_commentary_track case 0x0005://mvdhidden_commentary_data case 0x0006://mvdhidden_commentary_text_segment case 0x0008://mvdhidden_usercmd_weapons_ss // (same format as mvdhidden_usercmd_weapons) case 0x0009://mvdhidden_usercmd_weapon_instruction // case 0x000A://mvdhidden_paused_duration */ default: Con_ThrottlePrintf(&throttle, 1, "CL_ParseHiddenDemoMessage: Unknown cmd %i\n", cmd); MSG_ReadSkip(size); break; } } } #define SHOWNETEOM(x) if(cl_shownet.value>=2)Con_Printf ("%3i:%s\n", MSG_GetReadCount(), x); #define SHOWNET(x) if(cl_shownet.value>=2)Con_Printf ("%3i:%s\n", MSG_GetReadCount()-1, x); #define SHOWNET2(x, y) if(cl_shownet.value>=2)Con_Printf ("%3i:%3i:%s\n", MSG_GetReadCount()-1, y, x); /* ===================== CL_ParseServerMessage ===================== */ void CLQW_ParseServerMessage (void) { int cmd; char *s; unsigned int u; int i, j; int destsplit; vec3_t ang; float f; qboolean suggestcsqcdebug = false; inframe_t *inf; extern vec3_t demoangles; unsigned int cmdstart; cl.last_servermessage = realtime; CL_ClearProjectiles (); //clear out fixangles stuff inf = &cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK]; for (j = 0; j < MAX_SPLITS; j++) inf->packet_entities.fixangles[j] = false; if (cls.demoplayback == DPB_QUAKEWORLD) { inf->packet_entities.fixangles[0] = 2; VectorCopy(demoangles, inf->packet_entities.fixedangles[0]); } else if (cl.intermissionmode != IM_NONE) { for (destsplit = 0; destsplit < cl.splitclients; destsplit++) { inf->packet_entities.fixangles[destsplit] = 2; VectorCopy(cl.playerview[destsplit].intermissionangles, inf->packet_entities.fixedangles[destsplit]); } } // // if recording demos, copy the message out // if (cl_shownet.value == 1) Con_Printf ("%i ",net_message.cursize); else if (cl_shownet.value >= 2) Con_Printf ("------------------\n"); CL_ParseClientdata (); //vanilla QW has no timing info in the client and depends upon the client for all timing. //using the demo's timing for interpolation prevents unneccesary drift, and solves issues with demo seeking and other such things. if (cls.demoplayback == DPB_QUAKEWORLD && !(cls.fteprotocolextensions & PEXT_ACCURATETIMINGS)) { extern float demtime; if (cl.gametime != demtime) { cl.oldgametime = cl.gametime; cl.oldgametimemark = cl.gametimemark; cl.gametime = demtime; cl.gametimemark = realtime; } } if (realtime > packetusageflushtime) { memcpy(packetusage_saved, packetusage_pending, sizeof(packetusage_saved)); memset(packetusage_pending, 0, sizeof(packetusage_pending)); packetusageflushtime = realtime + packetusage_interval; } // // parse the message // while (1) { if (msg_badread) { CL_DumpPacket(); Host_EndGame ("CLQW_ParseServerMessage: Bad server message"); break; } cmdstart = MSG_GetReadCount(); cmd = MSG_ReadByte (); if (cmd == svcfte_choosesplitclient) { SHOWNET2(svc_qwstrings[cmd], cmd); destsplit = MSG_ReadByte() % MAX_SPLITS; cmd = MSG_ReadByte(); } else destsplit = cl.defaultnetsplit; if (cmd == -1) { SHOWNETEOM("END OF MESSAGE"); break; } SHOWNET2(svc_qwstrings[cmd], cmd); // other commands switch (cmd) { default: CL_DumpPacket(); Host_EndGame ("CLQW_ParseServerMessage: Illegible server message (%i@%i)%s", cmd, MSG_GetReadCount()-1, (!cl.csqcdebug && suggestcsqcdebug)?"\n'sv_csqcdebug 1' might aid in debugging this.":"" ); return; case svc_time: cl.oldgametime = cl.gametime; cl.gametime = MSG_ReadFloat(); cl.gametimemark = realtime; break; case svc_nop: // Con_Printf ("svc_nop\n"); //If we're using cl_shownet 2, combine large padding blocks into a single line, otherwise we'll get 1000+ lines from a single packet. while (svc_nop == MSG_PeekByte()) MSG_ReadByte(); break; case svc_disconnect: if (cls.demoplayback == DPB_MVD) //eztv fails to detect the end of demos. { s = MSG_ReadString(); Con_Printf(CON_WARNING"svc_disconnect: %s\n", s); } else if (cls.demoplayback) { CL_Disconnect(NULL); CL_NextDemo(); return; } else if (cls.state == ca_connected) { Host_EndGame ("Server disconnected\n"); } else Host_EndGame ("Server disconnected"); break; case svc_print: i = MSG_ReadByte (); s = MSG_ReadString (); CL_ParsePrint(s, i); break; case svc_centerprint: s = MSG_ReadString (); #ifdef PLUGINS if (Plug_CenterPrintMessage(s, destsplit)) #endif SCR_CenterPrint (destsplit, s, false); break; case svc_stufftext: s = MSG_ReadString (); CL_ParseStuffCmd(s, destsplit); break; case svc_damage: V_ParseDamage (&cl.playerview[destsplit]); break; case svc_serverdata: Cbuf_Execute (); // make sure any stuffed commands are done CLQW_ParseServerData (); break; case svcfte_splitscreenconfig: j = cl.splitclients; cl.splitclients = MSG_ReadByte(); for (i = 0; i < cl.splitclients && i < MAX_SPLITS; i++) { cl.playerview[i].playernum = MSG_ReadByte(); cl.playerview[i].viewentity = cl.playerview[i].playernum+1; if (i>=j) //its new. cl.playerview[i].chatstate = 0; } if (i < cl.splitclients) { Con_Printf("Server sent us too many seats!\n"); for (; i < cl.splitclients; i++) { //svcfte_choosesplitclient has a modulo that is also broken, but at least there's no parse errors this way MSG_ReadByte(); // CL_SendSeatClientCommand(true, i, drop"); } cl.splitclients = MAX_SPLITS; } break; #ifdef PEXT_SETVIEW case svc_setview: if (!(cls.fteprotocolextensions & PEXT_SETVIEW)) Con_Printf("^1PEXT_SETVIEW is meant to be disabled\n"); cl.playerview[destsplit].viewentity=MSGCL_ReadEntity(); break; #endif case svcfte_setanglebase: CL_ParseBaseAngle(destsplit); break; case svcfte_setangledelta: for (i=0 ; i<3 ; i++) ang[i] = cl.playerview[destsplit].viewangles[i] + MSG_ReadAngle16 (); if (!CSQC_Parse_SetAngles(destsplit, ang, true)) VectorCopy (ang, cl.playerview[destsplit].viewangles); VectorCopy (cl.playerview[destsplit].viewangles, cl.playerview[destsplit].simangles); VectorCopy (cl.playerview[destsplit].viewangles, cl.playerview[destsplit].intermissionangles); break; case svc_setangle: if (cls.demoplayback == DPB_MVD) { //I really don't get the point of fixangles in an mvd. just to disable interpolation for that frame? int pl = MSG_ReadByte(); for (i=0 ; i<3 ; i++) ang[i] = MSG_ReadAngle(); for (j = 0; j < cl.splitclients; j++) { playerview_t *pv = &cl.playerview[j]; if (Cam_TrackNum(pv) == pl) { inf->packet_entities.fixangles[j] = true; VectorCopy(ang, inf->packet_entities.fixedangles[j]); } } } else { inframe_t *inf = &cl.inframes[cls.netchan.incoming_sequence&UPDATE_MASK]; int fixtype = 2; if (cls.ezprotocolextensions1 & EZPEXT1_SETANGLEREASON) fixtype = MSG_ReadByte(); //0=unknown, 1=tele, 2=spawn for (i=0 ; i<3 ; i++) ang[i] = MSG_ReadAngle(); if (fixtype == 1) { //relative VectorAdd(ang, cl.playerview[destsplit].viewangles, ang); VectorSubtract(ang, cl.outframes[cls.netchan.incoming_sequence&UPDATE_MASK].cmd->angles, ang); } else fixtype = 2; //snap if (!CSQC_Parse_SetAngles(destsplit, ang, fixtype==1)) { inf->packet_entities.fixangles[destsplit] = true; VectorCopy (ang, cl.playerview[destsplit].viewangles); VectorCopy (ang, inf->packet_entities.fixedangles[destsplit]); } VectorCopy (cl.playerview[destsplit].viewangles, cl.playerview[destsplit].intermissionangles); } break; case svc_lightstyle: i = MSG_ReadByte (); if (i >= MAX_NET_LIGHTSTYLES) Host_EndGame ("svc_lightstyle > MAX_LIGHTSTYLES"); s = MSG_ReadString(); if (cl_shownet.value == 3) Con_Printf("\t%i=\"%s\"\n", i, s); R_UpdateLightStyle(i, s, 1, 1, 1); break; #ifdef PEXT_LIGHTSTYLECOL case svcfte_lightstylecol: if (!(cls.fteprotocolextensions & PEXT_LIGHTSTYLECOL)) Host_EndGame("PEXT_LIGHTSTYLECOL is meant to be disabled\n"); { int bits; vec3_t rgb; i = MSG_ReadByte (); bits = MSG_ReadByte(); if (bits & 0x40) i |= MSG_ReadByte()<<8; //high bits of style index. if (bits & 0x80) { rgb[0] = (bits&1)?MSG_ReadShort()/1024.0:0; rgb[1] = (bits&2)?MSG_ReadShort()/1024.0:0; rgb[2] = (bits&4)?MSG_ReadShort()/1024.0:0; } else { rgb[0] = (bits&1)?1:0; rgb[1] = (bits&2)?1:0; rgb[2] = (bits&4)?1:0; } if (i >= MAX_NET_LIGHTSTYLES) Host_EndGame ("svc_lightstyle > MAX_LIGHTSTYLES"); R_UpdateLightStyle(i, MSG_ReadString(), rgb[0], rgb[1], rgb[2]); } break; #endif case svc_sound: CLQW_ParseStartSoundPacket(); break; #ifdef PEXT_SOUNDDBL case svcfte_soundextended: CLNQ_ParseStartSoundPacket(); break; #endif case svc_stopsound: i = MSG_ReadShort(); S_StopSound(i>>3, i&7); break; #ifdef PEXT2_VOICECHAT case svcfte_voicechat: S_Voip_Parse(); break; #endif #ifdef TERRAIN case svcfte_brushedit: CL_Parse_BrushEdit(); break; #endif case svc_updatefrags: Sbar_Changed (); u = MSG_ReadPlayer(); if (u >= MAX_CLIENTS) Host_EndGame ("CL_ParseServerMessage: svc_updatefrags > MAX_SCOREBOARD"); cl.players[u].frags = MSG_ReadShort (); break; case svc_updateping: u = MSG_ReadPlayer(); if (u >= MAX_CLIENTS) Host_EndGame ("CL_ParseServerMessage: svc_updateping > MAX_SCOREBOARD"); cl.players[u].ping = MSG_ReadShort (); break; case svc_updatepl: u = MSG_ReadPlayer(); if (u >= MAX_CLIENTS) Host_EndGame ("CL_ParseServerMessage: svc_updatepl > MAX_SCOREBOARD"); cl.players[u].pl = MSG_ReadByte (); break; case svc_updateentertime: // time is sent over as seconds ago u = MSG_ReadPlayer(); if (u >= MAX_CLIENTS) Host_EndGame ("CL_ParseServerMessage: svc_updateentertime > MAX_SCOREBOARD"); cl.players[u].realentertime = realtime - MSG_ReadFloat (); break; case svc_spawnbaseline: i = MSGCL_ReadEntity (); if (!CL_CheckBaselines(i)) Host_EndGame("CL_ParseServerMessage: svc_spawnbaseline failed with size %i", i); CL_ParseBaseline (cl_baselines + i, CPNQ_ID); break; case svcfte_spawnbaseline2: CL_ParseBaselineDelta (); break; case svc_spawnstatic: CL_ParseStaticProt (CPNQ_ID); break; case svcfte_spawnstatic2: CL_ParseStaticProt (-1); break; case svc_temp_entity: #ifdef NQPROT CL_ParseTEnt (false); #else CL_ParseTEnt (); #endif break; case svcfte_temp_entity_sized: CL_ParseTEnt_Sized(); break; case svcfte_customtempent: CL_ParseCustomTEnt(); break; case svc_particle: CLNQ_ParseParticleEffect (); break; case svcfte_particle2: CL_ParseParticleEffect2 (); break; case svcfte_particle3: CL_ParseParticleEffect3 (); break; case svcfte_particle4: CL_ParseParticleEffect4 (); break; case svc_killedmonster: //fixme: update all player stats #ifdef QUAKESTATS cl.playerview[destsplit].stats[STAT_MONSTERS]++; cl.playerview[destsplit].statsf[STAT_MONSTERS]++; #endif break; case svc_foundsecret: //fixme: update all player stats #ifdef QUAKESTATS cl.playerview[destsplit].stats[STAT_SECRETS]++; cl.playerview[destsplit].statsf[STAT_SECRETS]++; #endif break; case svcqw_updatestatbyte: i = MSG_ReadByte (); j = MSG_ReadByte (); CL_SetStatNumeric(destsplit, i, j, j); break; case svcqw_updatestatlong: i = MSG_ReadByte (); j = MSG_ReadLong (); //make qbyte if nq compatability? CL_SetStatNumeric (destsplit, i, j, j); break; case svcfte_updatestatstring: i = MSG_ReadByte(); s = MSG_ReadString(); CL_SetStatString (destsplit, i, s); break; case svcfte_updatestatfloat: i = MSG_ReadByte(); f = MSG_ReadFloat(); CL_SetStatNumeric (destsplit, i, f, f); break; /* case svcfte_updatebigstat: CL_ParseExtendedStat(); break;*/ case svc_spawnstaticsound: CL_ParseStaticSound (false); break; case svcfte_spawnstaticsound2: CL_ParseStaticSound (MSG_ReadByte()); break; case svc_cdtrack: { //quakeworld got a crippled svc_cdtrack. unsigned int firsttrack; firsttrack = MSG_ReadByte (); Media_NumberedTrack (firsttrack, firsttrack); } break; case svc_intermission: if (cl.intermissionmode == IM_NONE) { TP_ExecTrigger ("f_mapend", false); if (cl.playerview[destsplit].spectator || cls.demoplayback) TP_ExecTrigger ("f_specmapend", true); else TP_ExecTrigger ("f_playmapend", true); cl.completed_time = cl.gametime; } cl.intermissionmode = IM_QWSCORES; for (i=0 ; i<3 ; i++) cl.playerview[destsplit].simorg[i] = MSG_ReadCoord (); for (i=0 ; i<3 ; i++) cl.playerview[destsplit].intermissionangles[i] = MSG_ReadAngle (); if (cls.demoseeking == DEMOSEEK_INTERMISSION) { cls.demoseeking = DEMOSEEK_NOT; //reached it. //FIXME: pause it. } break; case svc_finale: if (cl.intermissionmode == IM_NONE) { for (i = 0; i < MAX_SPLITS; i++) cl.playerview[i].simorg[2] += cl.playerview[i].viewheight; VectorCopy (cl.playerview[destsplit].simangles, cl.playerview[destsplit].intermissionangles); cl.completed_time = cl.gametime; } cl.intermissionmode = IM_NQFINALE; SCR_CenterPrint (destsplit, TL_Translate(com_language, MSG_ReadString ()), false); break; case svc_sellscreen: Cmd_ExecuteString ("help", RESTRICT_SERVER); break; case svc_smallkick: cl.playerview[destsplit].punchangle_cl = -2; break; case svc_bigkick: cl.playerview[destsplit].punchangle_cl = -4; break; case svc_muzzleflash: CL_MuzzleFlash (MSGCL_ReadEntity()); break; case svc_updateuserinfo: CL_UpdateUserinfo (); break; case svc_setinfo: CL_ParseSetInfo (); break; case svc_serverinfo: CL_ServerInfo (); break; case svcfte_setinfoblob: CL_ParseSetInfoBlob(); break; case svc_download: CL_ParseDownload (false); break; case svc_playerinfo: CLQW_ParsePlayerinfo (); break; case svc_nails: CL_ParseProjectiles (cl_spikeindex, false); break; case svc_nails2: CL_ParseProjectiles (cl_spikeindex, true); break; case svc_chokecount: // some preceding packets were choked i = MSG_ReadByte (); for (j=0 ; j net_message.cursize-MSG_GetReadCount()) Host_EndGame ("CLQ2_ParseZPacket: svcr1q2_zpacket truncated"); if (ulen > net_message.maxsize-net_message.cursize) Host_EndGame ("CLQ2_ParseZPacket: svcr1q2_zpacket overflow"); indata = net_message.data + MSG_GetReadCount(); outdata = net_message.data + net_message.cursize; MSG_ReadSkip(clen); restoremsg = net_message; net_message.currentbit = net_message.cursize<<3; net_message.cursize += ulen; memset(&s, 0, sizeof(s)); s.next_in = indata; s.avail_in = clen; s.total_in = 0; s.next_out = outdata; s.avail_out = ulen; s.total_out = 0; if (inflateInit2(&s, -15) != Z_OK) Host_EndGame ("CLQ2_ParseZPacket: unable to initialise zlib"); if (inflate(&s, Z_FINISH) != Z_STREAM_END) Host_EndGame ("CLQ2_ParseZPacket: stream truncated"); if (inflateEnd(&s) != Z_OK) Host_EndGame ("CLQ2_ParseZPacket: stream truncated"); if (s.total_out != ulen || s.total_in != clen) Host_EndGame ("CLQ2_ParseZPacket: stream truncated"); CLQ2_ParseServerMessage(); net_message = restoremsg; msg_badread = false; #endif } static void CLR1Q2_ParseSetting(void) { int setting = MSG_ReadLong(); int value = MSG_ReadLong(); if (setting == R1Q2_SVSET_FPS) { cl.q2svnetrate = value; if (cl.validsequence) Con_Printf("warning: fps rate changed mid-game\n"); //fixme: we need to clean up lerping stuff. if its now lower, we might have a whole load of things waiting ages for a timeout. } } static void CLQ2EX_ParseFog(void) { float d=0; qbyte r=255, g=255, b=255; unsigned short t=0; unsigned int flags = MSG_ReadByte(); if (flags & (1<<7)) flags |= MSG_ReadByte()<<8; if (flags & (1<<0)) d = MSG_ReadFloat(); //density if (flags & (1<<0)) MSG_ReadByte(); //density if (flags & (1<<1)) r = MSG_ReadByte(); //r if (flags & (1<<2)) g = MSG_ReadByte(); //g if (flags & (1<<3)) b = MSG_ReadByte(); //b if (flags & (1<<4)) t = MSG_ReadUInt16(); //time if (flags & (1<<5)) MSG_ReadFloat(); //h falloff if (flags & (1<<6)) MSG_ReadFloat(); //h density if (flags & (1<<8)) MSG_ReadByte(); //h s r if (flags & (1<<9)) MSG_ReadByte(); //h s g if (flags & (1<<10)) MSG_ReadByte(); //h s b if (flags & (1<<11)) MSG_ReadLong(); //h s dist if (flags & (1<<12)) MSG_ReadByte(); //h e r if (flags & (1<<13)) MSG_ReadByte(); //h e g if (flags & (1<<14)) MSG_ReadByte(); //h e b if (flags & (1<<15)) MSG_ReadLong(); //h e dist CL_ResetFog(FOGTYPE_AIR); cl.fog[FOGTYPE_AIR].density = d; cl.fog[FOGTYPE_AIR].colour[0] = SRGBf(r/255.0f); cl.fog[FOGTYPE_AIR].colour[1] = SRGBf(g/255.0f); cl.fog[FOGTYPE_AIR].colour[2] = SRGBf(b/255.0f); cl.fog[FOGTYPE_AIR].time += (t) / 100.0; cl.fog_locked = !!cl.fog[FOGTYPE_AIR].density; } static char *CLQ2EX_ReformatBinds(int seat, const char *source, char *buffer, size_t buffersize) { //q2e has various "%bind:cmd:$desc%" tags that should be expanded to some "
================================================ FILE: engine/web/gl_vidweb.c ================================================ #include "quakedef.h" #include "glquake.h" #include "vr.h" #include "shader.h" vfsfile_t *FSWEB_OpenTempHandle(int f); extern cvar_t gl_lateswap; extern qboolean gammaworks; extern qboolean vid_isfullscreen; qboolean mouseactive; extern qboolean mouseusedforgui; static struct { int id; unsigned axistobuttonp; //bitmask of whether we're currently reporting each axis as pressed. without saving values. unsigned axistobuttonn; int repeatkey; float repeattime; } gamepaddevices[] = {{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET}}; static int keyboardid[] = {0}; static int mouseid[] = {0,1,2,3,4,5,6,7}; static cvar_t *xr_enable; //refrains from starting up when 0 and closes it too, and forces it off too static cvar_t *xr_metresize; static cvar_t *xr_skipregularview; static void WebXR_Toggle_f(void) { emscriptenfte_xr_setup(-3); //toggle it. } static void WebXR_Start_f(void) { emscriptenfte_xr_setup(-2); //start it (any mode) } static void WebXR_End_f(void) { emscriptenfte_xr_setup(-1); //end it (if running) } static void WebXR_Start_Inline_f(void) { emscriptenfte_xr_setup(-1); //end it (if running) emscriptenfte_xr_setup(0); //start it (inline mode) } static void WebXR_Start_VR_f(void) { emscriptenfte_xr_setup(-1); //end it (if running) emscriptenfte_xr_setup(1); //start it (inline mode) } static void WebXR_Start_AR_f(void) { emscriptenfte_xr_setup(-1); //end it (if running) emscriptenfte_xr_setup(2); //start it (inline mode) } static void WebXR_Info_f(void) { int modes = emscriptenfte_xr_issupported(); if (modes == 0) { Con_Printf(S_COLOR_RED"WebXR is unavailable\n"); return; } if (modes < 0) Con_Printf("WebXR availability is unknown\n"); else { Con_Printf("WebXR-inline is %savailable\n", (modes & (1<<0))?S_COLOR_GREEN:S_COLOR_RED"un"); Con_Printf("WebXR-immersive-vr is %savailable\n", (modes & (1<<1))?S_COLOR_GREEN:S_COLOR_RED"un"); Con_Printf("WebXR-immersive-ar is %savailable\n", (modes & (1<<2))?S_COLOR_GREEN:S_COLOR_RED"un"); if (emscriptenfte_xr_isactive()) Con_Printf("WebXR is active\n"); else Con_Printf("WebXR is inactive\n"); } } static qboolean WebXR_Prepare (vrsetup_t *setupinfo) { //called before graphics context init. basically just checks if we can do vr. xr_enable = Cvar_Get2("xr_enable", "1", CVAR_SEMICHEAT, "Controls whether to use webxr rendering or not.", "WebXR configuration"); xr_metresize = Cvar_Get2("xr_metresize", "26.24671916", CVAR_ARCHIVE, "Size of a metre in game units", "WebXR configuration"); xr_skipregularview = Cvar_Get2("xr_skipregularview", "1", CVAR_ARCHIVE, "Skip rendering the regular view when OpenXR is active", "WebXR configuration"); return xr_enable->ival;//emscriptenfte_xr_issupported() != 0; } static qboolean WebXR_Init (vrsetup_t *setupinfo, rendererstate_t *info) { //called after graphics context init. Cmd_AddCommand("xr_toggle", WebXR_Toggle_f); Cmd_AddCommand("xr_start", WebXR_Start_f); Cmd_AddCommand("xr_end", WebXR_End_f); Cmd_AddCommand("xr_start_inline", WebXR_Start_Inline_f); Cmd_AddCommand("xr_start_vr", WebXR_Start_VR_f); Cmd_AddCommand("xr_start_ar", WebXR_Start_AR_f); Cmd_AddCommand("xr_info", WebXR_Info_f); return true; //we don't have much control here, so we have no failure paths here. probably the session isn't even created. } static unsigned int WebXR_SyncFrame (double *frametime) { //called in the client's main loop, to block/tweak frame times. True means the game should render as fast as possible. return emscriptenfte_xr_isactive(); //we're using openxr's session's sync stuff when this is active. } static void WebXR_MatrixToQuake(const float in[16], float out[12]) { float tempmat[16], tempmat2[16]; const float fixupmat[16] = { 0, 0, -1, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1}; const float reorient[16] = { 0, -1, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 1}; Matrix4_Multiply(in, fixupmat, tempmat2);//we want z-up... Matrix4_Multiply(reorient, tempmat2, tempmat);//rotate it. //transpose it cos smaller? urgh. out[ 0] = tempmat[0]; out[ 1] = tempmat[4]; out[ 2] = tempmat[8]; out[ 3] = tempmat[12]; out[ 4] = tempmat[1]; out[ 5] = tempmat[5]; out[ 6] = tempmat[9]; out[ 7] = tempmat[13]; out[ 8] = tempmat[2]; out[ 9] = tempmat[6]; out[10] = tempmat[10]; out[11] = tempmat[14]; //fix up offset scaling parts out[3] *= xr_metresize->value; out[7] *= xr_metresize->value; out[11]*= xr_metresize->value; } static qboolean WebXR_Render (void(*rendereye)(texid_t tex, const pxrect_t *viewport, const vec4_t fovoverride, const float projmatrix[16], const float eyematrix[12])) { //calls rendereye for each view we're meant to be drawing. //webxr uses separate viewpoints on the same fbo struct webxrinfo_s eye[16]; fbostate_t fbo; int oldfbo = 0; float eyematrix[12]; pxrect_t vp; int e, eyes = emscriptenfte_xr_geteyeinfo(countof(eye), eye); if (!eyes) return false; //erk? lets just do normal drawing. for (e = 0; e < eyes; e++) { fbo.fbo = eye[e].fbo; if (!e) oldfbo = GLBE_FBO_Push(&fbo); else GLBE_FBO_Push(&fbo); if (!eye[e].viewport[2] || !eye[e].viewport[3]) continue; //no pixels getting drawn... don't waste time (emulators that feel an urge to give a dodgy eye to report two eyes instead of one) vp.x = eye[e].viewport[0]; vp.width = eye[e].viewport[2]; vp.height = eye[e].viewport[3]; vp.maxheight = vp.y+vp.height; //negatives suck. vp.y = vp.maxheight-eye[e].viewport[1]-vp.height; //opengl sucks WebXR_MatrixToQuake(eye[e].transform, eyematrix); //really just a pointer to R_RenderEyeScene... rendereye(NULL, &vp, NULL, eye[e].projmatrix, eyematrix); } GLBE_FBO_Pop(oldfbo); if (!xr_enable->ival) emscriptenfte_xr_shutdown(); if (eye[e].fbo==0) return true; //always skip the non-vr screen when we're fighting over the same FB. return xr_skipregularview->ival; //skip non-vr rendering. } static void WebXR_Shutdown (void) { Cmd_RemoveCommand("xr_toggle"); Cmd_RemoveCommand("xr_start"); Cmd_RemoveCommand("xr_end"); Cmd_RemoveCommand("xr_start_inline"); Cmd_RemoveCommand("xr_start_vr"); Cmd_RemoveCommand("xr_start_ar"); Cmd_RemoveCommand("xr_info"); emscriptenfte_xr_shutdown(); } static struct plugvrfuncs_s webxrfuncs = { "WebXR", WebXR_Prepare, WebXR_Init, WebXR_SyncFrame, WebXR_Render, WebXR_Shutdown, }; static void *GLVID_getwebglfunction(char *functionname) { return NULL; } //the enumid is the value for the open function rather than the working id. static int J_AllocateDevID(void) { extern cvar_t in_skipplayerone; unsigned int id = (in_skipplayerone.ival?1:0), j; for (j = 0; j < countof(gamepaddevices);) { if (gamepaddevices[j++].id == id) { j = 0; id++; } } return id; } static void IN_GamePadButtonEvent(int joydevid, int button, int ispressed, int isstandardmapping) { //note that the gamepad API handles 'buttons' as float values, so triggers are here instead of as 'axis' values (unlike other APIs). on the plus side, we're no longer responsible for figuring out the required threshold value to denote a 'press', but we're not tracking half-presses and that's our fault and we don't care. static const int standardmapping[] = { //the order of these keys is different from that of xinput //however, the quake button codes should be the same. I really ought to define some K_ aliases for them. K_GP_A, K_GP_B, K_GP_X, K_GP_Y, K_GP_LEFT_SHOULDER, K_GP_RIGHT_SHOULDER, K_GP_LEFT_TRIGGER, K_GP_RIGHT_TRIGGER, K_GP_BACK, K_GP_START, K_GP_LEFT_STICK, K_GP_RIGHT_STICK, K_GP_DPAD_UP, K_GP_DPAD_DOWN, K_GP_DPAD_LEFT, K_GP_DPAD_RIGHT, K_GP_GUIDE, //K_GP_UNKNOWN }; if (joydevid < 0) { static const int standardxrmapping[] = { //Right Left K_GP_RIGHT_TRIGGER, K_GP_LEFT_TRIGGER, //Primary trigger/button K_GP_RIGHT_SHOULDER, K_GP_LEFT_SHOULDER, //Primary squeeze K_GP_TOUCHPAD, K_GP_MISC1, //Primary touchpad K_GP_RIGHT_STICK, K_GP_LEFT_STICK, //Primary thumbstick //'Additional inputs may be exposed after', which should be in some pseudo-prioritised order with the awkward ones last. K_GP_A, K_GP_DPAD_DOWN, K_GP_B, K_GP_DPAD_RIGHT, K_GP_X, K_GP_DPAD_LEFT, K_GP_Y, K_GP_DPAD_UP, }; button = button*2 + (joydevid != -1); //munge them into a single array cos I cba with all the extra conditionals if (button < countof(standardxrmapping)) button = standardxrmapping[button]; else return; //err... joydevid = countof(gamepaddevices)-1; } else if (isstandardmapping && button < countof(standardmapping)) button = standardmapping[button]; else if (button < 32+4) button = K_JOY1+button; else return; //err... if (joydevid < countof(gamepaddevices)) { if (DEVID_UNSET == gamepaddevices[joydevid].id) { if (!ispressed) return; //don't send axis events until its enabled. gamepaddevices[joydevid].id = J_AllocateDevID(); } if (ispressed) { gamepaddevices[joydevid].repeatkey = button; gamepaddevices[joydevid].repeattime = 1.0; } else if (gamepaddevices[joydevid].repeatkey == button) gamepaddevices[joydevid].repeatkey = 0; joydevid = gamepaddevices[joydevid].id; } IN_KeyEvent(joydevid, ispressed, button, 0); } static void IN_GamePadButtonRepeats(void) { int j; for (j = 0; j < countof(gamepaddevices); j++) { if (gamepaddevices[j].id == DEVID_UNSET) continue; if (!gamepaddevices[j].repeatkey) continue; gamepaddevices[j].repeattime -= host_frametime; if (gamepaddevices[j].repeattime < 0) { //it is time! gamepaddevices[j].repeattime = 0.25; //faster re-repeat than the initial delay. IN_KeyEvent(gamepaddevices[j].id, true, gamepaddevices[j].repeatkey, 0); //an extra down. no ups. } } } static void IN_GamePadAxisEvent(int joydevid, int axis, float value, int isstandardmapping) { static const struct { int axis; int poskey; //mostly for navigating menus, but oh well. int negkey; } standardmapping[] = { {GPAXIS_LT_RIGHT, K_GP_LEFT_THUMB_RIGHT, K_GP_LEFT_THUMB_LEFT}, {GPAXIS_LT_DOWN, K_GP_LEFT_THUMB_DOWN, K_GP_LEFT_THUMB_UP}, {GPAXIS_RT_RIGHT, K_GP_RIGHT_THUMB_RIGHT, K_GP_RIGHT_THUMB_LEFT}, {GPAXIS_RT_DOWN, K_GP_RIGHT_THUMB_DOWN, K_GP_RIGHT_THUMB_UP}, //this seems fucked. only 4 axis are defined as part of the standard mapping. triggers are implemented as buttons with a .value (instead of .pressed) but they don't seem to work at all. //emulating here should be giving dupes, but I don't know how else to get this shite to work properly. {GPAXIS_LT_AUX, K_GP_LEFT_TRIGGER,0}, {GPAXIS_RT_AUX, K_GP_RIGHT_TRIGGER,0}, }; int qdevid; int pos=0, neg=0; int qaxis; if (joydevid < 0) { static const int standardxrmapping[] = { //Right Left -1, -1, //Primary touchpad X -1, -1, //Primary touchpad Y GPAXIS_RT_RIGHT, GPAXIS_LT_RIGHT, //Primary thumbstick X GPAXIS_RT_DOWN, GPAXIS_LT_DOWN, //Primary thumbstick Y //'Additional inputs may be exposed after', which should be in some pseudo-prioritised order with the awkward ones last. }; qaxis = axis*2 + (joydevid != -1); //munge them into a single array cos I cba with all the extra conditionals if (qaxis < countof(standardxrmapping)) qaxis = standardxrmapping[qaxis]; else return; //err... joydevid = countof(gamepaddevices)-1; } else if (isstandardmapping) { if (axis >= 0 && axis < countof(standardmapping)) { pos = standardmapping[axis].poskey; neg = standardmapping[axis].negkey; qaxis = standardmapping[axis].axis; } else qaxis = axis; } else return; //random mappings? erk? if (joydevid < countof(gamepaddevices)) { qdevid = gamepaddevices[joydevid].id; if (qdevid == DEVID_UNSET) { if (value < -0.9 || value > 0.9) gamepaddevices[joydevid].id = J_AllocateDevID(); return; //don't send axis events until its enabled. } if (value > 0.5 && pos) { if (!(gamepaddevices[joydevid].axistobuttonp & (1u<value; org[1] = -px * xr_metresize->value; org[2] = py * xr_metresize->value; if (joydevid == -1) dev = "right"; else if (joydevid == -2) dev = "left"; else if (joydevid == -3) dev = "head"; else if (joydevid == -4) dev = "gaze"; else return; IN_SetHandPosition(dev, org, ang, NULL, NULL); } static void VID_Resized(int width, int height, float scale) { extern cvar_t vid_conautoscale, vid_conwidth; extern cvar_t vid_dpi_x, vid_dpi_y; vid.pixelwidth = width; vid.pixelheight = height; //Con_Printf("Resized: %i %i\n", vid.pixelwidth, vid.pixelheight); //if you're zooming in, it should stay looking like its zoomed. vid.dpi_x = 96*scale; vid.dpi_y = 96*scale; Cvar_ForceSetValue(&vid_dpi_x, vid.dpi_x); Cvar_ForceSetValue(&vid_dpi_y, vid.dpi_y); Cvar_ForceCallback(&vid_conautoscale); Cvar_ForceCallback(&vid_conwidth); } static unsigned int domkeytoquake(unsigned int code) { static const unsigned short tab[256] = { /* 0*/ 0,0,0,0,0,0,0,0, K_BACKSPACE,K_TAB,0,0,0,K_ENTER,0,0, /* 16*/ K_SHIFT,K_CTRL,K_ALT,K_PAUSE,K_CAPSLOCK,0,0,0,0,0,0,K_ESCAPE,0,0,0,0, /* 32*/ ' ',K_PGUP,K_PGDN,K_END,K_HOME,K_LEFTARROW,K_UPARROW,K_RIGHTARROW, K_DOWNARROW,0,0,0,K_PRINTSCREEN,K_INS,K_DEL,0, /* 48*/ '0','1','2','3','4','5','6','7', '8','9',0,';',0,'=',0,0, /* 64*/ 0,'a','b','c','d','e','f','g', 'h','i','j','k','l','m','n','o', /* 80*/ 'p','q','r','s','t','u','v','w', 'x','y','z',K_LWIN,K_RWIN,K_APP,0,0, /* 96*/ K_KP_INS,K_KP_END,K_KP_DOWNARROW,K_KP_PGDN,K_KP_LEFTARROW,K_KP_5,K_KP_RIGHTARROW,K_KP_HOME, K_KP_UPARROW,K_KP_PGDN,K_KP_STAR,K_KP_PLUS,0,K_KP_MINUS,K_KP_DEL,K_KP_SLASH, /*112*/ K_F1,K_F2,K_F3,K_F4,K_F5,K_F6,K_F7,K_F8,K_F9,K_F10,K_F11,K_F12,0,0,0,0, /*128*/ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, /*144*/ K_KP_NUMLOCK,K_SCRLCK,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, /*160*/ 0,0,0,'#',0,0,0,0, 0,0,0,0,0,'-',0,0, /*176*/ 0,0,0,0,0,0,0,0, 0,0,';','=',',','-','.','/', /*192*/ '`',0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, /*208*/ 0,0,0,0,0,0,0,0, 0,0,0,'[','\\',']','\'','`', /*224*/ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, /*240*/ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, }; if (!code) return 0; if (code >= sizeof(tab)/sizeof(tab[0])) { Con_DPrintf("You just pressed key %u, but I don't know what its meant to be\n", code); return 0; } if (!tab[code]) Con_DPrintf("You just pressed key %u, but I don't know what its meant to be\n", code); // Con_DPrintf("You just pressed dom key %u, which is quake key %u\n", code, tab[code]); return tab[code]; } static int DOM_KeyEvent(unsigned int devid, int down, int scan, int uni) { extern int shift_down; // Con_Printf("Key %s %i %i:%c\n", down?"down":"up", scan, uni, uni?(char)uni:' '); if (shift_down) { scan = domkeytoquake(scan); } else { scan = domkeytoquake(scan); } IN_KeyEvent(keyboardid[devid], down, scan, uni); //Chars which don't map to some printable ascii value get preventDefaulted. //This is to stop fucking annoying fucking things like backspace randomly destroying the page and thus game. //And it has to be conditional, or we don't get any unicode chars at all. //The behaviour browsers seem to give is retardedly unhelpful, and just results in hacks to detect keys that appear to map to ascii... //Preventing the browser from leaving the page etc should NOT mean I can no longer get ascii/unicode values, only that the browser stops trying to do something random due to the event. //If you are the person that decreed that this is the holy way, then please castrate yourself now. // if (scan == K_BACKSPACE || scan == K_LCTRL || scan == K_LALT || scan == K_LSHIFT || scan == K_RCTRL || scan == K_RALT || scan == K_RSHIFT) return true; // return false; } static int RemapTouchId(int id, qboolean final) { static int touchids[countof(mouseid)]; int i; if (!id) return id; for (i = 1; i < countof(touchids); i++) if (touchids[i] == id) { if (final) touchids[i] = 0; return mouseid[i]; } for (i = 1; i < countof(touchids); i++) if (touchids[i] == 0) { if (!final) touchids[i] = id; if (mouseid[i] == DEVID_UNSET) mouseid[i] = i; return mouseid[i]; } return id; } static void DOM_ButtonEvent(unsigned int devid, int down, int button) { devid = RemapTouchId(devid, !down); if (down == 2) { //fixme: the event is a float. we ignore that. while(button < 0) { IN_KeyEvent(devid, true, K_MWHEELUP, 0); IN_KeyEvent(devid, false, K_MWHEELUP, 0); button += 1; } while(button > 0) { IN_KeyEvent(devid, true, K_MWHEELDOWN, 0); IN_KeyEvent(devid, false, K_MWHEELUP, 0); button -= 1; } } else { //swap buttons 2 and 3, so rmb is still +forward by default and not +mlook. if (button == 2) button = 1; else if (button == 1) button = 2; if (button < 0) button = K_TOUCH; else button += K_MOUSE1; IN_KeyEvent(devid, down, button, 0); } } static void DOM_MouseMove(unsigned int devid, int abs, float x, float y, float z, float size) { devid = RemapTouchId(devid, false); IN_MouseMove(devid, abs, x, y, z, size); } static void DOM_LoadFile(char *loc, char *mime, int handle) { vfsfile_t *file = NULL; if (handle != -1) file = FSWEB_OpenTempHandle(handle); else { char str[1024]; if (!strcmp(mime, "joinurl") || !strcmp(mime, "observeurl") || !strcmp(mime, "connecturl")) { extern cvar_t spectator; if (!strcmp(mime, "joinurl")) Cvar_Set(&spectator, "0"); if (!strcmp(mime, "observeurl")) Cvar_Set(&spectator, "1"); Cbuf_AddText(va("connect %s\n", COM_QuotedString(loc, str, sizeof(str), false)), RESTRICT_INSECURE); return; } if (!strcmp(mime, "demourl")) { Cbuf_AddText(va("qtvplay %s\n", COM_QuotedString(loc, str, sizeof(str), false)), RESTRICT_INSECURE); return; } } //try and open it. generally downloading it from the server. if (!Host_RunFile(loc, strlen(loc), file)) { if (file) VFS_CLOSE(file); } } static void DOM_CbufAddText(const char *text) { Cbuf_AddText(text, RESTRICT_LOCAL); } static int VID_ShouldSwitchToFullscreen(void) { //if false, mouse grabs won't work and we'll be forced to touchscreen mode. //we can only go fullscreen when the user clicks something. //this means that the user will get pissed off at the fullscreen state changing when they first click on the menus after it loading up. //this is confounded by escape bringing up the menu. GRR IT CHANGED MODE!WTF IT CHANGED AGAIN FUCKING PIECE OF SHIT!. //annoying, but that's web browsers for you. the best thing we can do is to not regrab until they next click while actually back in the game. extern cvar_t vid_fullscreen; return !!vid_fullscreen.value && !Key_MouseShouldBeFree(); } qboolean GLVID_Init (rendererstate_t *info, unsigned char *palette) { vid_isfullscreen = true; if (!emscriptenfte_setupcanvas( info->width, info->height, VID_Resized, DOM_MouseMove, DOM_ButtonEvent, DOM_KeyEvent, DOM_LoadFile, DOM_CbufAddText, IN_GamePadButtonEvent, IN_GamePadAxisEvent, IN_GamePadOrientationEvent, VID_ShouldSwitchToFullscreen )) { Con_Printf("Couldn't set up canvas\n"); return false; } vid.activeapp = true; if (info->vr && !info->vr->Prepare(NULL)) info->vr = NULL; //not available. if (!GL_Init(info, GLVID_getwebglfunction)) return false; if (info->vr && !info->vr->Init(NULL, info)) return false; vid.vr = info->vr; qglViewport (0, 0, vid.pixelwidth, vid.pixelheight); VID_Resized(vid.pixelwidth, vid.pixelheight, 1); mouseactive = false; return true; } void GLVID_DeInit (void) { vid.activeapp = false; emscriptenfte_setupcanvas(-1, -1, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); GL_ForgetPointers(); } void GLVID_SwapBuffers (void) { //webgl doesn't support swapbuffers. //you can't use it for loading screens. //such things must result in waiting until the following frame. //although there IS a swapped-buffers event, which we should probably use in preference to requestanimationframe or whatever the call is. /* if (!vid_isfullscreen) { if (!in_windowed_mouse.value) { if (mouseactive) { IN_DeactivateMouse (); } } else { if ((key_dest == key_game||mouseusedforgui) && vid.activeapp) IN_ActivateMouse (); else if (!(key_dest == key_game || mouseusedforgui) || !vid.activeapp) IN_DeactivateMouse (); } } */ } qboolean GLVID_ApplyGammaRamps (unsigned int gammarampsize, unsigned short *ramps) { gammaworks = false; return gammaworks; } void GLVID_SetCaption(const char *text) { emscriptenfte_settitle(text); } void Sys_SendKeyEvents(void) { /*most callbacks happen outside our code, we don't need to poll for events - except for joysticks*/ qboolean shouldbefree = Key_MouseShouldBeFree(); emscriptenfte_updatepointerlock(in_windowed_mouse.ival && !shouldbefree, shouldbefree); emscriptenfte_polljoyevents(); IN_GamePadButtonRepeats(); } /*various stuff for joysticks, which we don't support in this port*/ void INS_Shutdown (void) { } void INS_ReInit (void) { } void INS_Move(void) { } void INS_Init (void) { //mneh, handy enough R_RegisterVRDriver(NULL, &webxrfuncs); } void INS_Accumulate(void) { } void INS_Commands (void) { } void INS_EnumerateDevices(void *ctx, void(*callback)(void *ctx, const char *type, const char *devicename, unsigned int *qdevid)) { size_t i; char foobar[64]; for (i = 0; i < countof(gamepaddevices); i++) { Q_snprintfz(foobar, sizeof(foobar), "gp%i", (int)i); callback(ctx, "gamepad", foobar, &gamepaddevices[i].id); } for (i = 0; i < countof(mouseid); i++) { Q_snprintfz(foobar, sizeof(foobar), "m%i", (int)i); callback(ctx, "mouse", foobar, &mouseid[i]); } for (i = 0; i < countof(keyboardid); i++) { Q_snprintfz(foobar, sizeof(foobar), "kb%i", (int)i); callback(ctx, "keyboard", foobar, &keyboardid[i]); } } enum controllertype_e INS_GetControllerType(int id) { size_t i; for (i = 0; i < countof(gamepaddevices); i++) { if (id == gamepaddevices[i].id) return CONTROLLER_UNKNOWN; //browsers don't really like providing more info, to thwart fingerprinting. shame. you should just use generic glyphs. } return CONTROLLER_NONE; //nuffin here. yay fingerprinting? } void INS_Rumble(int joy, quint16_t amp_low, quint16_t amp_high, quint32_t duration) { } void INS_RumbleTriggers(int joy, quint16_t left, quint16_t right, quint32_t duration) { } void INS_SetLEDColor(int id, vec3_t color) { } void INS_SetTriggerFX(int id, const void *data, size_t size) { } ================================================ FILE: engine/web/prejs.js ================================================ //Populate our filesystem from Module['files'] FTEH = {h: [], f: {}}; FTE_SW=null; if (!Module['canvas']) { //we need a canvas to throw our webgl crap at... Module['canvas'] = document.getElementById('canvas'); if (!Module['canvas']) { console.log("No canvas element defined yet."); Module.canvas = document.createElement("canvas"); Module.canvas.style.width="100%"; Module.canvas.style.height="100%"; document.body.appendChild(Module['canvas']); } } Module['loadcachedfiles'] = function() { //recover any previously saved files they might have drag+dropped. addRunDependency("loadcachedfiles"); try { caches.open('user').then((c)=>{Module['cache']=c;return c.keys();}).then((keys)=>{ const cache = Module['cache']; for(var r of keys) { const idx = r.url.indexOf("/_/") if (idx < 0) continue; //wtf? that entry should not have been in this cache object. const fn = r.url.substr(idx+3); addRunDependency(fn); const response = cache.match(r).then((response)=>{return response.arrayBuffer();}).then((buffer)=>{ let b = FTEH.h[_emscriptenfte_buf_createfromarraybuf(buffer)]; b.n = fn; FTEH.f[b.n] = b; }).finally(()=>{removeRunDependency(fn);}); } }).finally(()=>{removeRunDependency("loadcachedfiles");}); } catch(e) { removeRunDependency("loadcachedfiles"); } }; Module['preRun'] = Module['loadcachedfiles']; if (typeof Module['files'] !== "undefined" && Object.keys(Module['files']).length>0) { Module['preRun'] = function() { Module['loadcachedfiles'](); Module['curfile'] = undefined; let files = Module['files']; let names = Object.keys(files); for (let i = 0; i < names.length; i++) { let ab = files[names[i]]; let n = names[i]; if (typeof ab == "string") { //if its a string, assume it to be a url of some kind for us to resolve. addRunDependency(n); let xhr = new XMLHttpRequest(); xhr.responseType = "arraybuffer"; xhr.open("GET", ab); xhr.onload = function () { if (Module['curfile'] == n) Module['curfile'] = undefined; if (this.status >= 200 && this.status < 300) { let b = FTEH.h[_emscriptenfte_buf_createfromarraybuf(this.response)]; b.n = n; FTEH.f[b.n] = b; removeRunDependency(n); } else removeRunDependency(n); }; xhr.onprogress = function(e) { if (typeof Module['curfile'] == "undefined") Module['curfile'] = n; //take it. if (Module['setStatus'] && Module['curfile']==n) Module['setStatus'](n + ' (' + e.loaded + '/' + e.total + ')'); }; xhr.onerror = function () { if (Module['curfile'] == n) Module['curfile'] = undefined; removeRunDependency(n); }; xhr.send(); } else if (typeof ab.then == "function") { //a 'thenable' thing... assume it'll resolve into an arraybuffer. addRunDependency(n); ab.then( value => { //success let b = FTEH.h[_emscriptenfte_buf_createfromarraybuf(value)]; b.n = n; FTEH.f[b.n] = b; removeRunDependency(n); }, reason => { //failure console.log(reason); removeRunDependency(n); } ); } else { //otherwise assume array buffer. let b = FTEH.h[_emscriptenfte_buf_createfromarraybuf(ab)]; b.n = n; FTEH.f[b.n] = b; } } } } else if (!Module['manifest']) { let man = window.location.protocol + "//" + window.location.host + window.location.pathname; if (man.substr(-1) != '/') man += ".fmf"; else man += "index.fmf"; Module['manifest'] = man; if (window.location.hash != "") Module['manifest'] = window.location.hash.substring(1); } if (!Module['arguments']) //the html can be explicit about its args if it sets this to an empty array or w/e { Module['arguments'] = []; // use query string in URL as command line const qstrings = decodeURIComponent(window.location.search.substring(1)); if (qstrings != "") { const qstring = qstrings.split(" "); for (let i = 0; i < qstring.length; i++) { if (qstring[i] == '-manifest') man = undefined; //don't do double manifest args... if ((qstring[i] == '+sv_port_rtc' || qstring[i] == '+connect' || qstring[i] == '+join' || qstring[i] == '+observe' || qstring[i] == '+qtvplay') && i+1 < qstring.length) { Module['arguments'] = Module['arguments'].concat(qstring[i+0], qstring[i+1]); i++; } else if (!document.referrer) //ignore args from referers in order to try to protect against dodgy srgs a little. Module['arguments'] = Module['arguments'].concat(qstring[i]); } } if (Module['manifest'] != undefined) Module['arguments'] = Module['arguments'].concat(['-manifest', Module['manifest']]); //registerProtocolHandler needs to be able to pass it through to us... so only allow it if we're parsing args from the url. Module['mayregisterscemes'] = true; } ================================================ FILE: engine/web/sys_web.c ================================================ #include "quakedef.h" #include #ifndef isDedicated qboolean isDedicated; #endif quakeparms_t parms; void Sys_Error (const char *error, ...) { va_list argptr; char string[1024]; va_start (argptr,error); vsnprintf (string, sizeof (string), error, argptr); va_end (argptr); COM_WorkerAbort(string); Sys_Printf("Error: %s\n", string); Con_Print ("Quake Error: "); Con_Print (string); Con_Print ("\n"); Host_Shutdown (); emscriptenfte_alert(string); emscriptenfte_abortmainloop("Sys_Error", true); exit (1); } void Sys_RecentServer(char *command, char *target, char *title, char *desc) { } qboolean Sys_RandomBytes(qbyte *string, int len) { return false; } static qboolean sys_supportsansi; static char ansiremap[8] = {'0', '4', '2', '6', '1', '5', '3', '7'}; static size_t ApplyColour(char *out, size_t outsize, unsigned int chrflags) { char *s, *e; static int oldflags = CON_WHITEMASK; int bg, fg; if (!sys_supportsansi) return 0; if (oldflags == chrflags) return 0; s = out; e = out+outsize; oldflags = chrflags; *out++='\x1b'; *out++='['; *out++='0'; *out++=';'; if (chrflags & CON_BLINKTEXT) { //set blink flag *out++='5'; *out++=';'; } bg = (chrflags & CON_BGMASK) >> CON_BGSHIFT; fg = (chrflags & CON_FGMASK) >> CON_FGSHIFT; // don't handle intensive bit for background // as terminals differ too much in displaying \e[1;7;3?m bg &= 0x7; if (chrflags & CON_NONCLEARBG) { if (fg & 0x8) // intensive bit set for foreground { // set bold/intensity ansi flag *out++='1'; *out++=';'; fg &= 0x7; // strip intensive bit } // set foreground and background colors *out++='3'; *out++=ansiremap[fg]; *out++=';'; *out++='4'; *out++=ansiremap[bg]; *out++='m'; } else { switch(fg) { //to get around wierd defaults (like a white background) we have these special hacks for colours 0 and 7 case COLOR_BLACK: // set inverse *out++='7'; *out++='m'; break; case COLOR_GREY: *out++='1'; *out++=';'; *out++='3'; *out++='0'; *out++='m'; break; case COLOR_WHITE: // set nothing else *out++='m'; break; default: if (fg & 0x8) // intensive bit set for foreground { // set bold/intensity ansi flag *out++='1'; *out++=';'; fg &= 0x7; // strip intensive bit } *out++='3'; *out++=ansiremap[fg]; *out++='m'; break; } } return out-s; } //print into stdout void Sys_Printf (char *fmt, ...) { va_list argptr; char text[65536]; conchar_t ctext[countof(text)], *e, *c; unsigned int len = 0; unsigned int w, codeflags; va_start (argptr,fmt); vsnprintf (text, sizeof(text), fmt, argptr); va_end (argptr); //make sense of any markup e = COM_ParseFunString(CON_WHITEMASK, text, ctext, sizeof(ctext), false); //convert to utf-8 for the js to make sense of for (c = ctext; c < e; ) { c = Font_Decode(c, &codeflags, &w); if (codeflags & CON_HIDDEN) continue; if ((codeflags&CON_RICHFORECOLOUR) || (w == '\n' && (codeflags&CON_NONCLEARBG))) codeflags = CON_WHITEMASK; //make sure we don't get annoying backgrounds on other lines. len+=ApplyColour(text+len,sizeof(text)-1-len, codeflags); //dequake it as required, so its only codepoints the browser will understand. should probably deal with linefeeds specially. if (w >= 0xe000 && w < 0xe100) { //quake-encoded mess if ((w & 0x7f) >= 0x20) w &= 0x7f; //regular (discoloured) ascii else if (w & 0x80) { //c1 glyphs static char tab[32] = "---#@.@@@@ # >.." "[]0123456789.---"; w = tab[w&31]; } else { //c0 glyphs static char tab[32] = ".####.#### # >.." "[]0123456789.---"; w = tab[w&31]; } } else if (w < ' ' && w != '\t' && w != '\r' && w != '\n') w = '?'; //c0 chars are awkward len += utf8_encode(text+len, w, sizeof(text)-1-len); } len+=ApplyColour(text+len,sizeof(text)-1-len, CON_WHITEMASK); //force it back to white at the end of the print... just in case text[len] = 0; //now throw it at the browser's console.log. emscriptenfte_print(text); } #if 1 //use Performance.now() instead of Date.now() - its likely to both provide higher precision and no NTP/etc issues. double Sys_DoubleTime (void) { double t = emscriptenfte_uptime_ms()/1000; //we need it as seconds... static double old = -99999999; if (t < old) t = old; //don't let t step backwards, ever. this shouldn't happen, but some CPUs don't keep their high-precision timers synced properly. return old=t; } unsigned int Sys_Milliseconds(void) { return Sys_DoubleTime() * (uint64_t)1000; } #else unsigned int Sys_Milliseconds(void) { static int first = true; static unsigned long oldtime = 0, curtime = 0; unsigned long newtime; newtime = emscriptenfte_ticks_ms(); //return Date.now() if (first) { first = false; oldtime = newtime; } if (newtime < oldtime) Con_Printf("Sys_Milliseconds stepped backwards!\n"); else curtime += newtime - oldtime; oldtime = newtime; return curtime; } //return the current time, in the form of a double double Sys_DoubleTime (void) { return Sys_Milliseconds() / 1000.0; } #endif //create a directory. we don't do dirs. void Sys_mkdir (const char *path) { } qboolean Sys_rmdir (const char *path) { return true; } //unlink a file qboolean Sys_remove (const char *path) { emscriptenfte_buf_delete(path); return true; } qboolean Sys_Rename (const char *oldfname, const char *newfname) { return emscriptenfte_buf_rename(oldfname, newfname); } qboolean Sys_GetFreeDiskSpace(const char *path, quint64_t *freespace) { //not implemented. we could try querying local storage quotas, but our filesystem is otherwise purely ram so doesn't have much of a limit in 64bit browsers. hurrah for swap space. *freespace = 0; //just in case. return false; } //someone used the 'quit' command #include "glquake.h" void Sys_Quit (void) { if (host_initialized) { qglClearColor(0,0,0,1); qglClear(GL_COLOR_BUFFER_BIT); Draw_FunString (0, 0, "Reload the page to restart"); Host_Shutdown(); } exit (0); } struct enumctx_s { char name[MAX_OSPATH]; const char *gpath; size_t gpathlen; const char *match; int (*callback)(const char *, qofs_t, time_t mtime, void *, searchpathfuncs_t *); void *ctx; searchpathfuncs_t *spath; int ret; }; static void Sys_EnumeratedFile(void *vctx, size_t fsize) { //called for each enumerated file. //we don't need the whole EnumerateFiles2 thing as our filesystem is flat, so */* isn't an issue for us (we don't expect a lot of different 'files' if only because they're a pain to download). struct enumctx_s *ctx = vctx; if (!ctx->ret) return; //we're meant to stop when if it returns false... if (!strncmp(ctx->name, ctx->gpath, ctx->gpathlen)) //ignore any gamedir prefix if (wildcmp(ctx->match, ctx->name+ctx->gpathlen)) //match it within the searched gamedir... ctx->ret = ctx->callback(ctx->name+ctx->gpathlen, fsize, 0, ctx->ctx, ctx->spath); //call the callback } int Sys_EnumerateFiles (const char *gpath, const char *match, int (*func)(const char *, qofs_t, time_t mtime, void *, searchpathfuncs_t *), void *parm, searchpathfuncs_t *spath) { struct enumctx_s ctx; char tmp[MAX_OSPATH]; if (!gpath) gpath = ""; ctx.gpathlen = strlen(gpath); if (ctx.gpathlen && gpath[ctx.gpathlen-1] != '/') { //make sure gpath is /-terminated. if (ctx.gpathlen >= sizeof(tmp)-1) return false; //just no... Q_strncpyz(tmp, gpath, sizeof(tmp)); gpath = tmp; tmp[ctx.gpathlen++] = '/'; } ctx.gpath = gpath; ctx.match = match; ctx.callback = func; ctx.ctx = parm; ctx.spath = spath; ctx.ret = true; emscritenfte_buf_enumerate(Sys_EnumeratedFile, &ctx, sizeof(ctx.name)); return ctx.ret; } //blink window if possible (it's not) void Sys_ServerActivity(void) { } void Sys_CloseLibrary(dllhandle_t *lib) { } dllhandle_t *Sys_LoadLibrary(const char *name, dllfunction_t *funcs) { return NULL; } void *Sys_GetAddressForName(dllhandle_t *module, const char *exportname) { return NULL; } void Sys_BrowserRedirect_f(void) { emscriptenfte_window_location(Cmd_Argv(1)); } void Sys_OpenFile_f(void) { emscriptenfte_openfile(); } static void Sys_Register_File_Associations_f(void) { //we should be able to register 'web+foo://' schemes here. we can't skip the web+ part though, which is a shame. const char *s; char scheme[MAX_OSPATH]; const char *schemes = fs_manifest->schemes; for (s = schemes; (s=COM_ParseOut(s,scheme,sizeof(scheme)));) { EM_ASM({ try{ if (navigator.registerProtocolHandler) navigator.registerProtocolHandler( UTF8ToString($0), document.location.origin+document.location.pathname+"?%s"+document.location.hash, UTF8ToString($1)); } catch(e){} }, va("%s%s", strncmp(scheme,"web+",4)?"web+":"", scheme), fs_manifest->formalname?fs_manifest->formalname:fs_manifest->installation); } } char *Sys_URIScheme_NeedsRegistering(void) { //we have no way to query if we're registered or not. cl_main will default to bypassing this. return NULL; } void Sys_Init(void) { extern cvar_t vid_width, vid_height, vid_fullscreen; //vid_fullscreen takes effect only on mouse clicks, any suggestion to do a vid_restart is pointless. vid_fullscreen.flags &= ~CVAR_VIDEOLATCH; //these are not really supported. so silence any spam that suggests we do something about something not even supported. vid_width.flags &= ~CVAR_VIDEOLATCH; vid_height.flags &= ~CVAR_VIDEOLATCH; Cmd_AddCommandD("sys_browserredirect", Sys_BrowserRedirect_f, "Navigates the browser to a different url. For sites using quake maps as a more interesting sitemap."); if (EM_ASM_INT(return window.showOpenFilePicker!=undefined;)) //doesn't work in firefox. Cmd_AddCommandD("sys_openfile", Sys_OpenFile_f, "Opens a file picker"); //opens file picker if (EM_ASM_INT(return Module['mayregisterscemes'] != false;)) //needs to be able to pass args via the url. don't bother adding the command if it'll fail. hurrah for checkcmd Cmd_AddCommandD("sys_register_file_associations", Sys_Register_File_Associations_f, "Register this page as the default handler for web+scheme handlers.\n"); //can't really do feature detection for this... either we spit out unreadable text or we don't... sys_supportsansi = EM_ASM_INT(return navigator.userAgent.indexOf("FireFox")!=-1;); } void Sys_Shutdown(void) { emscriptenfte_setupmainloop(NULL); } int VARGS Sys_DebugLog(char *file, char *fmt, ...) { return 0; }; qboolean Sys_InitTerminal(void) { return true; } char *Sys_ConsoleInput(void) { return NULL; } void Sys_CloseTerminal (void) { } int Sys_MainLoop(double newtime) { extern cvar_t vid_vsync; static double oldtime; double time; if (newtime) newtime /= 1000; //use RAF's timing for slightly greater precision. else newtime = Sys_DoubleTime (); //otherwise fall back on internally consistent timing... if (newtime < oldtime) newtime = oldtime; //don't let ourselves go backwards... if (!oldtime) oldtime = newtime; time = newtime - oldtime; if (!host_initialized) { Sys_Printf ("Starting "FULLENGINENAME"\n"); Host_Init (&parms); return 1; } oldtime = newtime; Host_Frame (time); return vid_vsync.ival; } int QDECL main(int argc, char **argv) { memset(&parms, 0, sizeof(parms)); parms.basedir = ""; parms.argc = argc; parms.argv = (const char**)argv; #ifdef CONFIG_MANIFEST_TEXT parms.manifest = CONFIG_MANIFEST_TEXT; #endif COM_InitArgv (parms.argc, parms.argv); TL_InitLanguages(""); emscriptenfte_setupmainloop(Sys_MainLoop); return 0; } qboolean Sys_GetDesktopParameters(int *width, int *height, int *bpp, int *refreshrate) { return false; } #ifdef WEBCLIENT qboolean Sys_RunInstaller(void) { //not implemented return false; } #endif /*static char *clipboard_buffer; void Sys_Clipboard_PasteText(clipboardtype_t cbt, void (*callback)(void *cb, const char *utf8), void *ctx) { callback(ctx, clipboard_buffer); } void Sys_SaveClipboard(clipboardtype_t cbt, const char *text) { free(clipboard_buffer); clipboard_buffer = strdup(text); }*/ #ifdef MULTITHREAD #include //FIXME: swap this out for sys_linux_threads.c (our pthreads code) /* Thread creation calls */ void *Sys_CreateThread(char *name, int (*func)(void *), void *args, int priority, int stacksize) { // SDL threads do not support setting thread stack size return (void *)SDL_CreateThread(func, args); } void Sys_WaitOnThread(void *thread) { SDL_WaitThread((SDL_Thread *)thread, NULL); } /* Mutex calls */ // SDL mutexes don't have try-locks for mutexes in the spec so we stick with 1-value semaphores void *Sys_CreateMutex(void) { return (void *)SDL_CreateSemaphore(1); } qboolean Sys_TryLockMutex(void *mutex) { return !SDL_SemTryWait(mutex); } qboolean Sys_LockMutex(void *mutex) { return !SDL_SemWait(mutex); } qboolean Sys_UnlockMutex(void *mutex) { return !SDL_SemPost(mutex); } void Sys_DestroyMutex(void *mutex) { SDL_DestroySemaphore(mutex); } /* Conditional wait calls */ typedef struct condvar_s { SDL_mutex *mutex; SDL_cond *cond; } condvar_t; void *Sys_CreateConditional(void) { condvar_t *condv; SDL_mutex *mutex; SDL_cond *cond; condv = (condvar_t *)malloc(sizeof(condvar_t)); if (!condv) return NULL; mutex = SDL_CreateMutex(); cond = SDL_CreateCond(); if (mutex) { if (cond) { condv->cond = cond; condv->mutex = mutex; return (void *)condv; } else SDL_DestroyMutex(mutex); } free(condv); return NULL; } qboolean Sys_LockConditional(void *condv) { return !SDL_mutexP(((condvar_t *)condv)->mutex); } qboolean Sys_UnlockConditional(void *condv) { return !SDL_mutexV(((condvar_t *)condv)->mutex); } qboolean Sys_ConditionWait(void *condv) { return !SDL_CondWait(((condvar_t *)condv)->cond, ((condvar_t *)condv)->mutex); } qboolean Sys_ConditionSignal(void *condv) { return !SDL_CondSignal(((condvar_t *)condv)->cond); } qboolean Sys_ConditionBroadcast(void *condv) { return !SDL_CondBroadcast(((condvar_t *)condv)->cond); } void Sys_DestroyConditional(void *condv) { condvar_t *cv = (condvar_t *)condv; SDL_DestroyCond(cv->cond); SDL_DestroyMutex(cv->mutex); free(cv); } #endif void Sys_Sleep (double seconds) { //SDL_Delay(seconds * 1000); } ================================================ FILE: engine/xdk/FTEQW_XDK.sln ================================================ Microsoft Visual Studio Solution File, Format Version 8.00 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FTEQW_XDK", "FTEQW_XDK.vcproj", "{BF8776BA-CCAB-4B5F-AE88-784B2215C589}" ProjectSection(ProjectDependencies) = postProject EndProjectSection EndProject Global GlobalSection(SolutionConfiguration) = preSolution Debug = Debug Profile = Profile Profile_FastCap = Profile_FastCap Release = Release Release_LTCG = Release_LTCG EndGlobalSection GlobalSection(ProjectConfiguration) = postSolution {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Debug.ActiveCfg = Debug|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Debug.Build.0 = Debug|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Profile.ActiveCfg = Profile|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Profile.Build.0 = Profile|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Profile_FastCap.ActiveCfg = Profile_FastCap|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Profile_FastCap.Build.0 = Profile_FastCap|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Release.ActiveCfg = Release|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Release.Build.0 = Release|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Release_LTCG.ActiveCfg = Release_LTCG|Xbox {BF8776BA-CCAB-4B5F-AE88-784B2215C589}.Release_LTCG.Build.0 = Release_LTCG|Xbox EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution EndGlobalSection GlobalSection(ExtensibilityAddIns) = postSolution EndGlobalSection EndGlobal ================================================ FILE: engine/xdk/FTEQW_XDK.vcproj ================================================ ================================================ FILE: fte.m4 ================================================ dnl dnl This file will be processed to provide relevant package lists for the various packages built from FTE's source. dnl The output will need to be combined with any other packages, and then signed for these packages to be considered valid. dnl Users can add extra sources with the `pkg addsource URL' console command (will show a prompt, to try to avoid exploits). dnl define(`DATE',FTE_DATE)dnl define(`REVISION',FTE_REVISION)dnl define(`DLSIZE',`esyscmd(`stat --printf="%s" $1')')dnl define(`SHA512',`esyscmd(`fteqw -sha512 $1')')dnl define(`URL',`url "$1" dlsize "DLSIZE($1)" sha512 "SHA512($1)" ')dnl define(`CAT',`$1$2$3$4$5$6$7$8$9')dnl define(`ZIP',`unzipfile "$2" URL($1$3)')dnl define(`FILE',`file "$1$2$3$4$5$6$7$8$9"')dnl define(`WINENGINE',`category "Engine" ver "REVISION" gamedir "" license "GPLv2" { arch "win_x86-FTE$1" FILE(fteqw_,REVISION,_32.exe) ZIP(win32/,CAT($2,.exe),CAT($2,_win32.zip)) } { arch "win_x64-FTE$1" FILE(fteqw_,REVISION,_64.exe) ZIP(win64/,CAT($2,64.exe),CAT($2,_win64.zip)) }')dnl define(`LINENGINE2',`{ arch "linux_amd64-FTE$1" FILE(fteqw_,REVISION,_64.bin) ZIP(linux_amd64/,CAT($2,64),$3) }')dnl define(`LINENGINE',`LINENGINE2($1,$2,CAT($2_lin64.zip))')dnl define(`HIDE',)dnl define(`GAME',`ifelse(FTE_GAME,`$1',`$2' ,)')dnl define(`TEST',`ifelse(FTE_TEST,`1',` test "1" ',` test "0" ')')dnl { package "fte_cl" WINENGINE(-m,fteqw) LINENGINE(-m,fteqw) title "CAT(`FTE Engine ',DATE)" desc "The awesome FTE engine (multi-renderer build)" TEST()dnl } HIDE(` { package "fte_cl_gl" dnl WINENGINE(-gl,fteglqw) dnl //don't bother advertising it on linux dnl title "CAT(`FTE Engine ',DATE,` - OpenGL'") desc "The awesome FTE engine (OpenGL-only build)" TEST()dnl } { package "fte_cl_vk" dnl WINENGINE(-vk,ftevkqw) dnl //don't bother advertising it on linux dnl title "CAT(`FTE Engine ',DATE,` - Vulkan')" desc "The awesome FTE engine (Vulkan-only build)" TEST()dnl } { package "fte_cl_d3d" dnl WINENGINE(-d3d,fted3dqw) //no d3d on linux dnl title "CAT(`FTE Engine ',DATE,` - Direct3D')" desc "The awesome FTE engine (Direct3D-only build)" TEST()dnl }')dnl { package "fte_sv" WINENGINE(-sv,fteqwsv) LINENGINE2(-sv,fteqw-sv,fteqwsv_lin64.zip) title "CAT(`FTE Engine ',DATE,` - Server')" desc "The awesome FTE engine (server-only build)" TEST()dnl } define(`WINPLUG',`category "Plugins" ver "REVISION" gamedir "" license "GPLv2" { arch "win_x86" FILE(fteplug_$1_x86.REVISION.dll) URL(win32/fteplug_$1_x86.dll) } { arch "win_x64" FILE(fteplug_$1_x64.REVISION.dll) URL(win64/fteplug_$1_x64.dll) }')dnl define(`WIN64PLUG',`category "Plugins" ver "REVISION" gamedir "" license "GPLv2" { arch "win_x64" FILE(fteplug_$1_x64.REVISION.dll) URL(win64/fteplug_$1_x64.dll) }')dnl define(`LINPLUG',`{ arch "linux_amd64" FILE(fteplug_$1_amd64.REVISION,.so) URL(linux_amd64/fteplug_$1_amd64.so) }')dnl GAME(quake, `{ package "fteplug_ezhud" WINPLUG(ezhud) LINPLUG(ezhud) title "EzHud Plugin" replace "ezhud" desc "Some lame alternative configurable hud." TEST()dnl }')dnl GAME(quake, `{ package "fteplug_qi" WINPLUG(qi) LINPLUG(qi) category "Plugins" title "Quake Injector Plugin" replace "Quake Injector Plugin" author "Spike" website "https://www.quaddicted.com/reviews/" desc "Provides a way to quickly list+install+load numerous different maps and mods. Some better than others." desc "If youre a single-player fan then these will keep you going for quite some time." desc "The database used is from quaddicted.com." TEST()dnl }')dnl { package "fteplug_irc" WINPLUG(irc) LINPLUG(irc) title "IRC Plugin" replace "IRC Plugin" desc "Allows you to converse on IRC servers in-game." desc "Requires manual configuration." TEST()dnl } { package "fteplug_xmpp" WINPLUG(xmpp) LINPLUG(xmpp) title "XMPP Plugin" desc "Allows you to converse on XMPP servers. This also includes a method for NAT holepunching between contacts." desc "Requires manual configuration." TEST()dnl } { package "fteplug_openssl" WINPLUG(openssl) license "GPLv3" //Apache2+GPLv2=GPLv3 title "OpenSSL Plugin" author "Spike" desc "Provides TLS and DTLS support, instead of using Microsoft's probably-outdated libraries." desc "Required for fully functional DTLS support on windows." desc "Connecting to QEx servers requires additional setup." TEST()dnl } { package "fteplug_hl2" WINPLUG(hl2) LINPLUG(hl2) title "HalfLife2 Formats Plugin" desc "Provides support for HalfLife2 bsp and texture formats." desc "Some related games may work, but this is not guarenteed." desc "Requires mod support for full functionality." TEST()dnl } HIDE(` { package "fteplug_models" WINPLUG(models) LINPLUG(models) title "Model exporter plugin" desc "" TEST()dnl } { package fteplug_ode TEST()dnl } { package fteplug_cef TEST()dnl } { package "fteplug_ffmpeg" { arch "win_x86" file "fteplug_ffmpeg_x86."#REVISION#".dll" url "win32/fteplug_ffmpeg_x86.dll" } { arch "win_x64" file "fteplug_ffmpeg_x64."#REVISION#".dll" url "win64/fteplug_ffmpeg_x64.dll" } // { // arch "linux_x64" // file "fteplug_ffmpeg_amd64."#REVISION#".so" // url "linux_amd64/fteplug_ffmpeg_amd64.so" // } ver REVISION category "Plugins" title "FFmpeg Plugin" file "fteplug_ffmpeg" gamedir "" TEST()dnl } { package "libffmpeg" { arch "win_x86" url "win32/ffmpeg-4.0-x86.zip" } { arch "win_x64" url "win64/ffmpeg-4.0-x64.zip" } // { // arch "linux_x64" // url "linux_amd64/ffmpeg-4.0-fteplug_ezhud_amd64.so" // } ver "4.0" category "Plugins" title "FFmpeg Library" extract "zip" gamedir "" } ')dnl GAME(quake2, `{ package "q2game_baseq2" { arch "win_x86" FILE(q2gamex86_baseq2.dll) URL(win32/q2gamex86_baseq2.dll) } { arch "win_x64" FILE(q2gamex64_baseq2.dll) URL(win64/q2gamex64_baseq2.dll) } { arch "linux_amd64" FILE(q2gameamd64_baseq2.so) URL(linux_amd64/q2gameamd64_baseq2.so) } ver "20190606" category "Mods" title "Gamecode: Base Game" license "GPLv2" website "https://github.com/yquake2/yquake2" desc "Quake2 Gamecode (from yamagiq2). Required for single player or servers." TEST()dnl }')dnl GAME(quake, `{ package "fte_csaddon" category "Plugins" title "Ingame Map Editor" ver REVISION gamedir "fte" FILE(csaddon.dat) URL(csaddon/csaddon.dat) desc "This is Spikes map editing user interface. It is only active while running singleplayer (or sv_cheats is enabled)." desc "To activate, set the ca_show cvar to 1 (suggestion: ^abind c toggle ca_show^a)." license "GPLv2, source on the fte svn" author "Spike" TEST()dnl }')dnl GAME(quake, `{ package "fte_menusys" category "AfterQuake" title "Replacement Menus" ver REVISION gamedir "fte" FILE(menu.dat) URL(csaddon/menu.dat) desc "This provides a more modern mouse-first menu system." license "GPLv2, source on the fte svn" author "Spike" TEST()dnl }')dnl ================================================ FILE: ftechrootbuild.sh ================================================ #!/bin/sh #Script to set up a debian chroot suitable for compiling fte's public builds. #deterministic builds are attempted but also requires: # gcc/etc versions must match exactly (we use debian oldstable, which should reduce issues...) # sourcecode must be unmodified, particuarly if 'svnversion' reports modified even in an irrelevant file then embedded revision numbers will be wrong. # third party dependancies need to work and not get messed up (either me failing to re-run makelibs, random wget failures, or outdated revisions being removed from public access) # obtained sourcecode revision must match the binary you're trying to duplicate (pre-5601 will insist on updating to latest svn (which may not even have a public build), so expect problems trying to duplicate older builds when the scripts instead try to grab the most recent build). #for regular use you should probably set up schroot so you don't need to remember so many args #requires about 2.3gb for the chroot+win64 build. #android and emscripten targets require proper mounting of /proc and /dev and are NOT supported by this script. don't try enabling them FTEBUILD=/tmp/ftebuild #change freely CHUID=1000 #should generally be your non-root user id, giving you the same access in or out of the chroot... CHUSER=spike #sadly this matters. youll just need to pretend to be me inside your chroot for now. DEBIANMIRROR=http://ftp.uk.debian.org/debian/ DEBIANVERSION=stretch #oldstable now... should update to stable, but paranoid to update due to portability of glibc symbols. LANG= #no language packs installed, so would be spammy if the following rules inherit the host lang #FTEREVISON="-r 5601" #earlier than 5601 will fail (scripts will try to update to latest) #THREADS="-j 8" #override number of threads to compile with, if you have a decent cpu. #package lists #general packages required to get the build system working (python+unzip+etc is for third-party dependancies) GENERALPACKAGES= subversion build-essential automake ca-certificates unzip p7zip-full zip libtool python pkg-config #package list needed to crosscompile for windows WINTARGETPACKAGES= mingw-w64 #dev packages required to compile the linux build properly. Comment out for less downloads/diskusage LINUXTARGETPACKAGES= gcc-multilib g++-multilib mesa-common-dev libasound2-dev libxcursor-dev libgnutls28-dev #NOTE: chroot does NOT wipe all environment settings. some get carried over. This is a problem if your distro has a default PATH that excludes the system programs on debian, so this is included to be sure. export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games #Set up our chroot (you can skip this part entirely if you're preconfigured a VM) #make sure debootstrap is installed, without erroring out if you're not on debian-derivative (NOTE: such users will needs to manually install it first from somewhere!) (which apt-get>/dev/null) && apt-get install --no-install-recommends debootstrap #create the new debian chroot. it should receive the most recent versions of packages. debootstrap $DEBIANVERSION $FTEBUILD $DEBIANMIRROR echo "FTEBuild">$FTEBUILD/etc/debian_chroot #optional, so it shows if you decide to run a bash prompt inside the chroot. chroot $FTEBUILD adduser --uid $CHUID $CHUSER #create a user (with a homedir), so we dont depend upon root inside the guest, where possible #Install the extra packages needed to build chroot $FTEBUILD apt-get install --no-install-recommends $GENERALPACKAGES $WINTARGETPACKAGES $LINUXTARGETPACKAGES #NOW we finally start with non-debian downloads by grabbing the FTE sourcecode chroot $FTEBUILD su $CHUSER -c "svn checkout https://svn.code.sf.net/p/fteqw/code/trunk ~/quake/fteqw-code $FTEREVISON" #grab all the source code. #FTE has some setup bollocks, which does some toolchain checks and such. You can choose which targets to build here. #NOTE: the following line will download third-party packages. chroot $FTEBUILD su $CHUSER -c "cd ~/quake/fteqw-code && ./build_setup.sh --noupdate" #And finally the main rebuild thing. drop the --noupdate part if you want to build the latest-available revision. chroot $FTEBUILD su $CHUSER -c "cd ~/quake/fteqw-code && ./build_wip.sh --noupdate $THREADS" #to remove your chroot afterwards: #rm --one-file-system -rf $FTEBUILD ================================================ FILE: fteqtv/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ================================================ FILE: fteqtv/Makefile ================================================ CC=$(TOOLPREFIX)gcc AR=$(TOOLPREFIX)ar RANLIB=$(TOOLPREFIX)ranlib STRIP=$(TOOLPREFIX)strip SPIKEISALAZYBUGGERCFLAGS=-Wall -O2 STRIPFLAGS=--strip-unneeded --remove-section=.comment # 'cause Microsoft suck. CLIBNAME = c VPATH=../engine/common OBJS = netchan.o parse.o msg.o qw.o source.o bsp.o rcon.o mdfour.o crc.o control.o forward.o pmove.o menu.o httpsv.o sha1.o #I hate this big long list. INCLUDES=-I../engine/common -I../engine/client -I../engine/server -I../engine/qclib -I../engine/gl REVISION:=-DSVNREVISION=$(shell svnversion) all: qtv %.o: %.c $(CC) $(SPIKEISALAZYBUGGERCFLAGS) $(CFLAGS) $(INCLUDES) $(REVISION) $< -c -o $@ libqtvc: $(MAKE) -C libqtvc CC="$(CC)" CFLAGS="$(CFLAGS) $(SPIKEISALAZYBUGGERCFLAGS)" AR="$(AR)" RANLIB="$(RANLIB)" libqtvc/libqtvc.a: libqtvc qtv: libqtvc/libqtvc.a $(OBJS) qtv.h $(CC) $(SPIKEISALAZYBUGGERCFLAGS) $(CFLAGS) $(OBJS) $(LDFLAGS) -o $@.db -lm -l$(CLIBNAME) -Llibqtvc -lqtvc $(STRIP) $(STRIPFLAGS) $@.db -o $@ qtv.exe: *.c *.h $(MAKE) qtv CFLAGS=-mno-cygwin LDFLAGS="-lwsock32 -lwinmm" CLIBNAME=msvcrt mv qtv qtv.exe clean: rm -rf qtv qtv.exe qtv.db *.o $(MAKE) -C libqtvc clean .PHONY: libqtvc ================================================ FILE: fteqtv/bsd_string.h ================================================ #include #include size_t strlcpy(char *dst, const char *src, size_t siz); ================================================ FILE: fteqtv/bsp.c ================================================ /* Copyright (C) 1996-1997 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. */ #include "qtv.h" #define MAX_MAP_LEAFS 32767 typedef struct { float planen[3]; float planedist; int child[2]; } node_t; struct bsp_s { unsigned int fullchecksum; unsigned int visedchecksum; node_t *nodes; unsigned char *pvslump; unsigned char **pvsofs; unsigned char decpvs[(MAX_MAP_LEAFS+7)/8]; //decompressed pvs int pvsbytecount; int numintermissionspots; intermission_t intermissionspot[8]; }; static const intermission_t nullintermissionspot = {{0}}; typedef struct { int contents; int visofs; // -1 = no visibility info short mins[3]; // for frustum culling short maxs[3]; unsigned short firstmarksurface; unsigned short nummarksurfaces; #define NUM_AMBIENTS 4 unsigned char ambient_level[NUM_AMBIENTS]; } dleaf_t; typedef struct { unsigned int planenum; short children[2]; // negative numbers are -(leafs+1), not nodes short mins[3]; // for sphere culling short maxs[3]; unsigned short firstface; unsigned short numfaces; // counting both sides } dnode_t; typedef struct { float normal[3]; float dist; unsigned int type; // PLANE_X - PLANE_ANYZ ?remove? trivial to regenerate } dplane_t; typedef struct { int fileofs, filelen; } lump_t; #define LUMP_ENTITIES 0 #define LUMP_PLANES 1 #define LUMP_VISIBILITY 4 #define LUMP_NODES 5 #define LUMP_LEAFS 10 #define HEADER_LUMPS 15 typedef struct { int version; lump_t lumps[HEADER_LUMPS]; } dheader_t; void DecompressVis(unsigned char *in, unsigned char *out, int bytecount) { int c; unsigned char *end; for (end = out + bytecount; out < end; ) { c = *in; if (!c) { //a 0 is always followed by the count of 0s. c = in[1]; in += 2; for (; c > 4; c-=4) { *(unsigned int*)out = 0; out+=4; } for (; c; c--) *out++ = 0; } else { in++; *out++ = c; } } } void BSP_LoadEntities(bsp_t *bsp, char *entitydata) { char *v; char key[2048]; char value[2048]; enum {et_random, et_startspot, et_primarystart, et_intermission} etype; float org[3]; float angles[3]; qboolean foundstartspot = false; float startspotorg[3]; float startspotangles[3]; //char *COM_ParseToken (char *data, char *out, int outsize, const char *punctuation) while (entitydata) { entitydata = COM_ParseToken(entitydata, key, sizeof(key), NULL); if (!entitydata) break; if (!strcmp(key, "{")) { org[0] = 0; org[1] = 0; org[2] = 0; angles[0] = 0; angles[1] = 0; angles[2] = 0; etype = et_random; for(;;) { if(!entitydata) { printf("unexpected eof in bsp entities section\n"); return; } entitydata = COM_ParseToken(entitydata, key, sizeof(key), NULL); if (!strcmp(key, "}")) break; entitydata = COM_ParseToken(entitydata, value, sizeof(value), NULL); if (!strcmp(key, "origin")) { v = value; v = COM_ParseToken(v, key, sizeof(key), NULL); org[0] = atof(key); v = COM_ParseToken(v, key, sizeof(key), NULL); org[1] = atof(key); v = COM_ParseToken(v, key, sizeof(key), NULL); org[2] = atof(key); } if (!strcmp(key, "angles") || !strcmp(key, "angle") || !strcmp(key, "mangle")) { v = value; v = COM_ParseToken(v, key, sizeof(key), NULL); angles[0] = atof(key); v = COM_ParseToken(v, key, sizeof(key), NULL); if (v) { angles[1] = atof(key); v = COM_ParseToken(v, key, sizeof(key), NULL); angles[2] = atof(key); } else { angles[1] = angles[0]; angles[0] = 0; angles[2] = 0; } } if (!strcmp(key, "classname")) { if (!strcmp(value, "info_player_start")) etype = et_primarystart; if (!strcmp(value, "info_deathmatch_start")) etype = et_startspot; if (!strcmp(value, "info_intermission")) etype = et_intermission; } } switch (etype) { case et_random: //a random (unknown) entity break; case et_primarystart: //a single player start memcpy(startspotorg, org, sizeof(startspotorg)); memcpy(startspotangles, angles, sizeof(startspotangles)); foundstartspot = true; break; case et_startspot: if (!foundstartspot) { memcpy(startspotorg, org, sizeof(startspotorg)); memcpy(startspotangles, angles, sizeof(startspotangles)); foundstartspot = true; } break; case et_intermission: if (bsp->numintermissionspots < sizeof(bsp->intermissionspot)/sizeof(bsp->intermissionspot[0])) { bsp->intermissionspot[bsp->numintermissionspots].pos[0] = org[0]; bsp->intermissionspot[bsp->numintermissionspots].pos[1] = org[1]; bsp->intermissionspot[bsp->numintermissionspots].pos[2] = org[2]; bsp->intermissionspot[bsp->numintermissionspots].angle[0] = angles[0]; bsp->intermissionspot[bsp->numintermissionspots].angle[1] = angles[1]; bsp->intermissionspot[bsp->numintermissionspots].angle[2] = angles[2]; bsp->numintermissionspots++; } break; } } else { printf("data not expected here\n"); return; } } if (foundstartspot && !bsp->numintermissionspots) { bsp->intermissionspot[bsp->numintermissionspots].pos[0] = startspotorg[0]; bsp->intermissionspot[bsp->numintermissionspots].pos[1] = startspotorg[1]; bsp->intermissionspot[bsp->numintermissionspots].pos[2] = startspotorg[2]; bsp->intermissionspot[bsp->numintermissionspots].angle[0] = startspotangles[0]; bsp->intermissionspot[bsp->numintermissionspots].angle[1] = startspotangles[1]; bsp->intermissionspot[bsp->numintermissionspots].angle[2] = startspotangles[2]; bsp->numintermissionspots++; } } bsp_t *BSP_LoadModel(cluster_t *cluster, char *gamedir, char *bspname) { unsigned char *data; unsigned int size; char *entdata; dheader_t *header; dplane_t *planes; dnode_t *nodes; dleaf_t *leaf; int numnodes, i; int numleafs; unsigned int chksum; bsp_t *bsp; data = FS_ReadFile(gamedir, bspname, &size); if (!data) { Sys_Printf(cluster, "Couldn't open bsp file \"%s\" (gamedir \"%s\")\n", bspname, gamedir); return NULL; } header = (dheader_t*)data; if (size < sizeof(dheader_t) || data[0] != 29) { free(data); Sys_Printf(cluster, "BSP not version 29 (%s in %s)\n", bspname, gamedir); return NULL; } for (i = 0; i < HEADER_LUMPS; i++) { if (LittleLong(header->lumps[i].fileofs) + LittleLong(header->lumps[i].filelen) > size) { free(data); Sys_Printf(cluster, "BSP appears truncated (%s in gamedir %s)\n", bspname, gamedir); return NULL; } } planes = (dplane_t*)(data+LittleLong(header->lumps[LUMP_PLANES].fileofs)); nodes = (dnode_t*)(data+LittleLong(header->lumps[LUMP_NODES].fileofs)); leaf = (dleaf_t*)(data+LittleLong(header->lumps[LUMP_LEAFS].fileofs)); entdata = (char*)(data+LittleLong(header->lumps[LUMP_ENTITIES].fileofs)); numnodes = LittleLong(header->lumps[LUMP_NODES].filelen)/sizeof(dnode_t); numleafs = LittleLong(header->lumps[LUMP_LEAFS].filelen)/sizeof(dleaf_t); bsp = malloc(sizeof(bsp_t) + sizeof(node_t)*numnodes + LittleLong(header->lumps[LUMP_VISIBILITY].filelen) + sizeof(unsigned char *)*numleafs); bsp->numintermissionspots = 0; if (bsp) { bsp->fullchecksum = 0; bsp->visedchecksum = 0; for (i = 0; i < HEADER_LUMPS; i++) { if (i == LUMP_ENTITIES) continue; //entities never appear in any checksums chksum = Com_BlockChecksum(data + LittleLong(header->lumps[i].fileofs), LittleLong(header->lumps[i].filelen)); bsp->fullchecksum ^= chksum; if (i == LUMP_VISIBILITY || i == LUMP_LEAFS || i == LUMP_NODES) continue; bsp->visedchecksum ^= chksum; } bsp->nodes = (node_t*)(bsp+1); bsp->pvsofs = (unsigned char**)(bsp->nodes+numnodes); bsp->pvslump = (unsigned char*)(bsp->pvsofs+numleafs); bsp->pvsbytecount = (numleafs+7)/8; for (i = 0; i < numnodes; i++) { bsp->nodes[i].child[0] = LittleShort(nodes[i].children[0]); bsp->nodes[i].child[1] = LittleShort(nodes[i].children[1]); bsp->nodes[i].planedist = planes[LittleLong(nodes[i].planenum)].dist; bsp->nodes[i].planen[0] = planes[LittleLong(nodes[i].planenum)].normal[0]; bsp->nodes[i].planen[1] = planes[LittleLong(nodes[i].planenum)].normal[1]; bsp->nodes[i].planen[2] = planes[LittleLong(nodes[i].planenum)].normal[2]; } memcpy(bsp->pvslump, data+LittleLong(header->lumps[LUMP_VISIBILITY].fileofs), LittleLong(header->lumps[LUMP_VISIBILITY].filelen)); for (i = 0; i < numleafs; i++) { if (leaf[i].visofs < 0) bsp->pvsofs[i] = NULL; else bsp->pvsofs[i] = bsp->pvslump+leaf[i].visofs; } } BSP_LoadEntities(bsp, entdata); free(data); return bsp; } void BSP_Free(bsp_t *bsp) { free(bsp); } int BSP_SphereLeafNums_r(bsp_t *bsp, int first, int maxleafs, unsigned short *list, float *pos, float radius) { node_t *node; float dot; int rn; int numleafs = 0; if (!bsp) return 0; for(rn = first;rn >= 0;) { node = &bsp->nodes[rn]; dot = (node->planen[0]*pos[0] + node->planen[1]*pos[1] + node->planen[2]*pos[2]) - node->planedist; if (dot < -radius) rn = node->child[1]; else if (dot > radius) rn = node->child[0]; else { rn = BSP_SphereLeafNums_r(bsp, node->child[0], maxleafs-numleafs, list+numleafs, pos, radius); if (rn < 0) return -1; //ran out, so don't use pvs for this entity. else numleafs += rn; rn = node->child[1]; //both sides } } rn = -1-rn; if (rn <= 0) ; //leaf 0 has no pvs info, so don't add it. else if (maxleafs>numleafs) { list[numleafs] = rn-1; numleafs++; } else return -1; //there are just too many return numleafs; } unsigned int BSP_Checksum(bsp_t *bsp) { if (!bsp) return 0; return bsp->visedchecksum; } int BSP_SphereLeafNums(bsp_t *bsp, int maxleafs, unsigned short *list, float x, float y, float z, float radius) { float pos[3]; pos[0] = x; pos[1] = y; pos[2] = z; return BSP_SphereLeafNums_r(bsp, 0, maxleafs, list, pos, radius); } int BSP_LeafNum(bsp_t *bsp, float x, float y, float z) { node_t *node; float dot; int rn; if (!bsp) return 0; for(rn = 0;rn >= 0;) { node = &bsp->nodes[rn]; dot = node->planen[0]*x + node->planen[1]*y + node->planen[2]*z; rn = node->child[(dot-node->planedist) <= 0]; } return -1-rn; } qboolean BSP_Visible(bsp_t *bsp, int leafcount, unsigned short *list) { int i; if (!bsp) return true; if (leafcount < 0) //too many, so pvs was switched off. return true; for (i = 0; i < leafcount; i++) { if (bsp->decpvs[list[i]>>3] & (1<<(list[i]&7))) return true; } return false; } void BSP_SetupForPosition(bsp_t *bsp, float x, float y, float z) { int leafnum; if (!bsp) return; leafnum = BSP_LeafNum(bsp, x, y, z); DecompressVis(bsp->pvsofs[leafnum], bsp->decpvs, bsp->pvsbytecount); } const intermission_t *BSP_IntermissionSpot(bsp_t *bsp) { int spotnum; if (bsp) { if (bsp->numintermissionspots>0) { spotnum = rand()%bsp->numintermissionspots; return &bsp->intermissionspot[spotnum]; } } return &nullintermissionspot; } ================================================ FILE: fteqtv/cmd.h ================================================ #define MAX_ARGS 8 #define ARG_LEN 512 typedef struct cmdctxt_s cmdctxt_t; struct cmdctxt_s { cluster_t *cluster; sv_t *qtv; int streamid; //streamid, which is valid even if qtv is not, for specifying the streamid to use on connects char *arg[MAX_ARGS]; int argc; void (*printfunc)(cmdctxt_t *ctx, char *str); void *printcookie; int printcookiesize; //tis easier qboolean localcommand; }; typedef void (*consolecommand_t) (cmdctxt_t *ctx); void Cmd_Printf(cmdctxt_t *ctx, char *fmt, ...) PRINTFWARNING(2); #define Cmd_Argc(ctx) ctx->argc #define Cmd_Argv(ctx, num) (((unsigned int)ctx->argc <= (unsigned int)(num))?"": ctx->arg[num]) #define Cmd_IsLocal(ctx) ctx->localcommand void Cmd_ExecuteNow(cmdctxt_t *ctx, char *command); char *Rcon_Command(cluster_t *cluster, sv_t *source, char *command, char *resultbuffer, int resultbuffersize, int islocalcommand);//prints the command prints to an internal buffer ================================================ FILE: fteqtv/control.c ================================================ /* Contains the control routines that handle both incoming and outgoing stuff */ #include "qtv.h" #include #include "bsd_string.h" #ifndef _WIN32 #include #include #else #include #endif typedef struct { char name[56]; int offset; int length; } pakfile; // PACK, offset, lengthofpakfiles FILE *FindInPaks(char *gamedir, char *filename, int *size) { FILE *f; char fname[1024]; int i, j; int numfiles; unsigned int header[3]; pakfile pf; for (i = 0; ; i++) { sprintf(fname, "%s/pak%i.pak", gamedir, i); f = fopen(fname, "rb"); if (!f) return NULL; //ran out of possible pak files. fread(header, 1, sizeof(header), f); if (header[0] != *(unsigned int*)"PACK") { //err... hmm. fclose(f); continue; } numfiles = LittleLong(header[2])/sizeof(pakfile); fseek(f, LittleLong(header[1]), SEEK_SET); for (j = 0; j < numfiles; j++) { fread(&pf, 1, sizeof(pf), f); if (!strcmp(pf.name, filename)) { fseek(f, LittleLong(pf.offset), 0); if (size) *size = LittleLong(pf.length); return f; } } fclose(f); //not found } return NULL; } unsigned char *FS_ReadFile2(char *gamedir, char *filename, unsigned int *sizep) { int size; unsigned char *data; FILE *f; char fname[1024]; if (!*filename) return NULL; //try and read it straight out of the file system sprintf(fname, "%s/%s", gamedir, filename); f = fopen(fname, "rb"); if (!f) f = fopen(filename, "rb"); //see if we're being run from inside the gamedir if (!f) { f = FindInPaks(gamedir, filename, &size); if (!f) f = FindInPaks("id1", filename, &size); if (!f) { return NULL; } } else { fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET); } data = malloc(size); if (data) fread(data, 1, size, f); fclose(f); if (sizep) *sizep = size; return data; } unsigned char *FS_ReadFile(char *gamedir, char *filename, unsigned int *size) { unsigned char *data; if (!gamedir || !*gamedir || !strcmp(gamedir, "qw")) data = NULL; else data = FS_ReadFile2(gamedir, filename, size); if (!data) { data = FS_ReadFile2("qw", filename, size); if (!data) { data = FS_ReadFile2("id1", filename, size); if (!data) { return NULL; } } } return data; } #ifndef _WIN32 #define _cdecl #endif int _cdecl SortFilesByDate(const void *a, const void *b) { if (((const availdemo_t*)a)->time < ((const availdemo_t*)b)->time) return 1; if (((const availdemo_t*)a)->time > ((const availdemo_t*)b)->time) return -1; if (((const availdemo_t*)a)->smalltime < ((const availdemo_t*)b)->smalltime) return 1; if (((const availdemo_t*)a)->smalltime > ((const availdemo_t*)b)->smalltime) return -1; return 0; } void Cluster_BuildAvailableDemoList(cluster_t *cluster) { cluster->availdemoscount = 0; #ifdef _WIN32 { WIN32_FIND_DATA ffd; HANDLE h; char path[512]; snprintf(path, sizeof(path), "%s*.mvd", cluster->demodir); h = FindFirstFile(path, &ffd); if (h != INVALID_HANDLE_VALUE) { do { if (cluster->availdemoscount == sizeof(cluster->availdemos)/sizeof(cluster->availdemos[0])) break; strlcpy(cluster->availdemos[cluster->availdemoscount].name, ffd.cFileName, sizeof(cluster->availdemos[0].name)); cluster->availdemos[cluster->availdemoscount].size = ffd.nFileSizeLow; cluster->availdemos[cluster->availdemoscount].time = ffd.ftLastWriteTime.dwHighDateTime; cluster->availdemos[cluster->availdemoscount].smalltime = ffd.ftLastWriteTime.dwLowDateTime; cluster->availdemoscount++; } while(FindNextFile(h, &ffd)); FindClose(h); } } #else { DIR *dir; struct dirent *ent; struct stat sb; char fullname[512]; dir = opendir(cluster->demodir); //yeek! if (dir) { for(;;) { if (cluster->availdemoscount == sizeof(cluster->availdemos)/sizeof(cluster->availdemos[0])) break; ent = readdir(dir); if (!ent) break; if (*ent->d_name == '.') continue; //ignore 'hidden' files snprintf(fullname, sizeof(fullname), "%s%s", cluster->demodir, ent->d_name); if (stat(fullname, &sb)) continue; //some kind of error strlcpy(cluster->availdemos[cluster->availdemoscount].name, ent->d_name, sizeof(cluster->availdemos[0].name)); cluster->availdemos[cluster->availdemoscount].size = sb.st_size; cluster->availdemos[cluster->availdemoscount].time = sb.st_mtime; cluster->availdemoscount++; } closedir(dir); } else Sys_Printf(cluster, "Couldn't open dir %s for demo listings\n", cluster->demodir); } #endif qsort(cluster->availdemos, cluster->availdemoscount, sizeof(cluster->availdemos[0]), SortFilesByDate); } void Cluster_Run(cluster_t *cluster, qboolean dowait) { oproxy_t *pend, *pend2, *pend3; sv_t *sv, *old; tcpconnect_t *tc; int m; struct timeval timeout; fd_set socketset; fd_set socketset_wr; if (dowait) { //FIXME: use poll or epoll to work around FD_SETSIZE limits, though we're mostly only doing this for the sleeping. FD_ZERO(&socketset); FD_ZERO(&socketset_wr); m = 0; if (cluster->qwdsocket[0] != INVALID_SOCKET) { if (cluster->qwdsocket[0] < FD_SETSIZE) { FD_SET(cluster->qwdsocket[0], &socketset); if (cluster->qwdsocket[0] >= m) m = cluster->qwdsocket[0]+1; } } if (cluster->qwdsocket[1] != INVALID_SOCKET) { if (cluster->qwdsocket[1] < FD_SETSIZE) { FD_SET(cluster->qwdsocket[1], &socketset); if (cluster->qwdsocket[1] >= m) m = cluster->qwdsocket[1]+1; } } for (sv = cluster->servers; sv; sv = sv->next) { if (sv->usequakeworldprotocols && sv->sourcesock != INVALID_SOCKET) { if (sv->sourcesock >= FD_SETSIZE) continue; //panic... FD_SET(sv->sourcesock, &socketset); if (sv->sourcesock >= m) m = sv->sourcesock+1; } } for (tc = cluster->tcpconnects; tc; tc = tc->next) { if (tc->sock != INVALID_SOCKET && tc->sock < FD_SETSIZE) { FD_SET(tc->sock, &socketset); if (tc->sock >= m) m = tc->sock+1; } } TURN_AddFDs(cluster, &socketset, &m); for (pend = cluster->pendingproxies; pend; pend = pend->next) { if (pend->sock != INVALID_SOCKET && pend->sock < FD_SETSIZE) { FD_SET(pend->sock, &socketset); if (pend->file) //also wake up if we're doing some (large) file transfer and we can give them a bit more. FD_SET(pend->sock, &socketset_wr); if (pend->sock >= m) m = pend->sock+1; } } #ifndef _WIN32 #ifndef STDIN #define STDIN 0 #endif FD_SET(STDIN, &socketset); if (STDIN >= m) m = STDIN+1; #endif if (cluster->viewserver) { timeout.tv_sec = 0; timeout.tv_usec = 1000; } else { timeout.tv_sec = 10/1000; timeout.tv_usec = (100%1000)*1000; } m = select(m, &socketset, &socketset_wr, NULL, &timeout); #ifdef _WIN32 for (;;) { char buffer[8192]; char *result; char c; if (!_kbhit()) break; c = _getch(); if (c == '\n' || c == '\r') { Sys_Printf(cluster, "\n"); if (cluster->inputlength) { cluster->commandinput[cluster->inputlength] = '\0'; result = Rcon_Command(cluster, NULL, cluster->commandinput, buffer, sizeof(buffer), true); Sys_Printf(cluster, "%s", result); cluster->inputlength = 0; cluster->commandinput[0] = '\0'; } } else if (c == '\b') { if (cluster->inputlength > 0) { Sys_Printf(cluster, "%c", c); Sys_Printf(cluster, " "); Sys_Printf(cluster, "%c", c); cluster->inputlength--; cluster->commandinput[cluster->inputlength] = '\0'; } } else { Sys_Printf(cluster, "%c", c); if (cluster->inputlength < sizeof(cluster->commandinput)-1) { cluster->commandinput[cluster->inputlength++] = c; cluster->commandinput[cluster->inputlength] = '\0'; } } } #else if (FD_ISSET(STDIN, &socketset)) { char buffer[8192]; char *result; cluster->inputlength = read (STDIN, cluster->commandinput, sizeof(cluster->commandinput)); if (cluster->inputlength >= 1) { cluster->commandinput[cluster->inputlength-1] = 0; // rip off the /n and terminate cluster->inputlength--; if (cluster->inputlength) { cluster->commandinput[cluster->inputlength] = '\0'; result = Rcon_Command(cluster, NULL, cluster->commandinput, buffer, sizeof(buffer), true); printf("%s", result); cluster->inputlength = 0; cluster->commandinput[0] = '\0'; } } } #endif } cluster->curtime = Sys_Milliseconds(); for (sv = cluster->servers; sv; ) { old = sv; sv = sv->next; QTV_Run(old); } TURN_CheckFDs(cluster); SV_FindProxies(cluster->tcpsocket[0], cluster, NULL); //look for any other proxies wanting to muscle in on the action. SV_FindProxies(cluster->tcpsocket[1], cluster, NULL); //look for any other proxies wanting to muscle in on the action. QW_UpdateUDPStuff(cluster); while(cluster->pendingproxies) { pend2 = cluster->pendingproxies->next; if (SV_ReadPendingProxy(cluster, cluster->pendingproxies)) cluster->pendingproxies = pend2; else break; } if (cluster->pendingproxies) { for(pend = cluster->pendingproxies; pend && pend->next; ) { pend2 = pend->next; pend3 = pend2->next; if (SV_ReadPendingProxy(cluster, pend2)) { pend->next = pend3; pend = pend3; } else { pend = pend2; } } } } void DoCommandLine(cluster_t *cluster, int argc, char **argv) { int i; char commandline[8192]; char *result; char *arg; char buffer[8192]; //exec the - commands commandline[0] = '\0'; for (i = 1; i <= argc; i++) { if (i == argc) arg = ""; else { arg = argv[i]; if (!arg) //NeXT can do this supposedly arg = ""; } if(i == argc || *arg == '+' || *arg == '-') { if (commandline[0] == '-') { result = Rcon_Command(cluster, NULL, commandline+1, buffer, sizeof(buffer), true); Sys_Printf(cluster, "%s", result); } commandline[0] = '\0'; } strcat(commandline, arg); strcat(commandline, " "); } //exec the configs result = Rcon_Command(cluster, NULL, "exec qtv.cfg", buffer, sizeof(buffer), true); Sys_Printf(cluster, "%s", result); //exec the + commands commandline[0] = '\0'; for (i = 1; i <= argc; i++) { if (i == argc) arg = ""; else { arg = argv[i]; if (!arg) //NeXT can do this supposedly arg = ""; } if(i == argc || *arg == '+' || *arg == '-') { if (commandline[0] == '+') { result = Rcon_Command(cluster, NULL, commandline+1, buffer, sizeof(buffer), true); Sys_Printf(cluster, "%s", result); } commandline[0] = '\0'; } strcat(commandline, arg); strcat(commandline, " "); } } #ifndef LIBQTV int main(int argc, char **argv) { cluster_t *cluster; // soundtest(); #ifdef SIGPIPE signal(SIGPIPE, SIG_IGN); #endif #ifdef _WIN32 { WSADATA discard; WSAStartup(MAKEWORD(1,1), &discard); } #endif cluster = malloc(sizeof(*cluster)); if (cluster) { int j; memset(cluster, 0, sizeof(*cluster)); for (j = 0; j < SOCKETGROUPS; j++) { cluster->qwdsocket[j] = INVALID_SOCKET; cluster->tcpsocket[j] = INVALID_SOCKET; } cluster->anticheattime = 1*1000; cluster->tooslowdelay = 100; cluster->qwlistenportnum = 0; cluster->allownqclients = true; strcpy(cluster->hostname, DEFAULT_HOSTNAME); cluster->maxproxies = -1; //master protocol setup cluster->protocolname = strdup("FTE-Quake"); cluster->protocolver = 3; strlcpy(cluster->master, "master.frag-net.com:27950", sizeof(cluster->master)); //default to eukara's master server. cluster->mastersendtime = cluster->curtime; cluster->relayenabled = true; //allow qtv cluster->pingtreeenabled = false; //spammy. cluster->turnenabled = false; //leave turn off by default. we need to know a usable inbound port range, we can't depend on just outgoing ephemerial ones. misconfigured relays will result in failures so don't default this to on. #ifdef HAVE_EPOLL cluster->epfd = epoll_create1(0); #endif strcpy(cluster->demodir, "qw/demos/"); Sys_Printf(cluster, "QTV "QTV_VERSION_STRING"\n"); DoCommandLine(cluster, argc, argv); if (!cluster->numservers) { //probably running on a home user's computer if (cluster->qwdsocket[SG_IPV4] == INVALID_SOCKET && cluster->qwdsocket[SG_IPV6] == INVALID_SOCKET && !cluster->qwlistenportnum) { cluster->qwlistenportnum = 27599; NET_InitUDPSocket(cluster, cluster->qwlistenportnum, SG_IPV6); NET_InitUDPSocket(cluster, cluster->qwlistenportnum, SG_IPV4); } if (cluster->tcpsocket[SG_IPV4] == INVALID_SOCKET && cluster->tcpsocket[SG_IPV6] == INVALID_SOCKET && !cluster->tcplistenportnum) { cluster->tcplistenportnum = 27599; Net_TCPListen(cluster, cluster->tcplistenportnum, SG_IPV6); Net_TCPListen(cluster, cluster->tcplistenportnum, SG_IPV4); } Net_TCPListen(cluster, 1, SG_UNIX); Sys_Printf(cluster, "\n" "Welcome to QTV\n" "Please type\n" "qtv server:port\n" " to connect to a tcp server.\n" "qw server:port\n" " to connect to a regular qw server.\n" "demo qw/example.mvd\n" " to play a demo from an mvd.\n" "\n"); } // Cluster_BuildAvailableDemoList(cluster); while (!cluster->wanttoexit) { Cluster_Run(cluster, true); #ifdef VIEWER DemoViewer_Update(cluster->viewserver); #endif } free(cluster); } return 0; } #endif void QTV_Printf(sv_t *qtv, char *fmt, ...) { va_list argptr; char string[2048]; va_start (argptr, fmt); vsnprintf (string, sizeof(string)-1, fmt,argptr); string[sizeof(string)-1] = 0; va_end (argptr); if (qtv->silentstream) return; Sys_Printf(qtv->cluster, "%s", string); } //#ifdef LIBQTV //#ifndef _WIN32 //#define _cdecl //#endif //void _cdecl Con_Printf(char *fmt, ...); //#endif void Sys_Printf(cluster_t *cluster, char *fmt, ...) { va_list argptr; char string[2048]; unsigned char *t; va_start (argptr, fmt); vsnprintf (string, sizeof(string)-1, fmt,argptr); string[sizeof(string)-1] = 0; va_end (argptr); //#ifdef LIBQTV // Con_Printf("QTV: %s", string); //#endif for (t = (unsigned char*)string; *t; t++) { if (*t >= 146 && *t < 156) *t = *t - 146 + '0'; if (*t == 143) *t = '.'; if (*t == 157 || *t == 158 || *t == 159) *t = '-'; if (*t >= 128) *t -= 128; if (*t == 16) *t = '['; if (*t == 17) *t = ']'; if (*t == 29) *t = '-'; if (*t == 30) *t = '-'; if (*t == 31) *t = '-'; if (*t == '\a') //doh. :D *t = ' '; } printf("%s", string); } //FIXME: move this to an appropriate place #ifdef _WIN32 void Sys_mkdir(char *name) { _mkdir(name); } #elif defined(__linux__) void Sys_mkdir(char *name) { mkdir(name, 0777); } #else #warning no Sys_mkdir function defined, hope the default works for you void Sys_mkdir(char *name) { mkdir(name, 0777); } #endif void QTV_mkdir(char *path) { char *ofs; for (ofs = path+1 ; *ofs ; ofs++) { if (*ofs == '/') { // create the directory *ofs = 0; Sys_mkdir (path); *ofs = '/'; } } } /* unsigned char *FS_ReadFile2(char *gamedir, char *filename, unsigned int *sizep) { int size; unsigned char *data; FILE *f; char fname[1024]; if (!*filename) return NULL; //try and read it straight out of the file system sprintf(fname, "%s/%s", gamedir, filename); f = fopen(fname, "rb"); if (!f) f = fopen(filename, "rb"); //see if we're being run from inside the gamedir if (!f) { f = FindInPaks(gamedir, filename, &size); if (!f) f = FindInPaks("id1", filename, &size); if (!f) { return NULL; } } else { fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET); } data = malloc(size); if (data) fread(data, 1, size, f); fclose(f); if (sizep) *sizep = size; return data; } unsigned char *FS_ReadFile(char *gamedir, char *filename, unsigned int *size) { char *data; if (!gamedir || !*gamedir || !strcmp(gamedir, "qw")) data = NULL; else data = FS_ReadFile2(gamedir, filename, size); if (!data) { data = FS_ReadFile2("qw", filename, size); if (!data) { data = FS_ReadFile2("id1", filename, size); if (!data) { return NULL; } } } return data; } void Cluster_Run(cluster_t *cluster, qboolean dowait) { oproxy_t *pend, *pend2, *pend3; sv_t *sv, *old; int m; struct timeval timeout; fd_set socketset; if (dowait) { FD_ZERO(&socketset); m = 0; if (cluster->qwdsocket != INVALID_SOCKET) { FD_SET(cluster->qwdsocket, &socketset); if (cluster->qwdsocket >= m) m = cluster->qwdsocket+1; } for (sv = cluster->servers; sv; sv = sv->next) { if (sv->usequkeworldprotocols && sv->sourcesock != INVALID_SOCKET) { FD_SET(sv->sourcesock, &socketset); if (sv->sourcesock >= m) m = sv->sourcesock+1; } } #ifndef _WIN32 #ifndef STDIN #define STDIN 0 #endif FD_SET(STDIN, &socketset); if (STDIN >= m) m = STDIN+1; #endif if (cluster->viewserver) { timeout.tv_sec = 0; timeout.tv_usec = 1000; } else { timeout.tv_sec = 100/1000; timeout.tv_usec = (100%1000)*1000; } m = select(m, &socketset, NULL, NULL, &timeout); #ifdef _WIN32 for (;;) { char buffer[8192]; char *result; char c; if (!_kbhit()) break; c = _getch(); if (c == '\n' || c == '\r') { Sys_Printf(cluster, "\n"); if (cluster->inputlength) { cluster->commandinput[cluster->inputlength] = '\0'; result = Rcon_Command(cluster, NULL, cluster->commandinput, buffer, sizeof(buffer), true); Sys_Printf(cluster, "%s", result); cluster->inputlength = 0; cluster->commandinput[0] = '\0'; } } else if (c == '\b') { if (cluster->inputlength > 0) { Sys_Printf(cluster, "%c", c); Sys_Printf(cluster, " ", c); Sys_Printf(cluster, "%c", c); cluster->inputlength--; cluster->commandinput[cluster->inputlength] = '\0'; } } else { Sys_Printf(cluster, "%c", c); if (cluster->inputlength < sizeof(cluster->commandinput)-1) { cluster->commandinput[cluster->inputlength++] = c; cluster->commandinput[cluster->inputlength] = '\0'; } } } #else if (FD_ISSET(STDIN, &socketset)) { char buffer[8192]; char *result; cluster->inputlength = read (0, cluster->commandinput, sizeof(cluster->commandinput)); if (cluster->inputlength >= 1) { cluster->commandinput[cluster->inputlength-1] = 0; // rip off the /n and terminate cluster->inputlength--; if (cluster->inputlength) { cluster->commandinput[cluster->inputlength] = '\0'; result = Rcon_Command(cluster, NULL, cluster->commandinput, buffer, sizeof(buffer), true); printf("%s", result); cluster->inputlength = 0; cluster->commandinput[0] = '\0'; } } } #endif } cluster->curtime = Sys_Milliseconds(); for (sv = cluster->servers; sv; ) { old = sv; sv = sv->next; QTV_Run(old); } SV_FindProxies(cluster->tcpsocket, cluster, NULL); //look for any other proxies wanting to muscle in on the action. QW_UpdateUDPStuff(cluster); while(cluster->pendingproxies) { pend2 = cluster->pendingproxies->next; if (SV_ReadPendingProxy(cluster, cluster->pendingproxies)) cluster->pendingproxies = pend2; else break; } if (cluster->pendingproxies) { for(pend = cluster->pendingproxies; pend && pend->next; ) { pend2 = pend->next; pend3 = pend2->next; if (SV_ReadPendingProxy(cluster, pend2)) { pend->next = pend3; pend = pend3; } else { pend = pend2; } } } } */ ================================================ FILE: fteqtv/crc.c ================================================ /* Copyright (C) 1996-1997 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. */ /* crc.c */ #include "qtv.h" // this is a 16 bit, non-reflected CRC using the polynomial 0x1021 // and the initial and final xor values shown below... in other words, the // CCITT standard CRC used by XMODEM #define QCRC_INIT_VALUE 0xffff #define QCRC_XOR_VALUE 0x0000 static const unsigned short crctable[256] = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 }; void QCRC_Init(unsigned short *crcvalue) { *crcvalue = QCRC_INIT_VALUE; } void QCRC_ProcessByte(unsigned short *crcvalue, unsigned char data) { *crcvalue = (*crcvalue << 8) ^ crctable[(*crcvalue >> 8) ^ data]; } unsigned short QCRC_Value(unsigned short crcvalue) { return crcvalue ^ QCRC_XOR_VALUE; } unsigned short QCRC_Block (void *start, int count) { unsigned char *data = start; unsigned short crc; QCRC_Init (&crc); while (count--) crc = (crc << 8) ^ crctable[(crc >> 8) ^ *data++]; return crc; } ================================================ FILE: fteqtv/forward.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ /* This is the file responsible for handling incoming tcp connections. This includes mvd recording. Password checks and stuff are implemented here. This i server side stuff. */ #include "qtv.h" #include "time.h" #undef IN #define IN(x) buffer[(x)&(MAX_PROXY_BUFFER-1)] void CheckMVDConsistancy(unsigned char *buffer, int pos, int size) { /* int length; int msec, type; while(pos < size) { msec = IN(pos++); type = IN(pos++); if (type == dem_set) { pos+=8; continue; } if (type == dem_multiple) pos+=4; length = (IN(pos+0)<<0) + (IN(pos+1)<<8) + (IN(pos+2)<<16) + (IN(pos+3)<<24); pos+=4; if (length > MAX_MSGLEN) printf("too big (%i)\n", length); pos += length; } if (pos != size) printf("pos != size\n"); */ } void SV_FindProxies(SOCKET sock, cluster_t *cluster, sv_t *defaultqtv) { unsigned long nonblocking = true; oproxy_t *prox; if (sock == INVALID_SOCKET) return; sock = accept(sock, NULL, NULL); if (sock == INVALID_SOCKET) return; if (ioctlsocket (sock, FIONBIO, &nonblocking) == -1) { Sys_Printf(cluster, "failed to set client socket to nonblocking. dropping.\n"); closesocket(sock); //failed... return; } if (cluster->maxproxies >= 0 && cluster->numproxies >= cluster->maxproxies) { const char buffer[] = {dem_all, 1, 'P','r','o','x','y',' ','i','s',' ','f','u','l','l','.'}; send(sock, buffer, sizeof(buffer), 0); closesocket(sock); return; } prox = malloc(sizeof(*prox)); if (!prox) {//out of mem? closesocket(sock); return; } memset(prox, 0, sizeof(*prox)); prox->sock = sock; prox->file = NULL; cluster->numproxies++; prox->droptime = cluster->curtime + 5*1000; #if 1 prox->defaultstream = defaultqtv; prox->next = cluster->pendingproxies; cluster->pendingproxies = prox; #else prox->next = qtv->pendingproxies; qtv->pendingproxies = prox; Net_SendConnectionMVD(qtv, prox); #endif } void Fwd_ParseCommands(cluster_t *cluster, oproxy_t *prox) { netmsg_t buf; int packetlength; int bytes; bytes = NET_WebSocketRecv(prox->sock, &prox->websocket, prox->inbuffer+prox->inbuffersize, sizeof(prox->inbuffer)-prox->inbuffersize, NULL); if (bytes < 0) { if (qerrno != NET_EWOULDBLOCK && qerrno != NET_EAGAIN) //not a problem, so long as we can flush it later. { Sys_Printf(cluster, "network error from client proxy\n"); prox->drop = true; //drop them if we get any errors return; } bytes = 0; } else if (bytes == 0) { prox->drop = true; return; } prox->inbuffersize += bytes; for(;;) { if (prox->inbuffersize < 2) //we do need at least 3 bytes for anything useful break; packetlength = prox->inbuffer[0] + (prox->inbuffer[1]<<8); packetlength -= 2; //qqshka's inconsistent-upstream-sizes stupidity. if (packetlength+2 > prox->inbuffersize) break; InitNetMsg(&buf, prox->inbuffer+2, packetlength); buf.cursize = packetlength; while(buf.readpos < buf.cursize) { switch (ReadByte(&buf)) { case qtv_clc_stringcmd: { char stringbuf[1024]; ReadString(&buf, stringbuf, sizeof(stringbuf)); QTV_Printf(prox->stream, "ds: %s\n", stringbuf); } break; default: Sys_Printf(cluster, "Received unrecognized packet type from downstream proxy.\n"); buf.readpos = buf.cursize; break; } } packetlength+=2; memmove(prox->inbuffer, prox->inbuffer+packetlength, prox->inbuffersize - packetlength); prox->inbuffersize -= packetlength; } } void Net_TryFlushProxyBuffer(cluster_t *cluster, oproxy_t *prox) { unsigned char *buffer; int length; int bufpos; // if (prox->drop) // return; while (prox->bufferpos >= MAX_PROXY_BUFFER) { //so we never get any issues with wrapping.. prox->bufferpos -= MAX_PROXY_BUFFER; prox->buffersize -= MAX_PROXY_BUFFER; } bufpos = prox->bufferpos&(MAX_PROXY_BUFFER-1); length = prox->buffersize - prox->bufferpos; if (length > MAX_PROXY_BUFFER-bufpos) //cap the length correctly. length = MAX_PROXY_BUFFER-bufpos; if (!length) return; //already flushed. buffer = prox->buffer + bufpos; // CheckMVDConsistancy(prox->buffer, prox->bufferpos, prox->buffersize); if (bufpos+length > MAX_PROXY_BUFFER) Sys_Printf(cluster, "oversize flush\n"); if (prox->file) length = fwrite(buffer, 1, length, prox->file); else length = send(prox->sock, buffer, length, 0); switch (length) { case 0: //eof / they disconnected prox->drop = true; prox->flushing = false; break; case -1: length = qerrno; if (length != NET_EWOULDBLOCK && length != NET_EAGAIN) //not a problem, so long as we can flush it later. { Sys_Printf(cluster, "network error from client proxy\n"); prox->drop = true; //drop them if we get any errors prox->flushing = false; } break; default: prox->bufferpos += length; } } void Net_ProxySendString(cluster_t *cluster, oproxy_t *prox, void *buffer) { Net_ProxySend(cluster, prox, buffer, strlen(buffer)); } void Net_ProxySend(cluster_t *cluster, oproxy_t *prox, void *buffer, int length) { int wrap; if (!length) return; if (prox->websocket.websocket) { unsigned int c; int enclen = 0; int datatype = 2; //1=utf-8, 2=binary /*work out how much buffer space we'll need*/ if (datatype == 2) enclen += length; else { for (c = 0; c < length; c++) { if (((unsigned char*)buffer)[c] == 0 || ((unsigned char*)buffer)[c] >= 0x80) enclen += 2; else enclen += 1; } } if (prox->buffersize-prox->bufferpos + (enclen+4) > MAX_PROXY_BUFFER) { Net_TryFlushProxyBuffer(cluster, prox); //try flushing if (prox->buffersize-prox->bufferpos + (enclen+4) > MAX_PROXY_BUFFER) //damn, still too big. { //they're too slow. hopefully it was just momentary lag if (!prox->flushing) { printf("QTV client is too lagged\n"); prox->flushing = true; } return; } } if (enclen >= 126) { prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = 0x80|datatype; prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = 126; prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = enclen>>8; prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = enclen; } else { prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = 0x80|datatype; prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = enclen; } if (datatype == 2) { for(; length-->0; buffer = (char*)buffer+1) prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = *(unsigned char*)buffer; } else { //utf-8 (or really just bytes with upper bits truncated. sue me. while(length-->0) { c = *(unsigned char*)buffer; buffer = (char*)buffer+1; if (!c) c |= 0x100; /*will get truncated at the other end*/ if (c >= 0x80) { prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = 0xc0 | (c>>6); prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = 0x80 | (c & 0x3f); } else prox->buffer[prox->buffersize++&(MAX_PROXY_BUFFER-1)] = c; } } Net_TryFlushProxyBuffer(cluster, prox); //try flushing in a desperate attempt to reduce bugs in google chrome. return; } if (prox->buffersize-prox->bufferpos + length > MAX_PROXY_BUFFER) { Net_TryFlushProxyBuffer(cluster, prox); //try flushing if (prox->buffersize-prox->bufferpos + length > MAX_PROXY_BUFFER) //damn, still too big. { //they're too slow. hopefully it was just momentary lag if (!prox->flushing) { printf("QTV client is too lagged\n"); prox->flushing = true; } return; } } #if 1 //just simple prox->buffersize+=length; for (wrap = prox->buffersize-length; wrap < prox->buffersize; wrap++) { prox->buffer[wrap&(MAX_PROXY_BUFFER-1)] = *(unsigned char*)buffer; buffer = (char*)buffer+1; } #else //we don't do multiple wrappings, the above check cannot succeed if it were required. //find the wrap point wrap = prox->buffersize-(prox->buffersize&(MAX_PROXY_BUFFER-1)) + MAX_PROXY_BUFFER; wrap = wrap - (prox->buffersize&(MAX_PROXY_BUFFER-1)); //the ammount of data we can fit before wrapping. if (wrap > length) { //we don't wrap afterall memcpy(prox->buffer+(prox->buffersize)&(MAX_PROXY_BUFFER-1), buffer, length); prox->buffersize+=length; return; } memcpy(prox->buffer+prox->buffersize&(MAX_PROXY_BUFFER-1), buffer, wrap); buffer += wrap; length -= wrap; memcpy(prox->buffer, buffer, length); prox->buffersize+=length; #endif } void Prox_SendMessage(cluster_t *cluster, oproxy_t *prox, char *buf, int length, int dem_type, unsigned int playermask) { netmsg_t msg; char tbuf[16]; InitNetMsg(&msg, tbuf, sizeof(tbuf)); WriteByte(&msg, 0); WriteByte(&msg, dem_type); WriteLong(&msg, length); if (dem_type == dem_multiple) WriteLong(&msg, playermask); if (prox->buffersize-prox->bufferpos + length + msg.cursize > MAX_PROXY_BUFFER) { Net_TryFlushProxyBuffer(cluster, prox); //try flushing if (prox->buffersize-prox->bufferpos + length + msg.cursize > MAX_PROXY_BUFFER) //damn, still too big. { //they're too slow. hopefully it was just momentary lag prox->flushing = true; return; } } Net_ProxySend(cluster, prox, msg.data, msg.cursize); Net_ProxySend(cluster, prox, buf, length); } void Fwd_SendDownstream(sv_t *qtv, void *buffer, int length) { //broadcasts data to all client proxies, with dont-buffer oproxy_t *prox; for (prox = qtv->proxies; prox; prox = prox->next) { Prox_SendMessage(qtv->cluster, prox, buffer, length, dem_qtvdata, (unsigned int)-1); } } void Fwd_SayToDownstream(sv_t *qtv, char *message) { netmsg_t msg; char buffer[1024]; InitNetMsg(&msg, buffer, sizeof(buffer)); WriteByte(&msg, svc_print); WriteByte(&msg, PRINT_CHAT); WriteString2(&msg, "[QTV]"); WriteString(&msg, message); Fwd_SendDownstream(qtv, msg.data, msg.cursize); } void Prox_SendPlayerStats(sv_t *qtv, oproxy_t *prox) { char buffer[MAX_MSGLEN]; netmsg_t msg; int player, snum; InitNetMsg(&msg, buffer, sizeof(buffer)); for (player = 0; player < MAX_CLIENTS; player++) { for (snum = 0; snum < MAX_STATS; snum++) { if (qtv->map.players[player].stats[snum]) { if ((unsigned)qtv->map.players[player].stats[snum] > 255) { WriteByte(&msg, svc_updatestatlong); WriteByte(&msg, snum); WriteLong(&msg, qtv->map.players[player].stats[snum]); } else { WriteByte(&msg, svc_updatestat); WriteByte(&msg, snum); WriteByte(&msg, qtv->map.players[player].stats[snum]); } } } if (msg.cursize) { Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_stats|(player<<3), (1<map.players[i].active) // interesting, is this set to false if player disconnect from server? continue; flags = (DF_ORIGIN << 0) | (DF_ORIGIN << 1) | (DF_ORIGIN << 2) | (DF_ANGLES << 0) | (DF_ANGLES << 1) | (DF_ANGLES << 2) // angles is something what changed frequently, so may be not send it? | DF_EFFECTS | DF_SKINNUM // though it rare thingie, so better send it? | (qtv->map.players[i].dead ? DF_DEAD : 0) | (qtv->map.players[i].gibbed ? DF_GIB : 0) | DF_WEAPONFRAME // do we so really need it? | DF_MODEL; // generally, that why we wrote this function, so YES send this if (*qtv->map.players[i].userinfo && atoi(Info_ValueForKey(qtv->map.players[i].userinfo, "*spectator", buffer, sizeof(buffer)))) flags = DF_MODEL; // oh, that spec, just sent his model, may be even better ignore him? WriteByte (msg, svc_playerinfo); WriteByte (msg, i); WriteShort (msg, flags); WriteByte (msg, qtv->map.players[i].current.frame); // always sent for (j = 0 ; j < 3 ; j++) if (flags & (DF_ORIGIN << j)) WriteCoord (msg, qtv->map.players[i].current.origin[j], qtv->pext1); for (j = 0 ; j < 3 ; j++) if (flags & (DF_ANGLES << j)) WriteShort (msg, (qtv->map.players[i].current.angles[j]/360.0f)*0x10000); if (flags & DF_MODEL) // generally, that why we wrote this function, so YES send this WriteByte (msg, qtv->map.players[i].current.modelindex); if (flags & DF_SKINNUM) WriteByte (msg, qtv->map.players[i].current.skinnum); if (flags & DF_EFFECTS) WriteByte (msg, qtv->map.players[i].current.effects); if (flags & DF_WEAPONFRAME) WriteByte (msg, qtv->map.players[i].current.weaponframe); } } void Net_GreetingMessage(oproxy_t *prox) { char buffer[1024]; netmsg_t msg; InitNetMsg(&msg, buffer, sizeof(buffer)); WriteByte(&msg, svc_print); WriteByte(&msg, PRINT_HIGH); WriteString2(&msg, "Welcome to "); WriteString2(&msg, prox->stream->cluster->hostname); WriteString(&msg, "\n"); Prox_SendMessage(prox->stream->cluster, prox, msg.data, msg.cursize, dem_qtvdata, (unsigned)-1); } void Net_SendConnectionMVD(sv_t *qtv, oproxy_t *prox) { char buffer[MAX_MSGLEN*8]; netmsg_t msg; int prespawn; //only send connection data if there's actual data to be sent //if not, the other end will get the data when we receive it anyway. if (!*qtv->map.mapname) return; InitNetMsg(&msg, buffer, sizeof(buffer)); prox->flushing = false; BuildServerData(qtv, &msg, 0, NULL); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; for (prespawn = 0;prespawn >= 0;) { prespawn = SendList(qtv, prespawn, qtv->map.soundlist, svc_soundlist, &msg); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; } for (prespawn = 0;prespawn >= 0;) { prespawn = SendList(qtv, prespawn, qtv->map.modellist, svc_modellist, &msg); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; } Net_TryFlushProxyBuffer(qtv->cluster, prox); //that should be enough data to fill a packet. for(prespawn = 0;prespawn>=0;) { prespawn = Prespawn(qtv, 0, &msg, prespawn, MAX_CLIENTS-1); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; } //playerstates are delta-compressed, unfortunatly this isn't qwd (thanks to qqshka for showing my folly) Prox_SendInitialPlayers(qtv, prox, &msg); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; //we do need to send entity states. Prox_SendInitialEnts(qtv, prox, &msg); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; WriteByte(&msg, svc_stufftext); WriteString(&msg, "skins\n"); Prox_SendMessage(qtv->cluster, prox, msg.data, msg.cursize, dem_read, (unsigned)-1); msg.cursize = 0; Net_TryFlushProxyBuffer(qtv->cluster, prox); Prox_SendPlayerStats(qtv, prox); Net_TryFlushProxyBuffer(qtv->cluster, prox); if (!qtv->cluster->lateforward) Net_ProxySend(qtv->cluster, prox, qtv->buffer, qtv->forwardpoint); //send all the info we've not yet processed (but have already forwarded). if (prox->flushing) { Sys_Printf(qtv->cluster, "Connection data is too big, dropping proxy client\n"); prox->drop = true; //this is unfortunate... } else Net_TryFlushProxyBuffer(qtv->cluster, prox); } oproxy_t *Net_FileProxy(sv_t *qtv, char *filename) { oproxy_t *prox; FILE *f; f = fopen(filename, "wb"); if (!f) return NULL; //no full proxy check, this is going to be used by proxy admins, who won't want to have to raise the limit to start recording. prox = malloc(sizeof(*prox)); if (!prox) return NULL; memset(prox, 0, sizeof(*prox)); prox->sock = INVALID_SOCKET; prox->file = f; prox->next = qtv->proxies; qtv->proxies = prox; qtv->cluster->numproxies++; Net_SendConnectionMVD(qtv, prox); return prox; } qboolean Net_StopFileProxy(sv_t *qtv) { oproxy_t *prox; for (prox = qtv->proxies; prox; prox = prox->next) { if (prox->file) { prox->drop = true; return true; } } return false; } void SV_ForwardStream(sv_t *qtv, void *buffer, int length) { //forward the stream on to connected clients oproxy_t *prox, *next, *fre; CheckMVDConsistancy(buffer, 0, length); while (qtv->proxies && qtv->proxies->drop) { next = qtv->proxies->next; fre = qtv->proxies; if (fre->file) fclose(fre->file); else closesocket(fre->sock); free(fre); qtv->cluster->numproxies--; qtv->proxies = next; } for (prox = qtv->proxies; prox; prox = prox->next) { while (prox->next && prox->next->drop) { next = prox->next->next; fre = prox->next; if (fre->file) fclose(fre->file); else closesocket(fre->sock); if (fre->srcfile) fclose(fre->srcfile); free(fre); qtv->cluster->numproxies--; prox->next = next; } if (prox->flushing) //don't send it if we're trying to empty thier buffer. { if (prox->buffersize == prox->bufferpos) { if (!qtv->parsingconnectiondata) Net_SendConnectionMVD(qtv, prox); //they're up to date, resend the connection info. } else { Net_TryFlushProxyBuffer(qtv->cluster, prox); //try and flush it. continue; } } if (prox->drop) continue; //add the new data Net_ProxySend(qtv->cluster, prox, buffer, length); Net_TryFlushProxyBuffer(qtv->cluster, prox); // Net_TryFlushProxyBuffer(qtv->cluster, prox); // Net_TryFlushProxyBuffer(qtv->cluster, prox); #ifndef _MSC_VER #warning This is not the place for this #endif if (prox->sock != INVALID_SOCKET) { Fwd_ParseCommands(qtv->cluster, prox); } } } /*wrapper around recv to handle websocket connections*/ int NET_WebSocketRecv(SOCKET sock, wsrbuf_t *ws, unsigned char *out, unsigned int outlen, int *clen) { unsigned int mask = 0; unsigned int paylen; int len, i; if (clen) *clen = -1; if (!ws->websocket) return recv(sock, out, outlen, 0); if (!ws->wsbuflen) { len = recv(sock, ws->wsbuf, 2, 0); if (len > 0) ws->wsbuflen+=2; else return len; } if (ws->wsbuflen >= 2) { unsigned short ctrl = (ws->wsbuf[0]<<8) | ws->wsbuf[1]; len = 2; if ((ctrl & 0x7f) == 127) len += 8; else if ((ctrl & 0x7f) == 126) len += 2; if (ctrl & 0x80) len += 4; if (ws->wsbuflen < len) { paylen = recv(sock, ws->wsbuf+ws->wsbuflen, len - ws->wsbuflen, 0); if (paylen > 0) ws->wsbuflen += paylen; else return paylen; } /*headers are complete*/ if (ws->wsbuflen >= len) { if ((ctrl & 0x7f) == 127) { // paylen = paylen = ((ws->wsbuf[2]<<56) | (ws->wsbuf[3]<<48) | (ws->wsbuf[4]<<40) | (ws->wsbuf[5]<<32) | (ws->wsbuf[6]<<24) | (ws->wsbuf[7]<<16) | (ws->wsbuf[8]<<8) | (ws->wsbuf[9]<<0); // if (paylen < 65536) return 0; //error } else if ((ctrl & 0x7f) == 126) { paylen = (ws->wsbuf[2]<<8) | ws->wsbuf[3]; if (paylen < 126) return 0; //error } else paylen = ctrl & 0x7f; if (ctrl & 0x80) { ((unsigned char*)&mask)[0] = ws->wsbuf[ws->wsbuflen-4]; ((unsigned char*)&mask)[1] = ws->wsbuf[ws->wsbuflen-3]; ((unsigned char*)&mask)[2] = ws->wsbuf[ws->wsbuflen-2]; ((unsigned char*)&mask)[3] = ws->wsbuf[ws->wsbuflen-1]; } if (!(ctrl & 0x8000)) return 0; //can't handle fragmented frames switch((ctrl>>8) & 0xf) { case 1: /*text frame*/ len = 0; while (outlen>len && ws->wspushed < paylen) { unsigned char n; ctrl = recv(sock, &n, 1, 0); //FIXME: not my fastest code... if (ctrl <= 0) return len>0?len:ctrl; n ^= ((unsigned char*)&mask)[(ws->wspushed++)&3]; if (ws->wsbits) { *out++ = ((ws->wsbits&0x1f)<<6) | (n & 0x3f); len++; ws->wsbits = 0; } else if ((n & 0xe0) == 0xc0) { ws->wsbits = n; } else if (n & 0x80) return 0; //error else { *out++ = n; len++; } } if (ws->wspushed == paylen) { if (ws->wsbits) return 0; //error if (clen) *clen = ws->wspushed; ws->wspushed = 0; ws->wsbuflen = 0; } return len; case 2: /*binary frame*/ if (outlen > paylen - ws->wspushed) outlen = paylen - ws->wspushed; len = recv(sock, out, outlen, 0); if (len > 0) { for(i = 0; i < len; i++) out[i] ^= ((unsigned char*)&mask)[(ws->wspushed+i)&3]; ws->wspushed += len; } if (paylen == ws->wspushed) { /*success! move on to the next*/ if (clen) *clen = ws->wspushed; ws->wspushed = 0; ws->wsbuflen = 0; } return len; case 8: /*close*/ case 9: /*ping*/ case 10: /*pong*/ default: return 0; //unsupported } return 0; } return 0; } else return 0; } //returns true if the pending proxy should be unlinked //truth does not imply that it should be freed/released, just unlinked. qboolean SV_ReadPendingProxy(cluster_t *cluster, oproxy_t *pend) { //drop: ignored if flushing is still set. clear flushing and set drop on errors. char tempbuf[512]; unsigned char *s; unsigned char *e; char *colon; float clientversion = 0; int len; qboolean eoh; int headersize; qboolean raw; sv_t *qtv; if (pend->drop && !pend->flushing) { printf("pending drop\n"); if (pend->srcfile) fclose(pend->srcfile); if (pend->sock != INVALID_SOCKET) closesocket(pend->sock); free(pend); cluster->numproxies--; //REMEMBER TO PUT ANY CLEANUP INSIDE QW_TCPConnection TOO! return true; } #define QTVSVHEADER "QTVSV 1.1\n" Net_TryFlushProxyBuffer(cluster, pend); if (pend->flushing) { if (pend->srcfile) { #if 0 //bufferend = transmit point //buffersize = write point if (bufferend < buffersize) space = (MAX_PROXY_BUFFER - pend->buffersize) + pend->bufferend; else space = pend->bufferend - pend->buffersize; if (space < 256) /*don't bother reading if we're dribbling*/ return false; if (space > 0) /*never fully saturate so as to not confuse the ring*/ space--; if (space > MAX_PROXY_BUFFER - fread(prox->buffer + pend->buffersize, 1, space, pend->srcfile); #else while (pend->bufferpos == pend->buffersize) { char buffer[MAX_PROXY_BUFFER/2]; pend->droptime = cluster->curtime + 5*1000; len = fread(buffer, 1, sizeof(buffer), pend->srcfile); if (!len) { fclose(pend->srcfile); pend->srcfile = NULL; break; } Net_ProxySend(cluster, pend, buffer, len); Net_TryFlushProxyBuffer(cluster, pend); } #endif return false; //don't try reading anything yet } if (pend->bufferpos != pend->buffersize) return false; pend->flushing = false; if (pend->drop) return false; } if (pend->droptime < cluster->curtime) { printf("pending timeout\n"); pend->drop = true; pend->flushing = false; return false; } len = sizeof(pend->inbuffer) - pend->inbuffersize - 1; len = NET_WebSocketRecv(pend->sock, &pend->websocket, pend->inbuffer+pend->inbuffersize, len, NULL); if (len == 0) { pend->drop = true; printf("pending EOF\n"); return false; } else if (len > 0) pend->droptime = cluster->curtime + 5*1000; if (len < 0) { return false; } pend->inbuffersize += len; pend->inbuffer[pend->inbuffersize] = '\0'; if (pend->inbuffersize >= 4) { if (!strncmp(pend->inbuffer, "qizmo\n", 6)) { //carries unreliable packets printf("tcpconnect\n"); send(pend->sock, "qizmo\n", 6, 0); QW_TCPConnection(cluster, pend, NULL); return true; } if (ustrncmp(pend->inbuffer, "QTV\r", 4) && ustrncmp(pend->inbuffer, "QTV\n", 4) && ustrncmp(pend->inbuffer, "GET ", 4) && ustrncmp(pend->inbuffer, "POST ", 5)) { //I have no idea what the smeg you are. pend->drop = true; pend->inbuffer[16] = 0; Sys_Printf(cluster, "pending proxy: Connect for unrecognized protocol %s\n", pend->inbuffer); return false; } } //make sure there's a double \n somewhere eoh = false; for (s = pend->inbuffer; s<=pend->inbuffer+pend->inbuffersize; s++) { if (s[0] == '\n' && s[1] == '\n') { s += 2; eoh = true; break; } if (s[0] == '\n' && s[1] == '\r' && s[2] == '\n') { s += 3; eoh = true; break; } } if (!eoh) return false; //don't have enough yet headersize = s - pend->inbuffer; if (!ustrncmp(pend->inbuffer, "POST ", 5)) { HTTPSV_PostMethod(cluster, pend, (char*)s); return false; //not keen on this.. } else if (!ustrncmp(pend->inbuffer, "GET ", 4)) { pend->drop = true; colon = HTTPSV_GetMethod(cluster, pend); pend->flushing = true; memmove(pend->inbuffer, pend->inbuffer+headersize, pend->inbuffersize-headersize); pend->inbuffersize -= headersize; if (colon) { //carries unreliable packets printf("wsconnect\n"); QW_TCPConnection(cluster, pend, colon); return true; } return SV_ReadPendingProxy(cluster, pend); } raw = false; qtv = pend->defaultstream; e = pend->inbuffer; s = e; while(*e) { if (*e == '\n' || *e == '\r') { *e = '\0'; colon = strchr((char*)s, ':'); if (*s) { if (!colon) { if (!ustrcmp(s, "QTV")) { //just a qtv request (as in, not http or some other protocol) } else if (!ustrcmp(s, "SOURCELIST")) { //lists sources that are currently playing Net_ProxySendString(cluster, pend, QTVSVHEADER); if (!cluster->servers) { Net_ProxySendString(cluster, pend, "PERROR: No sources currently available\n"); } else { for (qtv = cluster->servers; qtv; qtv = qtv->next) { if (clientversion > 1) { int plyrs = 0; int prox = 0; int i; oproxy_t *o; for (i = 0; i < MAX_CLIENTS; i++) { if (*qtv->map.players[i].userinfo) plyrs++; } for (o = qtv->proxies; o; o = o->next) prox++; sprintf(tempbuf, "SRCSRV: %s\n", qtv->server); Net_ProxySendString(cluster, pend, tempbuf); sprintf(tempbuf, "SRCHOST: %s\n", qtv->map.hostname); Net_ProxySendString(cluster, pend, tempbuf); sprintf(tempbuf, "SRCPLYRS: %i\n", plyrs); Net_ProxySendString(cluster, pend, tempbuf); sprintf(tempbuf, "SRCVIEWS: %i\n", qtv->numviewers+prox); Net_ProxySendString(cluster, pend, tempbuf); sprintf(tempbuf, "SRCID: %i\n", qtv->streamid); //final part of each source Net_ProxySendString(cluster, pend, tempbuf); } else { sprintf(tempbuf, "ASOURCE: %i: %15s: %15s\n", qtv->streamid, qtv->server, qtv->map.hostname); Net_ProxySendString(cluster, pend, tempbuf); } } qtv = NULL; } Net_ProxySendString(cluster, pend, "\n"); pend->flushing = true; } else if (!ustrcmp(s, "REVERSE")) { //this is actually a server trying to connect to us //start up a new stream if (cluster->reverseallowed) { qtv = QTV_NewServerConnection(cluster, 0, "reverse"/*server*/, "", true, AD_REVERSECONNECT, false, 0); Net_ProxySendString(cluster, pend, QTVSVHEADER); Net_ProxySendString(cluster, pend, "VERSION: 1\n"); Net_ProxySendString(cluster, pend, "REVERSED\n"); Net_ProxySendString(cluster, pend, "\n"); //switch over the socket to the actual source connection rather than the pending Net_TryFlushProxyBuffer(cluster, pend); //flush anything... this isn't ideal, but should be small enough qtv->sourcesock = pend->sock; pend->sock = INVALID_SOCKET; memcpy(qtv->buffer, pend->inbuffer + headersize, pend->inbuffersize - headersize); qtv->parsingqtvheader = true; return false; } else { Net_ProxySendString(cluster, pend, QTVSVHEADER "PERROR: Reverse connections are disabled on this proxy\n"); pend->flushing = true; } } else if (!ustrcmp(s, "RECEIVE")) { //a client connection request without a source if (cluster->numservers == 1) { //only one stream anyway qtv = cluster->servers; } else { //try and hunt down an explicit stream (rather than a user-recorded one) int numfound = 0; sv_t *suitable = NULL; //shush noisy compilers for (qtv = cluster->servers; qtv; qtv = qtv->next) { if (qtv->autodisconnect == AD_NO) { suitable = qtv; numfound++; } } if (numfound == 1) qtv = suitable; } if (!qtv) { Net_ProxySendString(cluster, pend, QTVSVHEADER); Net_ProxySendString(cluster, pend, "PERROR: Multiple streams are currently playing\n"); Net_ProxySendString(cluster, pend, "\n"); pend->flushing = true; } } else if (!ustrcmp(s, "DEMOLIST")) { //lists sources that are currently playing int i; Cluster_BuildAvailableDemoList(cluster); Net_ProxySendString(cluster, pend, QTVSVHEADER); if (!cluster->availdemoscount) { Net_ProxySendString(cluster, pend, "PERROR: No demos currently available\n"); } else { for (i = 0; i < cluster->availdemoscount; i++) { sprintf(tempbuf, "ADEMO: %i: %15s\n", cluster->availdemos[i].size, cluster->availdemos[i].name); Net_ProxySendString(cluster, pend, tempbuf); } qtv = NULL; } Net_ProxySendString(cluster, pend, "\n"); pend->flushing = true; } else if (!ustrcmp(s, "AUTH")) { //lists the demos available on this proxy //part of the connection process, can be ignored if there's no password } else printf("Unrecognized token in QTV connection request (%s)\n", s); } else { *colon++ = '\0'; if (!ustrcmp(s, "VERSION")) { clientversion = atof(colon); } else if (!ustrcmp(s, "RAW")) raw = atoi(colon); /*else if (!ustrcmp(s, "ROUTE")) { //pure rewroute... //is this safe? probably not. s = QTVSVHEADER "PERROR: ROUTE command not yet implemented\n" "\n"; Net_ProxySend(cluster, pend, s, ustrlen(s)); pend->flushing = true; } */ else if (!ustrcmp(s, "SOURCE")) { //connects, creating a new source char *t; while (*colon == ' ') colon++; for (t = colon; *t; t++) if (*t < '0' || *t > '9') break; if (*t) qtv = QTV_NewServerConnection(cluster, 0, colon, "", false, AD_WHENEMPTY, true, false); else { //numerical source, use a stream id. for (qtv = cluster->servers; qtv; qtv = qtv->next) if (qtv->streamid == atoi(colon)) break; } } else if (!ustrcmp(s, "DEMO")) { //starts a demo off the server... source does the same thing though... char buf[256]; snprintf(buf, sizeof(buf), "demo:%s", colon); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_WHENEMPTY, true, false); if (!qtv) { Net_ProxySendString(cluster, pend, QTVSVHEADER "PERROR: couldn't open demo\n" "\n"); pend->flushing = true; } } else if (!ustrcmp(s, "AUTH")) { //lists the demos available on this proxy //part of the connection process, can be ignored if there's no password } else printf("Unrecognized token in QTV connection request (%s)\n", s); } } s = e+1; } e++; } if (!pend->flushing) { if (clientversion < 1) { Net_ProxySendString(cluster, pend, QTVSVHEADER "PERROR: Requested protocol version not supported\n" "\n"); pend->flushing = true; } else if (!qtv) { Net_ProxySendString(cluster, pend, QTVSVHEADER "PERROR: No stream selected\n" "\n"); pend->flushing = true; } } if (pend->flushing) return false; if (qtv->usequakeworldprotocols) { Net_ProxySendString(cluster, pend, QTVSVHEADER "PERROR: This version of QTV is unable to convert QuakeWorld to QTV protocols\n" "\n"); pend->flushing = true; return false; } if (cluster->maxproxies>=0 && cluster->numproxies >= cluster->maxproxies) { Net_ProxySendString(cluster, pend, QTVSVHEADER "TERROR: This QTV has reached it's connection limit\n" "\n"); pend->flushing = true; return false; } pend->next = qtv->proxies; qtv->proxies = pend; if (!raw) { Net_ProxySendString(cluster, pend, QTVSVHEADER); Net_ProxySendString(cluster, pend, "BEGIN: "); Net_ProxySendString(cluster, pend, qtv->server); Net_ProxySendString(cluster, pend, "\n\n"); } // else if (passwordprotected) //raw mode doesn't support passwords, so reject them // { // pend->flushing = true; // return; // } pend->stream = qtv; memmove(pend->inbuffer, pend->inbuffer+headersize, pend->inbuffersize-headersize); pend->inbuffersize -= headersize; Net_GreetingMessage(pend); Net_SendConnectionMVD(qtv, pend); return true; } ================================================ FILE: fteqtv/httpsv.c ================================================ #include "qtv.h" #define WEBPORTMANIFEST "https://triptohell.info/demo.fmf" //#define WEBPORTMANIFEST "http://localhost:8080/demo.fmf" //main reason to use connection close is because we're lazy and don't want to give sizes in advance (yes, we could use chunks..) size_t SHA1(unsigned char *digest, size_t maxdigestsize, const unsigned char *string, size_t stringlen); void tobase64(unsigned char *out, int outlen, unsigned char *in, int inlen) { static unsigned char tab[64] = { 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; unsigned int usedbits = 0; unsigned int val = 0; outlen--; while(inlen) { while(usedbits < 24 && inlen) { val <<= 8; val |= (*in++); inlen--; usedbits += 8; } if (outlen < 4) return; val <<= 24 - usedbits; *out++ = (usedbits > 0)?tab[(val>>18)&0x3f]:'='; *out++ = (usedbits > 6)?tab[(val>>12)&0x3f]:'='; *out++ = (usedbits > 12)?tab[(val>>6)&0x3f]:'='; *out++ = (usedbits > 18)?tab[(val>>0)&0x3f]:'='; val=0; usedbits = 0; } *out = 0; } static const char qfont_table[256] = { '\0', '#', '#', '#', '#', '.', '#', '#', '#', 9, 10, '#', ' ', 13, '.', '.', '[', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '<', '=', '>', ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '<', '<', '=', '>', '#', '#', '.', '#', '#', '#', '#', ' ', '#', ' ', '>', '.', '.', '[', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '<', '=', '>', ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '<' }; static void HTMLprintf(char *outb, int outl, char *fmt, ...) { va_list val; char qfmt[8192*4]; char *inb = qfmt; unsigned char inchar; va_start(val, fmt); vsnprintf(qfmt, sizeof(qfmt), fmt, val); va_end(val); qfmt[sizeof(qfmt)-1] = 0; outl--; outl -= 5; while (outl > 0 && *inb) { inchar = qfont_table[*(unsigned char*)inb]; if (inchar == '<') { *outb++ = '&'; *outb++ = 'l'; *outb++ = 't'; *outb++ = ';'; outl -= 4; } else if (inchar == '>') { *outb++ = '&'; *outb++ = 'g'; *outb++ = 't'; *outb++ = ';'; outl -= 4; } else if (inchar == '\n') { *outb++ = '<'; *outb++ = 'b'; *outb++ = 'r'; *outb++ = '/'; *outb++ = '>'; outl -= 5; } else if (inchar == '&') { *outb++ = '&'; *outb++ = 'a'; *outb++ = 'm'; *outb++ = 'p'; *outb++ = ';'; outl -= 5; } else { *outb++ = inchar; outl -= 1; } inb++; } *outb++ = 0; } static void HTTPSV_SendHTTPHeader(cluster_t *cluster, oproxy_t *dest, char *error_code, char *content_type, int csize, qboolean nocache) { char *s; char buffer[2048]; if (nocache) { if (csize >= 0) { s = "HTTP/1.1 %s OK\r\n" "Content-Type: %s\r\n" "Content-Length: %i\r\n" "Cache-Control: no-cache, must-revalidate\r\n" "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" "\r\n"; } else { s = "HTTP/1.1 %s OK\r\n" "Content-Type: %s\r\n" "Cache-Control: no-cache, must-revalidate\r\n" "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" "Connection: close\r\n" "\r\n"; } } else { if (csize >= 0) { s = "HTTP/1.1 %s OK\r\n" "Cache-Control: public, max-age=3600\r\n" "Content-Type: %s\r\n" "Content-Length: %i\r\n" "\r\n"; } else { s = "HTTP/1.1 %s OK\r\n" "Cache-Control: public, max-age=3600\r\n" "Content-Type: %s\r\n" "Connection: close\r\n" "\r\n"; } } snprintf(buffer, sizeof(buffer), s, error_code, content_type, csize); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); } static void HTTPSV_SendHTMLHeader(cluster_t *cluster, oproxy_t *dest, char *title, char *args) { char *s; char buffer[2048]; qboolean plugin = false; while (*args && *args != ' ') { if (*args == 'p') plugin = true; args++; } s = "\n" "\n" "\n" " \n" " %s\n" " \n" "\n" "
"; snprintf(buffer, sizeof(buffer), s, title, plugin?"?p":"", plugin?"?p":"", (!*cluster->adminpassword)?"":(plugin?"
  • Admin
  • ":"
  • Admin
  • ")/*, plugin?"
  • Basic
  • ":"
  • Plugin
  • "*/ ); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); } static void HTTPSV_SendHTMLFooter(cluster_t *cluster, oproxy_t *dest) { char *s; char buffer[2048]; /*Proxy version*/ snprintf(buffer, sizeof(buffer), "
    Server Version: "QTV_VERSION_STRING" "PROXYWEBSITE""); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); /*terminate html page*/ s = "\n" "\n"; Net_ProxySend(cluster, dest, s, strlen(s)); } #define HTMLPRINT(str) Net_ProxySend(cluster, dest, str "\n", strlen(str "\n")) #define HTTPPRINT(str) Net_ProxySend(cluster, dest, str, strlen(str)) static void HTTPSV_GenerateNowPlaying(cluster_t *cluster, oproxy_t *dest, char *args) { int player; char *s; char buffer[1024]; char plname[64]; sv_t *streams; qboolean plugin = false; qboolean activeonly = false; HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Now Playing", args); while (*args && *args != ' ') { if (*args == 'p') plugin = true; else if (*args == 'a') activeonly = true; args++; } if (!strcmp(cluster->hostname, DEFAULT_HOSTNAME)) snprintf(buffer, sizeof(buffer), "

    QuakeTV: Now Playing

    "); //don't show the hostname if its set to the default else snprintf(buffer, sizeof(buffer), "

    %s: Now Playing

    ", cluster->hostname); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); HTMLPRINT("
    "); for (streams = cluster->servers; streams; streams = streams->next) { if (activeonly) { for (player = 0; player < MAX_CLIENTS; player++) { if (streams->isconnected && streams->map.thisplayer == player) continue; if (*streams->map.players[player].userinfo) { break; } } if (player == MAX_CLIENTS) continue; } HTMLPRINT("
    "); HTMLprintf(buffer, sizeof(buffer), "%s (%s: %s)", streams->server, streams->map.gamedir, streams->map.mapname); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); if (plugin && !strncmp(streams->server, "tcp:", 4)) { snprintf(buffer, sizeof(buffer), " [ Join ]", streams->server); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); } if (plugin && !strncmp(streams->server, "udp:", 4)) snprintf(buffer, sizeof(buffer), " [ Join ]", streams->server+4); else snprintf(buffer, sizeof(buffer), " [ Watch Now]", streams->streamid); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); HTMLPRINT("
      "); for (player = 0; player < MAX_CLIENTS; player++) { if (streams->isconnected && streams->map.thisplayer == player) continue; if (*streams->map.players[player].userinfo) { Info_ValueForKey(streams->map.players[player].userinfo, "name", plname, sizeof(plname)); if (streams->map.players[player].frags < -90) { HTMLPRINT("
    • "); } else { HTMLPRINT("
    • "); } HTMLprintf(buffer, sizeof(buffer), "%s", plname); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); HTMLPRINT("
    • "); } } HTMLPRINT("
    "); } HTMLPRINT("
    "); if (!cluster->servers) { s = "No streams are currently being played
    "; Net_ProxySend(cluster, dest, s, strlen(s)); } HTTPSV_SendHTMLFooter(cluster, dest); } static void HTTPSV_GenerateCSSFile(cluster_t *cluster, oproxy_t *dest) { HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/css", -1, false); HTMLPRINT("* { font-family: Verdana, Helvetica, sans-serif; }"); HTMLPRINT("body { color: #000; background-color: #fff; padding: 0 0px; }"); HTMLPRINT("a { color: #00f; }"); HTMLPRINT("div.topdiv { display:flex; align-items: stretch; position: absolute; top: 0; right: 0; bottom: 0; left: 0; }"); HTMLPRINT("div.left { resize: horizontal; overflow: auto; flex 0 0 25%;}"); HTMLPRINT("div.right { padding:0; margin: 0; flex: auto; }"); HTMLPRINT("a.qtvfile { font-weight: bold; }"); HTMLPRINT("a:visited { color: #00f; }"); HTMLPRINT("a:hover { background-color: black; color: yellow; }"); HTMLPRINT("li.spectator { color: #666; font-size: 0.9ex; }"); HTMLPRINT("dl.nowplaying dd { margin: 0 0 2em 0; }"); HTMLPRINT("dl.nowplaying dt { margin: 1em 0 0 0; font-size: 1.1em; font-weight: bold; }"); HTMLPRINT("dl.nowplaying li { list-style: none; margin: 0 0 0 1em; padding: 0; }"); HTMLPRINT("dl.nowplaying ul { margin: 0 0 0 1em; padding: 0; }"); HTMLPRINT("canvas.emscripten { border: 0px none; padding:0; margin: 0; width: 100%; height: 100%;}"); HTMLPRINT("#navigation { background-color: #eef; }"); HTMLPRINT("#navigation li { display: inline; list-style: none; margin: 0 3em; }"); } static qboolean HTTPSV_GetHeaderField(char *s, char *field, char *buffer, int buffersize) { char *e; char *colon; int fieldnamelen = strlen(field); buffer[0] = 0; e = s; while(*e) { if (*e == '\n') { *e = '\0'; colon = strchr(s, ':'); *e = '\n'; if (!colon) { if (!strncmp(field, s, fieldnamelen)) { if (s[fieldnamelen] <= ' ') { return true; } } } else { if (fieldnamelen == colon - s) { if (!strncmp(field, s, colon-s)) { colon++; while (*colon == ' ') colon++; while (buffersize > 2) { if (!*colon || *colon == '\r' || *colon == '\n') break; *buffer++ = *colon++; buffersize--; } *buffer = 0; return true; } } } s = e+1; } e++; } return false; } static qboolean HTTPSV_GetHeaderCommaField(char *s, char **ctx, char *field, char *buffer, int buffersize) { //some http header fields are a,b,c,d\ne,f type lists. this function will read one token despite them being split between multiple headers. char *e; //end of current line... char *colon; int fieldnamelen = strlen(field); buffer[0] = 0; if (*ctx) e = *ctx; else e = s; if (*e == ',') { colon = e; goto foundfield; } while(*e) { if (*e == '\n') { *e = '\0'; colon = strchr(s, ':'); *e = '\n'; if (colon) { if (fieldnamelen == colon - s) { if (!strncmp(field, s, colon-s)) { foundfield: colon++; while (*colon == ' ') colon++; while (buffersize > 2) { if (!*colon || *colon == '\r' || *colon == '\n' || *colon == ',') break; *buffer++ = *colon++; buffersize--; } *buffer = 0; *ctx = colon; return true; } } } s = e+1; } e++; } return false; } static void HTTPSV_GenerateQTVStub(cluster_t *cluster, oproxy_t *dest, char *streamtype, char *streamid) { char *s; char hostname[128]; char buffer[1024]; char fname[256]; s = fname; while (*streamid > ' ') { if (s > fname + sizeof(fname)-4) //4 cos I'm too lazy to work out what the actual number should be break; if (*streamid == '%') { *s = 0; streamid++; if (*streamid >= 'a' && *streamid <= 'f') *s += 10 + *streamid-'a'; else if (*streamid >= 'A' && *streamid <= 'F') *s += 10 + *streamid-'A'; else if (*streamid >= '0' && *streamid <= '9') *s += *streamid-'0'; else break; *s <<= 4; streamid++; if (*streamid >= 'a' && *streamid <= 'f') *s += 10 + *streamid-'a'; else if (*streamid >= 'A' && *streamid <= 'F') *s += 10 + *streamid-'A'; else if (*streamid >= '0' && *streamid <= '9') *s += *streamid-'0'; else break; //don't let hackers try adding extra commands to it. if (*s == '$' || *s == ';' || *s == '\r' || *s == '\n') continue; streamid++; s++; } else if (*streamid == '$' || *streamid == ';' || *streamid == '\r' || *streamid == '\n') { //don't let hackers try adding extra commands to it. streamid++; } else *s++ = *streamid++; } *s = 0; streamid = fname; if (!HTTPSV_GetHeaderField((char*)dest->inbuffer, "Host", hostname, sizeof(hostname))) { HTTPSV_SendHTTPHeader(cluster, dest, "400", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Error", ""); s = "Your client did not send a Host field, which is required in HTTP/1.1\n
    " "Please try a different browser.\n" "" ""; Net_ProxySend(cluster, dest, s, strlen(s)); return; } /*if there's a port number on there, strip it*/ if (strchr(hostname, ':')) *strchr(hostname, ':') = 0; snprintf(buffer, sizeof(buffer), "[QTV]\r\n" "Stream: %s%s@%s:%i\r\n" "", //5, 256, 64. snprintf is not required, but paranoia is a wonderful thing. streamtype, streamid, hostname, cluster->tcplistenportnum); HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/x-quaketvident", strlen(buffer), false); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); } static void HTTPSV_GenerateQWSVStub(cluster_t *cluster, oproxy_t *dest, char *method, char *streamid) { char *s; char buffer[1024]; char fname[256]; s = fname; while (*streamid > ' ') { if (s > fname + sizeof(fname)-4) //4 cos I'm too lazy to work out what the actual number should be break; if (*streamid == '%') { *s = 0; streamid++; if (*streamid >= 'a' && *streamid <= 'f') *s += 10 + *streamid-'a'; else if (*streamid >= 'A' && *streamid <= 'F') *s += 10 + *streamid-'A'; else if (*streamid >= '0' && *streamid <= '9') *s += *streamid-'0'; else break; *s <<= 4; streamid++; if (*streamid <= ' ') break; else if (*streamid >= 'a' && *streamid <= 'f') *s += 10 + *streamid-'a'; else if (*streamid >= 'A' && *streamid <= 'F') *s += 10 + *streamid-'A'; else if (*streamid >= '0' && *streamid <= '9') *s += *streamid-'0'; else break; streamid++; s++; } else *s++ = *streamid++; } *s = 0; streamid = fname; snprintf(buffer, sizeof(buffer), "[QTV]\r\n" "%s: %s\r\n" "", method, streamid); HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/x-quaketvident", strlen(buffer), false); Net_ProxySend(cluster, dest, buffer, strlen(buffer)); } static char *HTTPSV_ParsePOST(char *post, char *buffer, int buffersize) { while(*post && *post != '&') { if (--buffersize>0) { if (*post == '+') *buffer++ = ' '; else if (*post == '%') { *buffer = 0; post++; if (*post == '\0' || *post == '&') break; else if (*post >= 'a' && *post <= 'f') *buffer += 10 + *post-'a'; else if (*post >= 'A' && *post <= 'F') *buffer += 10 + *post-'A'; else if (*post >= '0' && *post <= '9') *buffer += *post-'0'; *buffer <<= 4; post++; if (*post == '\0' || *post == '&') break; else if (*post >= 'a' && *post <= 'f') *buffer += 10 + *post-'a'; else if (*post >= 'A' && *post <= 'F') *buffer += 10 + *post-'A'; else if (*post >= '0' && *post <= '9') *buffer += *post-'0'; buffer++; } else *buffer++ = *post; } post++; } *buffer = 0; return post; } static void HTTPSV_GenerateAdmin(cluster_t *cluster, oproxy_t *dest, int streamid, char *postbody, char *args) { char pwd[64]; char cmd[256]; char result[8192]; char *s; char *o; int passwordokay = false; if (!*cluster->adminpassword) { HTTPSV_SendHTTPHeader(cluster, dest, "403", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Admin Error", args); s = "The admin password is disabled. You may not log in remotely.\n"; Net_ProxySend(cluster, dest, s, strlen(s)); return; } pwd[0] = 0; cmd[0] = 0; if (postbody) while (*postbody) { if (!strncmp(postbody, "pwd=", 4)) { postbody = HTTPSV_ParsePOST(postbody+4, pwd, sizeof(pwd)); } else if (!strncmp(postbody, "cmd=", 4)) { postbody = HTTPSV_ParsePOST(postbody+4, cmd, sizeof(cmd)); } else { while(*postbody && *postbody != '&') { postbody++; } if (*postbody == '&') postbody++; } } if (!*pwd) { if (postbody) o = "No Password"; else o = ""; } else if (!strcmp(pwd, cluster->adminpassword)) { passwordokay = true; //small hack (as http connections are considered non-connected proxies) cluster->numproxies--; if (*cmd) o = Rcon_Command(cluster, NULL, cmd, result, sizeof(result), false); else o = ""; cluster->numproxies++; } else { o = "Bad Password"; } if (o != result) { strlcpy(result, o, sizeof(result)); o = result; } HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Admin", args); s = "

    QuakeTV Admin: "; Net_ProxySend(cluster, dest, s, strlen(s)); s = cluster->hostname; Net_ProxySend(cluster, dest, s, strlen(s)); s = "

    "; Net_ProxySend(cluster, dest, s, strlen(s)); s = "
    " "
    " "Password " "
    " "Command " "" "
    " "
    "; Net_ProxySend(cluster, dest, s, strlen(s)); if (passwordokay) HTMLPRINT(""); else HTMLPRINT(""); while(*o) { s = strchr(o, '\n'); if (s) *s = 0; HTMLprintf(cmd, sizeof(cmd), "%s", o); Net_ProxySend(cluster, dest, cmd, strlen(cmd)); Net_ProxySend(cluster, dest, "
    ", 6); if (!s) break; o = s+1; } HTTPSV_SendHTMLFooter(cluster, dest); } static void HTTPSV_GenerateDemoListing(cluster_t *cluster, oproxy_t *dest, char *args) { int i; char link[512]; char *s; qboolean plugframe = false; for (s=args; *s && *s != ' ';) { if (*s == 'p') plugframe = true; s++; } HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Demos", args); s = "

    QuakeTV: Demo Listing

    "; Net_ProxySend(cluster, dest, s, strlen(s)); Cluster_BuildAvailableDemoList(cluster); for (i = 0; i < cluster->availdemoscount; i++) { if (plugframe) { snprintf(link, sizeof(link), "%s (%ikb)
    ", cluster->availdemos[i].name, cluster->availdemos[i].name, cluster->availdemos[i].size/1024); } else { snprintf(link, sizeof(link), "%s (%ikb)
    ", cluster->availdemos[i].name, cluster->availdemos[i].name, cluster->availdemos[i].size/1024); } Net_ProxySend(cluster, dest, link, strlen(link)); } snprintf(link, sizeof(link), "

    Total: %i demos

    ", cluster->availdemoscount); Net_ProxySend(cluster, dest, link, strlen(link)); HTTPSV_SendHTMLFooter(cluster, dest); } static void HTTPSV_GeneratePlugin(cluster_t *cluster, oproxy_t *dest) { char hostname[1024]; char *html; if (!HTTPSV_GetHeaderField((char*)dest->inbuffer, "Host", hostname, sizeof(hostname))) { HTTPSV_SendHTTPHeader(cluster, dest, "400", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Error", "p"); html = "Your client did not send a Host field, which is required in HTTP/1.1\n
    " "Please try a different browser.\n" "" ""; Net_ProxySend(cluster, dest, html, strlen(html)); return; } html = "\n" "QuakeTV With Plugin" " \n" "" "" "
    " "
    " "" "
    " "
    " "Canvas not supported" "
    " "
    " "" "" ""; HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/html", strlen(html), true); Net_ProxySend(cluster, dest, html, strlen(html)); } static void HTTPSV_GenerateDownload(cluster_t *cluster, oproxy_t *dest, char *filename, char *svroot, char *redir) { char fname[256]; char link[512]; char *s, *suppliedname; int len; char *mime = "application/x-forcedownload"; if (!strcmp(filename, "/ftewebgl.wasm")) mime = "application/wasm"; else if (!strcmp(filename, "/ftewebgl.js")) mime = "text/javascript"; if (!svroot || !*svroot) { if (redir) { HTTPPRINT("HTTP/1.0 302 Found\nLocation: "); HTTPPRINT(redir); HTTPPRINT("\n\n"); return; } HTTPSV_SendHTTPHeader(cluster, dest, "403", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "Permission denied", ""); HTMLPRINT("

    403: Forbidden

    "); HTMLPRINT("File downloads from this proxy are currently not permitted."); HTTPSV_SendHTMLFooter(cluster, dest); return; } suppliedname = s = fname + strlcpy(fname, svroot, sizeof(fname)); while (*filename > ' ') { if (s > fname + sizeof(fname)-4) //4 cos I'm too lazy to work out what the actual number should be break; if (*filename == '%') { *s = 0; filename++; if (*filename <= ' ') break; else if (*filename >= 'a' && *filename <= 'f') *s += 10 + *filename-'a'; else if (*filename >= 'A' && *filename <= 'F') *s += 10 + *filename-'A'; else if (*filename >= '0' && *filename <= '9') *s += *filename-'0'; *s <<= 4; filename++; if (*filename <= ' ') break; else if (*filename >= 'a' && *filename <= 'f') *s += 10 + *filename-'a'; else if (*filename >= 'A' && *filename <= 'F') *s += 10 + *filename-'A'; else if (*filename >= '0' && *filename <= '9') *s += *filename-'0'; s++; } else *s++ = *filename++; } *s = 0; if (!redir && (*suppliedname == '\\' || *suppliedname == '/' || strstr(suppliedname, "..") || strstr(suppliedname, "//") || strstr(suppliedname, "\\\\") || strchr(suppliedname, ':'))) { HTTPSV_SendHTTPHeader(cluster, dest, "403", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "Permission denied", ""); HTMLPRINT("

    403: Forbidden

    "); HTMLPRINT("

    "); HTMLprintf(link, sizeof(link), "The filename '%s' names an absolute path.", suppliedname); Net_ProxySend(cluster, dest, link, strlen(link)); HTMLPRINT("

    "); return; } len = strlen(fname); if (len > 4) { /*protect id's content (prevent downloading of bsps from pak files - we don't do pak files so just prevent the entire pak)*/ if (!stricmp(link+len-4, ".pak")) { HTTPSV_SendHTTPHeader(cluster, dest, "403", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "Permission denied", ""); HTMLPRINT("

    403: Forbidden

    "); HTMLPRINT("

    "); HTMLprintf(link, sizeof(link), "Pak files may not be downloaded.", suppliedname); Net_ProxySend(cluster, dest, link, strlen(link)); HTMLPRINT("

    "); return; } } printf("Download request for %s\n", fname); dest->srcfile = fopen(fname, "rb"); if (dest->srcfile) { long size; fseek(dest->srcfile, 0, SEEK_END); size = ftell(dest->srcfile); fseek(dest->srcfile, 0, SEEK_SET); HTTPSV_SendHTTPHeader(cluster, dest, "200", mime, size, false); } else if (redir) { HTTPPRINT("HTTP/1.0 302 Found\nLocation: "); HTTPPRINT(redir); HTTPPRINT("\n\n"); } else { HTTPSV_SendHTTPHeader(cluster, dest, "404", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "File not found", ""); HTMLPRINT("

    404: File not found

    "); HTMLPRINT("

    "); HTMLprintf(link, sizeof(link), "The file '%s' could not be found on this server", suppliedname); Net_ProxySend(cluster, dest, link, strlen(link)); HTMLPRINT("

    "); HTTPSV_SendHTMLFooter(cluster, dest); } } static qboolean urimatch(char *uri, char *match, int urilen) { int mlen = strlen(match); if (urilen < mlen) return false; if (strncmp(uri, match, mlen)) return false; if (urilen == mlen) return true; if (uri[mlen] == '?') return true; return false; } static qboolean uriargmatch(char *uri, char *match, int urilen, char **args) { int mlen = strlen(match); if (urilen < mlen) return false; if (strncmp(uri, match, mlen)) return false; if (urilen == mlen) { *args = ""; return true; } if (uri[mlen] == '?') { *args = uri+mlen+1; return true; } return false; } void HTTPSV_PostMethod(cluster_t *cluster, oproxy_t *pend, char *postdata) { char tempbuf[512]; char *s; int len; char *uri, *uriend; char *args; int urilen; uri = pend->inbuffer+4; while (*uri == ' ') uri++; uriend = strchr(uri, '\n'); s = strchr(uri, ' '); if (s && s < uriend) uriend = s; urilen = uriend - uri; if (!HTTPSV_GetHeaderField((char*)pend->inbuffer, "Content-Length", tempbuf, sizeof(tempbuf))) { s = "HTTP/1.1 411 OK\n" "Content-Type: text/html\n" "Connection: close\n" "\n" "QuakeTVNo Content-Length was provided.\n"; Net_ProxySend(cluster, pend, s, strlen(s)); pend->flushing = true; return; } len = atoi(tempbuf); if (pend->inbuffersize + len >= sizeof(pend->inbuffer)-20) { //too much data pend->flushing = true; return; } len = postdata - (char*)pend->inbuffer + len; if (len > pend->inbuffersize) return; //still need the body // if (len <= pend->inbuffersize) { if (uriargmatch(uri, "/admin.html", urilen, &args)) { HTTPSV_GenerateAdmin(cluster, pend, 0, postdata, args); } else { s = "HTTP/1.1 404 OK\n" "Content-Type: text/html\n" "Connection: close\n" "\n" "QuakeTVThat HTTP method is not supported for that URL.\n"; Net_ProxySend(cluster, pend, s, strlen(s)); } pend->flushing = true; return; } } #define REDIRECTIF(uri_,url_) \ if (urimatch(uri, uri_, urilen)) \ { \ s = "HTTP/1.0 302 Found\n" \ "Location: " url_ "\n" \ "\n"; \ Net_ProxySend(cluster, pend, s, strlen(s)); \ } #define REDIRECTIFNEEDED(uri_,url_) \ if (urimatch(uri, uri_, urilen)) \ { \ HTTPSV_GenerateDownload(cluster, pend, uri_, cluster->downloaddir, url_);\ } char *HTTPSV_GetMethod(cluster_t *cluster, oproxy_t *pend) { char connection[64]; char upgrade[64]; char *s; char *uri, *uriend; char *args; int urilen; uri = pend->inbuffer+4; while (*uri == ' ') uri++; uriend = strchr(uri, '\n'); s = strchr(uri, ' '); if (s && s < uriend) uriend = s; urilen = uriend - uri; if (!pend->websocket.websocket && uri[0] == '/' && HTTPSV_GetHeaderField((char*)pend->inbuffer, "Connection", connection, sizeof(connection)) && strstr(connection, "Upgrade")) { if (HTTPSV_GetHeaderField((char*)pend->inbuffer, "Upgrade", upgrade, sizeof(upgrade)) && !stricmp(upgrade, "websocket")) { char wsprot[64]; char ver[64]; char key[64]; char *ctx; HTTPSV_GetHeaderField((char*)pend->inbuffer, "Sec-WebSocket-Key", key, sizeof(key)); for (ctx = NULL; HTTPSV_GetHeaderCommaField((char*)pend->inbuffer, &ctx, "Sec-WebSocket-Protocol", wsprot, sizeof(wsprot)); ) { //if (!strcmp(wsprot, "quake")) //webquake. we don't support this! (no OOB and missing header flags and some screwy sequence numbers) if (!strcmp(wsprot, "fteqw") || //as a client (!strcmp(wsprot, "faketcp") && urilen==1&&!strncmp(uri,"/",1))) //as a qtv proxy (eztv style, but websocked). we are NOT proxying tcp. require a qtv handshake over the resulting websocket connection. break; //break out on the first one we know. this is the recommended way... } if (!*wsprot) { #define dest pend HTTPSV_SendHTTPHeader(cluster, pend, "404", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, pend, "Websocket SubProtocol not recognised", ""); HTMLPRINT("

    Websocket SubProtocol not recognised

    "); HTTPSV_SendHTMLFooter(cluster, pend); } else if (HTTPSV_GetHeaderField((char*)pend->inbuffer, "Sec-WebSocket-Version", ver, sizeof(ver)) && atoi(ver) != 13) { s = "HTTP/1.1 426 Upgrade Required\r\n" "Sec-WebSocket-Version: 13\r\n" "Access-Control-Allow-Origin: *\r\n" //allow cross-origin requests. this means you can use any domain to play on any public server. "\r\n"; Net_ProxySend(cluster, pend, s, strlen(s)); return NULL; } else { char acceptkey[20*2]; unsigned char sha1digest[20]; char padkey[512]; snprintf(padkey, sizeof(padkey), "%s258EAFA5-E914-47DA-95CA-C5AB0DC85B11", key); tobase64(acceptkey, sizeof(acceptkey), sha1digest, CalcHash(&hash_sha1, sha1digest, sizeof(sha1digest), padkey, strlen(padkey))); snprintf(padkey, sizeof(padkey), "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Access-Control-Allow-Origin: *\r\n" //allow cross-origin requests. this means you can use any domain to play on any public server. "Sec-WebSocket-Accept: %s\r\n" "Sec-WebSocket-Protocol: %s\r\n" "\r\n", acceptkey, wsprot); //send the websocket handshake response. Net_ProxySend(cluster, pend, padkey, strlen(padkey)); pend->websocket.websocket = true; pend->droptime = cluster->curtime + 20*1000; printf("websocket upgrade\n"); pend->drop = false; uri[urilen] = 0; //make sure its null terminated if (!strcmp(wsprot, "faketcp")) return NULL; //we're using websockets, don't treat it as a client else return strdup(uri+1); //its a client, track the url as the initial stream. } return NULL; } } if (urimatch(uri, "/plugin.html", urilen) || urimatch(uri, "/", urilen)) { HTTPSV_GeneratePlugin(cluster, pend); } else if (uriargmatch(uri, "/nowplaying.html", urilen, &args)) { HTTPSV_GenerateNowPlaying(cluster, pend, args); } else if (!strncmp(uri, "/watch.qtv?sid=", 15)) { HTTPSV_GenerateQTVStub(cluster, pend, "", (char*)pend->inbuffer+19); } else if (!strncmp(uri, "/watch.qtv?demo=", 16)) { HTTPSV_GenerateQTVStub(cluster, pend, "file:", (char*)pend->inbuffer+20); } else if (!strncmp(uri, "/watch.qtv?join=", 16)) { HTTPSV_GenerateQWSVStub(cluster, pend, "Join", (char*)pend->inbuffer+16); } else if (!strncmp(uri, "/watch.qtv?obsv=", 16)) { HTTPSV_GenerateQWSVStub(cluster, pend, "Observe", (char*)pend->inbuffer+16); } // else if (!strncmp((char*)pend->inbuffer+4, "/demo/", 6)) // { //fixme: make this send the demo as an http download // HTTPSV_GenerateQTVStub(cluster, pend, "file:", (char*)pend->inbuffer+10); // } else if (uriargmatch(uri, "/admin.html", urilen, &args)) { HTTPSV_GenerateAdmin(cluster, pend, 0, NULL, args); } #if defined(_DEBUG) || defined(DEBUG) else REDIRECTIF("/", "/plugin.html") #endif else REDIRECTIF("/", "/nowplaying.html") else REDIRECTIFNEEDED("/about.html", PROXYWEBSITE) else REDIRECTIFNEEDED("/favicon.ico", "https://fte.triptohell.info/favicon.ico") else REDIRECTIFNEEDED("/ftewebgl.js", "https://fte.triptohell.info/ftewebgl.js") else REDIRECTIFNEEDED("/ftewebgl.wasm", "https://fte.triptohell.info/ftewebgl.wasm") else if (uriargmatch(uri, "/demos.html", urilen, &args)) { HTTPSV_GenerateDemoListing(cluster, pend, args); } else if (!strncmp((char*)pend->inbuffer+4, "/file/", 6)) { HTTPSV_GenerateDownload(cluster, pend, (char*)pend->inbuffer+10, cluster->downloaddir, NULL); } else if (urimatch(uri, "/style.css", urilen)) { HTTPSV_GenerateCSSFile(cluster, pend); } else { #define dest pend HTTPSV_SendHTTPHeader(cluster, dest, "404", "text/html", -1, true); HTTPSV_SendHTMLHeader(cluster, dest, "Address not recognised", ""); HTMLPRINT("

    Address not recognised

    "); HTTPSV_SendHTMLFooter(cluster, dest); } return NULL; } ================================================ FILE: fteqtv/libqtvc/Makefile ================================================ OBJS = msvc_sucks.o glibc_sucks.o all: libqtvc.a libqtvc.a: $(OBJS) rm -f $@ $(AR) r $@ $^ $(RANLIB) $@ clean: rm -f $(OBJS) libqtvc.a ================================================ FILE: fteqtv/libqtvc/glibc_sucks.c ================================================ /* $OpenBSD: strlcpy.c,v 1.11 2006/05/05 15:27:38 millert Exp $ */ /* * Copyright (c) 1998 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include /* * Copy src to string dst of size siz. At most siz-1 characters * will be copied. Always NUL terminates (unless siz == 0). * Returns strlen(src); if retval >= siz, truncation occurred. */ size_t strlcpy(char *dst, const char *src, size_t siz) { char *d = dst; const char *s = src; size_t n = siz; /* Copy as many bytes as will fit */ if (n != 0) { while (--n != 0) { if ((*d++ = *s++) == '\0') break; } } /* Not enough room in dst, add NUL and traverse rest of src */ if (n == 0) { if (siz != 0) *d = '\0'; /* NUL-terminate dst */ while (*s++) ; } return(s - src - 1); /* count does not include NUL */ } ================================================ FILE: fteqtv/libqtvc/msvc_sucks.c ================================================ /* Copyright (C) 2007 Mark Olsen 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 included (LICENSE) 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. */ #ifdef _WIN32 #include #include #include #if _MSC_VER >= 1300 #define vsnprintf q_vsnprintf /*msvc doesn't null terminate. its insecute and thus useless*/ #endif static int vsnprintf_calcsize(const char *fmt, va_list va) { void *mem; unsigned int memsize; int ret; memsize = 1024; do { mem = malloc(memsize); ret = _vsnprintf(mem, memsize-1, fmt,va); if (ret == -1) memsize*= 2; free(mem); } while(ret == -1 && memsize); return ret; } int vsnprintf(char *buf, size_t buflen, const char *fmt, va_list va) { int ret; if (buflen == 0) return vsnprintf_calcsize(fmt, va); ret = _vsnprintf(buf, buflen-1, fmt, va); buf[buflen-1] = 0; if (ret == -1) return vsnprintf_calcsize(fmt, va); return ret; } int snprintf(char *buf, size_t buflen, const char *fmt, ...) { int ret; va_list va; va_start(va, fmt); ret = vsnprintf(buf, buflen, fmt, va); va_end(va); return ret; } #endif ================================================ FILE: fteqtv/mdfour.c ================================================ /* mdfour.c An implementation of MD4 designed for use in the samba SMB authentication protocol Copyright (C) 1997-1998 Andrew Tridgell 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: Free Software Foundation, Inc. 59 Temple Place - Suite 330 Boston, MA 02111-1307, USA $Id$ */ #include "qtv.h" #include /* XoXus: needed for memset call */ #ifndef _MDFOUR_H #define _MDFOUR_H #ifndef int32 #define int32 int #endif #if SIZEOF_INT > 4 #define LARGE_INT32 #endif #ifndef uint32 #define uint32 unsigned int32 #endif struct mdfour { uint32 A, B, C, D; uint32 totalN; }; static void mdfour_begin(struct mdfour *md); // old: MD4Init static void mdfour_update(struct mdfour *md, unsigned char *in, int n); //old: MD4Update static void mdfour_result(struct mdfour *md, unsigned char *out); // old: MD4Final static void mdfour(unsigned char *out, unsigned char *in, int n); #endif // _MDFOUR_H /* NOTE: This code makes no attempt to be fast! It assumes that a int is at least 32 bits long */ #define F(X,Y,Z) (((X)&(Y)) | ((~(X))&(Z))) #define G(X,Y,Z) (((X)&(Y)) | ((X)&(Z)) | ((Y)&(Z))) #define H(X,Y,Z) ((X)^(Y)^(Z)) #ifdef LARGE_INT32 #define lshift(x,s) ((((x)<<(s))&0xFFFFFFFF) | (((x)>>(32-(s)))&0xFFFFFFFF)) #else #define lshift(x,s) (((x)<<(s)) | ((x)>>(32-(s)))) #endif #define ROUND1(a,b,c,d,k,s) a = lshift(a + F(b,c,d) + X[k], s) #define ROUND2(a,b,c,d,k,s) a = lshift(a + G(b,c,d) + X[k] + 0x5A827999,s) #define ROUND3(a,b,c,d,k,s) a = lshift(a + H(b,c,d) + X[k] + 0x6ED9EBA1,s) /* this applies md4 to 64 byte chunks */ static void mdfour64(struct mdfour *m, uint32 *M) { int j; uint32 AA, BB, CC, DD; uint32 X[16]; uint32 A,B,C,D; for (j=0;j<16;j++) X[j] = M[j]; A = m->A; B = m->B; C = m->C; D = m->D; AA = A; BB = B; CC = C; DD = D; ROUND1(A,B,C,D, 0, 3); ROUND1(D,A,B,C, 1, 7); ROUND1(C,D,A,B, 2, 11); ROUND1(B,C,D,A, 3, 19); ROUND1(A,B,C,D, 4, 3); ROUND1(D,A,B,C, 5, 7); ROUND1(C,D,A,B, 6, 11); ROUND1(B,C,D,A, 7, 19); ROUND1(A,B,C,D, 8, 3); ROUND1(D,A,B,C, 9, 7); ROUND1(C,D,A,B, 10, 11); ROUND1(B,C,D,A, 11, 19); ROUND1(A,B,C,D, 12, 3); ROUND1(D,A,B,C, 13, 7); ROUND1(C,D,A,B, 14, 11); ROUND1(B,C,D,A, 15, 19); ROUND2(A,B,C,D, 0, 3); ROUND2(D,A,B,C, 4, 5); ROUND2(C,D,A,B, 8, 9); ROUND2(B,C,D,A, 12, 13); ROUND2(A,B,C,D, 1, 3); ROUND2(D,A,B,C, 5, 5); ROUND2(C,D,A,B, 9, 9); ROUND2(B,C,D,A, 13, 13); ROUND2(A,B,C,D, 2, 3); ROUND2(D,A,B,C, 6, 5); ROUND2(C,D,A,B, 10, 9); ROUND2(B,C,D,A, 14, 13); ROUND2(A,B,C,D, 3, 3); ROUND2(D,A,B,C, 7, 5); ROUND2(C,D,A,B, 11, 9); ROUND2(B,C,D,A, 15, 13); ROUND3(A,B,C,D, 0, 3); ROUND3(D,A,B,C, 8, 9); ROUND3(C,D,A,B, 4, 11); ROUND3(B,C,D,A, 12, 15); ROUND3(A,B,C,D, 2, 3); ROUND3(D,A,B,C, 10, 9); ROUND3(C,D,A,B, 6, 11); ROUND3(B,C,D,A, 14, 15); ROUND3(A,B,C,D, 1, 3); ROUND3(D,A,B,C, 9, 9); ROUND3(C,D,A,B, 5, 11); ROUND3(B,C,D,A, 13, 15); ROUND3(A,B,C,D, 3, 3); ROUND3(D,A,B,C, 11, 9); ROUND3(C,D,A,B, 7, 11); ROUND3(B,C,D,A, 15, 15); A += AA; B += BB; C += CC; D += DD; #ifdef LARGE_INT32 A &= 0xFFFFFFFF; B &= 0xFFFFFFFF; C &= 0xFFFFFFFF; D &= 0xFFFFFFFF; #endif for (j=0;j<16;j++) X[j] = 0; m->A = A; m->B = B; m->C = C; m->D = D; } static void copy64(uint32 *M, unsigned char *in) { int i; for (i=0;i<16;i++) M[i] = (in[i*4+3]<<24) | (in[i*4+2]<<16) | (in[i*4+1]<<8) | (in[i*4+0]<<0); } static void copy4(unsigned char *out,uint32 x) { out[0] = x&0xFF; out[1] = (x>>8)&0xFF; out[2] = (x>>16)&0xFF; out[3] = (x>>24)&0xFF; } static void mdfour_begin(struct mdfour *md) { md->A = 0x67452301; md->B = 0xefcdab89; md->C = 0x98badcfe; md->D = 0x10325476; md->totalN = 0; } static void mdfour_tail(struct mdfour *m, unsigned char *in, int n) { unsigned char buf[128]; uint32 M[16]; uint32 b; m->totalN += n; b = m->totalN * 8; memset(buf, 0, 128); if (n) memcpy(buf, in, n); buf[n] = 0x80; if (n <= 55) { copy4(buf+56, b); copy64(M, buf); mdfour64(m, M); } else { copy4(buf+120, b); copy64(M, buf); mdfour64(m, M); copy64(M, buf+64); mdfour64(m, M); } } static void mdfour_update(struct mdfour *m, unsigned char *in, int n) { uint32 M[16]; // if (n == 0) mdfour_tail(in, n); //Spike: This is where the bug was. while (n >= 64) { copy64(M, in); mdfour64(m, M); in += 64; n -= 64; m->totalN += 64; } mdfour_tail(m, in, n); } static void mdfour_result(struct mdfour *m, unsigned char *out) { copy4(out, m->A); copy4(out+4, m->B); copy4(out+8, m->C); copy4(out+12, m->D); } static void mdfour(unsigned char *out, unsigned char *in, int n) { struct mdfour md; mdfour_begin(&md); mdfour_update(&md, in, n); mdfour_result(&md, out); } /////////////////////////////////////////////////////////////// // MD4-based checksum utility functions // // Copyright (C) 2000 Jeff Teunissen // // Author: Jeff Teunissen // Date: 01 Jan 2000 unsigned Com_BlockChecksum (void *buffer, int length) { int digest[4]; unsigned val; mdfour ( (unsigned char *) digest, (unsigned char *) buffer, length ); val = digest[0] ^ digest[1] ^ digest[2] ^ digest[3]; return val; } void Com_BlockFullChecksum (void *buffer, int len, unsigned char *outbuf) { mdfour ( outbuf, (unsigned char *) buffer, len ); } ================================================ FILE: fteqtv/menu.c ================================================ #include "qtv.h" #define CENTERTIME 1.5 void Menu_Enter(cluster_t *cluster, viewer_t *viewer, int buttonnum) { //build a possible message, even though it'll probably not be sent sv_t *sv; int i, min; switch(viewer->menunum) { default: if (buttonnum < 0) QW_SetMenu(viewer, MENU_MAIN); //no other sort of back button... break; case MENU_MAIN: if (buttonnum < 0) viewer->menuop -= (MENU_MAIN_ITEMCOUNT + 1)/2; else if (buttonnum > 0) viewer->menuop += (MENU_MAIN_ITEMCOUNT + 1)/2; else if (buttonnum == 0) { switch(viewer->menuop) { case MENU_MAIN_STREAMS: //Streams QW_SetMenu(viewer, MENU_SERVERS); break; case MENU_MAIN_CLIENTLIST://Client List QW_SetMenu(viewer, MENU_CLIENTS); break; case MENU_MAIN_NEWSTREAM://New Stream QW_PrintfToViewer(viewer, "Not implemented yet\n"); break; case MENU_MAIN_DEMOS://Demos Cluster_BuildAvailableDemoList(cluster); QW_SetMenu(viewer, MENU_DEMOS); break; case MENU_MAIN_SERVERBROWSER://Server Browser QW_PrintfToViewer(viewer, "Not implemented yet\n"); break; case MENU_MAIN_ADMIN://Admin QW_SetMenu(viewer, MENU_ADMIN); break; case MENU_MAIN_PREVPROX://Previous Proxy if (viewer->isproxy) { QW_SetMenu(viewer, MENU_NONE); QW_StuffcmdToViewer(viewer, "say proxy:menu\n"); } else QW_PrintfToViewer(viewer, "No client proxy detected\n"); break; case MENU_MAIN_NEXTPROX://Next Proxy if (viewer->server && viewer->server->serverisproxy && viewer->server->controller == viewer) { viewer->server->proxyisselected = false; QW_SetMenu(viewer, MENU_NONE); SendClientCommand(viewer->server, "say .menu"); } else QW_PrintfToViewer(viewer, "No server proxy detected\n"); break; case MENU_MAIN_HELP://Help Menu QW_PrintfToViewer(viewer, "Not implemented yet\n"); break; } } break; case MENU_CLIENTS: if (buttonnum >= 0) { viewer_t *v = cluster->viewers; for (i = 0; i < viewer->menuop && v; i++) v = v->next; if (!v) break; if (v == viewer) { if (viewer->commentator) QW_SetCommentator(cluster, viewer, NULL); else QW_PrintfToViewer(viewer, "Please stop touching yourself\n"); } else QW_SetCommentator(cluster, viewer, v); } else QW_SetMenu(viewer, MENU_MAIN); break; case MENU_DEMOS: if (buttonnum >= 0) QW_StuffcmdToViewer(viewer, "say .demo %s\n", cluster->availdemos[viewer->menuop].name); else QW_SetMenu(viewer, MENU_MAIN); break; case MENU_ADMINSERVER: if (viewer->server) { i = 0; sv = viewer->server; if (i++ == viewer->menuop) { //auto disconnect sv->autodisconnect = sv->autodisconnect?AD_NO:AD_WHENEMPTY; } if (i++ == viewer->menuop) { //disconnect QTV_ShutdownStream(viewer->server); } if (i++ == viewer->menuop) { if (sv->controller == viewer) sv->controller = NULL; else { sv->controller = viewer; sv->controllersquencebias = viewer->netchan.outgoing_sequence - sv->netchan.outgoing_sequence; } } if (i++ == viewer->menuop) { //back QW_SetMenu(viewer, MENU_ADMIN); } break; } //fallthrough case MENU_SERVERS: if (buttonnum < 0) QW_SetMenu(viewer, MENU_MAIN); else if (!cluster->servers) { QW_StuffcmdToViewer(viewer, "echo Please enter a server ip\nmessagemode\n"); strcpy(viewer->expectcommand, "insecadddemo"); } else { if (viewer->menuop < 0) viewer->menuop = 0; i = 0; min = viewer->menuop - 10; if (min < 0) min = 0; for (sv = cluster->servers; sv && inext, i++) {//skip over the early connections. } min+=20; for (; sv && i < min; sv = sv->next, i++) { if (i == viewer->menuop) { /*if (sv->parsingconnectiondata || !sv->modellist[1].name[0]) { QW_PrintfToViewer(viewer, "But that stream isn't connected\n"); } else*/ { QW_SetViewersServer(cluster, viewer, sv); QW_SetMenu(viewer, MENU_NONE); viewer->thinksitsconnected = false; } break; } } } break; case MENU_ADMIN: i = 0; if (i++ == viewer->menuop) { //connection stuff QW_SetMenu(viewer, MENU_ADMINSERVER); } if (i++ == viewer->menuop) { //qw port QW_StuffcmdToViewer(viewer, "echo You will need to reconnect\n"); cluster->qwlistenportnum += (buttonnum<0)?-1:1; } if (i++ == viewer->menuop) { //hostname strcpy(viewer->expectcommand, "hostname"); QW_StuffcmdToViewer(viewer, "echo Please enter the new hostname\nmessagemode\n"); } if (i++ == viewer->menuop) { //master strcpy(viewer->expectcommand, "master"); QW_StuffcmdToViewer(viewer, "echo Please enter the master dns or ip\necho Enter '.' for masterless mode\nmessagemode\n"); } if (i++ == viewer->menuop) { //password strcpy(viewer->expectcommand, "password"); QW_StuffcmdToViewer(viewer, "echo Please enter the new rcon password\nmessagemode\n"); } if (i++ == viewer->menuop) { //add server strcpy(viewer->expectcommand, "messagemode"); QW_StuffcmdToViewer(viewer, "echo Please enter the new qtv server dns or ip\naddserver\n"); } if (i++ == viewer->menuop) { //add demo strcpy(viewer->expectcommand, "adddemo"); QW_StuffcmdToViewer(viewer, "echo Please enter the name of the demo to play\nmessagemode\n"); } if (i++ == viewer->menuop) { //choke cluster->chokeonnotupdated ^= 1; } if (i++ == viewer->menuop) { //late forwarding cluster->lateforward ^= 1; } if (i++ == viewer->menuop) { //no talking cluster->notalking ^= 1; } if (i++ == viewer->menuop) { //nobsp cluster->nobsp ^= 1; } if (i++ == viewer->menuop) { //back QW_SetMenu(viewer, MENU_NONE); } break; } } void WriteStringSelection(netmsg_t *b, qboolean selected, const char *str) { if (selected) { WriteByte(b, 13); while(*str) WriteByte(b, 128|*str++); } else { WriteByte(b, ' '); while(*str) WriteByte(b, *str++); } } void Menu_Draw(cluster_t *cluster, viewer_t *viewer) { char buffer[2048]; char str[256]; sv_t *sv; int i, min; unsigned char *s; netmsg_t m; if (viewer->backbuffered) return; if (viewer->menunum == MENU_FORWARDING) return; if (viewer->menuspamtime > cluster->curtime && viewer->menuspamtime < cluster->curtime + CENTERTIME*2000) return; viewer->menuspamtime = cluster->curtime + CENTERTIME*1000; InitNetMsg(&m, buffer, sizeof(buffer)); WriteByte(&m, svc_centerprint); WriteString2(&m, "/PFTEQTV "QTV_VERSION_STRING"\n"); WriteString2(&m, PROXYWEBSITE"\n"); WriteString2(&m, "-------------\n"); if (strcmp(cluster->hostname, DEFAULT_HOSTNAME)) WriteString2(&m, cluster->hostname); switch(viewer->menunum) { default: WriteString2(&m, "bad menu"); break; case MENU_MAIN: { WriteString2(&m, "\n\x1d\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1f\n"); while (viewer->menuop < 0) viewer->menuop += MENU_MAIN_ITEMCOUNT; while (viewer->menuop >= MENU_MAIN_ITEMCOUNT) viewer->menuop -= MENU_MAIN_ITEMCOUNT; i = viewer->menuop; WriteStringSelection(&m, i==MENU_MAIN_STREAMS, "Streams "); WriteStringSelection(&m, i==MENU_MAIN_CLIENTLIST, "Client List "); WriteByte(&m, '\n'); WriteStringSelection(&m, i==MENU_MAIN_NEWSTREAM, "New Stream "); WriteStringSelection(&m, i==MENU_MAIN_DEMOS, "Demos "); WriteByte(&m, '\n'); WriteStringSelection(&m, i==MENU_MAIN_SERVERBROWSER,"Server Browser "); WriteStringSelection(&m, i==MENU_MAIN_ADMIN, "Admin "); WriteByte(&m, '\n'); WriteStringSelection(&m, i==MENU_MAIN_PREVPROX, "Previous Proxy "); WriteStringSelection(&m, i==MENU_MAIN_NEXTPROX, "Next Proxy "); WriteByte(&m, '\n'); WriteStringSelection(&m, i==MENU_MAIN_HELP, "Help "); WriteString2(&m, " "); WriteString2(&m, "\n\x1d\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1f\n"); } break; case MENU_CLIENTS: { int start; viewer_t *v; char *srv; int c; v = cluster->viewers; if (viewer->menuop < 0) viewer->menuop = 0; if (viewer->menuop > cluster->numviewers - 1) viewer->menuop = cluster->numviewers - 1; WriteString2(&m, "\nActive Clients\n\n"); start = viewer->menuop & ~7; for (i = 0; i < start && v; i++) v = v->next; for (i = start; i < start+8 && v; i++, v = v->next) { for (c = strlen(v->name); c < 14; c++) WriteByte(&m, ' '); WriteStringSelection(&m, viewer->menuop == i, v->name); WriteString2(&m, ": "); if (v->server) { if (!v->server->sourcefile && !v->server->parsingconnectiondata) srv = v->server->map.hostname; else srv = v->server->server; } else srv = "None"; for (c = 0; c < 20; c++) { if (*srv) WriteByte(&m, *srv++); else WriteByte(&m, ' '); } WriteByte(&m, '\n'); } for (; i < start+8; i++) WriteByte(&m, '\n'); } break; case MENU_DEMOS: { int start; WriteString2(&m, "\nAvailable Demos\n\n"); if (cluster->availdemoscount == 0) { WriteString2(&m, "No demos are available"); break; } if (viewer->menuop < 0) viewer->menuop = 0; if (viewer->menuop > cluster->availdemoscount-1) viewer->menuop = cluster->availdemoscount-1; start = viewer->menuop & ~7; for (i = start; i < start+8; i++) { char cleanname[128]; char *us; strlcpy(cleanname, cluster->availdemos[i].name, sizeof(cleanname)); for (us = cleanname; *us; us++) if (*us == '_') *us = ' '; WriteStringSelection(&m, i == viewer->menuop, cleanname); WriteByte(&m, '\n'); } } break; case MENU_ADMINSERVER: //per-connection options if (viewer->server) { sv = viewer->server; WriteString2(&m, "\n\nConnection Admin\n"); WriteString2(&m, sv->map.hostname); if (sv->sourcefile) WriteString2(&m, " (demo)"); WriteString2(&m, "\n\n"); if (viewer->menuop < 0) viewer->menuop = 0; i = 0; WriteString2(&m, " auto disconnect"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); switch(viewer->server->autodisconnect) { default: case AD_NO: sprintf(str, "%-20s", "permanent connection"); break; case AD_REVERSECONNECT: sprintf(str, "%-20s", "when server disconnects"); break; case AD_WHENEMPTY: sprintf(str, "%-20s", "when inactive"); break; case AD_STATUSPOLL: sprintf(str, "%-20s", "idle when empty"); break; } WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, "force disconnect"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " take control"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " back"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); if (viewer->menuop >= i) viewer->menuop = i - 1; WriteString2(&m, "\n"); WriteString2(&m, " status"); WriteString2(&m, " : "); sprintf(str, "%-20s", viewer->server->status); WriteString2(&m, str); WriteString2(&m, "\n"); break; } //fallthrough case MENU_SERVERS: //connections list WriteString2(&m, "\n\nServers\n\n"); if (!cluster->servers) { WriteString2(&m, "No active connections"); } else { if (viewer->menuop < 0) viewer->menuop = 0; i = 0; min = viewer->menuop - 10; if (min < 0) min = 0; for (sv = cluster->servers; sv && inext, i++) {//skip over the early connections. } min+=20; for (; sv && i < min; sv = sv->next, i++) { //Info_ValueForKey(sv->serverinfo, "hostname", str, sizeof(str)); //if (sv->parsingconnectiondata || !sv->modellist[1].name[0]) // snprintf(str, sizeof(str), "%s", sv->server); snprintf(str, sizeof(str), "%s", *sv->map.hostname?sv->map.hostname:sv->server); if (i == viewer->menuop) for (s = (unsigned char *)str; *s; s++) { if ((unsigned)*s >= ' ') *s = 128 | (*s&~128); } WriteString2(&m, str); WriteString2(&m, "\n"); } } break; case MENU_ADMIN: //admin menu WriteString2(&m, "\n\nCluster Admin\n\n"); if (viewer->menuop < 0) viewer->menuop = 0; i = 0; WriteString2(&m, " this connection"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " port"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20i", cluster->qwlistenportnum); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " hostname"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", cluster->hostname); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " master"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", cluster->master); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " password"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " add server"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " add demo"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " choke"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", cluster->chokeonnotupdated?"yes":"no"); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, "delay forwarding"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", cluster->lateforward?"yes":"no"); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " talking"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", cluster->notalking?"no":"yes"); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " nobsp"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", cluster->nobsp?"yes":"no"); WriteString2(&m, str); WriteString2(&m, "\n"); WriteString2(&m, " back"); WriteString2(&m, (viewer->menuop==(i++))?" \r ":" : "); sprintf(str, "%-20s", "..."); WriteString2(&m, str); WriteString2(&m, "\n"); if (viewer->menuop >= i) viewer->menuop = i - 1; break; } WriteByte(&m, 0); SendBufferToViewer(viewer, m.data, m.cursize, true); } ================================================ FILE: fteqtv/msg.c ================================================ #include "qtv.h" void InitNetMsg(netmsg_t *b, void *buffer, int bufferlength) { b->data = buffer; b->maxsize = bufferlength; b->readpos = 0; b->cursize = 0; } unsigned char ReadByte(netmsg_t *b) { if (b->readpos >= b->cursize) { b->readpos = b->cursize+1; return 0; } return ((unsigned char *)b->data)[b->readpos++]; } unsigned short ReadShort(netmsg_t *b) { int b1, b2; b1 = ReadByte(b); b2 = ReadByte(b); return b1 | (b2<<8); } unsigned short ReadBigShort(netmsg_t *b) { int b1, b2; b1 = ReadByte(b); b2 = ReadByte(b); return (b1<<8) | b2; } unsigned int ReadLong(netmsg_t *b) { int s1, s2; s1 = ReadShort(b); s2 = ReadShort(b); return s1 | (s2<<16); } unsigned int ReadBigLong(netmsg_t *b) { unsigned int s1, s2; s1 = ReadBigShort(b); s2 = ReadBigShort(b); return (s1<<16) | s2; } unsigned int BigLong(unsigned int val) { union { unsigned int i; unsigned char c[4]; } v; v.i = val; return (v.c[0]<<24) | (v.c[1] << 16) | (v.c[2] << 8) | (v.c[3] << 0); } unsigned int SwapLong(unsigned int val) { union { unsigned int i; unsigned char c[4]; } v; unsigned char s; v.i = val; s = v.c[0]; v.c[0] = v.c[3]; v.c[3] = s; s = v.c[1]; v.c[1] = v.c[2]; v.c[2] = s; return v.i; } float ReadFloat(netmsg_t *b) { union { unsigned int i; float f; } u; u.i = ReadLong(b); return u.f; } void ReadString(netmsg_t *b, char *string, int maxlen) { maxlen--; //for null terminator while(maxlen) { *string = ReadByte(b); if (!*string) return; string++; maxlen--; } *string++ = '\0'; //add the null printf("ReadString: buffer is too small\n"); while(ReadByte(b)) //finish reading the string, even if we will loose part of it ; } float ReadCoord(netmsg_t *b, unsigned int pext1) { if (pext1 & PEXT_FLOATCOORDS) return ReadFloat(b); else return (short)ReadShort(b) / 8.0; } float ReadAngle(netmsg_t *b, unsigned int pext1) { if (pext1 & PEXT_FLOATCOORDS) return (ReadShort(b) * 360.0) / 0x10000; else return (ReadByte(b) * 360.0) / 0x100; } void WriteByte(netmsg_t *b, unsigned char c) { if (b->cursize>=b->maxsize) return; ((unsigned char*)b->data)[b->cursize++] = c; } void WriteShort(netmsg_t *b, unsigned short l) { WriteByte(b, (l&0x00ff)>>0); WriteByte(b, (l&0xff00)>>8); } void WriteBigShort(netmsg_t *b, unsigned short l) { WriteByte(b, (l&0xff00)>>8); WriteByte(b, (l&0x00ff)>>0); } void WriteLong(netmsg_t *b, unsigned int l) { WriteByte(b, (l&0x000000ff)>>0); WriteByte(b, (l&0x0000ff00)>>8); WriteByte(b, (l&0x00ff0000)>>16); WriteByte(b, (l&0xff000000)>>24); } void WriteBigLong(netmsg_t *b, unsigned int l) { WriteByte(b, (l&0xff000000)>>24); WriteByte(b, (l&0x00ff0000)>>16); WriteByte(b, (l&0x0000ff00)>>8); WriteByte(b, (l&0x000000ff)>>0); } void WriteFloat(netmsg_t *b, float f) { union { unsigned int i; float f; } u; u.f = f; WriteLong(b, u.i); } void WriteCoord(netmsg_t *b, float c, unsigned int pext) { if (pext & PEXT_FLOATCOORDS) WriteFloat(b, c); else WriteShort(b, (short)(c*8)); } void WriteAngle(netmsg_t *b, float a, unsigned int pext) { if (pext & PEXT_FLOATCOORDS) WriteShort(b, (a/360.0)*0x10000); else WriteByte(b, (a/360.0)*0x100 + 0.49); //round better, to avoid rounding bias } void WriteString2(netmsg_t *b, const char *str) { //no null terminator, convienience function. while(*str) WriteByte(b, *str++); } void WriteString(netmsg_t *b, const char *str) { while(*str) WriteByte(b, *str++); WriteByte(b, 0); } void WriteData(netmsg_t *b, const void *data, int length) { int i; unsigned char *buf; if (b->cursize + length > b->maxsize) //urm, that's just too big. :( return; buf = (unsigned char*)b->data+b->cursize; for (i = 0; i < length; i++) *buf++ = ((const unsigned char*)data)[i]; b->cursize+=length; } void WriteCoordf(netmsg_t *b, unsigned int pext, float fl) { if (pext & PEXT_FLOATCOORDS) WriteFloat(b, fl); else WriteShort(b, fl*8); } void WriteAnglef(netmsg_t *b, unsigned int pext, float fl) { if (pext & PEXT_FLOATCOORDS) WriteShort(b, (fl/360)*0x10000); else WriteByte(b, (fl/360)*0x100); } ================================================ FILE: fteqtv/net_qtv.h ================================================ /* This file is intended as a set of exports for an NQ-based engine. This is supported _purely_ for clients, and will not work for servers (safe no-op). [EndUser] how to use: to join a qw server: connect "udp:127.0.0.1:27500" to watch a qtv stream: connect "tcp:3@127.0.0.1:27599" (where '3' is the streamid) to watch an mvd demo: connect "demo:blahblah.mvd" - the demo will be loaded from $WORKINGDIR/qw/demos/ - note that $WORKINGDIR is NOT always the same dir as your engine is running from. The -basedir argument will break it, or engines that hunt down a 'proper' installation of quake instead. [Developer] how to incorporate into an nq engine: copy this file (net_qtv.h) into your source directory, next to your other net_*.h files load up net_win.c find the #include "net_wins.h" line. dupe it, call it net_qtv.h find the net_landrivers array. Dupe the first block, then edit the first block to be all QTV_foo functions. bump net_numlandrivers. For non-window operating systems, you'll need to do the same, just figure out which net_win.c equivelent function it uses first. :P certain engines may do weird things with the port. probably its best to just use Cmd_Args() for the connect command instead of Cmd_Argv(1), and to add port parsing to XXXX_GetAddrFromName instead of messing around with port cvars etc and ruining server configs. If your engine already has weird port behaviour, then its entirely your problem to fix. :P You probably want to tweak your menus a little to clean up the nq/qw/qtv connection distinctions. If you do want to make changes to libqtv, please consider joining the FTE team (or at least the irc channel) in order to contribute without forking. [Developer] how to compile libqtv: cflags MUST define 'LIBQTV' or it won't compile properly. The relevent exports are all tagged as 'EXPORT void PUBLIC fname(...)' (dllexport+cdecl in windows), feel free to define those properly if you're making a linux shared object without exporting all (potentially conflicting) internals. This means you can compile it as a dll without any issues, one with a standardized interface. Any libqtv-specific bugfixes can be released independantly from engine(s). Compiling a dll with msvc will generally automatically produce a .lib which you can directly link against. Alternatively, include both projects in your workspace and set up dependancies properly and it'll be automatically imported. [PowerUser] issues: its a full qtv proxy, but you can't get admin/rcon access to it. it doesn't read any configs, and has no console, thus you cannot set an rcon password/port. without console/rcon, you cannot enable any listening ports for other users. if you need a public qtv proxy, use a standalone version. */ //#define EXPORT is defined declspec export for dll builds. #ifdef _WIN32 #define PUBLIC __cdecl #endif #ifndef EXPORT #define EXPORT #endif #ifndef PUBLIC #define PUBLIC #endif #define PUBEXPORT EXPORT //vanilla 'net_win.c' datagram driver function listings PUBEXPORT int PUBLIC QTV_Init (void); PUBEXPORT void PUBLIC QTV_Shutdown (void); PUBEXPORT void PUBLIC QTV_Listen (qboolean state); PUBEXPORT int PUBLIC QTV_OpenSocket (int port); PUBEXPORT int PUBLIC QTV_CloseSocket (int socket); PUBEXPORT int PUBLIC QTV_Connect (int socket, struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_CheckNewConnections (void); PUBEXPORT int PUBLIC QTV_Read (int socket, byte *buf, int len, struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_Write (int socket, byte *buf, int len, struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_Broadcast (int socket, byte *buf, int len); PUBEXPORT char *PUBLIC QTV_AddrToString (struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_StringToAddr (char *string, struct qsockaddr *addr); //port is part of the string. libqtv will use its own default port. PUBEXPORT int PUBLIC QTV_GetSocketAddr (int socket, struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_GetNameFromAddr (struct qsockaddr *addr, char *name); PUBEXPORT int PUBLIC QTV_GetAddrFromName (char *name, struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_AddrCompare (struct qsockaddr *addr1, struct qsockaddr *addr2); PUBEXPORT int PUBLIC QTV_GetSocketPort (struct qsockaddr *addr); PUBEXPORT int PUBLIC QTV_SetSocketPort (struct qsockaddr *addr, int port); //additional functions for enhanced engines that changed the net_dgrm system api or other uses. PUBEXPORT void PUBLIC QTV_Command (char *command, char *resulttext, int resultlen); //sends a console command to the qtv proxy. you can make it a formal proxy with this. must have been inited. PUBEXPORT void PUBLIC QTV_RunFrame (void); //runs a proxy frame without needing a connection to be active. PUBEXPORT int PUBLIC QTV_StringPortToAddr (char *string, int port, struct qsockaddr *addr); //port may be part of the string. the specified port will be used, or just pass 0 for libqtv to make it up. generally you'll want to pass the 'port' cvar for port, and the exact argument that was typed in the string arg. ================================================ FILE: fteqtv/netchan.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ #include "qtv.h" #include #define curtime Sys_Milliseconds() void NET_InitUDPSocket(cluster_t *cluster, int port, int socketid) { int sock; int pf; struct sockaddr *address; struct sockaddr_in address4; struct sockaddr_in6 address6; int addrlen; unsigned long nonblocking = true; unsigned long v6only = false; #pragma message("fixme") switch(socketid) { case SG_IPV6: pf = PF_INET6; memset(&address6, 0, sizeof(address6)); address6.sin6_family = AF_INET6; address6.sin6_port = htons((u_short)port); address = (struct sockaddr*)&address6; addrlen = sizeof(address6); break; case SG_IPV4: pf = PF_INET; address4.sin_family = AF_INET; address4.sin_addr.s_addr = INADDR_ANY; address4.sin_port = htons((u_short)port); address = (struct sockaddr*)&address4; addrlen = sizeof(address4); break; default: return; //erk } if (socketid == SG_IPV4 && !v6only && cluster->qwdsocket[SG_IPV6] != INVALID_SOCKET) { int sz = sizeof(v6only); if (getsockopt(cluster->qwdsocket[SG_IPV6], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, &sz) == 0 && !v6only) port = 0; } if (!port) { if (cluster->qwdsocket[socketid] != INVALID_SOCKET) { closesocket(cluster->qwdsocket[socketid]); cluster->qwdsocket[socketid] = INVALID_SOCKET; Sys_Printf(cluster, "closed udp%i port\n", socketid?6:4); } return; } if ((sock = socket (pf, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) { return; } if (ioctlsocket (sock, FIONBIO, &nonblocking) == -1) { closesocket(sock); return; } if (pf == AF_INET6) if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)) == -1) v6only = true; if (bind (sock, (void *)address, addrlen) == -1) { printf("socket bind error %i (%s)\n", qerrno, strerror(qerrno)); closesocket(sock); return; } if (cluster->qwdsocket[socketid] != INVALID_SOCKET) { closesocket(cluster->qwdsocket[socketid]); Sys_Printf(cluster, "closed udp%i port\n", socketid?6:4); } cluster->qwdsocket[socketid] = sock; if (v6only) Sys_Printf(cluster, "opened udp%i port %i\n", socketid?6:4, port); else Sys_Printf(cluster, "opened udp port %i\n", port); } SOCKET NET_ChooseSocket(SOCKET sock[SOCKETGROUPS], netadr_t *toadr, netadr_t ina) { #ifdef AF_INET6 if (((struct sockaddr *)ina.sockaddr)->sa_family == AF_INET6) { *toadr = ina; return sock[SG_IPV6]; } if (sock[SG_IPV4] == INVALID_SOCKET && sock[SG_IPV6] != INVALID_SOCKET) { struct sockaddr_in6 *out = (struct sockaddr_in6*)toadr->sockaddr; struct sockaddr_in *in = (struct sockaddr_in*)ina.sockaddr; toadr->tcpcon = ina.tcpcon; memset(out, 0, sizeof(*out)); out->sin6_family = AF_INET6; *(short*)&out->sin6_addr.s6_addr[10] = 0xffff; *(int*)&out->sin6_addr.s6_addr[12] = in->sin_addr.s_addr; out->sin6_port = in->sin_port; return sock[SG_IPV6]; } #endif *toadr = ina; return sock[SG_IPV4]; } #ifdef LIBQTV void QTV_DoReceive(void *data, int length); #endif void NET_SendPacket(cluster_t *cluster, SOCKET sock, int length, void *data, netadr_t adr) { int ret; int alen; #ifdef LIBQTV if (((struct sockaddr *)&adr.sockaddr)->sa_family == AF_UNSPEC) { QTV_DoReceive(data, length); return; } #endif #ifdef AF_INET6 if (((struct sockaddr *)&adr.sockaddr)->sa_family == AF_INET6) alen = sizeof(struct sockaddr_in6); else #endif alen = sizeof(struct sockaddr_in); if (adr.tcpcon) { tcpconnect_t *dest; for (dest = cluster->tcpconnects; dest; dest = dest->next) { if (dest == adr.tcpcon) break; } if (dest) { int l; if (dest->websocket.websocket) { int datatype = 2; //1=utf-8, 2=binary int enclen = 0, c; if (datatype == 2) enclen = length; else { for (c = 0; c < length; c++) { if (((unsigned char*)data)[c] == 0 || ((unsigned char*)data)[c] >= 0x80) enclen += 2; else enclen += 1; } } if (dest->outbuffersize + 4+enclen < sizeof(dest->outbuffer)) { if (enclen >= 126) { dest->outbuffer[dest->outbuffersize++] = 0x80|datatype; dest->outbuffer[dest->outbuffersize++] = 126; dest->outbuffer[dest->outbuffersize++] = enclen>>8; dest->outbuffer[dest->outbuffersize++] = enclen; } else { dest->outbuffer[dest->outbuffersize++] = 0x80|datatype; dest->outbuffer[dest->outbuffersize++] = enclen; } if (datatype == 2) { memcpy(dest->outbuffer+dest->outbuffersize, data, enclen); dest->outbuffersize += enclen; } else { while(length-->0) { c = *(unsigned char*)data; data = (char*)data+1; if (!c) c |= 0x100; /*will get truncated at the other end*/ if (c >= 0x80) { dest->outbuffer[dest->outbuffersize++] = 0xc0 | (c>>6); dest->outbuffer[dest->outbuffersize++] = 0x80 | (c & 0x3f); } else dest->outbuffer[dest->outbuffersize++] = c; } } } } else { if (dest->outbuffersize + length < sizeof(dest->outbuffer)) { dest->outbuffer[dest->outbuffersize++] = length>>8; dest->outbuffer[dest->outbuffersize++] = length&0xff; memcpy(dest->outbuffer+dest->outbuffersize, data, length); dest->outbuffersize += length; } } if (dest->outbuffersize) { l = send(dest->sock, dest->outbuffer, dest->outbuffersize, 0); if (l > 0) { memmove(dest->outbuffer, dest->outbuffer+l, dest->outbuffersize-l); dest->outbuffersize-=l; } } } return; } ret = sendto(sock, data, length, 0, (struct sockaddr *)&adr.sockaddr, alen); if (ret < 0) { int er = qerrno; if (er == NET_EWOULDBLOCK || er == NET_EAGAIN) return; Sys_Printf(cluster, "udp send error %i (%s)\n", er, strerror(er)); } } #if 0 int Netchan_IsLocal (netadr_t adr) { struct sockaddr_in *sadr = (struct sockaddr_in *)&adr; unsigned char *bytes; switch(((struct sockaddr *)&adr)->sa_family) { case AF_INET: bytes = (unsigned char *)&((struct sockaddr_in *)&adr)->sin_addr; if (bytes[0] == 127 && /*actualy, it should be only the first octet, but hey*/ bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 1) return true; return false; case AF_INET6: bytes = (unsigned char *)&((struct sockaddr_in6 *)&adr)->sin6_addr; if (bytes[ 0] == 0 && bytes[ 1] == 0 && bytes[ 2] == 0 && bytes[ 3] == 0 && bytes[ 4] == 0 && bytes[ 5] == 0 && bytes[ 6] == 0 && bytes[ 7] == 0 && bytes[ 8] == 0 && bytes[ 9] == 0 && bytes[10] == 0 && bytes[11] == 0 && bytes[12] == 0 && bytes[13] == 0 && bytes[14] == 0 && bytes[15] == 1) return true; return false; default: return false; } } #endif #define PACKET_HEADER 8 /* packet header ------------- 31 sequence 1 does this message contain a reliable payload 31 acknowledge sequence 1 acknowledge receipt of even/odd message 16 qport The remote connection never knows if it missed a reliable message, the local side detects that it has been dropped by seeing a sequence acknowledge higher than the last reliable sequence, but without the correct even/odd bit for the reliable set. If the sender notices that a reliable message has been dropped, it will be retransmitted. It will not be retransmitted again until a message after the retransmit has been acknowledged and the reliable still failed to get there. If the sequence number is -1, the packet should be handled without a netcon. The reliable message can be added to at any time by doing MSG_Write* (&netchan->message, ). If the message buffer is overflowed, either by a single message, or by multiple frames worth piling up while the last reliable transmit goes unacknowledged, the netchan signals a fatal error. Reliable messages are always placed first in a packet, then the unreliable message is included if there is sufficient room. To the receiver, there is no distinction between the reliable and unreliable parts of the message, they are just processed out as a single larger message. Illogical packet sequence numbers cause the packet to be dropped, but do not kill the connection. This, combined with the tight window of valid reliable acknowledgement numbers provides protection against malicious address spoofing. The qport field is a workaround for bad address translating routers that sometimes remap the client's source port on a packet during gameplay. If the base part of the net address matches and the qport matches, then the channel matches even if the IP port differs. The IP port should be updated to the new value before sending out any replies. */ /* =============== Netchan_OutOfBand Sends an out-of-band datagram ================ */ void Netchan_OutOfBandSocket (cluster_t *cluster, SOCKET sock, netadr_t *adr, int length, void *data) { netmsg_t send; unsigned char send_buf[MAX_MSGLEN + PACKET_HEADER]; // write the packet header InitNetMsg (&send, send_buf, sizeof(send_buf)); WriteLong (&send, -1); // -1 sequence means out of band WriteData (&send, data, length); // send the datagram NET_SendPacket (cluster, sock, send.cursize, send.data, *adr); } /* =============== Netchan_OutOfBand Sends an out-of-band datagram ================ */ void Netchan_OutOfBand (cluster_t *cluster, netadr_t adr, int length, void *data) { netadr_t realadr; Netchan_OutOfBandSocket(cluster, NET_ChooseSocket(cluster->qwdsocket, &realadr, adr), &realadr, length, data); } /* =============== Netchan_OutOfBandPrint Sends a text message in an out-of-band datagram ================ */ void Netchan_OutOfBandPrint (cluster_t *cluster, netadr_t adr, char *format, ...) { va_list argptr; char string[8192]; va_start (argptr, format); #ifdef _WIN32 _vsnprintf (string, sizeof(string) - 1, format, argptr); string[sizeof(string) - 1] = '\0'; #else vsnprintf (string, sizeof(string), format, argptr); #endif // _WIN32 va_end (argptr); Netchan_OutOfBandSocket(cluster, NET_ChooseSocket(cluster->qwdsocket, &adr, adr), &adr, strlen(string), (unsigned char *)string); } /* =============== Netchan_Init =============== */ void Netchan_Init (netadr_t adr) { } /* ============== Netchan_Setup called to open a channel to a remote system ============== */ void Netchan_Setup (SOCKET sock, netchan_t *chan, netadr_t adr, int qport, qboolean isclient) { memset (chan, 0, sizeof(*chan)); chan->sock = sock; memcpy(&chan->remote_address, &adr, sizeof(netadr_t)); chan->qport = qport; chan->isclient = isclient; chan->last_received = curtime; InitNetMsg(&chan->message, chan->message_buf, sizeof(chan->message_buf)); chan->message.allowoverflow = true; chan->rate = 10000*1000; } /* =============== Netchan_CanPacket Returns true if the bandwidth choke isn't active ================ */ qboolean Netchan_CanPacket (netchan_t *chan) { unsigned int t; // unlimited bandwidth for local client // if (chan->remote_address.type == NA_LOOPBACK) // return true; t = curtime; if (chan->cleartime < t) return true; return false; } /* =============== Netchan_CanReliable Returns true if the bandwidth choke isn't ================ */ qboolean Netchan_CanReliable (netchan_t *chan) { if (chan->reliable_length) return false; // waiting for ack return Netchan_CanPacket (chan); } /* =============== Netchan_Transmit tries to send an unreliable message to a connection, and handles the transmition / retransmition of the reliable messages. A 0 length will still generate a packet and deal with the reliable messages. ================ */ void Netchan_Transmit (cluster_t *cluster, netchan_t *chan, int length, const void *data) { unsigned int t; netmsg_t send; unsigned char send_buf[MAX_NQMSGLEN + PACKET_HEADER]; qboolean send_reliable; unsigned w1, w2; if (chan->isnqprotocol) { int i; send.data = send_buf; send.maxsize = MAX_NQMSGLEN + PACKET_HEADER; send.cursize = 0; if (!chan->reliable_length && chan->message.cursize) { memcpy (chan->reliable_buf, chan->message_buf, chan->message.cursize); chan->reliable_length = chan->message.cursize; chan->reliable_start = 0; chan->message.cursize = 0; } i = chan->reliable_length - chan->reliable_start; if (i>0) { WriteLong(&send, 0); WriteLong(&send, SwapLong(chan->reliable_sequence)); if (i > MAX_NQDATAGRAM) i = MAX_NQDATAGRAM; WriteData (&send, chan->reliable_buf+chan->reliable_start, i); // if (length && send.cursize + length < send.maxsize) // { //throw the unreliable packet into the same one as the reliable (but not sent reliably) // WriteData (&send, data, length); // length = 0; // } if (chan->reliable_start+i == chan->reliable_length) *(int*)send_buf = BigLong(NETFLAG_DATA | NETFLAG_EOM | send.cursize); else *(int*)send_buf = BigLong(NETFLAG_DATA | send.cursize); NET_SendPacket(cluster, chan->sock, send.cursize, send.data, chan->remote_address); send.cursize = 0; if (chan->cleartime < curtime) chan->cleartime = curtime + (int)(send.cursize*chan->rate); else chan->cleartime += (int)(send.cursize*chan->rate); } // else if (!length) // { // length = 1; // data = "\x01"; // } //send out the unreliable (if still unsent) if (length) { WriteLong(&send, 0); WriteLong(&send, SwapLong(chan->outgoing_unreliable)); chan->outgoing_unreliable++; WriteData (&send, data, length); *(int*)send_buf = BigLong(NETFLAG_UNRELIABLE | send.cursize); NET_SendPacket (cluster, chan->sock, send.cursize, send.data, chan->remote_address); if (chan->cleartime < curtime) chan->cleartime = (int)(curtime + send.cursize*chan->rate); else chan->cleartime += (int)(send.cursize*chan->rate); send.cursize = 0; } return; } // check for message overflow if (chan->message.overflowed) { chan->drop = true; // printf ("%s:Outgoing message overflow\n" // , NET_AdrToString (chan->remote_address)); return; } // if the remote side dropped the last reliable message, resend it send_reliable = false; if (chan->incoming_acknowledged > chan->last_reliable_sequence && chan->incoming_reliable_acknowledged != chan->reliable_sequence) send_reliable = true; // if the reliable transmit buffer is empty, copy the current message out if (!chan->reliable_length && chan->message.cursize) { memcpy (chan->reliable_buf, chan->message_buf, chan->message.cursize); chan->reliable_length = chan->message.cursize; chan->message.cursize = 0; chan->reliable_sequence ^= 1; send_reliable = true; } // write the packet header send.data = send_buf; send.maxsize = sizeof(send_buf); send.readpos = send.cursize = 0; w1 = chan->outgoing_sequence | (send_reliable<<31); w2 = chan->incoming_sequence | (chan->incoming_reliable_sequence<<31); chan->outgoing_sequence++; WriteLong (&send, w1); WriteLong (&send, w2); // send the qport if we are a client if (chan->isclient) WriteShort (&send, chan->qport); // copy the reliable message to the packet first if (send_reliable) { WriteData (&send, chan->reliable_buf, chan->reliable_length); chan->last_reliable_sequence = chan->outgoing_sequence; } // add the unreliable part if space is available if (send.maxsize - send.cursize >= length) WriteData (&send, data, length); // send the datagram // i = chan->outgoing_sequence & (MAX_LATENT-1); // chan->outgoing_size[i] = send.cursize; // chan->outgoing_time[i] = curtime; NET_SendPacket (cluster, chan->sock, send.cursize, send.data, chan->remote_address); t = curtime; if (chan->cleartime < t) chan->cleartime = t + (int)(send.cursize*chan->rate); else chan->cleartime += (int)(send.cursize*chan->rate); #ifndef CLIENTONLY // if (chan->sock == NS_SERVER && sv_paused.value) // chan->cleartime = curtime; #endif /* if (showpackets.value) Com_Printf ("--> s=%i(%i) a=%i(%i) %i\n" , chan->outgoing_sequence , send_reliable , chan->incoming_sequence , chan->incoming_reliable_sequence , send.cursize); */ } qboolean NQNetchan_Process(cluster_t *cluster, netchan_t *chan, netmsg_t *msg) { int header; int sequence; int drop; msg->readpos = 0; header = SwapLong(ReadLong(msg)); if (msg->cursize != (header & NETFLAG_LENGTH_MASK)) return false; //size was wrong, couldn't have been ours. if (header & NETFLAG_CTL) return false; //huh? sequence = SwapLong(ReadLong(msg)); if (header & NETFLAG_ACK) { if (sequence == chan->reliable_sequence) { chan->reliable_start += MAX_NQDATAGRAM; if (chan->reliable_start >= chan->reliable_length) { chan->reliable_length = 0; //they got the entire message chan->reliable_start = 0; } chan->incoming_reliable_acknowledged = chan->reliable_sequence; chan->reliable_sequence++; chan->last_received = curtime; } // else if (sequence < chan->reliable_sequence) // Con_DPrintf("Stale ack recieved\n"); // else if (sequence > chan->reliable_sequence) // Con_Printf("Future ack recieved\n"); return false; //don't try execing the 'payload'. I hate ack packets. } if (header & NETFLAG_UNRELIABLE) { if (sequence < chan->incoming_unreliable) { // Con_DPrintf("Stale datagram recieved\n"); return false; } drop = sequence - chan->incoming_unreliable - 1; if (drop > 0) { // Con_DPrintf("Dropped %i datagrams\n", drop); // chan->drop_count += drop; } chan->incoming_unreliable = sequence; chan->last_received = curtime; chan->incoming_acknowledged++; // chan->good_count++; return 1; } if (header & NETFLAG_DATA) { int runt[2]; //always reply. a stale sequence probably means our ack got lost. runt[0] = BigLong(NETFLAG_ACK | 8); runt[1] = BigLong(sequence); NET_SendPacket (cluster, chan->sock, 8, (void*)runt, chan->remote_address); chan->last_received = curtime; if (sequence == chan->incoming_reliable_sequence) { chan->incoming_reliable_sequence++; if (chan->in_fragment_length + msg->cursize-8 >= sizeof(chan->in_fragment_buf)) { chan->drop = true; return false; } memcpy(chan->in_fragment_buf + chan->in_fragment_length, (char*)msg->data+8, msg->cursize-8); chan->in_fragment_length += msg->cursize-8; if (header & NETFLAG_EOM) { msg->cursize = 0; WriteData(msg, chan->in_fragment_buf, chan->in_fragment_length); chan->in_fragment_length = 0; msg->readpos = 0; return 2; //we can read it now } } // else // Con_DPrintf("Stale reliable (%i)\n", sequence); return false; } return false; //not supported. } /* ================= Netchan_Process called when the current net_message is from remote_address modifies net_message so that it points to the packet payload ================= */ qboolean Netchan_Process (netchan_t *chan, netmsg_t *msg) { unsigned sequence, sequence_ack; unsigned reliable_ack, reliable_message; // get sequence numbers msg->readpos = 0; sequence = ReadLong (msg); sequence_ack = ReadLong (msg); // read the qport if we are a server if (!chan->isclient) if (chan->qport != ReadShort (msg)) return false; reliable_message = sequence >> 31; reliable_ack = sequence_ack >> 31; sequence &= ~(1<<31); sequence_ack &= ~(1<<31); /* if (showpackets.value) Com_Printf ("<-- s=%i(%i) a=%i(%i) %i\n" , sequence , reliable_message , sequence_ack , reliable_ack , net_message.cursize); */ // // discard stale or duplicated packets // if (sequence <= (unsigned)chan->incoming_sequence) { /* if (showdrop.value) Com_Printf ("%s:Out of order packet %i at %i\n" , NET_AdrToString (chan->remote_address) , sequence , chan->incoming_sequence); */ return false; } // // dropped packets don't keep the message from being used // /* chan->dropped = sequence - (chan->incoming_sequence+1); if (chan->dropped > 0) { chan->drop_count += 1; if (showdrop.value) Com_Printf ("%s:Dropped %i packets at %i\n" , NET_AdrToString (chan->remote_address) , chan->dropped , sequence); } */ // // if the current outgoing reliable message has been acknowledged // clear the buffer to make way for the next // if (reliable_ack == (unsigned)chan->reliable_sequence) chan->reliable_length = 0; // it has been received // // if this message contains a reliable message, bump incoming_reliable_sequence // chan->incoming_sequence = sequence; chan->incoming_acknowledged = sequence_ack; chan->incoming_reliable_acknowledged = reliable_ack; if (reliable_message) chan->incoming_reliable_sequence ^= 1; // // the message can now be read from the current message pointer // update statistics counters // // chan->frame_latency = chan->frame_latency*OLD_AVG // + (chan->outgoing_sequence-sequence_ack)*(1.0-OLD_AVG); // chan->frame_rate = chan->frame_rate*OLD_AVG // + (curtime - chan->last_received)*(1.0-OLD_AVG); // chan->good_count += 1; chan->last_received = curtime; return true; } ================================================ FILE: fteqtv/nq_api.c ================================================ /* This file is intended as a set of exports for an NQ-based engine. This is supported _purely_ for clients, and will not work for servers. documentation can be found inside net_qtv.h */ #include "qtv.h" int build_number(void); static cluster_t *cluster; //note that a qsockaddr is only 16 bytes. //this is not enough for ipv6 etc. struct qsockaddr { int ipid; }; static char resolvedadrstring[128]; static int lastadrid; #ifdef _WIN32 #define EXPORT __declspec(dllexport) #endif #include "net_qtv.h" PUBEXPORT void PUBLIC QTV_Command (char *command, char *results, int resultlen) { if (!cluster) return; Rcon_Command(cluster, NULL, command, results, resultlen, true); } PUBEXPORT void PUBLIC QTV_RunFrame (void) { if (!cluster) return; Cluster_Run(cluster, false); } EXPORT int PUBLIC QTV_Init (void) { if (cluster) return 0; cluster = malloc(sizeof(*cluster)); if (cluster) { memset(cluster, 0, sizeof(*cluster)); cluster->qwdsocket[0] = INVALID_SOCKET; cluster->qwdsocket[1] = INVALID_SOCKET; cluster->tcpsocket[0] = INVALID_SOCKET; cluster->tcpsocket[1] = INVALID_SOCKET; cluster->anticheattime = 1*1000; cluster->tooslowdelay = 100; cluster->qwlistenportnum = 0; cluster->allownqclients = true; strcpy(cluster->hostname, DEFAULT_HOSTNAME); cluster->buildnumber = build_number(); cluster->maxproxies = -1; strcpy(cluster->demodir, "qw/demos/"); return 0; } return -1; } EXPORT void PUBLIC QTV_Shutdown (void) { } EXPORT void PUBLIC QTV_Listen (qboolean state) { } EXPORT int PUBLIC QTV_OpenSocket (int port) { return 0; } EXPORT int PUBLIC QTV_CloseSocket (int socket) { //give it a chance to close any server connections from us disconnecting (should have already send disconnect message, but won't have run the server so not noticed the lack of viewers) Cluster_Run(cluster, false); return 0; } EXPORT int PUBLIC QTV_Connect (int socket, struct qsockaddr *addr) { if (addr->ipid == lastadrid) { strlcpy(cluster->autojoinadr, resolvedadrstring, sizeof(cluster->autojoinadr)); return 0; } else { cluster->autojoinadr[0] = 0; return -1; } return 0; } EXPORT int PUBLIC QTV_CheckNewConnections (void) { return -1; } static byte pendingbuf[8][1032]; static int pendinglen[8]; static unsigned int pendingin, pendingout; void QTV_DoReceive(void *data, int length) { int idx; if (length > sizeof(pendingbuf[0])) return; idx = pendingout++; idx &= 7; memcpy(pendingbuf[idx], data, length); pendinglen[idx] = length; } EXPORT int PUBLIC QTV_Read (int socket, byte *buf, int len, struct qsockaddr *addr) { if (pendingout == pendingin) { Cluster_Run(cluster, false); Cluster_Run(cluster, false); } while (pendingin != pendingout) { int idx = pendingin++; idx &= 7; if (pendinglen[idx] > len) continue; //error memcpy(buf, pendingbuf[idx], pendinglen[idx]); return pendinglen[idx]; } return 0; } EXPORT int PUBLIC QTV_Write (int socket, byte *buf, int len, struct qsockaddr *addr) { netmsg_t m; netadr_t from; from.tcpcon = NULL; ((struct sockaddr*)from.sockaddr)->sa_family = AF_UNSPEC; m.cursize = len; m.data = buf; m.readpos = 0; QW_ProcessUDPPacket(cluster, &m, from); if (pendingout == pendingin) Cluster_Run(cluster, false); return 0; } EXPORT int PUBLIC QTV_Broadcast (int socket, byte *buf, int len) { netmsg_t m; netadr_t from; from.tcpcon = NULL; ((struct sockaddr*)from.sockaddr)->sa_family = AF_UNSPEC; m.cursize = len; m.data = buf; m.readpos = 0; QW_ProcessUDPPacket(cluster, &m, from); return 0; } EXPORT char *PUBLIC QTV_AddrToString (struct qsockaddr *addr) { return 0; } EXPORT int PUBLIC QTV_StringPortToAddr (char *string, int port, struct qsockaddr *addr) { if (!strncmp(string, "file:", 5) || !strncmp(string, "demo:", 5)) { snprintf(resolvedadrstring, sizeof(resolvedadrstring), "%s", string); addr->ipid = ++lastadrid; return 0; } if (!strncmp(string, "qw:", 3) || !strncmp(string, "udp:", 4) || !strncmp(string, "tcp:", 4)) { if (port) snprintf(resolvedadrstring, sizeof(resolvedadrstring), "%s:%i", string, port); else snprintf(resolvedadrstring, sizeof(resolvedadrstring), "%s", string); addr->ipid = ++lastadrid; return 0; } return -1; } EXPORT int PUBLIC QTV_StringToAddr (char *string, struct qsockaddr *addr) { return QTV_StringPortToAddr(string, 0, addr); } EXPORT int PUBLIC QTV_GetSocketAddr (int socket, struct qsockaddr *addr) { return 0; } EXPORT int PUBLIC QTV_GetNameFromAddr (struct qsockaddr *addr, char *name) { return 0; } EXPORT int PUBLIC QTV_GetAddrFromName (char *name, struct qsockaddr *addr) { return QTV_StringToAddr(name, addr); } EXPORT int PUBLIC QTV_AddrCompare (struct qsockaddr *addr1, struct qsockaddr *addr2) { return 0; } EXPORT int PUBLIC QTV_GetSocketPort (struct qsockaddr *addr) { return 0; } EXPORT int PUBLIC QTV_SetSocketPort (struct qsockaddr *addr, int port) { return 0; } ================================================ FILE: fteqtv/parse.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ #include "qtv.h" #include "bsd_string.h" #define ParseError(m) (m)->readpos = (m)->cursize+1 // static const entity_state_t null_entity_state; void SendBufferToViewer(viewer_t *v, const char *buffer, int length, qboolean reliable) { if (reliable) { //try and put it in the normal reliable if (!v->backbuffered && v->netchan.message.cursize+length < v->netchan.message.maxsize) WriteData(&v->netchan.message, buffer, length); else if (v->backbuffered>0 && v->backbuf[v->backbuffered-1].cursize+length < v->backbuf[v->backbuffered-1].maxsize) //try and put it in the current backbuffer WriteData(&v->backbuf[v->backbuffered-1], buffer, length); else if (v->backbuffered == MAX_BACK_BUFFERS) { v->netchan.message.cursize = 0; WriteByte(&v->netchan.message, svc_print); if (!v->netchan.isnqprotocol) WriteByte(&v->netchan.message, PRINT_HIGH); WriteString(&v->netchan.message, "backbuffer overflow\n"); if (!v->drop) Sys_Printf(NULL, "%s backbuffers overflowed\n", v->name); //FIXME v->drop = true; //we would need too many backbuffers. } else { //create a new backbuffer if (!v->backbuf[v->backbuffered].data) { InitNetMsg(&v->backbuf[v->backbuffered], (unsigned char *)malloc(MAX_BACKBUF_SIZE), MAX_BACKBUF_SIZE); } v->backbuf[v->backbuffered].cursize = 0; //make sure it's empty WriteData(&v->backbuf[v->backbuffered], buffer, length); v->backbuffered++; } } } void Multicast(sv_t *tv, void *buffer, int length, int to, unsigned int playermask, int suitablefor) { viewer_t *v; switch(to) { case dem_multiple: case dem_single: case dem_stats: //check and send to them only if they're tracking this player(s). for (v = tv->cluster->viewers; v; v = v->next) { if (v->thinksitsconnected||suitablefor&CONNECTING) if (v->server == tv) if (v->trackplayer>=0) if ((1<trackplayer)&playermask) { if (suitablefor&(v->netchan.isnqprotocol?NQ:QW)) SendBufferToViewer(v, buffer, length, true); //FIXME: change the reliable depending on message type } } break; default: //send to all for (v = tv->cluster->viewers; v; v = v->next) { if (v->thinksitsconnected||suitablefor&CONNECTING) if (v->server == tv) if (suitablefor&(v->netchan.isnqprotocol?NQ:QW)) SendBufferToViewer(v, buffer, length, true); //FIXME: change the reliable depending on message type } break; } } void Broadcast(cluster_t *cluster, void *buffer, int length, int suitablefor) { viewer_t *v; for (v = cluster->viewers; v; v = v->next) { if (suitablefor&(v->netchan.isnqprotocol?NQ:QW)) SendBufferToViewer(v, buffer, length, true); } } void ConnectionData(sv_t *tv, void *buffer, int length, int to, unsigned int playermask, int suitablefor) { if (!tv->parsingconnectiondata) Multicast(tv, buffer, length, to, playermask, suitablefor); else if (tv->controller) { if (suitablefor&(tv->controller->netchan.isnqprotocol?NQ:QW)) SendBufferToViewer(tv->controller, buffer, length, true); } } static void ParseServerData(sv_t *tv, netmsg_t *m, int to, unsigned int playermask) { unsigned int protocol; unsigned int supported; viewer_t *v; //free the old map state QTV_CleanupMap(tv); tv->pext1 = 0; tv->pext2 = 0; tv->pexte = 0; //when it comes to QTV, the proxy 'blindly' forwards the data after parsing the header, so we need to support EVERYTHING the original server might. //and if we don't, then we might have troubles. for(;;) { protocol = ReadLong(m); switch (protocol) { case PROTOCOL_VERSION: break; case PROTOCOL_VERSION_FTE: protocol = ReadLong(m); tv->pext1 = protocol; //HAVE supported = PEXT_SETVIEW|PEXT_ACCURATETIMINGS; /*simple forwarding*/ supported |= PEXT_256PACKETENTITIES|PEXT_VIEW2|PEXT_HLBSP|PEXT_Q2BSP|PEXT_Q3BSP; //features other than the protocol (stats, simple limits etc) supported |= PEXT_FLOATCOORDS|PEXT_SPAWNSTATIC2; //working // supported |= PEXT_CHUNKEDDOWNLOADS; //shouldn't be relevant... supported |= PEXT_TRANS|PEXT_MODELDBL|PEXT_ENTITYDBL|PEXT_ENTITYDBL2|PEXT_SOUNDDBL; //replaced by replacementdeltas. supported |= PEXT_SCALE|PEXT_TRANS|PEXT_FATNESS|PEXT_COLOURMOD|PEXT_HEXEN2|PEXT_SETATTACHMENT|PEXT_DPFLAGS; //stuff that we ought to handle, but don't currently //PEXT_LIGHTSTYLECOL - woo, fancy rgb colours //PEXT_CUSTOMTEMPEFFECTS - required for hexen2's effects. kinda messy. //PEXT_TE_BULLET - implies nq tents too. //HARD... //PEXT_CSQC -- all bets are off if we receive a csqc ent update //totally optional... so will probably never be added... //PEXT_HULLSIZE - bigger players... maybe. like anyone can depend on this... not supported with mvd players so w/e //PEXT_CHUNKEDDOWNLOADS - not sure there's much point //PEXT_SPLITSCREEN - irrelevant for mvds. might be useful as a qw client, but who cares. not enough servers have it active. //PEXT_SHOWPIC - rare, lame, limited. just yuck. if (protocol & ~supported) { int i; const char *names[] = { "PEXT_SETVIEW", "PEXT_SCALE", "PEXT_LIGHTSTYLECOL", "PEXT_TRANS", "PEXT_VIEW2", "0x00000020", "PEXT_ACCURATETIMINGS", "PEXT_SOUNDDBL", "PEXT_FATNESS", "PEXT_HLBSP", "PEXT_TE_BULLET", "PEXT_HULLSIZE", "PEXT_MODELDBL", "PEXT_ENTITYDBL", "PEXT_ENTITYDBL2", "PEXT_FLOATCOORDS", "0x00010000", "PEXT_Q2BSP", "PEXT_Q3BSP", "PEXT_COLOURMOD", "PEXT_SPLITSCREEN", "PEXT_HEXEN2", "PEXT_SPAWNSTATIC2", "PEXT_CUSTOMTEMPEFFECTS", "PEXT_256PACKETENTITIES", "0x02000000", "PEXT_SHOWPIC", "PEXT_SETATTACHMENT", "0x10000000", "PEXT_CHUNKEDDOWNLOADS","PEXT_CSQC", "PEXT_DPFLAGS", }; for (i = 0; i < sizeof(names)/sizeof(names[0]); i++) { if (protocol & ~supported & (1u<cluster, "ParseMessage: PROTOCOL_VERSION_FTE (%s) not supported\n", names[i]); supported |= (1u<cluster, "ParseMessage: PROTOCOL_VERSION_FTE (%x) not supported\n", protocol & ~supported); } continue; case PROTOCOL_VERSION_FTE2: protocol = ReadLong(m); tv->pext2 = protocol; supported = 0; // supported |= PEXT2_PRYDONCURSOR|PEXT2_VOICECHAT|PEXT2_SETANGLEDELTA|PEXT2_REPLACEMENTDELTAS|PEXT2_MAXPLAYERS; //FIXME: handle the svc and clc if they arrive. supported |= PEXT2_VOICECHAT; //WANT //PEXT2_SETANGLEDELTA //PEXT2_REPLACEMENTDELTAS //PEXT2_SETANGLEDELTA //PEXT2_PREDINFO //PEXT2_PRYDONCURSOR if (protocol & ~supported) Sys_Printf(tv->cluster, "ParseMessage: PROTOCOL_VERSION_FTE2 (%x) not supported\n", protocol & ~supported); continue; case PROTOCOL_VERSION_EZQUAKE1: tv->pexte = protocol = ReadLong(m); supported = PEXTE_HIDDENMESSAGES; if (protocol & ~supported) Sys_Printf(tv->cluster, "ParseMessage: Unsupported MVD1 protocol flags %#x\n", protocol); continue; case PROTOCOL_VERSION_HUFFMAN: Sys_Printf(tv->cluster, "ParseMessage: PROTOCOL_VERSION_HUFFMAN not supported\n"); ParseError(m); return; case PROTOCOL_VERSION_VARLENGTH: { int len = ReadLong(m); if (len < 0 || len > 8192) { Sys_Printf(tv->cluster, "ParseMessage: PROTOCOL_VERSION_VARLENGTH invalid\n"); ParseError(m); return; } protocol = ReadLong(m);/*ident*/ switch(protocol) { default: m->readpos += len; Sys_Printf(tv->cluster, "ParseMessage: PROTOCOL_VERSION_VARLENGTH (%x) not supported\n", protocol); ParseError(m); return; } } continue; case PROTOCOL_VERSION_FRAGMENT: protocol = ReadLong(m); Sys_Printf(tv->cluster, "ParseMessage: PROTOCOL_VERSION_FRAGMENT not supported\n"); ParseError(m); return; default: Sys_Printf(tv->cluster, "ParseMessage: Unknown protocol version %#x\n", protocol); ParseError(m); return; } break; } tv->mapstarttime = tv->parsetime; tv->parsingconnectiondata = true; tv->clservercount = ReadLong(m); //we don't care about server's servercount, it's all reliable data anyway. tv->map.trackplayer = -1; ReadString(m, tv->map.gamedir, sizeof(tv->map.gamedir)); #define DEFAULTGAMEDIR "qw" if (strchr(tv->map.gamedir, ':')) //nuke any multiple gamedirs - we need to read maps which would fail if its not a valid single path. *strchr(tv->map.gamedir, ';') = 0; if (!*tv->map.gamedir) strcpy(tv->map.gamedir, DEFAULTGAMEDIR); if (!*tv->map.gamedir || *tv->map.gamedir == '.' || !strcmp(tv->map.gamedir, ".") || strstr(tv->map.gamedir, "..") || strstr(tv->map.gamedir, "/") || strstr(tv->map.gamedir, "\\") || strstr(tv->map.gamedir, ":") ) { QTV_Printf(tv, "Ignoring unsafe gamedir: \"%s\"\n", tv->map.gamedir); strcpy(tv->map.gamedir, DEFAULTGAMEDIR); } if (tv->usequakeworldprotocols) tv->map.thisplayer = ReadByte(m)&~128; else { tv->map.thisplayer = MAX_CLIENTS-1; /*tv->servertime =*/ ReadFloat(m); } if (tv->controller) tv->controller->thisplayer = tv->map.thisplayer; ReadString(m, tv->map.mapname, sizeof(tv->map.mapname)); QTV_Printf(tv, "Gamedir: %s\n", tv->map.gamedir); QTV_Printf(tv, "---------------------\n"); Sys_Printf(tv->cluster, "Stream %i: %s\n", tv->streamid, tv->map.mapname); QTV_Printf(tv, "---------------------\n"); // get the movevars tv->map.movevars.gravity = ReadFloat(m); tv->map.movevars.stopspeed = ReadFloat(m); tv->map.movevars.maxspeed = ReadFloat(m); tv->map.movevars.spectatormaxspeed = ReadFloat(m); tv->map.movevars.accelerate = ReadFloat(m); tv->map.movevars.airaccelerate = ReadFloat(m); tv->map.movevars.wateraccelerate = ReadFloat(m); tv->map.movevars.friction = ReadFloat(m); tv->map.movevars.waterfriction = ReadFloat(m); tv->map.movevars.entgrav = ReadFloat(m); for (v = tv->cluster->viewers; v; v = v->next) { if (v->server == tv) v->thinksitsconnected = false; } if ((!tv->controller || tv->controller->netchan.isnqprotocol) && tv->usequakeworldprotocols) { tv->netchan.message.cursize = 0; //mvdsv sucks SendClientCommand(tv, "soundlist %i 0\n", tv->clservercount); } else ConnectionData(tv, (void*)((char*)m->data+m->startpos), m->readpos - m->startpos, to, dem_read, QW); if (tv->controller) { QW_ClearViewerState(tv->controller); tv->controller->trackplayer = tv->map.thisplayer; } strcpy(tv->status, "Receiving soundlist\n"); } /*called if the server changed the map.serverinfo, so we can corrupt it again*/ void QTV_UpdatedServerInfo(sv_t *tv) { qboolean fromproxy; char text[1024]; char value[256]; Info_ValueForKey(tv->map.serverinfo, "*qtv", value, sizeof(value)); if (*value) { fromproxy = true; tv->serverisproxy = fromproxy; } else fromproxy = false; //add on our extra infos Info_SetValueForStarKey(tv->map.serverinfo, "*qtv", QTV_VERSION_STRING, sizeof(tv->map.serverinfo)); Info_SetValueForStarKey(tv->map.serverinfo, "*z_ext", Z_EXT_STRING, sizeof(tv->map.serverinfo)); Info_ValueForKey(tv->map.serverinfo, "hostname", tv->map.hostname, sizeof(tv->map.hostname)); //change the hostname (the qtv's hostname with the server's hostname in brackets) Info_ValueForKey(tv->map.serverinfo, "hostname", value, sizeof(value)); if (fromproxy && strchr(value, '(') && value[strlen(value)-1] == ')') //already has brackets { //the fromproxy check is because it's fairly common to find a qw server with brackets after it's name. char *s; s = strchr(value, '('); //so strip the parent proxy's hostname, and put our hostname first, leaving the origional server's hostname within the brackets snprintf(text, sizeof(text), "%s %s", tv->cluster->hostname, s); } else { if (tv->sourcefile) snprintf(text, sizeof(text), "%s (recorded from: %s)", tv->cluster->hostname, value); else snprintf(text, sizeof(text), "%s (live: %s)", tv->cluster->hostname, value); } Info_SetValueForStarKey(tv->map.serverinfo, "hostname", text, sizeof(tv->map.serverinfo)); } static void ParseCDTrack(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { char nqversion[3]; tv->map.cdtrack = ReadByte(m); ConnectionData(tv, (void*)((char*)m->data+m->startpos), m->readpos - m->startpos, to, mask, QW); nqversion[0] = svc_cdtrack; nqversion[1] = tv->map.cdtrack; nqversion[2] = tv->map.cdtrack; ConnectionData(tv, nqversion, 3, to, mask, NQ); } static void ParseStufftext(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { viewer_t *v; char text[1024]; ReadString(m, text, sizeof(text)); // Sys_Printf(tv->cluster, "stuffcmd: %s", text); if (!strcmp(text, "say proxy:menu\n")) { //qizmo's 'previous proxy' message tv->proxyisselected = true; if (tv->controller) QW_SetMenu(tv->controller, MENU_MAIN); tv->serverisproxy = true; //FIXME: Detect this properly on qizmo } else if (!strncmp(text, "//I am a proxy", 14)) tv->serverisproxy = true; else if (!strncmp(text, "//set prox_inmenu ", 18)) { if (tv->controller) QW_SetMenu(tv->controller, atoi(text+18)?MENU_FORWARDING:MENU_NONE); } // else if (!strncmp(text, "//set protocolname ", 19)) // else if (!strncmp(text, "//set recorddate ", 17)) //reports when the demo was originally recorded, without needing to depend upon metadata. // else if (!strncmp(text, "//paknames ", 11)) // else if (!strncmp(text, "//paks ", 7)) // else if (!strncmp(text, "//vwep ", 7)) else if (strstr(text, "screenshot")) { if (tv->controller) { //let it through to the controller SendBufferToViewer(tv->controller, (char*)m->data+m->startpos, m->readpos - m->startpos, true); } return; //this was generating far too many screenshots when watching demos } else if (!strcmp(text, "skins\n")) { const char newcmd[10] = {svc_stufftext, 'c', 'm', 'd', ' ', 'n','e','w','\n','\0'}; tv->parsingconnectiondata = false; strcpy(tv->status, "On server\n"); for (v = tv->cluster->viewers; v; v = v->next) { if (v->server == tv && (v != tv->controller || v->netchan.isnqprotocol)) { v->servercount++; SendBufferToViewer(v, newcmd, sizeof(newcmd), true); } } if (tv->controller && !tv->controller->netchan.isnqprotocol) SendBufferToViewer(tv->controller, (char*)m->data+m->startpos, m->readpos - m->startpos, true); else if (tv->usequakeworldprotocols) SendClientCommand(tv, "begin %i\n", tv->clservercount); return; } else if (!strncmp(text, "fullserverinfo ", 15)) { /*strip newline*/ text[strlen(text)-1] = '\0'; /*strip trailing quote*/ text[strlen(text)-1] = '\0'; //copy over the server's serverinfo strlcpy(tv->map.serverinfo, text+16, sizeof(tv->map.serverinfo)); QTV_UpdatedServerInfo(tv); if (tv->controller && (tv->controller->netchan.isnqprotocol == false)) SendBufferToViewer(tv->controller, (char*)m->data+m->startpos, m->readpos - m->startpos, true); return; } else if (!strncmp(text, "cmd prespawn ", 13)) { if (tv->usequakeworldprotocols) SendClientCommand(tv, "%s", text+4); return; //commands the game server asked for are pointless. } else if (!strncmp(text, "cmd spawn ", 10)) { if (tv->usequakeworldprotocols) SendClientCommand(tv, "%s", text+4); return; //commands the game server asked for are pointless. } else if (!strncmp(text, "cmd ", 4)) { if (tv->controller) SendBufferToViewer(tv->controller, (char*)m->data+m->startpos, m->readpos - m->startpos, true); else if (tv->usequakeworldprotocols) SendClientCommand(tv, "%s", text+4); return; //commands the game server asked for are pointless. } else if (!strncmp(text, "reconnect", 9)) { if (tv->controller) SendBufferToViewer(tv->controller, (char*)m->data+m->startpos, m->readpos - m->startpos, true); else if (tv->usequakeworldprotocols) SendClientCommand(tv, "new\n"); return; } else if (!strncmp(text, "packet ", 7)) { if (tv->controller) { //if we're acting as a proxy, forward the realip packets, and ONLY to the controller //quakeworld proxies are usually there for routing or protocol advantages, NOT privacy //(client can always ignore it themselves, but a server might ban you, but at least they'll be less inclined to ban the proxy). SendBufferToViewer(tv->controller, (char*)m->data+m->startpos, m->readpos - m->startpos, true); return; } if(tv->usequakeworldprotocols) {//eeeevil hack for proxy-spectating char *ptr; char arg[3][ARG_LEN]; netadr_t adr; ptr = text; ptr = COM_ParseToken(ptr, arg[0], ARG_LEN, ""); ptr = COM_ParseToken(ptr, arg[1], ARG_LEN, ""); ptr = COM_ParseToken(ptr, arg[2], ARG_LEN, ""); NET_StringToAddr(arg[1], &adr, PROX_DEFAULTSERVERPORT); Netchan_OutOfBandSocket(tv->cluster, tv->sourcesock, &adr, strlen(arg[2]), arg[2]); //this is an evil hack SendClientCommand(tv, "new\n"); return; } Sys_Printf(tv->cluster, "packet stuffcmd in an mvd\n"); //shouldn't ever happen, try ignoring it. return; } else if (tv->usequakeworldprotocols && !strncmp(text, "setinfo ", 8)) { Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, Q1); if (!tv->controller) SendClientCommand(tv, "%s", text); } else { Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, Q1); return; } } static void ParseSetInfo(sv_t *tv, netmsg_t *m) { int pnum; char key[64]; char value[256]; pnum = ReadByte(m); ReadString(m, key, sizeof(key)); ReadString(m, value, sizeof(value)); if (pnum < MAX_CLIENTS) Info_SetValueForStarKey(tv->map.players[pnum].userinfo, key, value, sizeof(tv->map.players[pnum].userinfo)); ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, dem_all, (unsigned)-1, QW); } static void ParseServerinfo(sv_t *tv, netmsg_t *m) { char key[64]; char value[256]; ReadString(m, key, sizeof(key)); ReadString(m, value, sizeof(value)); if (strcmp(key, "hostname")) //don't allow the hostname to change, but allow the server to change other serverinfos. Info_SetValueForStarKey(tv->map.serverinfo, key, value, sizeof(tv->map.serverinfo)); ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, dem_all, (unsigned)-1, QW); } static void ParsePrint(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { char text[1024]; char nqbuffer[1024]; int level; level = ReadByte(m); ReadString(m, text, sizeof(text)-2); if (level == 3) { //FIXME: that number shouldn't be hard-coded if (!strncmp(text, "#0:qtv_say:#", 12) || !strncmp(text, "#0:qtv_say_game:#", 17) || !strncmp(text, "#0:qtv_say_team_game:#", 22)) { char *colon; colon = strchr(text, ':'); colon = strchr(colon+1, ':'); colon = strchr(colon+1, ':'); if (colon) { //de-fuck qqshka's extra gibberish. snprintf(nqbuffer, sizeof(nqbuffer), "%c%c[QTV]%s\n", svc_print, 3, colon+1); Multicast(tv, nqbuffer, strlen(nqbuffer)+1, to, mask, QW|CONNECTING); snprintf(nqbuffer, sizeof(nqbuffer), "%c%c[QTV]%s\n", svc_print, 1, colon+1); Multicast(tv, nqbuffer, strlen(nqbuffer)+1, to, mask, NQ|CONNECTING); return; } } strlcpy(nqbuffer+2, text, sizeof(nqbuffer)-2); nqbuffer[1] = 1; //nq chat is prefixed with a 1 } else { strlcpy(nqbuffer+1, text, sizeof(nqbuffer)-1); } nqbuffer[0] = svc_print; if ((to&dem_mask) == dem_all || to == dem_read) { if (level > 1) { QTV_Printf(tv, "%s", text); } } Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW|CONNECTING); Multicast(tv, nqbuffer, strlen(nqbuffer)+1, to, mask, NQ|CONNECTING); } static void ParseCenterprint(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { viewer_t *v; char text[1024]; ReadString(m, text, sizeof(text)); switch(to) { case dem_multiple: case dem_single: case dem_stats: //check and send to them only if they're tracking this player(s). for (v = tv->cluster->viewers; v; v = v->next) { if (!v->menunum || v->menunum == MENU_FORWARDING) if (v->thinksitsconnected) if (v->server == tv) if (v->trackplayer>=0) if ((1<trackplayer)&mask) { SendBufferToViewer(v, (char*)m->data+m->startpos, m->readpos - m->startpos, true); //FIXME: change the reliable depending on message type } } break; default: //send to all for (v = tv->cluster->viewers; v; v = v->next) { if (!v->menunum || v->menunum == MENU_FORWARDING) if (v->thinksitsconnected) if (v->server == tv) SendBufferToViewer(v, (char*)m->data+m->startpos, m->readpos - m->startpos, true); //FIXME: change the reliable depending on message type } break; } } static int ParseList(sv_t *tv, netmsg_t *m, filename_t *list, int to, unsigned int mask, qboolean big) { int first; if (big) first = ReadShort(m)+1; else first = ReadByte(m)+1; for (; first < MAX_LIST; first++) { ReadString(m, list[first].name, sizeof(list[first].name)); // printf("read %i: %s\n", first, list[first].name); if (!*list[first].name) break; // printf("%i: %s\n", first, list[first].name); } return ReadByte(m); } static void ParseEntityState(sv_t *tv, entity_state_t *es, netmsg_t *m) //for baselines/static entities { int i; es->modelindex = ReadByte(m); es->frame = ReadByte(m); es->colormap = ReadByte(m); es->skinnum = ReadByte(m); for (i = 0; i < 3; i++) { es->origin[i] = ReadCoord(m, tv->pext1); es->angles[i] = ReadAngle(m, tv->pext1); } } static void ParseStaticSound(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { if (tv->map.staticsound_count == MAX_STATICSOUNDS) { tv->map.staticsound_count--; // don't be fatal. Sys_Printf(tv->cluster, "Too many static sounds\n"); } tv->map.staticsound[tv->map.staticsound_count].origin[0] = ReadCoord(m, tv->pext1); tv->map.staticsound[tv->map.staticsound_count].origin[1] = ReadCoord(m, tv->pext1); tv->map.staticsound[tv->map.staticsound_count].origin[2] = ReadCoord(m, tv->pext1); tv->map.staticsound[tv->map.staticsound_count].soundindex = ReadByte(m); tv->map.staticsound[tv->map.staticsound_count].volume = ReadByte(m); tv->map.staticsound[tv->map.staticsound_count].attenuation = ReadByte(m); tv->map.staticsound_count++; ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, Q1); } static void ParseIntermission(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { ReadShort(m); ReadShort(m); ReadShort(m); ReadByte(m); ReadByte(m); ReadByte(m); Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } extern const usercmd_t nullcmd; static void ParsePlayerInfo(sv_t *tv, netmsg_t *m, qboolean clearoldplayers) { usercmd_t nonnullcmd; int flags; int num; int i; if (clearoldplayers) { for (i = 0; i < MAX_CLIENTS; i++) { //hide players //they'll be sent after this packet. tv->map.players[i].oldactive = tv->map.players[i].active; tv->map.players[i].active = false; } } num = ReadByte(m); if (num >= MAX_CLIENTS) { num = 0; // don't be fatal. Sys_Printf(tv->cluster, "Too many svc_playerinfos, wrapping\n"); } tv->map.players[num].old = tv->map.players[num].current; if (tv->usequakeworldprotocols) { flags = (unsigned short)ReadShort (m); tv->map.players[num].current.origin[0] = ReadCoord (m, tv->pext1); tv->map.players[num].current.origin[1] = ReadCoord (m, tv->pext1); tv->map.players[num].current.origin[2] = ReadCoord (m, tv->pext1); tv->map.players[num].current.frame = ReadByte(m); if (flags & PF_MSEC) ReadByte (m); if (flags & PF_COMMAND) { ReadDeltaUsercmd(m, &nullcmd, &nonnullcmd); tv->map.players[num].current.angles[0] = nonnullcmd.angles[0]; tv->map.players[num].current.angles[1] = nonnullcmd.angles[1]; tv->map.players[num].current.angles[2] = nonnullcmd.angles[2]; } else { //the only reason we'd not get a command is if it's us. if (tv->controller) { tv->map.players[num].current.angles[0] = tv->controller->ucmds[2].angles[0]; tv->map.players[num].current.angles[1] = tv->controller->ucmds[2].angles[1]; tv->map.players[num].current.angles[2] = tv->controller->ucmds[2].angles[2]; } else { tv->map.players[num].current.angles[0] = tv->proxyplayerangles[0]; tv->map.players[num].current.angles[1] = tv->proxyplayerangles[1]; tv->map.players[num].current.angles[2] = tv->proxyplayerangles[2]; } } for (i=0 ; i<3 ; i++) { if (flags & (PF_VELOCITY1<map.players[num].current.velocity[i] = ReadShort(m); else tv->map.players[num].current.velocity[i] = 0; } tv->map.players[num].gibbed = !!(flags & PF_GIB); tv->map.players[num].dead = !!(flags & PF_DEAD); if (flags & PF_MODEL) tv->map.players[num].current.modelindex = ReadByte (m); else tv->map.players[num].current.modelindex = tv->map.modelindex_player; if (flags & PF_SKINNUM) tv->map.players[num].current.skinnum = ReadByte (m); else tv->map.players[num].current.skinnum = 0; if (flags & PF_EFFECTS) tv->map.players[num].current.effects = ReadByte (m); else tv->map.players[num].current.effects = 0; if (flags & PF_WEAPONFRAME) tv->map.players[num].current.weaponframe = ReadByte (m); else tv->map.players[num].current.weaponframe = 0; tv->map.players[num].active = true; } else { flags = ReadShort(m); tv->map.players[num].gibbed = !!(flags & DF_GIB); tv->map.players[num].dead = !!(flags & DF_DEAD); tv->map.players[num].current.frame = ReadByte(m); for (i = 0; i < 3; i++) { if (flags & (DF_ORIGIN << i)) tv->map.players[num].current.origin[i] = ReadCoord (m, tv->pext1); } for (i = 0; i < 3; i++) { if (flags & (DF_ANGLES << i)) { tv->map.players[num].current.angles[i] = (ReadShort(m)/(float)0x10000)*360; } } if (flags & DF_MODEL) tv->map.players[num].current.modelindex = ReadByte (m); if (flags & DF_SKINNUM) tv->map.players[num].current.skinnum = ReadByte (m); if (flags & DF_EFFECTS) tv->map.players[num].current.effects = ReadByte (m); if (flags & DF_WEAPONFRAME) tv->map.players[num].current.weaponframe = ReadByte (m); tv->map.players[num].active = true; } tv->map.players[num].leafcount = BSP_SphereLeafNums(tv->map.bsp, MAX_ENTITY_LEAFS, tv->map.players[num].leafs, tv->map.players[num].current.origin[0], tv->map.players[num].current.origin[1], tv->map.players[num].current.origin[2], 32); } static int readentitynum(netmsg_t *m, unsigned int *retflags) { int entnum; unsigned int flags; flags = ReadShort(m); if (!flags) { *retflags = 0; return 0; } entnum = flags&511; flags &= ~511; if (flags & U_MOREBITS) { flags |= ReadByte(m); if (flags & UX_EVENMORE) flags |= ReadByte(m)<<16; if (flags & UX_YETMORE) flags |= ReadByte(m)<<24; } if (flags & UX_ENTITYDBL) entnum += 512; if (flags & UX_ENTITYDBL2) entnum += 1024; *retflags = flags; return entnum; } static void ParseEntityDelta(sv_t *tv, netmsg_t *m, const entity_state_t *old, entity_state_t *new, unsigned int flags, entity_t *ent, qboolean forcerelink) { memcpy(new, old, sizeof(entity_state_t)); if (flags & U_MODEL) { if (flags & UX_MODELDBL) new->modelindex = ReadByte(m)|0x100; //doubled limit... else new->modelindex = ReadByte(m); } else if (flags & UX_MODELDBL) new->modelindex = ReadShort(m); //more sane path... if (flags & U_FRAME) new->frame = ReadByte(m); if (flags & U_COLORMAP) new->colormap = ReadByte(m); if (flags & U_SKIN) new->skinnum = ReadByte(m); if (flags & U_EFFECTS) new->effects = (new->effects&0xff00)|ReadByte(m); if (flags & U_ORIGIN1) new->origin[0] = ReadCoord(m, tv->pext1); if (flags & U_ANGLE1) new->angles[0] = ReadAngle(m, tv->pext1); if (flags & U_ORIGIN2) new->origin[1] = ReadCoord(m, tv->pext1); if (flags & U_ANGLE2) new->angles[1] = ReadAngle(m, tv->pext1); if (flags & U_ORIGIN3) new->origin[2] = ReadCoord(m, tv->pext1); if (flags & U_ANGLE3) new->angles[2] = ReadAngle(m, tv->pext1); if (flags & UX_SCALE) new->scale = ReadByte(m); if (flags & UX_ALPHA) new->alpha = ReadByte(m); if (flags & UX_FATNESS) new->fatness = (signed char)ReadByte(m); if (flags & UX_DRAWFLAGS) new->drawflags = ReadByte(m); if (flags & UX_ABSLIGHT) new->abslight = ReadByte(m); if (flags & UX_COLOURMOD) { new->colormod[0] = ReadByte(m); new->colormod[1] = ReadByte(m); new->colormod[2] = ReadByte(m); } if (flags & UX_DPFLAGS) { // these are bits for the 'flags' field of the entity_state_t new->dpflags = ReadByte(m); } if (flags & UX_TAGINFO) { new->tagentity = ReadShort(m); new->tagindex = ReadShort(m); } if (flags & UX_LIGHT) { new->light[0] = ReadShort(m); new->light[1] = ReadShort(m); new->light[2] = ReadShort(m); new->light[3] = ReadShort(m); new->lightstyle = ReadByte(m); new->lightpflags = ReadByte(m); } if (flags & UX_EFFECTS16) new->effects = (new->effects&0x00ff)|(ReadByte(m)<<8); if (forcerelink || (flags & (U_ORIGIN1|U_ORIGIN2|U_ORIGIN3|U_MODEL))) { if (ent) ent->leafcount = BSP_SphereLeafNums(tv->map.bsp, MAX_ENTITY_LEAFS, ent->leafs, new->origin[0], new->origin[1], new->origin[2], 32); } } static int ExpandFrame(unsigned int newmax, frame_t *frame) { entity_state_t *newents; unsigned short *newnums; if (newmax < frame->maxents) return true; newmax += 16; newents = malloc(sizeof(*newents) * newmax); if (!newents) return false; newnums = malloc(sizeof(*newnums) * newmax); if (!newnums) { free(newents); return false; } memcpy(newents, frame->ents, sizeof(*newents) * frame->maxents); memcpy(newnums, frame->entnums, sizeof(*newnums) * frame->maxents); if (frame->ents) free(frame->ents); if (frame->entnums) free(frame->entnums); frame->ents = newents; frame->entnums = newnums; frame->maxents = newmax; return true; } static void ParsePacketEntities(sv_t *tv, netmsg_t *m, int deltaframe) { frame_t *newframe; frame_t *oldframe; int oldcount; int newnum, oldnum; int newindex, oldindex; unsigned int flags; viewer_t *v; tv->map.nailcount = 0; tv->physicstime = tv->curtime; if (tv->cluster->chokeonnotupdated) { for (v = tv->cluster->viewers; v; v = v->next) { if (v->server == tv) v->chokeme = false; } for (v = tv->cluster->viewers; v; v = v->next) { if (v->server == tv && v->netchan.isnqprotocol) v->maysend = true; } } if (deltaframe != -1) deltaframe &= (ENTITY_FRAMES-1); if (tv->usequakeworldprotocols) { newframe = &tv->map.frame[tv->netchan.incoming_sequence & (ENTITY_FRAMES-1)]; if (tv->netchan.outgoing_sequence - tv->netchan.incoming_sequence >= ENTITY_FRAMES - 1) { //should drop it Sys_Printf(tv->cluster, "Outdated frames\n"); } else if (deltaframe != -1 && newframe->oldframe != deltaframe) Sys_Printf(tv->cluster, "Mismatching delta frames\n"); } else { deltaframe = tv->netchan.incoming_sequence & (ENTITY_FRAMES-1); tv->netchan.incoming_sequence++; newframe = &tv->map.frame[tv->netchan.incoming_sequence & (ENTITY_FRAMES-1)]; } if (deltaframe != -1) { oldframe = &tv->map.frame[deltaframe]; oldcount = oldframe->numents; } else { oldframe = NULL; oldcount = 0; } oldindex = 0; newindex = 0; //printf("frame\n"); for(;;) { newnum = readentitynum(m, &flags); if (!newnum) { //end of packet //any remaining old ents need to be copied to the new frame while (oldindex < oldcount) { //printf("Propogate (spare)\n"); if (!ExpandFrame(newindex, newframe)) break; memcpy(&newframe->ents[newindex], &oldframe->ents[oldindex], sizeof(entity_state_t)); newframe->entnums[newindex] = oldframe->entnums[oldindex]; newindex++; oldindex++; } break; } if (oldindex >= oldcount) oldnum = 0xffff; else oldnum = oldframe->entnums[oldindex]; while(newnum > oldnum) { //printf("Propogate (unchanged)\n"); if (!ExpandFrame(newindex, newframe)) break; memcpy(&newframe->ents[newindex], &oldframe->ents[oldindex], sizeof(entity_state_t)); newframe->entnums[newindex] = oldframe->entnums[oldindex]; newindex++; oldindex++; if (oldindex >= oldcount) oldnum = 0xffff; else oldnum = oldframe->entnums[oldindex]; } if (newnum < oldnum) { //this ent wasn't in the last packet //printf("add\n"); if (flags & U_REMOVE) { //remove this ent... just don't copy it across. //printf("add\n"); continue; } if (!ExpandFrame(newindex, newframe)) break; ParseEntityDelta(tv, m, &tv->map.entity[newnum].baseline, &newframe->ents[newindex], flags, &tv->map.entity[newnum], true); newframe->entnums[newindex] = newnum; newindex++; } else if (newnum == oldnum) { if (flags & U_REMOVE) { //remove this ent... just don't copy it across. //printf("add\n"); oldindex++; continue; } //printf("Propogate (changed)\n"); if (!ExpandFrame(newindex, newframe)) break; ParseEntityDelta(tv, m, &oldframe->ents[oldindex], &newframe->ents[newindex], flags, &tv->map.entity[newnum], false); newframe->entnums[newindex] = newnum; newindex++; oldindex++; } } newframe->numents = newindex; return; /* //luckilly, only updated entities are here, so that keeps cpu time down a bit. for (;;) { flags = ReadShort(m); if (!flags) break; entnum = flags & 511; if (tv->maxents < entnum) tv->maxents = entnum; flags &= ~511; memcpy(&tv->entity[entnum].old, &tv->entity[entnum].current, sizeof(entity_state_t)); //ow. if (flags & U_REMOVE) { tv->entity[entnum].current.modelindex = 0; continue; } if (!tv->entity[entnum].current.modelindex) //lerp from baseline { memcpy(&tv->entity[entnum].current, &tv->entity[entnum].baseline, sizeof(entity_state_t)); forcerelink = true; } else forcerelink = false; if (flags & U_MOREBITS) flags |= ReadByte(m); if (flags & U_MODEL) tv->entity[entnum].current.modelindex = ReadByte(m); if (flags & U_FRAME) tv->entity[entnum].current.frame = ReadByte(m); if (flags & U_COLORMAP) tv->entity[entnum].current.colormap = ReadByte(m); if (flags & U_SKIN) tv->entity[entnum].current.skinnum = ReadByte(m); if (flags & U_EFFECTS) tv->entity[entnum].current.effects = ReadByte(m); if (flags & U_ORIGIN1) tv->entity[entnum].current.origin[0] = ReadShort(m); if (flags & U_ANGLE1) tv->entity[entnum].current.angles[0] = ReadByte(m); if (flags & U_ORIGIN2) tv->entity[entnum].current.origin[1] = ReadShort(m); if (flags & U_ANGLE2) tv->entity[entnum].current.angles[1] = ReadByte(m); if (flags & U_ORIGIN3) tv->entity[entnum].current.origin[2] = ReadShort(m); if (flags & U_ANGLE3) tv->entity[entnum].current.angles[2] = ReadByte(m); tv->entity[entnum].updatetime = tv->curtime; if (!tv->entity[entnum].old.modelindex) //no old state memcpy(&tv->entity[entnum].old, &tv->entity[entnum].current, sizeof(entity_state_t)); //copy the new to the old, so we don't end up with interpolation glitches if ((flags & (U_ORIGIN1 | U_ORIGIN2 | U_ORIGIN3)) || forcerelink) tv->entity[entnum].leafcount = BSP_SphereLeafNums(tv->bsp, MAX_ENTITY_LEAFS, tv->entity[entnum].leafs, tv->entity[entnum].current.origin[0], tv->entity[entnum].current.origin[1], tv->entity[entnum].current.origin[2], 32); } */ } void ParseSpawnStatic(sv_t *tv, netmsg_t *m, int to, unsigned int mask, qboolean delta) { if (tv->map.spawnstatic_count == MAX_STATICENTITIES) { tv->map.spawnstatic_count--; // don't be fatal. Sys_Printf(tv->cluster, "Too many static entities\n"); } if (delta) { unsigned int flags; readentitynum(m, &flags); ParseEntityDelta(tv, m, &null_entity_state, &tv->map.spawnstatic[tv->map.spawnstatic_count], flags, NULL, false); } else ParseEntityState(tv, &tv->map.spawnstatic[tv->map.spawnstatic_count], m); tv->map.spawnstatic_count++; ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, Q1); } static void ParseBaseline(sv_t *tv, netmsg_t *m, int to, unsigned int mask, qboolean delta) { unsigned int entnum; if (delta) { entity_state_t es; unsigned int flags; entnum = readentitynum(m, &flags); ParseEntityDelta(tv, m, &null_entity_state, &es, flags, NULL, false); if (entnum >= MAX_ENTITIES) { ParseError(m); return; } tv->map.entity[entnum].baseline = es; } else { entnum = ReadShort(m); if (entnum >= MAX_ENTITIES) { ParseError(m); return; } ParseEntityState(tv, &tv->map.entity[entnum].baseline, m); } ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, Q1); } static void ParseUpdatePing(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { int pnum; int ping; pnum = ReadByte(m); ping = ReadShort(m); if (pnum < MAX_CLIENTS) tv->map.players[pnum].ping = ping; else Sys_Printf(tv->cluster, "svc_updateping: invalid player number\n"); Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } static void ParseUpdateFrags(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { int pnum; int frags; pnum = ReadByte(m); frags = (signed short)ReadShort(m); if (pnum < MAX_CLIENTS) tv->map.players[pnum].frags = frags; else Sys_Printf(tv->cluster, "svc_updatefrags: invalid player number\n"); Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, (pnum < 16)?Q1:QW); } static void ParseUpdateStat(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { unsigned int pnum; int value; int statnum; statnum = ReadByte(m); value = ReadByte(m); if (statnum < MAX_STATS) { for (pnum = 0; pnum < MAX_CLIENTS; pnum++) { if (mask & (1<map.players[pnum].stats[statnum] = value; } } else Sys_Printf(tv->cluster, "svc_updatestat: invalid stat number\n"); // Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } static void ParseUpdateStatLong(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { unsigned int pnum; int value; int statnum; statnum = ReadByte(m); value = ReadLong(m); if (statnum < MAX_STATS) { for (pnum = 0; pnum < MAX_CLIENTS; pnum++) { if (mask & (1<map.players[pnum].stats[statnum] = value; } } else Sys_Printf(tv->cluster, "svc_updatestatlong: invalid stat number\n"); // Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } static void ParseUpdateUserinfo(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { int pnum; pnum = ReadByte(m); ReadLong(m); if (pnum < MAX_CLIENTS) ReadString(m, tv->map.players[pnum].userinfo, sizeof(tv->map.players[pnum].userinfo)); else { Sys_Printf(tv->cluster, "svc_updateuserinfo: invalid player number\n"); while (ReadByte(m)) //suck out the message. { } } ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } static void ParsePacketloss(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { unsigned int pnum; int value; pnum = ReadByte(m)%MAX_CLIENTS; value = ReadByte(m); if (pnum < MAX_CLIENTS) tv->map.players[pnum].packetloss = value; else Sys_Printf(tv->cluster, "svc_updatepl: invalid player number\n"); Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } static void ParseUpdateEnterTime(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { unsigned int pnum; float value; pnum = ReadByte(m)%MAX_CLIENTS; value = ReadFloat(m); if (pnum < MAX_CLIENTS) tv->map.players[pnum].entertime = value; else Sys_Printf(tv->cluster, "svc_updateentertime: invalid player number\n"); ConnectionData(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } static void ParseSound(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { #define SND_VOLUME (1<<15) // a qbyte #define SND_ATTENUATION (1<<14) // a qbyte #define DEFAULT_SOUND_PACKET_VOLUME 255 #define DEFAULT_SOUND_PACKET_ATTENUATION 1.0 int i; int channel; unsigned char vol; unsigned char atten; unsigned char sound_num; float org[3]; int ent; netmsg_t nqversion; unsigned char nqbuffer[64]; InitNetMsg(&nqversion, nqbuffer, sizeof(nqbuffer)); channel = (unsigned short)ReadShort(m); if (channel & SND_VOLUME) vol = ReadByte (m); else vol = DEFAULT_SOUND_PACKET_VOLUME; if (channel & SND_ATTENUATION) atten = ReadByte (m) / 64.0; else atten = DEFAULT_SOUND_PACKET_ATTENUATION; sound_num = ReadByte (m); ent = (channel>>3)&1023; channel &= 7; for (i=0 ; i<3 ; i++) org[i] = ReadCoord (m, tv->pext1); Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); WriteByte(&nqversion, svc_sound); i = 0; if (vol != DEFAULT_SOUND_PACKET_VOLUME) i |= 1; if (atten != DEFAULT_SOUND_PACKET_ATTENUATION) i |= 2; if (ent > 8191 || channel > 7) i |= 8; if (sound_num > 255) i |= 16; WriteByte(&nqversion, i); if (i & 1) WriteByte(&nqversion, vol); if (i & 2) WriteByte(&nqversion, atten*64); if (i & 8) { WriteShort(&nqversion, ent); WriteByte(&nqversion, channel); } else WriteShort(&nqversion, (ent<<3) | channel); if (i & 16) WriteShort(&nqversion, sound_num); else WriteByte(&nqversion, sound_num); WriteCoord(&nqversion, org[0], tv->pext1); WriteCoord(&nqversion, org[1], tv->pext1); WriteCoord(&nqversion, org[2], tv->pext1); Multicast(tv, nqversion.data, nqversion.cursize, to, mask, NQ); } static void ParseDamage(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { ReadByte (m); ReadByte (m); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, QW); } enum { TE_SPIKE = 0, TE_SUPERSPIKE = 1, TE_GUNSHOT = 2, TE_EXPLOSION = 3, TE_TAREXPLOSION = 4, TE_LIGHTNING1 = 5, TE_LIGHTNING2 = 6, TE_WIZSPIKE = 7, TE_KNIGHTSPIKE = 8, TE_LIGHTNING3 = 9, TE_LAVASPLASH = 10, TE_TELEPORT = 11, TE_BLOOD = 12, TE_LIGHTNINGBLOOD = 13, }; static void ParseTempEntity(sv_t *tv, netmsg_t *m, int to, unsigned int mask) { int i; int dest = QW; char nqversion[64]; int nqversionlength=0; i = ReadByte (m); switch(i) { case TE_SPIKE: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_SUPERSPIKE: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_GUNSHOT: ReadByte (m); nqversion[0] = svc_temp_entity; nqversion[1] = TE_GUNSHOT; if (tv->pext1 & PEXT_FLOATCOORDS) nqversionlength = 2+3*4; else nqversionlength = 2+3*2; for (i = 2; i < nqversionlength; i++) nqversion[i] = ReadByte (m); break; case TE_EXPLOSION: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_TAREXPLOSION: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_LIGHTNING1: case TE_LIGHTNING2: case TE_LIGHTNING3: ReadShort (m); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_WIZSPIKE: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_KNIGHTSPIKE: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_LAVASPLASH: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_TELEPORT: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); dest |= NQ; break; case TE_BLOOD: ReadByte (m); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); //FIXME: generate svc_particle for nq break; case TE_LIGHTNINGBLOOD: ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); ReadCoord (m, tv->pext1); //FIXME: generate svc_particle for nq break; default: Sys_Printf(tv->cluster, "temp entity %i not recognised\n", i); return; } Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, to, mask, dest); if (nqversionlength) Multicast(tv, nqversion, nqversionlength, to, mask, NQ); } void ParseLightstyle(sv_t *tv, netmsg_t *m) { int style; style = ReadByte(m); if (style < MAX_LIGHTSTYLES) ReadString(m, tv->map.lightstyle[style].name, sizeof(tv->map.lightstyle[style].name)); else { Sys_Printf(tv->cluster, "svc_lightstyle: invalid lightstyle index (%i)\n", style); while (ReadByte(m)) //suck out the message. { } } Multicast(tv, (char*)m->data+m->startpos, m->readpos - m->startpos, dem_read, (unsigned)-1, Q1); } void ParseNails(sv_t *tv, netmsg_t *m, qboolean nails2) { int count; int i; count = (unsigned char)ReadByte(m); while(count > sizeof(tv->map.nails) / sizeof(tv->map.nails[0])) {//they sent too many, suck it out. count--; if (nails2) ReadByte(m); for (i = 0; i < 6; i++) ReadByte(m); } tv->map.nailcount = count; while(count-- > 0) { if (nails2) tv->map.nails[count].number = ReadByte(m); else tv->map.nails[count].number = count; for (i = 0; i < 6; i++) tv->map.nails[count].bits[i] = ReadByte(m); } } void ParseDownload(sv_t *tv, netmsg_t *m) { //warning this needs looking at (controller downloads) int size, b; unsigned int percent; char buffer[2048]; size = (signed short)ReadShort(m); percent = ReadByte(m); if (size < 0) { Sys_Printf(tv->cluster, "Downloading failed\n"); if (tv->downloadfile) fclose(tv->downloadfile); tv->downloadfile = NULL; tv->errored = ERR_PERMANENT; QW_StreamPrint(tv->cluster, tv, NULL, "Map download failed\n"); return; } for (b = 0; b < size; b++) buffer[b] = ReadByte(m); if (!tv->downloadfile) { Sys_Printf(tv->cluster, "Not downloading anything\n"); tv->errored = ERR_PERMANENT; return; } fwrite(buffer, 1, size, tv->downloadfile); if (percent == 100) { fclose(tv->downloadfile); tv->downloadfile = NULL; snprintf(buffer, sizeof(buffer), "%s/%s", (*tv->map.gamedir)?tv->map.gamedir:"id1", tv->map.modellist[1].name); rename(tv->downloadname, buffer); Sys_Printf(tv->cluster, "Download complete\n"); tv->map.bsp = BSP_LoadModel(tv->cluster, tv->map.gamedir, tv->map.modellist[1].name); if (!tv->map.bsp) { Sys_Printf(tv->cluster, "Failed to read BSP\n"); tv->errored = ERR_PERMANENT; } else { SendClientCommand(tv, "prespawn %i 0 %i\n", tv->clservercount, LittleLong(BSP_Checksum(tv->map.bsp))); strcpy(tv->status, "Prespawning\n"); } } else { snprintf(tv->status, sizeof(tv->status), "Downloading map, %i%%\n", percent); SendClientCommand(tv, "nextdl\n"); } } void ParseMessage(sv_t *tv, void *buffer, int length, int to, int mask) { int lastsvc; int svc = -1; int i; netmsg_t buf; qboolean clearoldplayers = true; buf.cursize = length; buf.maxsize = length; buf.readpos = 0; buf.data = buffer; buf.startpos = 0; while(buf.readpos < buf.cursize) { lastsvc = svc; if (buf.readpos > buf.cursize) { Sys_Printf(tv->cluster, "Read past end of parse buffer\n, last was %i\n", lastsvc); return; } buf.startpos = buf.readpos; svc = ReadByte(&buf); // printf("%i\n", svc); switch (svc) { case svc_bad: ParseError(&buf); Sys_Printf(tv->cluster, "ParseMessage: svc_bad, last was %i\n", lastsvc); return; case svc_nop: //quakeworld isn't meant to send these. QTV_Printf(tv, "nop\n"); break; case svc_disconnect: //mvdsv safely terminates it's mvds with an svc_disconnect. //the client is meant to read that and disconnect without reading the intentionally corrupt packet following it. //however, our demo playback is chained and looping and buffered. //so we've already found the end of the source file and restarted parsing. //in fte at least, the server does give the packet the correct length //I hope mvdsv is the same if (tv->sourcetype != SRC_DEMO) { #ifndef _MSC_VER #warning QTV is meant to disconnect when servers tells it to. #endif QTV_Printf(tv, "Maliciously ignoring svc_disconnect from upstream...\n"); //ideally the client would be the one to close() the socket first so its the one that gets stuck in TIME_WAIT instead of the server. // FIXME: Servers are today sending the svc_disconnect in a non-standard way, which makes QTV drop when it shouldn't. // Tell the server developers to fix the servers. //tv->drop = true; } else { while(ReadByte(&buf)) ; } return; case svc_updatestat: ParseUpdateStat(tv, &buf, to, mask); break; //#define svc_version 4 // [long] server version case svc_nqsetview: ReadShort(&buf); //no actual handling is done! break; case svc_sound: ParseSound(tv, &buf, to, mask); break; case svc_nqtime: ReadFloat(&buf); //no actual handling is done! break; case svc_print: ParsePrint(tv, &buf, to, mask); break; case svc_stufftext: ParseStufftext(tv, &buf, to, mask); break; case svc_setangle: if (!tv->usequakeworldprotocols) ReadByte(&buf); tv->proxyplayerangles[0] = ReadAngle(&buf, tv->pext1); tv->proxyplayerangles[1] = ReadAngle(&buf, tv->pext1); tv->proxyplayerangles[2] = ReadAngle(&buf, tv->pext1); if (tv->usequakeworldprotocols && tv->controller) SendBufferToViewer(tv->controller, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, true); /*{ char nq[7]; nq[0] = svc_setangle; nq[1] = tv->proxyplayerangles[0]; nq[2] = tv->proxyplayerangles[1]; nq[3] = tv->proxyplayerangles[2]; // Multicast(tv, nq, 4, to, mask, Q1); }*/ break; case svc_serverdata: ParseServerData(tv, &buf, to, mask); break; case svc_lightstyle: ParseLightstyle(tv, &buf); break; //#define svc_updatename 13 // [qbyte] [string] case svc_updatefrags: ParseUpdateFrags(tv, &buf, to, mask); break; //#define svc_clientdata 15 // //#define svc_stopsound 16 // //#define svc_updatecolors 17 // [qbyte] [qbyte] [qbyte] case svc_particle: ReadCoord(&buf, tv->pext1); ReadCoord(&buf, tv->pext1); ReadCoord(&buf, tv->pext1); ReadByte(&buf); ReadByte(&buf); ReadByte(&buf); ReadByte(&buf); ReadByte(&buf); Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, dem_read, (unsigned)-1, Q1); break; case svc_damage: ParseDamage(tv, &buf, to, mask); break; case svc_spawnstatic: ParseSpawnStatic(tv, &buf, to, mask, false); break; case svcfte_spawnstatic2: if (tv->pext1 & PEXT_SPAWNSTATIC2) ParseSpawnStatic(tv, &buf, to, mask, true); else goto badsvc; break; case svc_spawnbaseline: ParseBaseline(tv, &buf, to, mask, false); break; case svcfte_spawnbaseline2: if (tv->pext1 & PEXT_SPAWNSTATIC2) ParseBaseline(tv, &buf, to, mask, true); else goto badsvc; break; case svc_temp_entity: ParseTempEntity(tv, &buf, to, mask); break; case svc_setpause: // [qbyte] on / off tv->map.ispaused = ReadByte(&buf); Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, dem_read, (unsigned)-1, Q1); break; //#define svc_signonnum 25 // [qbyte] used for the signon sequence case svc_centerprint: ParseCenterprint(tv, &buf, to, mask); break; case svc_spawnstaticsound: ParseStaticSound(tv, &buf, to, mask); break; case svc_intermission: ParseIntermission(tv, &buf, to, mask); break; case svc_finale: while(ReadByte(&buf)) ; Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, dem_read, (unsigned)-1, Q1); break; case svc_cdtrack: ParseCDTrack(tv, &buf, to, mask); break; case svc_sellscreen: Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, dem_read, (unsigned)-1, Q1); break; //#define svc_cutscene 34 //hmm... nq only... added after qw tree splitt? case svc_smallkick: case svc_bigkick: Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, to, mask, QW); break; case svc_updateping: ParseUpdatePing(tv, &buf, to, mask); break; case svc_updateentertime: ParseUpdateEnterTime(tv, &buf, to, mask); break; case svc_updatestatlong: ParseUpdateStatLong(tv, &buf, to, mask); break; case svc_muzzleflash: ReadShort(&buf); Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, to, mask, QW); break; case svc_updateuserinfo: ParseUpdateUserinfo(tv, &buf, to, mask); break; case svc_download: // [short] size [size bytes] ParseDownload(tv, &buf); break; case svc_playerinfo: ParsePlayerInfo(tv, &buf, clearoldplayers); clearoldplayers = false; break; case svc_nails: ParseNails(tv, &buf, false); break; case svc_chokecount: ReadByte(&buf); break; case svcfte_modellistshort: case svc_modellist: i = ParseList(tv, &buf, tv->map.modellist, to, mask, svc==svcfte_modellistshort); if (!i) { int j; if (tv->map.bsp) BSP_Free(tv->map.bsp); if (tv->cluster->nobsp)// || !tv->usequkeworldprotocols) tv->map.bsp = NULL; else tv->map.bsp = BSP_LoadModel(tv->cluster, tv->map.gamedir, tv->map.modellist[1].name); tv->map.numinlines = 0; for (j = 2; j < 256; j++) { if (*tv->map.modellist[j].name != '*') break; tv->map.numinlines = j; } tv->map.modelindex_player = 0; tv->map.modelindex_spike = 0; for (j = 2; j < 256; j++) { if (!*tv->map.modellist[j].name) break; if (!strcmp(tv->map.modellist[j].name, "progs/player.mdl")) tv->map.modelindex_player = j; if (!strcmp(tv->map.modellist[j].name, "progs/spike.mdl")) tv->map.modelindex_spike = j; } strcpy(tv->status, "Prespawning\n"); } ConnectionData(tv, (void*)((char*)buf.data+buf.startpos), buf.readpos - buf.startpos, to, mask, QW); if ((!tv->controller || tv->controller->netchan.isnqprotocol) && tv->usequakeworldprotocols) { if (i) SendClientCommand(tv, "modellist %i %i\n", tv->clservercount, i); else if (!tv->map.bsp && !tv->cluster->nobsp) { if (tv->downloadfile) { fclose(tv->downloadfile); unlink(tv->downloadname); Sys_Printf(tv->cluster, "Was already downloading %s\nOld download canceled\n", tv->downloadname); tv->downloadfile = NULL; } snprintf(tv->downloadname, sizeof(tv->downloadname), "%s/%s.tmp", (*tv->map.gamedir)?tv->map.gamedir:"id1", tv->map.modellist[1].name); QTV_mkdir(tv->downloadname); tv->downloadfile = fopen(tv->downloadname, "wb"); if (!tv->downloadfile) { Sys_Printf(tv->cluster, "Couldn't open temporary file %s\n", tv->downloadname); SendClientCommand(tv, "prespawn %i 0 %i\n", tv->clservercount, LittleLong(BSP_Checksum(tv->map.bsp))); } else { char buffer[512]; strcpy(tv->status, "Downloading map\n"); Sys_Printf(tv->cluster, "Attempting download of %s\n", tv->downloadname); SendClientCommand(tv, "download %s\n", tv->map.modellist[1].name); snprintf(buffer, sizeof(buffer), "[QTV] Attempting map download (%s)\n", tv->map.modellist[1].name); QW_StreamPrint(tv->cluster, tv, NULL, buffer); } } else { SendClientCommand(tv, "prespawn %i 0 %i\n", tv->clservercount, LittleLong(BSP_Checksum(tv->map.bsp))); } } break; case svcfte_soundlistshort: case svc_soundlist: i = ParseList(tv, &buf, tv->map.soundlist, to, mask, svc==svcfte_soundlistshort); if (!i) strcpy(tv->status, "Receiving modellist\n"); ConnectionData(tv, (void*)((char*)buf.data+buf.startpos), buf.readpos - buf.startpos, to, mask, QW); if ((!tv->controller || tv->controller->netchan.isnqprotocol) && tv->usequakeworldprotocols) { if (i) SendClientCommand(tv, "soundlist %i %i\n", tv->clservercount, i); else SendClientCommand(tv, "modellist %i 0\n", tv->clservercount); } break; case svc_packetentities: // FlushPacketEntities(tv); ParsePacketEntities(tv, &buf, -1); break; case svc_deltapacketentities: ParsePacketEntities(tv, &buf, ReadByte(&buf)); break; case svc_entgravity: // gravity change, for prediction ReadFloat(&buf); Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, to, mask, QW); break; case svc_maxspeed: // maxspeed change, for prediction ReadFloat(&buf); Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, to, mask, QW); break; case svc_setinfo: ParseSetInfo(tv, &buf); break; case svc_serverinfo: ParseServerinfo(tv, &buf); break; case svc_updatepl: ParsePacketloss(tv, &buf, to, mask); break; case svc_nails2: ParseNails(tv, &buf, true); break; case svc_killedmonster: Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, to, mask, Q1); break; case svc_foundsecret: Multicast(tv, (char*)buf.data+buf.startpos, buf.readpos - buf.startpos, to, mask, Q1); break; default: badsvc: buf.readpos = buf.startpos; Sys_Printf(tv->cluster, "Can't handle svc %i, last was %i\n", (unsigned int)ReadByte(&buf), lastsvc); return; } } } ================================================ FILE: fteqtv/pmove.c ================================================ #include "qtv.h" #include #ifndef M_PI #define M_PI 3.1415926535897932384626433832795 #endif void AngleVectors (vec3_t angles, float *forward, float *right, float *up) { float angle; float sr, sp, sy, cr, cp, cy; angle = angles[1] * (M_PI*2 / 360); sy = sin(angle); cy = cos(angle); angle = angles[0] * (M_PI*2 / 360); sp = sin(angle); cp = cos(angle); angle = angles[2] * (M_PI*2 / 360); sr = sin(angle); cr = cos(angle); forward[0] = cp*cy; forward[1] = cp*sy; forward[2] = -sp; right[0] = (-1*sr*sp*cy+-1*cr*-sy); right[1] = (-1*sr*sp*sy+-1*cr*cy); right[2] = -1*sr*cp; up[0] = (cr*sp*cy+-sr*-sy); up[1] = (cr*sp*sy+-sr*cy); up[2] = cr*cp; } #define DotProduct(a,b) ((a[0]*b[0]) + (a[1]*b[1]) + (a[2]*b[2])) #define VectorCopy(a,b) do{b[0]=a[0];b[1]=a[1];b[2]=a[2];}while(0) #define VectorClear(v) do{v[0]=0;v[1]=0;v[2]=0;}while(0) #define VectorScale(i,s,o) do{o[0]=i[0]*s;o[1]=i[1]*s;o[2]=i[2]*s;}while(0) #define Length(v) sqrt(DotProduct(v, v)) #define VectorMA(base,s,m,out) do{out[0]=base[0]+s*m[0];out[1]=base[1]+s*m[1];out[2]=base[2]+s*m[2];}while(0) #define SHORT2ANGLE(s) ((s*360.0f)/65536) float VectorNormalize(vec3_t v) { float len, ilen; len = Length(v); if (len) { ilen = 1/len; v[0] *= ilen; v[1] *= ilen; v[2] *= ilen; } return len; } void PM_SpectatorMove (pmove_t *pmove) { float speed, drop, friction, control, newspeed; float currentspeed, addspeed, accelspeed; int i; vec3_t wishvel; float fmove, smove; vec3_t wishdir; float wishspeed; // friction speed = Length (pmove->velocity); if (speed < 1) { VectorClear (pmove->velocity); } else { drop = 0; friction = pmove->movevars.friction*1.5; // extra friction control = speed < pmove->movevars.stopspeed ? pmove->movevars.stopspeed : speed; drop += control*friction*pmove->frametime; // scale the velocity newspeed = speed - drop; if (newspeed < 0) newspeed = 0; newspeed /= speed; VectorScale (pmove->velocity, newspeed, pmove->velocity); } // accelerate fmove = pmove->cmd.forwardmove; smove = pmove->cmd.sidemove; VectorNormalize (pmove->forward); VectorNormalize (pmove->right); for (i=0 ; i<3 ; i++) wishvel[i] = pmove->forward[i]*fmove + pmove->right[i]*smove; wishvel[2] += pmove->cmd.upmove; VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); // // clamp to server defined max speed // if (wishspeed > pmove->movevars.spectatormaxspeed) { VectorScale (wishvel, pmove->movevars.spectatormaxspeed/wishspeed, wishvel); wishspeed = pmove->movevars.spectatormaxspeed; } currentspeed = DotProduct(pmove->velocity, wishdir); addspeed = wishspeed - currentspeed; // Buggy QW spectator mode, kept for compatibility // if (pmove->pm_type == PM_OLD_SPECTATOR) { if (addspeed <= 0) return; } if (addspeed > 0) { accelspeed = pmove->movevars.accelerate*pmove->frametime*wishspeed; if (accelspeed > addspeed) accelspeed = addspeed; for (i=0 ; i<3 ; i++) pmove->velocity[i] += accelspeed*wishdir[i]; } // move VectorMA (pmove->origin, pmove->frametime, pmove->velocity, pmove->origin); } void PM_PlayerMove (pmove_t *pmove) { pmove->frametime = pmove->cmd.msec * 0.001; /* if (pmove.pm_type == PM_NONE || pmove.pm_type == PM_FREEZE) { PM_CategorizePosition (); return; } */ // take angles directly from command pmove->angles[0] = pmove->cmd.angles[0]; pmove->angles[1] = pmove->cmd.angles[1]; pmove->angles[2] = pmove->cmd.angles[2]; AngleVectors (pmove->angles, pmove->forward, pmove->right, pmove->up); // if (pmove->pm_type == PM_SPECTATOR || pmove->pm_type == PM_OLD_SPECTATOR) { PM_SpectatorMove (pmove); // pmove->onground = false; return; } } ================================================ FILE: fteqtv/protocol.h ================================================ //limitations of the protocol #define MAX_SERVERINFO_STRING 1024 //standard quakeworld has 512 here. #define MAX_USERINFO 1024 //standard quakeworld has 192 here. #define MAX_CLIENTS 32 #define MAX_LIST 256 #define MAX_MODELS MAX_LIST #define MAX_SOUNDS MAX_LIST #define MAX_ENTITIES 512 #define MAX_STATICSOUNDS 256 #define MAX_STATICENTITIES 128 #define MAX_LIGHTSTYLES 64 #define MAX_PROXY_INBUFFER 4096 //max bytes from a downstream proxy. #define MAX_PROXY_BUFFER (1<<17) //must be power-of-two (buffer sizes for downstream, both sv/cl) #define PREFERRED_PROXY_BUFFER 4096 //the ammount of data we try to leave in our input buffer (must be large enough to contain any single mvd frame) #define ENTS_PER_FRAME 64 //max number of entities per frame (OUCH!). #define ENTITY_FRAMES 64 //number of frames to remember for deltaing #define Z_EXT_SERVERTIME (1<<3) // STAT_TIME #define Z_EXT_STRING "8" //qw specific #define PRINT_CHAT 3 #define PRINT_HIGH 2 #define PRINT_MEDIUM 1 #define PRINT_LOW 0 #define MAX_STATS 32 #define STAT_HEALTH 0 #define STAT_FRAGS 1 #define STAT_WEAPONMODELI 2 #define STAT_AMMO 3 #define STAT_ARMOR 4 #define STAT_WEAPONFRAME 5 #define STAT_SHELLS 6 #define STAT_NAILS 7 #define STAT_ROCKETS 8 #define STAT_CELLS 9 #define STAT_ACTIVEWEAPON 10 #define STAT_TOTALSECRETS 11 #define STAT_TOTALMONSTERS 12 #define STAT_SECRETS 13 // bumped on client side by svc_foundsecret #define STAT_MONSTERS 14 // bumped by svc_killedmonster #define STAT_ITEMS 15 #define STAT_TIME 17 //A ZQ hack, sending time via a stat. //this allows l33t engines to interpolate properly without spamming at a silly high fps. //limits #define NQ_PACKETS_PER_SECOND 20 #define MAX_MSGLEN 8192 //the biggest datagram size we allow #define MAX_NQMSGLEN 8000 //nq has large reliable packets for the connection data #define MAX_QWMSGLEN 1450 //qw is fully split into individual packets #define MAX_NQDATAGRAM 1024 //nq datagrams are only 1k #define MAX_BACKBUF_SIZE 1000 //this is smaller so we don't loose too many entities when lagging //NQ transport layer defines #define NETFLAG_LENGTH_MASK 0x0000ffff #define NETFLAG_DATA 0x00010000 #define NETFLAG_ACK 0x00020000 #define NETFLAG_NAK 0x00040000 #define NETFLAG_EOM 0x00080000 #define NETFLAG_UNRELIABLE 0x00100000 #define NETFLAG_CTL 0x80000000 #define CCREQ_CONNECT 0x01 #define CCREQ_SERVER_INFO 0x02 #define CCREP_ACCEPT 0x81 #define CCREP_REJECT 0x82 #define CCREP_SERVER_INFO 0x83 #define NQ_NETCHAN_GAMENAME "QUAKE" #define NQ_NETCHAN_VERSION 3 //end NQ specific //the clcs sent via the udp connections enum { clc_bad = 0, clc_nop = 1, clc_disconnect = 2, //NQ only clc_move = 3, // [[usercmd_t] clc_stringcmd = 4, // [string] message clc_delta = 5, // [byte] sequence number, requests delta compression of message clc_tmove = 6, // teleport request, spectator only clc_upload = 7 // teleport request, spectator only }; //these are the clcs sent upstream via the tcp streams enum { qtv_clc_bad = 0, qtv_clc_stringcmd = 1, qtv_clc_commentarydata = 8 }; #define svc_bad 0 #define svc_nop 1 #define svc_disconnect 2 #define svc_updatestat 3 // [qbyte] [qbyte] //#define svc_version 4 // [long] server version (not used anywhere) #define svc_nqsetview 5 // [short] entity number #define svc_sound 6 // #define svc_nqtime 7 // [float] server time #define svc_print 8 // [qbyte] id [string] null terminated string #define svc_stufftext 9 // [string] stuffed into client's console buffer // the string should be \n terminated #define svc_setangle 10 // [angle3] set the view angle to this absolute value #define svc_serverdata 11 // [long] protocol ... #define svc_lightstyle 12 // [qbyte] [string] #define svc_nqupdatename 13 // [qbyte] [string] #define svc_updatefrags 14 // [qbyte] [short] #define svc_nqclientdata 15 // //#define svc_stopsound 16 // (not used anywhere) #define svc_nqupdatecolors 17 // [qbyte] [qbyte] [qbyte] #define svc_particle 18 // [vec3] #define svc_damage 19 #define svc_spawnstatic 20 //#define svc_spawnstatic2 21 (not used anywhere) #define svcfte_spawnstatic2 21 #define svc_spawnbaseline 22 #define svc_temp_entity 23 // variable #define svc_setpause 24 // [qbyte] on / off #define svc_nqsignonnum 25 // [qbyte] used for the signon sequence #define svc_centerprint 26 // [string] to put in center of the screen #define svc_killedmonster 27 #define svc_foundsecret 28 #define svc_spawnstaticsound 29 // [coord3] [qbyte] samp [qbyte] vol [qbyte] aten #define svc_intermission 30 // [vec3_t] origin [vec3_t] angle (show scoreboard and stuff) #define svc_finale 31 // [string] text ('congratulations blah blah') #define svc_cdtrack 32 // [qbyte] track #define svc_sellscreen 33 //#define svc_cutscene 34 //hmm... nq only... added after qw tree splitt? (intermission without the scores) #define svc_smallkick 34 // set client punchangle to 2 #define svc_bigkick 35 // set client punchangle to 4 #define svc_updateping 36 // [qbyte] [short] #define svc_updateentertime 37 // [qbyte] [float] #define svc_updatestatlong 38 // [qbyte] [long] #define svc_muzzleflash 39 // [short] entity #define svc_updateuserinfo 40 // [qbyte] slot [long] uid // [string] userinfo #define svc_download 41 // [short] size [size bytes] #define svc_playerinfo 42 // variable #define svc_nails 43 // [qbyte] num [48 bits] xyzpy 12 12 12 4 8 #define svc_chokecount 44 // [qbyte] packets choked #define svc_modellist 45 // [strings] #define svc_soundlist 46 // [strings] #define svc_packetentities 47 // [...] #define svc_deltapacketentities 48 // [...] #define svc_maxspeed 49 // maxspeed change, for prediction #define svc_entgravity 50 // gravity change, for prediction #define svc_setinfo 51 // setinfo on a client #define svc_serverinfo 52 // serverinfo #define svc_updatepl 53 // [qbyte] [qbyte] #define svc_nails2 54 //mvd only - [qbyte] num [52 bits] nxyzpy 8 12 12 12 4 8 #define svcfte_soundlistshort 56 #define svcfte_modellistshort 60 #define svcfte_spawnbaseline2 66 #define dem_audio 0 #define dem_cmd 0 #define dem_read 1 #define dem_set 2 #define dem_multiple 3 #define dem_single 4 #define dem_stats 5 #define dem_all 6 #define dem_qtvdata (dem_all | (1<<4)) //special packet that contains qtv data (spectator chat, etc. clients need to parse this as soon as it is sent to them, which might or might not be awkward for them) #define dem_mask 7 #define PROTOCOL_VERSION_NQ 15 #define PROTOCOL_VERSION 28 #define PROTOCOL_VERSION_FTE (('F'<<0) + ('T'<<8) + ('E'<<16) + ('X' << 24)) //fte extensions. #define PROTOCOL_VERSION_FTE2 (('F'<<0) + ('T'<<8) + ('E'<<16) + ('2' << 24)) //fte extensions. #define PROTOCOL_VERSION_EZQUAKE1 (('M'<<0) + ('V'<<8) + ('D'<<16) + ('1' << 24)) //ezquake/mvdsv extensions #define PROTOCOL_VERSION_HUFFMAN (('H'<<0) + ('U'<<8) + ('F'<<16) + ('F' << 24)) //packet compression #define PROTOCOL_VERSION_VARLENGTH (('v'<<0) + ('l'<<8) + ('e'<<16) + ('n' << 24)) //variable length handshake #define PROTOCOL_VERSION_FRAGMENT (('F'<<0) + ('R'<<8) + ('A'<<16) + ('G' << 24)) //supports fragmentation/packets larger than 1450 #define PEXT_SETVIEW 0x00000001 #define PEXT_SCALE 0x00000002 //obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_LIGHTSTYLECOL 0x00000004 #define PEXT_TRANS 0x00000008 //obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_VIEW2 0x00000010 //#define PEXT_BULLETENS 0x00000020 //no longer supported #define PEXT_ACCURATETIMINGS 0x00000040 #define PEXT_SOUNDDBL 0x00000080 //revised startsound protocol #define PEXT_FATNESS 0x00000100 //obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_HLBSP 0x00000200 #define PEXT_TE_BULLET 0x00000400 //obsoleted by fully custom particle effects. its an ooold extension, okay? :/ #define PEXT_HULLSIZE 0x00000800 //obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_MODELDBL 0x00001000 //obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_ENTITYDBL 0x00002000 //max of 1024 ents instead of 512. obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_ENTITYDBL2 0x00004000 //max of 2048 ents instead of 512 #define PEXT_FLOATCOORDS 0x00008000 //supports floating point origins. //#define PEXT_VWEAP 0x00010000 //cause an extra qbyte to be sent, and an extra list of models for vweaps. #define PEXT_Q2BSP 0x00020000 #define PEXT_Q3BSP 0x00040000 #define PEXT_COLOURMOD 0x00080000 //this replaces an older value which would rarly have caried any actual data. #define PEXT_SPLITSCREEN 0x00100000 #define PEXT_HEXEN2 0x00200000 //more stats and working particle builtin. #define PEXT_SPAWNSTATIC2 0x00400000 //Sends an entity delta instead of a baseline. obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT_CUSTOMTEMPEFFECTS 0x00800000 //supports custom temp ents. #define PEXT_256PACKETENTITIES 0x01000000 //Client can recieve 256 packet entities. obsoleted by PEXT2_REPLACEMENTDELTAS //#define PEXT_NEVERUSED 0x02000000 #define PEXT_SHOWPIC 0x04000000 #define PEXT_SETATTACHMENT 0x08000000 //md3 tags (needs networking, they need to lerp). //#define PEXT_NEVERUSED 0x10000000 #define PEXT_CHUNKEDDOWNLOADS 0x20000000 //alternate file download method. Hopefully it'll give quadroupled download speed, especially on higher pings. #define PEXT_CSQC 0x40000000 //csqc additions. extra stats, particle svcs #define PEXT_DPFLAGS 0x80000000 //extra flags for viewmodel/externalmodel and possible other persistant style flags. obsoleted by PEXT2_REPLACEMENTDELTAS #define PEXT2_PRYDONCURSOR 0x00000001 #define PEXT2_VOICECHAT 0x00000002 #define PEXT2_SETANGLEDELTA 0x00000004 #define PEXT2_OLDREPLACEMENTDELTAS 0x00000008 //weaponframe was part of the entity state. that flag is now the player's v_angle. #define PEXT2_MAXPLAYERS 0x00000010 //Client is able to cope with more players than 32. abs max becomes 255, due to colormap issues. #define PEXT2_PREDINFO 0x00000020 //movevar stats, NQ input sequences+acks. #define PEXT2_NEWSIZEENCODING 0x00000040 //richer size encoding. #define PEXT2_INFOBLOBS 0x00000080 //serverinfo+userinfo lengths can be MUCH higher (protocol is unbounded, but expect low sanity limits on userinfo), and contain nulls etc. //#define PEXT2_PK3DOWNLOADS 0x10000000 //retrieve a list of pk3s/pk3s/paks for downloading (with optional URL and crcs) #define PEXTE_HIDDENMESSAGES 0x20 //random demo metadata... //flags on entities #define U_ORIGIN1 (1<<9) #define U_ORIGIN2 (1<<10) #define U_ORIGIN3 (1<<11) #define U_ANGLE2 (1<<12) #define U_FRAME (1<<13) #define U_REMOVE (1<<14) // REMOVE this entity, don't add it #define U_MOREBITS (1<<15) // if MOREBITS is set, these additional flags are read in next #define U_ANGLE1 (1<<0) #define U_ANGLE3 (1<<1) #define U_MODEL (1<<2) #define U_COLORMAP (1<<3) #define U_SKIN (1<<4) #define U_EFFECTS (1<<5) #define U_SOLID (1<<6) // the entity should be solid for prediction #define UX_EVENMORE (1<<7) #define UX_SCALE (1<<16) //scaler of alias models #define UX_ALPHA (1<<17) //transparency value #define UX_FATNESS (1<<18) //qbyte describing how fat an alias model should be. (moves verticies along normals). Useful for vacuum chambers... #define UX_MODELDBL (1<<19) //extra bit for modelindexes #define UX_UNUSED1 (1<<20) #define UX_ENTITYDBL (1<<21) //use an extra qbyte for origin parts, cos one of them is off #define UX_ENTITYDBL2 (1<<22) //use an extra qbyte for origin parts, cos one of them is off #define UX_YETMORE (1<<23) //even more extension info stuff. #define UX_DRAWFLAGS (1<<24) //use an extra qbyte for origin parts, cos one of them is off #define UX_ABSLIGHT (1<<25) //Force a lightlevel #define UX_COLOURMOD (1<<26) //rgb #define UX_DPFLAGS (1<<27) #define UX_TAGINFO (1<<28) #define UX_LIGHT (1<<29) #define UX_EFFECTS16 (1<<30) #define UX_FARMORE (1<<31) //flags on players #define PF_MSEC (1<<0) #define PF_COMMAND (1<<1) #define PF_VELOCITY1 (1<<2) #define PF_VELOCITY2 (1<<3) #define PF_VELOCITY3 (1<<4) #define PF_MODEL (1<<5) #define PF_SKINNUM (1<<6) #define PF_EFFECTS (1<<7) #define PF_WEAPONFRAME (1<<8) // only sent for view player #define PF_DEAD (1<<9) // don't block movement any more #define PF_GIB (1<<10) // offset the view height differently //flags on players in mvds #define DF_ORIGIN 1 #define DF_ANGLES (1<<3) #define DF_EFFECTS (1<<6) #define DF_SKINNUM (1<<7) #define DF_DEAD (1<<8) #define DF_GIB (1<<9) #define DF_WEAPONFRAME (1<<10) #define DF_MODEL (1<<11) ================================================ FILE: fteqtv/qtv.h ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ #ifdef __GNUC__ #define LittleLong(x) ({ typeof(x) _x = (x); _x = (((unsigned char *)&_x)[0]|(((unsigned char *)&_x)[1]<<8)|(((unsigned char *)&_x)[2]<<16)|(((unsigned char *)&_x)[3]<<24)); _x; }) #define LittleShort(x) ({ typeof(x) _x = (x); _x = (((unsigned char *)&_x)[0]|(((unsigned char *)&_x)[1]<<8)); _x; }) #else #define LittleLong(x) (x) #define LittleShort(x) (x) #endif //this is for a future version //#define COMMENTARY //each server that we are connected to has it's own state. //it should be easy enough to use one thread per server. //mvd info is forwarded to other proxies instantly //qwd stuff is buffered and delayed. :( //this means that when a new proxy connects, we have to send initial state as well as a chunk of pending state, expect to need to send new data before the proxy even has all the init stuff. We may need to raise MAX_PROXY_BUFFER to be larger than on the server #ifdef __GNUC__ #define PRINTFWARNING(x) __attribute__((format(printf, x, (x+1)))) #else #define PRINTFWARNING(x) /*nothing*/ #endif //how does multiple servers work //each proxy acts as a cluster of connections to servers //when a viewer connects, they are given a list of active server connections //if there's only one server connection, they are given that one automatically. #if defined(__APPLE__) && defined(__MACH__) #define MACOSX #endif #include /*work around fucked MSVC functions. we use our own for these*/ #if _MSC_VER >= 1300 && _MSC_VER < 1900 #include #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif #define vsnprintf q_vsnprintf /*msvc doesn't null terminate. its insecute and thus useless*/ #define stricmp _stricmp /*msvc just doesn't work properly*/ #define chdir _chdir #define gwtcwd _getcwd #endif #ifdef _WIN32 #include #include //this includes windows.h and is the reason for much compiling slowness with windows builds. #ifdef IPPROTO_IPV6 #include #else #define IPPROTO_IPV6 41 #ifndef EAI_NONAME #define EAI_NONAME 8 #endif struct ip6_scope_id { union { struct { u_long Zone : 28; u_long Level : 4; }; u_long Value; }; }; struct in6_addr { u_char s6_addr[16]; /* IPv6 address */ }; typedef struct sockaddr_in6 { short sin6_family; u_short sin6_port; u_long sin6_flowinfo; struct in6_addr sin6_addr; union { u_long sin6_scope_id; struct ip6_scope_id sin6_scope_struct; }; }; #if !(_MSC_VER >= 1500) struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; char* ai_canonname; struct sockaddr * ai_addr; struct addrinfo * ai_next; }; #endif #endif #ifdef _MSC_VER #pragma comment (lib, "wsock32.lib") #endif #define qerrno WSAGetLastError() #define NET_EWOULDBLOCK WSAEWOULDBLOCK #define NET_EINPROGRESS WSAEINPROGRESS #define NET_ECONNREFUSED WSAECONNREFUSED #define NET_ENOTCONN WSAENOTCONN //we have special functions to properly terminate sprintf buffers in windows. //we assume other systems are designed with even a minor thought to security. #if !defined(__MINGW32__) #define unlink _unlink //why do MS have to be so awkward? int snprintf(char *buffer, int buffersize, char *format, ...) PRINTFWARNING(3); int vsnprintf(char *buffer, int buffersize, const char *format, va_list argptr); #else #define unlink remove //seems mingw misses something #endif #ifdef _MSC_VER //okay, so warnings are here to help... they're ugly though. #pragma warning(disable: 4761) //integral size mismatch in argument #pragma warning(disable: 4244) //conversion from float to short #pragma warning(disable: 4018) //signed/unsigned mismatch #endif #elif defined(__CYGWIN__) #include #include #include #include #include #include #include #include #include #define ioctlsocket ioctl #define closesocket close #elif (defined(unix) && !defined(__CYGWIN__)) || defined(ixemul) // I hope by adding MACOSX here it doesnt stop it from being natively built on macosx #include #include #include #include #include #include #include #include #include #include #include #include #include #define ioctlsocket ioctl #define closesocket close #if defined(__linux__) && !defined(ANDROID) // #define HAVE_EPOLL #endif #ifdef HAVE_EPOLL #include #endif #elif (defined(__MORPHOS__) && !defined(ixemul)) #include #include #include #include #include #include #include #include #define qerrno Errno() #define ioctlsocket IoctlSocket #define closesocket CloseSocket #else #error "Please insert required headers here" //try the cygwin ones #endif #ifndef NET_EWOULDBLOCK #define NET_EWOULDBLOCK EWOULDBLOCK #define NET_EINPROGRESS EINPROGRESS #define NET_ECONNREFUSED ECONNREFUSED #define NET_ENOTCONN ENOTCONN #endif #ifndef NET_EAGAIN #define NET_EAGAIN NET_EWOULDBLOCK #endif #ifndef IPV6_V6ONLY #define IPV6_V6ONLY 27 #endif #ifndef pgetaddrinfo #ifndef _WIN32 #define pgetaddrinfo getaddrinfo #define pfreeaddrinfo freeaddrinfo #endif #endif #ifndef SOCKET #define SOCKET int #endif #ifndef INVALID_SOCKET #define INVALID_SOCKET -1 #endif #ifndef qerrno #define qerrno errno #endif #include #include #ifndef _WIN32 //stricmp is ansi, strcasecmp is unix. #define stricmp strcasecmp #define strnicmp strncasecmp #endif #define ustrlen(s) strlen((char*)(s)) #define ustrcmp(s1,s2) strcmp((char*)(s1),(char*)(s2)) #define ustrncmp(s1,s2,l) strncmp((char*)(s1),(char*)(s2),l) size_t strlcpy(char *dst, const char *src, size_t siz); typedef struct { unsigned int digestsize; unsigned int contextsize; //you need to alloca(te) this much memory... void (*init) (void *context); void (*process) (void *context, const void *data, size_t datasize); void (*terminate) (unsigned char *digest, void *context); } hashfunc_t; extern hashfunc_t hash_md5; extern hashfunc_t hash_sha1; /*extern hashfunc_t hash_sha2_224; extern hashfunc_t hash_sha2_256; extern hashfunc_t hash_sha2_384; extern hashfunc_t hash_sha2_512;*/ size_t CalcHash(hashfunc_t *hash, unsigned char *digest, size_t maxdigestsize, const unsigned char *string, size_t stringlen); unsigned int CalcHashInt(const hashfunc_t *hash, const void *data, size_t datasize); size_t CalcHMAC(hashfunc_t *hashfunc, unsigned char *digest, size_t maxdigestsize, const unsigned char *data, size_t datalen, const unsigned char *key, size_t keylen); #ifdef LIBQTV #define Sys_Printf QTVSys_Printf #endif #ifndef STRINGIFY #define STRINGIFY2(s) #s #define STRINGIFY(s) STRINGIFY2(s) #endif #ifdef SVNREVISION #define QTV_VERSION_STRING STRINGIFY(SVNREVISION) #else //#include "../engine/common/bothdefs.h" //#define QTV_VERSION_STRING STRINGIFY(FTE_VER_MAJOR)"."STRINGIFY(FTE_VER_MINOR) #define QTV_VERSION_STRING "v?""?""?" #endif #define PROX_DEFAULTSERVERPORT 27500 #define PROX_DEFAULTLISTENPORT 27501 #define PROX_DEFAULTSERVER "localhost:27500" #define DEFAULT_HOSTNAME "FTEQTV" #define PROXYWEBSITE "http://fte.triptohell.info" //url for program #define MAX_ENTITY_LEAFS 32 #include "protocol.h" #if !defined(__cplusplus) && (!defined(__STDC_VERSION__) || (defined(__STDC_VERSION__) && (__STDC_VERSION__ < 202311L))) typedef enum {false, true} qboolean; #else typedef int qboolean; #endif #ifdef __cplusplus extern "C" { #endif typedef struct { void *tcpcon; /*value not indirected, only compared*/ char sockaddr[64]; } netadr_t; #ifdef COMMENTARY typedef struct soundcapt_s { int (*update)(struct soundcapt_s *ghnd, int samplechunks, char *buffer); void (*close)(struct soundcapt_s *ptr); } soundcapt_t; typedef struct soundplay_s { int (*update)(struct soundplay_s *ghnd, int samplechunks, char *buffer); void (*close)(struct soundplay_s *ptr); } soundplay_t; #endif typedef struct { unsigned int readpos; unsigned int cursize; unsigned int maxsize; void *data; unsigned int startpos; qboolean overflowed; qboolean allowoverflow; } netmsg_t; typedef struct { SOCKET sock; netadr_t remote_address; unsigned short qport; unsigned int last_received; unsigned int cleartime; int maxdatagramlen; int maxreliablelen; int reliable_length; qboolean drop; qboolean isclient; qboolean isnqprotocol; netmsg_t message; char message_buf[MAX_MSGLEN]; //reliable message being built char reliable_buf[MAX_MSGLEN]; //reliable message that we're making sure arrives. float rate; unsigned int incoming_acknowledged; unsigned int last_reliable_sequence; unsigned int incoming_reliable_acknowledged; unsigned int incoming_reliable_sequence; unsigned int reliable_sequence; unsigned int incoming_sequence; unsigned int outgoing_sequence; unsigned int reliable_start; unsigned int outgoing_unreliable; unsigned int incoming_unreliable; unsigned int in_fragment_length; char in_fragment_buf[MAX_NQMSGLEN]; } netchan_t; typedef struct { #define MAX_QPATH 64 char name[MAX_QPATH]; } filename_t; typedef struct { unsigned char frame; unsigned short modelindex; unsigned char colormap; unsigned char skinnum; float origin[3]; float angles[3]; unsigned short effects; unsigned char scale; unsigned char fatness; unsigned char colormod[3]; unsigned char alpha; unsigned short light[4]; unsigned char lightstyle; unsigned char lightpflags; unsigned char abslight; unsigned char drawflags; unsigned char dpflags; unsigned char tagindex; //networked as a short, should have been a byte to match dpp5+. unsigned short tagentity; } entity_state_t; typedef struct { //qw does players weirdly. unsigned char frame; unsigned char modelindex; //colormap unsigned char skinnum; float origin[3]; float angles[3]; unsigned char effects; short velocity[3]; unsigned char weaponframe; } player_state_t; typedef struct { unsigned int stats[MAX_STATS]; char userinfo[MAX_USERINFO]; int ping; int packetloss; int frags; float entertime; int leafcount; unsigned short leafs[MAX_ENTITY_LEAFS]; qboolean oldactive:1; qboolean active:1; qboolean gibbed:1; qboolean dead:1; player_state_t current; player_state_t old; } playerinfo_t; typedef struct { entity_state_t ents[ENTS_PER_FRAME]; //ouchie ouchie! unsigned short entnum[ENTS_PER_FRAME]; int numents; } packet_entities_t; typedef struct { unsigned char msec; float angles[3]; short forwardmove, sidemove, upmove; unsigned char buttons; unsigned char impulse; } usercmd_t; extern const usercmd_t nullcmd; typedef float vec3_t[3]; typedef struct { float gravity; float maxspeed; float spectatormaxspeed; float accelerate; float airaccelerate; float waterfriction; float entgrav; float stopspeed; float wateraccelerate; float friction; } movevars_t; typedef struct { //in / out vec3_t origin; vec3_t velocity; //in usercmd_t cmd; movevars_t movevars; //internal vec3_t angles; float frametime; vec3_t forward, right, up; } pmove_t; #define MBTN_UP (1u<<0) #define MBTN_DOWN (1u<<1) #define MBTN_LEFT (1u<<2) #define MBTN_RIGHT (1u<<3) #define MBTN_ENTER (1u<<4) #define MAX_BACK_BUFFERS 16 typedef struct sv_s sv_t; typedef struct cluster_s cluster_t; typedef struct viewer_s { //viewers are regular clients connected over udp. //they may be watching a communal stream, or they might themselves be playing through the proxy, directly controlling the stream. qboolean drop; unsigned int timeout; unsigned int nextpacket; //for nq clients netchan_t netchan; qboolean maysend; qboolean chokeme; qboolean thinksitsconnected; qboolean conmenussupported; qboolean isproxy; unsigned int pext1, pext2; int servercount; netmsg_t backbuf[MAX_BACK_BUFFERS]; //note data is malloced! int backbuffered; unsigned int currentstats[MAX_STATS]; int trackplayer; int thisplayer; int userid; packet_entities_t frame[ENTITY_FRAMES]; int delta_frames[ENTITY_FRAMES]; struct viewer_s *next; struct viewer_s *commentator; char name[32]; char userinfo[1024]; int lost; //packets usercmd_t ucmds[3]; unsigned int lasttime; unsigned int menubuttons; int settime; //the time that we last told the client. vec3_t velocity; vec3_t origin; int isadmin; char expectcommand[16]; sv_t *server; int menuspamtime; int menunum; int menuop; int fwdval; //for scrolling up/down the menu using +forward/+back :) int firstconnect; } viewer_t; typedef struct { qboolean websocket; //true if we need to use special handling unsigned char wsbuf[16]; int wsbuflen; int wspushed; int wsbits; } wsrbuf_t; //'other proxy', these are mvd stream clients. typedef struct oproxy_s { int authkey; unsigned int droptime; qboolean flushing; qboolean drop; sv_t *defaultstream; sv_t *stream; FILE *srcfile; //buffer is padded with data from this file when its empty FILE *file; //recording a demo (written to) SOCKET sock; //playing to a proxy wsrbuf_t websocket; unsigned char inbuffer[MAX_PROXY_INBUFFER]; unsigned int inbuffersize; //amount of data available. unsigned char buffer[MAX_PROXY_BUFFER]; unsigned int buffersize; //use cyclic buffering. unsigned int bufferpos; struct oproxy_s *next; } oproxy_t; typedef struct tcpconnect_s { struct tcpconnect_s *next; SOCKET sock; wsrbuf_t websocket; netadr_t peeraddr; char *initialstreamname; unsigned char inbuffer[MAX_PROXY_INBUFFER]; unsigned int inbuffersize; //amount of data available. unsigned char outbuffer[MAX_PROXY_INBUFFER]; unsigned int outbuffersize; //amount of data available. } tcpconnect_t; typedef struct { float origin[3]; unsigned short soundindex; unsigned char volume; unsigned char attenuation; } staticsound_t; typedef struct bsp_s bsp_t; typedef struct { entity_state_t baseline; // entity_state_t current; // entity_state_t old; // unsigned int updatetime; //to stop lerping when it's an old entity (bodies, stationary grenades, ...) int leafcount; unsigned short leafs[MAX_ENTITY_LEAFS]; } entity_t; #define MAX_ENTITY_FRAMES 64 typedef struct { int oldframe; int numents; int maxents; entity_state_t *ents; //dynamically allocated unsigned short *entnums; //dynamically allocated } frame_t; typedef struct { unsigned char number; char bits[6]; } nail_t; typedef struct { float pos[3]; float angle[3]; } intermission_t; typedef enum { SRC_BAD, SRC_DEMO, SRC_DEMODIR, SRC_UDP, SRC_TCP } sourcetype_t; typedef enum { ERR_NONE, //stream is fine ERR_PAUSED, ERR_RECONNECT, //stream needs to reconnect ERR_PERMANENT, //permanent error, transitioning to disabled next frame ERR_DISABLED, //stream is disabled, can be set to reconnect by admin ERR_DROP //stream _will_ be forgotten about next frame } errorstate_t; struct sv_s { //details about a server connection (also known as stream) char connectpassword[64]; //password given to server netadr_t serveraddress; netchan_t netchan; qboolean serverquery; sourcetype_t sourcetype; //proxy chaining qboolean serverisproxy; qboolean proxyisselected; qboolean upstreamacceptschat; qboolean upstreamacceptsdownload; // qboolean parsingqtvheader; unsigned char upstreambuffer[2048]; int upstreambuffersize; unsigned int parsetime; unsigned int parsespeed; FILE *downloadfile; char downloadname[256]; char status[64]; qboolean silentstream; qboolean usequakeworldprotocols; unsigned int pext1; unsigned int pext2; unsigned int pexte; int challenge; unsigned short qport; int isconnected; int clservercount; unsigned int nextsendpings; unsigned int timeout; unsigned int packetratelimiter; viewer_t *controller; int controllersquencebias; qboolean proxyplayer; //a player is actually playing on the proxy. usercmd_t proxyplayerucmds[3]; int proxyplayerucmdnum; int proxyplayerbuttons; float proxyplayerangles[3]; float proxyplayerimpulse; qboolean maysend; FILE *sourcefile; unsigned int filelength; SOCKET sourcesock; int last_random_number; // for demo directories randomizing stuff // SOCKET tcpsocket; //tcp + mvd protocol // int tcplistenportnum; oproxy_t *proxies; qboolean parsingconnectiondata; //so reject any new connects for now unsigned int mapstarttime; unsigned int physicstime; //the last time all the ents moved. unsigned int simtime; unsigned int curtime; unsigned int oldpackettime; unsigned int nextpackettime; unsigned int nextconnectattempt; errorstate_t errored; enum autodisconnect_e { AD_NO, AD_WHENEMPTY, AD_REVERSECONNECT, AD_STATUSPOLL } autodisconnect; unsigned int numviewers; cluster_t *cluster; sv_t *next; //next proxy->server connection #ifdef COMMENTARY //audio stuff soundcapt_t *comentrycapture; #endif //options: char server[MAX_QPATH]; int streamid; struct mapstate_s { //this structure is freed+memset in QTV_CleanupMap bsp_t *bsp; int numinlines; nail_t nails[32]; int nailcount; char gamedir[MAX_QPATH]; char mapname[256]; //world.message movevars_t movevars; int cdtrack; entity_t entity[MAX_ENTITIES]; frame_t frame[MAX_ENTITY_FRAMES]; // int maxents; staticsound_t staticsound[MAX_STATICSOUNDS]; int staticsound_count; entity_state_t spawnstatic[MAX_STATICENTITIES]; int spawnstatic_count; filename_t lightstyle[MAX_LIGHTSTYLES]; char serverinfo[MAX_SERVERINFO_STRING]; char hostname[MAX_QPATH]; playerinfo_t players[MAX_CLIENTS]; filename_t modellist[MAX_MODELS]; filename_t soundlist[MAX_SOUNDS]; int modelindex_spike; // qw is wierd. int modelindex_player; // qw is wierd. int trackplayer; int thisplayer; qboolean ispaused; } map; unsigned char buffer[MAX_PROXY_BUFFER]; //this doesn't cycle. int buffersize; //it memmoves down int forwardpoint; //the point in the stream that we've forwarded up to. }; typedef struct { char name[128]; int size; int time, smalltime; } availdemo_t; enum { SG_IPV4, SG_IPV6, SG_UNIX, SOCKETGROUPS }; typedef struct turnclient_s turnclient_t; struct cluster_s { SOCKET qwdsocket[SOCKETGROUPS]; //udp + quakeworld protocols SOCKET tcpsocket[SOCKETGROUPS]; //tcp listening socket (for mvd and listings and stuff) tcpconnect_t *tcpconnects; //'tcpconnect' qizmo-compatible quakeworld-over-tcp connection char commandinput[512]; int inputlength; unsigned int mastersendtime; unsigned int mastersequence; unsigned int curtime; #ifdef HAVE_EPOLL int epfd; #endif unsigned int numrelays; turnclient_t *turns; char chalkey[64]; //to identify the master properly. probably kinda pointless. base64 encoded. unsigned char turnkey[32]; //raw key shared with broker to prove TURN identity was given by broker. NOTE: we are not verifying each, so we depend on clockskew to prevent any longterm abuse. there's no accounts anywhere though so anyone can get a key if they ask properly. qboolean turnenabled; unsigned short turn_minport, turn_maxport; //set to 0 to let the OS decide. char *protocolname; int protocolver; unsigned char turn_ipv4[4]; unsigned char turn_ipv6[16]; unsigned int numpeers; struct relaypeer_s *relaypeer; unsigned int relay_lastping; unsigned int relay_lastquery; qboolean relayenabled; qboolean pingtreeenabled; viewer_t *viewers; int numviewers; sv_t *servers; int numservers; int nextstreamid; int nextuserid; sv_t *viewserver; //options char autojoinadr[128]; //new clients automatically .join this server int qwlistenportnum; int tcplistenportnum; char adminpassword[256];//password required for rcon etc char qtvpassword[256]; //password required to connect a proxy char hostname[256]; char master[MAX_QPATH]; char demodir[MAX_QPATH]; char downloaddir[MAX_QPATH]; //must be slash terminated, or empty. char plugindatasource[256]; //sued by the http server for use with npfte char mapsource[256]; //sued by the http server for use with npfte qboolean chokeonnotupdated; qboolean lateforward; qboolean notalking; qboolean nobsp; qboolean allownqclients; //nq clients require no challenge qboolean nouserconnects; //prohibit users from connecting to new streams. qboolean reverseallowed; //demos can be submitted from servers via 'qtvreverse' without needing to keep idle connections live. int anticheattime; //intial connection buffer delay (set high to block specing enemies) int tooslowdelay; //if stream ran out of data, stop parsing for this long int maxviewers; int numproxies; int maxproxies; qboolean wanttoexit; oproxy_t *pendingproxies; availdemo_t availdemos[2048]; int availdemoscount; }; #define MENU_NONE 0 #define MENU_MAIN 1//MENU_SERVERS #define MENU_SERVERS 2 #define MENU_CLIENTS 3 #define MENU_ADMIN 4 #define MENU_ADMINSERVER 5 #define MENU_DEMOS 6 #define MENU_FORWARDING 7 #define MENU_SERVERBROWSER 8 #define MENU_HELP 9 #define MENU_DEFAULT MENU_MAIN enum { MENU_MAIN_STREAMS, MENU_MAIN_NEWSTREAM, MENU_MAIN_SERVERBROWSER, MENU_MAIN_PREVPROX, MENU_MAIN_HELP, MENU_MAIN_CLIENTLIST, MENU_MAIN_DEMOS, MENU_MAIN_ADMIN, MENU_MAIN_NEXTPROX, MENU_MAIN_ITEMCOUNT }; unsigned char ReadByte(netmsg_t *b); unsigned short ReadShort(netmsg_t *b); unsigned short ReadBigShort(netmsg_t *b); unsigned int ReadLong(netmsg_t *b); unsigned int ReadBigLong(netmsg_t *b); float ReadFloat(netmsg_t *b); void ReadString(netmsg_t *b, char *string, int maxlen); float ReadCoord(netmsg_t *b, unsigned int pext); float ReadAngle(netmsg_t *b, unsigned int pext); unsigned int SwapLong(unsigned int val); unsigned int BigLong(unsigned int val); //flags for where a message can be sent, for easy broadcasting #define Q1 (NQ|QW) #define QW 1 #define NQ 2 #define CONNECTING 4 #include "cmd.h" void InitNetMsg(netmsg_t *b, void *buffer, int bufferlength); unsigned char ReadByte(netmsg_t *b); unsigned short ReadShort(netmsg_t *b); unsigned int ReadLong(netmsg_t *b); float ReadFloat(netmsg_t *b); void ReadString(netmsg_t *b, char *string, int maxlen); void WriteByte(netmsg_t *b, unsigned char c); void WriteShort(netmsg_t *b, unsigned short l); void WriteBigShort(netmsg_t *b, unsigned short l); void WriteLong(netmsg_t *b, unsigned int l); void WriteBigLong(netmsg_t *b, unsigned int l); void WriteFloat(netmsg_t *b, float f); void WriteCoord(netmsg_t *b, float c, unsigned int pext); void WriteAngle(netmsg_t *b, float a, unsigned int pext); void WriteString2(netmsg_t *b, const char *str); void WriteString(netmsg_t *b, const char *str); void WriteData(netmsg_t *b, const void *data, int length); void Multicast(sv_t *tv, void *buffer, int length, int to, unsigned int playermask,int suitablefor); void Broadcast(cluster_t *cluster, void *buffer, int length, int suitablefor); void ParseMessage(sv_t *tv, void *buffer, int length, int to, int mask); void BuildServerData(sv_t *tv, netmsg_t *msg, int servercount, viewer_t *spectatorflag); void BuildNQServerData(sv_t *tv, netmsg_t *msg, qboolean mvd, int servercount); void QW_UpdateUDPStuff(cluster_t *qtv); void QW_TCPConnection(cluster_t *cluster, oproxy_t *sock, char *initialstringname/*strduped*/); unsigned int Sys_Milliseconds(void); void Prox_SendInitialEnts(sv_t *qtv, oproxy_t *prox, netmsg_t *msg); qboolean QTV_ConnectStream(sv_t *qtv, char *serverurl); void QTV_ShutdownStream(sv_t *qtv); qboolean NET_StringToAddr (char *s, netadr_t *sadr, int defaultport); void QTV_Printf(sv_t *qtv, char *format, ...) PRINTFWARNING(2); void QTV_UpdatedServerInfo(sv_t *tv); void QTV_CleanupMap(sv_t *qtv); void SendBufferToViewer(viewer_t *v, const char *buffer, int length, qboolean reliable); void QW_PrintfToViewer(viewer_t *v, char *format, ...) PRINTFWARNING(2); void QW_StuffcmdToViewer(viewer_t *v, char *format, ...) PRINTFWARNING(2); void QW_StreamPrint(cluster_t *cluster, sv_t *server, viewer_t *allbut, char *message); void QW_StreamStuffcmd(cluster_t *cluster, sv_t *server, char *fmt, ...) PRINTFWARNING(3); void QTV_SayCommand(cluster_t *cluster, sv_t *qtv, viewer_t *v, char *fullcommand); //execute a command from a view void QW_SetViewersServer(cluster_t *cluster, viewer_t *viewer, sv_t *sv); void QW_SetMenu(viewer_t *v, int menunum); void QW_SetCommentator(cluster_t *cluster, viewer_t *v, viewer_t *commentator); void QW_FreeViewer(cluster_t *cluster, viewer_t *viewer); void QW_ClearViewerState(viewer_t *viewer); void PM_PlayerMove (pmove_t *pmove); void Netchan_Setup (SOCKET sock, netchan_t *chan, netadr_t adr, int qport, qboolean isclient); void Netchan_OutOfBandPrint (cluster_t *cluster, netadr_t adr, char *format, ...) PRINTFWARNING(3); //int Netchan_IsLocal (netadr_t adr); void NET_InitUDPSocket(cluster_t *cluster, int port, int socketid); void NET_SendPacket(cluster_t *cluster, SOCKET sock, int length, void *data, netadr_t adr); SOCKET NET_ChooseSocket(SOCKET sock[SOCKETGROUPS], netadr_t *toadr, netadr_t in); qboolean Net_CompareAddress(netadr_t *s1, netadr_t *s2, int qp1, int qp2); qboolean Netchan_Process (netchan_t *chan, netmsg_t *msg); qboolean NQNetchan_Process(cluster_t *cluster, netchan_t *chan, netmsg_t *msg); void Netchan_Transmit (cluster_t *cluster, netchan_t *chan, int length, const void *data); void Netchan_OutOfBandSocket (cluster_t *cluster, SOCKET sock, netadr_t *adr, int length, void *data); void Netchan_OutOfBand(cluster_t *cluster, netadr_t adr, int length, void *data); qboolean Netchan_CanPacket (netchan_t *chan); int NET_WebSocketRecv(SOCKET sock, wsrbuf_t *ws, unsigned char *out, unsigned int outlen, int *wslen); int SendList(sv_t *qtv, int first, const filename_t *list, int svc, netmsg_t *msg); int Prespawn(sv_t *qtv, int curmsgsize, netmsg_t *msg, int bufnum, int thisplayer); bsp_t *BSP_LoadModel(cluster_t *cluster, char *gamedir, char *bspname); void BSP_Free(bsp_t *bsp); int BSP_LeafNum(bsp_t *bsp, float x, float y, float z); unsigned int BSP_Checksum(bsp_t *bsp); int BSP_SphereLeafNums(bsp_t *bsp, int maxleafs, unsigned short *list, float x, float y, float z, float radius); qboolean BSP_Visible(bsp_t *bsp, int leafcount, unsigned short *list); void BSP_SetupForPosition(bsp_t *bsp, float x, float y, float z); const intermission_t *BSP_IntermissionSpot(bsp_t *bsp); unsigned short QCRC_Block (void *start, int count); unsigned short QCRC_Value(unsigned short crcvalue); void WriteDeltaUsercmd (netmsg_t *m, const usercmd_t *from, usercmd_t *move); void SendClientCommand(sv_t *qtv, char *fmt, ...) PRINTFWARNING(2); void QTV_Run(sv_t *qtv); char *COM_ParseToken (char *data, char *out, int outsize, const char *punctuation); char *Info_ValueForKey (char *s, const char *key, char *buffer, int buffersize); void Info_SetValueForStarKey (char *s, const char *key, const char *value, int maxsize); void ReadDeltaUsercmd (netmsg_t *m, const usercmd_t *from, usercmd_t *move); unsigned Com_BlockChecksum (void *buffer, int length); void Com_BlockFullChecksum (void *buffer, int len, unsigned char *outbuf); void Cluster_BuildAvailableDemoList(cluster_t *cluster); void Sys_Printf(cluster_t *cluster, char *fmt, ...) PRINTFWARNING(2); //void Sys_mkdir(char *path); void QTV_mkdir(char *path); void Net_ProxySend(cluster_t *cluster, oproxy_t *prox, void *buffer, int length); oproxy_t *Net_FileProxy(sv_t *qtv, char *filename); sv_t *QTV_NewServerConnection(cluster_t *cluster, int streamid, char *server, char *password, qboolean force, enum autodisconnect_e autodisconnect, qboolean noduplicates, qboolean query); void Net_TCPListen(cluster_t *cluster, int port, int socketid); qboolean Net_StopFileProxy(sv_t *qtv); void SV_FindProxies(SOCKET sock, cluster_t *cluster, sv_t *defaultqtv); qboolean SV_ReadPendingProxy(cluster_t *cluster, oproxy_t *pend); void SV_ForwardStream(sv_t *qtv, void *buffer, int length); int SV_SayToUpstream(sv_t *qtv, char *message); void SV_SayToViewers(sv_t *qtv, char *message); unsigned char *FS_ReadFile(char *gamedir, char *filename, unsigned int *size); void ChooseFavoriteTrack(sv_t *tv); void DemoViewer_Update(sv_t *svtest); void Fwd_SayToDownstream(sv_t *qtv, char *message); //httpsv.c char *HTTPSV_GetMethod(cluster_t *cluster, oproxy_t *pend); //if a websocket request, return value is the stream name void HTTPSV_PostMethod(cluster_t *cluster, oproxy_t *pend, char *postdata); void tobase64(unsigned char *out, int outlen, unsigned char *in, int inlen); //menu.c void Menu_Enter(cluster_t *cluster, viewer_t *viewer, int buttonnum); void Menu_Draw(cluster_t *cluster, viewer_t *viewer); //relay.c void TURN_CheckFDs(cluster_t *cluster); void TURN_AddFDs(cluster_t *cluster, fd_set *set, int *m); qboolean TURN_IsRequest(cluster_t *cluster, netmsg_t *m, netadr_t *from); //handles both TURN/STUN packets, and relays inbound qwfwd connections too. void Fwd_NewQWFwd(cluster_t *cluster, netadr_t *from, char *targ); //creates a new qwfwd context. void TURN_RelayStatus(cmdctxt_t *ctx); void Fwd_PingStatus(cluster_t *cluster, netadr_t *from, qboolean ext); void Fwd_ParseServerList(cluster_t *cluster, netmsg_t *m, int af); void Fwd_PingResponse(cluster_t *cluster, netadr_t *from); #ifdef __cplusplus } #endif ================================================ FILE: fteqtv/qw.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ #include "qtv.h" #include #include static const filename_t ConnectionlessModelList[] = {{""}, {"maps/start.bsp"}, {"progs/player.mdl"}, {""}}; static const filename_t ConnectionlessSoundList[] = {{""}, {""}}; const entity_state_t nullentstate = {0}; void SV_WriteDelta(int entnum, const entity_state_t *from, const entity_state_t *to, netmsg_t *msg, qboolean force, unsigned int pext); const intermission_t nullstreamspot = {{544, 288, 64}, {0, 90, 0}}; void QTV_Say(cluster_t *cluster, sv_t *qtv, viewer_t *v, char *message, qboolean noupwards); void QTV_DefaultMovevars(movevars_t *vars) { vars->gravity = 800; vars->maxspeed = 320; vars->spectatormaxspeed = 500; vars->accelerate = 10; vars->airaccelerate = 0.7f; vars->waterfriction = 4; vars->entgrav = 1; vars->stopspeed = 10; vars->wateraccelerate = 10; vars->friction = 4; } const usercmd_t nullcmd = {0}; #define CM_ANGLE1 (1<<0) #define CM_ANGLE3 (1<<1) #define CM_FORWARD (1<<2) #define CM_SIDE (1<<3) #define CM_UP (1<<4) #define CM_BUTTONS (1<<5) #define CM_IMPULSE (1<<6) #define CM_ANGLE2 (1<<7) void ReadDeltaUsercmd (netmsg_t *m, const usercmd_t *from, usercmd_t *move) { int bits; memcpy (move, from, sizeof(*move)); bits = ReadByte (m); // read current angles if (bits & CM_ANGLE1) move->angles[0] = (ReadShort (m)/(float)0x10000)*360; if (bits & CM_ANGLE2) move->angles[1] = (ReadShort (m)/(float)0x10000)*360; if (bits & CM_ANGLE3) move->angles[2] = (ReadShort (m)/(float)0x10000)*360; // read movement if (bits & CM_FORWARD) move->forwardmove = ReadShort(m); if (bits & CM_SIDE) move->sidemove = ReadShort(m); if (bits & CM_UP) move->upmove = ReadShort(m); // read buttons if (bits & CM_BUTTONS) move->buttons = ReadByte (m); if (bits & CM_IMPULSE) move->impulse = ReadByte (m); // read time to run command move->msec = ReadByte (m); // always sent } void WriteDeltaUsercmd (netmsg_t *m, const usercmd_t *from, usercmd_t *move) { int bits = 0; if (move->angles[0] != from->angles[0]) bits |= CM_ANGLE1; if (move->angles[1] != from->angles[1]) bits |= CM_ANGLE2; if (move->angles[2] != from->angles[2]) bits |= CM_ANGLE3; if (move->forwardmove != from->forwardmove) bits |= CM_FORWARD; if (move->sidemove != from->sidemove) bits |= CM_SIDE; if (move->upmove != from->upmove) bits |= CM_UP; if (move->buttons != from->buttons) bits |= CM_BUTTONS; if (move->impulse != from->impulse) bits |= CM_IMPULSE; WriteByte (m, bits); // read current angles if (bits & CM_ANGLE1) WriteShort (m, (move->angles[0]/360.0)*0x10000); if (bits & CM_ANGLE2) WriteShort (m, (move->angles[1]/360.0)*0x10000); if (bits & CM_ANGLE3) WriteShort (m, (move->angles[2]/360.0)*0x10000); // read movement if (bits & CM_FORWARD) WriteShort(m, move->forwardmove); if (bits & CM_SIDE) WriteShort(m, move->sidemove); if (bits & CM_UP) WriteShort(m, move->upmove); // read buttons if (bits & CM_BUTTONS) WriteByte (m, move->buttons); if (bits & CM_IMPULSE) WriteByte (m, move->impulse); // read time to run command WriteByte (m, move->msec); // always sent } void BuildServerData(sv_t *tv, netmsg_t *msg, int servercount, viewer_t *viewer) { movevars_t movevars; WriteByte(msg, svc_serverdata); if (viewer) { //its for an actual viewer, tailor the extensions... if (viewer->pext1) { WriteLong(msg, PROTOCOL_VERSION_FTE); WriteLong(msg, viewer->pext1); } if (viewer->pext2) { WriteLong(msg, PROTOCOL_VERSION_FTE2); WriteLong(msg, viewer->pext2); } } else { //we're just forwarding, use the same extensions our source used. if (tv->pext1) { WriteLong(msg, PROTOCOL_VERSION_FTE); WriteLong(msg, tv->pext1); } if (tv->pext2) { WriteLong(msg, PROTOCOL_VERSION_FTE2); WriteLong(msg, tv->pext2); } if (tv->pexte) { WriteLong(msg, PROTOCOL_VERSION_EZQUAKE1); WriteLong(msg, tv->pexte); } } WriteLong(msg, PROTOCOL_VERSION); WriteLong(msg, servercount); if (!tv) { //dummy connection, for choosing a game to watch. WriteString(msg, "qw"); if (!viewer) WriteFloat(msg, 0); else WriteByte(msg, (MAX_CLIENTS-1) | (128)); WriteString(msg, "FTEQTV Proxy"); // get the movevars QTV_DefaultMovevars(&movevars); WriteFloat(msg, movevars.gravity); WriteFloat(msg, movevars.stopspeed); WriteFloat(msg, movevars.maxspeed); WriteFloat(msg, movevars.spectatormaxspeed); WriteFloat(msg, movevars.accelerate); WriteFloat(msg, movevars.airaccelerate); WriteFloat(msg, movevars.wateraccelerate); WriteFloat(msg, movevars.friction); WriteFloat(msg, movevars.waterfriction); WriteFloat(msg, movevars.entgrav); WriteByte(msg, svc_stufftext); WriteString2(msg, "fullserverinfo \""); WriteString2(msg, "\\*QTV\\"QTV_VERSION_STRING); WriteString(msg, "\"\n"); } else { WriteString(msg, tv->map.gamedir); if (!viewer) WriteFloat(msg, 0); else { if (tv->controller == viewer) WriteByte(msg, viewer->thisplayer); else WriteByte(msg, viewer->thisplayer | 128); } WriteString(msg, tv->map.mapname); // get the movevars WriteFloat(msg, tv->map.movevars.gravity); WriteFloat(msg, tv->map.movevars.stopspeed); WriteFloat(msg, tv->map.movevars.maxspeed); WriteFloat(msg, tv->map.movevars.spectatormaxspeed); WriteFloat(msg, tv->map.movevars.accelerate); WriteFloat(msg, tv->map.movevars.airaccelerate); WriteFloat(msg, tv->map.movevars.wateraccelerate); WriteFloat(msg, tv->map.movevars.friction); WriteFloat(msg, tv->map.movevars.waterfriction); WriteFloat(msg, tv->map.movevars.entgrav); WriteByte(msg, svc_stufftext); WriteString2(msg, "fullserverinfo \""); WriteString2(msg, tv->map.serverinfo); WriteString(msg, "\"\n"); } } void BuildNQServerData(sv_t *tv, netmsg_t *msg, qboolean mvd, int playernum) { int i; WriteByte(msg, svc_serverdata); if (tv && tv->pext1 & PEXT_FLOATCOORDS) { WriteLong(msg, 999); WriteLong(msg, (1<<1)|(1<<4)); //short angles, float coords, same as PEXT_FLOATCOORDS } else WriteLong(msg, PROTOCOL_VERSION_NQ); WriteByte(msg, 16); //MAX_CLIENTS WriteByte(msg, 1); //game type if (!tv || tv->parsingconnectiondata ) { //dummy connection, for choosing a game to watch. WriteString(msg, "FTEQTV Proxy"); //modellist for (i = 1; *ConnectionlessModelList[i].name; i++) { WriteString(msg, ConnectionlessModelList[i].name); } WriteString(msg, ""); //soundlist for (i = 1; *ConnectionlessSoundList[i].name; i++) { WriteString(msg, ConnectionlessSoundList[i].name); } WriteString(msg, ""); WriteByte(msg, svc_cdtrack); WriteByte(msg, 0); //two of them, yeah... weird, eh? WriteByte(msg, 0); WriteByte(msg, svc_nqsetview); WriteShort(msg, playernum+1); WriteByte(msg, svc_nqsignonnum); WriteByte(msg, 1); } else { //dummy connection, for choosing a game to watch. WriteString(msg, tv->map.mapname); //modellist for (i = 1; *tv->map.modellist[i].name; i++) { WriteString(msg, tv->map.modellist[i].name); } WriteString(msg, ""); //soundlist for (i = 1; *tv->map.soundlist[i].name; i++) { WriteString(msg, tv->map.soundlist[i].name); } WriteString(msg, ""); WriteByte(msg, svc_cdtrack); WriteByte(msg, tv->map.cdtrack); //two of them, yeah... weird, eh? WriteByte(msg, tv->map.cdtrack); WriteByte(msg, svc_nqsetview); WriteShort(msg, playernum+1); WriteByte(msg, svc_nqsignonnum); WriteByte(msg, 1); } } void QW_ClearViewerState(viewer_t *viewer) { memset(viewer->currentstats, 0, sizeof(viewer->currentstats)); } void SendServerData(sv_t *tv, viewer_t *viewer) { netmsg_t msg; char buffer[MAX_MSGLEN]; InitNetMsg(&msg, buffer, viewer->netchan.maxreliablelen); if (tv) { viewer->pext1 = tv->pext1; viewer->pext2 = tv->pext2; } else viewer->pext1 = viewer->pext2 = 0; if (tv && (tv->controller == viewer || !tv->controller)) viewer->thisplayer = tv->map.thisplayer; else viewer->thisplayer = viewer->netchan.isnqprotocol?15:MAX_CLIENTS-1; if (viewer->netchan.isnqprotocol) BuildNQServerData(tv, &msg, false, viewer->thisplayer); else BuildServerData(tv, &msg, viewer->servercount, viewer); SendBufferToViewer(viewer, msg.data, msg.cursize, true); viewer->thinksitsconnected = false; if (tv && (tv->controller == viewer) && !viewer->netchan.isnqprotocol) viewer->thinksitsconnected = true; QW_ClearViewerState(viewer); } void SendNQSpawnInfoToViewer(cluster_t *cluster, viewer_t *viewer, netmsg_t *msg) { char buffer[64]; int i; int colours; sv_t *tv = viewer->server; WriteByte(msg, svc_nqtime); WriteFloat(msg, (cluster->curtime - (tv?tv->mapstarttime:0))/1000.0f); if (tv) { for (i=0; imap.players[i].userinfo, "name", buffer, sizeof(buffer)); WriteString (msg, buffer); //fixme WriteByte (msg, svc_updatefrags); WriteByte (msg, i); WriteShort (msg, tv->map.players[i].frags); Info_ValueForKey(tv->map.players[i].userinfo, "bottomcolor", buffer, sizeof(buffer)); colours = atoi(buffer); Info_ValueForKey(tv->map.players[i].userinfo, "topcolor", buffer, sizeof(buffer)); colours |= atoi(buffer)*16; WriteByte (msg, svc_nqupdatecolors); WriteByte (msg, i); WriteByte (msg, colours); } } else { for (i=0; i < 16; i++) { WriteByte (msg, svc_nqupdatename); WriteByte (msg, i); WriteString (msg, ""); WriteByte (msg, svc_updatefrags); WriteByte (msg, i); WriteShort (msg, 0); WriteByte (msg, svc_nqupdatecolors); WriteByte (msg, i); WriteByte (msg, 0); } } WriteByte(msg, svc_nqsignonnum); WriteByte(msg, 3); } int SendCurrentUserinfos(sv_t *tv, int cursize, netmsg_t *msg, int i, int thisplayer) { char name[1024]; if (i < 0) return i; if (i >= MAX_CLIENTS) return i; for (; i < MAX_CLIENTS; i++) { if (i == thisplayer && (!tv || !(tv->controller || tv->proxyplayer))) { WriteByte(msg, svc_updateuserinfo); WriteByte(msg, i); WriteLong(msg, i+1); WriteString2(msg, "\\*spectator\\1\\name\\"); // Print the number of people on QTV along with the hostname if (tv && tv->map.hostname[0]) { if (tv->map.hostname[0]) snprintf(name, sizeof(name), "[%d] %s", tv->numviewers, tv->map.hostname); else snprintf(name, sizeof(name), "[%d] FTEQTV", tv->numviewers); } else snprintf(name, sizeof(name), "FTEQTV"); /* if (tv) { char tmp[MAX_QPATH]; snprintf(tmp strlcat(name, itoa(tv->numviewers), sizeof(name)); //snprintf(name, sizeof(name), "%s %d", name, tv->numviewers); } */ WriteString(msg, name); WriteByte(msg, svc_updatefrags); WriteByte(msg, i); WriteShort(msg, 9999); WriteByte(msg, svc_updateping); WriteByte(msg, i); WriteShort(msg, 0); WriteByte(msg, svc_updatepl); WriteByte(msg, i); WriteByte(msg, 0); continue; } if (!tv) continue; if (msg->cursize+cursize+strlen(tv->map.players[i].userinfo) > 768) { return i; } WriteByte(msg, svc_updateuserinfo); WriteByte(msg, i); WriteLong(msg, i+1); WriteString(msg, tv->map.players[i].userinfo); WriteByte(msg, svc_updatefrags); WriteByte(msg, i); WriteShort(msg, tv->map.players[i].frags); WriteByte(msg, svc_updateping); WriteByte(msg, i); WriteShort(msg, tv->map.players[i].ping); WriteByte(msg, svc_updatepl); WriteByte(msg, i); WriteByte(msg, tv->map.players[i].packetloss); } i++; return i; } void WriteEntityState(netmsg_t *msg, entity_state_t *es, unsigned int pext) { int i; WriteByte(msg, es->modelindex); WriteByte(msg, es->frame); WriteByte(msg, es->colormap); WriteByte(msg, es->skinnum); for (i = 0; i < 3; i++) { WriteCoord(msg, es->origin[i], pext); WriteAngle(msg, es->angles[i], pext); } } int SendCurrentBaselines(sv_t *tv, int cursize, netmsg_t *msg, int maxbuffersize, int i) { if (i < 0 || i >= MAX_ENTITIES) return i; for (; i < MAX_ENTITIES; i++) { if (msg->cursize+cursize+16 > maxbuffersize) { return i; } if (tv->map.entity[i].baseline.modelindex) { if (tv->pext1 & PEXT_SPAWNSTATIC2) { WriteByte(msg, svcfte_spawnbaseline2); SV_WriteDelta(i, &nullentstate, &tv->map.entity[i].baseline, msg, true, tv->pext1); } else { WriteByte(msg, svc_spawnbaseline); WriteShort(msg, i); WriteEntityState(msg, &tv->map.entity[i].baseline, tv->pext1); } } } return i; } int SendCurrentLightmaps(sv_t *tv, int cursize, netmsg_t *msg, int maxbuffersize, int i) { if (i < 0 || i >= MAX_LIGHTSTYLES) return i; for (; i < MAX_LIGHTSTYLES; i++) { if (msg->cursize+cursize+strlen(tv->map.lightstyle[i].name) > maxbuffersize) { return i; } WriteByte(msg, svc_lightstyle); WriteByte(msg, i); WriteString(msg, tv->map.lightstyle[i].name); } return i; } int SendStaticSounds(sv_t *tv, int cursize, netmsg_t *msg, int maxbuffersize, int i) { if (i < 0 || i >= MAX_STATICSOUNDS) return i; for (; i < MAX_STATICSOUNDS; i++) { if (msg->cursize+cursize+16 > maxbuffersize) { return i; } if (!tv->map.staticsound[i].soundindex) continue; WriteByte(msg, svc_spawnstaticsound); WriteCoord(msg, tv->map.staticsound[i].origin[0], tv->pext1); WriteCoord(msg, tv->map.staticsound[i].origin[1], tv->pext1); WriteCoord(msg, tv->map.staticsound[i].origin[2], tv->pext1); WriteByte(msg, tv->map.staticsound[i].soundindex); WriteByte(msg, tv->map.staticsound[i].volume); WriteByte(msg, tv->map.staticsound[i].attenuation); } return i; } int SendStaticEntities(sv_t *tv, int cursize, netmsg_t *msg, int maxbuffersize, int i) { if (i < 0 || i >= MAX_STATICENTITIES) return i; for (; i < MAX_STATICENTITIES; i++) { if (msg->cursize+cursize+16 > maxbuffersize) { return i; } if (!tv->map.spawnstatic[i].modelindex) continue; if (tv->pext1 & PEXT_SPAWNSTATIC2) { WriteByte(msg, svcfte_spawnstatic2); SV_WriteDelta(i, &nullentstate, &tv->map.spawnstatic[i], msg, true, tv->pext1); } else { WriteByte(msg, svc_spawnstatic); WriteEntityState(msg, &tv->map.spawnstatic[i], tv->pext1); } } return i; } int SendList(sv_t *qtv, int first, const filename_t *list, int svc, netmsg_t *msg) { int i; WriteByte(msg, svc); WriteByte(msg, first); for (i = first+1; i < 256; i++) { // printf("write %i: %s\n", i, list[i].name); WriteString(msg, list[i].name); if (!*list[i].name) //fixme: this probably needs testing for where we are close to the limit { //no more WriteByte(msg, 0); return -1; } if (msg->cursize > 768) { //truncate i--; break; } } WriteByte(msg, 0); WriteByte(msg, i); return i; } void QW_StreamPrint(cluster_t *cluster, sv_t *server, viewer_t *allbut, char *message) { viewer_t *v; for (v = cluster->viewers; v; v = v->next) { if (v->server == server) { if (v == allbut) continue; QW_PrintfToViewer(v, "%s", message); } } } void QW_StreamStuffcmd(cluster_t *cluster, sv_t *server, char *fmt, ...) { viewer_t *v; va_list argptr; char buf[1024]; char cmd[512]; netmsg_t msg; va_start (argptr, fmt); vsnprintf (cmd, sizeof(cmd), fmt, argptr); va_end (argptr); InitNetMsg(&msg, buf, sizeof(buf)); WriteByte(&msg, svc_stufftext); WriteString(&msg, cmd); for (v = cluster->viewers; v; v = v->next) { if (v->server == server) { SendBufferToViewer(v, msg.data, msg.cursize, true); } } } void QW_SetViewersServer(cluster_t *cluster, viewer_t *viewer, sv_t *sv) { char buffer[1024]; sv_t *oldserver; oldserver = viewer->server; if (viewer->server) viewer->server->numviewers--; viewer->server = sv; if (viewer->server) viewer->server->numviewers++; if (!sv || !sv->parsingconnectiondata) { if (sv != oldserver) QW_StuffcmdToViewer(viewer, "cmd new\n"); viewer->thinksitsconnected = false; } viewer->servercount++; viewer->origin[0] = 0; viewer->origin[1] = 0; viewer->origin[2] = 0; if (sv != oldserver) { if (sv && oldserver) { snprintf(buffer, sizeof(buffer), "%cQTV%c%s leaves to watch %s (%i)\n", 91+128, 93+128, viewer->name, *sv->map.hostname?sv->map.hostname:sv->server, sv->streamid); QW_StreamPrint(cluster, oldserver, viewer, buffer); } snprintf(buffer, sizeof(buffer), "%cQTV%c%s joins the stream\n", 91+128, 93+128, viewer->name); QW_StreamPrint(cluster, sv, viewer, buffer); } } //fixme: will these want to have state?.. int NewChallenge(cluster_t *cluster, netadr_t *addr) { unsigned int r = 0, l; unsigned char *digest; void *ctx; hashfunc_t *func = &hash_sha1; static time_t t; //reminder: Challenges exist so clients can't spoof their source address and waste our ram without us being able to ban them without banning everyone. size_t sz = 0; if (((struct sockaddr*)addr->sockaddr)->sa_family == AF_INET) sz = sizeof(struct sockaddr_in); else if (((struct sockaddr*)addr->sockaddr)->sa_family == AF_INET6) sz = sizeof(struct sockaddr_in6); //else error ctx = alloca(func->contextsize); func->init(ctx); if (!t) //must be constant, so only do this if its still 0. t = time(NULL); func->process(ctx, addr, sz); //hash their address primarily. func->process(ctx, cluster->turnkey, sizeof(cluster->turnkey)); //might not be set... func->process(ctx, &t, sizeof(t)); //extra privacy, sizeof doesn't matter as its only our process that cares //func->process(ctx, cluster, sizeof(cluster)); //a random pointer too, because zomgwtf digest = alloca(func->digestsize); func->terminate(digest, ctx); for (l = 0; l < func->digestsize; l++) r ^= digest[l]<<((l%sizeof(r))*8); return r; } qboolean ChallengePasses(cluster_t *cluster, netadr_t *addr, int challenge) { if (challenge == NewChallenge(cluster, addr)) return true; return false; } void NewClient(cluster_t *cluster, viewer_t *viewer) { sv_t *initialserver; initialserver = NULL; if (viewer->netchan.remote_address.tcpcon) { tcpconnect_t *dest; for (dest = cluster->tcpconnects; dest; dest = dest->next) { if (dest == viewer->netchan.remote_address.tcpcon) break; } if (*dest->initialstreamname) { initialserver = QTV_NewServerConnection(cluster, 0, dest->initialstreamname, "", false, AD_WHENEMPTY, true, false); if (initialserver && initialserver->sourcetype == SRC_UDP) initialserver->controller = viewer; } } if (initialserver) ; //already picked it via websocket resources. else if (*cluster->autojoinadr) { initialserver = QTV_NewServerConnection(cluster, 0, cluster->autojoinadr, "", false, AD_WHENEMPTY, true, false); if (initialserver && initialserver->sourcetype == SRC_UDP) initialserver->controller = viewer; } else if (cluster->nouserconnects && cluster->numservers == 1) { initialserver = cluster->servers; if (!initialserver->map.modellist[1].name[0]) initialserver = NULL; //damn, that server isn't ready } QW_SetViewersServer(cluster, viewer, initialserver); viewer->userid = ++cluster->nextuserid; viewer->timeout = cluster->curtime + 15*1000; viewer->trackplayer = -1; viewer->menunum = -1; QW_SetMenu(viewer, MENU_NONE); #ifndef LIBQTV QW_PrintfToViewer(viewer, "Welcome to FTEQTV %s\n", QTV_VERSION_STRING); QW_StuffcmdToViewer(viewer, "alias admin \"cmd admin\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:up\" \"say proxy:menu up\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:down\" \"say proxy:menu down\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:right\" \"say proxy:menu right\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:left\" \"say proxy:menu left\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:select\" \"say proxy:menu select\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:home\" \"say proxy:menu home\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:end\" \"say proxy:menu end\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:menu\" \"say proxy:menu\"\n"); QW_StuffcmdToViewer(viewer, "alias \"proxy:backspace\" \"say proxy:menu backspace\"\n"); QW_StuffcmdToViewer(viewer, "alias \".help\" \"say .help\"\n"); QW_StuffcmdToViewer(viewer, "alias \".disconnect\" \"say .disconnect\"\n"); QW_StuffcmdToViewer(viewer, "alias \".menu\" \"say .menu\"\n"); QW_StuffcmdToViewer(viewer, "alias \".admin\" \"say .admin\"\n"); QW_StuffcmdToViewer(viewer, "alias \".reset\" \"say .reset\"\n"); QW_StuffcmdToViewer(viewer, "alias \".clients\" \"say .clients\"\n"); // QW_StuffcmdToViewer(viewer, "alias \".qtv\" \"say .qtv\"\n"); // QW_StuffcmdToViewer(viewer, "alias \".join\" \"say .join\"\n"); // QW_StuffcmdToViewer(viewer, "alias \".observe\" \"say .observe\"\n"); QW_PrintfToViewer(viewer, "Type admin for the admin menu\n"); #endif } void ParseUserInfo(cluster_t *cluster, viewer_t *viewer) { char buf[1024]; float rate; char temp[64]; viewer->isproxy = false; Info_ValueForKey(viewer->userinfo, "*qtv", temp, sizeof(temp)); if (*temp) viewer->isproxy = true; Info_ValueForKey(viewer->userinfo, "Qizmo", temp, sizeof(temp)); if (*temp) viewer->isproxy = true; Info_ValueForKey(viewer->userinfo, "name", temp, sizeof(temp)); if (!*temp) strcpy(temp, "unnamed"); if (!*viewer->name) Sys_Printf(cluster, "Viewer %s connected\n", temp); if (strcmp(viewer->name, temp)) { if (*viewer->name) { snprintf(buf, sizeof(buf), "%cQTV%c%s changed name to %cQTV%c%s\n", 91+128, 93+128, viewer->name, 91+128, 93+128, temp ); } else { snprintf(buf, sizeof(buf), "%cQTV%c%s joins the stream\n", 91+128, 93+128, temp ); } if (!viewer->server || viewer->server->controller != viewer) QW_StreamPrint(cluster, viewer->server, NULL, buf); } strlcpy(viewer->name, temp, sizeof(viewer->name)); Info_ValueForKey(viewer->userinfo, "rate", temp, sizeof(temp)); rate = atof(temp); if (!rate) rate = 2500; if (rate < 250) rate = 250; if (rate > 10000) rate = 10000; viewer->netchan.rate = 1000.0f / rate; } void NewNQClient(cluster_t *cluster, netadr_t *addr) { // sv_t *initialserver; int header; int len; int i; unsigned char buffer[64]; viewer_t *viewer = NULL; if (cluster->numviewers >= cluster->maxviewers && cluster->maxviewers) { buffer[4] = CCREP_REJECT; strcpy((char*)buffer+5, "Sorry, proxy is full.\n"); len = strlen((char*)buffer+5)+5; } /* else { buffer[4] = CCREP_REJECT; strcpy((char*)buffer+5, "NQ not supported yet\n"); len = strlen((char*)buffer+5)+5; }*/ else if (!(viewer = malloc(sizeof(viewer_t)))) { buffer[4] = CCREP_REJECT; strcpy((char*)buffer+5, "Out of memory\n"); len = strlen((char*)buffer+5)+5; } else { buffer[4] = CCREP_ACCEPT; buffer[5] = (cluster->qwlistenportnum&0x00ff)>>0; buffer[6] = (cluster->qwlistenportnum&0xff00)>>8; buffer[7] = 0; buffer[8] = 0; len = 4+1+4; } *(int*)buffer = NETFLAG_CTL | len; header = (buffer[0]<<24) + (buffer[1]<<16) + (buffer[2]<<8) + buffer[3]; *(int*)buffer = header; NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, addr, *addr), len, buffer, *addr); if (!viewer) return; memset(viewer, 0, sizeof(*viewer)); Netchan_Setup (NET_ChooseSocket(cluster->qwdsocket, addr, *addr), &viewer->netchan, *addr, 0, false); viewer->netchan.isnqprotocol = true; viewer->netchan.maxdatagramlen = MAX_NQDATAGRAM; viewer->netchan.maxreliablelen = MAX_NQMSGLEN; viewer->firstconnect = true; viewer->next = cluster->viewers; cluster->viewers = viewer; for (i = 0; i < ENTITY_FRAMES; i++) viewer->delta_frames[i] = -1; cluster->numviewers++; sprintf(viewer->userinfo, "\\name\\%s", "unnamed"); ParseUserInfo(cluster, viewer); NewClient(cluster, viewer); if (!viewer->server) QW_StuffcmdToViewer(viewer, "cmd new\n"); } void NewQWClient(cluster_t *cluster, netadr_t *addr, char *connectmessage) { // sv_t *initialserver; viewer_t *viewer; char qport[32]; char challenge[32]; char infostring[1024]; char prx[256]; int i; connectmessage+=11; connectmessage = COM_ParseToken(connectmessage, qport, sizeof(qport), ""); connectmessage = COM_ParseToken(connectmessage, challenge, sizeof(challenge), ""); connectmessage = COM_ParseToken(connectmessage, infostring, sizeof(infostring), ""); if (!ChallengePasses(cluster, addr, atoi(challenge))) { Netchan_OutOfBandPrint(cluster, *addr, "n" "Bad challenge"); return; } Info_ValueForKey(infostring, "prx", prx,sizeof(prx)); if (*prx) { Fwd_NewQWFwd(cluster, addr, prx); return; } viewer = malloc(sizeof(viewer_t)); if (!viewer) { Netchan_OutOfBandPrint(cluster, *addr, "n" "Out of memory"); return; } memset(viewer, 0, sizeof(viewer_t)); Netchan_Setup (NET_ChooseSocket(cluster->qwdsocket, addr, *addr), &viewer->netchan, *addr, atoi(qport), false); viewer->netchan.message.maxsize = MAX_QWMSGLEN; viewer->netchan.maxdatagramlen = MAX_QWMSGLEN; viewer->netchan.maxreliablelen = MAX_QWMSGLEN; viewer->firstconnect = true; viewer->next = cluster->viewers; cluster->viewers = viewer; for (i = 0; i < ENTITY_FRAMES; i++) viewer->delta_frames[i] = -1; cluster->numviewers++; strlcpy(viewer->userinfo, infostring, sizeof(viewer->userinfo)); ParseUserInfo(cluster, viewer); Netchan_OutOfBandPrint(cluster, *addr, "j"); NewClient(cluster, viewer); } void QW_SetMenu(viewer_t *v, int menunum) { if ((v->menunum==MENU_NONE) != (menunum==MENU_NONE)) { if (v->isproxy) { if (menunum != MENU_NONE) QW_StuffcmdToViewer(v, "//set prox_inmenu 1\n"); else QW_StuffcmdToViewer(v, "//set prox_inmenu 0\n"); } else { if (menunum != MENU_NONE) { QW_StuffcmdToViewer(v, "//set prox_inmenu 1\n"); QW_StuffcmdToViewer(v, "alias \"+proxjump\" \"say proxy:menu enter\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxfwd\" \"say proxy:menu up\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxback\" \"say proxy:menu down\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxleft\" \"say proxy:menu left\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxright\" \"say proxy:menu right\"\n"); QW_StuffcmdToViewer(v, "alias \"-proxjump\" \" \"\n"); QW_StuffcmdToViewer(v, "alias \"-proxfwd\" \" \"\n"); QW_StuffcmdToViewer(v, "alias \"-proxback\" \" \"\n"); QW_StuffcmdToViewer(v, "alias \"-proxleft\" \" \"\n"); QW_StuffcmdToViewer(v, "alias \"-proxright\" \" \"\n"); } else { QW_StuffcmdToViewer(v, "//set prox_inmenu 0\n"); QW_StuffcmdToViewer(v, "alias \"+proxjump\" \"+jump\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxfwd\" \"+forward\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxback\" \"+back\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxleft\" \"+moveleft\"\n"); QW_StuffcmdToViewer(v, "alias \"+proxright\" \"+moveright\"\n"); QW_StuffcmdToViewer(v, "alias \"-proxjump\" \"-jump\"\n"); QW_StuffcmdToViewer(v, "alias \"-proxfwd\" \"-forward\"\n"); QW_StuffcmdToViewer(v, "alias \"-proxback\" \"-back\"\n"); QW_StuffcmdToViewer(v, "alias \"-proxleft\" \"-moveleft\"\n"); QW_StuffcmdToViewer(v, "alias \"-proxright\" \"-moveright\"\n"); } QW_StuffcmdToViewer(v, "-forward\n"); QW_StuffcmdToViewer(v, "-back\n"); QW_StuffcmdToViewer(v, "-moveleft\n"); QW_StuffcmdToViewer(v, "-moveright\n"); } } if (v->server && v->server->controller == v) { if (menunum==MENU_NONE || menunum==MENU_FORWARDING) v->server->proxyisselected = false; else v->server->proxyisselected = true; } v->menunum = menunum; v->menuop = 0; v->menuspamtime = 0; } void QTV_Rcon(cluster_t *cluster, char *message, netadr_t *from) { char buffer[8192]; char *command; int passlen; if (!*cluster->adminpassword) { Netchan_OutOfBandPrint(cluster, *from, "n" "Bad rcon_password.\n"); return; } while(*message > '\0' && *message <= ' ') message++; command = strchr(message, ' '); passlen = command-message; if (passlen != strlen(cluster->adminpassword) || strncmp(message, cluster->adminpassword, passlen)) { Netchan_OutOfBandPrint(cluster, *from, "n" "Bad rcon_password.\n"); return; } Netchan_OutOfBandPrint(cluster, *from, "n%s", Rcon_Command(cluster, NULL, command, buffer, sizeof(buffer), false)); } void QTV_Status(cluster_t *cluster, netadr_t *from) { int i; char buffer[8192]; sv_t *sv; netmsg_t msg; char elem[256]; InitNetMsg(&msg, buffer, sizeof(buffer)); WriteLong(&msg, -1); WriteByte(&msg, 'n'); WriteString2(&msg, "\\*QTV\\"); WriteString2(&msg, QTV_VERSION_STRING); if (cluster->numservers==1) { //show this server's info sv = cluster->servers; WriteString2(&msg, sv->map.serverinfo); WriteString2(&msg, "\n"); for (i = 0;i < MAX_CLIENTS; i++) { if (i == sv->map.thisplayer) continue; if (!sv->map.players[i].active) continue; //userid sprintf(elem, "%i", i); WriteString2(&msg, elem); WriteString2(&msg, " "); //frags sprintf(elem, "%i", sv->map.players[i].frags); WriteString2(&msg, elem); WriteString2(&msg, " "); //time (minuites) sprintf(elem, "%i", 0); WriteString2(&msg, elem); WriteString2(&msg, " "); //ping sprintf(elem, "%i", sv->map.players[i].ping); WriteString2(&msg, elem); WriteString2(&msg, " "); //name Info_ValueForKey(sv->map.players[i].userinfo, "name", elem, sizeof(elem)); WriteString2(&msg, "\""); WriteString2(&msg, elem); WriteString2(&msg, "\" "); //skin Info_ValueForKey(sv->map.players[i].userinfo, "skin", elem, sizeof(elem)); WriteString2(&msg, "\""); WriteString2(&msg, elem); WriteString2(&msg, "\" "); WriteString2(&msg, " "); //tc Info_ValueForKey(sv->map.players[i].userinfo, "topcolor", elem, sizeof(elem)); WriteString2(&msg, elem); WriteString2(&msg, " "); //bc Info_ValueForKey(sv->map.players[i].userinfo, "bottomcolor", elem, sizeof(elem)); WriteString2(&msg, elem); WriteString2(&msg, " "); WriteString2(&msg, "\n"); } } else { WriteString2(&msg, "\\hostname\\"); WriteString2(&msg, cluster->hostname); for (sv = cluster->servers, i = 0; sv; sv = sv->next, i++) { sprintf(elem, "\\%i\\", sv->streamid); WriteString2(&msg, elem); WriteString2(&msg, sv->server); // sprintf(elem, " (%s)", sv->serveraddress); // WriteString2(&msg, elem); } WriteString2(&msg, "\n"); } WriteByte(&msg, 0); NET_SendPacket(cluster, NET_ChooseSocket(cluster->qwdsocket, from, *from), msg.cursize, msg.data, *from); } static void QTV_GetInfo(cluster_t *cluster, netadr_t *from, char *args) { //ftemaster support char challenge[256], tmp[64]; char protocolname[MAX_QPATH]; char buffer[8192]; netmsg_t msg; qboolean authed = false; InitNetMsg(&msg, buffer, sizeof(buffer)); args = COM_ParseToken(args, challenge, sizeof(challenge), ""); while((args = COM_ParseToken(args, tmp, sizeof(tmp), ""))) { if (!strncmp(tmp, "c=",2) && !strcmp(tmp+2, cluster->chalkey)) authed = true; //they're able to read our outgoing packets. assume not intercepted (at least blocks spoofed packets). should really use (d)tls. this is more to protect our resources than anything else though, so doesn't need to be strong. else if (!strncmp(tmp, "a=",2) && authed) { netadr_t adr; if (NET_StringToAddr(tmp+2, &adr, 0)) { //master told us our IP. we can use that to report to turn clients if (((struct sockaddr*)&adr.sockaddr)->sa_family == AF_INET) memcpy(cluster->turn_ipv4, &((struct sockaddr_in*)&adr.sockaddr)->sin_addr, 4); else if (((struct sockaddr*)&adr.sockaddr)->sa_family == AF_INET6) memcpy(cluster->turn_ipv6, &((struct sockaddr_in6*)&adr.sockaddr)->sin6_addr, 16); } } } COM_ParseToken(cluster->protocolname?cluster->protocolname:"FTE-Quake", protocolname, sizeof(protocolname), ""); //we can only report one, so report the first. //response packet header WriteLong(&msg, ~0u); // if (fullstatus) // WriteString2(&msg, "statusResponse\n"); // else WriteString2(&msg, "infoResponse\n"); //first line contains the serverinfo, or some form of it WriteString2(&msg, "\\*QTV\\"); WriteString2(&msg, QTV_VERSION_STRING); // WriteString2(&msg, "\\*fp\\"); WriteString2(&msg, hash(cert)); if (authed) { //only reported to the master server to generate time-based auth tokens. tobase64(tmp,sizeof(tmp), cluster->turnkey, sizeof(cluster->turnkey)); WriteString2(&msg, "\\_turnkey\\"); WriteString2(&msg, tmp); } WriteString2(&msg, "\\challenge\\"); WriteString2(&msg, challenge); WriteString2(&msg, "\\gamename\\"); WriteString2(&msg, protocolname); snprintf(tmp, sizeof(tmp), "%i%s", cluster->protocolname?cluster->protocolver:3, "t"); //'w':quakeworld, 'n'/'d':netquake, 'x':qe, 't':qtv, 'r':turnrelay, 'f':fwd WriteString2(&msg, "\\protocol\\"); WriteString2(&msg, tmp); WriteString2(&msg, "\\clients\\"); WriteString2(&msg, "0"); WriteString2(&msg, "\\sv_maxclients\\"); WriteString2(&msg, "0"); WriteString2(&msg, "\\modname\\"); WriteString2(&msg, "QTV"); WriteString2(&msg, "\\mapname\\"); WriteString2(&msg, "QTV"); WriteString2(&msg, "\\hostname\\"); WriteString2(&msg, cluster->hostname); snprintf(tmp, sizeof(tmp), "%i", cluster->tcplistenportnum); WriteString2(&msg, "\\sv_port_tcp\\"); WriteString2(&msg, tmp); /*if (fullstatus) { client_t *cl; char *start = resp; if (resp != response+sizeof(response)) { resp[-1] = '\n'; //replace the null terminator that we already wrote //on the following lines we have an entry for each client for (i=0 ; istate == cs_connected || cl->state == cs_spawned || cl->name[0]) && !cl->spectator) { Q_strncpyz(resp, va( "%d %d \"%s\" \"%s\"\n" , cl->old_frags, SV_CalcPing(cl, false), cl->team, cl->name ), sizeof(response) - (resp-response)); resp += strlen(resp); } } *resp++ = 0; //this might not be a null if (resp == response+sizeof(response)) { //we're at the end of the buffer, it's full. bummer //replace 12 bytes with infoResponse memcpy(response+4, "infoResponse", 12); //move down by len(statusResponse)-len(infoResponse) bytes memmove(response+4+12, response+4+14, resp-response-(4+14)); start -= 14-12; //fix this pointer resp = start; resp[-1] = 0; //reset the \n } } }*/ WriteByte(&msg, 0); NET_SendPacket(cluster, NET_ChooseSocket(cluster->qwdsocket, from, *from), msg.cursize, msg.data, *from); } void QTV_StatusResponse(cluster_t *cluster, char *msg, netadr_t *from) { int p, tc, bc; char name[64], skin[64], token[64]; sv_t *sv; char *eol; for (sv = cluster->servers; sv; sv = sv->next) { /*ignore connected streams*/ if (sv->isconnected) continue; /*and only streams that we could have requested this from*/ if (sv->autodisconnect != AD_STATUSPOLL) continue; if (Net_CompareAddress(&sv->serveraddress, from, 0, 1)) break; } /*not a valid server... weird.*/ if (!sv) return; /*skip the n directive*/ msg++; eol = strchr(msg, '\n'); if (!eol) return; *eol = 0; strlcpy(sv->map.serverinfo, msg, sizeof(sv->map.serverinfo)); QTV_UpdatedServerInfo(sv); // Info_ValueForKey(sv->map.serverinfo, "map", sv->map.mapname, sizeof(sv->map.mapname)); Info_ValueForKey(sv->map.serverinfo, "*gamedir", sv->map.gamedir, sizeof(sv->map.gamedir)); if (!*sv->map.gamedir) strlcpy(sv->map.gamedir, "qw", sizeof(sv->map.gamedir)); for(p = 0; p < MAX_CLIENTS; p++) { msg = eol+1; eol = strchr(msg, '\n'); if (!eol) break; *eol = 0; sv->map.players[p].active = false; //userid msg = COM_ParseToken(msg, token, sizeof(token), NULL); //frags msg = COM_ParseToken(msg, token, sizeof(token), NULL); sv->map.players[p].frags = atoi(token); //time (minuites) msg = COM_ParseToken(msg, token, sizeof(token), NULL); //ping msg = COM_ParseToken(msg, token, sizeof(token), NULL); sv->map.players[p].ping = atoi(token); //name msg = COM_ParseToken(msg, name, sizeof(name), NULL); //skin msg = COM_ParseToken(msg, skin, sizeof(skin), NULL); //tc msg = COM_ParseToken(msg, token, sizeof(token), NULL); tc = atoi(token); //bc msg = COM_ParseToken(msg, token, sizeof(token), NULL); bc = atoi(token); snprintf(sv->map.players[p].userinfo, sizeof(sv->map.players[p].userinfo), "\\name\\%s\\skin\\%s\\topcolor\\%i\\bottomcolor\\%i", name, skin, tc, bc); } for(; p < MAX_CLIENTS; p++) { sv->map.players[p].active = false; *sv->map.players[p].userinfo = 0; } } void ConnectionlessPacket(cluster_t *cluster, netadr_t *from, netmsg_t *m) { char buffer[MAX_QWMSGLEN]; int i; ReadLong(m); ReadString(m, buffer, sizeof(buffer)); if (!strncmp(buffer, "n\\", 2)) { QTV_StatusResponse(cluster, buffer, from); return; } if (!strncmp(buffer, "rcon ", 5)) { QTV_Rcon(cluster, buffer+5, from); return; } if (!strncmp(buffer, "ping", 4)) { //ack Netchan_OutOfBandPrint(cluster, *from, "l"); return; } if (!strncmp(buffer, "status", 6)) { QTV_Status(cluster, from); return; } if (!strncmp(buffer, "getinfo", 7)) { QTV_GetInfo(cluster, from, buffer+7); return; } if (!strncmp(buffer, "getchallenge", 12)) { i = NewChallenge(cluster, from); if (!cluster->relayenabled) Netchan_OutOfBandPrint(cluster, *from, "c%i", i); else { //special response to say we don't support dtls, but can proxy it, so use dtlsconnect without needing to send any private info until the final target is determined. snprintf(buffer, sizeof(buffer), "c%i%cDTLS\xff\xff\xff\xff", i, 0); //PROTOCOL_VERSION_DTLSUPGRADE Netchan_OutOfBand(cluster, *from, strlen(buffer)+9, buffer); } return; } if (!strncmp(buffer, "connect 28 ", 11)) { if (cluster->numviewers >= cluster->maxviewers && cluster->maxviewers) Netchan_OutOfBandPrint(cluster, *from, "n" "Sorry, proxy is full.\n"); else NewQWClient(cluster, from, buffer); return; } if (!strncmp(buffer, "getserversExtResponse", 21) && cluster->pingtreeenabled) { //q3-style serverlist response m->readpos = 4+21; Fwd_ParseServerList(cluster, m, -1); return; } if (!strncmp(buffer, "d\n", 2) && cluster->pingtreeenabled) { //legacy qw serverlist response m->readpos = 4+2; Fwd_ParseServerList(cluster, m, AF_INET); return; } if (!strcmp(buffer, "l") && cluster->pingtreeenabled) { //qw ping response Fwd_PingResponse(cluster, from); return; } if (!strncmp(buffer, "pingstatus", 10) && cluster->pingtreeenabled) { int ext = false; char arg[64]; if (buffer[10] == ' ') { char *s = buffer + 11; while (*s) { s = COM_ParseToken(s, arg,sizeof(arg), ""); // if (!strcmp(arg, "ext")) ext = true; } } Fwd_PingStatus(cluster, from, ext); return; } if (!strncmp(buffer, "dtlsconnect ", 12) && cluster->relayenabled) { //dtlsconnect challenge [finalip@middleip@targetip] char challenge[64]; char *s = COM_ParseToken(buffer+12, challenge,sizeof(challenge), ""); // if (ChallengePasses(cluster, from, atoi(challenge))) { while(*s == ' ') s++; Fwd_NewQWFwd(cluster, from, s); //will send a challenge to the target. //the relay code will pass the response to the client triggering a new dtlsconnect. //eventually punching all the way through to the target which will respond with a dtlsopened. //the client will then be free to send its dtls handshakes, with the server's certificate matched against the fingerprint reported by the master. //this should ensure there's no tampering. //note that we cannot read any disconnect hints when they're encrypted, so we'll be depending on timeouts (which also avoids malicious disconnect spoofs, yay?) } return; } // if (buffer[0] == 'l' && (!buffer[1] || buffer[1] == '\n')) // { // Sys_Printf(cluster, "Ack from %s\n", ); // } } void SV_WriteDelta(int entnum, const entity_state_t *from, const entity_state_t *to, netmsg_t *msg, qboolean force, unsigned int pext) { unsigned int i; unsigned int bits; bits = 0; if (entnum >= 2048) { //panic if (!force) return; //erk! oh noes! woe is me! } else if (entnum >= 1024+512) bits |= UX_ENTITYDBL|UX_ENTITYDBL2; else if (entnum >= 1024) bits |= UX_ENTITYDBL2; else if (entnum >= 512) bits |= UX_ENTITYDBL; if (from->angles[0] != to->angles[0]) bits |= U_ANGLE1; if (from->angles[1] != to->angles[1]) bits |= U_ANGLE2; if (from->angles[2] != to->angles[2]) bits |= U_ANGLE3; if (from->origin[0] != to->origin[0]) bits |= U_ORIGIN1; if (from->origin[1] != to->origin[1]) bits |= U_ORIGIN2; if (from->origin[2] != to->origin[2]) bits |= U_ORIGIN3; if (from->colormap != to->colormap) bits |= U_COLORMAP; if (from->skinnum != to->skinnum) bits |= U_SKIN; if (from->modelindex != to->modelindex) { if (to->modelindex > 0xff) { if (to->modelindex <= 0x1ff) bits |= U_MODEL|UX_MODELDBL; //0x100|byte else bits |= UX_MODELDBL; //short } else bits |= U_MODEL; } if (from->frame != to->frame) bits |= U_FRAME; if ((from->effects&0xff) != (to->effects&0xff)) bits |= U_EFFECTS; if ((from->effects&0xff00) != (to->effects&0xff00) && pext & PEXT_DPFLAGS) bits |= UX_EFFECTS16; if (from->alpha != to->alpha && pext & PEXT_TRANS) bits |= UX_ALPHA; if (from->scale != to->scale && pext & PEXT_SCALE) bits |= UX_SCALE; if (from->fatness != to->fatness && pext & PEXT_FATNESS) bits |= UX_FATNESS; if (from->drawflags != to->drawflags && pext & PEXT_HEXEN2) bits |= UX_DRAWFLAGS; if (from->abslight != to->abslight && pext & PEXT_HEXEN2) bits |= UX_ABSLIGHT; if ((from->colormod[0]!= to->colormod[0] || from->colormod[1] != to->colormod[1] || from->colormod[2] != to->colormod[2]) && pext & PEXT_COLOURMOD) bits |= UX_COLOURMOD; if (from->dpflags != to->dpflags && pext & PEXT_DPFLAGS) bits |= UX_DPFLAGS; if ((from->tagentity != to->tagentity || from->tagindex != to->tagindex) && pext & PEXT_SETATTACHMENT) bits |= UX_TAGINFO; if ((from->light[0] != to->light[0] || from->light[1] != to->light[1] || from->light[2] != to->light[2] || from->light[3] != to->light[3] || from->lightstyle != to->lightstyle || from->lightpflags != to->lightpflags) && pext & PEXT_DPFLAGS) bits |= UX_LIGHT; if (bits & 0xff000000) bits |= UX_YETMORE; if (bits & 0x00ff0000) bits |= UX_EVENMORE; if (bits & 0x000000ff) bits |= U_MOREBITS; if (!bits && !force) return; i = (entnum&511) | (bits&~511); WriteShort (msg, i); if (bits & U_MOREBITS) WriteByte (msg, bits&255); if (bits & UX_EVENMORE) WriteByte (msg, bits>>16); if (bits & UX_YETMORE) WriteByte (msg, bits>>24); if (bits & U_MODEL) WriteByte (msg, to->modelindex&255); else if (bits & UX_MODELDBL) WriteShort(msg, to->modelindex&0xffff); if (bits & U_FRAME) WriteByte (msg, to->frame); if (bits & U_COLORMAP) WriteByte (msg, to->colormap); if (bits & U_SKIN) WriteByte (msg, to->skinnum); if (bits & U_EFFECTS) WriteByte (msg, to->effects&0x00ff); if (bits & U_ORIGIN1) WriteCoord(msg, to->origin[0], pext); if (bits & U_ANGLE1) WriteAngle(msg, to->angles[0], pext); if (bits & U_ORIGIN2) WriteCoord(msg, to->origin[1], pext); if (bits & U_ANGLE2) WriteAngle(msg, to->angles[1], pext); if (bits & U_ORIGIN3) WriteCoord(msg, to->origin[2], pext); if (bits & U_ANGLE3) WriteAngle(msg, to->angles[2], pext); if (bits & UX_SCALE) WriteByte (msg, to->scale); if (bits & UX_ALPHA) WriteByte (msg, to->alpha); if (bits & UX_FATNESS) WriteByte (msg, to->fatness); if (bits & UX_DRAWFLAGS) WriteByte (msg, to->drawflags); if (bits & UX_ABSLIGHT) WriteByte (msg, to->abslight); if (bits & UX_COLOURMOD) { WriteByte (msg, to->colormod[0]); WriteByte (msg, to->colormod[1]); WriteByte (msg, to->colormod[2]); } if (bits & UX_DPFLAGS) { // these are bits for the 'flags' field of the entity_state_t WriteByte (msg, to->dpflags); } if (bits & UX_TAGINFO) { WriteShort (msg, to->tagentity); WriteShort (msg, to->tagindex); } if (bits & UX_LIGHT) { WriteShort (msg, to->light[0]); WriteShort (msg, to->light[1]); WriteShort (msg, to->light[2]); WriteShort (msg, to->light[3]); WriteByte (msg, to->lightstyle); WriteByte (msg, to->lightpflags); } if (bits & UX_EFFECTS16) WriteByte (msg, to->effects>>8); } void SV_EmitPacketEntities (const sv_t *qtv, const viewer_t *v, const packet_entities_t *to, netmsg_t *msg) { const entity_state_t *baseline; const packet_entities_t *from; int oldindex, newindex; int oldnum, newnum; int oldmax; int delta_frame; delta_frame = v->delta_frames[v->netchan.outgoing_sequence&(ENTITY_FRAMES-1)]; // this is the frame that we are going to delta update from if (delta_frame != -1) { from = &v->frame[delta_frame & (ENTITY_FRAMES-1)]; oldmax = from->numents; WriteByte (msg, svc_deltapacketentities); WriteByte (msg, delta_frame); } else { oldmax = 0; // no delta update from = NULL; WriteByte (msg, svc_packetentities); } newindex = 0; oldindex = 0; //Con_Printf ("---%i to %i ----\n", client->delta_sequence & UPDATE_MASK // , client->netchan.outgoing_sequence & UPDATE_MASK); while (newindex < to->numents || oldindex < oldmax) { newnum = newindex >= to->numents ? 9999 : to->entnum[newindex]; oldnum = oldindex >= oldmax ? 9999 : from->entnum[oldindex]; if (newnum == oldnum) { // delta update from old position //Con_Printf ("delta %i\n", newnum); SV_WriteDelta (newnum, &from->ents[oldindex], &to->ents[newindex], msg, false, qtv->pext1); oldindex++; newindex++; continue; } if (newnum < oldnum) { // this is a new entity, send it from the baseline baseline = &qtv->map.entity[newnum].baseline; //Con_Printf ("baseline %i\n", newnum); SV_WriteDelta (newnum, baseline, &to->ents[newindex], msg, true, qtv->pext1); newindex++; continue; } if (newnum > oldnum) { // the old entity isn't present in the new message //Con_Printf ("remove %i\n", oldnum); WriteShort (msg, oldnum | U_REMOVE); oldindex++; continue; } } WriteShort (msg, 0); // end of packetentities } void Prox_SendInitialEnts(sv_t *qtv, oproxy_t *prox, netmsg_t *msg) { frame_t *frame; int i, entnum; WriteByte(msg, svc_packetentities); frame = &qtv->map.frame[qtv->netchan.incoming_sequence & (ENTITY_FRAMES-1)]; for (i = 0; i < frame->numents; i++) { entnum = frame->entnums[i]; SV_WriteDelta(entnum, &qtv->map.entity[entnum].baseline, &frame->ents[i], msg, true, qtv->pext1); } WriteShort(msg, 0); } static float InterpolateAngle(float current, float ideal, float fraction) { float move; move = ideal - current; if (move >= 180) move -= 360; else if (move <= -180) move += 360; return current + fraction * move; } void SendLocalPlayerState(sv_t *tv, viewer_t *v, int playernum, netmsg_t *msg) { int flags; int j; unsigned int pext1 = tv?tv->pext1:0; WriteByte(msg, svc_playerinfo); WriteByte(msg, playernum); if (tv && tv->controller == v) { //we're the one that is actually playing. v->trackplayer = tv->map.thisplayer; flags = 0; if (tv->map.players[tv->map.thisplayer].current.weaponframe) flags |= PF_WEAPONFRAME; if (tv->map.players[tv->map.thisplayer].current.effects) flags |= PF_EFFECTS; if (tv->map.players[tv->map.thisplayer].dead) flags |= PF_DEAD; if (tv->map.players[tv->map.thisplayer].gibbed) flags |= PF_GIB; for (j=0 ; j<3 ; j++) if (tv->map.players[tv->map.thisplayer].current.velocity[j]) flags |= (PF_VELOCITY1<map.players[tv->map.thisplayer].current.origin[0], tv->pext1); WriteCoord(msg, tv->map.players[tv->map.thisplayer].current.origin[1], tv->pext1); WriteCoord(msg, tv->map.players[tv->map.thisplayer].current.origin[2], tv->pext1); WriteByte(msg, tv->map.players[tv->map.thisplayer].current.frame); for (j=0 ; j<3 ; j++) if (flags & (PF_VELOCITY1<map.players[tv->map.thisplayer].current.velocity[j]); if (flags & PF_MODEL) WriteByte(msg, tv->map.players[tv->map.thisplayer].current.modelindex); if (flags & PF_SKINNUM) WriteByte(msg, tv->map.players[tv->map.thisplayer].current.skinnum); if (flags & PF_EFFECTS) WriteByte(msg, tv->map.players[tv->map.thisplayer].current.effects); if (flags & PF_WEAPONFRAME) WriteByte(msg, tv->map.players[tv->map.thisplayer].current.weaponframe); } else { flags = 0; for (j=0 ; j<3 ; j++) if ((int)v->velocity[j]) flags |= (PF_VELOCITY1<origin[0], pext1); WriteCoord(msg, v->origin[1], pext1); WriteCoord(msg, v->origin[2], pext1); WriteByte(msg, 0); for (j=0 ; j<3 ; j++) if (flags & (PF_VELOCITY1<velocity[j]); } } #define UNQ_MOREBITS (1<<0) #define UNQ_ORIGIN1 (1<<1) #define UNQ_ORIGIN2 (1<<2) #define UNQ_ORIGIN3 (1<<3) #define UNQ_ANGLE2 (1<<4) #define UNQ_NOLERP (1<<5) // don't interpolate movement #define UNQ_FRAME (1<<6) #define UNQ_SIGNAL (1<<7) // just differentiates from other updates // svc_update can pass all of the fast update bits, plus more #define UNQ_ANGLE1 (1<<8) #define UNQ_ANGLE3 (1<<9) #define UNQ_MODEL (1<<10) #define UNQ_COLORMAP (1<<11) #define UNQ_SKIN (1<<12) #define UNQ_EFFECTS (1<<13) #define UNQ_LONGENTITY (1<<14) #define UNQ_UNUSED (1<<15) #define SU_VIEWHEIGHT (1<<0) #define SU_IDEALPITCH (1<<1) #define SU_PUNCH1 (1<<2) #define SU_PUNCH2 (1<<3) #define SU_PUNCH3 (1<<4) #define SU_VELOCITY1 (1<<5) #define SU_VELOCITY2 (1<<6) #define SU_VELOCITY3 (1<<7) //define SU_AIMENT (1<<8) AVAILABLE BIT #define SU_ITEMS (1<<9) #define SU_ONGROUND (1<<10) // no data follows, the bit is it #define SU_INWATER (1<<11) // no data follows, the bit is it #define SU_WEAPONFRAME (1<<12) #define SU_ARMOR (1<<13) #define SU_WEAPON (1<<14) void SendNQClientData(sv_t *tv, viewer_t *v, netmsg_t *msg) { playerinfo_t *pl; int bits; int i; if (!tv) return; if (v->trackplayer < 0) { WriteByte (msg, svc_nqclientdata); WriteShort (msg, SU_VIEWHEIGHT|SU_ITEMS); WriteByte (msg, 22); //viewheight WriteLong (msg, 0); //items WriteShort (msg, 1000); //health WriteByte (msg, 0); //currentammo WriteByte (msg, 0); //shells WriteByte (msg, 0); //nails WriteByte (msg, 0); //rockets WriteByte (msg, 0); //cells WriteByte (msg, 0); //active weapon return; } else pl = &tv->map.players[v->trackplayer]; bits = 0; if (!pl->dead) bits |= SU_VIEWHEIGHT; if (0) bits |= SU_IDEALPITCH; bits |= SU_ITEMS; if ( 0) bits |= SU_ONGROUND; if ( 0 ) bits |= SU_INWATER; for (i=0 ; i<3 ; i++) { if (0) bits |= (SU_PUNCH1<current.weaponframe) bits |= SU_WEAPONFRAME; if (pl->stats[STAT_ARMOR]) bits |= SU_ARMOR; // if (pl->stats[STAT_WEAPON]) bits |= SU_WEAPON; // send the data WriteByte (msg, svc_nqclientdata); WriteShort (msg, bits); if (bits & SU_VIEWHEIGHT) WriteByte (msg, 22); if (bits & SU_IDEALPITCH) WriteByte (msg, 0); for (i=0 ; i<3 ; i++) { if (bits & (SU_PUNCH1<stats[STAT_ITEMS]); if (bits & SU_WEAPONFRAME) WriteByte (msg, pl->current.weaponframe); if (bits & SU_ARMOR) WriteByte (msg, pl->stats[STAT_ARMOR]); if (bits & SU_WEAPON) WriteByte (msg, pl->stats[STAT_WEAPONMODELI]); WriteShort (msg, pl->stats[STAT_HEALTH]); WriteByte (msg, pl->stats[STAT_AMMO]); WriteByte (msg, pl->stats[STAT_SHELLS]); WriteByte (msg, pl->stats[STAT_NAILS]); WriteByte (msg, pl->stats[STAT_ROCKETS]); WriteByte (msg, pl->stats[STAT_CELLS]); WriteByte (msg, pl->stats[STAT_ACTIVEWEAPON]); } void SendNQPlayerStates(cluster_t *cluster, sv_t *tv, viewer_t *v, netmsg_t *msg) { int e; int i; usercmd_t to; float lerp; int bits; float org[3]; entity_t *ent; playerinfo_t *pl; unsigned int pext1; memset(&to, 0, sizeof(to)); if (tv) { pext1 = tv->pext1; WriteByte(msg, svc_nqtime); WriteFloat(msg, (tv->physicstime - tv->mapstarttime)/1000.0f); BSP_SetupForPosition(tv->map.bsp, v->origin[0], v->origin[1], v->origin[2]); lerp = ((tv->simtime - tv->oldpackettime)/1000.0f) / ((tv->nextpackettime - tv->oldpackettime)/1000.0f); lerp = 1; // if (tv->controller == v) // lerp = 1; } else { WriteByte(msg, svc_nqtime); WriteFloat(msg, (cluster->curtime)/1000.0f); lerp = 1; pext1 = 0; } SendNQClientData(tv, v, msg); if (tv) { if (v != tv->controller) { if (v->trackplayer >= 0) { WriteByte(msg, svc_nqsetview); WriteShort(msg, v->trackplayer+1); WriteByte(msg, svc_setangle); WriteAngle(msg, InterpolateAngle(tv->map.players[v->trackplayer].old.angles[0], tv->map.players[v->trackplayer].current.angles[0], lerp), tv->pext1); WriteAngle(msg, InterpolateAngle(tv->map.players[v->trackplayer].old.angles[1], tv->map.players[v->trackplayer].current.angles[1], lerp), tv->pext1); WriteAngle(msg, InterpolateAngle(tv->map.players[v->trackplayer].old.angles[2], tv->map.players[v->trackplayer].current.angles[2], lerp), tv->pext1); } else { WriteByte(msg, svc_nqsetview); WriteShort(msg, v->thisplayer+1); } } for (e = 0; e < MAX_CLIENTS; e++) { pl = &tv->map.players[e]; ent = &tv->map.entity[e+1]; if (e == v->thisplayer && v->trackplayer < 0) { bits = UNQ_ORIGIN1 | UNQ_ORIGIN2 | UNQ_ORIGIN3 | UNQ_COLORMAP; if (e+1 > 255) bits |= UNQ_LONGENTITY; if (bits > 255) bits |= UNQ_MOREBITS; WriteByte (msg,bits | UNQ_SIGNAL); if (bits & UNQ_MOREBITS) WriteByte (msg, bits>>8); if (bits & UNQ_LONGENTITY) WriteShort (msg,e+1); else WriteByte (msg,e+1); if (bits & UNQ_MODEL) WriteByte (msg, 0); if (bits & UNQ_FRAME) WriteByte (msg, 0); if (bits & UNQ_COLORMAP) WriteByte (msg, 0); if (bits & UNQ_SKIN) WriteByte (msg, 0); if (bits & UNQ_EFFECTS) WriteByte (msg, 0); if (bits & UNQ_ORIGIN1) WriteCoord (msg, v->origin[0], tv->pext1); if (bits & UNQ_ANGLE1) WriteAngle(msg, -v->ucmds[2].angles[0], tv->pext1); if (bits & UNQ_ORIGIN2) WriteCoord (msg, v->origin[1], tv->pext1); if (bits & UNQ_ANGLE2) WriteAngle(msg, v->ucmds[2].angles[1], tv->pext1); if (bits & UNQ_ORIGIN3) WriteCoord (msg, v->origin[2], tv->pext1); if (bits & UNQ_ANGLE3) WriteAngle(msg, v->ucmds[2].angles[2], tv->pext1); continue; } if (!pl->active) continue; if (v != tv->controller && e != v->trackplayer) if (pl->current.modelindex >= tv->map.numinlines && !BSP_Visible(tv->map.bsp, pl->leafcount, pl->leafs)) //don't cull bsp objects, like nq... continue; // send an update bits = 0; for (i=0 ; i<3 ; i++) { org[i] = (lerp)*pl->current.origin[i] + (1-lerp)*pl->old.origin[i]; bits |= UNQ_ORIGIN1<current.angles[0] != ent->baseline.angles[0] ) bits |= UNQ_ANGLE1; if ( pl->current.angles[1] != ent->baseline.angles[1] ) bits |= UNQ_ANGLE2; if ( pl->current.angles[2] != ent->baseline.angles[2] ) bits |= UNQ_ANGLE3; // if (pl->v.movetype == MOVETYPE_STEP) // bits |= UNQ_NOLERP; // don't mess up the step animation if (ent->baseline.colormap != e+1 || ent->baseline.colormap > 15) bits |= UNQ_COLORMAP; if (ent->baseline.skinnum != pl->current.skinnum) bits |= UNQ_SKIN; if (ent->baseline.frame != pl->current.frame) bits |= UNQ_FRAME; if (ent->baseline.effects != pl->current.effects) bits |= UNQ_EFFECTS; if (ent->baseline.modelindex != pl->current.modelindex) bits |= UNQ_MODEL; if (e+1 > 255) bits |= UNQ_LONGENTITY; if (bits > 255) bits |= UNQ_MOREBITS; // // write the message // WriteByte (msg,bits | UNQ_SIGNAL); if (bits & UNQ_MOREBITS) WriteByte (msg, bits>>8); if (bits & UNQ_LONGENTITY) WriteShort (msg,e+1); else WriteByte (msg,e+1); if (bits & UNQ_MODEL) WriteByte (msg, pl->current.modelindex); if (bits & UNQ_FRAME) WriteByte (msg, pl->current.frame); if (bits & UNQ_COLORMAP) WriteByte (msg, (e>=15)?0:(e+1)); if (bits & UNQ_SKIN) WriteByte (msg, pl->current.skinnum); if (bits & UNQ_EFFECTS) WriteByte (msg, pl->current.effects); if (bits & UNQ_ORIGIN1) WriteCoord (msg, org[0], tv->pext1); if (bits & UNQ_ANGLE1) WriteAngle(msg, -pl->current.angles[0], tv->pext1); if (bits & UNQ_ORIGIN2) WriteCoord (msg, org[1], tv->pext1); if (bits & UNQ_ANGLE2) WriteAngle(msg, pl->current.angles[1], tv->pext1); if (bits & UNQ_ORIGIN3) WriteCoord (msg, org[2], tv->pext1); if (bits & UNQ_ANGLE3) WriteAngle(msg, pl->current.angles[2], tv->pext1); } { int newindex = 0; entity_state_t *newstate; int newnum; frame_t *topacket; int snapdist = 128; //in quake units int miss; snapdist = snapdist*8; snapdist = snapdist*snapdist; topacket = &tv->map.frame[tv->netchan.incoming_sequence&(ENTITY_FRAMES-1)]; for (newindex = 0; newindex < topacket->numents; newindex++) { //don't pvs cull bsp models //pvs cull everything else newstate = &topacket->ents[newindex]; newnum = topacket->entnums[newindex]; if (v != tv->controller) if (newstate->modelindex >= tv->map.numinlines && !BSP_Visible(tv->map.bsp, tv->map.entity[newnum].leafcount, tv->map.entity[newnum].leafs)) continue; if (msg->cursize + 128 > msg->maxsize) break; // send an update bits = 0; for (i=0 ; i<3 ; i++) { miss = (newstate->origin[i]) - ent->baseline.origin[i]; if ( miss <= -1/8.0 || miss >= 1/8.0 ) bits |= UNQ_ORIGIN1<angles[0] != ent->baseline.angles[0]) bits |= UNQ_ANGLE1; if (newstate->angles[1] != ent->baseline.angles[1]) bits |= UNQ_ANGLE2; if (newstate->angles[2] != ent->baseline.angles[2]) bits |= UNQ_ANGLE3; // if (ent->v.movetype == MOVETYPE_STEP) // bits |= UNQ_NOLERP; // don't mess up the step animation if (newstate->colormap != ent->baseline.colormap || ent->baseline.colormap > 15) bits |= UNQ_COLORMAP; if (newstate->skinnum != ent->baseline.skinnum) bits |= UNQ_SKIN; if (newstate->frame != ent->baseline.frame) bits |= UNQ_FRAME; if (newstate->effects != ent->baseline.effects) bits |= UNQ_EFFECTS; if (newstate->modelindex != ent->baseline.modelindex) bits |= UNQ_MODEL; if (newnum >= 256) bits |= UNQ_LONGENTITY; if (bits >= 256) bits |= UNQ_MOREBITS; // // write the message // WriteByte (msg,bits | UNQ_SIGNAL); if (bits & UNQ_MOREBITS) WriteByte (msg, bits>>8); if (bits & UNQ_LONGENTITY) WriteShort (msg,newnum); else WriteByte (msg,newnum); if (bits & UNQ_MODEL) WriteByte (msg, newstate->modelindex); if (bits & UNQ_FRAME) WriteByte (msg, newstate->frame); if (bits & UNQ_COLORMAP) WriteByte (msg, (newstate->colormap>15)?0:(newstate->colormap)); if (bits & UNQ_SKIN) WriteByte (msg, newstate->skinnum); if (bits & UNQ_EFFECTS) WriteByte (msg, newstate->effects); if (bits & UNQ_ORIGIN1) WriteCoord (msg, newstate->origin[0], pext1); if (bits & UNQ_ANGLE1) WriteAngle(msg, newstate->angles[0], pext1); if (bits & UNQ_ORIGIN2) WriteCoord (msg, newstate->origin[1], pext1); if (bits & UNQ_ANGLE2) WriteAngle(msg, newstate->angles[1], pext1); if (bits & UNQ_ORIGIN3) WriteCoord (msg, newstate->origin[2], pext1); if (bits & UNQ_ANGLE3) WriteAngle(msg, newstate->angles[2], pext1); } } } else { WriteByte(msg, svc_nqsetview); WriteShort(msg, v->thisplayer+1); WriteShort (msg,UNQ_MOREBITS|UNQ_MODEL|UNQ_ORIGIN1 | UNQ_ORIGIN2 | UNQ_ORIGIN3 | UNQ_SIGNAL); WriteByte (msg, v->thisplayer+1); WriteByte (msg, 2); //model WriteCoord (msg, v->origin[0], pext1); WriteCoord (msg, v->origin[1], pext1); WriteCoord (msg, v->origin[2], pext1); } } //returns self, or the final commentator viewer_t *GetCommentator(viewer_t *v) { viewer_t *orig = v; int runaway = 10; while(runaway-- > 0) { if (!v->commentator) break; if (v->commentator->thinksitsconnected == false) break; if (v->commentator->server != orig->server) break; v = v->commentator; } return v; } void SendPlayerStates(sv_t *tv, viewer_t *v, netmsg_t *msg) { bsp_t *bsp = (!tv || tv->controller == v ? NULL : tv->map.bsp); viewer_t *cv; packet_entities_t *e; int i; usercmd_t to; unsigned short flags; float interp; float lerp; int track; int snapdist = 128; //in quake units snapdist = snapdist*8; snapdist = snapdist*snapdist; memset(&to, 0, sizeof(to)); if (tv) { if (tv->physicstime != v->settime)// && tv->cluster->chokeonnotupdated) { WriteByte(msg, svc_updatestatlong); WriteByte(msg, STAT_TIME); WriteLong(msg, v->settime); v->settime = tv->physicstime; } BSP_SetupForPosition(bsp, v->origin[0], v->origin[1], v->origin[2]); lerp = ((tv->simtime - tv->oldpackettime)/1000.0f) / ((tv->nextpackettime - tv->oldpackettime)/1000.0f); if (lerp < 0) lerp = 0; if (lerp > 1) lerp = 1; if (tv->controller == v) { lerp = 1; track = tv->map.thisplayer; v->trackplayer = tv->map.thisplayer; } else { cv = GetCommentator(v); track = cv->trackplayer; if (cv != v && track < 0) { //following a commentator track = MAX_CLIENTS-2; } if (v->trackplayer != track) QW_StuffcmdToViewer (v, "track %i\n", track); if (!v->commentator && track >= 0 && !v->backbuffered) { if (v->trackplayer != tv->map.trackplayer && tv->usequakeworldprotocols) if (!tv->map.players[v->trackplayer].active && tv->map.players[tv->map.trackplayer].active) { QW_StuffcmdToViewer (v, "track %i\n", tv->map.trackplayer); } } } for (i = 0; i < MAX_CLIENTS; i++) { if (i == v->thisplayer) { SendLocalPlayerState(tv, v, i, msg); continue; } if (!tv->map.players[i].active && track != i) continue; //bsp cull. currently tracked player is always visible if (track != i && !BSP_Visible(bsp, tv->map.players[i].leafcount, tv->map.players[i].leafs)) continue; flags = PF_COMMAND; if (track == i && tv->map.players[i].current.weaponframe) flags |= PF_WEAPONFRAME; if (tv->map.players[i].current.modelindex != tv->map.modelindex_player) flags |= PF_MODEL; if (tv->map.players[i].dead || !tv->map.players[i].active) flags |= PF_DEAD; if (tv->map.players[i].gibbed || !tv->map.players[i].active) flags |= PF_GIB; if (tv->map.players[i].current.effects != 0) flags |= PF_EFFECTS; if (tv->map.players[i].current.skinnum != 0) flags |= PF_SKINNUM; if (tv->map.players[i].current.velocity[0]) flags |= PF_VELOCITY1; if (tv->map.players[i].current.velocity[1]) flags |= PF_VELOCITY2; if (tv->map.players[i].current.velocity[2]) flags |= PF_VELOCITY3; WriteByte(msg, svc_playerinfo); WriteByte(msg, i); WriteShort(msg, flags); if (!tv->map.players[i].active || !tv->map.players[i].oldactive || (tv->map.players[i].current.origin[0] - tv->map.players[i].old.origin[0])*(tv->map.players[i].current.origin[0] - tv->map.players[i].old.origin[0]) > snapdist || (tv->map.players[i].current.origin[1] - tv->map.players[i].old.origin[1])*(tv->map.players[i].current.origin[1] - tv->map.players[i].old.origin[1]) > snapdist || (tv->map.players[i].current.origin[2] - tv->map.players[i].old.origin[2])*(tv->map.players[i].current.origin[2] - tv->map.players[i].old.origin[2]) > snapdist) { //teleported (or respawned), so don't interpolate WriteCoord(msg, tv->map.players[i].current.origin[0], tv->pext1); WriteCoord(msg, tv->map.players[i].current.origin[1], tv->pext1); WriteCoord(msg, tv->map.players[i].current.origin[2], tv->pext1); } else { //send interpolated angles interp = (lerp)*tv->map.players[i].current.origin[0] + (1-lerp)*tv->map.players[i].old.origin[0]; WriteCoord(msg, interp, tv->pext1); interp = (lerp)*tv->map.players[i].current.origin[1] + (1-lerp)*tv->map.players[i].old.origin[1]; WriteCoord(msg, interp, tv->pext1); interp = (lerp)*tv->map.players[i].current.origin[2] + (1-lerp)*tv->map.players[i].old.origin[2]; WriteCoord(msg, interp, tv->pext1); } WriteByte(msg, tv->map.players[i].current.frame); if (flags & PF_MSEC) { WriteByte(msg, 0); } if (flags & PF_COMMAND) { if (!tv->map.players[i].active || !tv->map.players[i].oldactive) { to.angles[0] = tv->map.players[i].current.angles[0]; to.angles[1] = tv->map.players[i].current.angles[1]; to.angles[2] = tv->map.players[i].current.angles[2]; } else { to.angles[0] = InterpolateAngle(tv->map.players[i].old.angles[0], tv->map.players[i].current.angles[0], lerp); to.angles[1] = InterpolateAngle(tv->map.players[i].old.angles[1], tv->map.players[i].current.angles[1], lerp); to.angles[2] = InterpolateAngle(tv->map.players[i].old.angles[2], tv->map.players[i].current.angles[2], lerp); } WriteDeltaUsercmd(msg, &nullcmd, &to); } //vel if (flags & PF_VELOCITY1) WriteShort(msg, tv->map.players[i].current.velocity[0]); if (flags & PF_VELOCITY2) WriteShort(msg, tv->map.players[i].current.velocity[1]); if (flags & PF_VELOCITY3) WriteShort(msg, tv->map.players[i].current.velocity[2]); //model if (flags & PF_MODEL) WriteByte(msg, tv->map.players[i].current.modelindex); //skin if (flags & PF_SKINNUM) WriteByte (msg, tv->map.players[i].current.skinnum); //effects if (flags & PF_EFFECTS) WriteByte (msg, tv->map.players[i].current.effects); //weaponframe if (flags & PF_WEAPONFRAME) WriteByte(msg, tv->map.players[i].current.weaponframe); } } else { lerp = 1; SendLocalPlayerState(tv, v, v->thisplayer, msg); } e = &v->frame[v->netchan.outgoing_sequence&(ENTITY_FRAMES-1)]; e->numents = 0; if (tv) { int oldindex = 0, newindex = 0; entity_state_t *newstate; int newnum, oldnum; frame_t *frompacket, *topacket; topacket = &tv->map.frame[tv->netchan.incoming_sequence&(ENTITY_FRAMES-1)]; if (tv->usequakeworldprotocols) { frompacket = &tv->map.frame[(topacket->oldframe)&(ENTITY_FRAMES-1)]; } else { frompacket = &tv->map.frame[(tv->netchan.incoming_sequence-1)&(ENTITY_FRAMES-1)]; } for (newindex = 0; newindex < topacket->numents; newindex++) { //don't pvs cull bsp models //pvs cull everything else newstate = &topacket->ents[newindex]; newnum = topacket->entnums[newindex]; if (newstate->modelindex >= tv->map.numinlines && !BSP_Visible(bsp, tv->map.entity[newnum].leafcount, tv->map.entity[newnum].leafs)) continue; e->entnum[e->numents] = newnum; memcpy(&e->ents[e->numents], newstate, sizeof(entity_state_t)); if (frompacket != topacket) //optimisation for qw protocols { entity_state_t *oldstate; if (oldindex < frompacket->numents) { oldnum = frompacket->entnums[oldindex]; while(oldnum < newnum) { oldindex++; if (oldindex >= frompacket->numents) break; //no more oldnum = frompacket->entnums[oldindex]; } if (oldnum == newnum) { //ent exists in old packet oldstate = &frompacket->ents[oldindex]; } else { oldstate = newstate; } } else { //reached end, definatly not in packet oldstate = newstate; } if ((newstate->origin[0] - oldstate->origin[0])*(newstate->origin[0] - oldstate->origin[0]) > snapdist || (newstate->origin[1] - oldstate->origin[1])*(newstate->origin[1] - oldstate->origin[1]) > snapdist || (newstate->origin[2] - oldstate->origin[2])*(newstate->origin[2] - oldstate->origin[2]) > snapdist) { //teleported (or respawned), so don't interpolate e->ents[e->numents].origin[0] = newstate->origin[0]; e->ents[e->numents].origin[1] = newstate->origin[1]; e->ents[e->numents].origin[2] = newstate->origin[2]; } else { e->ents[e->numents].origin[0] = (lerp)*newstate->origin[0] + (1-lerp)*oldstate->origin[0]; e->ents[e->numents].origin[1] = (lerp)*newstate->origin[1] + (1-lerp)*oldstate->origin[1]; e->ents[e->numents].origin[2] = (lerp)*newstate->origin[2] + (1-lerp)*oldstate->origin[2]; } } e->numents++; if (e->numents == ENTS_PER_FRAME) break; } } SV_EmitPacketEntities(tv, v, e, msg); if (tv && tv->map.nailcount) { WriteByte(msg, svc_nails); WriteByte(msg, tv->map.nailcount); for (i = 0; i < tv->map.nailcount; i++) { WriteByte(msg, tv->map.nails[i].bits[0]); WriteByte(msg, tv->map.nails[i].bits[1]); WriteByte(msg, tv->map.nails[i].bits[2]); WriteByte(msg, tv->map.nails[i].bits[3]); WriteByte(msg, tv->map.nails[i].bits[4]); WriteByte(msg, tv->map.nails[i].bits[5]); } } } void UpdateStats(sv_t *qtv, viewer_t *v) { viewer_t *cv; netmsg_t msg; char buf[6]; int i; static const unsigned int nullstats[MAX_STATS] = {1000}; const unsigned int *stats; InitNetMsg(&msg, buf, sizeof(buf)); if (v->commentator && v->thinksitsconnected) cv = v->commentator; else cv = v; if (qtv && qtv->controller == cv) stats = qtv->map.players[qtv->map.thisplayer].stats; else if (cv->trackplayer == -1 || !qtv) stats = nullstats; else stats = qtv->map.players[cv->trackplayer].stats; for (i = 0; i < MAX_STATS; i++) { if (v->currentstats[i] != stats[i]) { if (v->netchan.isnqprotocol) { //nq only supports 32bit stats WriteByte(&msg, svc_updatestat); WriteByte(&msg, i); WriteLong(&msg, stats[i]); } else if (stats[i] < 256) { WriteByte(&msg, svc_updatestat); WriteByte(&msg, i); WriteByte(&msg, stats[i]); } else { WriteByte(&msg, svc_updatestatlong); WriteByte(&msg, i); WriteLong(&msg, stats[i]); } SendBufferToViewer(v, msg.data, msg.cursize, true); msg.cursize = 0; v->currentstats[i] = stats[i]; } } } //returns the next prespawn 'buffer' number to use, or -1 if no more //FIXME: viewer may support fewer/different extensions vs the the stream. int Prespawn(sv_t *qtv, int curmsgsize, netmsg_t *msg, int bufnum, int thisplayer) { int r, ni; r = bufnum; ni = SendCurrentUserinfos(qtv, curmsgsize, msg, bufnum, thisplayer); r += ni - bufnum; bufnum = ni; bufnum -= MAX_CLIENTS; ni = SendCurrentBaselines(qtv, curmsgsize, msg, 768, bufnum); r += ni - bufnum; bufnum = ni; bufnum -= MAX_ENTITIES; ni = SendCurrentLightmaps(qtv, curmsgsize, msg, 768, bufnum); r += ni - bufnum; bufnum = ni; bufnum -= MAX_LIGHTSTYLES; ni = SendStaticSounds(qtv, curmsgsize, msg, 768, bufnum); r += ni - bufnum; bufnum = ni; bufnum -= MAX_STATICSOUNDS; ni = SendStaticEntities(qtv, curmsgsize, msg, 768, bufnum); r += ni - bufnum; bufnum = ni; bufnum -= MAX_STATICENTITIES; if (bufnum == 0) return -1; return r; } void PMove(viewer_t *v, usercmd_t *cmd) { sv_t *qtv; pmove_t pmove; if (v->server && v->server->controller == v) { v->origin[0] = v->server->map.players[v->server->map.thisplayer].current.origin[0]; v->origin[1] = v->server->map.players[v->server->map.thisplayer].current.origin[1]; v->origin[2] = v->server->map.players[v->server->map.thisplayer].current.origin[2]; v->velocity[0] = v->server->map.players[v->server->map.thisplayer].current.velocity[0]; v->velocity[1] = v->server->map.players[v->server->map.thisplayer].current.velocity[1]; v->velocity[2] = v->server->map.players[v->server->map.thisplayer].current.velocity[2]; return; } pmove.origin[0] = v->origin[0]; pmove.origin[1] = v->origin[1]; pmove.origin[2] = v->origin[2]; pmove.velocity[0] = v->velocity[0]; pmove.velocity[1] = v->velocity[1]; pmove.velocity[2] = v->velocity[2]; pmove.cmd = *cmd; qtv = v->server; if (qtv) { pmove.movevars = qtv->map.movevars; } else { QTV_DefaultMovevars(&pmove.movevars); } PM_PlayerMove(&pmove); v->origin[0] = pmove.origin[0]; v->origin[1] = pmove.origin[1]; v->origin[2] = pmove.origin[2]; v->velocity[0] = pmove.velocity[0]; v->velocity[1] = pmove.velocity[1]; v->velocity[2] = pmove.velocity[2]; } void QW_SetCommentator(cluster_t *cluster, viewer_t *v, viewer_t *commentator) { // if (v->commentator == commentator) // return; WriteByte(&v->netchan.message, svc_setinfo); WriteByte(&v->netchan.message, MAX_CLIENTS-2); WriteString(&v->netchan.message, "name"); if (commentator) { WriteString(&v->netchan.message, commentator->name); QW_StuffcmdToViewer(v, "track %i\n", MAX_CLIENTS-2); QW_PrintfToViewer(v, "Following commentator %s\n", commentator->name); if (v->server != commentator->server) QW_SetViewersServer(cluster, v, commentator->server); } else { WriteString(&v->netchan.message, ""); if (v->commentator ) QW_PrintfToViewer(v, "Commentator disabled\n"); } v->commentator = commentator; } void QTV_SayCommand(cluster_t *cluster, sv_t *qtv, viewer_t *v, char *fullcommand) { char command[256]; char *args; args = COM_ParseToken(fullcommand, command, sizeof(command), NULL); if (!args) args = ""; while(*args && *args <= ' ') args++; #pragma message("fixme: These all need testing") if (!strcmp(command, "help")) { QW_PrintfToViewer(v, "Website: "PROXYWEBSITE"\n" "Commands:\n" ".bind\n" " Bind your keys to drive the menu.\n" ".clients\n" " Lists the users connected to this\n" " proxy.\n" ".qtvinfo\n" " Print info about the current QTV\n" " you're on.\n" ".demo gamedir/demoname.mvd \n" " Start a new stream on the specified\n" " demo.\n" ".disconnect\n" " Disconnect from any server or\n" " stream you're on.\n" ".join qwserver:port\n" " Play on the specified server.\n" ".observe qwserver:port\n" " Spectate on the specified server.\n" ".qtv tcpserver:port\n" " Start a new stream on the specified\n" " server.\n" ".guimenu\n" " Bring up the GUI-based menu\n" " interface.\n" ".tuimenu\n" " Bring up the text-based menu\n" " interface.\n" ".menu\n" " Automatically chooses an interface\n" " that your client supports.\n" ".admin\n" " Log in to administrate this QTV\n" " proxy.\n" ); } else if (!strcmp(command, "qtvinfo")) { char buf[256]; netadr_t addr; unsigned char *ip; gethostname(buf, sizeof(buf)); //ask the operating system for the local dns name NET_StringToAddr(buf, &addr, 0); //look that up ip = (unsigned char*)&((struct sockaddr_in *)&addr)->sin_addr; QW_PrintfToViewer(v, "[QuakeTV] %s | %i.%i.%i.%i\n", cluster->hostname, ip[0], ip[1], ip[2], ip[3]); } else if (!strcmp(command, "menu")) { v->menuspamtime = cluster->curtime-1; COM_ParseToken(args, command, sizeof(command), NULL); if (!strcmp(command, "up")) { v->menuop -= 1; } else if (!strcmp(command, "down")) { v->menuop += 1; } else if (!strcmp(command, "enter")) { Menu_Enter(cluster, v, 0); } else if (!strcmp(command, "use")) { Menu_Enter(cluster, v, 0); } else if (!strcmp(command, "right")) { Menu_Enter(cluster, v, 1); } else if (!strcmp(command, "left")) { Menu_Enter(cluster, v, -1); } else if (!strcmp(command, "select")) { Menu_Enter(cluster, v, 0); } else if (!strcmp(command, "home")) { v->menuop -= 100000; } else if (!strcmp(command, "end")) { v->menuop += 100000; } else if (!strcmp(command, "back")) { QW_SetMenu(v, MENU_DEFAULT); } else if (!strcmp(command, "enter")) { if (v->menunum) Menu_Enter(cluster, v, 0); else QW_SetMenu(v, MENU_SERVERS); } else if (!strcmp(command, "bind") || !strcmp(command, "bindstd")) { QW_StuffcmdToViewer(v, "bind uparrow \"say proxy:menu up\"\n"); QW_StuffcmdToViewer(v, "bind downarrow \"say proxy:menu down\"\n"); QW_StuffcmdToViewer(v, "bind rightarrow \"say proxy:menu right\"\n"); QW_StuffcmdToViewer(v, "bind leftarrow \"say proxy:menu left\"\n"); QW_StuffcmdToViewer(v, "bind enter \"say proxy:menu select\"\n"); QW_StuffcmdToViewer(v, "bind home \"say proxy:menu home\"\n"); QW_StuffcmdToViewer(v, "bind end \"say proxy:menu end\"\n"); QW_StuffcmdToViewer(v, "bind pause \"say proxy:menu\"\n"); QW_StuffcmdToViewer(v, "bind backspace \"say proxy:menu back\"\n"); QW_PrintfToViewer(v, "All keys bound\n"); } else if (!*command) { if (v->menunum) QW_SetMenu(v, MENU_NONE); else if (v->conmenussupported) goto guimenu; else goto tuimenu; } else QW_PrintfToViewer(v, "\"menu %s\" not recognised\n", command); } else if (!strcmp(command, "tuimenu")) { tuimenu: if (v->menunum) QW_SetMenu(v, MENU_NONE); else QW_SetMenu(v, MENU_MAIN); } else if (!strcmp(command, "guimenu")) { sv_t *sv; int y; qboolean shownheader; guimenu: QW_SetMenu(v, MENU_NONE); shownheader = false; /* I've removed the following from this function as it covered the menu (~Moodles): "menupic 0 4 gfx/qplaque.lmp\n" "menupic 96 4 gfx/p_option.lmp\n" */ QW_StuffcmdToViewer(v, "alias menucallback\n" "{\n" "menuclear\n" "if (option == \"OBSERVE\")\n" "{\necho Spectating server $_server\nsay .observe $_server\n}\n" "if (option == \"QTV\")\n" "{\necho Streaming from qtv at $_server\nsay .qtv $_server\n}\n" "if (option == \"JOIN\")\n" "{\necho Joining game at $_server\nsay .join $_server\n}\n" "if (option == \"ADMIN\")\n" "{\nsay .guiadmin\n}\n" "if (option == \"DEMOS\")\n" "{\nsay .demos\n}\n" "if (\"stream \" isin option)\n" "{\necho Changing stream\nsay .$option\n}\n" "}\n" "conmenu menucallback\n" "menuedit 48 36 \"^aServer:\" \"_server\"\n" "menutext 48 52 \"Demos\" DEMOS\n" "menutext 104 52 \"Join\" JOIN\n" "menutext 152 52 \"Observe\" OBSERVE\n" "menutext 224 52 \"QTV\" QTV\n" "menutext 48 84 \"Admin\" ADMIN\n" "menutext 48 92 \"Close Menu\" cancel\n" "menutext 48 116 \"Type in a server address and\"\n" "menutext 48 124 \"click join to play in the game,\"\n" "menutext 48 132 \"observe(udp) to watch, or qtv(tcp)\"\n" "menutext 48 140 \"to connect to a stream or proxy.\"\n" ); y = 140+16; for (sv = cluster->servers; sv; sv = sv->next) { if (!shownheader) { shownheader = true; QW_StuffcmdToViewer(v, "menutext 72 %i \"^aActive Games:\"\n", y); y+=8; } QW_StuffcmdToViewer(v, "menutext 32 %i \"%30s\" \"stream %i\"\n", y, *sv->map.hostname?sv->map.hostname:sv->server, sv->streamid); y+=8; } if (!shownheader) QW_StuffcmdToViewer(v, "menutext 72 %i \"There are no active games\"\n", y); } else if (!strcmp(command, "demos")) { if (v->conmenussupported) goto guidemos; else goto tuidemos; } else if (!strcmp(command, "guidemos")) { int maxshowndemos; char sizestr[13]; int start; int i; guidemos: maxshowndemos = 12; if (!*args) Cluster_BuildAvailableDemoList(cluster); start = atoi(args); //FIXME QW_SetMenu(v, MENU_NONE); /* I've removed the following from this function as it covered the menu (~Moodles): "menupic 0 4 gfx/qplaque.lmp\n" "menupic 96 4 gfx/p_option.lmp\n" */ QW_StuffcmdToViewer(v, "alias menucallback\n" "{\n" "menuclear\n" "if (option == \"PREV\")\n" "{\nsay .demos %i\n}\n" "if (option == \"NEXT\")\n" "{\nsay .demos %i\n}\n" "if (\"demo \" isin option)\n" "{\necho Changing stream\nsay .$option\n}\n" "}\n" "conmenu menucallback\n", start - maxshowndemos, start + maxshowndemos ); if (start < 0) start = 0; if (start-maxshowndemos >= 0) QW_StuffcmdToViewer(v, "menutext 48 52 \"Prev\" \"PREV\"\n"); if (start+maxshowndemos <= cluster->availdemoscount) QW_StuffcmdToViewer(v, "menutext 152 52 \"Next\" \"NEXT\"\n"); for (i = start; i < start+maxshowndemos; i++) { if (i >= cluster->availdemoscount) break; if (cluster->availdemos[i].size < 1024) snprintf(sizestr, sizeof(sizestr), "%4ib", cluster->availdemos[i].size); else if (cluster->availdemos[i].size < 1024*1024) snprintf(sizestr, sizeof(sizestr), "%4ikb", cluster->availdemos[i].size/1024); else if (cluster->availdemos[i].size < 1024*1024*1024) snprintf(sizestr, sizeof(sizestr), "%4imb", cluster->availdemos[i].size/(1024*1024)); else// if (cluster->availdemos[i].size < 1024*1024*1024*1024) snprintf(sizestr, sizeof(sizestr), "%4igb", cluster->availdemos[i].size/(1024*1024*1024)); // else // *sizestr = 0; QW_StuffcmdToViewer(v, "menutext 32 %i \"%6s %-30s\" \"demo %s\"\n", (i-start)*8 + 52+16, sizestr, cluster->availdemos[i].name, cluster->availdemos[i].name); } } else if (!strncmp(command, ".tuidemos", 9)) { tuidemos: if (!*args) Cluster_BuildAvailableDemoList(cluster); if (v->menunum == MENU_DEMOS) QW_SetMenu(v, MENU_NONE); else QW_SetMenu(v, MENU_DEMOS); } else if (!strcmp(command, "admin")) { if (v->conmenussupported) goto guiadmin; else goto tuiadmin; } else if (!strcmp(command, "guiadmin")) { guiadmin: if (!*cluster->adminpassword) { /* I've removed the following from this function as it covered the menu (~Moodles): "menupic 16 4 gfx/qplaque.lmp\n" "menupic - 4 gfx/p_option.lmp\n" */ QW_StuffcmdToViewer(v, "alias menucallback\n" "{\n" "menuclear\n" "}\n" "conmenu menucallback\n" "menutext 72 48 \"No admin password is set\"\n" "menutext 72 56 \"Admin access is prohibited\"\n" ); } else if (v->isadmin) //already an admin, so don't show admin login screen QW_SetMenu(v, MENU_ADMIN); else { /* I've removed the following from this function as it covered the menu (~Moodles): "menupic 16 4 gfx/qplaque.lmp\n" "menupic - 4 gfx/p_option.lmp\n" */ QW_StuffcmdToViewer(v, "alias menucallback\n" "{\n" "menuclear\n" "if (option == \"log\")\n" "{\nsay $_password\n}\n" "set _password \"\"\n" "}\n" "conmenu menucallback\n" "menuedit 16 32 \" Password\" \"_password\"\n" "menutext 72 48 \"Log in QW\" log\n" "menutext 192 48 \"Cancel\" cancel\n" ); strcpy(v->expectcommand, "admin"); } } else if (!strcmp(command, "tuiadmin")) { tuiadmin: if (!*cluster->adminpassword) { /*if (Netchan_IsLocal(v->netchan.remote_address)) { Sys_Printf(cluster, "Local player %s logs in as admin\n", v->name); QW_SetMenu(v, MENU_ADMIN); v->isadmin = true; } else*/ QW_PrintfToViewer(v, "There is no admin password set\nYou may not log in.\n"); } else if (v->isadmin) QW_SetMenu(v, MENU_ADMIN); else { strcpy(v->expectcommand, "admin"); QW_StuffcmdToViewer(v, "echo Please enter the rcon password\nmessagemode\n"); } } else if (!strcmp(command, "reset")) { QW_SetCommentator(cluster, v, NULL); QW_SetViewersServer(cluster, v, NULL); QW_SetMenu(v, MENU_SERVERS); } else if (!strcmp(command, "connect") || !strcmp(command, "qw") || !strcmp(command, "observe") || !strcmp(command, "join")) { char buf[256]; int isjoin = false; if (!strcmp(command, "join") || !strcmp(command, "connect")) isjoin = true; snprintf(buf, sizeof(buf), "udp:%s", args); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_WHENEMPTY, !isjoin, false); if (qtv) { QW_SetMenu(v, MENU_NONE); QW_SetViewersServer(cluster, v, qtv); if (isjoin) qtv->controller = v; QW_PrintfToViewer(v, "Connected to %s\n", qtv->server); } else if (cluster->nouserconnects) QW_PrintfToViewer(v, "you may not do that here\n"); else QW_PrintfToViewer(v, "Failed to connect to server \"%s\", connection aborted\n", buf); } else if (!strcmp(command, "qtv")) { char buf[256]; snprintf(buf, sizeof(buf), "tcp:%s", args); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_WHENEMPTY, true, false); if (qtv) { QW_SetMenu(v, MENU_NONE); QW_SetViewersServer(cluster, v, qtv); QW_PrintfToViewer(v, "Connected to %s\n", qtv->server); } else if (cluster->nouserconnects) QW_PrintfToViewer(v, "Ask an admin to connect first\n"); else QW_PrintfToViewer(v, "Failed to connect to server \"%s\", connection aborted\n", buf); } else if (!strcmp(command, "qtvinfo")) { char buf[256]; snprintf(buf, sizeof(buf), "[QuakeTV] %s\n", qtv->server); // Print a short line with info about the server QW_PrintfToViewer(v, "%s", buf); } else if (!strcmp(command, "stream")) { int id; id = atoi(args); for (qtv = cluster->servers; qtv; qtv = qtv->next) { if (qtv->streamid == id) { break; } } if (qtv) { QW_SetMenu(v, MENU_NONE); QW_SetViewersServer(cluster, v, qtv); QW_PrintfToViewer(v, "Watching to %s\n", qtv->server); } else { QW_PrintfToViewer(v, "Stream \"%s\" not recognised. Stream id is invalid or terminated.\n", args); } } else if (!strcmp(command, "demo")) { char buf[256]; snprintf(buf, sizeof(buf), "file:%s", args); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_WHENEMPTY, true, false); if (qtv) { QW_SetMenu(v, MENU_NONE); QW_SetViewersServer(cluster, v, qtv); QW_PrintfToViewer(v, "Streaming from %s\n", qtv->server); } else QW_PrintfToViewer(v, "Demo \"%s\" does not exist on proxy\n", buf); } else if (!strcmp(command, "disconnect")) { QW_SetMenu(v, MENU_SERVERS); QW_SetViewersServer(cluster, v, NULL); QW_PrintfToViewer(v, "Connected\n"); } else if (!strcmp(command, "clients")) { viewer_t *ov; int skipfirst = 0; int printable = 30; int remaining = 0; for (ov = cluster->viewers; ov; ov = ov->next) { if (skipfirst > 0) { skipfirst--; } else if (printable > 0) { printable--; if (ov->server) { if (ov->server->controller == ov) QW_PrintfToViewer(v, "%i: %s: *%s\n", ov->userid, ov->name, ov->server->server); else QW_PrintfToViewer(v, "%i: %s: %s\n", ov->userid, ov->name, ov->server->server); } else QW_PrintfToViewer(v, "%i: %s: %s\n", ov->userid, ov->name, "None"); } else remaining++; } if (remaining) QW_PrintfToViewer(v, "%i clients not shown\n", remaining); } else if (!strcmp(command, "followid")) { int id = atoi(args); viewer_t *cv; for (cv = cluster->viewers; cv; cv = cv->next) { if (cv->userid == id) { QW_SetCommentator(cluster, v, cv); return; } } QW_PrintfToViewer(v, "Couldn't find that player\n"); QW_SetCommentator(cluster, v, NULL); } else if (!strcmp(command, "follow")) { int id = atoi(args); viewer_t *cv; for (cv = cluster->viewers; cv; cv = cv->next) { if (!strcmp(cv->name, args)) { QW_SetCommentator(cluster, v, cv); return; } } if (id) { for (cv = cluster->viewers; cv; cv = cv->next) { if (cv->userid == id) { QW_SetCommentator(cluster, v, cv); return; } } } QW_PrintfToViewer(v, "Couldn't find that player\n"); QW_SetCommentator(cluster, v, NULL); } else if (!strcmp(command, "follow")) { QW_SetCommentator(cluster, v, NULL); } else if (!strcmp(command, "bind")) { QW_StuffcmdToViewer(v, "bind uparrow +proxfwd\n"); QW_StuffcmdToViewer(v, "bind downarrow +proxback\n"); QW_StuffcmdToViewer(v, "bind rightarrow +proxright\n"); QW_StuffcmdToViewer(v, "bind leftarrow +proxleft\n"); QW_PrintfToViewer(v, "Keys bound\n"); } else if (!strcmp(command, "bsay")) { char buf[1024]; netmsg_t msg; viewer_t *ov; if (cluster->notalking) return; for (ov = cluster->viewers; ov; ov = ov->next) { InitNetMsg(&msg, buf, sizeof(buf)); WriteByte(&msg, svc_print); if (ov->netchan.isnqprotocol) WriteByte(&msg, 1); else { if (ov->conmenussupported) { WriteByte(&msg, 3); //PRINT_CHAT WriteString2(&msg, "[^sBQTV^s]^s^5"); } else { WriteByte(&msg, 2); //PRINT_HIGH WriteByte(&msg, 91+128); WriteString2(&msg, "BQTV"); WriteByte(&msg, 93+128); WriteByte(&msg, 0); WriteByte(&msg, svc_print); WriteByte(&msg, 3); //PRINT_CHAT } } WriteString2(&msg, v->name); WriteString2(&msg, ": "); // WriteString2(&msg, "\x8d "); WriteString2(&msg, args); WriteString(&msg, "\n"); if (msg.maxsize == msg.cursize) return; SendBufferToViewer(ov, msg.data, msg.cursize, true); } } else { QW_PrintfToViewer(v, "QTV Proxy command not recognised\n"); } } static void QTV_DoSay(cluster_t *cluster, sv_t *qtv, const char *viewername, char *message) { char buf[1024]; netmsg_t msg; viewer_t *ov; if (cluster->notalking) return; if (qtv) SV_SayToUpstream(qtv, message); for (ov = cluster->viewers; ov; ov = ov->next) { if (ov->server != qtv) continue; InitNetMsg(&msg, buf, sizeof(buf)); WriteByte(&msg, svc_print); if (ov->netchan.isnqprotocol) { WriteByte(&msg, 1); WriteByte(&msg, '['); WriteString2(&msg, "QTV"); WriteByte(&msg, ']'); } else { if (ov->conmenussupported) { WriteByte(&msg, 2); //PRINT_HIGH WriteByte(&msg, 91+128); WriteString2(&msg, "QTV"); WriteByte(&msg, 93+128); WriteString2(&msg, "^5"); } else { WriteByte(&msg, 2); //PRINT_HIGH WriteByte(&msg, 91+128); WriteString2(&msg, "QTV"); WriteByte(&msg, 93+128); WriteByte(&msg, 0); WriteByte(&msg, svc_print); WriteByte(&msg, 3); //PRINT_CHAT } } WriteString2(&msg, viewername); WriteString2(&msg, ": "); // WriteString2(&msg, "\x8d "); if (ov->conmenussupported) WriteString2(&msg, "^s"); WriteString2(&msg, message); WriteString(&msg, "\n"); SendBufferToViewer(ov, msg.data, msg.cursize, true); } } void QTV_Say(cluster_t *cluster, sv_t *qtv, viewer_t *v, char *message, qboolean noupwards) { char buf[1024]; if (message[strlen(message)-1] == '\"') message[strlen(message)-1] = '\0'; if (*v->expectcommand) { buf[sizeof(buf)-1] = '\0'; if (!strcmp(v->expectcommand, "hostname")) { strlcpy(cluster->hostname, message, sizeof(cluster->hostname)); } else if (!strcmp(v->expectcommand, "master")) { strlcpy(cluster->master, message, sizeof(cluster->master)); if (!strcmp(cluster->master, ".")) *cluster->master = '\0'; cluster->mastersendtime = cluster->curtime; } else if (!strcmp(v->expectcommand, "addserver")) { snprintf(buf, sizeof(buf), "tcp:%s", message); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_NO, false, false); if (qtv) { QW_SetViewersServer(cluster, v, qtv); QW_PrintfToViewer(v, "Connected to \"%s\"\n", message); } else QW_PrintfToViewer(v, "Failed to connect to server \"%s\", connection aborted\n", message); } else if (!strcmp(v->expectcommand, "admin")) { if (!strcmp(message, cluster->adminpassword)) { QW_SetMenu(v, MENU_ADMIN); v->isadmin = true; Sys_Printf(cluster, "Player %s logs in as admin\n", v->name); } else { QW_PrintfToViewer(v, "Admin password incorrect\n"); Sys_Printf(cluster, "Player %s gets incorrect admin password\n", v->name); } } else if (!strcmp(v->expectcommand, "insecadddemo")) { snprintf(buf, sizeof(buf), "file:%s", message); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_NO, false, false); if (!qtv) QW_PrintfToViewer(v, "Failed to play demo \"%s\"\n", message); else { QW_SetViewersServer(cluster, v, qtv); QW_PrintfToViewer(v, "Opened demo file \"%s\".\n", message); } } else if (!strcmp(v->expectcommand, "adddemo")) { snprintf(buf, sizeof(buf), "file:%s", message); qtv = QTV_NewServerConnection(cluster, 0, buf, "", false, AD_NO, false, false); if (!qtv) QW_PrintfToViewer(v, "Failed to play demo \"%s\"\n", message); else { QW_SetViewersServer(cluster, v, qtv); QW_PrintfToViewer(v, "Opened demo file \"%s\".\n", message); } } else if (!strcmp(v->expectcommand, "setmvdport")) { int newp = atoi(message); Net_TCPListen(cluster, newp, true); Net_TCPListen(cluster, newp, false); cluster->tcplistenportnum = newp; } else { QW_PrintfToViewer(v, "Command %s was not recognised\n", v->expectcommand); } *v->expectcommand = '\0'; return; } if (*message == '.') { if (message[1] == '.') //double it up to say it message++; else { //this is always execed (. is local server) QTV_SayCommand(cluster, qtv, v, message+1); return; } } else if (!strncmp(message, "proxy:", 6)) { //this is execed on the 'active' server if (qtv && (qtv->controller == v && !qtv->proxyisselected)) SendClientCommand(qtv, "say \"%s\"", message); else QTV_SayCommand(cluster, qtv, v, message+6); return; } else if (*message == ',') { if (message[1] == ',') //double up to say it message++; else { if (qtv && (qtv->controller == v && qtv->serverisproxy)) SendClientCommand(qtv, "say \"%s\"", message); else QTV_SayCommand(cluster, qtv, v, message+1); return; } } if (!strncmp(message, ".", 1)) message++; *v->expectcommand = '\0'; if (qtv && qtv->usequakeworldprotocols && !noupwards) { if (qtv->controller == v) { SendClientCommand(qtv, "say \"%s\"", message); if (cluster->notalking) return; } else { if (cluster->notalking) return; SendClientCommand(qtv, "say \"[%s]: %s\"", v->name, message); } //FIXME: we ought to broadcast this to everyone not watching that qtv. } else { // If the current viewer is the player, pass on the say_team if (qtv && qtv->controller == v) { SendClientCommand(qtv, "say_team \"%s\"", message); return; } QTV_DoSay(cluster, v->server, v->name, message); } } viewer_t *QW_IsOn(cluster_t *cluster, char *name) { viewer_t *v; for (v = cluster->viewers; v; v = v->next) if (!stricmp(v->name, name)) //this needs to allow dequakified names. return v; return NULL; } void QW_PrintfToViewer(viewer_t *v, char *format, ...) { int pos = 0; va_list argptr; char buf[1024]; buf[pos++] = svc_print; if (!v->netchan.isnqprotocol) buf[pos++] = 2; //PRINT_HIGH va_start (argptr, format); vsnprintf (buf+pos, sizeof(buf)-pos, format, argptr); va_end (argptr); SendBufferToViewer(v, buf, strlen(buf)+1, true); } void QW_StuffcmdToViewer(viewer_t *v, char *format, ...) { va_list argptr; char buf[1024]; va_start (argptr, format); vsnprintf (buf+1, sizeof(buf)-1, format, argptr); va_end (argptr); buf[0] = svc_stufftext; SendBufferToViewer(v, buf, strlen(buf)+1, true); } void QW_PositionAtIntermission(sv_t *qtv, viewer_t *v) { netmsg_t msg; char buf[7]; const intermission_t *spot; unsigned int pext1; if (qtv) { spot = BSP_IntermissionSpot(qtv->map.bsp); pext1 = qtv->pext1; } else { spot = &nullstreamspot; pext1 = 0; } v->origin[0] = spot->pos[0]; v->origin[1] = spot->pos[1]; v->origin[2] = spot->pos[2]; InitNetMsg(&msg, buf, sizeof(buf)); WriteByte (&msg, svc_setangle); WriteAngle(&msg, spot->angle[0], pext1); WriteAngle(&msg, spot->angle[1], pext1); WriteAngle(&msg, 0, pext1); SendBufferToViewer(v, msg.data, msg.cursize, true); } void ParseNQC(cluster_t *cluster, sv_t *qtv, viewer_t *v, netmsg_t *m) { char buf[MAX_NQMSGLEN]; netmsg_t msg; int mtype; while (m->readpos < m->cursize) { switch ((mtype=ReadByte(m))) { case clc_nop: break; case clc_stringcmd: ReadString (m, buf, sizeof(buf)); printf("stringcmd: %s\n", buf); if (!strcmp(buf, "new")) { if (qtv && qtv->parsingconnectiondata) QW_StuffcmdToViewer(v, "cmd new\n"); else { SendServerData(qtv, v); } } else if (!strncmp(buf, "prespawn", 8)) { msg.data = buf; msg.maxsize = sizeof(buf); msg.cursize = 0; msg.overflowed = 0; if (qtv) { SendCurrentBaselines(qtv, 64, &msg, msg.maxsize, 0); SendCurrentLightmaps(qtv, 64, &msg, msg.maxsize, 0); SendStaticSounds(qtv, 64, &msg, msg.maxsize, 0); SendStaticEntities(qtv, 64, &msg, msg.maxsize, 0); } WriteByte (&msg, svc_nqsignonnum); WriteByte (&msg, 2); SendBufferToViewer(v, msg.data, msg.cursize, true); } else if (!strncmp(buf, "setinfo", 5)) { #define TOKENIZE_PUNCTUATION "" int i; char arg[3][ARG_LEN]; char *command = buf; for (i = 0; i < 3; i++) { command = COM_ParseToken(command, arg[i], ARG_LEN, TOKENIZE_PUNCTUATION); } Info_SetValueForStarKey(v->userinfo, arg[1], arg[2], sizeof(v->userinfo)); ParseUserInfo(cluster, v); // Info_ValueForKey(v->userinfo, "name", v->name, sizeof(v->name)); if (v->server && v->server->controller == v) SendClientCommand(v->server, "%s", buf); } else if (!strncmp(buf, "name ", 5)) { Info_SetValueForStarKey(v->userinfo, "name", buf+5, sizeof(v->userinfo)); ParseUserInfo(cluster, v); if (v->server && v->server->controller == v) SendClientCommand(v->server, "setinfo name \"%s\"", v->name); } else if (!strncmp(buf, "color ", 6)) { /* fixme */ } else if (!strncmp(buf, "spawn", 5)) { msg.data = buf; msg.maxsize = sizeof(buf); msg.cursize = 0; msg.overflowed = 0; SendNQSpawnInfoToViewer(cluster, v, &msg); SendBufferToViewer(v, msg.data, msg.cursize, true); QW_PositionAtIntermission(qtv, v); v->thinksitsconnected = true; } else if (!strncmp(buf, "begin", 5)) { int oldmenu; v->thinksitsconnected = true; oldmenu = v->menunum; QW_SetMenu(v, MENU_NONE); QW_SetMenu(v, oldmenu); if (!v->server) QTV_Say(cluster, v->server, v, ".menu", false); } else if (!strncmp(buf, "say \".", 6)) QTV_Say(cluster, qtv, v, buf+5, false); else if (!strncmp(buf, "say .", 5)) QTV_Say(cluster, qtv, v, buf+4, false); else if (v->server && v == v->server->controller) SendClientCommand(v->server, "%s", buf); // else if (!strcmp(buf, "pause")) // qtv->errored = ERR_PAUSED; else if (!strncmp(buf, "say \"", 5)) QTV_Say(cluster, qtv, v, buf+5, false); else if (!strncmp(buf, "say ", 4)) QTV_Say(cluster, qtv, v, buf+4, false); else if (!strncmp(buf, "say_team \"", 10)) QTV_Say(cluster, qtv, v, buf+10, true); else if (!strncmp(buf, "say_team ", 9)) QTV_Say(cluster, qtv, v, buf+9, true); else { QW_PrintfToViewer(v, "Command not recognised\n"); Sys_Printf(cluster, "NQ client sent unrecognized stringcmd %s\n", buf); } break; case clc_disconnect: if (!v->drop) Sys_Printf(cluster, "NQ viewer %s disconnects\n", v->name); v->drop = true; return; case clc_move: v->ucmds[0] = v->ucmds[1]; v->ucmds[1] = v->ucmds[2]; ReadFloat(m); //time, for pings //three angles { unsigned int pext1; if (v->server) pext1 = v->server->pext1; else pext1 = 0; v->ucmds[2].angles[0] = ReadAngle(m, pext1); v->ucmds[2].angles[1] = ReadAngle(m, pext1); v->ucmds[2].angles[2] = ReadAngle(m, pext1); } //three direction values v->ucmds[2].forwardmove = ReadShort(m); v->ucmds[2].sidemove = ReadShort(m); v->ucmds[2].upmove = ReadShort(m); //one button v->ucmds[2].buttons = ReadByte(m); //one impulse v->ucmds[2].impulse = ReadByte(m); v->ucmds[2].msec = cluster->curtime - v->lasttime; v->lasttime = cluster->curtime; if (v->menunum) { int mb = 0; if (v->ucmds[2].forwardmove > 0) mb = MBTN_UP; if (v->ucmds[2].forwardmove < 0) mb = MBTN_DOWN; if (v->ucmds[2].sidemove > 0) mb = MBTN_RIGHT; if (v->ucmds[2].sidemove < 0) mb = MBTN_LEFT; if (v->ucmds[2].buttons & 2) mb = MBTN_ENTER; if (mb & ~v->menubuttons & MBTN_UP) v->menuop -= 1; if (mb & ~v->menubuttons & MBTN_DOWN) v->menuop += 1; if (mb & ~v->menubuttons & MBTN_RIGHT) Menu_Enter(cluster, v, 1); if (mb & ~v->menubuttons & MBTN_LEFT) Menu_Enter(cluster, v, -1); if (mb & ~v->menubuttons & MBTN_ENTER) Menu_Enter(cluster, v, 0); if (v->menubuttons != mb) v->menuspamtime = cluster->curtime-1; v->ucmds[2].forwardmove = 0; v->ucmds[2].sidemove = 0; v->ucmds[2].buttons = 0; v->menubuttons = mb; } else v->menubuttons = ~0; //so nothing gets instantly flagged once we enter a menu. if (v->server && v->server->controller == v) return; PMove(v, &v->ucmds[2]); if ((v->ucmds[1].buttons&1) != (v->ucmds[2].buttons&1) && (v->ucmds[2].buttons&1)) { if(v->server) { int t; for (t = v->trackplayer+1; t < MAX_CLIENTS; t++) { if (v->server->map.players[t].active) { break; } }/* if (t == MAX_CLIENTS) for (t = 0; t <= v->trackplayer; t++) { if (v->server->players[t].active) { break; } } */ if (t >= MAX_CLIENTS) { if (v->trackplayer >= 0) QW_PrintfToViewer(v, "Stopped tracking\n"); else QW_PrintfToViewer(v, "Not tracking\n"); v->trackplayer = -1; //no trackable players found } else { v->trackplayer = t; Info_ValueForKey(v->server->map.players[t].userinfo, "name", buf, sizeof(buf)); QW_PrintfToViewer(v, "Now tracking: %s\n", buf); } } } if ((v->ucmds[1].buttons&2) != (v->ucmds[2].buttons&2) && (v->ucmds[2].buttons&2)) { if (!v->server && !v->menunum) QW_SetMenu(v, MENU_DEFAULT); if(v->server) { int t; if (v->trackplayer < 0) { for (t = MAX_CLIENTS-1; t >= v->trackplayer; t--) { if (v->server->map.players[t].active) { break; } } } else { for (t = v->trackplayer-1; t >= 0; t--) { if (v->server->map.players[t].active) { break; } } } if (t < 0) { v->trackplayer = -1; //no trackable players found QW_PrintfToViewer(v, "Not tracking\n"); } else { v->trackplayer = t; Info_ValueForKey(v->server->map.players[t].userinfo, "name", buf, sizeof(buf)); QW_PrintfToViewer(v, "Now tracking: %s\n", buf); } } } if (v->trackplayer > -1 && v->server) { v->origin[0] = v->server->map.players[v->trackplayer].current.origin[0]; v->origin[1] = v->server->map.players[v->trackplayer].current.origin[1]; v->origin[2] = v->server->map.players[v->trackplayer].current.origin[2]; } break; default: Sys_Printf(cluster, "Bad message type %i\n", mtype); return; } } } void ParseQWC(cluster_t *cluster, sv_t *qtv, viewer_t *v, netmsg_t *m) { // usercmd_t oldest, oldcmd, newcmd; char buf[1024]; netmsg_t msg; int i; int iscont; v->delta_frames[v->netchan.incoming_sequence & (ENTITY_FRAMES-1)] = -1; while (m->readpos < m->cursize) { i = ReadByte(m); switch (i) { case clc_nop: return; case clc_delta: v->delta_frames[v->netchan.incoming_sequence & (ENTITY_FRAMES-1)] = ReadByte(m); break; case clc_stringcmd: ReadString (m, buf, sizeof(buf)); iscont = v->server && v->server->controller == v; if (!strncmp(buf, "cmd ", 4)) { if (v->server && v->server->controller == v) SendClientCommand(v->server, "%s", buf+4); } else if (iscont && !strncmp(buf, "pext ", 5)) { //FIXME: include ones we can parse properly... SendClientCommand(v->server, "pext"); } else if (!iscont && !strcmp(buf, "new")) { if (qtv && qtv->parsingconnectiondata) QW_StuffcmdToViewer(v, "cmd new\n"); else { // QW_StuffcmdToViewer(v, "//querycmd conmenu\n"); SendServerData(qtv, v); } } else if (!iscont && !strncmp(buf, "modellist ", 10)) { char *cmd = buf+10; int svcount = atoi(cmd); int first; while((*cmd >= '0' && *cmd <= '9') || *cmd == '-') cmd++; first = atoi(cmd); InitNetMsg(&msg, buf, sizeof(buf)); if (svcount != v->servercount) { //looks like we changed map without them. SendServerData(qtv, v); return; } if (!qtv) SendList(qtv, first, ConnectionlessModelList, svc_modellist, &msg); else SendList(qtv, first, qtv->map.modellist, svc_modellist, &msg); SendBufferToViewer(v, msg.data, msg.cursize, true); } else if (!iscont && !strncmp(buf, "soundlist ", 10)) { char *cmd = buf+10; int svcount = atoi(cmd); int first; while((*cmd >= '0' && *cmd <= '9') || *cmd == '-') cmd++; first = atoi(cmd); InitNetMsg(&msg, buf, sizeof(buf)); if (svcount != v->servercount) { //looks like we changed map without them. SendServerData(qtv, v); return; } if (!qtv) SendList(qtv, first, ConnectionlessSoundList, svc_soundlist, &msg); else SendList(qtv, first, qtv->map.soundlist, svc_soundlist, &msg); SendBufferToViewer(v, msg.data, msg.cursize, true); } else if (!iscont && !strncmp(buf, "prespawn", 8)) { char skin[128]; if (atoi(buf + 9) != v->servercount) SendServerData(qtv, v); //we're old. else { int crc; int r; char *s; s = buf+8; while(*s == ' ') s++; while((*s >= '0' && *s <= '9') || *s == '-') s++; while(*s == ' ') s++; r = atoi(s); if (r == 0) { while((*s >= '0' && *s <= '9') || *s == '-') s++; while(*s == ' ') s++; crc = atoi(s); if (qtv && qtv->controller == v) { if (!qtv->map.bsp) { //warning do we still actually need to do this ourselves? Or can we just forward what the user stated? QW_PrintfToViewer(v, "QTV doesn't have that map (%s), sorry.\n", qtv->map.modellist[1].name); qtv->errored = ERR_DROP; } else if (crc != BSP_Checksum(qtv->map.bsp)) { QW_PrintfToViewer(v, "QTV's map (%s) does not match the servers\n", qtv->map.modellist[1].name); qtv->errored = ERR_DROP; } } } InitNetMsg(&msg, buf, sizeof(buf)); if (qtv) { r = Prespawn(qtv, v->netchan.message.cursize, &msg, r, v->thisplayer); SendBufferToViewer(v, msg.data, msg.cursize, true); } else { r = SendCurrentUserinfos(qtv, v->netchan.message.cursize, &msg, r, v->thisplayer); if (r > MAX_CLIENTS) r = -1; SendBufferToViewer(v, msg.data, msg.cursize, true); } if (r < 0) sprintf(skin, "%ccmd spawn\n", svc_stufftext); else sprintf(skin, "%ccmd prespawn %i %i\n", svc_stufftext, v->servercount, r); SendBufferToViewer(v, skin, strlen(skin)+1, true); } } else if (!iscont && !strncmp(buf, "spawn", 5)) { char skin[64]; sprintf(skin, "%cskins\n", svc_stufftext); SendBufferToViewer(v, skin, strlen(skin)+1, true); QW_PositionAtIntermission(qtv, v); } else if (iscont && !strncmp(buf, "begin", 5)) { //the client made it! v->thinksitsconnected = true; qtv->parsingconnectiondata = false; SendClientCommand(v->server, "%s", buf); } else if (!iscont && !strncmp(buf, "begin", 5)) { int oldmenu; viewer_t *com; if (atoi(buf+6) != v->servercount) SendServerData(qtv, v); //this is unfortunate! else { v->thinksitsconnected = true; if (qtv && qtv->map.ispaused) { char msgb[] = {svc_setpause, 1}; SendBufferToViewer(v, msgb, sizeof(msgb), true); } oldmenu = v->menunum; QW_SetMenu(v, MENU_NONE); QW_SetMenu(v, oldmenu); com = v->commentator; v->commentator = NULL; QW_SetCommentator(cluster, v, com); if (v->firstconnect) { QW_StuffcmdToViewer(v, "f_qtv\n"); v->firstconnect = false; } if (!v->server) QTV_Say(cluster, v->server, v, ".menu", false); } } else if (!strncmp(buf, "download", 8)) { netmsg_t m; InitNetMsg(&m, buf, sizeof(buf)); WriteByte(&m, svc_download); WriteShort(&m, -1); WriteByte(&m, 0); SendBufferToViewer(v, m.data, m.cursize, true); } else if (!strncmp(buf, "drop", 4)) { if (!v->drop) Sys_Printf(cluster, "QW viewer %s disconnects\n", v->name); v->drop = true; } else if (!strcmp(buf, "pause")) { if (qtv->errored == ERR_PAUSED) qtv->errored = ERR_NONE; else if (qtv->sourcetype == SRC_DEMO && (1 || v->isadmin)) qtv->errored = ERR_PAUSED; else QW_PrintfToViewer(v, "You may not pause this stream\n"); } else if (!strncmp(buf, "ison", 4)) { viewer_t *other; if ((other = QW_IsOn(cluster, buf+5))) { if (!other->server) QW_PrintfToViewer(v, "%s is on the proxy, but not yet watching a game\n", other->name); else QW_PrintfToViewer(v, "%s is watching %s\n", buf+5, other->server->server); } else QW_PrintfToViewer(v, "%s is not on the proxy, sorry\n", buf+5); //the apology is to make the alternatives distinct. } else if (!strncmp(buf, "ptrack ", 7)) { v->trackplayer = atoi(buf+7); // if (v->trackplayer != MAX_CLIENTS-2) // QW_SetCommentator(v, NULL); } else if (!strncmp(buf, "ptrack", 6)) { v->trackplayer = -1; QW_SetCommentator(cluster, v, NULL); //clicking out will stop the client from tracking thier commentator } else if (!iscont && !strncmp(buf, "pings", 5)) { } else if (!strncmp(buf, "say \"", 5)) QTV_Say(cluster, qtv, v, buf+5, false); else if (!strncmp(buf, "say ", 4)) QTV_Say(cluster, qtv, v, buf+4, false); else if (!strncmp(buf, "say_team \"", 10)) QTV_Say(cluster, qtv, v, buf+10, true); else if (!strncmp(buf, "say_team ", 9)) QTV_Say(cluster, qtv, v, buf+9, true); else if (!strncmp(buf, "servers", 7)) { QW_SetMenu(v, MENU_SERVERS); } else if (!strncmp(buf, "setinfo", 5)) { #define TOKENIZE_PUNCTUATION "" int i; char arg[3][ARG_LEN]; char *command = buf; for (i = 0; i < 3; i++) { command = COM_ParseToken(command, arg[i], ARG_LEN, TOKENIZE_PUNCTUATION); } Info_SetValueForStarKey(v->userinfo, arg[1], arg[2], sizeof(v->userinfo)); ParseUserInfo(cluster, v); // Info_ValueForKey(v->userinfo, "name", v->name, sizeof(v->name)); if (v->server && v->server->controller == v) SendClientCommand(v->server, "%s", buf); } else if (!strncmp(buf, "cmdsupported ", 13)) { if (!strcmp(buf+13, "conmenu")) v->conmenussupported = true; else if (v->server && v->server->controller == v) SendClientCommand(v->server, "%s", buf); } else if (!qtv) { //all the other things need an active server. QW_PrintfToViewer(v, "Choose a server first DEBUG:(%s)\n", buf); } else if (!strncmp(buf, "serverinfo", 5)) { char *key, *value, *end; int len; netmsg_t m; InitNetMsg(&m, buf, sizeof(buf)); WriteByte(&m, svc_print); WriteByte(&m, 2); end = qtv->map.serverinfo; for(;;) { if (!*end) break; key = end; value = strchr(key+1, '\\'); if (!value) break; end = strchr(value+1, '\\'); if (!end) end = value+strlen(value); len = value-key; key++; while(*key != '\\' && *key) WriteByte(&m, *key++); for (; len < 20; len++) WriteByte(&m, ' '); value++; while(*value != '\\' && *value) WriteByte(&m, *value++); WriteByte(&m, '\n'); } WriteByte(&m, 0); // WriteString(&m, qtv->serverinfo); SendBufferToViewer(v, m.data, m.cursize, true); } else { if (v->server && v->server->controller == v) { SendClientCommand(v->server, "%s", buf); } else Sys_Printf(cluster, "Client sent unknown string command: %s\n", buf); } break; case clc_move: v->lost = ReadByte(m); ReadByte(m); ReadDeltaUsercmd(m, &nullcmd, &v->ucmds[0]); ReadDeltaUsercmd(m, &v->ucmds[0], &v->ucmds[1]); ReadDeltaUsercmd(m, &v->ucmds[1], &v->ucmds[2]); PMove(v, &v->ucmds[2]); if (v->ucmds[0].buttons & 2) { if (!v->server && !v->menunum) QW_SetMenu(v, MENU_DEFAULT); } break; case clc_tmove: v->origin[0] = ReadCoord(m, v->pext1); v->origin[1] = ReadCoord(m, v->pext1); v->origin[2] = ReadCoord(m, v->pext1); break; case clc_upload: Sys_Printf(cluster, "Client uploads are not supported from %s\n", v->name); v->drop = true; return; default: Sys_Printf(cluster, "bad clc from %s\n", v->name); v->drop = true; return; } } } static const char dropcmd[] = {svc_stufftext, 'd', 'i', 's', 'c', 'o', 'n', 'n', 'e', 'c', 't', '\n', '\0'}; void QW_FreeViewer(cluster_t *cluster, viewer_t *viewer) { char buf[1024]; viewer_t *oview; int i; //note: unlink them yourself. snprintf(buf, sizeof(buf), "%cQTV%c%s leaves the proxy\n", 91+128, 93+128, viewer->name); QW_StreamPrint(cluster, viewer->server, NULL, buf); Sys_Printf(cluster, "Dropping viewer %s\n", viewer->name); //spam them thrice, then forget about them Netchan_Transmit(cluster, &viewer->netchan, strlen(dropcmd)+1, dropcmd); Netchan_Transmit(cluster, &viewer->netchan, strlen(dropcmd)+1, dropcmd); Netchan_Transmit(cluster, &viewer->netchan, strlen(dropcmd)+1, dropcmd); for (i = 0; i < MAX_BACK_BUFFERS; i++) { if (viewer->backbuf[i].data) free(viewer->backbuf[i].data); } if (viewer->server) { if (viewer->server->controller == viewer) { if (viewer->server->autodisconnect == AD_WHENEMPTY) viewer->server->errored = ERR_DROP; else viewer->server->controller = NULL; } viewer->server->numviewers--; } for (oview = cluster->viewers; oview; oview = oview->next) { if (oview->commentator == viewer) QW_SetCommentator(cluster, oview, NULL); } free(viewer); cluster->numviewers--; } void SendViewerPackets(cluster_t *cluster, viewer_t *v) { char buffer[MAX_MSGLEN]; netmsg_t m; int read; sv_t *useserver; v->drop |= v->netchan.drop; if (v->timeout < cluster->curtime) { Sys_Printf(cluster, "Viewer %s timed out\n", v->name); v->drop = true; } if (v->netchan.isnqprotocol && (v->server == NULL || v->server->parsingconnectiondata)) { v->maysend = (v->nextpacket < cluster->curtime); } if (!Netchan_CanPacket(&v->netchan)) { return; } if (v->maysend) //don't send incompleate connection data. { // printf("maysend (%i, %i)\n", cluster->curtime, v->nextpacket); v->nextpacket = v->nextpacket + 1000/NQ_PACKETS_PER_SECOND; if (v->nextpacket < cluster->curtime) v->nextpacket = cluster->curtime; if (v->nextpacket > cluster->curtime+1000/NQ_PACKETS_PER_SECOND) v->nextpacket = cluster->curtime+1000/NQ_PACKETS_PER_SECOND; useserver = v->server; if (useserver && useserver->controller == v) v->netchan.outgoing_sequence = useserver->netchan.incoming_sequence; else { if (useserver && useserver->parsingconnectiondata) useserver = NULL; } v->maysend = false; InitNetMsg(&m, buffer, v->netchan.maxdatagramlen); m.cursize = 0; if (v->thinksitsconnected) { if (v->netchan.isnqprotocol) SendNQPlayerStates(cluster, useserver, v, &m); else SendPlayerStates(useserver, v, &m); UpdateStats(useserver, v); } if (v->menunum) Menu_Draw(cluster, v); else if (v->server && v->server->parsingconnectiondata && v->server->controller != v) { WriteByte(&m, svc_centerprint); WriteString(&m, v->server->status); } if (v->server && v->server->controller == v) { int saved; saved = v->netchan.incoming_sequence; v->netchan.incoming_sequence = v->server->netchan.incoming_sequence; Netchan_Transmit(cluster, &v->netchan, m.cursize, m.data); v->netchan.incoming_sequence = saved; } else Netchan_Transmit(cluster, &v->netchan, m.cursize, m.data); if (!v->netchan.message.cursize && v->backbuffered) {//shift the backbuffers around memcpy(v->netchan.message.data, v->backbuf[0].data, v->backbuf[0].cursize); v->netchan.message.cursize = v->backbuf[0].cursize; for (read = 0; read < v->backbuffered; read++) { if (read == v->backbuffered-1) { v->backbuf[read].cursize = 0; } else { memcpy(v->backbuf[read].data, v->backbuf[read+1].data, v->backbuf[read+1].cursize); v->backbuf[read].cursize = v->backbuf[read+1].cursize; } } v->backbuffered--; } } // else // printf("maynotsend (%i, %i)\n", cluster->curtime, v->nextpacket); } void QW_ProcessUDPPacket(cluster_t *cluster, netmsg_t *m, netadr_t from) { char tempbuffer[256]; int qport; viewer_t *v; sv_t *useserver; if (*(int*)m->data == -1) { //connectionless message if (TURN_IsRequest(cluster, m, &from)) return; m->readpos = 0; ConnectionlessPacket(cluster, &from, m); return; } if (m->cursize < 10) //otherwise it's a runt or bad. { qport = 0; } else { //read the qport ReadLong(m); ReadLong(m); qport = ReadShort(m); } for (v = cluster->viewers; v; v = v->next) { if (v->netchan.isnqprotocol) { if (Net_CompareAddress(&v->netchan.remote_address, &from, 0, 1)) { if (NQNetchan_Process(cluster, &v->netchan, m)) { useserver = v->server; if (useserver && useserver->parsingconnectiondata) useserver = NULL; v->timeout = cluster->curtime + 15*1000; ParseNQC(cluster, useserver, v, m); if (v->server && v->server->controller == v) { QTV_Run(v->server); } } return; } } else { if (Net_CompareAddress(&v->netchan.remote_address, &from, v->netchan.qport, qport)) { if (v->server && v->server->controller == v && v->maysend) SendViewerPackets(cluster, v); //do this before we read the new sequences if (Netchan_Process(&v->netchan, m)) { useserver = v->server; if (useserver && useserver->parsingconnectiondata && useserver->controller != v) useserver = NULL; v->timeout = cluster->curtime + 15*1000; if (v->server && v->server->controller == v) { // v->maysend = true; v->server->maysend = true; } else { v->netchan.outgoing_sequence = v->netchan.incoming_sequence; //compensate for client->server packetloss. if (!v->server) v->maysend = true; else if (!v->chokeme || !cluster->chokeonnotupdated) { v->maysend = true; v->chokeme = cluster->chokeonnotupdated; } } ParseQWC(cluster, useserver, v, m); if (v->server && v->server->controller == v) { QTV_Run(v->server); } } return; } } } m->readpos = 0; if (TURN_IsRequest(cluster, m, &from)) return; m->readpos = 0; if (cluster->allownqclients) { unsigned int ctrl; //NQ connectionless packet? ctrl = ReadLong(m); ctrl = SwapLong(ctrl); if (ctrl & NETFLAG_CTL) { //looks hopeful switch(ReadByte(m)) { case CCREQ_SERVER_INFO: ReadString(m, tempbuffer, sizeof(tempbuffer)); if (!strcmp(tempbuffer, NQ_NETCHAN_GAMENAME)) { m->cursize = 0; WriteLong(m, 0); WriteByte(m, CCREP_SERVER_INFO); WriteString(m, "??"); WriteString(m, cluster->hostname); WriteString(m, "Quake TV"); WriteByte(m, cluster->numviewers>255?255:cluster->numviewers); WriteByte(m, cluster->maxviewers>255?255:cluster->maxviewers); WriteByte(m, NQ_NETCHAN_VERSION); *(int*)m->data = BigLong(NETFLAG_CTL | m->cursize); NET_SendPacket(cluster, NET_ChooseSocket(cluster->qwdsocket, &from, from), m->cursize, m->data, from); } break; case CCREQ_CONNECT: ReadString(m, tempbuffer, sizeof(tempbuffer)); if (!strcmp(tempbuffer, NQ_NETCHAN_GAMENAME)) { if (ReadByte(m) == NQ_NETCHAN_VERSION) { //proquake extensions /*int mod =*/ ReadByte(m); /*int modver =*/ ReadByte(m); /*int flags =*/ ReadByte(m); /*int passwd =*/ ReadLong(m); //fte extension, sent so that dual-protocol servers will not create connections for dual-protocol clients //the connectnq command disables this (as well as the qw hand shake) if you really want to use nq protocols with fte clients ReadString(m, tempbuffer, sizeof(tempbuffer)); if (!strncmp(tempbuffer, "getchallenge", 12)) break; //drop any old nq clients from this address for (v = cluster->viewers; v; v = v->next) { if (v->netchan.isnqprotocol) { if (Net_CompareAddress(&v->netchan.remote_address, &from, 0, 1)) { Sys_Printf(cluster, "Dup connect from %s\n", v->name); v->drop = true; } } } NewNQClient(cluster, &from); } } break; default: break; } } } } void QW_TCPConnection(cluster_t *cluster, oproxy_t *sock, char *initialstreamname) { int alen; tcpconnect_t *tc; //clean up the pending source a bit... if (sock->srcfile) fclose(sock->srcfile), sock->srcfile = NULL; if (sock->drop) tc = NULL; //FIXME else tc = malloc(sizeof(*tc)); if (!tc) { closesocket(sock->sock); free(initialstreamname); } else { //okay, we're adding this as a client //try and disable nagle, we don't really want to be wasting time not sending anything. int _true = 1; setsockopt(sock->sock, IPPROTO_TCP, TCP_NODELAY, (char *)&_true, sizeof(_true)); tc->sock = sock->sock; tc->websocket = sock->websocket; //copy it over tc->inbuffersize = sock->inbuffersize; memcpy(tc->inbuffer, sock->inbuffer, tc->inbuffersize); tc->outbuffersize = sock->buffersize; memcpy(tc->outbuffer, sock->buffer+sock->bufferpos, tc->outbuffersize); memset(&tc->peeraddr, 0, sizeof(tc->peeraddr)); tc->peeraddr.tcpcon = tc; alen = sizeof(tc->peeraddr.sockaddr); getpeername(sock->sock, (struct sockaddr*)&tc->peeraddr.sockaddr, &alen); tc->initialstreamname = initialstreamname; tc->next = cluster->tcpconnects; cluster->tcpconnects = tc; } //okay, we're done with it. free(sock); cluster->numproxies--; } void QW_UpdateUDPStuff(cluster_t *cluster) { char buffer[MAX_MSGLEN]; //contains read info netadr_t from; int fromsize = sizeof(from.sockaddr); int read; netmsg_t m; int socketno; tcpconnect_t *tc, **l; viewer_t *v, *f; if (*cluster->master && (cluster->curtime > cluster->mastersendtime || cluster->mastersendtime > cluster->curtime + 4*1000*60)) //urm... time wrapped? { if (NET_StringToAddr(cluster->master, &from, 27000)) { if (cluster->turnenabled) sprintf(buffer, "\377\377\377\377""heartbeat FTEMaster c=%s\n", cluster->chalkey); //fill buffer with a heartbeat else if (cluster->protocolname) sprintf(buffer, "\377\377\377\377""heartbeat Darkplaces\n"); //older, broader compatibility. else sprintf(buffer, "a\n%i\n0\n", cluster->mastersequence++); //fill buffer with a heartbeat //why is there no \xff\xff\xff\xff ?.. NET_SendPacket(cluster, NET_ChooseSocket(cluster->qwdsocket, &from, from), strlen(buffer), buffer, from); } else Sys_Printf(cluster, "Cannot resolve master %s\n", cluster->master); cluster->mastersendtime = cluster->curtime + 3*1000*60; //3 minuites. } /* initialised for reading */ InitNetMsg(&m, buffer, sizeof(buffer)); socketno = 0; for (;;) { if (cluster->qwdsocket[socketno] == INVALID_SOCKET) { socketno++; if (socketno >= SOCKETGROUPS) break; continue; } memset(&from, 0, sizeof(from)); read = recvfrom(cluster->qwdsocket[socketno], buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&from.sockaddr, (unsigned*)&fromsize); if (read < 0) //it's bad. { socketno++; if (socketno >= SOCKETGROUPS) break; continue; } if (read <= 5) //otherwise it's a runt or bad. { if (read == 1 && *buffer == 'l') { //ffs. easier to just fix it up here. buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0xff; buffer[4] = 'l'; read = 5; } else continue; } m.cursize = read; m.data = buffer; m.readpos = 0; buffer[m.cursize] = 0; //make sure its null terminated. QW_ProcessUDPPacket(cluster, &m, from); } for (tc = cluster->tcpconnects; tc; tc = tc->next) { if (tc->outbuffersize) { int clen = send(tc->sock, tc->outbuffer, tc->outbuffersize, 0); if (clen > 0) { memmove(tc->outbuffer, tc->outbuffer+clen, tc->outbuffersize-clen); tc->outbuffersize-=clen; } } } for (l = &cluster->tcpconnects; *l; ) { int clen; tc = *l; read = sizeof(tc->inbuffer) - tc->inbuffersize; read = NET_WebSocketRecv(tc->sock, &tc->websocket, tc->inbuffer+tc->inbuffersize, read, &clen); if (read > 0) tc->inbuffersize += read; if (read == 0 || read < 0) { if (read == 0 || qerrno != NET_EWOULDBLOCK) { *l = tc->next; if (tc->sock != INVALID_SOCKET) closesocket(tc->sock); free(tc->initialstreamname); free(tc); continue; } } if (clen >= 0) { /*if it really is a webclient connection, then the stream will be packetized already so we don't waste extra space*/ m.data = tc->inbuffer; m.readpos = 0; m.cursize = read; QW_ProcessUDPPacket(cluster, &m, tc->peeraddr); memmove(tc->inbuffer, tc->inbuffer+read, tc->inbuffersize - (read)); tc->inbuffersize -= read; continue; //ask to read the next packet } else { while (tc->inbuffersize >= 2) { read = (tc->inbuffer[0]<<8) | tc->inbuffer[1]; if (tc->inbuffersize >= 2+read) { m.data = tc->inbuffer+2; m.readpos = 0; m.cursize = read; QW_ProcessUDPPacket(cluster, &m, tc->peeraddr); memmove(tc->inbuffer, tc->inbuffer+2+read, tc->inbuffersize - (2+read)); tc->inbuffersize -= 2+read; } else break; } } l = &(*l)->next; } if (cluster->viewers && cluster->viewers->drop) { // Sys_Printf(cluster, "Dropping viewer %s\n", v->name); f = cluster->viewers; cluster->viewers = f->next; QW_FreeViewer(cluster, f); } for (v = cluster->viewers; v; v = v->next) { if (v->next && v->next->drop) { //free the next/ // Sys_Printf(cluster, "Dropping viewer %s\n", v->name); f = v->next; v->next = f->next; QW_FreeViewer(cluster, f); } SendViewerPackets(cluster, v); } } ================================================ FILE: fteqtv/rcon.c ================================================ /* Copyright (C) 1996-1997 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. */ #include "qtv.h" #include #ifdef _WIN32 #include #else #include #endif #include "bsd_string.h" #define MAX_INFO_KEY 64 #if defined(SVNREVISION) && defined(SVNDATE) #define QTVBUILD STRINGIFY(SVNREVISION)", "STRINGIFY(SVNDATE) #elif defined(SVNREVISION) #define QTVBUILD STRINGIFY(SVNREVISION)", "__DATE__ #else #define QTVBUILD __DATE__ #endif //I apologise for this if it breaks your formatting or anything #define HELPSTRING "\ FTEQTV proxy commands: (build "QTVBUILD")\n\ ----------------------\n\ connect, qtv, addserver\n\ connect to a MVD stream (TCP)\n\ qtvlist\n\ lists available streams on a proxy\n\ qw\n\ connect to a server as a player (UDP)\n\ adddemo\n\ play a demo from a MVD file\n\ port\n\ UDP port for QuakeWorld client connections\n\ mvdport\n\ specify TCP port for MVD broadcasting\n\ maxviewers, maxproxies\n\ limit number of connections\n\ status, choke, late, talking, nobsp, reconnect, exec, password, master, hostname, record, stop, quit\n\ other random commands\n\ \n" char *Info_ValueForKey (char *s, const char *key, char *buffer, int buffersize) { char pkey[1024]; char *o; if (*s == '\\') s++; while (1) { o = pkey; while (*s != '\\') { if (!*s) { *buffer='\0'; return buffer; } *o++ = *s++; if (o+2 >= pkey+sizeof(pkey)) //hrm. hackers at work.. { *buffer='\0'; return buffer; } } *o = 0; s++; o = buffer; while (*s != '\\' && *s) { if (!*s) { *buffer='\0'; return buffer; } *o++ = *s++; if (o+2 >= buffer+buffersize) //hrm. hackers at work.. { *buffer='\0'; return buffer; } } *o = 0; if (!strcmp (key, pkey) ) return buffer; if (!*s) { *buffer='\0'; return buffer; } s++; } } void Info_RemoveKey (char *s, const char *key) { char *start; char pkey[1024]; char value[1024]; char *o; if (strstr (key, "\\")) { // printf ("Key has a slash\n"); return; } while (1) { start = s; if (*s == '\\') s++; o = pkey; while (*s != '\\') { if (!*s) return; *o++ = *s++; } *o = 0; s++; o = value; while (*s != '\\' && *s) { if (!*s) return; *o++ = *s++; } *o = 0; if (!strcmp (key, pkey) ) { //strip out the value by copying the next string over the top of this one //(we were using strcpy, but valgrind moans and glibc fucks it up. they should not overlap... so use our own crappy inefficient alternative) while(*s) *start++ = *s++; *start = 0; return; } if (!*s) return; } } void Info_SetValueForStarKey (char *s, const char *key, const char *value, int maxsize) { char newv[1024], *v; int c; #ifdef SERVERONLY extern cvar_t sv_highchars; #endif if (strstr (key, "\\") || strstr (value, "\\") ) { // printf ("Key has a slash\n"); return; } if (strstr (key, "\"") || strstr (value, "\"") ) { // printf ("Key has a quote\n"); return; } if (strlen(key) >= MAX_INFO_KEY || strlen(value) >= MAX_INFO_KEY) { // printf ("Key or value is too long\n"); return; } // this next line is kinda trippy if (*(v = Info_ValueForKey(s, key, newv, sizeof(newv)))) { // key exists, make sure we have enough room for new value, if we don't, // don't change it! if (strlen(value) - strlen(v) + strlen(s) + 1 > maxsize) { // Con_Printf ("Info string length exceeded\n"); return; } } Info_RemoveKey (s, key); if (!value || !strlen(value)) return; snprintf (newv, sizeof(newv)-1, "\\%s\\%s", key, value); if ((int)(strlen(newv) + strlen(s) + 1) > maxsize) { // printf ("info buffer is too small\n"); return; } // only copy ascii values s += strlen(s); v = newv; while (*v) { c = (unsigned char)*v++; // c &= 127; // strip high bits if (c > 13) // && c < 127) *s++ = c; } *s = 0; } #define DEFAULT_PUNCTUATION "(,{})(\':;=!><&|+" char *COM_ParseToken (char *data, char *out, int outsize, const char *punctuation) { int c; int len; if (!punctuation) punctuation = DEFAULT_PUNCTUATION; len = 0; out[0] = 0; if (!data) return NULL; // skip whitespace skipwhite: while ( (c = *data) <= ' ') { if (c == 0) return NULL; // end of file; data++; } // skip // comments if (c=='/') { if (data[1] == '/') { while (*data && *data != '\n') data++; goto skipwhite; } else if (data[1] == '*') { data+=2; while (*data && (*data != '*' || data[1] != '/')) data++; data+=2; goto skipwhite; } } // handle quoted strings specially if (c == '\"') { data++; while (1) { if (len >= outsize-1) { out[len] = '\0'; return data; } c = *data++; if (c=='\"' || !c) { out[len] = 0; return data; } out[len] = c; len++; } } // parse single characters if (strchr(punctuation, c)) { out[len] = c; len++; out[len] = 0; return data+1; } // parse a regular word do { if (len >= outsize-1) break; out[len] = c; data++; len++; c = *data; if (strchr(punctuation, c)) break; } while (c>32); out[len] = 0; return data; } void Cmd_Printf(cmdctxt_t *ctx, char *fmt, ...) { va_list argptr; char string[2048]; va_start (argptr, fmt); vsnprintf (string, sizeof(string)-1, fmt,argptr); string[sizeof(string)-1] = 0; va_end (argptr); if (ctx->printfunc) ctx->printfunc(ctx, string); else if (ctx->qtv) QTV_Printf(ctx->qtv, "%s", string); else Sys_Printf(ctx->cluster, "%s", string); } void Cmd_Hostname(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) < 2) { if (*ctx->cluster->hostname) Cmd_Printf(ctx, "Current hostname is \"%s\"\n", ctx->cluster->hostname); else Cmd_Printf(ctx, "No master server is currently set.\n"); } else { strlcpy(ctx->cluster->hostname, Cmd_Argv(ctx, 1), sizeof(ctx->cluster->hostname)); Cmd_Printf(ctx, "hostname set to \"%s\"\n", ctx->cluster->hostname); } } void Cmd_Master(cmdctxt_t *ctx) { SOCKET s; char *newval = Cmd_Argv(ctx, 1); netadr_t addr; if (Cmd_Argc(ctx) < 2) { if (*ctx->cluster->master) Cmd_Printf(ctx, "Subscribed to a master server (use '-' to clear)\n"); else Cmd_Printf(ctx, "No master server is currently set.\n"); return; } if (!strcmp(newval, "-")) { strlcpy(ctx->cluster->master, "", sizeof(ctx->cluster->master)); Cmd_Printf(ctx, "Master server cleared\n"); return; } if (!NET_StringToAddr(newval, &addr, 27000)) //send a ping like a qw server does. this is kinda pointless of course. { Cmd_Printf(ctx, "Couldn't resolve address\n"); return; } strlcpy(ctx->cluster->master, newval, sizeof(ctx->cluster->master)); ctx->cluster->mastersendtime = ctx->cluster->curtime; s = NET_ChooseSocket(ctx->cluster->qwdsocket, &addr, addr); if (s != INVALID_SOCKET) NET_SendPacket (ctx->cluster, s, 1, "k", addr); Cmd_Printf(ctx, "Master server set.\n"); } void Cmd_UDPPort(cmdctxt_t *ctx) { int newp = atoi(Cmd_Argv(ctx, 1)); ctx->cluster->qwlistenportnum = newp; NET_InitUDPSocket(ctx->cluster, newp, SG_IPV6); NET_InitUDPSocket(ctx->cluster, newp, SG_IPV4); } void Cmd_AdminPassword(cmdctxt_t *ctx) { if (!Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "Rejecting remote password change.\n"); return; } if (Cmd_Argc(ctx) < 2) { if (*ctx->cluster->adminpassword) Cmd_Printf(ctx, "An admin password is currently set\n"); else Cmd_Printf(ctx, "No admin password is currently set\n"); } else { strlcpy(ctx->cluster->adminpassword, Cmd_Argv(ctx, 1), sizeof(ctx->cluster->adminpassword)); Cmd_Printf(ctx, "Password changed.\n"); } } void Cmd_GenericQuery(cmdctxt_t *ctx, int dataset) { char *method = "tcp:"; char *address, *password; if (Cmd_Argc(ctx) < 2) { Cmd_Printf(ctx, "%s requires an ip:port parameter\n", Cmd_Argv(ctx, 0)); return; } address = Cmd_Argv(ctx, 1); password = Cmd_Argv(ctx, 2); //this is evil /* Holy crap, Spike... Holy crap. */ memmove(address+strlen(method), address, ARG_LEN-(1+strlen(method))); strncpy(address, method, strlen(method)); if (!QTV_NewServerConnection(ctx->cluster, ctx->streamid, address, password, false, AD_NO, false, dataset)) Cmd_Printf(ctx, "Failed to connect to \"%s\", connection aborted\n", address); Cmd_Printf(ctx, "Querying \"%s\"\n", address); } void Cmd_QTVList(cmdctxt_t *ctx) { Cmd_GenericQuery(ctx, 1); } void Cmd_QTVDemoList(cmdctxt_t *ctx) { Cmd_GenericQuery(ctx, 2); } void Cmd_GenericConnect(cmdctxt_t *ctx, char *method, enum autodisconnect_e autoclose) { sv_t *sv; char *address, *password; if (Cmd_Argc(ctx) < 2) { if (!strncmp(method, "file", 4)) Cmd_Printf(ctx, "%s requires a demo name parameter\n", Cmd_Argv(ctx, 0)); else if (!strncmp(method, "dir", 3)) Cmd_Printf(ctx, "%s requires a demo directory parameter\n", Cmd_Argv(ctx, 0)); else Cmd_Printf(ctx, "%s requires an ip:port parameter\n", Cmd_Argv(ctx, 0)); return; } address = Cmd_Argv(ctx, 1); password = Cmd_Argv(ctx, 2); //this is evil /* Holy crap, Spike... Holy crap. */ memmove(address+strlen(method), address, ARG_LEN-(1+strlen(method))); strncpy(address, method, strlen(method)); sv = QTV_NewServerConnection(ctx->cluster, ctx->streamid?ctx->streamid:1, address, password, false, autoclose, false, false); if (!sv) Cmd_Printf(ctx, "Failed to connect to \"%s\", connection aborted\n", address); else Cmd_Printf(ctx, "Source registered \"%s\" as stream %i\n", address, sv->streamid); } void Cmd_QTVConnect(cmdctxt_t *ctx) { Cmd_GenericConnect(ctx, "tcp:", AD_NO); } void Cmd_QWConnect(cmdctxt_t *ctx) { Cmd_GenericConnect(ctx, "udp:", AD_STATUSPOLL); } void Cmd_MVDConnect(cmdctxt_t *ctx) { Cmd_GenericConnect(ctx, "file:", AD_NO); } void Cmd_DirMVDConnect(cmdctxt_t *ctx) { srand(time(NULL)); Cmd_GenericConnect(ctx, "dir:", AD_NO); } void Cmd_Exec(cmdctxt_t *ctx) { FILE *f; char line[512], *l; char *fname = Cmd_Argv(ctx, 1); if (!Cmd_IsLocal(ctx)) { if (*fname == '\\' || *fname == '/' || strstr(fname, "..") || fname[1] == ':') { Cmd_Printf(ctx, "Absolute paths are prohibited.\n"); return; } if (!strncmp(fname, "usercfg/", 8)) //this is how we stop users from execing a 50gb pk3.. { Cmd_Printf(ctx, "Remote-execed configs must be in the usercfg directory\n"); return; } } f = fopen(fname, "rt"); if (!f) { Cmd_Printf(ctx, "Couldn't exec \"%s\"\n", fname); return; } else { Cmd_Printf(ctx, "Execing \"%s\"\n", fname); while(fgets(line, sizeof(line)-1, f)) { l = line; while(*(unsigned char*)l <= ' ' && *l) l++; if (*l && l[0] != '/' && l[1] != '/') { Cmd_ExecuteNow(ctx, l); } } fclose(f); } } void catbuffer(char *buffer, int bufsize, char *format, ...) { va_list argptr; unsigned int buflen; buflen = strlen(buffer); va_start(argptr, format); vsnprintf(buffer + buflen, bufsize - buflen, format, argptr); va_end(argptr); } void Cmd_Say(cmdctxt_t *ctx) { int i; viewer_t *v; char message[8192]; message[0] = '\0'; for (i = 1; i < Cmd_Argc(ctx); i++) catbuffer(message, sizeof(message)-1, "%s%s", i==1?"":" ", Cmd_Argv(ctx, i)); if (ctx->qtv) { if (!SV_SayToUpstream(ctx->qtv, message)) SV_SayToViewers(ctx->qtv, message); } else { //we don't have to remember the client proxies here... no streams = no active client proxies for (v = ctx->cluster->viewers; v; v = v->next) { QW_PrintfToViewer(v, "proxy: %s\n", message); } } Cmd_Printf(ctx, "proxy: %s\n", message); } void Cmd_Status(cmdctxt_t *ctx) { Cmd_Printf(ctx, "QTV Status:\n"); Cmd_Printf(ctx, " %i sources%s\n", ctx->cluster->numservers, ctx->cluster->nouserconnects?" (admin only)":" (user allowed)"); Cmd_Printf(ctx, " %i udp clients %s\n", ctx->cluster->numviewers, ctx->cluster->allownqclients?" (qw+nq)":" (qw only)"); if (ctx->cluster->maxproxies) Cmd_Printf(ctx, " %i tcp clients (of %i)\n", ctx->cluster->numproxies, ctx->cluster->maxproxies); else Cmd_Printf(ctx, " %i tcp clients\n", ctx->cluster->numproxies); TURN_RelayStatus(ctx); Cmd_Printf(ctx, "Common Options:\n"); Cmd_Printf(ctx, " Hostname %s\n", ctx->cluster->hostname); if (ctx->cluster->chokeonnotupdated) Cmd_Printf(ctx, " Choke\n"); if (ctx->cluster->lateforward) Cmd_Printf(ctx, " Late forwarding (delayed streams)\n"); if (!ctx->cluster->notalking) Cmd_Printf(ctx, " Talking allowed\n"); if (ctx->cluster->nobsp) Cmd_Printf(ctx, " No BSP loading\n"); if (ctx->cluster->tcpsocket[SG_UNIX] != INVALID_SOCKET) Cmd_Printf(ctx, " unix socket open\n"); if (ctx->cluster->tcpsocket[SG_IPV4] != INVALID_SOCKET || ctx->cluster->tcpsocket[SG_IPV6] != INVALID_SOCKET) Cmd_Printf(ctx, " tcp port %i\n", ctx->cluster->tcplistenportnum); if (ctx->cluster->qwdsocket[SG_IPV4] != INVALID_SOCKET || ctx->cluster->qwdsocket[SG_IPV6] != INVALID_SOCKET) Cmd_Printf(ctx, " udp port %i\n", ctx->cluster->qwlistenportnum); Cmd_Printf(ctx, "\n"); if (ctx->qtv) { Cmd_Printf(ctx, "Selected server: %s\n", ctx->qtv->server); if (ctx->qtv->sourcefile) Cmd_Printf(ctx, " Playing from file\n"); if (ctx->qtv->sourcesock != INVALID_SOCKET) Cmd_Printf(ctx, " Connected\n"); if (ctx->qtv->parsingqtvheader || ctx->qtv->parsingconnectiondata) Cmd_Printf(ctx, " Waiting for gamestate\n"); if (ctx->qtv->usequakeworldprotocols) { Cmd_Printf(ctx, " QuakeWorld protocols\n"); if (ctx->qtv->controller) { Cmd_Printf(ctx, " Controlled by %s\n", ctx->qtv->controller->name); } } else if (ctx->qtv->sourcesock == INVALID_SOCKET && !ctx->qtv->sourcefile) Cmd_Printf(ctx, " Connection not established\n"); if (*ctx->qtv->map.modellist[1].name) { Cmd_Printf(ctx, " Map name %s\n", ctx->qtv->map.modellist[1].name); } if (*ctx->qtv->connectpassword) Cmd_Printf(ctx, " Using a password\n"); if (ctx->qtv->errored == ERR_DISABLED) Cmd_Printf(ctx, " Stream is disabled\n"); if (ctx->qtv->autodisconnect == AD_WHENEMPTY) Cmd_Printf(ctx, " Stream is user created\n"); else if (ctx->qtv->autodisconnect == AD_REVERSECONNECT) Cmd_Printf(ctx, " Stream is server created\n"); /* if (ctx->qtv->tcpsocket != INVALID_SOCKET) { Cmd_Printf(ctx, " Listening for proxies (%i)\n", ctx->qtv->tcplistenportnum); } */ if (ctx->qtv->map.bsp) { Cmd_Printf(ctx, " BSP (%s) is loaded\n", ctx->qtv->map.mapname); } } } void Cmd_UserConnects(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) < 2) { Cmd_Printf(ctx, "userconnects is set to %i\n", !ctx->cluster->nouserconnects); } else { ctx->cluster->nouserconnects = !atoi(Cmd_Argv(ctx, 1)); Cmd_Printf(ctx, "userconnects is now %i\n", !ctx->cluster->nouserconnects); } } void Cmd_Choke(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) < 2) { if (ctx->cluster->chokeonnotupdated) Cmd_Printf(ctx, "proxy will not interpolate packets\n"); else Cmd_Printf(ctx, "proxy will smooth action at the expense of extra packets\n"); return; } ctx->cluster->chokeonnotupdated = !!atoi(Cmd_Argv(ctx, 1)); Cmd_Printf(ctx, "choke-until-update set to %i\n", ctx->cluster->chokeonnotupdated); } void Cmd_Late(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) < 2) { if (ctx->cluster->lateforward) Cmd_Printf(ctx, "forwarded streams will be artificially delayed\n"); else Cmd_Printf(ctx, "forwarded streams are forwarded immediatly\n"); return; } ctx->cluster->lateforward = !!atoi(Cmd_Argv(ctx, 1)); Cmd_Printf(ctx, "late forwarding set\n"); } void Cmd_ReverseAllowed(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) >= 2) ctx->cluster->reverseallowed = !!atoi(Cmd_Argv(ctx, 1)); Cmd_Printf(ctx, "reverse connections are %s\n", ctx->cluster->reverseallowed?"enabled":"disabled"); } void Cmd_Talking(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) < 2) { if (ctx->cluster->notalking) Cmd_Printf(ctx, "viewers may not talk\n"); else Cmd_Printf(ctx, "viewers may talk freely\n"); return; } ctx->cluster->notalking = !atoi(Cmd_Argv(ctx, 1)); Cmd_Printf(ctx, "talking permissions set\n"); } void Cmd_NoBSP(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (!*val) { if (ctx->cluster->nobsp) Cmd_Printf(ctx, "no bsps will be loaded\n"); else Cmd_Printf(ctx, "attempting to load bsp files\n"); } else { ctx->cluster->nobsp = !!atoi(val); Cmd_Printf(ctx, "nobsp will change at start of next map\n"); } } void Cmd_MaxViewers(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (!*val) { if (ctx->cluster->maxviewers) Cmd_Printf(ctx, "maxviewers is currently %i\n", ctx->cluster->maxviewers); else Cmd_Printf(ctx, "maxviewers is currently unlimited\n"); } else { ctx->cluster->maxviewers = atoi(val); Cmd_Printf(ctx, "maxviewers set\n"); } } void Cmd_AllowNQ(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (!*val) { Cmd_Printf(ctx, "allownq is currently %i\n", ctx->cluster->allownqclients); } else { ctx->cluster->allownqclients = !!atoi(val); Cmd_Printf(ctx, "allownq set\n"); } } void Cmd_InitialDelay(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (!*val) { Cmd_Printf(ctx, "initialdelay is currently %g seconds\n", ctx->cluster->anticheattime/1000.f); } else { ctx->cluster->anticheattime = atof(val)*1000; if (ctx->cluster->anticheattime < 1) ctx->cluster->anticheattime = 1; Cmd_Printf(ctx, "initialdelay set\n"); } } void Cmd_SlowDelay(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (!*val) { Cmd_Printf(ctx, "slowdelay is currently %g seconds\n", ctx->cluster->tooslowdelay/1000.f); } else { ctx->cluster->tooslowdelay = atof(val)*1000; if (ctx->cluster->tooslowdelay < 1) ctx->cluster->tooslowdelay = 1; Cmd_Printf(ctx, "slowdelay set\n"); } } void Cmd_MaxProxies(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (!*val) { if (ctx->cluster->maxproxies) Cmd_Printf(ctx, "maxproxies is currently %i\n", ctx->cluster->maxproxies); else Cmd_Printf(ctx, "maxproxies is currently unlimited\n"); } else { ctx->cluster->maxproxies = atoi(val); Cmd_Printf(ctx, "maxproxies set\n"); } } void Cmd_Ping(cmdctxt_t *ctx) { netadr_t addr; char *val = Cmd_Argv(ctx, 1); if (NET_StringToAddr(val, &addr, 27500)) { NET_SendPacket (ctx->cluster, NET_ChooseSocket(ctx->cluster->qwdsocket, &addr, addr), 1, "k", addr); Cmd_Printf(ctx, "pinged\n"); } Cmd_Printf(ctx, "couldn't resolve\n"); } void Cmd_Help(cmdctxt_t *ctx) { Cmd_Printf(ctx, HELPSTRING); } void Cmd_Echo(cmdctxt_t *ctx) { Cmd_Printf(ctx, "%s", Cmd_Argv(ctx, 1)); } void Cmd_Quit(cmdctxt_t *ctx) { if (!Cmd_IsLocal(ctx)) Cmd_Printf(ctx, "Remote shutdown refused.\n"); ctx->cluster->wanttoexit = true; Cmd_Printf(ctx, "Shutting down.\n"); } void Cmd_Streams(cmdctxt_t *ctx) { sv_t *qtv; char *status; Cmd_Printf(ctx, "Streams:\n"); for (qtv = ctx->cluster->servers; qtv; qtv = qtv->next) { switch (qtv->errored) { case ERR_NONE: if (qtv->controller) status = " (player controlled)"; else if (qtv->autodisconnect == AD_STATUSPOLL) status = " (polling)"; else if (qtv->parsingconnectiondata) status = " (connecting)"; else status = ""; break; case ERR_PAUSED: status = " (paused)"; break; case ERR_DISABLED: status = " (disabled)"; break; case ERR_DROP: //a user should never normally see this, but there is a chance status = " (dropping)"; break; case ERR_RECONNECT: //again, rare status = " (reconnecting)"; break; default: //some other kind of error, transitioning status = " (errored)"; break; } Cmd_Printf(ctx, "%i: %s%s\n", qtv->streamid, qtv->server, status); if (qtv->upstreamacceptschat) Cmd_Printf(ctx, " (dbg) can chat!\n"); } } void Cmd_DemoSpeed(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); if (*val) { ctx->qtv->parsespeed = atof(val)*1000; Cmd_Printf(ctx, "Setting demo speed to %f\n", ctx->qtv->parsespeed/1000.0f); } else Cmd_Printf(ctx, "Playing demo at %f speed\n", ctx->qtv->parsespeed/1000.0f); } void Cmd_Disconnect(cmdctxt_t *ctx) { QTV_ShutdownStream(ctx->qtv); Cmd_Printf(ctx, "Disconnected\n"); } void Cmd_Halt(cmdctxt_t *ctx) { if (ctx->qtv->errored == ERR_DISABLED || ctx->qtv->errored == ERR_PERMANENT) { Cmd_Printf(ctx, "Stream is already halted\n"); } else { ctx->qtv->errored = ERR_PERMANENT; Cmd_Printf(ctx, "Stream will disconnect\n"); } } void Cmd_Pause(cmdctxt_t *ctx) { if (ctx->qtv->errored == ERR_PAUSED) { ctx->qtv->errored = ERR_NONE; Cmd_Printf(ctx, "Stream unpaused.\n"); } else if (ctx->qtv->errored == ERR_NONE) { if (ctx->qtv->sourcetype == SRC_DEMO) { ctx->qtv->errored = ERR_PAUSED; Cmd_Printf(ctx, "Stream paused.\n"); } else Cmd_Printf(ctx, "Sorry, only demos may be paused.\n"); } } void Cmd_Resume(cmdctxt_t *ctx) { if (ctx->qtv->errored == ERR_PAUSED) { ctx->qtv->errored = ERR_NONE; Cmd_Printf(ctx, "Stream unpaused.\n"); } else { if (ctx->qtv->errored == ERR_NONE) Cmd_Printf(ctx, "Stream is already functional\n"); ctx->qtv->errored = ERR_RECONNECT; Cmd_Printf(ctx, "Stream will attempt to reconnect\n"); } } void Cmd_Record(cmdctxt_t *ctx) { char *fname = Cmd_Argv(ctx, 1); if (!*fname) Cmd_Printf(ctx, "record requires a filename on the proxy's machine\n"); if (!Cmd_IsLocal(ctx)) { if (*fname == '\\' || *fname == '/' || strstr(fname, "..") || fname[1] == ':') { Cmd_Printf(ctx, "Absolute paths are prohibited.\n"); return; } } if (Net_FileProxy(ctx->qtv, fname)) Cmd_Printf(ctx, "Recording to disk\n"); else Cmd_Printf(ctx, "Failed to open file\n"); } void Cmd_Stop(cmdctxt_t *ctx) { if (Net_StopFileProxy(ctx->qtv)) Cmd_Printf(ctx, "stopped\n"); else Cmd_Printf(ctx, "not recording to disk\n"); } void Cmd_Reconnect(cmdctxt_t *ctx) { if (ctx->qtv->autodisconnect == AD_REVERSECONNECT) Cmd_Printf(ctx, "Stream is a reverse connection (command rejected)\n"); // else if (ctx->qtv->autodisconnect == AD_STATUSPOLL && !ctx->qtv->numviewers && !ctx->qtv->proxies) // Cmd_Printf(ctx, "Not reconnecting to idle server\n"); else if (QTV_ConnectStream(ctx->qtv, ctx->qtv->server)) Cmd_Printf(ctx, "Reconnected\n"); else Cmd_Printf(ctx, "Failed to reconnect (will keep trying)\n"); } void Cmd_MVDPort(cmdctxt_t *ctx) { char *val = Cmd_Argv(ctx, 1); int newp = atoi(val); if (!*val) { Cmd_Printf(ctx, "Listening for tcp connections on port %i\n", ctx->cluster->tcplistenportnum); return; } if (!newp) { if (ctx->cluster->tcpsocket[0] != INVALID_SOCKET && ctx->cluster->tcpsocket[1] != INVALID_SOCKET) Cmd_Printf(ctx, "Already closed\n"); } Net_TCPListen(ctx->cluster, newp, true); Net_TCPListen(ctx->cluster, newp, false); ctx->cluster->tcplistenportnum = newp; } void Cmd_DemoList(cmdctxt_t *ctx) { int i; int count; Cluster_BuildAvailableDemoList(ctx->cluster); count = ctx->cluster->availdemoscount; Cmd_Printf(ctx, "%i demos\n", count); for (i = 0; i < count; i++) { Cmd_Printf(ctx, " %7i %s\n", ctx->cluster->availdemos[i].size, ctx->cluster->availdemos[i].name); } } void Cmd_BaseDir(cmdctxt_t *ctx) { char *val; val = Cmd_Argv(ctx, 1); if (!Cmd_IsLocal(ctx)) Cmd_Printf(ctx, "Sorry, you may not use this command remotely\n"); if (*val) chdir(val); else { char buffer[256]; val = getcwd(buffer, sizeof(buffer)); if (val) Cmd_Printf(ctx, "basedir is: %s\n", val); else Cmd_Printf(ctx, "system error getting basedir\n"); } } void Cmd_DemoDir(cmdctxt_t *ctx) { char *val; val = Cmd_Argv(ctx, 1); if (*val) { if (!Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "Sorry, but I don't trust this code that well!\n"); return; } while (*val > 0 &&*val <= ' ') val++; if (strchr(val, '.') || strchr(val, ':') || *val == '/') Cmd_Printf(ctx, "Rejecting path\n"); else { strlcpy(ctx->cluster->demodir, val, sizeof(ctx->cluster->demodir)); Cmd_Printf(ctx, "Changed demo dir to \"%s\"\n", ctx->cluster->demodir); } } else { Cmd_Printf(ctx, "Current demo directory is \"%s\"\n", ctx->cluster->demodir); } } void Cmd_DLDir(cmdctxt_t *ctx) { char *val; val = Cmd_Argv(ctx, 1); if (!Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "dldir may not be used remotely\n"); return; } if (*val) { while (*val > 0 &&*val <= ' ') val++; // if (strchr(val, '.') || strchr(val, ':') || *val == '/') // Cmd_Printf(ctx, "Rejecting path\n"); // else { strlcpy(ctx->cluster->downloaddir, val, sizeof(ctx->cluster->downloaddir)); Cmd_Printf(ctx, "Changed download dir to \"%s\"\n", ctx->cluster->downloaddir); } } else { Cmd_Printf(ctx, "Current download directory is \"%s\"\n", ctx->cluster->downloaddir); } } void Cmd_PluginDataSource(cmdctxt_t *ctx) { char *val; val = Cmd_Argv(ctx, 1); if (!Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "plugindatasource may not be used remotely\n"); return; } if (*val) { while (*val > 0 &&*val <= ' ') val++; strlcpy(ctx->cluster->plugindatasource, val, sizeof(ctx->cluster->plugindatasource)); Cmd_Printf(ctx, "Changed plugindatasource to \"%s\"\n", ctx->cluster->plugindatasource); } else { Cmd_Printf(ctx, "Current plugindatasource is \"%s\"\n", ctx->cluster->plugindatasource); } } void Cmd_MapSource(cmdctxt_t *ctx) { char *val; val = Cmd_Argv(ctx, 1); if (!Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "mapsource may not be used remotely\n"); return; } if (*val) { while (*val > 0 &&*val <= ' ') val++; strlcpy(ctx->cluster->mapsource, val, sizeof(ctx->cluster->mapsource)); Cmd_Printf(ctx, "Changed mapsource url to \"%s\"\n", ctx->cluster->mapsource); } else { Cmd_Printf(ctx, "Current mapsource url is \"%s\"\n", ctx->cluster->mapsource); } } void Cmd_MuteStream(cmdctxt_t *ctx) { char *val; val = Cmd_Argv(ctx, 1); if (*val) { ctx->qtv->silentstream = atoi(val); Cmd_Printf(ctx, "Stream is now %smuted\n", ctx->qtv->silentstream?"":"un"); } else Cmd_Printf(ctx, "Stream is currently %smuted\n", ctx->qtv->silentstream?"":"un"); } #ifdef VIEWER void Cmd_Watch(cmdctxt_t *ctx) { if (!localcommand) { Cmd_Printf(ctx, "watch is not permitted remotly\n"); } if (cluster->viewserver == qtv) { cluster->viewserver = NULL; Cmd_Printf(ctx, "Stopped watching\n"); } else { cluster->viewserver = qtv; Cmd_Printf(ctx, "Watching\n"); } } #endif #ifdef __linux__ #include qboolean Sys_RandomBytes(unsigned char *out, int len) { qboolean res; int fd = open("/dev/urandom", 0); res = (read(fd, out, len) == len); close(fd); return res; } #else qboolean Sys_RandomBytes(unsigned char *out, int len) { return false; } #endif static void Cmd_Turn(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) < 2) { if (ctx->cluster->turnenabled && ctx->cluster->turn_minport) Cmd_Printf(ctx, "turn is enabled, using ports %i-%i\n", ctx->cluster->turn_minport, ctx->cluster->turn_maxport); else if (ctx->cluster->turnenabled) Cmd_Printf(ctx, "turn is enabled, using ephemerial ports\n"); else Cmd_Printf(ctx, "turn is disabled\n"); return; } if (!Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "turn support may not be configured remotely\n"); return; } if (Cmd_Argc(ctx) >= 3) { //two args - assume a two number range, so turn it on. ctx->cluster->turnenabled = true; ctx->cluster->turn_minport = atoi(Cmd_Argv(ctx, 1)); ctx->cluster->turn_maxport = atoi(Cmd_Argv(ctx, 2)); } else if ( atoi(Cmd_Argv(ctx, 1))) //a boolean. turn it back on.. ctx->cluster->turnenabled = true; //switch it back on with whatever port range it previously had. probably 0-0 for ephemerial. probably bad for the relay's firewalls... else ctx->cluster->turnenabled = false; //and off. if (!*ctx->cluster->chalkey && ctx->cluster->turnenabled) { unsigned char chalkey[12]; if (!Sys_RandomBytes(chalkey, sizeof(chalkey)) || !Sys_RandomBytes(ctx->cluster->turnkey, sizeof(ctx->cluster->turnkey))) { Cmd_Printf(ctx, "no random generator\n"); ctx->cluster->turnenabled = false; return; } tobase64(ctx->cluster->chalkey,sizeof(ctx->cluster->chalkey), chalkey, sizeof(chalkey)); } if (ctx->cluster->turnenabled && ctx->cluster->turn_minport) Cmd_Printf(ctx, "turn keys updated, using ports %i-%i\n", ctx->cluster->turn_minport, ctx->cluster->turn_maxport); else if (ctx->cluster->turnenabled) Cmd_Printf(ctx, "turn keys updated, using ephemerial ports\n"); else Cmd_Printf(ctx, "turn disabled\n"); } static void Cmd_Relay(cmdctxt_t *ctx) { if (Cmd_Argc(ctx) >= 2) { if (Cmd_IsLocal(ctx)) { Cmd_Printf(ctx, "relay support may not be configured remotely\n"); return; } switch(atoi(Cmd_Argv(ctx, 1))) { case 0: ctx->cluster->relayenabled = ctx->cluster->pingtreeenabled = false; Cmd_Printf(ctx, "turn disabled\n"); break; case 1: ctx->cluster->relayenabled = ctx->cluster->pingtreeenabled = true; break; default: ctx->cluster->relayenabled = true; ctx->cluster->pingtreeenabled = false; break; } } if (ctx->cluster->relayenabled && ctx->cluster->pingtreeenabled) Cmd_Printf(ctx, "relay is enabled (with pinging)\n"); else if (ctx->cluster->relayenabled) Cmd_Printf(ctx, "relay is enabled, WITHOUT pinging\n"); else Cmd_Printf(ctx, "relay is disabled\n"); } static void Cmd_ProtocolName(cmdctxt_t *ctx) { free(ctx->cluster->protocolname); ctx->cluster->protocolname = strdup(Cmd_Argv(ctx, 1)); ctx->cluster->protocolver = atoi(Cmd_Argv(ctx, 2)); } typedef struct rconcommands_s { char *name; qboolean serverspecific; //works within a qtv context qboolean clusterspecific; //works without a qtv context (ignores context) consolecommand_t func; char *description; } rconcommands_t; extern const rconcommands_t rconcommands[]; void Cmd_Commands(cmdctxt_t *ctx) { const rconcommands_t *cmd; consolecommand_t lastfunc = NULL; Cmd_Printf(ctx, "Say Commands:\n"); for (cmd = rconcommands; cmd->name; cmd++) { if (cmd->func == lastfunc) continue; //no spamming alternative command names Cmd_Printf(ctx, "%s: %s\n", cmd->name, cmd->description?cmd->description:"no description available"); lastfunc = cmd->func; } } const rconcommands_t rconcommands[] = { {"exec", 1, 1, Cmd_Exec, "executes a config file"}, {"status", 1, 1, Cmd_Status, "prints proxy/stream status" }, {"say", 1, 1, Cmd_Say, "says to a stream"}, {"help", 0, 1, Cmd_Help, "shows the brief intro help text"}, {"commands", 0, 1, Cmd_Commands, "prints the list of commands"}, {"apropos", 0, 1, Cmd_Commands, "prints all commands"}, {"hostname", 0, 1, Cmd_Hostname, "changes the hostname seen in server browsers"}, {"master", 0, 1, Cmd_Master, "specifies which master server to use"}, {"udpport", 0, 1, Cmd_UDPPort, "specifies to listen on a provided udp port for regular qw clients"}, {"port", 0, 1, Cmd_UDPPort}, {"adminpassword", 0, 1, Cmd_AdminPassword,"specifies the password for qtv administrators"}, {"rconpassword", 0, 1, Cmd_AdminPassword}, {"qtvlist", 0, 1, Cmd_QTVList, "queries a seperate proxy for a list of available streams"}, {"qtvdemolist", 0, 1, Cmd_QTVDemoList, "queries a seperate proxy for a list of available demos"}, {"qtv", 0, 1, Cmd_QTVConnect, "adds a new tcp/qtv stream"}, {"addserver", 0, 1, Cmd_QTVConnect}, {"connect", 0, 1, Cmd_QTVConnect}, {"qw", 0, 1, Cmd_QWConnect, "adds a new udp/qw stream"}, {"observe", 0, 1, Cmd_QWConnect}, {"demos", 0, 1, Cmd_DemoList, "shows the list of demos available on this proxy"}, {"demo", 0, 1, Cmd_MVDConnect, "adds a demo as a new stream"}, {"playdemo", 0, 1, Cmd_MVDConnect}, {"dir", 0, 1, Cmd_DirMVDConnect, "adds a directory of demos as a new stream"}, {"playdir", 0, 1, Cmd_DirMVDConnect}, {"choke", 0, 1, Cmd_Choke, "chokes packets to the data rate in the stream, disables proxy-side interpolation"}, {"late", 0, 1, Cmd_Late, "enforces a time delay on packets sent through this proxy"}, {"talking", 0, 1, Cmd_Talking, "permits viewers to talk to each other"}, {"nobsp", 0, 1, Cmd_NoBSP, "disables loading of bsp files"}, {"userconnects", 0, 1, Cmd_UserConnects, "prevents users from creating thier own streams"}, {"maxviewers", 0, 1, Cmd_MaxViewers, "sets a limit on udp/qw client connections"}, {"maxproxies", 0, 1, Cmd_MaxProxies, "sets a limit on tcp/qtv client connections"}, {"demodir", 0, 1, Cmd_DemoDir, "specifies where to get the demo list from"}, {"basedir", 0, 1, Cmd_BaseDir, "specifies where to get any files required by the game. this is prefixed to the server-specified game dir."}, {"ping", 0, 1, Cmd_Ping, "sends a udp ping to a qtv proxy or server"}, {"reconnect", 0, 1, Cmd_Reconnect, "forces a stream to reconnect to its server (restarts demos)"}, {"echo", 0, 1, Cmd_Echo, "a useless command that echos a string"}, {"quit", 0, 1, Cmd_Quit, "closes the qtv"}, {"exit", 0, 1, Cmd_Quit}, {"streams", 0, 1, Cmd_Streams, "shows a list of active streams"}, {"allownq", 0, 1, Cmd_AllowNQ, "permits nq clients to connect. This can be disabled as this code is less tested than the rest"}, {"initialdelay",0, 1, Cmd_InitialDelay, "Specifies the duration for which new connections will be buffered. Large values prevents players from spectating their enemies as a cheap wallhack."}, {"slowdelay", 0, 1, Cmd_SlowDelay, "If a server is not sending enough data, the proxy will delay parsing for this long."}, {"turn", 0, 1, Cmd_Turn, "Controls whether we accept turn requests."}, {"relay", 0, 1, Cmd_Relay, "Controls whether we accept qwfwd-style relay requests."}, {"qwfwd", 0, 1, Cmd_Relay}, {"protocolname",0, 1, Cmd_ProtocolName, "Protocol Name:Version used to register with master."}, {"halt", 1, 0, Cmd_Halt, "disables a stream, preventing it from reconnecting until someone tries watching it anew. Boots current spectators"}, {"disable", 1, 0, Cmd_Halt}, {"pause", 1, 0, Cmd_Pause, "Pauses a demo stream."}, {"resume", 1, 0, Cmd_Resume, "reactivates a stream, allowing it to reconnect"}, {"enable", 1, 0, Cmd_Resume}, {"mute", 1, 0, Cmd_MuteStream, "hides prints that come from the game server"}, {"mutestream", 1, 0, Cmd_MuteStream}, {"disconnect", 1, 0, Cmd_Disconnect, "fully closes a stream"}, {"record", 1, 0, Cmd_Record, "records a stream to a demo"}, {"stop", 1, 0, Cmd_Stop, "stops recording of a demo"}, {"demospeed", 1, 0, Cmd_DemoSpeed, "changes the rate the demo is played at"}, {"tcpport", 0, 1, Cmd_MVDPort, "specifies which port to listen on for tcp/qtv connections"}, {"mvdport", 0, 1, Cmd_MVDPort}, {"dldir", 0, 1, Cmd_DLDir, "specifies the path to download stuff from (http://server/file/ maps to this native path)"}, {"plugindatasource",0,1,Cmd_PluginDataSource, "Specifies the dataDownload property for plugins in the web server"}, {"mapsource", 0, 1, Cmd_MapSource,"Public URL for where to download missing maps from"}, #ifdef VIEWER {"watch", 1, 0, Cmd_Watch, "specifies to watch that stream in the built-in viewer"}, #endif {NULL} }; void Cmd_ExecuteNow(cmdctxt_t *ctx, char *command) { #define TOKENIZE_PUNCTUATION "" int i; char arg[MAX_ARGS][ARG_LEN]; char *sid; char *cmdname; for (sid = command; *sid; sid++) { if (*sid == ':') break; if (*sid < '0' || *sid > '9') break; } if (*sid == ':') { i = atoi(command); command = sid+1; ctx->streamid = i; for (ctx->qtv = ctx->cluster->servers; ctx->qtv; ctx->qtv = ctx->qtv->next) if (ctx->qtv->streamid == i) break; } else ctx->streamid = 0; ctx->argc = 0; for (i = 0; i < MAX_ARGS; i++) { command = COM_ParseToken(command, arg[i], ARG_LEN, TOKENIZE_PUNCTUATION); ctx->arg[i] = arg[i]; if (command) ctx->argc++; } cmdname = Cmd_Argv(ctx, 0); //if there's only one stream, set that as the selected stream if (!ctx->qtv && ctx->cluster->numservers==1) ctx->qtv = ctx->cluster->servers; if (ctx->qtv) { //if there is a specific connection targetted for (i = 0; rconcommands[i].name; i++) { if (rconcommands[i].serverspecific) if (!strcmp(rconcommands[i].name, cmdname)) { rconcommands[i].func(ctx); return; } } } for (i = 0; rconcommands[i].name; i++) { if (!strcmp(rconcommands[i].name, cmdname)) { if (rconcommands[i].clusterspecific) { rconcommands[i].func(ctx); return; } else if (rconcommands[i].serverspecific) { Cmd_Printf(ctx, "Command \"%s\" requires a targeted server.\n", cmdname); return; } } } Cmd_Printf(ctx, "Command \"%s\" not recognised.\n", cmdname); } void Rcon_PrintToBuffer(cmdctxt_t *ctx, char *msg) { if (ctx->printcookiesize < 1) return; while (ctx->printcookiesize>2 && *msg) { ctx->printcookiesize--; *(char*)ctx->printcookie = *msg++; ctx->printcookie = ((char*)ctx->printcookie)+1; } ctx->printcookiesize--; *(char*)ctx->printcookie = 0; } char *Rcon_Command(cluster_t *cluster, sv_t *source, char *command, char *resultbuffer, int resultbuffersize, int islocalcommand) { cmdctxt_t ctx; ctx.cluster = cluster; ctx.qtv = source; ctx.argc = 0; ctx.printfunc = Rcon_PrintToBuffer; ctx.printcookie = resultbuffer; ctx.printcookiesize = resultbuffersize; ctx.localcommand = islocalcommand; *(char*)ctx.printcookie = 0; Cmd_ExecuteNow(&ctx, command); return resultbuffer; } ================================================ FILE: fteqtv/relay.c ================================================ /* Copyright (C) 2024 'Spoike'. 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 included (GNU.txt) 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. */ /* we have two types of relay here. 1) TURN - blind(ish) forwarding. standard protocol for use as ICE/WebRTC relays. see TURN_IS_NOT_BLIND 2) qwfwd - compatible with qqshka's fork of the original qwfwd. relay incercepts `prx` userinfo in the connect requests to forward to the real peer. unlike qqshka's version we relay the next hop's extension handshakes instead of doing them on the proxy. we can also relay 'dtlsconnect' requests. for use with `connectbr` and compat with things that expect it. */ #include "qtv.h" #include #include #include #include "bsd_string.h" #define QWFWDTIMEOUT (30*1000) //how long the relay will stay open for when doing qw-specific forwarding. #define TURN_IS_NOT_BLIND //filters what's allowed through - we must have seen a stun binding request/response from the peer before we allow non-binding traffic through. This should help protect the owner's lan a bit. #if defined(_DEBUG) && !defined(LIBQTV) //unsafe stuff I'm leaving enabled in debug builds cos debugging is painful when they're protected. #define ALLOWPRIVATE //allow relaying to private lan addresses, so I can test with hosting on localhost #endif #define QRY_SERVERLISTINTERVAL 60*1000 //interval between asking for server listings. #define QRY_REPINGINTERVAL 60*1000 //don't ever spam any server faster than this. #define QRY_PINGINTERVAL 100 //time between outgoing pings #define QRY_TIMEOUT 20*60*1000 //servers will be removed if not seen in any server listings for this long. static void Fwd_DoPing(cluster_t *cluster); #define countof(x) (sizeof(x)/sizeof((x)[0])) struct turnclient_s { //this is the end that opened the connection struct turnclient_s *next; SOCKET remotesock; //the udp socket we're sending to struct { unsigned int timeout; //reset to 5 mins on refresh. netadr_t remoteaddr; #ifdef TURN_IS_NOT_BLIND qboolean seenstun; //this is for ICE support. so we can impose a rule that the peer MUST have sent a stun packet before we allow the client to send any non-stun packets. this reduces the chance of being abused for attacks. can potentially still ddos with stun, but shouldn't be an amplification at least. #endif } remotes[8]; int udpsockid; //-1 for private tcp SOCKET clientsock; //stoopid tcp clients... netadr_t relayaddr; //yay for udp... netadr_t clientaddr; //yay for udp... unsigned int timeout; char *username; //username they authed with. may not switch users once established. char *auth; //auth token generated by the master (recomputed). unsigned char key[64]; unsigned int keysize; qboolean isfwd; }; typedef struct { unsigned short msgtype; unsigned short msglen; unsigned int magiccookie; unsigned int transactid[3]; } stunhdr_t; //class #define STUN_REQUEST 0x0000 #define STUN_REPLY 0x0100 #define STUN_ERROR 0x0110 #define STUN_INDICATION 0x0010 //request #define STUN_BINDING 0x0001 #define STUN_ALLOCATE 0x0003 //TURN #define STUN_REFRESH 0x0004 //TURN #define STUN_SEND 0x0006 //TURN send indications contain data to forward #define STUN_DATA 0x0007 //TURN data indications contain reply data. #define STUN_CREATEPERM 0x0008 //TURN #define STUN_CHANBIND 0x0009 //TURN //misc stuff... #define STUN_MAGIC_COOKIE 0x2112a442 //attributes #define STUNATTR_MAPPED_ADDRESS 0x0001 //#define STUNATTR_RESPONSE_ADDRESS 0x0002 //#define STUNATTR_CHANGE_REQUEST 0x0003 //#define STUNATTR_SOURCE_ADDRESS 0x0004 //#define STUNATTR_CHANGED_ADDRESS 0x0005 #define STUNATTR_USERNAME 0x0006 //#define STUNATTR_PASSWORD 0x0007 #define STUNATTR_MSGINTEGRITIY_SHA1 0x0008 #define STUNATTR_ERROR_CODE 0x0009 //#define STUNATTR_UNKNOWN_ATTRIBUTES 0x000a //#define STUNATTR_REFLECTED_FROM 0x000b //#define STUNATTR_CHANNELNUMBER 0x000c //TURN #define STUNATTR_LIFETIME 0x000d //TURN //#define STUNATTR_ 0x000e //#define STUNATTR_ 0x000f //#define STUNATTR_ 0x0010 //#define STUNATTR_ 0x0011 #define STUNATTR_XOR_PEER_ADDRESS 0x0012 //TURN #define STUNATTR_DATA 0x0013 //TURN #define STUNATTR_REALM 0x0014 //TURN #define STUNATTR_NONCE 0x0015 //TURN #define STUNATTR_XOR_RELAYED_ADDRESS 0x0016 //TURN #define STUNATTR_REQUESTED_ADDRFAM 0x0017 //TURN //#define STUNATTR_EVEN_PORT 0x0018 //TURN #define STUNATTR_REQUESTED_TRANSPORT 0x0019 //TURN #define STUNATTR_DONT_FRAGMENT 0x001a //TURN #define STUNATTR_MSGINTEGRITIY_SHA2_256 0x001c #define STUNATTR_XOR_MAPPED_ADDRESS 0x0020 //#define STUNATTR_ICE_PRIORITY 0x0024 //ICE //#define STUNATTR_ICE_USE_CANDIDATE 0x0025 //ICE //0x8000 attributes are optional, and may be silently ignored without issue. #define STUNATTR_ADDITIONAL_ADDRFAM 0x8000 //TURN -- listen for ipv6 in addition to ipv4 #define STUNATTR_SOFTWARE 0x8022 //TURN #define STUNATTR_FINGERPRINT 0x8028 //#define STUNATTR_ICE_CONTROLLED 0x8029 //ICE //#define STUNATTR_ICE_CONTROLLING 0x802A //ICE static void TURN_Send(cluster_t *cluster, netmsg_t *m, netadr_t *clientadr) { netadr_t sockaddr; SOCKET sock = NET_ChooseSocket(cluster->qwdsocket, &sockaddr, *clientadr); unsigned char *bytes = m->data; //update the size. bytes[2] = (m->cursize-20)>>8; bytes[3] = (m->cursize-20)&0xff; //send it. NET_SendPacket(cluster, sock, m->cursize, bytes, sockaddr); } static int TURN_FindPermission(cluster_t *cluster, struct turnclient_s *t, netadr_t *peeraddr) { //we only allow packets to/from authorised peers. int i; for (i = 0; i < countof(t->remotes); i++) { if ((signed int)(t->remotes[i].timeout-cluster->curtime) < 0) continue; //look for a live one... if (Net_CompareAddress(&t->remotes[i].remoteaddr,peeraddr, 0,1)) return i; } return -1; } static qboolean TURN_Permissable(netadr_t *adr) { //we only allow packets to/from authorised peers. we don't worry about inbound ports to give symetric-nat peers a chance. if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET) { struct sockaddr_in *a = (struct sockaddr_in *)&adr->sockaddr; unsigned int ip = a->sin_addr.s_addr; if ((ip&BigLong(0xffff0000)) == BigLong(0xA9FE0000)) //169.254.x.x/16 return false;//link-local else if ((ip&BigLong(0xff000000)) == BigLong(0x0a000000)) //10.x.x.x/8 return false;//private else if ((ip&BigLong(0xff000000)) == BigLong(0x7f000000)) //127.x.x.x/8 return false;//localhost else if ((ip&BigLong(0xfff00000)) == BigLong(0xac100000)) //172.16.x.x/12 return false;//private else if ((ip&BigLong(0xffff0000)) == BigLong(0xc0a80000)) //192.168.x.x/16 return false;//private // else if ((ip&BigLong(0xffc00000)) == BigLong(0x64400000)) //100.64.x.x/10 // return false;//CGNAT else if (ip == BigLong(0x00000000)) //0.0.0.0/32 return false;//inaddr_any } if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET6) { struct sockaddr_in6 *a = (struct sockaddr_in6 *)&adr->sockaddr; unsigned char *ip6 = a->sin6_addr.s6_addr; if ((*(int*)ip6&BigLong(0xffc00000)) == BigLong(0xfe800000)) //fe80::/10 return false;//link-local else if ((*(int*)ip6&BigLong(0xfe000000)) == BigLong(0xfc00000)) //fc::/7 return false;//ULA/private else if (*(int*)ip6 == BigLong(0x20010000)) //2001::/32 return false;//toredo else if ((*(int*)ip6&BigLong(0xffff0000)) == BigLong(0x20020000)) //2002::/16 return false;//6to4 else if (memcmp(ip6, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1", 16) == 0) //::1 return false;//localhost else if (memcmp(ip6, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16) == 0) //:: return false;//inaddr_any else if (memcmp(ip6, "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12) == 0) //::ffff:x.y.z.w return false; //no bypassing ipv4 checks } return true; } #ifdef ALLOWPRIVATE static qboolean TURN_PermissableIgnore(netadr_t *adr) { if (TURN_Permissable(adr)) return true; //its private... but we're allowing it anyway - with warning fprintf(stderr, "WARNING: Allowing relay to access private address\n"); return true; } #define TURN_Permissable TURN_PermissableIgnore #endif void TURN_AddFDs(cluster_t *cluster, fd_set *set, int *m) { struct turnclient_s *t; for (t = cluster->turns; t; t = t->next) { if (t->remotesock < FD_SETSIZE) { FD_SET(t->remotesock, set); if (t->remotesock >= *m) *m = t->remotesock+1; } } } static void TURN_AddXorAddress(netmsg_t *m, int atrtype, netadr_t *adr) { unsigned short xoredport; unsigned int xoredaddr; unsigned char *xor = ((unsigned char*)m->data+4); if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET) { struct sockaddr_in *out = (struct sockaddr_in *)&adr->sockaddr; WriteBigShort(m, atrtype); WriteBigShort(m, 4+4); WriteBigShort(m, 1); //ipv4 xoredport = out->sin_port ^ *(unsigned short*)xor; WriteData(m, &xoredport, sizeof(xoredport)); xoredaddr = out->sin_addr.s_addr ^ *(unsigned int*)xor; WriteData(m, &xoredaddr, sizeof(xoredaddr)); } if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET6) { struct sockaddr_in6 *out = (struct sockaddr_in6 *)&adr->sockaddr; if (((unsigned short*)&out->sin6_addr)[0] == 0 && ((unsigned short*)&out->sin6_addr)[1] == 0 && ((unsigned short*)&out->sin6_addr)[2] == 0 && ((unsigned short*)&out->sin6_addr)[3] == 0 && ((unsigned short*)&out->sin6_addr)[4] == 0 && ((unsigned short*)&out->sin6_addr)[5] == 0xffff) { //we're listening on a hybrid socket, so if we get an ipv4 packet reply with a proper ipv4 address. WriteBigShort(m, atrtype); WriteBigShort(m, 4+4); WriteBigShort(m, 1); //ipv4 xoredport = out->sin6_port ^ *(unsigned short*)xor; WriteData(m, &xoredport, sizeof(xoredport)); xoredaddr = ((unsigned int*)&out->sin6_addr)[3] ^ *(unsigned int*)xor; WriteData(m, &xoredaddr, sizeof(xoredaddr)); } else { WriteBigShort(m, atrtype); WriteBigShort(m, 4+16); WriteBigShort(m, 2); //ipv6 xoredport = out->sin6_port ^ *(unsigned short*)xor; WriteData(m, &xoredport, sizeof(xoredport)); xoredaddr = ((unsigned int*)&out->sin6_addr)[0] ^ ((unsigned int*)xor)[0]; WriteData(m, &xoredaddr, sizeof(xoredaddr)); xoredaddr = ((unsigned int*)&out->sin6_addr)[1] ^ ((unsigned int*)xor)[1]; WriteData(m, &xoredaddr, sizeof(xoredaddr)); xoredaddr = ((unsigned int*)&out->sin6_addr)[2] ^ ((unsigned int*)xor)[2]; WriteData(m, &xoredaddr, sizeof(xoredaddr)); xoredaddr = ((unsigned int*)&out->sin6_addr)[3] ^ ((unsigned int*)xor)[3]; WriteData(m, &xoredaddr, sizeof(xoredaddr)); } } } static void TURN_ReadXorAddress(netmsg_t *m, netadr_t *adr) { int type = ReadBigShort(m)&0xff; unsigned char *xor = ((unsigned char*)m->data+4); memset(adr, 0, sizeof(*adr)); if (type == 1) { struct sockaddr_in *out = (struct sockaddr_in *)&adr->sockaddr; memset(out, 0, sizeof(*out)); out->sin_family = AF_INET; out->sin_port = *(unsigned short*)((unsigned char*)m->data+m->readpos) ^ *(unsigned short*)xor; m->readpos += 2; out->sin_addr.s_addr = *(unsigned int*)((unsigned char*)m->data+m->readpos) ^ *(unsigned int*)xor; m->readpos += 4; } else if (type == 2) { struct sockaddr_in6 *out = (struct sockaddr_in6 *)&adr->sockaddr; memset(out, 0, sizeof(*out)); out->sin6_family = AF_INET6; out->sin6_port = *(unsigned short*)((unsigned char*)m->data+m->cursize) ^ *(unsigned short*)xor; m->readpos += 2; ((unsigned int*)&out->sin6_addr)[0] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[0] ^ ((unsigned int*)xor)[0]; ((unsigned int*)&out->sin6_addr)[1] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[1] ^ ((unsigned int*)xor)[1]; ((unsigned int*)&out->sin6_addr)[2] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[2] ^ ((unsigned int*)xor)[2]; ((unsigned int*)&out->sin6_addr)[3] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[3] ^ ((unsigned int*)xor)[3]; m->readpos += 16; } } #ifdef TURN_IS_NOT_BLIND static qboolean TURN_PacketIsStun(unsigned char *msg, size_t sz) { //used to check if a packet going through the relay is a stun packet. //we require the peer to 'reply' with a stun packet before we allow non-stun packets through. //this prevents us from being used to attack other services, for the most part. if (sz < 20) return false; //too small for even just the header if ((((msg[0]<<8)|msg[1])&~STUN_ERROR) != STUN_BINDING) return false; //not valid for opening the connection... don't let turn allocation requests through... if (((msg[2]<<8)|(msg[3]<<0)) != sz-20) return false; //bad size if (((msg[4]<<24)|(msg[5]<<16)|(msg[6]<<8)|(msg[7]<<0)) != STUN_MAGIC_COOKIE) return false; //could actually be stun, but w/e //that's probably enough checks. return true; } static qboolean TURN_PacketIsBanned(unsigned char *msg, size_t sz) { //if it looks like a stun packet, and isn't a binding, assume its a TURN packet and block it. no nesting turn packets over turn. if (sz >= 20) if (((msg[4]<<24)|(msg[5]<<16)|(msg[6]<<8)|(msg[7]<<0)) == STUN_MAGIC_COOKIE) if (((msg[0]<<8)|msg[1]) != STUN_BINDING) if (((msg[2]<<8)|(msg[3]<<0)) == sz-20) return true; if (TURN_PacketIsStun(msg,sz)) return false; //allow the ICE probes. if (sz && (msg[0] >= 20 && msg[0] <= 63)) return false; //always allow dtls return true; //block everything else. we're expecting these to be run by arbitrary third parties, so unencrypted stuff is --ed. } #endif void TURN_CheckFDs(cluster_t *cluster) { int ofs = 20 + 4+20 + 4; char buf[8192]; int len; int addrlen; struct turnclient_s *t, **link; netadr_t from = {NULL}; //FIXME: use epoll or something. this loop is stupid. for (link = &cluster->turns; (t = *link);) { if ((int)(t->timeout-cluster->curtime) < 0) { //check timeouts, kill if expired. cluster->numrelays--; //printf("relay %s timeout\n", t->username); *link = t->next; //remove it closesocket(t->remotesock); free(t); continue; } link = &t->next; addrlen = sizeof(from.sockaddr); len = recvfrom(t->remotesock, buf+ofs, sizeof(buf)-ofs, 0, (struct sockaddr*)&from.sockaddr, &addrlen); if (len > 0) { int perm = TURN_FindPermission(cluster, t, &from); if (perm < 0) { //Sys_Printf(cluster, "TURN: (inbound) peer not authorised\n"); continue; } if (t->isfwd) { //just directly forward it to the client. netadr_t fup; if (len > 4 && *(unsigned int*)(buf+ofs) == ~0) t->remotes[perm].seenstun = true; //proper out of band packets, woo... assume its okay. else if (!t->remotes[perm].seenstun) continue; //err... its giving weird responses... NET_SendPacket(cluster, NET_ChooseSocket(cluster->qwdsocket, &fup, t->clientaddr), len, buf+ofs, fup); continue; } #ifdef TURN_IS_NOT_BLIND if (!t->remotes[perm].seenstun) { if (TURN_PacketIsStun(buf+ofs, len)) t->remotes[perm].seenstun = true; //its an ICE/stun binding packet. peer is a genuine ice peer, open it up. else continue; //don't let anything through at all until we've seen a stun-binding packet from the peer (required as part of ICE). } else if (TURN_PacketIsBanned(buf+ofs, len)) continue; #endif { netmsg_t o = {0}; o.maxsize = ofs; ofs -= 4; if (((struct sockaddr*)from.sockaddr)->sa_family == AF_INET) ofs -= 4+8; else if (((struct sockaddr*)from.sockaddr)->sa_family == AF_INET6) ofs -= 4+20; ofs -= 20; o.maxsize -= ofs; o.data = buf+ofs; o.cursize=0; WriteBigShort(&o, STUN_DATA|STUN_INDICATION); WriteBigShort(&o, 0); //size (filled in later) WriteBigLong(&o, STUN_MAGIC_COOKIE); WriteBigLong(&o, 0); WriteBigLong(&o, 0); WriteBigLong(&o, 0); TURN_AddXorAddress(&o, STUNATTR_XOR_PEER_ADDRESS, &from); WriteBigShort(&o, STUNATTR_DATA); WriteBigShort(&o, len); //should be at our original write pos now... o.cursize += len; o.maxsize = sizeof(buf)-ofs; while(o.cursize&3) WriteByte(&o, 0); //pad it to 4 bytes, to make chrome happy. TURN_Send(cluster, &o, &t->clientaddr); } } } Fwd_DoPing(cluster); } static struct turnclient_s *TURN_Allocate(cluster_t *cluster, netadr_t *clientadr, int fam, const char *username, const char *realm) { unsigned char dig[64]; struct turnclient_s *t; SOCKET sock; size_t i; int pf; struct sockaddr *address; struct sockaddr_in address4; struct sockaddr_in6 address6; socklen_t addrlen; unsigned long nonblocking = true; unsigned long v6only = false; unsigned short *port; unsigned int tries = 5; switch(fam) { case 2: pf = PF_INET6; memset(&address6, 0, sizeof(address6)); address6.sin6_family = AF_INET6; port = &address6.sin6_port; address = (struct sockaddr*)&address6; addrlen = sizeof(address6); break; case 1: pf = PF_INET; address4.sin_family = AF_INET; address4.sin_addr.s_addr = INADDR_ANY; port = &address4.sin_port; address = (struct sockaddr*)&address4; addrlen = sizeof(address4); break; default: return NULL; //erk } if ((sock = socket (pf, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) return NULL; #if defined(FD_SETSIZE) && !defined(HAVE_EPOLL) if (sock >= FD_SETSIZE) { //'select' cannot cope with this fd. dom't bug out. closesocket(sock); return NULL; } #endif if (ioctlsocket (sock, FIONBIO, &nonblocking) == -1) { closesocket(sock); return NULL; } if (pf == AF_INET6) setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)); #if defined(_WIN32) && defined(SO_EXCLUSIVEADDRUSE) //win32 is so fucked up setsockopt(newsocket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *)&_true, sizeof(_true)); #endif for (tries = 5; ; tries--) { if (!realm) //qwfwd always uses ephemerial ports. they're outgoing and thus don't need special attention to work around the relay's firewall. *port = 0; else if (cluster->turn_maxport <= cluster->turn_minport) //something screwy *port = htons(cluster->turn_minport); else *port = htons(cluster->turn_minport + rand()%(cluster->turn_maxport-cluster->turn_minport)); //pick a random port from the allowed range (constrained by firewall/router settings). if (bind (sock, (void *)address, addrlen) != -1) break; //success. if (tries <= 0 || *port==0) { //ran out of attempts to pick a random usable port. closesocket(sock); return NULL; } } t = calloc(1, sizeof(*t) + 64 + strlen(username)+1); t->remotesock = sock; memcpy(t->relayaddr.sockaddr, address, addrlen); getsockname(sock, (struct sockaddr*)t->relayaddr.sockaddr, &addrlen); //find how it was actually bound //report the public address reported by the master. if (((struct sockaddr *)t->relayaddr.sockaddr)->sa_family == AF_INET) memcpy(&((struct sockaddr_in *)t->relayaddr.sockaddr)->sin_addr, cluster->turn_ipv4, sizeof(cluster->turn_ipv4)); else if (((struct sockaddr *)t->relayaddr.sockaddr)->sa_family == AF_INET6) memcpy(&((struct sockaddr_in6 *)t->relayaddr.sockaddr)->sin6_addr, cluster->turn_ipv6, sizeof(cluster->turn_ipv6)); t->clientaddr = *clientadr; t->timeout = cluster->curtime + 5*60*1000; for (i = 0; i < countof(t->remotes); i++) t->remotes[i].timeout = cluster->curtime - 1; //some time in the past. t->auth = (char*)(t+1); t->username = t->auth+64; strcpy(t->username, username); tobase64(t->auth,64, dig, CalcHMAC(&hash_sha1, dig,sizeof(dig), t->username,strlen(t->username), cluster->turnkey,sizeof(cluster->turnkey))); //compute our 'long-term' key. stoopid md5. so we can send the right responses. if (realm) { hashfunc_t *pwdhash = &hash_md5; size_t usersz = strlen(t->username); size_t realmsz = strlen(realm); size_t authsz = strlen(t->auth); char *tmpkey = alloca(usersz + realmsz + authsz + 3); memcpy(tmpkey+0, t->username, usersz); tmpkey[usersz] = ':'; memcpy(tmpkey+usersz+1, realm, realmsz); tmpkey[usersz+1+realmsz] = ':'; memcpy(tmpkey+usersz+1+realmsz+1, t->auth, authsz); tmpkey[usersz+1+realmsz+1+authsz] = '\0'; t->keysize = CalcHash(pwdhash, t->key,sizeof(t->key), tmpkey,strlen(tmpkey)); } else t->isfwd = true; cluster->numrelays++; t->next = cluster->turns; cluster->turns = t; #ifdef HAVE_EPOLL { struct epoll_event ev; ev.data.ptr = t; ev.events = EPOLLIN; epoll_ctl(cluster->epfd, EPOLL_CTL_ADD, t->remotesock, &ev); } #endif return t; } static size_t TURN_GenerateNonce(netadr_t *adr, char *buf, size_t bufsize) { //needs to be reproducible. //also pretty much needs to be base64. stoopid stun rules. char key[64]; size_t keysize; hashfunc_t *func = &hash_sha1; //any. void *ctx = alloca(func->contextsize); if (sizeof(key) < func->digestsize) return 0; //panic func->init(ctx); func->process(ctx, "weewoo", 6); if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET) { struct sockaddr_in *out = (struct sockaddr_in *)&adr->sockaddr; func->process(ctx, (void*)out,sizeof(*out)); } else if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET6) { struct sockaddr_in6 *out = (struct sockaddr_in6 *)&adr->sockaddr; func->process(ctx, (void*)out,sizeof(*out)); } else return 0; //shouldn't be possible... should probably put an assert here. func->terminate(key, ctx); keysize = func->digestsize; tobase64(buf,bufsize, key,keysize); return strlen(buf); } static qboolean TURN_ValidateNonce(unsigned char *nonce, netadr_t *adr) { unsigned char buf[20]; size_t insz = (nonce?nonce[-1]:0); //read the attribute's length. lazily. size_t needsz = TURN_GenerateNonce(adr, buf, sizeof(buf)); if (insz != needsz) return false; //screwy size... reject it. else return !memcmp(nonce, buf, needsz); } static qboolean TURN_ValidateIntegrity(cluster_t *cluster, netmsg_t *m, const unsigned char *user, const unsigned char *realm, const unsigned char *integritycheck, const struct turnclient_s *t) { int attroffset = (integritycheck-(unsigned char*)m->data); unsigned char needintegrity[32]; size_t keysize; unsigned char key[32]; char authbuf[64]; char *auth; hashfunc_t *pwdhash = &hash_md5; hashfunc_t *hash; if (((integritycheck[-4]<<8)|integritycheck[-3]) == STUNATTR_MSGINTEGRITIY_SHA1) hash = &hash_sha1; // else if (((integritycheck[-4]<<8)|integritycheck[-3]) == STUNATTR_MSGINTEGRITIY_SHA2_256) // hash = &hash_sha2_256; else return false; //can't validate this if (((integritycheck[-2]<<8)|integritycheck[-1]) != hash->digestsize) return false; //nope... screwy. if (t) { //we already validated the username... make sure the connection stays using the same one. if (strcmp(user, t->username)) return false; //err... it changed? no, get lost. auth = t->auth; //still using the same password, too } else { //validate the username... we just check the timestamp bit. time_t timestamp = strtoull(user, NULL, 0); time_t now = time(NULL); unsigned char dig[32]; if (timestamp > now+10) return false; //clockskew? reject timestamps in the future... if (timestamp+60*60 < now) return false; //more than an hour old, you'll need to get a new authorisation. //figure out what the correct auth string should be. keysize = CalcHMAC(&hash_sha1, dig,sizeof(dig), user,strlen(user), cluster->turnkey,sizeof(cluster->turnkey)); tobase64(authbuf,sizeof(authbuf), dig, keysize); auth = authbuf; //this should match what the client was told by the broker. } //compute our 'long-term' key. stoopid md5. { size_t usersz = strlen(user); size_t realmsz = strlen(realm); size_t authsz = strlen(auth); char *tmpkey = alloca(usersz + realmsz + authsz + 3); memcpy(tmpkey+0, user, usersz); tmpkey[usersz] = ':'; memcpy(tmpkey+usersz+1, realm, realmsz); tmpkey[usersz+1+realmsz] = ':'; memcpy(tmpkey+usersz+1+realmsz+1, auth, authsz); tmpkey[usersz+1+realmsz+1+authsz] = '\0'; keysize = CalcHash(pwdhash, key,sizeof(key), tmpkey,strlen(tmpkey)); } //message integrity is a bit annoying ((unsigned char*)m->data)[2] = ((attroffset+hash->digestsize-20)>>8)&0xff; //hashed header length is up to the end of the hmac attribute ((unsigned char*)m->data)[3] = ((attroffset+hash->digestsize-20)>>0)&0xff; attroffset-=4; //but the hash is to the start of the attribute's header //compute the hmac that we're actually checking, success if it matches. keysize = CalcHMAC(hash, needintegrity, sizeof(needintegrity), m->data, attroffset, key,keysize); return !memcmp(needintegrity, integritycheck, keysize); } static void TURN_AddIntegrity(cluster_t *cluster, netmsg_t *m, const struct turnclient_s *t) { size_t keysize; hashfunc_t *hash = &hash_sha1; unsigned char integrity[64]; unsigned int crc; if (t) { //message integrity is a bit annoying ((unsigned char*)m->data)[2] = ((m->cursize+4+hash->digestsize-20)>>8)&0xff; //hashed header length is up to the end of the hmac attribute ((unsigned char*)m->data)[3] = ((m->cursize+4+hash->digestsize-20)>>0)&0xff; keysize = CalcHMAC(hash, integrity, sizeof(integrity), m->data, m->cursize, t->key,t->keysize); WriteBigShort(m, STUNATTR_MSGINTEGRITIY_SHA1); WriteBigShort(m, keysize); WriteData(m, integrity, keysize); } #ifndef NO_ZLIB ((unsigned char*)m->data)[2] = ((m->cursize+8-20)>>8)&0xff; //dummy length ((unsigned char*)m->data)[3] = ((m->cursize+8-20)>>0)&0xff; crc = crc32(0, m->data, m->cursize)^0x5354554e; WriteBigShort(m, STUNATTR_FINGERPRINT); WriteBigShort(m, sizeof(crc)); WriteBigLong(m, crc); #endif } static qboolean TURN_ValidateFingerprint(cluster_t *cluster, netmsg_t *m, unsigned int needcrc) { //this just verifies that its actually a correct non-corrupted STUN packet. otherwise assume some other multiplexed protocol. unsigned int crc; if (m->cursize != m->readpos) return false; //must be the last attribute. so don't actually need to fiddle the size. ((unsigned char*)m->data)[2] = (((m->readpos-20)>>8)&0xff); //restore the size (integrity might have fucked with it) ((unsigned char*)m->data)[3] = (((m->readpos-20)>>0)&0xff); crc = crc32(0, m->data, m->readpos-8)^0x5354554e; if (needcrc == crc) return true; //needs to match. return false; //oops. } qboolean TURN_IsRequest(cluster_t *cluster, netmsg_t *m, netadr_t *from) { stunhdr_t hdr; unsigned short atr, len, prot; unsigned int nextread; unsigned int error = 0; void *data; int fam = 1; size_t datasize; size_t lifetime; netadr_t peeraddr[8]; int inpeers; struct turnclient_s *t; unsigned char *innonce, *inrealm, *inuser, *inintegrity; unsigned char noncebuf[64]; //try and identify them first. for (t = cluster->turns; t; t = t->next) { if (Net_CompareAddress(&t->clientaddr, from, 0, 1)) { //forward it as-is... mostly. if (!t->isfwd) //turn... check the packet for stun/turn stuff. we now know their connection. woo. break; if (m->cursize>=11 && !memcmp("\xff\xff\xff\xff""connect ", m->data, 11)) { //connect ver qport challenge info //we need to pop one proxy from the 'prx' list so the next proxy doesn't try to connect to itself. char userinfo[1024]; char prx[256]; char *s = (char*)m->data+11; char *at; char *infostart; size_t pre,infolen,trail; //parse it a bit s = COM_ParseToken(s, noncebuf,sizeof(noncebuf), ""); //read the ver if (atoi(noncebuf)!=28) return true; //that protocol ain't supported... nor allowed. get lost. we won't be able to block it over dtls but we also shouldn't need to care (yay for using dtlsconnect instead, which leaks no userinfo beyond the target chain) s = COM_ParseToken(s, noncebuf,sizeof(noncebuf), ""); //read the qport infostart = s= COM_ParseToken(s, noncebuf,sizeof(noncebuf), ""); //read the challenge (probably not ours) s = COM_ParseToken(s, userinfo,sizeof(userinfo), ""); //read the challenge (probably not ours) //change the userinfo Info_ValueForKey(userinfo, "prx", prx,sizeof(prx)); at = strrchr(prx, '@'); if (!*prx) return true; //err... no next hop? we're meant to be handling it? wtf? else if (at) *at = 0; //strip off the last proxy (the one we're meant to be proxying to) else *prx = 0; //clear it entirely. next hop is the final server. lucky client. Info_SetValueForStarKey(userinfo, "prx", prx, sizeof(userinfo)); //we could set some "*clientip" key, appending 'from', so the server can use that instead of realip mess, but that'd require the server to trust us when banning. //hack up the packet to include the new info (with any trailing data still in place, ready for the next hop to see. pre = (infostart-(char*)m->data); infolen = strlen(userinfo); trail = m->cursize-(s-(char*)m->data); m->cursize = pre+2+infolen+1+trail; if (m->cursize > m->maxsize) return true; //don't crash. memmove(&((char*)m->data)[pre+infolen+3], s, trail); ((char*)m->data)[pre] = ' '; ((char*)m->data)[pre+1] = '\"'; memcpy(&((char*)m->data)[pre+2], userinfo, infolen); ((char*)m->data)[pre+2+infolen] = '\"'; //should have wiped the userinfo part... //((char*)m->data)[m->cursize] = 0; //for debugging. } else if (m->cursize>=15 && !memcmp("\xff\xff\xff\xff""dtlsconnect ", m->data, 15)) { //dtlsconnect challenge target //we need to strip the target. the connect will then be hidden from us. char *peer = COM_ParseToken((char*)m->data+15, noncebuf,sizeof(noncebuf), ""); //read the peer's challenge. char *at = strrchr(peer, '@'); if (!*peer) return true; //no next hop? wtf? peer = (at?at:peer); m->cursize = peer-(char*)m->data; //just truncate it. //while (*peer == '@' || *peer == ' ') // peer++; //NET_StringToAddr(peer, &peeraddr[0], 27500); //if (!Net_CompareAddress(&peeraddr[0], t->remotes[0].remoteaddr)) // return true; //tried connecting to somewhere else... doesn't make sense. } t->timeout = t->remotes[0].timeout = cluster->curtime + QWFWDTIMEOUT; //renew it from c2s packets. if (!t->remotes[0].seenstun && m->cursize < 4 && *(unsigned int*)m->data!=~0) return true; //don't allow it through until we get a proper response from the target to know its actually valid and not a ddos. NET_SendPacket(cluster, t->remotesock, m->cursize, m->data, t->remotes[0].remoteaddr); return true; } } if (m->cursize < 20 || !cluster->turnenabled) return false; hdr.msgtype = ReadBigShort(m); // if (hdr.msgtype != STUN_ALLOCATE) // return false; hdr.msglen = ReadBigShort(m); if (20+hdr.msglen != m->cursize) return false; //nope... sized wrong. must be something else. hdr.magiccookie = ReadBigLong(m); if (hdr.magiccookie != STUN_MAGIC_COOKIE) return false; //might be an older version, but we don't care. don't let it multiplex badly. hdr.transactid[0] = ReadBigLong(m); hdr.transactid[1] = ReadBigLong(m); hdr.transactid[2] = ReadBigLong(m); t = NULL; prot = 0; lifetime = 0; data = NULL; datasize = 0; inuser = inrealm = innonce = inintegrity = NULL; inpeers = 0; //Sys_Printf(cluster, "TURN: msgtype %x\n", hdr.msgtype); for(; m->readpos < m->cursize; m->readpos = nextread) { atr = ReadBigShort(m); len = ReadBigShort(m); nextread = m->readpos + ((len+3)&~3); if (nextread > m->cursize) return false; //nope, corrupt //Sys_Printf(cluster, " TURN: attribute %04x\n", atr); if (atr == STUNATTR_FINGERPRINT) { #ifdef NO_ZLIB continue; #else if (!TURN_ValidateFingerprint(cluster, m, ReadBigLong(m))) { //Sys_Printf(cluster, " TURN: Invalid fingerprint\n"); return false; } #endif } /*else if (atr == STUNATTR_MSGINTEGRITIY_SHA2_256 && (!inintegrity || (inintegrity[-4]<<8)|inintegrity[-3]==STUNATTR_MSGINTEGRITIY_SHA1)) { inintegrity = (char*)m->data + m->readpos; }*/ else if (inintegrity) { //Sys_Printf(cluster, " TURN: Ignoring post-integrity %04x\n", atr); continue; //anything after the integrity must be ignored (except those things above). } else if (atr == STUNATTR_MSGINTEGRITIY_SHA1) inintegrity = (char*)m->data + m->readpos; else if (atr == STUNATTR_REQUESTED_TRANSPORT) prot = ReadBigLong(m)>>24; else if (atr == STUNATTR_LIFETIME) lifetime = ReadBigLong(m)*1000; else if (atr == STUNATTR_REQUESTED_ADDRFAM) fam = ReadBigLong(m); else if (atr == STUNATTR_ADDITIONAL_ADDRFAM) { //optional //fam = ReadBigLong(m); } else if (atr == STUNATTR_DONT_FRAGMENT) ; else if (atr == STUNATTR_SOFTWARE) ; else if (atr == STUNATTR_USERNAME) inuser = (char*)m->data + m->readpos; else if (atr == STUNATTR_REALM) inrealm = (char*)m->data + m->readpos; else if (atr == STUNATTR_NONCE) innonce = (char*)m->data + m->readpos; else if (atr == STUNATTR_DATA && hdr.msgtype == (STUN_INDICATION|STUN_SEND)) { data = (char*)m->data + m->readpos; datasize = len; } else if (atr == STUNATTR_XOR_PEER_ADDRESS && ( hdr.msgtype == (STUN_INDICATION|STUN_SEND) || hdr.msgtype == STUN_CREATEPERM)) { if (inpeers < countof(peeraddr)) TURN_ReadXorAddress(m, &peeraddr[inpeers++]); //FIXME: we should support multiple of these. } else if (atr & 0x8000) { continue; //unknown optional attributes } else error = 420; } if (!error && prot != 17 && hdr.msgtype == STUN_ALLOCATE) //only udp supported between relay and remote peer. error = 442; if (!error) { if (hdr.msgtype == STUN_CREATEPERM || hdr.msgtype == STUN_REFRESH || hdr.msgtype == STUN_ALLOCATE) { if (!inuser || !inrealm || !innonce || !inintegrity) error = 401; //something is null... else if (!TURN_ValidateNonce(innonce, from)) error = 438; //'Stale Nonce' shouldn't really be happening... client somehow changed address? else if (t && strcmp(inuser, t->username)) error = 441; //'Wrong Credentials' - no changing usernames! else if (!TURN_ValidateIntegrity(cluster, m, inuser, inrealm, inintegrity, t)) { //Sys_Printf(cluster, " TURN: Validation failed\n"); error = 401; //'Unauthorised' } else if (!t && hdr.msgtype == STUN_ALLOCATE) { t = TURN_Allocate(cluster, from, fam, inuser, inrealm); if (!t) error = 508; //'Insufficient Capacity' } } } if (!t && !error) error = 437; //'Allocation Mismatch' if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH || hdr.msgtype == STUN_CREATEPERM) { struct{ stunhdr_t hdr; unsigned int data[64]; } pkt; netmsg_t o = {0, 0, sizeof(pkt), &pkt}; if (t && !error && hdr.msgtype == STUN_CREATEPERM) { //FIXME: request could include multiple peers int i, p; for (p = 0; p < inpeers; p++) if (!TURN_Permissable(&peeraddr[p])) error = 403; //no relaying to localhost/etc. no bypassing firewalls. if (!error) { for (p = 0; p < inpeers; p++) { for (i = 0; i < countof(t->remotes); i++) { if ((signed int)(t->remotes[i].timeout-cluster->curtime) < 0) continue; //look for a live one... if (Net_CompareAddress(&t->remotes[i].remoteaddr, &peeraddr[p], 0,1)) { t->remotes[i].timeout = cluster->curtime + 5*60*1000; //bump it. break; } } if (i == countof(t->remotes)) { for (i = 0; i < countof(t->remotes); i++) { if ((signed int)(t->remotes[i].timeout-cluster->curtime) < 0) { //this one's dead... t->remotes[i].remoteaddr = peeraddr[p]; t->remotes[i].timeout = cluster->curtime + 5*60*1000; break; } } if (i == countof(t->remotes)) error = 508; //'Insufficient Capacity' } } } } if (error) WriteBigShort(&o, STUN_ERROR|hdr.msgtype); else WriteBigShort(&o, STUN_REPLY|hdr.msgtype); WriteBigShort(&o, 0); WriteBigLong(&o, hdr.magiccookie); WriteBigLong(&o, hdr.transactid[0]); WriteBigLong(&o, hdr.transactid[1]); WriteBigLong(&o, hdr.transactid[2]); if (error) { //WriteBigShort(STUNATTR_SOFTWARE); WriteBigShort(&o, STUNATTR_ERROR_CODE); WriteBigShort(&o, 4); WriteBigLong(&o, ((error/100)<<8) | (error%100)); //this is stupid. if (error == 420) ;//WriteBigShort(STUNATTR_UNKNOWN_ATTRIBUTES); if ((error == 401 && !innonce) || error == 438) { char *realm = "fteqtv"; size_t sz = TURN_GenerateNonce(from, noncebuf, sizeof(noncebuf)); WriteBigShort(&o, STUNATTR_NONCE); WriteBigShort(&o, sz); WriteData(&o, noncebuf, sz); while (o.cursize&3) WriteByte(&o, 0); sz = strlen(realm); WriteBigShort(&o, STUNATTR_REALM); WriteBigShort(&o, sz); WriteData(&o, realm, sz); while (o.cursize&3) WriteByte(&o, 0); } } else if (t) { if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH) { if (lifetime > 5*60*1000) lifetime = 5*60*1000; if (lifetime && t->timeout < lifetime) t->timeout = lifetime; } lifetime = t->timeout; //WriteBigShort(STUNATTR_SOFTWARE); if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH) { WriteBigShort(&o, STUNATTR_LIFETIME); WriteBigShort(&o, 4); WriteBigLong(&o, lifetime/1000); } if (hdr.msgtype == STUN_ALLOCATE) { TURN_AddXorAddress(&o, STUNATTR_XOR_RELAYED_ADDRESS, &t->relayaddr); TURN_AddXorAddress(&o, STUNATTR_XOR_MAPPED_ADDRESS, from); } if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH || hdr.msgtype == STUN_CREATEPERM) TURN_AddIntegrity(cluster, &o, t); } else return true; //wut? TURN_Send(cluster, &o, from); return true; } else if (hdr.msgtype == (STUN_INDICATION|STUN_SEND)) { //just sending to the other end if (t && inpeers == 1) { int perm = TURN_FindPermission(cluster, t, &peeraddr[0]); //sends do NOT refresh. if (perm >= 0) { #ifdef TURN_IS_NOT_BLIND if (!t->remotes[perm].seenstun && !TURN_PacketIsStun(data, datasize)) ; //don't relay non-stun packets until we know the peer is running a stun server too. else if (TURN_PacketIsBanned(data, datasize)) ; else #endif NET_SendPacket(cluster, t->remotesock, datasize, data, peeraddr[0]); } } return true; } else return false; //dunno what that rubbish is. } void Fwd_NewQWFwd(cluster_t *cluster, netadr_t *from, char *targ) { char *rechallenge = "\xff\xff\xff\xffgetchallenge\n"; struct turnclient_s *t; netadr_t adr; char *at = strrchr(targ, '@'); if (at) targ = at+1; //we only care about the next hop here, not the full route. if (NET_StringToAddr(targ, &adr, 27500)) { if (!cluster->relayenabled) { Netchan_OutOfBandPrint(cluster, *from, "n" "Relay not enabled.\n"); return; //don't allow it. } else if (!TURN_Permissable(&adr)) { //don't route to 127.* or 192.168.* etc. Netchan_OutOfBandPrint(cluster, *from, "n" "Target address is private\n"); return; } else t = TURN_Allocate(cluster, from, (((struct sockaddr*)adr.sockaddr)->sa_family==AF_INET6)?2:1, "fwd", NULL); if (t) { t->remotes[0].remoteaddr = adr; t->remotes[0].timeout = cluster->curtime + QWFWDTIMEOUT; //Netchan_OutOfBandPrint(cluster, *from, "n" "relay established\n"); //send a quick challenge to the remote as if it came from the client. the client can then deal with it when it comes back, instead of waiting for the client's next getchallenge timeout. NET_SendPacket(cluster, t->remotesock, strlen(rechallenge), rechallenge, t->remotes[0].remoteaddr); return; } else Netchan_OutOfBandPrint(cluster, *from, "n" "Unable to set up relay\n"); //ran out of FDs? } else Netchan_OutOfBandPrint(cluster, *from, "n" "Unanle to resolve target address: %s\n", targ); } struct relaypeer_s { netadr_t adr; unsigned int lastalive; //last time it was reported by a master (removed after timeout) unsigned int lastpong; //when we last saw a response from them unsigned int lastping; //timestamp we last sent a ping unsigned int curping; //ping time from last pong (or PING_UNRESPONSIVE) #define PING_UNRESPONSIVE (~0u) struct relaypeer_s *next; }; static void Fwd_AddPeer(cluster_t *cluster, netadr_t *a) { struct relaypeer_s *rp; //this loop could be improved with a hash table. for (rp = cluster->relaypeer; rp; rp = rp->next) { if (Net_CompareAddress(&rp->adr, a, 0, 1)) break; } if (!rp) { //add it... if (!TURN_Permissable(a)) return; //unless its a private address. rp = malloc(sizeof(*rp)); rp->adr = *a; rp->lastpong = cluster->curtime; rp->lastping = cluster->curtime+0x80000000; //something that's expired. rp->curping = PING_UNRESPONSIVE; //don't know yet. rp->next = cluster->relaypeer; cluster->relaypeer = rp; cluster->numpeers += 1; } rp->lastalive = cluster->curtime; //mark as still alive. } void Fwd_ParseServerList(cluster_t *cluster, netmsg_t *m, int af) { //response from master netadr_t a; int j; char t; while(m->readpos < m->cursize) { if (af == AF_INET) t = '\\'; else if (af == AF_INET6) t = '/'; else t = ReadByte(m); //fancy protocol that has leading ids if (t == '\\') { //ipv4 struct sockaddr_in *o = (struct sockaddr_in *)a.sockaddr; if (m->readpos+6 > m->cursize) break; //eof? memset(&a, 0, sizeof(a)); o->sin_family = AF_INET; for (j = 0; j < 4; j++) ((char*)&o->sin_addr)[j] = ReadByte(m); ((char*)&o->sin_port)[0] = ReadByte(m); ((char*)&o->sin_port)[1] = ReadByte(m); } else if (t == '/') { //ipv6 struct sockaddr_in6 *o = (struct sockaddr_in6 *)a.sockaddr; if (m->readpos+18 > m->cursize) break; //eof? memset(&a, 0, sizeof(a)); o->sin6_family = AF_INET6; for (j = 0; j < 16; j++) ((char*)&o->sin6_addr)[j] = ReadByte(m); ((char*)&o->sin6_port)[0] = ReadByte(m); ((char*)&o->sin6_port)[1] = ReadByte(m); continue; //pingstatus does not support ipv6, so don't bother pinging any. } else break; //erk? Fwd_AddPeer(cluster, &a); } } void Fwd_PingResponse(cluster_t *cluster, netadr_t *from) { struct relaypeer_s *rp; //this loop could be improved with a hash table. for (rp = cluster->relaypeer; rp; rp = rp->next) { if (Net_CompareAddress(&rp->adr, from, 0, 1)) { //we have no way to verify the peer. //its probably better to allow an attacker to force a large ping rather than a low one. its more obvious. //the real target should respond eventually giving an upper bound for the ping value. //don't extend aliveness though, if a peer stops heartbeating then we just let it hide. only the master responses may extend its lifetime. rp->curping = cluster->curtime - rp->lastping; rp->lastpong = cluster->curtime; return; } } } static void Fwd_DoPing(cluster_t *cluster) { struct relaypeer_s *rp, **l; if (!cluster->pingtreeenabled || !cluster->relayenabled) return; //don't spam at all. if (cluster->curtime-cluster->relay_lastping < QRY_PINGINTERVAL) return; //don't burst cluster->relay_lastping = cluster->curtime; if (cluster->curtime-cluster->relay_lastquery >= QRY_SERVERLISTINTERVAL) { netadr_t adr; cluster->relay_lastquery = cluster->curtime; if (NET_StringToAddr(cluster->master, &adr, 27950)) { if (((struct sockaddr_in *)adr.sockaddr)->sin_family == AF_INET && ((struct sockaddr_in *)adr.sockaddr)->sin_port == htons(27000)) { netadr_t realadr; NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &realadr, adr), 2, "c\n", realadr); //legacy. screwy. no 0xff prefix. } else Netchan_OutOfBandPrint(cluster, adr, "getserversExt %s %u empty full"/*" ipv6"*/, cluster->protocolname, cluster->protocolver); } return; //space it out with pings. } for (l = &cluster->relaypeer; (rp=*l);) { if (cluster->curtime-rp->lastping >= QRY_REPINGINTERVAL) //its been long enough since this one was pinged... { if (cluster->curtime - rp->lastalive > QRY_TIMEOUT) { //its been too long. cut our losses. its dead jim. *l = rp->next; free(rp); cluster->numpeers -= 1; continue; } //okay, we're pinging this one. rp->lastping = cluster->curtime; Netchan_OutOfBandPrint(cluster, rp->adr, "k"); //tiny unrealistic ping... //unlink it... walk em till the end, and then insert there. if there's 10000 servers then it'll just ping slower... much slower... and fail to report them all in a single udp packet, but hey. for (*l = rp->next; *l; l = &(*l)->next) ; *l = rp; rp->next = NULL; return; //only ping one. } l = &rp->next; } } void Fwd_PingStatus(cluster_t *cluster, netadr_t *from, qboolean ext) { struct relaypeer_s *rp; char buffer[8192]; netmsg_t send; netadr_t fixup; int j; int resetsize; if (!cluster->pingtreeenabled || !cluster->relayenabled) return; //don't bother responding. InitNetMsg (&send, buffer, sizeof(buffer)); //small prefix... WriteLong (&send, -1); // -1 sequence means out of band if (ext) WriteString2 (&send, "pinglist"); else WriteByte (&send, 'n'); // 'a2c_print'... yeah, this is fucked. resetsize = send.cursize; for (rp = cluster->relaypeer; rp; rp = rp->next) { if (rp->lastping > 0x7fff) continue; //don't report dead ones. don't do negatives either! if (((struct sockaddr*)rp->adr.sockaddr)->sa_family == AF_INET) { struct sockaddr_in *a = ((struct sockaddr_in*)rp->adr.sockaddr); if (send.cursize > 1400) { NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &fixup, *from), send.cursize, send.data, fixup); send.cursize = resetsize; } if (ext) WriteByte (&send, '\\'); for (j = 0; j < 4; j++) WriteByte (&send, ((char*)&a->sin_addr)[j]); WriteShort(&send, ntohs(a->sin_port)); WriteShort(&send, rp->curping); } else if (((struct sockaddr*)rp->adr.sockaddr)->sa_family == AF_INET6) { struct sockaddr_in6 *a = ((struct sockaddr_in6*)rp->adr.sockaddr); if (send.cursize > 1400) { NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &fixup, *from), send.cursize, send.data, fixup); send.cursize = resetsize; } if (ext) WriteByte (&send, '/'); else continue; for (j = 0; j < 16; j++) WriteByte (&send, ((char*)&a->sin6_addr)[j]); WriteShort(&send, ntohs(a->sin6_port)); WriteShort(&send, rp->curping); } } // send the datagram NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &fixup, *from), send.cursize, send.data, fixup); } void TURN_RelayStatus(cmdctxt_t *ctx) { cluster_t *cluster = ctx->cluster; unsigned int live=0, unreach=0, dead=0; struct relaypeer_s *rp; unsigned int turns=0, fwds=0; turnclient_t *t; for (t = cluster->turns; t; t = t->next) { if (t->isfwd) fwds++; else turns++; } for (rp = cluster->relaypeer; rp; rp = rp->next) { if (rp->curping == PING_UNRESPONSIVE) unreach++; //never got a response. else if (cluster->curtime-rp->lastpong > 125*1000) dead++; //no response in the last 2 mins else live++; } Cmd_Printf(ctx, " %i relays (%i TURN%s, %i fwd%s)\n", cluster->numrelays, turns, cluster->turnenabled?"":"[OFF]", fwds, cluster->relayenabled?"":"[OFF]"); if (ctx->cluster->turnenabled) Cmd_Printf(ctx, " relaying through %i.%i.%i.%i %i-%i\n", ctx->cluster->turn_ipv4[0],ctx->cluster->turn_ipv4[1],ctx->cluster->turn_ipv4[2],ctx->cluster->turn_ipv4[3], ctx->cluster->turn_minport, ctx->cluster->turn_maxport); else if (cluster->relayenabled) Cmd_Printf(ctx, " relaying through %i.%i.%i.%i\n", ctx->cluster->turn_ipv4[0],ctx->cluster->turn_ipv4[1],ctx->cluster->turn_ipv4[2],ctx->cluster->turn_ipv4[3]); if (cluster->relayenabled) { if (cluster->pingtreeenabled) Cmd_Printf(ctx, " %i peers (%i live, %i stale, %i unreach)\n", cluster->numpeers, live, dead, unreach); else Cmd_Printf(ctx, " pinging disabled\n"); } } ================================================ FILE: fteqtv/sc_dsound.c ================================================ #include "qtv.h" #ifdef COMMENTARY #include static HANDLE hInstDS; static HRESULT (WINAPI *pDirectSoundCaptureCreate)(GUID FAR *lpGUID, LPDIRECTSOUNDCAPTURE FAR *lplpDS, IUnknown FAR *pUnkOuter); typedef struct { soundcapt_t funcs; LPDIRECTSOUNDCAPTURE DSCapture; LPDIRECTSOUNDCAPTUREBUFFER DSCaptureBuffer; long lastreadpos; WAVEFORMATEX wfxFormat; long bufferbytes; } dscapture_t; void DSOUND_CloseCapture(soundcapt_t *ghnd) { dscapture_t *hnd = (dscapture_t*)ghnd; if (hnd->DSCaptureBuffer) { IDirectSoundCaptureBuffer_Stop(hnd->DSCaptureBuffer); IDirectSoundCaptureBuffer_Release(hnd->DSCaptureBuffer); hnd->DSCaptureBuffer=NULL; } if (hnd->DSCapture) { IDirectSoundCapture_Release(hnd->DSCapture); hnd->DSCapture=NULL; } free(hnd); } int DSOUND_UpdateCapture(soundcapt_t *ghnd, int samplechunks, char *buffer) { dscapture_t *hnd = (dscapture_t*)ghnd; HRESULT hr; LPBYTE lpbuf1 = NULL; LPBYTE lpbuf2 = NULL; DWORD dwsize1 = 0; DWORD dwsize2 = 0; DWORD capturePos; DWORD readPos; long filled; int inputbytes; inputbytes = hnd->wfxFormat.wBitsPerSample/8; // Query to see how much data is in buffer. hr = IDirectSoundCaptureBuffer_GetCurrentPosition(hnd->DSCaptureBuffer, &capturePos, &readPos ); if( hr != DS_OK ) { return 0; } filled = readPos - hnd->lastreadpos; if( filled < 0 ) filled += hnd->bufferbytes; // unwrap offset if (filled > samplechunks) //figure out how much we need to empty it by, and if that's enough to be worthwhile. filled = samplechunks; else if (filled < samplechunks) return 0; if ((filled/inputbytes) & 1) //force even numbers of samples filled -= inputbytes; // Lock free space in the DS hr = IDirectSoundCaptureBuffer_Lock ( hnd->DSCaptureBuffer, hnd->lastreadpos, filled, (void **) &lpbuf1, &dwsize1, (void **) &lpbuf2, &dwsize2, 0); if (hr == DS_OK) { // Copy from DS to the buffer memcpy( buffer, lpbuf1, dwsize1); if(lpbuf2 != NULL) { memcpy( buffer+dwsize1, lpbuf2, dwsize2); } // Update our buffer offset and unlock sound buffer hnd->lastreadpos = (hnd->lastreadpos + dwsize1 + dwsize2) % (hnd->bufferbytes); IDirectSoundCaptureBuffer_Unlock ( hnd->DSCaptureBuffer, lpbuf1, dwsize1, lpbuf2, dwsize2); } else { return 0; } return filled/inputbytes; } soundcapt_t *SND_InitCapture (int speed, int bits) { dscapture_t *hnd; DSCBUFFERDESC bufdesc; if (!hInstDS) { hInstDS = LoadLibrary("dsound.dll"); if (hInstDS == NULL) { printf ("Couldn't load dsound.dll\n"); return NULL; } } if (!pDirectSoundCaptureCreate) { pDirectSoundCaptureCreate = (void *)GetProcAddress(hInstDS,"DirectSoundCaptureCreate"); if (!pDirectSoundCaptureCreate) { printf ("Couldn't get DS proc addr (DirectSoundCaptureCreate)\n"); return NULL; } } hnd = malloc(sizeof(dscapture_t)); memset(hnd, 0, sizeof(*hnd)); hnd->wfxFormat.wFormatTag = WAVE_FORMAT_PCM; hnd->wfxFormat.nChannels = 1; hnd->wfxFormat.nSamplesPerSec = speed; hnd->wfxFormat.wBitsPerSample = bits; hnd->wfxFormat.nBlockAlign = hnd->wfxFormat.nChannels * (hnd->wfxFormat.wBitsPerSample / 8); hnd->wfxFormat.nAvgBytesPerSec = hnd->wfxFormat.nSamplesPerSec * hnd->wfxFormat.nBlockAlign; hnd->wfxFormat.cbSize = 0; bufdesc.dwSize = sizeof(bufdesc); bufdesc.dwBufferBytes = hnd->bufferbytes = speed*bits/8; //1 sec bufdesc.dwFlags = 0; bufdesc.dwReserved = 0; bufdesc.lpwfxFormat = &hnd->wfxFormat; pDirectSoundCaptureCreate(NULL, &hnd->DSCapture, NULL); if (FAILED(IDirectSoundCapture_CreateCaptureBuffer(hnd->DSCapture, &bufdesc, &hnd->DSCaptureBuffer, NULL))) { printf ("Couldn't create a capture buffer\n"); IDirectSoundCapture_Release(hnd->DSCapture); free(hnd); return NULL; } IDirectSoundCaptureBuffer_Start(hnd->DSCaptureBuffer, DSBPLAY_LOOPING); hnd->lastreadpos = 0; hnd->funcs.update = DSOUND_UpdateCapture; hnd->funcs.close = DSOUND_CloseCapture; return &hnd->funcs; } /* void soundtestcallback (char *buffer, int samples, int bitspersample) { FILE *f; f = fopen("c:/test.raw", "at"); fseek(f, 0, SEEK_END); fwrite(buffer, samples, bitspersample/8, f); fclose(f); } void soundtest(void) { soundcapt_t *capt; capt = SNDDMA_InitCapture(11025, 8); while(1) capt->update(capt, 1400, soundtestcallback); capt->close(capt); } */ #endif ================================================ FILE: fteqtv/source.c ================================================ /* Copyright (C) 1996-1997 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 included (GNU.txt) 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. */ //connection notes //The connection is like http. //The stream starts with a small header. //The header is a list of 'key: value' pairs, separated by new lines. //The header ends with a totally blank line. //to record an mvd from telnet or somesuch, you would use: //"QTV\nRAW: 1\n\n" //VERSION: a list of the different qtv protocols supported. Multiple versions can be specified. The first is assumed to be the preferred version. //RAW: if non-zero, send only a raw mvd with no additional markup anywhere (for telnet use). Doesn't work with challenge-based auth, so will only be accepted when proxy passwords are not required. //AUTH: specifies an auth method, the exact specs varies based on the method // PLAIN: the password is sent as a PASSWORD line // MD4: the server responds with an "AUTH: MD4\n" line as well as a "CHALLENGE: somerandomchallengestring\n" line, the client sends a new 'initial' request with CHALLENGE: MD4\nRESPONSE: hexbasedmd4checksumhere\n" // etc: same idea as md4 // CCITT: same as md4, but using the CRC stuff common to all quake engines. should not be used. // if the supported/allowed auth methods don't match, the connection is silently dropped. //SOURCE: which stream to play from, DEFAULT is special. Without qualifiers, it's assumed to be a tcp address. //COMPRESSION: Suggests a compression method (multiple are allowed). You'll get a COMPRESSION response, and compression will begin with the binary data. //SOURCELIST: Asks for a list of active sources from the proxy. //DEMOLIST: Asks for a list of available mvd demos. //Response: //if using RAW, there will be no header or anything //Otherwise you'll get a QTVSV %f response (%f being the protocol version being used) //same structure, terminated by a \n //AUTH: Server requires auth before proceeding. If you don't support the method the server says, then, urm, the server shouldn't have suggested it. //CHALLENGE: used with auth //COMPRESSION: Method of compression used. Compression begins with the raw data after the connection process. //ASOURCE: names a source //ADEMO: gives a demo file name #include "qtv.h" #include #include "bsd_string.h" #ifndef _WIN32 #include #include #endif #include #ifdef UNIXSOCKETS #include #endif #define RECONNECT_TIME (1000*30) #define RECONNECT_TIME_DEMO (1000*5) #define UDPRECONNECT_TIME (1000) #define STATUSPOLL_TIME 1000*30 #define PINGSINTERVAL_TIME (1000*5) #define UDPTIMEOUT_LENGTH (1000*20) #define UDPPACKETINTERVAL (1000/72) void Net_SendConnectionMVD(sv_t *qtv, oproxy_t *prox); void Net_QueueUpstream(sv_t *qtv, int size, char *buffer); qboolean NET_StringToAddr (char *s, netadr_t *sadr, int defaultport) { struct hostent *h; char *colon; char copy[128]; memset (sadr, 0, sizeof(netadr_t)); #ifdef USEIPX if ((strlen(s) >= 23) && (s[8] == ':') && (s[21] == ':')) // check for an IPX address { unsigned int val; ((struct sockaddr_ipx *)sadr)->sa_family = AF_IPX; copy[2] = 0; DO(0, sa_netnum[0]); DO(2, sa_netnum[1]); DO(4, sa_netnum[2]); DO(6, sa_netnum[3]); DO(9, sa_nodenum[0]); DO(11, sa_nodenum[1]); DO(13, sa_nodenum[2]); DO(15, sa_nodenum[3]); DO(17, sa_nodenum[4]); DO(19, sa_nodenum[5]); sscanf (&s[22], "%u", &val); ((struct sockaddr_ipx *)sadr)->sa_socket = htons((unsigned short)val); } else #endif #ifndef _WIN32 if (1) {//ipv6 method (can return ipv4 addresses too) struct addrinfo *addrinfo, *pos; struct addrinfo udp6hint; int error; char *port; char dupbase[256]; int len; memset(&udp6hint, 0, sizeof(udp6hint)); udp6hint.ai_family = 0;//Any... we check for AF_INET6 or 4 udp6hint.ai_socktype = SOCK_DGRAM; udp6hint.ai_protocol = IPPROTO_UDP; if (*s == '[') { s++; colon = strchr(s, ']'); if (!colon || colon-s >= sizeof(copy)) return false; //too long to handle. memcpy(copy, s, colon-s); copy[colon-s] = 0; colon++; if (*colon == ':') port = colon; else port = NULL; s = copy; } else { port = s + strlen(s); while(port >= s) { if (*port == ':') break; port--; } } if (port == s) port = NULL; if (port) { len = port - s; if (len > sizeof(dupbase)-1) len = sizeof(dupbase)-1; memcpy(dupbase, s, len); dupbase[len] = 0; error = getaddrinfo(dupbase, port+1, &udp6hint, &addrinfo); } else error = EAI_NONAME, addrinfo=NULL; if (error) //failed, try string with no port. error = getaddrinfo(s, NULL, &udp6hint, &addrinfo); //remember, this func will return any address family that could be using the udp protocol... (ip4 or ip6) if (error) { return false; } ((struct sockaddr*)sadr->sockaddr)->sa_family = 0; for (pos = addrinfo; pos; pos = pos->ai_next) { switch(pos->ai_family) { case AF_INET6: if (((struct sockaddr_in *)sadr->sockaddr)->sin_family == AF_INET6) break; //first one should be best... //fallthrough case AF_INET: memcpy(sadr->sockaddr, addrinfo->ai_addr, addrinfo->ai_addrlen); if (pos->ai_family == AF_INET) goto dblbreak; //don't try finding any more, this is quake, they probably prefer ip4... break; } } dblbreak: pfreeaddrinfo (addrinfo); if (!((struct sockaddr*)sadr->sockaddr)->sa_family) //none suitablefound return false; } else #endif { //old fashioned method struct sockaddr_in *sin = (struct sockaddr_in *)sadr->sockaddr; sin->sin_family = AF_INET; sin->sin_port = htons(defaultport); strcpy (copy, s); // strip off a trailing :port if present for (colon = copy ; *colon ; colon++) if (*colon == ':') { *colon = 0; sin->sin_port = htons((short)atoi(colon+1)); } if (copy[0] >= '0' && copy[0] <= '9') //this is the wrong way to test. a server name may start with a number. { *(int *)&sin->sin_addr = inet_addr(copy); } else { if (! (h = gethostbyname(copy)) ) return 0; if (h->h_addrtype != AF_INET) return 0; *(int *)&sin->sin_addr = *(int *)h->h_addr_list[0]; } } return true; } qboolean Net_CompareAddress(netadr_t *s1, netadr_t *s2, int qp1, int qp2) { struct sockaddr *g1=(void*)s1->sockaddr, *g2=(void*)s2->sockaddr; if (g1->sa_family != g2->sa_family) { //urgh... if (g1->sa_family == AF_INET6 && g2->sa_family == AF_INET && ( ((unsigned int*)&((struct sockaddr_in6 *)g1)->sin6_addr)[0] == 0 && ((unsigned int*)&((struct sockaddr_in6 *)g1)->sin6_addr)[1] == 0 && ((unsigned short*)&((struct sockaddr_in6 *)g1)->sin6_addr)[4] == 0 && ((unsigned short*)&((struct sockaddr_in6 *)g1)->sin6_addr)[5] == 0xffff)) { struct sockaddr_in6 *i1=(void*)s1->sockaddr; struct sockaddr_in *i2=(void*)s2->sockaddr; if (((unsigned int*)&i1->sin6_addr)[3] != *(unsigned int*)&i2->sin_addr) return false; if (i1->sin6_port != i2->sin_port && qp1 != qp2) //allow qports to match instead of ports, if required. return false; return true; } if (g1->sa_family == AF_INET && g2->sa_family == AF_INET6 && ( ((unsigned int*)&((struct sockaddr_in6 *)g2)->sin6_addr)[0] == 0 && ((unsigned int*)&((struct sockaddr_in6 *)g2)->sin6_addr)[1] == 0 && ((unsigned short*)&((struct sockaddr_in6 *)g2)->sin6_addr)[4] == 0 && ((unsigned short*)&((struct sockaddr_in6 *)g2)->sin6_addr)[5] == 0xffff)) { struct sockaddr_in6 *i1=(void*)s2->sockaddr; struct sockaddr_in *i2=(void*)s1->sockaddr; if (((unsigned int*)&i1->sin6_addr)[3] != *(unsigned int*)&i2->sin_addr) return false; if (i1->sin6_port != i2->sin_port && qp1 != qp2) //allow qports to match instead of ports, if required. return false; return true; } return false; } switch(g1->sa_family) { default: return true; case AF_INET: { struct sockaddr_in *i1=(void*)s1->sockaddr, *i2=(void*)s2->sockaddr; if (*(unsigned int*)&i1->sin_addr != *(unsigned int*)&i2->sin_addr) return false; if (i1->sin_port != i2->sin_port && qp1 != qp2) //allow qports to match instead of ports, if required. return false; return true; } case AF_INET6: { struct sockaddr_in6 *i1=(void*)s1->sockaddr, *i2=(void*)s2->sockaddr; if (((unsigned int*)&i1->sin6_addr)[0] != ((unsigned int*)&i2->sin6_addr)[0]) return false; if (((unsigned int*)&i1->sin6_addr)[1] != ((unsigned int*)&i2->sin6_addr)[1]) return false; if (((unsigned int*)&i1->sin6_addr)[2] != ((unsigned int*)&i2->sin6_addr)[2]) return false; if (((unsigned int*)&i1->sin6_addr)[3] != ((unsigned int*)&i2->sin6_addr)[3]) return false; if (i1->sin6_port != i2->sin6_port && qp1 != qp2) //allow qports to match instead of ports, if required. return false; return true; } } return false; } void Net_TCPListen(cluster_t *cluster, int port, int socketid) { SOCKET sock; union { struct sockaddr s; struct sockaddr_in ipv4; struct sockaddr_in6 ipv6; #ifdef UNIXSOCKETS struct sockaddr_un un; #endif } address; int prot; int addrsize; int _true = true; // int fromlen; unsigned long nonblocking = true; unsigned long v6only = false; const char *famname; switch(socketid) { #ifdef UNIXSOCKETS case SG_UNIX: prot = AF_UNIX; memset(&address.un, 0, sizeof(address.un)); address.un.sun_family = prot; memcpy(address.un.sun_path, "\0qtv", 4); addrsize = offsetof(struct sockaddr_un, sun_path[4]); famname = "unix"; break; #endif case SG_IPV6: prot = AF_INET6; memset(&address.ipv6, 0, sizeof(address.ipv6)); address.ipv6.sin6_family = prot; address.ipv6.sin6_port = htons((u_short)port); addrsize = sizeof(struct sockaddr_in6); if (v6only) famname = "tcp6"; else famname = "tcp"; break; case SG_IPV4: prot = AF_INET; address.ipv4.sin_family = prot; address.ipv4.sin_addr.s_addr = INADDR_ANY; address.ipv4.sin_port = htons((u_short)port); addrsize = sizeof(struct sockaddr_in); famname = "tcp4"; break; default: return; //some kind of error. avoid unintialised warnings. } if (socketid==SG_IPV4 && !v6only && cluster->tcpsocket[SG_IPV6] != INVALID_SOCKET) { //if we already have a hybrid ipv6 socket, don't bother with ipv4 too int sz = sizeof(v6only); if (getsockopt(cluster->tcpsocket[1], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, &sz) == 0 && !v6only) port = 0; } if (cluster->tcpsocket[socketid] != INVALID_SOCKET) { closesocket(cluster->tcpsocket[socketid]); cluster->tcpsocket[socketid] = INVALID_SOCKET; Sys_Printf(cluster, "closed %s port\n", famname); } if (!port) return; if ((sock = socket (prot, SOCK_STREAM, 0)) == INVALID_SOCKET) { cluster->tcpsocket[socketid] = INVALID_SOCKET; return; } if (ioctlsocket (sock, FIONBIO, &nonblocking) == -1) { cluster->tcpsocket[socketid] = INVALID_SOCKET; closesocket(sock); return; } #ifdef _WIN32 //win32 is so fucked up // setsockopt(newsocket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *)&_true, sizeof(_true)); #endif setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&_true, sizeof(_true)); if (socketid == SG_IPV6) { if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)) == -1) v6only = true; if (v6only) famname = "tcp6"; else famname = "tcp"; } if (bind (sock, &address.s, addrsize) == -1) { printf("socket bind error %i (%s)\n", qerrno, strerror(qerrno)); closesocket(sock); return; } listen(sock, 2); //don't listen for too many clients at once. Sys_Printf(cluster, "opened %s port %i\n", famname, port); cluster->tcpsocket[socketid] = sock; } char *strchrrev(char *str, char chr) { char *firstchar = str; for (str = str + strlen(str)-1; str>=firstchar; str--) if (*str == chr) return str; return NULL; } void Net_SendQTVConnectionRequest(sv_t *qtv, char *authmethod, char *challenge) { char *at; char *str; char hash[512]; //due to mvdsv sucking and stuff, we try using raw connections where possibleso that we don't end up expecting a header. //at some point, this will be forced to 1 qtv->parsingqtvheader = true;//!!*qtv->connectpassword; qtv->buffersize = 0; qtv->forwardpoint = 0; str = "QTV\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "VERSION: 1\n"; Net_QueueUpstream(qtv, strlen(str), str); if (qtv->serverquery) { if (qtv->serverquery == 2) { str = "DEMOLIST\n"; Net_QueueUpstream(qtv, strlen(str), str); } else { str = "SOURCELIST\n"; Net_QueueUpstream(qtv, strlen(str), str); } } else { at = strchrrev(qtv->server, '@'); if (at) { *at = '\0'; str = "SOURCE: "; Net_QueueUpstream(qtv, strlen(str), str); if (strncmp(qtv->server, "tcp:", 4)) { str = qtv->server; Net_QueueUpstream(qtv, strlen(str), str); } else { str = strchr(qtv->server, ':'); if (str) { str++; Net_QueueUpstream(qtv, strlen(str), str); } } str = "\n"; Net_QueueUpstream(qtv, strlen(str), str); *at = '@'; } else { str = "RECEIVE\n"; Net_QueueUpstream(qtv, strlen(str), str); } if (!qtv->parsingqtvheader) { str = "RAW: 1\n"; Net_QueueUpstream(qtv, strlen(str), str); } else { if (authmethod) { if (!strcmp(authmethod, "PLAIN")) { str = "AUTH: PLAIN\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "PASSWORD: \""; Net_QueueUpstream(qtv, strlen(str), str); str = qtv->connectpassword; Net_QueueUpstream(qtv, strlen(str), str); str = "\"\n"; Net_QueueUpstream(qtv, strlen(str), str); } /*else if (challenge && strlen(challenge)>=32 && !strcmp(authmethod, "CCITT")) { unsigned short crcvalue; str = "AUTH: CCITT\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "PASSWORD: \""; Net_QueueUpstream(qtv, strlen(str), str); snprintf(hash, sizeof(hash), "%s%s", challenge, qtv->connectpassword); crcvalue = QCRC_Block(hash, strlen(hash)); sprintf(hash, "0x%X", (unsigned int)QCRC_Value(crcvalue)); str = hash; Net_QueueUpstream(qtv, strlen(str), str); str = "\"\n"; Net_QueueUpstream(qtv, strlen(str), str); }*/ else if (challenge && strlen(challenge)>=8 && !strcmp(authmethod, "MD4")) { unsigned int md4sum[4]; str = "AUTH: MD4\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "PASSWORD: \""; Net_QueueUpstream(qtv, strlen(str), str); snprintf(hash, sizeof(hash), "%s%s", challenge, qtv->connectpassword); Com_BlockFullChecksum (hash, strlen(hash), (unsigned char*)md4sum); sprintf(hash, "%X%X%X%X", md4sum[0], md4sum[1], md4sum[2], md4sum[3]); //FIXME: bad formatting! str = hash; Net_QueueUpstream(qtv, strlen(str), str); str = "\"\n"; Net_QueueUpstream(qtv, strlen(str), str); } else if (challenge && strlen(challenge)>=8 && !strcmp(authmethod, "SHA1")) { unsigned char digest[20]; str = "AUTH: SHA1\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "PASSWORD: \""; Net_QueueUpstream(qtv, strlen(str), str); snprintf(hash, sizeof(hash), "%s%s", challenge, qtv->connectpassword); CalcHash(&hash_sha1, (unsigned char*)digest, sizeof(digest), hash, strlen(hash)); tobase64(hash, sizeof(hash), digest, hash_sha1.digestsize); str = hash; Net_QueueUpstream(qtv, strlen(str), str); str = "\"\n"; Net_QueueUpstream(qtv, strlen(str), str); } else if (!strcmp(authmethod, "NONE")) { str = "AUTH: NONE\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "PASSWORD: \n"; Net_QueueUpstream(qtv, strlen(str), str); } else { qtv->errored = ERR_PERMANENT; qtv->upstreambuffersize = 0; Sys_Printf(qtv->cluster, "Auth method %s was not usable\n", authmethod); return; } } else { str = "AUTH: SHA1\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "AUTH: MD4\n"; Net_QueueUpstream(qtv, strlen(str), str); // str = "AUTH: CCITT\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "AUTH: PLAIN\n"; Net_QueueUpstream(qtv, strlen(str), str); str = "AUTH: NONE\n"; Net_QueueUpstream(qtv, strlen(str), str); } } } str = "\n"; Net_QueueUpstream(qtv, strlen(str), str); } qboolean Net_ConnectToTCPServer(sv_t *qtv, char *ip) { int err; netadr_t from; unsigned long nonblocking = true; int afam; int pfam; int asz; if (!NET_StringToAddr(ip, &qtv->serveraddress, 27500)) { Sys_Printf(qtv->cluster, "Stream %i: Unable to resolve %s\n", qtv->streamid, ip); strcpy(qtv->status, "Unable to resolve server\n"); return false; } afam = ((struct sockaddr*)&qtv->serveraddress.sockaddr)->sa_family; pfam = ((afam==AF_INET6)?PF_INET6:PF_INET); asz = ((afam==AF_INET6)?sizeof(struct sockaddr_in6):sizeof(struct sockaddr_in)); qtv->sourcesock = socket(pfam, SOCK_STREAM, IPPROTO_TCP); if (qtv->sourcesock == INVALID_SOCKET) { strcpy(qtv->status, "Network error\n"); return false; } if (afam == AF_INET6) { qboolean v6only = true; setsockopt(qtv->sourcesock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)); } memset(&from, 0, sizeof(from)); ((struct sockaddr*)&from)->sa_family = afam; if (bind(qtv->sourcesock, (struct sockaddr *)&from, sizeof(from)) == -1) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; strcpy(qtv->status, "Network error\n"); return false; } if (ioctlsocket (qtv->sourcesock, FIONBIO, &nonblocking) == -1) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; strcpy(qtv->status, "Network error\n"); return false; } if (connect(qtv->sourcesock, (struct sockaddr *)&qtv->serveraddress.sockaddr, asz) == INVALID_SOCKET) { err = qerrno; if (err != NET_EINPROGRESS && err != NET_EAGAIN && err != NET_EWOULDBLOCK) //bsd sockets are meant to return EINPROGRESS, but some winsock drivers use EWOULDBLOCK instead. *sigh*... { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; strcpy(qtv->status, "Connection failed\n"); return false; } } //make sure the buffers are empty. we could have disconnected prematurly qtv->upstreambuffersize = 0; qtv->buffersize = 0; qtv->forwardpoint = 0; //read the notes at the start of this file for what these text strings mean Net_SendQTVConnectionRequest(qtv, NULL, NULL); return true; } qboolean Net_ConnectToUDPServer(sv_t *qtv, char *ip) { netadr_t from; unsigned long nonblocking = true; int afam, pfam, asz; if (!NET_StringToAddr(ip, &qtv->serveraddress, 27500)) { Sys_Printf(qtv->cluster, "Stream %i: Unable to resolve %s\n", qtv->streamid, ip); return false; } afam = ((struct sockaddr*)&qtv->serveraddress.sockaddr)->sa_family; pfam = ((afam==AF_INET6)?PF_INET6:PF_INET); asz = ((afam==AF_INET6)?sizeof(struct sockaddr_in6):sizeof(struct sockaddr_in)); qtv->sourcesock = socket(pfam, SOCK_DGRAM, IPPROTO_UDP); if (qtv->sourcesock == INVALID_SOCKET) return false; if (afam == AF_INET6) { qboolean v6only = true; setsockopt(qtv->sourcesock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)); } memset(&from, 0, sizeof(from)); ((struct sockaddr*)&from)->sa_family = afam; if (bind(qtv->sourcesock, (struct sockaddr *)&from, asz) == -1) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; return false; } if (ioctlsocket (qtv->sourcesock, FIONBIO, &nonblocking) == -1) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; return false; } qtv->qport = Sys_Milliseconds()*1000+Sys_Milliseconds(); return true; } qboolean DemoFilenameIsOkay(sv_t* qtv, char *fname, char *dir) { int len; if (dir) { if (strchr(dir, '/')) return false; //unix path seperator if (strchr(dir, '\\')) return false; //windows path seperator if (strchr(dir, ':')) return false; //mac path seperator if (*(dir) == '.') return false; } if (strchr(fname, '/')) return false; //unix path seperator if (strchr(fname, '\\')) return false; //windows path seperator if (strchr(fname, ':')) return false; //mac path seperator if (*(fname) == '.') return false; //now make certain that the last four characters are '.mvd' and not something like '.cfg' perhaps len = strlen(fname); if (len < 5) return false; if (strcmp(fname+len-4, ".mvd")) return false; return true; /* if (strchr(fname, '\\')) { char *s; Con_Printf("Warning: \\ characters in filename %s\n", fname); while((s = strchr(fname, '\\'))) *s = '/'; } if (strstr(fname, "..")) { Con_Printf("Error: '..' characters in filename %s\n", fname); } else if (fname[0] == '/') { Con_Printf("Error: absolute path in filename %s\n", fname); } else if (strstr(fname, ":")) //win32 drive seperator (or mac path seperator, but / works there and they're used to it) { Con_Printf("Error: absolute path in filename %s\n", fname); } else return false; return true; */ } qboolean Net_ConnectToDemoServer(sv_t* qtv, char* ip, char* dir) { char fullname[512]; qtv->sourcesock = INVALID_SOCKET; if (DemoFilenameIsOkay(qtv, ip, dir)) { if (!dir) snprintf(fullname, sizeof(fullname), "%s%s", qtv->cluster->demodir, ip); else snprintf(fullname, sizeof(fullname), "%s%s/%s", qtv->cluster->demodir, dir, ip); qtv->sourcefile = fopen(fullname, "rb"); } else qtv->sourcefile = NULL; if (qtv->sourcefile) { char smallbuffer[17]; fseek(qtv->sourcefile, 0, SEEK_END); qtv->filelength = ftell(qtv->sourcefile); //attempt to detect the end of the file fseek(qtv->sourcefile, 0-(long)sizeof(smallbuffer), SEEK_CUR); fread(smallbuffer, 1, 17, qtv->sourcefile); //0 is the time if (smallbuffer[1] == dem_all || smallbuffer[1] == dem_read) //mvdsv changed it to read... { //2,3,4,5 are the length if (smallbuffer[6] == svc_disconnect) { if (!strcmp(smallbuffer+7, "EndOfDemo")) { qtv->filelength -= 17; } } } fseek(qtv->sourcefile, 0, SEEK_SET); return true; } if (!dir) Sys_Printf(qtv->cluster, "Stream %i: Unable to open file %s\n", qtv->streamid, ip); else Sys_Printf(qtv->cluster, "Stream %i: Unable to open file %s in directory %s\n", qtv->streamid, ip, dir); return false; } qboolean Net_ConnectToDemoDirServer(sv_t* qtv, char *ip) { char fullname[512]; qtv->sourcesock = INVALID_SOCKET; snprintf(fullname, sizeof(fullname), "%s%s", qtv->cluster->demodir, ip); #ifdef _WIN32 // TODO: code for Windows directories goes here // TODO: possible to do this without copy-pasting the entire GNU/Linux code below? // TODO: also, what about MAC OS X? Sys_Printf(qtv->cluster, "Windows support coming soon!\n"); return false; #else { DIR *dir; struct dirent* ent; dir = opendir(fullname); if (dir) { char demoname[512]; int current_demo = 0; int file_count = 0; int random_number = 1; // always this value if the directory contains one file // count the files, important for determining a random demo file while ((ent = readdir(dir)) != NULL) { int len; // only count files neding in .mvd len = strlen(ent->d_name); if (len < 5) { continue; } if (strcmp(ent->d_name+len-4, ".mvd")) { continue; } if (ent->d_type == DT_REG && *(ent->d_name) != '.') file_count++; // only add non-hidden and regular files } if (file_count == 0) { // empty directory Sys_Printf(qtv->cluster, "Stream %i: Error: Directory has no demos.\n", qtv->streamid); closedir(dir); return false; } closedir(dir); dir = opendir(fullname); // FIXME: not sure if srand should only be called once somewhere? // FIXME: this is not really shuffling the demos, but does introduce some variety if (file_count > 1) { //srand(time(NULL)); while ((random_number = rand()%file_count + 1) == qtv->last_random_number); qtv->last_random_number = random_number; } while (1) { int len; ent = readdir(dir); if (!ent) { // reached the end of the directory, shouldn't happen Sys_Printf(qtv->cluster, "Stream %i: Error: Reached end of directory (%s%s)\n", qtv->streamid, qtv->cluster->demodir, ip); closedir(dir); return false; } if (ent->d_type != DT_REG || *(ent->d_name) == '.') { continue; // ignore hidden and non-regular files } //now make certain that the last four characters are '.mvd' and not something like '.cfg' perhaps len = strlen(ent->d_name); if (len < 5) { continue; } if (strcmp((ent->d_name)+len-4, ".mvd")) { continue; } if (++current_demo != random_number) continue; snprintf(demoname, sizeof(demoname), "%s/%s", ip, ent->d_name); qtv->sourcefile = fopen(demoname, "rb"); closedir(dir); if (Net_ConnectToDemoServer(qtv, ent->d_name, ip) == true) { return true; } else { return false; } } closedir(dir); } else { Sys_Printf(qtv->cluster, "Stream %i: Unable to open directory %s\n", qtv->streamid, qtv->cluster->demodir); return false; } } #endif return false; } /*figures out the ip to connect to, and decides the protocol for it*/ char *Net_DiagnoseProtocol(sv_t *qtv) { char *at; sourcetype_t type = SRC_BAD; char *ip = qtv->server; if (!strncmp(ip, "udp:", 4)) { type = SRC_UDP; ip += 4; } else if (!strncmp(ip, "tcp:", 4)) { type = SRC_TCP; ip += 4; } else if (!strncmp(ip, "demo:", 5)) { type = SRC_DEMO; ip += 5; } else if (!strncmp(ip, "file:", 5)) { type = SRC_DEMO; ip += 5; } else if (!strncmp(ip, "dir:", 4)) { type = SRC_DEMODIR; ip += 4; } at = strchrrev(ip, '@'); if (at && (type == SRC_DEMO || type == SRC_DEMODIR || type == SRC_TCP)) { if (type == SRC_DEMO || type == SRC_DEMODIR) type = SRC_TCP; ip = at+1; } qtv->sourcetype = type; return ip; } qboolean Net_ConnectToServer(sv_t *qtv) { char *ip = Net_DiagnoseProtocol(qtv); qtv->usequakeworldprotocols = false; qtv->pext1 = 0; qtv->pext2 = 0; if (qtv->sourcetype == SRC_DEMO || qtv->sourcetype == SRC_DEMODIR) { qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME_DEMO; //wait half a minuite before trying to reconnect } else qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME; //wait half a minuite before trying to reconnect switch( qtv->sourcetype) { case SRC_DEMO: return Net_ConnectToDemoServer(qtv, ip, NULL); case SRC_DEMODIR: return Net_ConnectToDemoDirServer(qtv, ip); case SRC_UDP: qtv->usequakeworldprotocols = true; return Net_ConnectToUDPServer(qtv, ip); case SRC_TCP: return Net_ConnectToTCPServer(qtv, ip); default: Sys_Printf(qtv->cluster, "Unknown source type %s\n", ip); return false; } } void Net_QueueUpstream(sv_t *qtv, int size, char *buffer) { if (qtv->usequakeworldprotocols) return; if (qtv->upstreambuffersize + size > sizeof(qtv->upstreambuffer)) { Sys_Printf(qtv->cluster, "Stream %i: Upstream queue overflowed for %s\n", qtv->streamid, qtv->server); strcpy(qtv->status, "Upstream overflow"); qtv->errored = ERR_RECONNECT; return; } memcpy(qtv->upstreambuffer + qtv->upstreambuffersize, buffer, size); qtv->upstreambuffersize += size; } qboolean Net_WriteUpstream(sv_t *qtv) { int len; if (qtv->upstreambuffersize && qtv->sourcesock != INVALID_SOCKET) { len = send(qtv->sourcesock, qtv->upstreambuffer, qtv->upstreambuffersize, 0); if (len == 0) return false; if (len < 0) { int err = qerrno; if (err != NET_EWOULDBLOCK && err != NET_EAGAIN && err != NET_ENOTCONN) { if (err) { Sys_Printf(qtv->cluster, "Stream %i: Error: source socket error %i (%s)\n", qtv->streamid, err, strerror(err)); strcpy(qtv->status, "Network error\n"); } else { Sys_Printf(qtv->cluster, "Stream %i: Error: server %s disconnected\n", qtv->streamid, qtv->server); strcpy(qtv->status, "Server disconnected"); } qtv->errored = ERR_RECONNECT; //if the server is down, we'll detect it on reconnect } return false; } qtv->upstreambuffersize -= len; memmove(qtv->upstreambuffer, qtv->upstreambuffer + len, qtv->upstreambuffersize); } return true; } void SV_SendUpstream(sv_t *qtv, netmsg_t *nm) { char size[2]; int ffs = nm->cursize+2; //qq fucked up and made his upstream inconsistent with downstream. ffs. size[0] = (ffs&0x00ff)>>0; size[1] = (ffs&0xff00)>>8; Net_QueueUpstream(qtv, 2, size); Net_QueueUpstream(qtv, nm->cursize, nm->data); Net_WriteUpstream(qtv); //try and flush it } int SV_SayToUpstream(sv_t *qtv, char *message) { char buffer[1024]; netmsg_t nm; if (!qtv->upstreamacceptschat) { #ifndef _MSC_VER #warning This is incomplete! #endif //Sys_Printf(qtv->cluster, "not forwarding say\n"); return 0; } InitNetMsg(&nm, buffer, sizeof(buffer)); WriteByte(&nm, qtv_clc_stringcmd); WriteString2(&nm, "say "); WriteString(&nm, message); SV_SendUpstream(qtv, &nm); return 1; } void SV_SayToViewers(sv_t *qtv, char *message) { Fwd_SayToDownstream(qtv, message); #ifndef _MSC_VER #warning Send to viewers here too #endif } //This function 1: parses the 'don't delay' packets in the stream // 2: returns the length of continuous data (that is, whole-packet bytes that have not been truncated by the networking layer) // this means we know that the client proxies have valid data, at least from our side. int SV_EarlyParse(sv_t *qtv, unsigned char *buffer, int remaining) { int lengthofs; int length; int available = 0; while(1) { if (remaining < 2) return available; //buffer[0] is time switch (buffer[1]&dem_mask) { case dem_set: lengthofs = 0; //to silence gcc, nothing more break; case dem_multiple: lengthofs = 6; break; default: lengthofs = 2; break; } if (lengthofs > 0) { if (lengthofs+4 > remaining) return available; length = (buffer[lengthofs]<<0) + (buffer[lengthofs+1]<<8) + (buffer[lengthofs+2]<<16) + (buffer[lengthofs+3]<<24); length += lengthofs+4; if (length > MAX_MSGLEN) printf("Probably corrupt mvd (length %i)\n", length); } else length = 10; if (remaining < length) return available; if ((buffer[1]&dem_mask) == dem_all && (buffer[1] & ~dem_mask) && qtv->sourcetype != SRC_DEMO) //dem_qtvdata { ParseMessage(qtv, buffer+lengthofs+4, length - (lengthofs+4), buffer[1], 0xffffffff); } remaining -= length; available += length; buffer += length; } } qboolean Net_ReadStream(sv_t *qtv) { int maxreadable; int read; void *buffer; int err; maxreadable = MAX_PROXY_BUFFER - qtv->buffersize; if (!maxreadable) return true; //this is bad! buffer = qtv->buffer + qtv->buffersize; if (qtv->sourcefile) { if (maxreadable > PREFERRED_PROXY_BUFFER-qtv->buffersize) maxreadable = PREFERRED_PROXY_BUFFER-qtv->buffersize; if (maxreadable<=0) return true; //reuse read a little... read = ftell(qtv->sourcefile); if (read+maxreadable > qtv->filelength) maxreadable = qtv->filelength-read; //clamp to the end of the file //even if that 'end' is before the svc_disconnect read = fread(buffer, 1, maxreadable, qtv->sourcefile); } else { unsigned int errsize; errsize = sizeof(err); err = 0; getsockopt(qtv->sourcesock, SOL_SOCKET, SO_ERROR, (char*)&err, &errsize); if (err == NET_ECONNREFUSED) { Sys_Printf(qtv->cluster, "Stream %i: Error: server %s refused connection\n", qtv->streamid, qtv->server); closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; qtv->upstreambuffersize = 0; //probably contains initial connection request info return false; } read = recv(qtv->sourcesock, buffer, maxreadable, 0); } if (read > 0) { qtv->buffersize += read; if (!qtv->cluster->lateforward && !qtv->parsingqtvheader) //qtv header being the auth part of the connection rather than the stream { int forwardable; //this has the effect of not only parsing early packets, but also saying how much complete data there is. forwardable = SV_EarlyParse(qtv, qtv->buffer+qtv->forwardpoint, qtv->buffersize - qtv->forwardpoint); if (forwardable > 0) { SV_ForwardStream(qtv, qtv->buffer+qtv->forwardpoint, forwardable); qtv->forwardpoint += forwardable; } } } else { if (read == 0) err = 0; else err = qerrno; if (read == 0 || (err != NET_EWOULDBLOCK && err != NET_EAGAIN && err != NET_ENOTCONN)) //ENOTCONN can be returned whilst waiting for a connect to finish. { if (qtv->sourcefile) Sys_Printf(qtv->cluster, "Stream %i: Error: End of file\n", qtv->streamid); else if (read) Sys_Printf(qtv->cluster, "Stream %i: Error: source socket error %i (%s)\n", qtv->streamid, qerrno, strerror(qerrno)); else Sys_Printf(qtv->cluster, "Stream %i: Error: server %s disconnected\n", qtv->streamid, qtv->server); if (qtv->sourcesock != INVALID_SOCKET) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; } if (qtv->sourcefile) { fclose(qtv->sourcefile); qtv->sourcefile = NULL; } return false; } } return true; } unsigned int Sys_Milliseconds(void) { #ifdef _WIN32 #ifdef _MSC_VER #pragma comment(lib, "winmm.lib") #endif #if 0 static firsttime = 1; static starttime; if (firsttime) { starttime = timeGetTime() + 1000*20; firsttime = 0; } return timeGetTime() - starttime; #endif return timeGetTime(); #else //assume every other system follows standards. unsigned int t; struct timeval tv; gettimeofday(&tv, NULL); t = ((unsigned int)tv.tv_sec)*1000 + (((unsigned int)tv.tv_usec)/1000); return t; #endif } /* void NetSleep(sv_t *tv) { int m; struct timeval timeout; fd_set socketset; FD_ZERO(&socketset); m = 0; if (tv->sourcesock != INVALID_SOCKET) { FD_SET(tv->sourcesock, &socketset); if (tv->sourcesock >= m) m = tv->sourcesock+1; } if (tv->qwdsocket != INVALID_SOCKET) { FD_SET(tv->qwdsocket, &socketset); if (tv->sourcesock >= m) m = tv->sourcesock+1; } #ifndef _WIN32 #ifndef STDIN #define STDIN 0 #endif FD_SET(STDIN, &socketset); if (STDIN >= m) m = STDIN+1; #endif timeout.tv_sec = 100/1000; timeout.tv_usec = (100%1000)*1000; select(m, &socketset, NULL, NULL, &timeout); #ifdef _WIN32 for (;;) { char buffer[8192]; char *result; char c; if (!kbhit()) break; c = getch(); if (c == '\n' || c == '\r') { printf("\n"); if (tv->inputlength) { tv->commandinput[tv->inputlength] = '\0'; result = Rcon_Command(tv->cluster, tv, tv->commandinput, buffer, sizeof(buffer), true); printf("%s", result); tv->inputlength = 0; tv->commandinput[0] = '\0'; } } else if (c == '\b') { if (tv->inputlength > 0) { tv->inputlength--; tv->commandinput[tv->inputlength] = '\0'; } } else { if (tv->inputlength < sizeof(tv->commandinput)-1) { tv->commandinput[tv->inputlength++] = c; tv->commandinput[tv->inputlength] = '\0'; } }if (FD_ISSET(STDIN, &socketset)) printf("\r%s \b", tv->commandinput); } #else if (FD_ISSET(STDIN, &socketset)) { char buffer[8192]; char *result; tv->inputlength = read (0, tv->commandinput, sizeof(tv->commandinput)); if (tv->inputlength >= 1) { tv->commandinput[tv->inputlength-1] = 0; // rip off the /n and terminate if (tv->inputlength) { tv->commandinput[tv->inputlength] = '\0'; result = Rcon_Command(tv, tv->commandinput, buffer, sizeof(buffer), true); printf("%s", result); tv->inputlength = 0; tv->commandinput[0] = '\0'; } } } #endif } */ void Trim(char *s) { char *f; f = s; while(*f <= ' ' && *f > '\0') f++; while(*f > ' ') *s++ = *f++; *s = '\0'; } qboolean QTV_ConnectStream(sv_t *qtv, char *serverurl) { if (qtv->sourcesock != INVALID_SOCKET) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; } if (qtv->sourcefile) { fclose(qtv->sourcefile); qtv->sourcefile = NULL; } memcpy(qtv->server, serverurl, sizeof(qtv->server)-1); if (qtv->autodisconnect == AD_STATUSPOLL && !qtv->numviewers && !qtv->proxies) { char *ip; ip = Net_DiagnoseProtocol(qtv); if (!NET_StringToAddr(ip, &qtv->serveraddress, 27500)) { Sys_Printf(qtv->cluster, "Stream %i: Unable to resolve %s\n", qtv->streamid, ip); return false; } return true; } *qtv->map.serverinfo = '\0'; Info_SetValueForStarKey(qtv->map.serverinfo, "*version", "FTEQTV", sizeof(qtv->map.serverinfo)); Info_SetValueForStarKey(qtv->map.serverinfo, "*qtv", QTV_VERSION_STRING, sizeof(qtv->map.serverinfo)); Info_SetValueForStarKey(qtv->map.serverinfo, "hostname", qtv->cluster->hostname, sizeof(qtv->map.serverinfo)); Info_SetValueForStarKey(qtv->map.serverinfo, "maxclients", "99", sizeof(qtv->map.serverinfo)); if (!strncmp(qtv->server, "file:", 5)) Info_SetValueForStarKey(qtv->map.serverinfo, "server", "file", sizeof(qtv->map.serverinfo)); else Info_SetValueForStarKey(qtv->map.serverinfo, "server", qtv->server, sizeof(qtv->map.serverinfo)); if (qtv->autodisconnect == AD_REVERSECONNECT) { //added because of paranoia rather than need. Should never occur. printf("bug: autoclose==2\n"); strcpy(qtv->status, "Network error\n"); return false; } else if (!Net_ConnectToServer(qtv)) { Sys_Printf(qtv->cluster, "Stream %i: Couldn't connect (%s)\n", qtv->streamid, qtv->server); return false; } if (qtv->sourcesock == INVALID_SOCKET) { qtv->parsetime = Sys_Milliseconds(); // Sys_Printf(qtv->cluster, "Stream %i: Playing from file\n", qtv->streamid); } else { qtv->parsetime = Sys_Milliseconds() + qtv->cluster->anticheattime; } return true; } void QTV_CleanupMap(sv_t *qtv) { int i; //free the bsp BSP_Free(qtv->map.bsp); qtv->map.bsp = NULL; //clean up entity state for (i = 0; i < ENTITY_FRAMES; i++) { if (qtv->map.frame[i].ents) { free(qtv->map.frame[i].ents); qtv->map.frame[i].ents = NULL; } if (qtv->map.frame[i].entnums) { free(qtv->map.frame[i].entnums); qtv->map.frame[i].entnums = NULL; } } memset(&qtv->map, 0, sizeof(qtv->map)); } void QTV_DisconnectFromSource(sv_t *qtv) { // close the source handle if (qtv->sourcesock != INVALID_SOCKET) { if (qtv->usequakeworldprotocols) { char dying[] = {clc_stringcmd, 'd', 'r', 'o', 'p', '\0'}; Netchan_Transmit (qtv->cluster, &qtv->netchan, sizeof(dying), dying); Netchan_Transmit (qtv->cluster, &qtv->netchan, sizeof(dying), dying); Netchan_Transmit (qtv->cluster, &qtv->netchan, sizeof(dying), dying); } closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; } if (qtv->sourcefile) { fclose(qtv->sourcefile); qtv->sourcefile = NULL; } //cancel downloads if (qtv->downloadfile) { fclose(qtv->downloadfile); qtv->downloadfile = NULL; unlink(qtv->downloadname); *qtv->downloadname = '\0'; } } void QTV_Cleanup(sv_t *qtv, qboolean leaveadmins) { //disconnects the stream viewer_t *v; cluster_t *cluster; oproxy_t *prox; oproxy_t *old; cluster = qtv->cluster; //set connected viewers to a different stream if (cluster->viewserver == qtv) cluster->viewserver = NULL; for (v = cluster->viewers; v; v = v->next) { //warning fixme: honour leaveadmins if (v->server == qtv) { //they were watching this one QW_SetViewersServer(qtv->cluster, v, NULL); QW_SetMenu(v, MENU_NONE); QTV_SayCommand(cluster, v->server, v, "menu"); QW_PrintfToViewer(v, "Stream %s is closing\n", qtv->server); } } QTV_DisconnectFromSource(qtv); QTV_CleanupMap(qtv); //boot connected downstream proxies for (prox = qtv->proxies; prox; ) { if (prox->file) fclose(prox->file); if (prox->sock != INVALID_SOCKET) closesocket(prox->sock); old = prox; prox = prox->next; free(old); cluster->numproxies--; } } void QTV_ShutdownStream(sv_t *qtv) { sv_t *peer; cluster_t *cluster; Sys_Printf(qtv->cluster, "Stream %i: Closing source %s\n", qtv->streamid, qtv->server); QTV_Cleanup(qtv, false); //unlink it cluster = qtv->cluster; if (cluster->servers == qtv) cluster->servers = qtv->next; else { for (peer = cluster->servers; peer->next; peer = peer->next) { if (peer->next == qtv) { peer->next = qtv->next; break; } } } free(qtv); cluster->numservers--; } void SendClientCommand(sv_t *qtv, char *fmt, ...) { va_list argptr; char buf[1024]; va_start (argptr, fmt); vsnprintf (buf, sizeof(buf), fmt, argptr); va_end (argptr); WriteByte(&qtv->netchan.message, clc_stringcmd); WriteString(&qtv->netchan.message, buf); } void ChooseFavoriteTrack(sv_t *tv) { int frags, best, pnum; char buffer[64]; frags = -10000; best = -1; if (tv->controller || tv->proxyplayer) best = tv->map.trackplayer; else { for (pnum = 0; pnum < MAX_CLIENTS; pnum++) { if (*tv->map.players[pnum].userinfo && !atoi(Info_ValueForKey(tv->map.players[pnum].userinfo, "*spectator", buffer, sizeof(buffer)))) { if (tv->map.thisplayer == pnum) continue; if (frags < tv->map.players[pnum].frags) { best = pnum; frags = tv->map.players[pnum].frags; } } } } if (best != tv->map.trackplayer) { SendClientCommand (tv, "ptrack %i\n", best); tv->map.trackplayer = best; if (tv->usequakeworldprotocols) QW_StreamStuffcmd(tv->cluster, tv, "track %i\n", best); } } static const unsigned char chktbl[1024] = { 0x78,0xd2,0x94,0xe3,0x41,0xec,0xd6,0xd5,0xcb,0xfc,0xdb,0x8a,0x4b,0xcc,0x85,0x01, 0x23,0xd2,0xe5,0xf2,0x29,0xa7,0x45,0x94,0x4a,0x62,0xe3,0xa5,0x6f,0x3f,0xe1,0x7a, 0x64,0xed,0x5c,0x99,0x29,0x87,0xa8,0x78,0x59,0x0d,0xaa,0x0f,0x25,0x0a,0x5c,0x58, 0xfb,0x00,0xa7,0xa8,0x8a,0x1d,0x86,0x80,0xc5,0x1f,0xd2,0x28,0x69,0x71,0x58,0xc3, 0x51,0x90,0xe1,0xf8,0x6a,0xf3,0x8f,0xb0,0x68,0xdf,0x95,0x40,0x5c,0xe4,0x24,0x6b, 0x29,0x19,0x71,0x3f,0x42,0x63,0x6c,0x48,0xe7,0xad,0xa8,0x4b,0x91,0x8f,0x42,0x36, 0x34,0xe7,0x32,0x55,0x59,0x2d,0x36,0x38,0x38,0x59,0x9b,0x08,0x16,0x4d,0x8d,0xf8, 0x0a,0xa4,0x52,0x01,0xbb,0x52,0xa9,0xfd,0x40,0x18,0x97,0x37,0xff,0xc9,0x82,0x27, 0xb2,0x64,0x60,0xce,0x00,0xd9,0x04,0xf0,0x9e,0x99,0xbd,0xce,0x8f,0x90,0x4a,0xdd, 0xe1,0xec,0x19,0x14,0xb1,0xfb,0xca,0x1e,0x98,0x0f,0xd4,0xcb,0x80,0xd6,0x05,0x63, 0xfd,0xa0,0x74,0xa6,0x86,0xf6,0x19,0x98,0x76,0x27,0x68,0xf7,0xe9,0x09,0x9a,0xf2, 0x2e,0x42,0xe1,0xbe,0x64,0x48,0x2a,0x74,0x30,0xbb,0x07,0xcc,0x1f,0xd4,0x91,0x9d, 0xac,0x55,0x53,0x25,0xb9,0x64,0xf7,0x58,0x4c,0x34,0x16,0xbc,0xf6,0x12,0x2b,0x65, 0x68,0x25,0x2e,0x29,0x1f,0xbb,0xb9,0xee,0x6d,0x0c,0x8e,0xbb,0xd2,0x5f,0x1d,0x8f, 0xc1,0x39,0xf9,0x8d,0xc0,0x39,0x75,0xcf,0x25,0x17,0xbe,0x96,0xaf,0x98,0x9f,0x5f, 0x65,0x15,0xc4,0x62,0xf8,0x55,0xfc,0xab,0x54,0xcf,0xdc,0x14,0x06,0xc8,0xfc,0x42, 0xd3,0xf0,0xad,0x10,0x08,0xcd,0xd4,0x11,0xbb,0xca,0x67,0xc6,0x48,0x5f,0x9d,0x59, 0xe3,0xe8,0x53,0x67,0x27,0x2d,0x34,0x9e,0x9e,0x24,0x29,0xdb,0x69,0x99,0x86,0xf9, 0x20,0xb5,0xbb,0x5b,0xb0,0xf9,0xc3,0x67,0xad,0x1c,0x9c,0xf7,0xcc,0xef,0xce,0x69, 0xe0,0x26,0x8f,0x79,0xbd,0xca,0x10,0x17,0xda,0xa9,0x88,0x57,0x9b,0x15,0x24,0xba, 0x84,0xd0,0xeb,0x4d,0x14,0xf5,0xfc,0xe6,0x51,0x6c,0x6f,0x64,0x6b,0x73,0xec,0x85, 0xf1,0x6f,0xe1,0x67,0x25,0x10,0x77,0x32,0x9e,0x85,0x6e,0x69,0xb1,0x83,0x00,0xe4, 0x13,0xa4,0x45,0x34,0x3b,0x40,0xff,0x41,0x82,0x89,0x79,0x57,0xfd,0xd2,0x8e,0xe8, 0xfc,0x1d,0x19,0x21,0x12,0x00,0xd7,0x66,0xe5,0xc7,0x10,0x1d,0xcb,0x75,0xe8,0xfa, 0xb6,0xee,0x7b,0x2f,0x1a,0x25,0x24,0xb9,0x9f,0x1d,0x78,0xfb,0x84,0xd0,0x17,0x05, 0x71,0xb3,0xc8,0x18,0xff,0x62,0xee,0xed,0x53,0xab,0x78,0xd3,0x65,0x2d,0xbb,0xc7, 0xc1,0xe7,0x70,0xa2,0x43,0x2c,0x7c,0xc7,0x16,0x04,0xd2,0x45,0xd5,0x6b,0x6c,0x7a, 0x5e,0xa1,0x50,0x2e,0x31,0x5b,0xcc,0xe8,0x65,0x8b,0x16,0x85,0xbf,0x82,0x83,0xfb, 0xde,0x9f,0x36,0x48,0x32,0x79,0xd6,0x9b,0xfb,0x52,0x45,0xbf,0x43,0xf7,0x0b,0x0b, 0x19,0x19,0x31,0xc3,0x85,0xec,0x1d,0x8c,0x20,0xf0,0x3a,0xfa,0x80,0x4d,0x2c,0x7d, 0xac,0x60,0x09,0xc0,0x40,0xee,0xb9,0xeb,0x13,0x5b,0xe8,0x2b,0xb1,0x20,0xf0,0xce, 0x4c,0xbd,0xc6,0x04,0x86,0x70,0xc6,0x33,0xc3,0x15,0x0f,0x65,0x19,0xfd,0xc2,0xd3 }; unsigned char COM_BlockSequenceCRCByte (void *base, int length, int sequence) { unsigned short crc; const unsigned char *p; unsigned char chkb[60 + 4]; p = chktbl + (sequence % (sizeof(chktbl) - 4)); if (length > 60) length = 60; memcpy (chkb, base, length); chkb[length] = (sequence & 0xff) ^ p[0]; chkb[length+1] = p[1]; chkb[length+2] = ((sequence>>8) & 0xff) ^ p[2]; chkb[length+3] = p[3]; length += 4; crc = QCRC_Block(chkb, length); crc &= 0xff; return crc; } void SetMoveCRC(sv_t *qtv, netmsg_t *msg) { char *outbyte; outbyte = (char*)msg->data + msg->startpos+1; *outbyte = COM_BlockSequenceCRCByte( outbyte+1, msg->cursize - (msg->startpos+2), qtv->netchan.outgoing_sequence); } void QTV_ParseQWStream(sv_t *qtv) { char buffer[1500]; netadr_t from; unsigned int fromlen; int readlen; netmsg_t msg; fromlen = sizeof(from.sockaddr); //bug: this won't work on (free)bsd for (;;) { from.tcpcon = NULL; readlen = recvfrom(qtv->sourcesock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&from.sockaddr, &fromlen); if (readlen < 0) { //FIXME: Check for error break; } if (readlen > sizeof(buffer)-1) break; //oversized! buffer[readlen] = 0; if (*(int*)buffer == -1) { if (buffer[4] == 'c') { //got a challenge strcpy(qtv->status, "Attemping connection\n"); qtv->challenge = atoi(buffer+5); if (qtv->controller) sprintf(buffer, "connect %i %i %i \"%s\\*qtv\\1\\Qizmo\\2.9 notimer\"", 28, qtv->qport, qtv->challenge, qtv->controller->userinfo); else if (qtv->proxyplayer) sprintf(buffer, "connect %i %i %i \"%s\\name\\%s\"", 28, qtv->qport, qtv->challenge, "\\*ver\\fteqtv\\spectator\\0\\rate\\10000", qtv->cluster->hostname); else sprintf(buffer, "connect %i %i %i \"%s\\name\\%s\"", 28, qtv->qport, qtv->challenge, "\\*ver\\fteqtv\\spectator\\1\\rate\\10000", qtv->cluster->hostname); Netchan_OutOfBandSocket(qtv->cluster, qtv->sourcesock, &qtv->serveraddress, strlen(buffer), buffer); continue; } if (buffer[4] == 'n') { strlcpy(qtv->status, buffer+5, sizeof(qtv->status)); Sys_Printf(qtv->cluster, "%s: %s", qtv->server, buffer+5); continue; } if (buffer[4] == 'j') { strcpy(qtv->status, "Waiting for gamestate\n"); Netchan_Setup(qtv->sourcesock, &qtv->netchan, qtv->serveraddress, qtv->qport, true); qtv->map.trackplayer = -1; qtv->isconnected = true; qtv->timeout = qtv->curtime + UDPTIMEOUT_LENGTH; SendClientCommand(qtv, "new"); Sys_Printf(qtv->cluster, "Stream %i: Connected!\n", qtv->streamid); continue; } Sys_Printf(qtv->cluster, "Stream %i: %s: unrecognized connectionless packet:\n%s\n", qtv->streamid, qtv->server, buffer+4); continue; } memset(&msg, 0, sizeof(msg)); msg.cursize = readlen; msg.data = buffer; msg.maxsize = readlen; qtv->timeout = qtv->curtime + UDPTIMEOUT_LENGTH; if (!Netchan_Process(&qtv->netchan, &msg)) continue; ParseMessage(qtv, (char*)msg.data + msg.readpos, msg.cursize - msg.readpos, dem_all, -1); qtv->oldpackettime = qtv->nextpackettime; qtv->nextpackettime = qtv->parsetime; qtv->parsetime = qtv->curtime; if (qtv->simtime < qtv->oldpackettime) qtv->simtime = qtv->oldpackettime; //too old if (qtv->controller) { qtv->controller->maysend = true; //if (qtv->controller->netchan.outgoing_sequence != qtv->controller->netchan.incoming_sequence) //printf("bug is here\n"); } } } #ifdef COMMENTARY #include #endif void QTV_CollectCommentry(sv_t *qtv) { #define usespeex 0 #ifdef COMMENTARY int samps; unsigned char buffer[8192+6]; unsigned char *uchar; signed char *schar; int bytesleft; if (!qtv->comentrycapture) { if (0) { // if (usespeex) // qtv->comentrycapture = SND_InitCapture(11025, 16); // else qtv->comentrycapture = SND_InitCapture(11025, 8); } return; } while(1) { //the protocol WILL be different. Don't add compatibility for this code. buffer[0] = 0; buffer[1] = dem_audio; buffer[2] = 255; buffer[3] = 255; buffer[4] = 8; buffer[5] = 11*5; /* if (usespeex) { SpeexBits bits; void *enc_state; int frame_size; spx_int16_t pcmdata[8192/2]; samps=qtv->comentrycapture->update(qtv->comentrycapture, 2048, (char*)pcmdata); speex_bits_init(&bits); enc_state = speex_encoder_init(&speex_nb_mode); speex_encoder_ctl(enc_state,SPEEX_GET_FRAME_SIZE,&frame_size); speex_bits_reset(&bits); speex_encode_int(enc_state, (spx_int16_t*)pcmdata, &bits); samps = speex_bits_write(&bits, buffer+6, sizeof(buffer)-6); speex_bits_destroy(&bits); speex_encoder_destroy(enc_state); } else*/ { samps=qtv->comentrycapture->update(qtv->comentrycapture, 2048, buffer+6); bytesleft = samps; schar = buffer+6; uchar = buffer+6; while(bytesleft-->0) { *schar++ = *uchar++ - 128; } } buffer[2] = samps&255; buffer[3] = samps>>8; if (samps) SV_ForwardStream(qtv, buffer, 6 + samps); if (samps < 64) break; } #endif } void QTV_Run(sv_t *qtv) { int from; int to; int lengthofs; unsigned int length; unsigned char *buffer; int oldcurtime; int packettime; if (qtv->numviewers == 0 && qtv->proxies == NULL) { if (qtv->autodisconnect == AD_WHENEMPTY) { Sys_Printf(qtv->cluster, "Stream %i: %s became inactive\n", qtv->streamid, qtv->server); qtv->errored = ERR_DROP; } else if (qtv->autodisconnect == AD_STATUSPOLL && qtv->isconnected) { /*switch to status polling instead of packet spamming*/ qtv->errored = ERR_RECONNECT; } } if (qtv->errored) { if (qtv->errored == ERR_DISABLED) { //this keeps any connected proxies ticking over. //probably we should drop them instead - the connection will only be revived if one of them reconnects. SV_ForwardStream(qtv, NULL, 0); return; } else if (qtv->errored == ERR_PERMANENT) { QTV_Cleanup(qtv, false); //frees various pieces of context qtv->errored = ERR_DISABLED; return; } else if (qtv->errored == ERR_DROP) { QTV_ShutdownStream(qtv); //destroys the stream return; } } //we will read out as many packets as we can until we're up to date //note: this can cause real issues when we're overloaded for any length of time //each new packet comes with a leading msec byte (msecs from last packet) //then a type, an optional destination mask, and a 4byte size. //the 4 byte size is probably excessive, a short would do. //some of the types have thier destination mask encoded inside the type byte, yielding 8 types, and 32 max players. //if we've no got enough data to read a new packet, we print a message and wait an extra two seconds. this will add a pause, connected clients will get the same pause, and we'll just try to buffer more of the game before playing. //we'll stay 2 secs behind when the tcp stream catches up, however. This could be bad especially with long up-time. //All timings are in msecs, which is in keeping with the mvd times, but means we might have issues after 72 or so days. //the following if statement will reset the parse timer. It might cause the game to play too soon, the buffersize checks in the rest of the function will hopefully put it back to something sensible. oldcurtime = qtv->curtime; qtv->curtime = Sys_Milliseconds(); if (oldcurtime > qtv->curtime) { Sys_Printf(qtv->cluster, "Time wrapped\n"); qtv->parsetime = qtv->curtime; } if (qtv->errored == ERR_PAUSED) { if (!qtv->parsingconnectiondata) qtv->parsetime = qtv->curtime; } if (qtv->errored == ERR_RECONNECT) { qtv->buffersize = 0; qtv->forwardpoint = 0; QTV_DisconnectFromSource(qtv); qtv->isconnected = 0; qtv->errored = ERR_NONE; qtv->nextconnectattempt = qtv->curtime; //make the reconnect happen _now_ } if (qtv->sourcetype == SRC_UDP) { qtv->simtime += qtv->curtime - oldcurtime; if (qtv->simtime > qtv->nextpackettime) qtv->simtime = qtv->nextpackettime; //too old if (!qtv->isconnected && (qtv->curtime >= qtv->nextconnectattempt || qtv->curtime < qtv->nextconnectattempt - (UDPRECONNECT_TIME+STATUSPOLL_TIME))) { if (qtv->errored == ERR_DISABLED) { strcpy(qtv->status, "Given up connecting\n"); } else if (qtv->autodisconnect == AD_STATUSPOLL) { QTV_DisconnectFromSource(qtv); Netchan_OutOfBand(qtv->cluster, qtv->serveraddress, 13, "status\n"); qtv->nextconnectattempt = qtv->curtime + STATUSPOLL_TIME; return; } else { strcpy(qtv->status, "Attemping challenge\n"); if (qtv->sourcesock == INVALID_SOCKET && !qtv->sourcefile) { if (!QTV_ConnectStream(qtv, qtv->server)) //reconnect it { qtv->errored = ERR_PERMANENT; } } if (qtv->errored == ERR_NONE) Netchan_OutOfBandSocket(qtv->cluster, qtv->sourcesock, &qtv->serveraddress, 13, "getchallenge\n"); } qtv->nextconnectattempt = qtv->curtime + UDPRECONNECT_TIME; } if (qtv->sourcesock == INVALID_SOCKET && !qtv->sourcefile) return; QTV_ParseQWStream(qtv); if (qtv->isconnected) { char buffer[128]; netmsg_t msg; memset(&msg, 0, sizeof(msg)); msg.data = buffer; msg.maxsize = sizeof(buffer); if (qtv->curtime >= qtv->timeout || qtv->curtime < qtv->timeout - UDPTIMEOUT_LENGTH*2) { Sys_Printf(qtv->cluster, "Stream %i: Timeout\n", qtv->streamid); qtv->isconnected = false; return; } if (qtv->controller && !qtv->controller->netchan.isnqprotocol) { qtv->netchan.outgoing_sequence = qtv->controller->netchan.incoming_sequence; if (qtv->maysend) { qtv->maysend = false; qtv->packetratelimiter = qtv->curtime; } else qtv->packetratelimiter = qtv->curtime + 1; } else { if (qtv->curtime < qtv->packetratelimiter - UDPPACKETINTERVAL*2) qtv->packetratelimiter = qtv->curtime; } if (qtv->curtime >= qtv->packetratelimiter) { if (qtv->curtime >= qtv->nextsendpings || qtv->curtime < qtv->nextsendpings - PINGSINTERVAL_TIME*2) { qtv->nextsendpings = qtv->curtime + PINGSINTERVAL_TIME; SendClientCommand(qtv, "pings\n"); } ChooseFavoriteTrack(qtv); //if we froze somehow, don't speedcheat by a burst of 10000+ packets while we were frozen in a debugger or disk spinup or whatever if (qtv->packetratelimiter < qtv->curtime - UDPPACKETINTERVAL*2) qtv->packetratelimiter = qtv->curtime; if (qtv->map.trackplayer >= 0) { qtv->packetratelimiter += UDPPACKETINTERVAL; WriteByte(&msg, clc_tmove); WriteShort(&msg, qtv->map.players[qtv->map.trackplayer].current.origin[0]); WriteShort(&msg, qtv->map.players[qtv->map.trackplayer].current.origin[1]); WriteShort(&msg, qtv->map.players[qtv->map.trackplayer].current.origin[2]); } else if (qtv->controller) { qtv->packetratelimiter += UDPPACKETINTERVAL; if (qtv->controller->netchan.isnqprotocol) { memcpy(&qtv->controller->ucmds[0], &qtv->controller->ucmds[1], sizeof(qtv->controller->ucmds[0])); memcpy(&qtv->controller->ucmds[1], &qtv->controller->ucmds[2], sizeof(qtv->controller->ucmds[0])); qtv->controller->ucmds[2].msec = UDPPACKETINTERVAL; } WriteByte(&msg, clc_tmove); WriteShort(&msg, qtv->controller->origin[0]); WriteShort(&msg, qtv->controller->origin[1]); WriteShort(&msg, qtv->controller->origin[2]); /* qtv->controller->ucmds[0].angles[1] = qtv->curtime*120; qtv->controller->ucmds[1].angles[1] = qtv->curtime*120; qtv->controller->ucmds[2].angles[1] = qtv->curtime*120; */ msg.startpos = msg.cursize; WriteByte(&msg, clc_move); WriteByte(&msg, 0); WriteByte(&msg, 0); WriteDeltaUsercmd(&msg, &nullcmd, &qtv->controller->ucmds[0]); WriteDeltaUsercmd(&msg, &qtv->controller->ucmds[0], &qtv->controller->ucmds[1]); WriteDeltaUsercmd(&msg, &qtv->controller->ucmds[1], &qtv->controller->ucmds[2]); SetMoveCRC(qtv, &msg); } else if (qtv->proxyplayer || qtv->map.trackplayer < 0) { usercmd_t *cmd[3]; cmd[0] = &qtv->proxyplayerucmds[(qtv->proxyplayerucmdnum-2)%3]; cmd[1] = &qtv->proxyplayerucmds[(qtv->proxyplayerucmdnum-1)%3]; cmd[2] = &qtv->proxyplayerucmds[(qtv->proxyplayerucmdnum-0)%3]; cmd[2]->angles[0] = (qtv->proxyplayerangles[0]/360)*0x10000; cmd[2]->angles[1] = (qtv->proxyplayerangles[1]/360)*0x10000; cmd[2]->angles[2] = (qtv->proxyplayerangles[2]/360)*0x10000; cmd[2]->buttons = qtv->proxyplayerbuttons & 255; cmd[2]->forwardmove = (qtv->proxyplayerbuttons & (1<<8))?800:0 + (qtv->proxyplayerbuttons & (1<<9))?-800:0; cmd[2]->sidemove = (qtv->proxyplayerbuttons & (1<<11))?800:0 + (qtv->proxyplayerbuttons & (1<<10))?-800:0; cmd[2]->msec = qtv->curtime - qtv->packetratelimiter; cmd[2]->impulse = qtv->proxyplayerimpulse; if (cmd[2]->msec < 13) cmd[2]->msec = 13; qtv->packetratelimiter += cmd[2]->msec; qtv->proxyplayerimpulse = 0; msg.startpos = msg.cursize; WriteByte(&msg, clc_move); WriteByte(&msg, 0); WriteByte(&msg, 0); WriteDeltaUsercmd(&msg, &nullcmd, cmd[0]); WriteDeltaUsercmd(&msg, cmd[0], cmd[1]); WriteDeltaUsercmd(&msg, cmd[1], cmd[2]); qtv->proxyplayerucmdnum++; SetMoveCRC(qtv, &msg); } to = qtv->netchan.outgoing_sequence & (ENTITY_FRAMES-1); from = qtv->netchan.incoming_sequence & (ENTITY_FRAMES-1); if (qtv->map.frame[from].numents) { //remember which one we came from qtv->map.frame[to].oldframe = from; WriteByte(&msg, clc_delta); WriteByte(&msg, qtv->map.frame[to].oldframe); //let the server know } else qtv->map.frame[to].oldframe = -1; qtv->map.frame[to].numents = 0; Netchan_Transmit(qtv->cluster, &qtv->netchan, msg.cursize, msg.data); } } return; } else qtv->simtime = qtv->curtime; if (qtv->sourcesock == INVALID_SOCKET && !qtv->sourcefile) { if (qtv->errored == ERR_DISABLED) return; if (qtv->sourcetype == SRC_DEMODIR || qtv->curtime >= qtv->nextconnectattempt || qtv->curtime < qtv->nextconnectattempt - RECONNECT_TIME*2) { if (qtv->autodisconnect == AD_REVERSECONNECT) //2 means a reverse connection { qtv->errored = ERR_DROP; return; } if (!QTV_ConnectStream(qtv, qtv->server)) //reconnect it { qtv->errored = ERR_PERMANENT; return; } } } // SV_FindProxies(qtv->tcpsocket, qtv->cluster, qtv); //look for any other proxies wanting to muscle in on the action. if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET) { if (!Net_ReadStream(qtv)) { //if we have an error reading it //if it's valid, give up //what should we do here? //obviously, we need to keep reading the stream to keep things smooth } Net_WriteUpstream(qtv); } if (qtv->parsingqtvheader) { float svversion; int length; char *start; char *nl; char *colon; char *end; char value[128]; char challenge[128]; char authmethod[128]; // qtv->buffer[qtv->buffersize] = 0; // Sys_Printf(qtv->cluster, "msg: ---%s---\n", qtv->buffer); *authmethod = 0; qtv->parsetime = qtv->curtime; length = qtv->buffersize; if (length > 6) length = 6; if (ustrncmp(qtv->buffer, "QTVSV ", length)) { Sys_Printf(qtv->cluster, "Stream %i: Server is not a QTV server (or is incompatible)\n", qtv->streamid); //printf("%i, %s\n", qtv->buffersize, qtv->buffer); qtv->errored = ERR_PERMANENT; return; } if (length < 6) return; //not ready yet end = (char*)qtv->buffer + qtv->buffersize - 1; for (nl = (char*)qtv->buffer; nl < end; nl++) { if (nl[0] == '\n' && nl[1] == '\n') break; } if (nl == end) return; //we need more header still //we now have a complete packet. svversion = atof((char*)qtv->buffer + 6); if ((int)svversion != 1) { Sys_Printf(qtv->cluster, "Stream %i: QTV server doesn't support a compatible protocol version (returned %i)\n", qtv->streamid, atoi((char*)qtv->buffer + 6)); qtv->errored = ERR_PERMANENT; return; } qtv->upstreamacceptschat = svversion>=1.1; qtv->upstreamacceptsdownload = svversion>=1.1; length = (nl - (char*)qtv->buffer) + 2; end = nl; nl[1] = '\0'; start = strchr((char*)qtv->buffer, '\n')+1; while((nl = strchr(start, '\n'))) { *nl = '\0'; colon = strchr(start, ':'); if (colon) { *colon = '\0'; colon++; while (*colon == ' ') colon++; COM_ParseToken(colon, value, sizeof(value), NULL); } else { colon = ""; *value = '\0'; } //read the notes at the top of this file for which messages to expect if (!strcmp(start, "AUTH")) strcpy(authmethod, value); else if (!strcmp(start, "CHALLENGE")) strcpy(challenge, colon); else if (!strcmp(start, "COMPRESSION")) { //we don't support compression, we didn't ask for it. Sys_Printf(qtv->cluster, "Stream %i: QTV server wrongly used compression\n", qtv->streamid); qtv->errored = ERR_PERMANENT; return; } else if (!strcmp(start, "PERROR")) { Sys_Printf(qtv->cluster, "\nStream %i: Server PERROR from %s: %s\n\n", qtv->streamid, qtv->server, colon); qtv->errored = ERR_PERMANENT; qtv->buffersize = 0; qtv->forwardpoint = 0; return; } else if (!strcmp(start, "TERROR") || !strcmp(start, "ERROR")) { //we don't support compression, we didn't ask for it. Sys_Printf(qtv->cluster, "\nStream %i: Server TERROR from %s: %s\n\n", qtv->streamid, qtv->server, colon); qtv->buffersize = 0; qtv->forwardpoint = 0; if (qtv->autodisconnect == AD_WHENEMPTY || qtv->autodisconnect == AD_REVERSECONNECT) qtv->errored = ERR_DROP; //if its a user registered stream, drop it immediatly else { //otherwise close the socket (this will result in a timeout and reconnect) if (qtv->sourcesock != INVALID_SOCKET) { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; } } return; } else if (!strcmp(start, "ASOURCE")) { Sys_Printf(qtv->cluster, "SRC: %s\n", colon); } else if (!strcmp(start, "ADEMO")) { int size; size = atoi(colon); colon = strchr(colon, ':'); if (!colon) colon = ""; else colon = colon+1; while(*colon == ' ') colon++; if (size > 1024*1024) Sys_Printf(qtv->cluster, "DEMO: (%3imb) %s\n", size/(1024*1024), colon); else Sys_Printf(qtv->cluster, "DEMO: (%3ikb) %s\n", size/1024, colon); } else if (!strcmp(start, "PRINT")) { Sys_Printf(qtv->cluster, "Stream %i: QTV server: %s\n", qtv->streamid, colon); } else if (!strcmp(start, "BEGIN")) { qtv->parsingqtvheader = false; } else { Sys_Printf(qtv->cluster, "DBG: QTV server responded with a %s key\n", start); } start = nl+1; } qtv->buffersize -= length; memmove(qtv->buffer, qtv->buffer + length, qtv->buffersize); if (qtv->serverquery) { Sys_Printf(qtv->cluster, "End of list\n"); qtv->errored = ERR_DROP; qtv->buffersize = 0; qtv->forwardpoint = 0; return; } else if (*authmethod) { //we need to send a challenge response now. Net_SendQTVConnectionRequest(qtv, authmethod, challenge); return; } else if (qtv->parsingqtvheader) { Sys_Printf(qtv->cluster, "Stream %i: QTV server sent no begin command - assuming incompatible\n\n", qtv->streamid); qtv->errored = ERR_PERMANENT; qtv->buffersize = 0; qtv->forwardpoint = 0; return; } qtv->parsetime = Sys_Milliseconds() + qtv->cluster->anticheattime; if (!qtv->usequakeworldprotocols) Sys_Printf(qtv->cluster, "Stream %i: Connection established, buffering for %g seconds\n", qtv->streamid, qtv->cluster->anticheattime/1000.0f); SV_ForwardStream(qtv, qtv->buffer, qtv->forwardpoint); } QTV_CollectCommentry(qtv); while (qtv->curtime >= qtv->parsetime) { if (qtv->buffersize < 2) { //not enough stuff to play. if (qtv->parsetime < qtv->curtime) { qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay; // if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET) // QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid); } break; } buffer = qtv->buffer; switch (qtv->buffer[1]&dem_mask) { case dem_set: length = 10; if (qtv->buffersize < length) { //not enough stuff to play. qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay; // if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET) // QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid); continue; } qtv->parsetime += buffer[0]; //well this was pointless if (qtv->forwardpoint < length) //we're about to destroy this data, so it had better be forwarded by now! { SV_ForwardStream(qtv, qtv->buffer, length); qtv->forwardpoint += length; } memmove(qtv->buffer, qtv->buffer+10, qtv->buffersize-(length)); qtv->buffersize -= length; qtv->forwardpoint -= length; continue; case dem_multiple: lengthofs = 6; break; default: lengthofs = 2; break; } if (qtv->buffersize < lengthofs+4) { //the size parameter doesn't fit. // if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET) // QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid); qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay; break; } length = (buffer[lengthofs]<<0) + (buffer[lengthofs+1]<<8) + (buffer[lengthofs+2]<<16) + (buffer[lengthofs+3]<<24); if (length > MAX_MSGLEN) { //THIS SHOULDN'T HAPPEN! //Blame the upstream proxy! Sys_Printf(qtv->cluster, "Stream %i: Warning: corrupt input packet (%i bytes) too big! Flushing and reconnecting!\n", qtv->streamid, length); if (qtv->sourcefile) { fclose(qtv->sourcefile); qtv->sourcefile = NULL; } else { closesocket(qtv->sourcesock); qtv->sourcesock = INVALID_SOCKET; } qtv->buffersize = 0; qtv->forwardpoint = 0; break; } if (length+lengthofs+4 > qtv->buffersize) { // if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET) // QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid); qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay; //add two seconds break; //can't parse it yet. } // if (qtv->sourcesock != INVALID_SOCKET) // { // QTV_Printf(qtv, "Forcing demo speed to play at 100% speed\n"); // qtv->parsespeed = 1000; //no speeding up/slowing down routed demos // } packettime = buffer[0]; if (qtv->parsespeed>0) packettime = ((1000*packettime) / qtv->parsespeed); qtv->nextpackettime = qtv->parsetime + packettime; if (qtv->nextpackettime < qtv->curtime) { switch(qtv->buffer[1]&dem_mask) { case dem_multiple: if ((qtv->pexte&PEXTE_HIDDENMESSAGES) && 0 == (buffer[lengthofs-4]<<0) + (buffer[lengthofs-3]<<8) + (buffer[lengthofs-2]<<16) + (buffer[lengthofs-1]<<24)) ; //fucked hidden message crap. don't trip up on it. else ParseMessage(qtv, buffer+lengthofs+4, length, qtv->buffer[1]&dem_mask, (buffer[lengthofs-4]<<0) + (buffer[lengthofs-3]<<8) + (buffer[lengthofs-2]<<16) + (buffer[lengthofs-1]<<24)); break; case dem_single: case dem_stats: ParseMessage(qtv, buffer+lengthofs+4, length, qtv->buffer[1]&dem_mask, 1<<(qtv->buffer[1]>>3)); break; case dem_all: if (qtv->buffer[1] & ~dem_mask) //dem_qtvdata if (qtv->sourcetype != SRC_DEMO) break; //fallthrough case dem_read: ParseMessage(qtv, buffer+lengthofs+4, length, qtv->buffer[1], 0xffffffff); break; default: Sys_Printf(qtv->cluster, "Message type %i\n", qtv->buffer[1]&dem_mask); break; } length = lengthofs+4+length; //make length be the length of the entire packet qtv->oldpackettime = qtv->curtime; if (qtv->buffersize) { //svc_disconnect can flush our input buffer (to prevent the EndOfDemo part from interfering) if (qtv->forwardpoint < length) //we're about to destroy this data, so it had better be forwarded by now! { SV_ForwardStream(qtv, qtv->buffer, length); qtv->forwardpoint += length; } memmove(qtv->buffer, qtv->buffer+length, qtv->buffersize-(length)); qtv->buffersize -= length; qtv->forwardpoint -= length; } if (qtv->sourcefile) { Net_ReadStream(qtv); qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME_DEMO; } else qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME; qtv->parsetime += packettime; } else break; } } sv_t *QTV_NewServerConnection(cluster_t *cluster, int newstreamid, char *server, char *password, qboolean force, enum autodisconnect_e autoclose, qboolean noduplicates, qboolean query) { sv_t *qtv; if (noduplicates) { for (qtv = cluster->servers; qtv; qtv = qtv->next) { if (!strcmp(qtv->server, server)) { //if the stream detected some permanent/config error, try reconnecting again (of course this only happens when someone tries using the stream) //warning review this logic if (qtv->errored == ERR_DISABLED) { if (!(!QTV_ConnectStream(qtv, server) && !force)) //try and wake it up qtv->errored = ERR_NONE; } return qtv; } } } if (!newstreamid) //no fixed id? generate a default id newstreamid = 100; //make sure it doesn't conflict for(;;newstreamid++) { for (qtv = cluster->servers; qtv; qtv = qtv->next) { if (qtv->streamid == newstreamid) break; } if (!qtv) break; } if (autoclose) if (cluster->nouserconnects) return NULL; qtv = malloc(sizeof(sv_t)); if (!qtv) return NULL; memset(qtv, 0, sizeof(*qtv)); //set up a default config // qtv->tcplistenportnum = PROX_DEFAULTLISTENPORT; strcpy(qtv->server, PROX_DEFAULTSERVER); memcpy(qtv->connectpassword, password, sizeof(qtv->connectpassword)-1); // qtv->tcpsocket = INVALID_SOCKET; qtv->sourcesock = INVALID_SOCKET; qtv->autodisconnect = autoclose; qtv->parsingconnectiondata = true; qtv->serverquery = query; qtv->silentstream = true; qtv->parsespeed = 1000; qtv->streamid = newstreamid; qtv->cluster = cluster; qtv->next = cluster->servers; if (autoclose != AD_REVERSECONNECT) //2 means reverse connection (don't ever try reconnecting) { if (!QTV_ConnectStream(qtv, server) && !force) { QTV_Cleanup(qtv, false); free(qtv); return NULL; } } cluster->servers = qtv; cluster->numservers++; return qtv; } ================================================ FILE: fteqtv/sp_dsound.c ================================================ #include "qtv.h" #ifdef COMMENTARY #include static HANDLE hInstDS; static HRESULT (WINAPI *pDirectSoundCreate)(LPGUID, LPDIRECTSOUND *, LPUNKNOWN); typedef struct { soundplay_t funcs; LPDIRECTSOUND ds; LPDIRECTSOUNDBUFFER dsbuf; int buffersize; int writepos; int readpos; int sampbytes; } dsplay_t; int DSOUND_UpdatePlayback(soundplay_t *sp, int samplechunks, char *buffer) { int ret; dsplay_t *dsp = (dsplay_t*)sp; char *sbuf; int sbufsize; int writable; int remaining = samplechunks; if (!samplechunks) return 0; IDirectSoundBuffer_GetCurrentPosition(dsp->dsbuf, &dsp->readpos, NULL); dsp->readpos /= dsp->sampbytes; while (ret = IDirectSoundBuffer_Lock(dsp->dsbuf, 0, dsp->buffersize*dsp->sampbytes, (void**)&sbuf, &sbufsize, NULL, NULL, 0)) { if (!FAILED(ret)) break; if (ret == DSERR_BUFFERLOST) printf("Buffer lost\n"); else break; // if (FAILED(IDirectSoundBuffer_Resore(dsp->dsbuf))) // return 0; } //memset(sbuf, 0, sbufsize); writable = remaining; if (writable > sbufsize/dsp->sampbytes - dsp->writepos) writable = sbufsize/dsp->sampbytes - dsp->writepos; memcpy(sbuf+dsp->writepos*dsp->sampbytes, buffer, writable*dsp->sampbytes); remaining -= writable; buffer += writable*dsp->sampbytes; dsp->writepos += writable; dsp->writepos %= dsp->buffersize; if (samplechunks > 0) { writable = remaining; if (writable > dsp->readpos) writable = dsp->readpos; memcpy(sbuf, buffer, writable*dsp->sampbytes); remaining -= writable; dsp->writepos += writable; dsp->writepos %= dsp->buffersize; } IDirectSoundBuffer_Unlock(dsp->dsbuf, sbuf, sbufsize, NULL, 0); printf("%i %i\n", 100*dsp->readpos / dsp->buffersize, 100*dsp->writepos / dsp->buffersize); return samplechunks - remaining; } void DSOUND_Shutdown(soundplay_t *dsp) { } soundplay_t *SND_InitPlayback(int speed, int bits) { int ret; DSBCAPS caps; DSBUFFERDESC bufdesc; LPDIRECTSOUND ds; dsplay_t *hnd; WAVEFORMATEX format; if (!hInstDS) { hInstDS = LoadLibrary("dsound.dll"); if (hInstDS == NULL) { printf ("Couldn't load dsound.dll\n"); return NULL; } pDirectSoundCreate = (void *)GetProcAddress(hInstDS,"DirectSoundCreate"); if (!pDirectSoundCreate) { printf ("Couldn't get DS proc addr\n"); return NULL; } // pDirectSoundEnumerate = (void *)GetProcAddress(hInstDS,"DirectSoundEnumerateA"); } ds = NULL; pDirectSoundCreate(NULL, &ds, NULL); if (!ds) return NULL; hnd = malloc(sizeof(*hnd)); memset(hnd, 0, sizeof(*hnd)); hnd->funcs.update = DSOUND_UpdatePlayback; hnd->funcs.close = DSOUND_Shutdown; hnd->ds = ds; hnd->sampbytes = bits/8; if (FAILED(IDirectSound_SetCooperativeLevel (hnd->ds, GetDesktopWindow(), DSSCL_EXCLUSIVE))) printf("SetCooperativeLevel failed\n"); memset(&bufdesc, 0, sizeof(bufdesc)); bufdesc.dwSize = sizeof(bufdesc); // bufdesc.dwFlags |= DSBCAPS_GLOBALFOCUS; //so we hear it if quake is loaded bufdesc.dwFlags |= DSBCAPS_PRIMARYBUFFER; //so we can set speed bufdesc.dwFlags |= DSBCAPS_CTRLVOLUME; bufdesc.lpwfxFormat = NULL; bufdesc.dwBufferBytes = 0; format.wFormatTag = WAVE_FORMAT_PCM; format.cbSize = 0; format.nChannels = 1; format.wBitsPerSample = bits; format.nSamplesPerSec = speed; format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8; format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; ret = IDirectSound_CreateSoundBuffer(hnd->ds, &bufdesc, &hnd->dsbuf, NULL); if (!hnd->dsbuf) { printf("Couldn't create primary buffer\n"); DSOUND_Shutdown(&hnd->funcs); return NULL; } if (FAILED(IDirectSoundBuffer_SetFormat(hnd->dsbuf, &format))) printf("SetFormat failed\n"); //and now make a secondary buffer bufdesc.dwFlags = 0; bufdesc.dwFlags |= DSBCAPS_CTRLFREQUENCY; bufdesc.dwFlags |= DSBCAPS_LOCSOFTWARE; bufdesc.dwFlags |= DSBCAPS_GLOBALFOCUS; bufdesc.dwBufferBytes = speed * format.nChannels * hnd->sampbytes; bufdesc.lpwfxFormat = &format; ret = IDirectSound_CreateSoundBuffer(hnd->ds, &bufdesc, &hnd->dsbuf, NULL); if (!hnd->dsbuf) { printf("Couldn't create secondary buffer\n"); DSOUND_Shutdown(&hnd->funcs); return NULL; } memset(&caps, 0, sizeof(caps)); caps.dwSize = sizeof(caps); IDirectSoundBuffer_GetCaps(hnd->dsbuf, &caps); hnd->buffersize = caps.dwBufferBytes / hnd->sampbytes; //clear out the buffer { char *buffer; int buffersize=0; IDirectSoundBuffer_Play(hnd->dsbuf, 0, 0, DSBPLAY_LOOPING); ret = IDirectSoundBuffer_Lock(hnd->dsbuf, 0, hnd->buffersize*hnd->sampbytes, (void**)&buffer, &buffersize, NULL, NULL, 0); memset(buffer, 0, buffersize); IDirectSoundBuffer_Unlock(hnd->dsbuf, buffer, buffersize, NULL, 0); IDirectSoundBuffer_Stop(hnd->dsbuf); } //DSERR_INVALIDPARAM IDirectSoundBuffer_Play(hnd->dsbuf, 0, 0, DSBPLAY_LOOPING); IDirectSoundBuffer_GetCurrentPosition(hnd->dsbuf, &hnd->readpos, &hnd->writepos); hnd->writepos = hnd->readpos + speed / 2; //position our write position a quater of a second infront of the read position printf("%i %i\n", 100*hnd->readpos / hnd->buffersize, 100*hnd->writepos / hnd->buffersize); return &hnd->funcs; } /* void soundtest(void) { int speed = 22100*2; int bits = 16; int i; int sampsavailable; short buffer[1024]; soundcapt_t *capt; soundplay_t *play; capt = SND_InitCapture (speed, bits); if (!capt) { printf("Failed to init capturer\n"); exit(0); } play = SND_InitPlayback (speed, bits); if (!play) { printf("Failed to init playback\n"); exit(0); } for(;;) { for (i = 0; i < sizeof(buffer)/sizeof(buffer[0]); i++) buffer[i] = rand(); sampsavailable = capt->update(capt, sizeof(buffer)/(bits/8), (char*)buffer); play->update(play, sampsavailable, (char*)buffer); Sleep(1); } } */ #endif ================================================ FILE: fteqw_readme.txt ================================================ FTEQW README ============ This file contains the following sections: 1. ABOUT 2. FEATURES 3. INSTALLING 4. NETWORK QUICK-START 5. TWEAKS 6. TROUBLESHOOTING 7. CONTACT 8. LICENSE 1. ABOUT ======== FTEQW is the advanced, portable Quake engine. It supports multiple games running on idTech, plus its own set of games that developers have created. Due to the vast amount of supported formats, features and innovations inside the engine and its very own QuakeC compiler (fteqcc), it's very much considered the swiss-army knife of Quake engines. 2. FEATURES =========== - Portable engine that runs on x86, amd64, ARM/64, PPC64LE and Web - Hybrid protocol engine that supports multiple games - Rendering API support for D3D8, D3D9, D3D11, OpenGL, Vulkan - Splitscreen support for Quake and most mods - In-game voice chat powered by either Opus or Speex - Advanced renderer features, powered by a strong material system and support for both HLSL and GLSL shader code - Multiple audio backends, from OSS to SDL_Sound and OpenAL Soft, plus API so developers can take advantage of AL EAX reverb features. And yes, DirectSound. - Integrated next-generation QuakeC compiler and debugger with support for breakpoints, real-time ingame attribute debugging and much more - Support for IPv4 and IPv6 - Video output presets, to make your games either look like the original versions, or more modern with real-time lighting and more accurate shading - Support for CD-DA/Red Book music replacement in a variety of formats, such as Vorbis, MPEG-3, WAVE and FLAC (ffmpeg plugin required) 3. INSTALLING ============= Put the engine binary and desired plugins for your platform into the root of the game directory. Supported games: Quake and its Missionpacks QuakeWorld Quake II and its Missionpacks Quake III Arena and Team Arena HeXen II and its Missionpack If you want to be explicit about the game you're starting, you can pass the command-line parameters '-quake2', '-quake3' or '-hexen2' for the respective game. This will make sure that in a crowded universal game directory, FTE starts the right game. If you want to install music replacement files, you put them into the music/ folder with the trackXX naming convention, starting with track02. Important note regarding Quake II based support ----------------------------------------------- If you're running a 64-bit version of FTEQW, then you also need 64-bit game-logic for Quake II. We recommend getting the game .dll/.so from the Yamagi Quake II project for your respective platform. It's recommended that you do for win32 as well, as that will ensure that save games work properly and you can stop worrying about them becoming incompatible between other machines. 4. NETWORK QUICK-START ====================== Upon launching FTEQW, you can use the multiplayer menu's own built-in server browser to join and connect to a vast array of matches across all the different protocols. No more QuakeSpy/GameSpy 3D client required! If you want to host a game, you can either run a listen server with the ports forwarded, or host a listen server using frag-net.com's online service. Start a new multiplayer server, change the "Public" setting to "Holepunch" and players will automatically see your game in their server-browser once it's started. You can also set FTEQW to run in a terminal/command-prompt for hosting a dedicated server session. Simply pass the command-line argument like so: fteqw -dedicated This will create an interactive shell reminiscent to the console that's accessible in-game. You can host a game directly with frag-net.com without having to worry about port-forwarding and have a map up and running like so: fteqw +set sv_public 2 +set sv_playerslots 8 +map dm4 5. TWEAKS ========= You can apply tweaks by opening the console (SHIFT+ESC) and entering commands into the line buffer. In there you can enter console variables (cvars) that affect how the game behaves, as well as enter console commands that trigger an action in the engine. You can always find a list of both of these with the console commands 'cvarlist' and 'cmdlist' respectively. Some example commands ------------------------ bind Binds a command to a key, e.g. bind F12 quit map Starts a new game on , e.g. map dm4 connect
    Establishes a connection to the specified IP/hostname address. disconnect Closes and remote or local game session. cfg_save Save amy unsaved configuration changes. quit Quits the game. About console variables ----------------------- Unlike other Quake engines, FTEs console variable system is more akin to those of later idTech engines. Console variables can be set temporarily (until the next engine restart) or permanently. set sv_port 26000 Sets the current port to 26000 for this session seta sv_port 26000 Sets the current port gets set to 26000 and makes sure it will get 'archived', aka saved. Sometimes, you'll be able to change a cvar without entering 'set' or 'seta' beforehand. This is due to the cvar/cmd suggestion system. When you simply set a cvar that way, 'set' is assumed. So those changes are only temporary. Commandline arguments --------------------- You can pass cvars and commands directly to the engine binary, prefixed with a plus symbol like so: fteqw +set sv_port 26000 +map dm4 There's also other interesting commandline only arguments you can pass: -nohome Don't attempt to save configs, saves, screenshots in the home or user directory. -basedir Specifies the root game directory path. -basegame Specifies which folder to look in for main game data. -game Specifies which mod folder to load over the game data -window Tells the renderer to run in a window -manifest Specifies a game manifest to load. This is for advanced game and mod switching. -dedicated Run the engine in dedicated server mode, no video out. Changing games and mods ----------------------- Launching a Quake 1 mod can be done like so: fteqw -game fortress That will assume a basegame of 'id1' and the mod 'fortress' will be loaded on top of it. However, if you're playing a game that uses no data from Quake 1: fteqw -basegame openquartz Then it'll never even touch or peek into the folder 'id1'. Of course you can pass -game after that, too. Understanding manifests (advanced users) ---------------------------------------- You can setup custom game configurations with FTE's manifest files. Those can be quite advanced, as they might inherit multiple directories, change the name of the window title, set binds, aliases and cvars ahead of time and much more. FTEMANIFEST 1 GAME funky NAME "Funky QW Game" BASEGAME id1 BASEGAME qw BASEGAME funky An example manifest that will load id1, qw and then funky. If you wanted, you can set default cvars and aliases in there like so: -set sv_example 1234 -seta cl_foobar 5678 -alias funky1 "impulse 416" +bind g funky1 Note the dash and plus symbols, they actually notate when to execute the command in question. '-' means before the engine loads the game config whereas '+' notates it will override anything that will usually be set by the game. You'd then save this as, for example, funky.fmf and load the manifest via the command-line: fteqw -manifest funky.fmf 6. TROUBLESHOOTING ================== If you're running FTEQW on an older machine with Intel GMA graphics, you probably want to try running the engine in D3D9 mode if you're encountering any graphical issues: fteqw +set vid_renderer d3d9 If you can only run OpenGL but still have graphical issues, try forcing support for the builtin GLSL off: fteqw +set gl_blacklist_debug_glsl 1 If FTEQW is seemingly not saving your settings, make sure you tell it to save your config when you quit the game. If it still does not work somehow, enter the console command 'cfg_save' into the console. It should output where the file gets saved or if there's any problems writing the configuration file. If OpenAL is causing crashes at launch (happens with some distributions, that is out of our control) then try starting FTEQW with: fteqw +set s_al_disable 1 7. CONTACT ========== If you need more help, have suggestions or want to hang out with the developers that make FTEQW what it is, join us on IRC! No account required. irc.quakenet.org #fte Bug reports welcome! 8. LICENSE ========== Copyright (c) 2004-2020 FTE's team and its contributors Quake source (c) 1999 id Software FTEQW is supplied to you under the terms of the same license as the original Quake sources, the GNU General Public License Version 2. Please read the 'LICENSE' file for details. The latest source is always available at: http://svn.code.sf.net/p/fteqw/code/trunk/ Binaries are usually built at: https://fte.triptohell.info/moodles/ ================================================ FILE: ftetools_readme.txt ================================================ FTE TOOLS README ============ This file contains the following sections: 1. FTEQCC 2. PACKAGE MANAGEMENT 3. IQM TOOL 4. IMAGE TOOL 5. CONTACT 6. LICENSE 1. FTEQCC ======== FTEQCC is used to compile Full FTEQCC documentation can be found here: https://fte.triptohell.info/moodles/fteqcc/README.html 2. PACKAGE MANAGEMENT =========== The commandline version of fteqcc doubles up as a pak/pk3 creator/extractor `fteqcc -l PACKAGENAME` Lists the contents of a file on the stdout. `fteqcc -x PACKAGENAME` Extracts all files from the named package to the working directory. You should normally use ../foo.pak in order to avoid overwriting files unintentionally. `fteqcc -x PACKAGENAME SUBDIR` Extracts specific files from the named package. `fteqcc -0 SUBDIR` Generates a `subdir.pak` file with the contents of that subdir. Unlike most pak generators, the pak will also be openable with any zip tool for users to easily view files (they shouldn't edit them though). `fteqcc -9 SUBDIR` Generates a `subdir.pk3` file with the contents of that subdir. Contents will be compressed to reduce filesize, but won't work in vanilla quake. `fteqcc -z SUBDIR` Generate a `subdir.pk3` dictionary file, and an (additional) `subdir.pXX` file (name generated sequentially). The dictionary file will be versioned with different servers potentially using different versions. This prevents re-downloading redundant copies of the same files for each different version. The spanned data files are not versioned, so these should only be generated by authoritive developers (ie: not same-filename forks). Many engines support interpretting `foo.pk3dir` subdirs as proto-packages, which makes it easier to test packages without constant re-compression. 3. IQM TOOL =========== About: This is a commandline tool for creating iqm files from intermediate files exported from modelling programs. Possible Inputs: smd gltf glb iqe md5mesh md5anim fbx obj Outputs: iqm / vvm The default format. 'vvm' is a moniker for the backwards-compatible extensions fte supports which are not supported by other engines/tools. mdl quake1's model format. Horrible format, but a handy fallback for people who refuse to use better engines. Usage: IQM tool Can be used two ways. `iqm foo.cmd` uses a command script to decide which files to read, modifiers for each sequence or mesh, etc. `iqm foo.iqm foo.gltf` converts the gltf file to an iqm file. Animation rate will be guessed. `iqm foo.mdl foo.gltf` writes out a quake1 mdl file. Framegroups will be used for each animation sequence. Command script: #COMMENT //COMMENT Just a comment, ignored. exec filename Invokes an external script (useful when you have multiple models with the same animations or so). modelflags BITS Sets the output model's flags. Queryable in gamecode. mesh MESHNAME ATTRIBUTELIST Overrides attributes of an imported mesh to use for the visible part of the model. Use the `import` command to actually import the geometry. Attributes are: contents BITSORNAMES Controls which traces may impact the imported surfaces. You should normally specify 0 if you are using hitboxes. Default is CONTENTS_BODY. surfaceflags BITSORNAMES Misc info eg reported by tracelines. body NUM The 'body' number which can be queried in gamecode (typically used to report eg headshots). geomset SET ID Sets this mesh to only draw when geomset SET has been configured as ID. lodrange MIN MAX This mesh will only be drawn when the screen coverage is within the specified range. Exact interpretation can vary according to engine cvars. hitbox BODYNUM BONENAME MINS MAXS Creates a cuboid mesh around the named bone (as a box in the base pose). The mesh will be given CONTENTS_BODY such that it will be hit by hitmesh traces. The bodynum arg can be queried via gamecode. bone NAME ATTRIBUTELIST rename NEWNAME The bone read from input files will be written as NEWNAME in the output file (so gamecode can manipulate things consistently). group GROUPNUM Bones can be reordered in the output according to their group numbers. Only the root bone of each group needs its group specified (children will inherit). This simplies bone range operations in gamecode import FILENAME ATTRIBUTELIST model FILENAME ATTRIBUTELIST scene FILENAME ATTRIBUTELIST animation FILENAME ATTRIBUTELIST Loads model AND animation data from an imported file. File formats must match the given filename extensions. Attributes are: Any attributes supported by the mesh command (such attributes here define the default values for imported meshes). name NEWNAME The animation will be queryable to gamecode as the NEWNAME. fps RATE Overrides the frame rate of the animation. loop Specifies that the animation must loop. clamp Specifies that the animation must NOT loop (animation will stop once it reaches the last pose). unpack Generates single-pose animations, consistent with the way vanilla QC animates. pack Generates multiple-pose animations, gamecode will be able to specify animation and time separately. nomesh Do NOT generate meshes from this file. noanim Do NOT generate animations from this file. materialprefix PATH Prefixes the imported texture with an additional path (to avoid conflicts with other files). start FRAME end FRAME Limits inputed data to the specified poses. scale SCALE Resizes the input data. rotate PITCH ROLL YAW Rotates the imported data (quake is +x=forward, +y=left, +z=up). translate X Y Z Moves the mesh+bones around a bit. event [SEQUENCE:]POSE EVCODE EVSTRING Defines a model event at the specified timestamp. output FILENAME output_iqm FILENAME output_vvm FILENAME output_qmdl FILENAME output_md16 FILENAME Specifies the file to be written. You may have one per supported output model format, each will contain roughly equivelent data (where supported). 4. IMAGE TOOL ============= About: This is a tool for converting various image file types to more favourable ones. This includes generating wad files, cubemap images, etc. Wad Examples: `imgtool --ext mip [--PIXFMT] [--resize W H] *.EXT` Converts the input files to quake's miptex format. Input files will not be changed. If compression was specified, the resulting miptex will contain an additional high-colour alternative. If --resize is used, the paletted data will be resized without affecting the high-colour alternative. Supported pixel formats: rgba8,rgb8, rgb565,rgba5551,rgba4444, l8, bc1-7, etc1,etc2,etcp,etca, astc*x*, e5bgr9 NOTE: use of alternative formats requires a qbsp which does NOT strip this information. The vanilla qbsp will work fine for this purpose, but more advanced qbsp utils have a tendancy to strip the info (including ericw's, sadly). `imgtool -w WADFILE *.mip` Packs the specified miptex files into the named wad file. `imgtool -x --ext EXT WADORBSP` Extracts the textures from the specified wad or bsp file, saving them as EXT files. General Examples: `imgtool --help` Shows a list of compressed pixel formats, and supported file formats. `imgtool -i FILE` Shows info about the named file(s). `imgtool --ext ktx [--PIXFMT] *.EXT` `imgtool --ext dds [--PIXFMT] *.EXT` Converts the input files to a hardware-friendly file format (with the specified pixel format). Input files will not be changed. ktx supports all recognised hardware formats. dds is more limited (and eg doesn't support etc or astc). hud artwork should generally be astc4x4 or bc7 for best quality. models and walls can be of lower quality, astc6x6, etc2, or bc1 are good choices. for hdr pixel formats, try astc4x4_hdr or bc6. `imgtool --ext png *.EXT` Converts the input files to png format for viewing/editing. Input files will not be changed. `imgtool --cube [--PIXFMT] -o mysky.dds mysky_*.tga` Converts 6 input files to a single cubemap file. Inputs will be reordered according to quake's typical cubemap postfixes (and flipped as appropriate), otherwise be careful with wildcards. `imgtool --2darray [--PIXFMT] -o foo.ktx IDX0 IDX1 IDXN` Converts the input textures into a 2d texture array. Input file order matters. `imgtool --3d [--PIXFMT] -o foo.ktx LAYER0 LAYER1 LAYERN` Converts the input textures into a 3d texture. Input file order matters. Which pixel format to use: ASTC: The ldr-only profile is part of the gles3.2 spec (but likely to be emulated on nvidia). ASTC has a range of block sizes with each block being 16 bytes, thus larger block sizes yield greater compression. This format supports multiple planes, which reduces issues with multiple gradients in a block, which means it can cope with pixel art better than eg s3tc with larger block sizes. Bits are distributed according to usage per block, this means it can use more bits for the rgb channels where alpha is constant, giving it more useful bits than eg bc3. ASTC is able to use a second set of weights for any single channel (not just alpha), which makes it suitable for encoding normalmaps for instance. To encode ASTC pixel formats, you will need to install astcenc - https://github.com/ARM-software/astc-encoder/releases (or use astc-encoded source files). ETC2: Part of the gles3.0/gl4.3 spec ETC2 is a superset of ETC1, and has been somewhat obsoleted by ASTC. BC1/2/3: AKA s3tc, AKA dxt, available only via optional extensions, but is mandated by d3d 9_1 feature level. These formats all encode two 565 colours per 4*4 block with 2-bits per pixel for interpolation, which can result in discolouration. bc2 uses an additional 64bit block to encode 4bit alpha. bc3 also uses an additional block for alpha, but does so simiarly to its rgb values (two 8bit alpha values, with 3-way interpolation). To encode s3tc pixel formats, you will need to install libnvtt-bin - https://github.com/castano/nvidia-texture-tools (or export from image editors as dds or ktx files). BC4/5: AKA rgtc, part of the gl3.0 spec, or d3d10. These formats reuse bc3's 'alpha' compression to encode one or two channels respectively. BC1 can be handy for heightmaps/greyscale/etc, while BC5 can be useful for 2-channel normalmaps. They are not generally useful as eg wall textures. BC6/7: AKA bptc, part of the gl4.2, or d3d11. These formats support one or two planes per block, which means they can cope with pixel art quite a bit better than bc1/3. While they're the same size as bc3 (twice that of bc1), they have multiple modes per block that allow them to eg avoid wasting bits on unused alpha data. The difference is that BC7 encodes RGBA ldr data, while BC6 encodes RGB hdr data. To encode s3tc pixel formats, you will need to install (debian experimental) libnvtt-bin - https://github.com/castano/nvidia-texture-tools (or export from image editors as dds or ktx files). Too Long didn't read: Use ASTC if you're targetting modern mobile devices (astc6x6 for walls, astc4x4 for huds, or something). Use ETC2 if you're targetting older mobile users. Use BC1 for desktop model/wall textures, and bc3 for things with non-binary alpha channels. Use BC7 for desktop hud textures or other textures where BC1 does a terrible job. Use jpegs if you just want to get filesize down without caring about performance. If the user's hardware doesn't support the used formats then FTE can software-decode, so if you're expecting both mobile+desktop users with a single set of textures then favour mobile (desktop GPUs have better memory bandwidth). 5. CONTACT ========== If you need more help, have suggestions or want to hang out with the developers that make FTE what it is, join us on IRC! No account required. irc.quakenet.org #fte Bug reports welcome! 6. LICENSE ========== Copyright (c) 2004-2020 FTE's team and its contributors Quake source (c) 1999 id Software FTEQW is supplied to you under the terms of the same license as the original Quake sources, the GNU General Public License Version 2. Please read the 'LICENSE' file for details. The latest source is always available at: http://svn.code.sf.net/p/fteqw/code/trunk/ Binaries are usually built at: https://fte.triptohell.info/moodles/ ================================================ FILE: games/fortressone.fmf ================================================ FTEMANIFEST 1 game quake name "FortressOne" gamedir fortress protocolname FortressOne //mixing fortressone and other fortress mods in the same gamedir gets messy (especially when protocolnames are being overriden inside configs) mainconfig "fo.cfg" //homedir usage is used only where required (or previously created). things like flatpak require its use. //see quake-demo.fmf archivedpackage id1/pak0.pak 0x4f069cac id1/pak0.pak https://updates.triptohell.info/moodles/live/QUAKE_SW.zip //this is made complicated because we can't just use the pk3 as-is etc (engine will sandbox the menuqc if not signed). package fortress/fortressone.pk3 mirror "https://github.com/FortressOne/fte-config/releases/download/1.1.1-beta.1/fortressone-fte-linux-1.1.1-beta.1-portable.zip" crc 0x7d74337f prefix "/FortressOne/fortress" filesize 197849205 sha512 "42a0deaa571f30c56dac08603d94d968a603a6c2d64db29473cb25f7e1db29fe63260cc8cda2e41dbbb3decd5e1c5440aa6bbceeae45c3225dad359fe0559021" signature "Spike:OrVRb1AuVmPGSmPlMS37DmEs1UIOeAZpSFj8s9jweUDbSqbkBnY6+5tBI43MxqDtIRMhK1+zDoaRxyvR2HErks2hf1wVoDnwpVACi893tvhSKQ0yfKUfkdpqm8aQM7AU/22ZGj5zav6RtxoX+np/7rzfET0fHzCSaQS1d6/TeJaQ5rMPX13Bgu0CenuOD1rvVNXXPMD5d887Kd+y/kz4OVUH0/xkjua5LHsWwDroC5AwzQI/EsWbcJx+xppihKNdMboAr51dALDuWLeSYzswPoJZWppv9D0WbjLohiZU04gmys7JdLn1cclz5MvIinLNFK//adQHrISqDFJqBygni8k90A0BNwSjdVzf9f4bYtzwS5qFRNgYtONyb380cpFZtMMKLdLRne1K8Y4FljzaJztcbm5qBQCLjAaK+C405tUDddDToCyyGsO6Zxf7yzhlSXNTWv/qRgKx91w5pJNyACgYnspxr+xOX+6ewchZZi07uPOPwVrxoAkuIrkiJtUe" ================================================ FILE: games/freehl.fmf ================================================ //Mostly just defers to the engine's `-halflife` rules. FTEMANIFEST 1 GAME halflife NAME "Half-Life (FreeHL)" ================================================ FILE: games/hexen2-demo.fmf ================================================ //Note - the hexen2 demo is an unmaintained variation of an earlier build, incompatible with the later game. Any basedirs must be entirely separate. game hexen2 name "Hexen2 Demo" package "data/pak0.pak" crc 0x3bbcb56c mirror "unzip:H2Demo/Install/Hexen2/data1/pak0.pak,https://www.quaddicted.com/files/idgames2/idstuff/hexen2/H2Demo.exe" ================================================ FILE: games/ktx.fmf ================================================ //this file is intended for dedicated servers, but I guess clients might have reason to use it too. expect quirks though. FTEMANIFEST 1 game quake name "nQuakeSV-KTX" gamedir ktx homedirmode never //auto/always downloadsurl https://fte.triptohell.info/moodles/ktx.meta install quake_shareware //we need(ish) a pak0! install nquakesv_configs //ktx breaks without some configs install nquakesv_gamecode //can't run ktx without ktx! //install nquakesv_maps //urgh, massive bloated download. //gah, make sure stuff still works when embedded in a browser. archivedpackage id1/pak0.pak 0x4f069cac id1/pak0.pak https://fte.triptohell.info/qsw106.zip ================================================ FILE: games/quake-demo.fmf ================================================ //This can skip most of its details as the important stuff is baked into the engine and its best to avoid dupes. //View some other file for a real example. //Registered Quake is a strict superset of the shareware version, so we don't really need to care about whether its a demo or not. FTEManifestVer 1 game quake name Quake //the shareware license requires distribution in whole... so we'll just download the whole shareware zip and extract only what we need - assuming the user does not already have a copy. archivedpackage id1/pak0.pak 0x4f069cac id1/pak0.pak https://fte.triptohell.info/moodles/live/QUAKE_SW.zip ================================================ FILE: games/xonotic_85.fmf ================================================ //-prefixed lines are effectively inserted before the default.cfg //+prefixed lines are inserted AFTER default.cfg and will thus conflict/override the mod's own settings game xonotic name "FTEized Xonotic (0.8.5)" protocolname "Xonotic" basegame data basegame *ftedata //so stuff gets written here instead. //xonotic 0.8.5 packages. //package "data/font-unifont-20220627.pk3" crc 0xa39ce3ad mirror "unzip:Xonotic/data/font-unifont-20220627.pk3,https://github.com/garymoon/xonotic/releases/download/xonotic-v0.8.5/xonotic-0.8.5.zip" //package "data/font-xolonium-20220627.pk3" crc 0x9553d8a4 mirror "unzip:Xonotic/data/font-xolonium-20220627.pk3,https://github.com/garymoon/xonotic/releases/download/xonotic-v0.8.5/xonotic-0.8.5.zip" //package "data/xonotic-20220627-data.pk3" crc 0x57a1ba9c mirror "unzip:Xonotic/data/xonotic-20220627-data.pk3,https://github.com/garymoon/xonotic/releases/download/xonotic-v0.8.5/xonotic-0.8.5.zip" //package "data/xonotic-20220627-maps.pk3" crc 0x1d3d7cf1 mirror "unzip:Xonotic/data/xonotic-20220627-maps.pk3,https://github.com/garymoon/xonotic/releases/download/xonotic-v0.8.5/xonotic-0.8.5.zip" //package "data/xonotic-20220627-music.pk3" crc 0x5d1dd373 mirror "unzip:Xonotic/data/xonotic-20220627-music.pk3,https://github.com/garymoon/xonotic/releases/download/xonotic-v0.8.5/xonotic-0.8.5.zip" //package "data/xonotic-20220627-nexcompat.pk3" crc 0x83f613b9 mirror "unzip:Xonotic/data/xonotic-20220627-nexcompat.pk3,https://github.com/garymoon/xonotic/releases/download/xonotic-v0.8.5/xonotic-0.8.5.zip" //This sucks. overrides the *.dat files to work around gmqcc bugs (also smaller dat files at runtime, but still stupid and annoying and means compat hits the fan when using the standard csprogs on dp servers) // cd qcsrc && make QCC=fteqcc QCCFLAGS_WERROR= && zip xonotic-fixups-9.8.5.pk3 ../*.dat ../*.lno //package "data/xonotic-fixups-0.8.5.pk3" crc 0xe27b8ad3 //mirror "xonotic-fixups-0.8.5.pk3" //-set pr_fixbrokenqccarrays 2 //this can be used instead, but can cause its own problems. -set dpcompat_set 1 //gah -set dpcompat_console 1 //DP's $ stuff works differently from quakeworld. definitely more annoying, but xonotic's configs expect dp behaviour -set dpcompat_smallerfonts 1 //in case its needed. -set dpcompat_strcat_limit 16383 //work around xonotic network compatibility issue. -set v_gammainverted 1 -set con_stayhidden 0 -set allow_download_pakcontents 1 -set allow_download_refpackages 0 -set sv_bigcoords "" -set map_autoopenportals 1 -set sv_port 26000 -set cl_defaultport 26000 -set r_particlesdesc effectinfo -if ($dedicated < 1) then set qport 654 //xonotic expects this cvar to exist only in clients (and uses it to detect client vs dedicated server). this entirely ignores 'setrenderer sv' of course. good luck with that one. -set gl_info_extensions "GL_EXT_texture_compression_s3tc GL_ARB_texture_compression" //fte doesn't have a cvar that contains opengl extensions, in part because they don't apply to other renderers thus making it kinda useless. and xonotic likes spamming warning messages, even though they're invalid half the time, and impossible for a user to work around the rest of the time. -set cl_movement 1 //xonotic's physics are inconsistent with itself, but it judders even without prediction so we might as well enable it -set cl_movement_replay "" //just to silence spam. actual value doesn't change anything in fte, and xonotic keeps changing it randomly anyway. -set sv_nqplayerphysics 0 //xonotic runs async player physics. we've no need to force anything. -set pr_enable_uriget 0 //enabling this causes xonotic's menu to get pissy about updates, which probably won't work very well with fte, so best to block all that crap. sadly this also breaks the stats stuff. -set cl_lerp_smooth 0 //don't run in the past. DP has nothing like that the whole servertime/serverprevtime stuff will just get confused like hell. -set r_shadow_realtime_nonworld_lightmaps 2 //DP's q3bsp lighting is doubled relative to q3, for some reason. -set con_chatsize 8 //doesn't exist in FTE, resulting in qc division by 0 if not set. -set con_textsize 12 //keep the console sized at a fixed 12pt point, regardless of actual res. //con_stayhidden 1 //don't pop up the console randomly. -set dpcompat_makeshitup 2 //ignore most of what the shaders are saying and just make shit up, adding deluxe+specular+fullbrights+reflectcube etc. -set dpcompat_findradiusarealinks 1 //matches dp. should help performance. -set sv_gameplayfix_spawnbeforethinks 1 //some nq mods actually break without this glitch. DP forces a fix, so match that behaviour because xonotic needs it. //I copied this list out of the dp sourcecode. I don't know if xonotic needs them, but it normally has them anyway. //fte doesn't even implement them all either, so that's fun. still, if it ever does then they'll at least get their expected values. -set sv_gameplayfix_blowupfallenzombies 1 -set sv_gameplayfix_findradiusdistancetobox 1 -set sv_gameplayfix_grenadebouncedownslopes 1 -set sv_gameplayfix_slidemoveprojectiles 1 -set sv_gameplayfix_upwardvelocityclearsongroundflag 1 -set sv_gameplayfix_setmodelrealbox 1 -set sv_gameplayfix_droptofloorstartsolid 1 -set sv_gameplayfix_droptofloorstartsolid_nudgetocorrect 1 -set sv_gameplayfix_noairborncorpse 1 -set sv_gameplayfix_noairborncorpse_allowsuspendeditems 1 -set sv_gameplayfix_easierwaterjump 1 -set sv_gameplayfix_delayprojectiles 1 -set sv_gameplayfix_multiplethinksperframe 1 -set sv_gameplayfix_fixedcheckwatertransition 1 -set sv_gameplayfix_q1bsptracelinereportstexture 1 -set sv_gameplayfix_swiminbmodels 1 -set sv_gameplayfix_downtracesupportsongroundflag 1 -set sv_gameplayfix_q2airaccelerate 0 //most of these cvars are not defined by FTE, but xonotic expects them anyway. so make sure they're there for the pmove code. -set sv_jumpvelocity 270 -set sv_maxairspeed 30 -set sv_nostep 0 -set sv_jumpstep 1 -set sv_wateraccelerate -1 -set sv_waterfriction -1 -set sv_airaccel_sideways_friction 0 -set sv_airaccel_qw 1 -set sv_airaccel_qw_stretchfactor 0 -set sv_airstopaccelerate 0 -set sv_airstrafeaccelerate 0 -set sv_maxairstrafespeed 0 -set sv_airstrafeaccel_qw 0 -set sv_aircontrol 0 -set sv_aircontrol_penalty 0 -set sv_aircontrol_power 2 -set sv_aircontrol_backwards 0 -set sv_airspeedlimit_nonqw 0 -set sv_warsowbunny_turnaccel 0 -set sv_warsowbunny_accel 0.1585 -set sv_warsowbunny_topspeed 925 -set sv_warsowbunny_backtosideratio 0.8 -set sv_airaccelerate -1 //dp's default... to mean defer to sv_accelerate. //all these cvars are implemented using a custom conback shader. -set scr_conalphafactor 1 -set scr_conalpha2factor 0 -set scr_conalpha3factor 0 -set scr_conbrightness 1 -set scr_conscroll_x 0 -set scr_conscroll_y 0 -set scr_conscroll2_x 0 -set scr_conscroll2_y 0 -set scr_conscroll3_x 0 -set scr_conscroll3_y 0 -set r_textcontrast 1 //fixes colourpicker widget -set com_parseutf8 1 //fte's name. interpret text as utf-8 when printing. -set utf8_enable 1 //not fte's cvar name. have the qc builtins operate on codepoints rather than bytes (slower, still unaware of emoji, accents, etc). -alias playermodel model //FTE uses userinfo stuff for this stuff -alias playerskin skin //FTE uses userinfo stuff for this stuff -set pr_csqc_memsize "64m" //xonotic is shit and inefficient. it really does need FAR too much memory. -set pr_ssqc_memsize "96m" //xonotic is shit and inefficient. it really does need FAR too much memory. -set r_deluxemapping 1 //load deluxemaps, cos they're a little prettier -set dpcompat_nopremulpics 1 //0 has problems with dds files etc, which is frankly a shame. //this won't force the menu to use a less laggy cursor, but should at least give the console a little more personality. -set cl_cursor "gfx/menu/luma/cursor.jpg" -set cl_cursor_scale 0.333 -set cl_cursor_bias_x 10 -set cl_cursor_bias_y 3.33 -set vid_gl20 1 //make sure various menu options are not greyed out -set gl_specular 1 //DP's default, that makes things too shiny (because if you're going to make a texture then sadly most people want it to be seen) -set dpcompat_corruptglobals 1 //stomp on random qc globals that were meant to be cachable from one frame to the next. -set vid_pixelheight 1 //DP mods have a nasty tendancy to get confused when this cvar doesn't exist. -set s_al_disable 1 //xonotic seems to clog ALL the openal audio buffers with useless sounds, so disable openal to prevent that from happening. -set dpcompat_nopreparse 1 //No unicasts and stuff split over packets mean lengths of custom stuff cannot be determined, resulting in translation of other things failing to translate. -set cl_loopbackprotocol dpp7 //needs to be some sort of nq protocol due to nopreparse. might as well match dp and network player velocity entirely separately from its position... -set sv_listen_dp 1 //listen for dp-protocol client connections -set sv_listen_qw 0 //ignore standard QW clients (they'll just get dp responses which they'll ignore) -set sv_listen_nq 0 //nq-protocol clients will just be ignored. -set sv_bigcoords 1 //kinda required for dpp7 to function correctly. -set r_particlesdesc effectinfo -set sv_mintic 0.0333 //should match dp's rate. -set sv_maxtic 0 //fixed tick rates -set cl_nolerp 0 -set sv_cullentities_trace 0 //still needs work. fte still has performance issues with tracing through patches -set sv_curl_serverpackages "" //not in FTE, but xonotic warns when missing. -set _cl_name "Stalking is illegal" -set pr_autocreatecvars 0 //so we're more likely to notice unknown cvars. //misc cvars that the gamecode checks for for unknowable reasons //set r_texture_dds_load 1 //set vid_desktopfullscreen ${vid_fullscreen==2} //set hud_panel_notify_print //set con_chattime //set con_chatsound //set net_slist_pause //set r_viewfbo ${r_hdr_framebuffer} //set r_depthfirst //set gl_vbo //removed from dp too //set v_glslgamma //set r_glsl_saturation //set r_hdr_scenebrightness //set sys_memsize_virtual //set sys_memsize_physical //set mod_q3bsp_nolightmaps //set r_shadow_gloss ${gl_specular} //set r_water ${r_portalrecursion>0} //set r_water_resolutionmultiplier ${r_reflectrefract_scale} //set cl_decals //set cl_decals_models //set r_drawdecals_drawdistance //set cl_decals_fadetime //set r_shadow_usenormalmap //set r_coronas_occlusionquery //set r_motionblur //set cl_particles //set r_drawparticles_drawdistance //set snd_staticvolume //set snd_channel0volume //set snd_channel3volume //set snd_channel6volume //set snd_channel7volume //set snd_channel4volume //set snd_channel2volume //set snd_channel1volume //set snd_mutewhenidle ${snd_inactive==0} //set snd_speed ${snd_khz} //set snd_channels ${snd_numspeakers} //set snd_swapstereo ${snd_leftisright} //set snd_spatialization_control //set m_accelerate //set con_closeontoggleconsole //set cl_movement_track_canjump //set _cl_rate ${rate} //set cl_curl_maxdownloads //set cl_curl_maxspeed //set shownetgraph ${r_netgraph} //set cl_minfps //set cl_maxidlefps ${cl_idlefps} //set showtime //set cl_capturevideo //set g_campaignxonoticbeta_won //set r_glsl_postprocess //set joy_active //set con_chatrect_x //set con_chatrect_y //set con_chatwidth //set net_slist_favorites //set cl_bobfall //set cl_smoothviewheight -maxplayers 16 //xonotic doesn't like empty to mean ((deathmatch||coop)?32:1), so be explicit. -deathmatch 1 //normally gets set to 1 in dp when maxplayers is forced (coop be damned). -pr_precachepic_slow 1 //xonotic sucks for this. its abuse of drawgetimagesize slows stuff down too. -set mod_precache 2 //disabling this slashes ram usage. there might be some stutter from loading textures, but no outright stalls at least. the prediction misses are worse. -set mod_precache 0 //FIXME: override sv_precacheplayermodels instead. //FIXME: -set _q3bsp_bihtraces 1 //work around q3map2 being so defective and throwing 5000 brushes into a single leaf. -set sv_gameplayfix_setmodelrealbox 0 //setting this to 0 prevents the server from stalling waiting for the thing to load. +set sv_precacheplayermodels 0 //fte has loader threads, and the extra upfront load time is just annoying (override the mod's default). -set s_precache 0 //mneh, sounds are overrated anyway. ================================================ FILE: imgtool.c ================================================ #include "quakedef.h" #include "shader.h" #undef stderr #define stderr stdout #include #include #ifdef _WIN32 #include #endif static qboolean verbose; void VARGS Sys_Error (const char *fmt, ...) { va_list argptr; va_start (argptr,fmt); vfprintf (stderr,fmt,argptr); va_end (argptr); fflush(stderr); exit(1); } void VARGS Con_Printf (const char *fmt, ...) { va_list argptr; va_start (argptr,fmt); vfprintf (stderr,fmt,argptr); va_end (argptr); fflush(stderr); } void VARGS Con_TPrintf (const char *fmt, ...) { va_list argptr; va_start (argptr,fmt); vfprintf (stderr,fmt,argptr); va_end (argptr); fflush(stderr); } void VARGS Con_DPrintf (const char *fmt, ...) { va_list argptr; if (!verbose) return; va_start (argptr,fmt); vfprintf (stderr,fmt,argptr); va_end (argptr); fflush(stderr); } void VARGS Con_ThrottlePrintf (float *timer, int developerlevel, const char *fmt, ...) { va_list argptr; va_start (argptr,fmt); vfprintf (stderr,fmt,argptr); va_end (argptr); fflush(stderr); } void *ZF_Malloc(size_t size) { #if defined(__linux__) void *ret = NULL; if (!posix_memalign(&ret, max(sizeof(float)*4, sizeof(void*)), size)) memset(ret, 0, size); return ret; #else return calloc(size, 1); #endif } void *Z_Malloc(size_t size) { void *r = ZF_Malloc(size); if (!r) exit(1); return r; } void *BZ_Malloc(size_t size) { return Z_Malloc(size); } void *BZF_Malloc(size_t size) { return Z_Malloc(size); } void BZ_Free(void *p) { free(p); } void Z_Free(void *p) { free(p); } #ifdef _WIN32 // don't use these functions in MSVC8 #if (_MSC_VER < 1400) int QDECL linuxlike_snprintf(char *buffer, int size, const char *format, ...) { #undef _vsnprintf int ret; va_list argptr; if (size <= 0) return 0; size--; va_start (argptr, format); ret = _vsnprintf (buffer,size, format,argptr); va_end (argptr); buffer[size] = '\0'; return ret; } int QDECL linuxlike_vsnprintf(char *buffer, int size, const char *format, va_list argptr) { #undef _vsnprintf int ret; if (size <= 0) return 0; size--; ret = _vsnprintf (buffer,size, format,argptr); buffer[size] = '\0'; return ret; } #elif (_MSC_VER < 1900) int VARGS linuxlike_snprintf_vc8(char *buffer, int size, const char *format, ...) { int ret; va_list argptr; va_start (argptr, format); ret = vsnprintf_s (buffer,size, _TRUNCATE, format,argptr); va_end (argptr); return ret; } #endif #endif #include void FS_CreatePath(const char *pname, enum fs_relative relativeto) { char *t = strdup(pname), *sl = t; while ((sl=strchr(sl, '/'))) { *sl=0; #ifdef _WIN32 CreateDirectoryA(t, NULL); #else mkdir(t, 0777); #endif *sl++='/'; } free(t); } qboolean FS_Remove (const char *path, enum fs_relative relativeto) { //remove is part of c89. if (remove(path) == -1) return false; return true; } qboolean FS_SystemPath(const char *fname, enum fs_relative relativeto, char *out, int outlen) { Q_strncpyz(out, fname, outlen); return true; } char *COM_SkipPath (const char *pathname) { const char *last; last = pathname; while (*pathname) { if (*pathname=='/' || *pathname == '\\') last = pathname+1; pathname++; } return (char *)last; } #ifdef __unix__ #include #endif void *FS_LoadMallocFile (const char *path, size_t *fsize) { qbyte *data = NULL; FILE *f; #ifdef __unix__ struct stat sb; if (stat(path, &sb) < 0) return NULL; if ((sb.st_mode&S_IFMT) != S_IFREG) return NULL; #endif f = fopen(path, "rb"); if (f) { long int sz; if (fseek(f, 0, SEEK_END) >= 0) { sz = ftell(f); if (sz >= 0) { *fsize = sz; fseek(f, 0, SEEK_SET); data = ZF_Malloc(*fsize+1); if (data) { data[*fsize] = 0; fread(data, 1, *fsize, f); } else Con_Printf("Unable to allocate memory for %s\n", path); } } fclose(f); } return data; } #ifdef _WIN32 dllhandle_t *Sys_LoadLibrary(const char *name, dllfunction_t *funcs) { return NULL; } #else #include void Sys_CloseLibrary(dllhandle_t *lib) { dlclose((void*)lib); } dllhandle_t *Sys_LoadLibrary(const char *name, dllfunction_t *funcs) { int i; dllhandle_t *lib; lib = NULL; if (!lib) lib = dlopen (name, RTLD_LOCAL|RTLD_LAZY); // if (!lib && !strstr(name, ".so")) // lib = dlopen (va("%s.so", name), RTLD_LOCAL|RTLD_LAZY); if (!lib) { // Con_DPrintf("%s\n", dlerror()); return NULL; } if (funcs) { for (i = 0; funcs[i].name; i++) { *funcs[i].funcptr = dlsym(lib, funcs[i].name); if (!*funcs[i].funcptr) break; } if (funcs[i].name) { Con_DPrintf("Unable to find symbol \"%s\" in \"%s\"\n", funcs[i].name, name); Sys_CloseLibrary((dllhandle_t*)lib); lib = NULL; } } return (dllhandle_t*)lib; } #endif struct imgfile_s { vfsfile_t pub; FILE *f; }; static qboolean QDECL ImgFile_Close(struct vfsfile_s *file) { struct imgfile_s *f = (struct imgfile_s*)file; fclose(f->f); free(f); return true; } static int QDECL ImgFile_WriteBytes(struct vfsfile_s *file, const void *buffer, int bytestowrite) { struct imgfile_s *f = (struct imgfile_s*)file; return fwrite(buffer, 1, bytestowrite, f->f); } static qboolean QDECL ImgFile_Seek(struct vfsfile_s *file, qofs_t newofs) { struct imgfile_s *f = (struct imgfile_s*)file; if (fseek(f->f, newofs, SEEK_SET)==0) return true; //success return false; } static qofs_t QDECL ImgFile_Tell(struct vfsfile_s *file) { struct imgfile_s *f = (struct imgfile_s*)file; return ftell(f->f); } vfsfile_t *QDECL FS_OpenVFS(const char *filename, const char *mode, enum fs_relative relativeto) { if (!strcmp(mode, "wb")) { struct imgfile_s *r = malloc(sizeof(*r)); r->f = fopen(filename, mode); r->pub.seekstyle = SS_UNSEEKABLE; r->pub.Close = ImgFile_Close; r->pub.WriteBytes = ImgFile_WriteBytes; r->pub.Seek = ImgFile_Seek; r->pub.Tell = ImgFile_Tell; if (r->f) return &r->pub; free(r); } return NULL; } qboolean COM_WriteFile (const char *filename, enum fs_relative fsroot, const void *data, int len) { vfsfile_t *f = FS_OpenVFS(filename, "wb", fsroot); qboolean ret = false; if (f) { ret = len==VFS_WRITE(f, data, len); if (!VFS_CLOSE(f)) ret = false; } return ret; } void QDECL Q_strncpyz(char *d, const char *s, int n) { int i; n--; if (n < 0) return; //this could be an error for (i=0; *s; i++) { if (i == n) break; *d++ = *s++; } *d='\0'; } qboolean VARGS Q_vsnprintfz (char *dest, size_t size, const char *fmt, va_list argptr) { size_t ret; #ifdef _WIN32 //doesn't null terminate. //returns -1 on truncation ret = _vsnprintf (dest, size, fmt, argptr); dest[size-1] = 0; //shitty paranoia #else //always null terminates. //returns length regardless of truncation. ret = vsnprintf (dest, size, fmt, argptr); #endif #ifdef _DEBUG if (ret>=size) Sys_Error("Q_vsnprintfz: Truncation\n"); #endif //if ret is -1 (windows oversize, or general error) then it'll be treated as unsigned so really long. this makes the following check quite simple. return ret>=size; } qboolean VARGS Q_snprintfz (char *dest, size_t size, const char *fmt, ...) { va_list argptr; qboolean ret; va_start (argptr, fmt); ret = Q_vsnprintfz(dest, size, fmt, argptr); va_end (argptr); return ret; } //palette data is used in lmps, as well as written into pcxes or wads, probably some other things. qbyte *host_basepal; unsigned int d_8to24rgbtable[256]; unsigned int d_8to24bgrtable[256]; static qbyte default_quakepal[768] = { //the quake palette was released into the public domain (or at least gpl) to ease development of tools writing quake-format data. 0,0,0,15,15,15,31,31,31,47,47,47,63,63,63,75,75,75,91,91,91,107,107,107,123,123,123,139,139,139,155,155,155,171,171,171,187,187,187,203,203,203,219,219,219,235,235,235,15,11,7,23,15,11,31,23,11,39,27,15,47,35,19,55,43,23,63,47,23,75,55,27,83,59,27,91,67,31,99,75,31,107,83,31,115,87,31,123,95,35,131,103,35,143,111,35,11,11,15,19,19,27,27,27,39,39,39,51,47,47,63,55,55,75,63,63,87,71,71,103,79,79,115,91,91,127,99,99, 139,107,107,151,115,115,163,123,123,175,131,131,187,139,139,203,0,0,0,7,7,0,11,11,0,19,19,0,27,27,0,35,35,0,43,43,7,47,47,7,55,55,7,63,63,7,71,71,7,75,75,11,83,83,11,91,91,11,99,99,11,107,107,15,7,0,0,15,0,0,23,0,0,31,0,0,39,0,0,47,0,0,55,0,0,63,0,0,71,0,0,79,0,0,87,0,0,95,0,0,103,0,0,111,0,0,119,0,0,127,0,0,19,19,0,27,27,0,35,35,0,47,43,0,55,47,0,67, 55,0,75,59,7,87,67,7,95,71,7,107,75,11,119,83,15,131,87,19,139,91,19,151,95,27,163,99,31,175,103,35,35,19,7,47,23,11,59,31,15,75,35,19,87,43,23,99,47,31,115,55,35,127,59,43,143,67,51,159,79,51,175,99,47,191,119,47,207,143,43,223,171,39,239,203,31,255,243,27,11,7,0,27,19,0,43,35,15,55,43,19,71,51,27,83,55,35,99,63,43,111,71,51,127,83,63,139,95,71,155,107,83,167,123,95,183,135,107,195,147,123,211,163,139,227,179,151, 171,139,163,159,127,151,147,115,135,139,103,123,127,91,111,119,83,99,107,75,87,95,63,75,87,55,67,75,47,55,67,39,47,55,31,35,43,23,27,35,19,19,23,11,11,15,7,7,187,115,159,175,107,143,163,95,131,151,87,119,139,79,107,127,75,95,115,67,83,107,59,75,95,51,63,83,43,55,71,35,43,59,31,35,47,23,27,35,19,19,23,11,11,15,7,7,219,195,187,203,179,167,191,163,155,175,151,139,163,135,123,151,123,111,135,111,95,123,99,83,107,87,71,95,75,59,83,63, 51,67,51,39,55,43,31,39,31,23,27,19,15,15,11,7,111,131,123,103,123,111,95,115,103,87,107,95,79,99,87,71,91,79,63,83,71,55,75,63,47,67,55,43,59,47,35,51,39,31,43,31,23,35,23,15,27,19,11,19,11,7,11,7,255,243,27,239,223,23,219,203,19,203,183,15,187,167,15,171,151,11,155,131,7,139,115,7,123,99,7,107,83,0,91,71,0,75,55,0,59,43,0,43,31,0,27,15,0,11,7,0,0,0,255,11,11,239,19,19,223,27,27,207,35,35,191,43, 43,175,47,47,159,47,47,143,47,47,127,47,47,111,47,47,95,43,43,79,35,35,63,27,27,47,19,19,31,11,11,15,43,0,0,59,0,0,75,7,0,95,7,0,111,15,0,127,23,7,147,31,7,163,39,11,183,51,15,195,75,27,207,99,43,219,127,59,227,151,79,231,171,95,239,191,119,247,211,139,167,123,59,183,155,55,199,195,55,231,227,87,127,191,255,171,231,255,215,255,255,103,0,0,139,0,0,179,0,0,215,0,0,255,0,0,255,243,147,255,247,199,255,255,255,159,91,83 }; qbyte GetPaletteIndexNoFB(int red, int green, int blue) { int i; int best=0; int bestdist=INT_MAX; int dist; for (i = 0; i < 256-32; i++) { const int diff[3] = { host_basepal[i*3+0]-red, host_basepal[i*3+1]-green, host_basepal[i*3+2]-blue}; dist = DotProduct(diff,diff); if (dist < bestdist) { bestdist = dist; best = i; if (!dist) break; } } return best; } qbyte GetPaletteIndexRange(int first, int stop, int red, int green, int blue) { int i; int best=0; int bestdist=INT_MAX; int dist; for (i = first; i < stop; i++) { const int diff[3] = { host_basepal[i*3+0]-red, host_basepal[i*3+1]-green, host_basepal[i*3+2]-blue}; dist = DotProduct(diff,diff); if (dist < bestdist) { bestdist = dist; best = i; if (!dist) break; } } return best; } const char *palette = NULL; sh_config_t sh_config; viddef_t vid; void ImgTool_SetupPalette(void) { int i; FILE *fPAL; static qbyte cust_pal[768]; host_basepal = default_quakepal; if (palette) { fPAL = fopen(palette, "rb"); if (fPAL != NULL) { Con_Printf("using user-specified palette for files using palette.lmp\n"); fread(cust_pal, 1, 768, fPAL); fclose(fPAL); host_basepal = cust_pal; } else Con_Printf("cannot find palette file %s\n", palette); } else Con_Printf("using built-in Quake palette for files using palette.lmp\n"); for (i = 0; i < 256; i++) { d_8to24rgbtable[i] = (host_basepal[i*3+0]<<0)|(host_basepal[i*3+1]<<8)|(host_basepal[i*3+2]<<16); d_8to24bgrtable[i] = (host_basepal[i*3+0]<<16)|(host_basepal[i*3+1]<<8)|(host_basepal[i*3+2]<<0); } sh_config.texture2d_maxsize = 1u<<31; sh_config.texture3d_maxsize = 1u<<31; sh_config.texture2darray_maxlayers = 1u<<31; sh_config.texturecube_maxsize = 8192; sh_config.texture_non_power_of_two = true; sh_config.texture_non_power_of_two_pic = true; sh_config.texture_allow_block_padding = true; sh_config.npot_rounddown = true; //shouldn't be relevant sh_config.havecubemaps = true; //I don't think this matters. Image_Init(); } #ifdef IMGTOOL static void ImgTool_FreeMips(struct pendingtextureinfo *mips) { size_t i; if (mips) { for (i = 0; i < mips->mipcount; i++) if (mips->mip[i].needfree) BZ_Free(mips->mip[i].data); if (mips->extrafree) BZ_Free(mips->extrafree); BZ_Free(mips); } } typedef struct { unsigned int offset; // Position of the entry in WAD unsigned int dsize; // Size of the entry in WAD file unsigned int size; // Size of the entry in memory char type; // type of entry char cmprs; // Compression. 0 if none. short dummy; // Not used char name[16]; // we use only first 8 } wad2entry_t; typedef struct { char magic[4]; //should be WAD2 unsigned int num; //number of entries unsigned int offset; //location of directory } wad2_t; static const char *imagetypename[] = {"2D", "3D", "Cube", "2DArray", "CubemapArray", "INVALID", "INVALID", "INVALID", "INVALID", "INVALID"}; struct opts_s { int textype; const char *defaultext; //.dds or whatever when the output's extension is not explicitly given. unsigned int flags; //image flags to use (affects how textures get interpreted a little) unsigned int mipnum; //when exporting to a mipless format, this is the mip level that is actually written. default 0. uploadfmt_t newpixelformat; //try to convert to this pixel format on export. int width, height; }; static qboolean ImgTool_MipExport(struct opts_s *args, vfsfile_t *outfile, struct pendingtextureinfo *in, const char *mipname, int wadtype); static struct pendingtextureinfo *ImgTool_DecodeMiptex(struct opts_s *args, miptex_t *mip, size_t fsize, qbyte *basepal); void Image_GenerateMips(struct pendingtextureinfo *mips, unsigned int flags); int Image_WritePNG (const char *filename, enum fs_relative fsroot, int compression, void **buffers, int numbuffers, qintptr_t bufferstride, int width, int height, enum uploadfmt fmt, qboolean writemetadata); qboolean WriteTGA(const char *filename, enum fs_relative fsroot, const qbyte *fte_restrict rgb_buffer, qintptr_t bytestride, int width, int height, enum uploadfmt fmt); static enum uploadfmt ImgTool_ASTCToLDR(uploadfmt_t fmt) { if (fmt >= PTI_ASTC_FIRST && fmt <= PTI_ASTC_LAST) { if (fmt >= PTI_ASTC_4X4_HDR) return (fmt-PTI_ASTC_4X4_HDR)+PTI_ASTC_4X4_LDR; if (fmt >= PTI_ASTC_4X4_SRGB) return (fmt-PTI_ASTC_4X4_SRGB)+PTI_ASTC_4X4_LDR; } if (fmt == PTI_BC1_RGB) return PTI_BC1_RGBA; return fmt; } #ifdef _WIN32 static void FS_MakeTempName(char *out, size_t outsize, char *prefix, char *suffix) { static char temp_path[MAX_PATH]; char temp_file_name[MAX_PATH]; if (!*temp_path && !GetTempPathA(sizeof(temp_path), temp_path)) Sys_Error("FS_MakeTempName failed to get temp path\n"); if (!GetTempFileNameA(temp_path, prefix, 0, temp_file_name)) Sys_Error("FS_MakeTempName failed\n"); Q_snprintfz(out, outsize, "%s%s", temp_file_name, suffix); } #else #include static void FS_MakeTempName(char *out, size_t outsize, char *prefix, char *suffix) { snprintf(out, outsize, "/tmp/%sXXXXXX%s", prefix, suffix); close(mkstemps(out, strlen(suffix))); //bsd4.3/posix1-2001 } #endif static qboolean ImgTool_HasAlpha(struct pendingtextureinfo *mips) { if (mips->encoding == PTI_RGBA8 || mips->encoding == PTI_BGRA8 || mips->encoding == PTI_LLLA8 || mips->encoding == PTI_RGBA8_SRGB || mips->encoding == PTI_BGRA8_SRGB) { size_t l = 0, pixels, p; qbyte *d; for (l = 0; l < mips->mipcount; l++) { pixels = mips->mip[l].width * mips->mip[l].height * mips->mip[l].depth * 4; d = mips->mip[l].data; d+=3; for (p = 0; p < pixels; p+=4) if (d[p] != 255) return true; //a transparent pixel! } return false; } else if (mips->encoding == PTI_L8A8 || mips->encoding == PTI_L8A8_SRGB) { size_t l = 0, pixels, p; qbyte *d; for (l = 0; l < mips->mipcount; l++) { pixels = mips->mip[l].width * mips->mip[l].height * mips->mip[l].depth * 2; d = mips->mip[l].data; d+=1; for (p = 0; p < pixels; p+=2) if (d[p] != 255) return true; //a transparent pixel! } return false; } else if (mips->encoding == PTI_RGBA16) { size_t l = 0, pixels, p; unsigned short *d; for (l = 0; l < mips->mipcount; l++) { pixels = mips->mip[l].width * mips->mip[l].height * mips->mip[l].depth * 4; d = mips->mip[l].data; d+=3; for (p = 0; p < pixels; p+=4) if (d[p] != 0xffff) return true; //a transparent pixel! } return false; } else return Image_FormatHasAlpha(mips->encoding); } //copys all the data out and everything! static struct pendingtextureinfo *ImgTool_DupeMipchain(struct pendingtextureinfo *src) { struct pendingtextureinfo *dest; qbyte *data; size_t size = 0; size_t m; for(m = 0; m < src->mipcount; m++) size += src->mip[m].datasize; dest = Z_Malloc(sizeof(*dest)+size); *dest = *src; data = (qbyte*)(dest+1); for(m = 0; m < src->mipcount; m++) { dest->mip[m].data = data; dest->mip[m].needfree = false; memcpy(data, src->mip[m].data, src->mip[m].datasize); data += src->mip[m].datasize; } dest->extrafree = NULL; //part of the texinfo itself. return dest; } static qboolean ImgTool_ConvertPixelFormat(struct opts_s *args, const char *inname, struct pendingtextureinfo *mips) { struct pendingtextureinfo tmp, *ret; size_t m; char raw[MAX_OSPATH]; char comp[MAX_OSPATH]; char command[MAX_OSPATH*3]; qbyte *fdata; size_t fsize; int bb,bw,bh,bd; qboolean canktx = false; uploadfmt_t targfmt = args->newpixelformat; int d,l, layers, r; //force it to bc1 if bc2 or bc3 with no alpha channel. if ((targfmt == PTI_BC2_RGBA || targfmt == PTI_BC3_RGBA) && !ImgTool_HasAlpha(mips)) targfmt = PTI_BC1_RGB; if (targfmt >= PTI_ASTC_FIRST && targfmt <= PTI_ASTC_LAST) { Q_snprintfz(command, sizeof(command), "astcenc -c"); canktx = true; } else if (targfmt == PTI_BC1_RGB) Q_snprintfz(command, sizeof(command), "nvcompress -bc1%s", (args->flags&IF_TRYBUMP)?"n":""); else if (targfmt == PTI_BC1_RGB_SRGB) Q_snprintfz(command, sizeof(command), "nvcompress -bc1%s -srgb -dds10", (args->flags&IF_TRYBUMP)?"n":""); else if (targfmt == PTI_BC1_RGBA) Q_snprintfz(command, sizeof(command), "nvcompress -bc1a"); else if (targfmt == PTI_BC1_RGBA_SRGB) Q_snprintfz(command, sizeof(command), "nvcompress -bc1a -srgb -dds10"); else if (targfmt == PTI_BC2_RGBA) Q_snprintfz(command, sizeof(command), "nvcompress -bc2"); else if (targfmt == PTI_BC2_RGBA_SRGB) Q_snprintfz(command, sizeof(command), "nvcompress -bc2 -srgb -dds10"); else if (targfmt == PTI_BC3_RGBA) Q_snprintfz(command, sizeof(command), "nvcompress -bc3%s", (args->flags&IF_TRYBUMP)?"n":""); else if (targfmt == PTI_BC3_RGBA_SRGB) Q_snprintfz(command, sizeof(command), "nvcompress -bc3%s -srgb -dds10", (args->flags&IF_TRYBUMP)?"n":""); else if (targfmt == PTI_BC4_R) Q_snprintfz(command, sizeof(command), "nvcompress -bc4"); else if (targfmt == PTI_BC5_RG) Q_snprintfz(command, sizeof(command), "nvcompress -bc5"); else if (targfmt == PTI_BC6_RGB_SFLOAT || targfmt == PTI_BC6_RGB_UFLOAT) Q_snprintfz(command, sizeof(command), "nvcompress -bc6"); else if (targfmt == PTI_BC7_RGBA) Q_snprintfz(command, sizeof(command), "nvcompress -bc7"); else if (targfmt == PTI_BC7_RGBA_SRGB) Q_snprintfz(command, sizeof(command), "nvcompress -bc7 -srgb"); else { if (mips->encoding != targfmt) { qboolean forceformats[PTI_MAX]; for (m = 0; m < PTI_MAX; m++) forceformats[m] = (m == targfmt); Image_ChangeFormat(mips, forceformats, PTI_INVALID, inname); if (mips->encoding == targfmt) return true; //switch to common formats... for (m = 0; m < PTI_MAX; m++) forceformats[m] = (m == targfmt) || (m==PTI_RGBA8) || (m==PTI_RGBA32F); Image_ChangeFormat(mips, forceformats, PTI_INVALID, inname); //and try again... for (m = 0; m < PTI_MAX; m++) forceformats[m] = (m == targfmt); Image_ChangeFormat(mips, forceformats, PTI_INVALID, inname); return (mips->encoding == targfmt); } return true; } if (canktx) FS_MakeTempName(raw, sizeof(raw), "itr", ".ktx"); else FS_MakeTempName(raw, sizeof(raw), "itr", ".png"); FS_MakeTempName(comp, sizeof(comp), "itc", ".ktx"); tmp.type = mips->type; tmp.encoding = mips->encoding; tmp.extrafree = NULL; tmp.mipcount = 1; Image_BlockSizeForEncoding(targfmt, &bb, &bw, &bh, &bd); Q_snprintfz(command+strlen(command), sizeof(command)-strlen(command), " \"%s\" \"%s\"", raw, comp); if (targfmt >= PTI_ASTC_FIRST && targfmt <= PTI_ASTC_LAST) { if (bd!=1) Q_snprintfz(command+strlen(command), sizeof(command)-strlen(command), " %ix%ix%i -exhaustive", bw, bh, bd); else Q_snprintfz(command+strlen(command), sizeof(command)-strlen(command), " %ix%i -exhaustive", bw, bh); } if (targfmt >= PTI_ASTC_4X4_SRGB && targfmt <= PTI_ASTC_12X12_SRGB) Q_strncatz(command, " -srgb", sizeof(command)); if (targfmt >= PTI_ASTC_4X4_HDR && targfmt <= PTI_ASTC_12X12_HDR) Q_strncatz(command, " -hdr", sizeof(command)); if (targfmt >= PTI_BC1_RGB && targfmt <= PTI_BC7_RGBA_SRGB && (strstr(inname, "_n.")||strstr(inname, "_norm."))) Q_strncatz(command, " -normal", sizeof(command)); //looks like a normalmap... tweak metrics to favour normalised results. #ifdef _WIN32 Q_strncatz(command, "> NUL 2>&1", sizeof(command)); #else Q_strncatz(command, ">> /dev/null", sizeof(command)); #endif if (!canktx) { qboolean allowformats[PTI_MAX]; //make sure the source pixel format is acceptable if we're forced to write a png for (m = 0; m < PTI_MAX; m++) allowformats[m] = (m == PTI_RGBA8) || (m == PTI_RGBX8) || (m == PTI_BGRA8) || (m == PTI_BGRX8) || (m == PTI_LLLA8) || (m == PTI_LLLX8) || (m == PTI_RGBA16) || (m == PTI_L8) || (m == PTI_L8A8) || /*(m == PTI_L16) ||*/ (m == PTI_BGR8) || (m == PTI_BGR8) || 0; Image_ChangeFormat(mips, allowformats, PTI_INVALID, inname); } // Con_Printf("%s: Compressing %u mips\n", inname, mips->mipcount); Image_BlockSizeForEncoding(mips->encoding, &bb, &bw, &bh, &bd); for (m = 0; m < mips->mipcount; m++) { qbyte *srcdata = mips->mip[m].data; size_t srcsize = mips->mip[m].datasize; if (mips->type == PTI_3D) { layers = 1; d = mips->mip[m].depth; tmp.type = PTI_2D; } else { layers = mips->mip[m].depth; d = 1; tmp.type = PTI_2D; } for (l = 0; l < layers; l++) { // Con_DPrintf("Compressing %s mip %u, layer %u\n", inname, (unsigned)m, l); tmp.mip[0] = mips->mip[m]; tmp.mip[0].needfree = false; tmp.mip[0].depth = d; tmp.mip[0].datasize = srcsize/layers; tmp.mip[0].data = srcdata + l * tmp.mip[0].datasize; (void)tmp; if (canktx) { #ifdef IMAGEFMT_KTX if (!Image_WriteKTXFile(raw, FS_SYSTEM, &tmp)) #endif break; } else { #ifdef AVAIL_PNGLIB if (!Image_WritePNG(raw, FS_SYSTEM, 0, &tmp.mip[0].data, 1, tmp.mip[0].width*bb, tmp.mip[0].width, tmp.mip[0].height, tmp.encoding, false)) #endif break; } r = system(command); if (r != EXIT_SUCCESS) { Con_Printf("The following system command failed with code %i: %s\n", r, command); break; } fdata = FS_LoadMallocFile(comp, &fsize); ret = Image_LoadMipsFromMemory(IF_NOMIPMAP, comp, comp, fdata, fsize); if (ret && ret->mip[0].width == mips->mip[m].width && ret->mip[0].height == mips->mip[m].height && ret->mip[0].depth == d && ImgTool_ASTCToLDR(ret->encoding) == ImgTool_ASTCToLDR(targfmt)) { if (layers == 1) //just copy it over. FIXME: memory leak mips->mip[m] = ret->mip[0]; else { if (!l) { mips->mip[m].datasize = ret->mip[0].datasize * layers; mips->mip[m].data = BZ_Malloc(mips->mip[m].datasize); mips->mip[m].needfree = true; } else if (ret->mip[0].datasize != mips->mip[m].datasize/layers) break; //erk..? memcpy((qbyte*)mips->mip[m].data + l * ret->mip[0].datasize, ret->mip[0].data, ret->mip[0].datasize); } continue; } else if (!ret) Con_Printf("Failed to read intermediate file %s\n", comp); else Con_Printf("intermediate file %s has unexpected size/depth/format %s:%i*%i*%i vs %s:%i*%i*%i\n", comp, Image_FormatName(ret->encoding), ret->mip[0].width, ret->mip[0].height, ret->mip[0].depth, Image_FormatName(targfmt), mips->mip[m].width, mips->mip[m].height, d); break; } if (l != layers) break; } mips->encoding = targfmt; mips->mipcount = m; if (mips->mipcount && targfmt >= PTI_BC1_RGB && targfmt <= PTI_BC7_RGBA_SRGB) { //d3d has some annoying limitations. //do not warn for astc files, their block sizes are too weird. Image_BlockSizeForEncoding(targfmt, &bb, &bw, &bh, &bd); if (mips->mip[0].width%bw || mips->mip[0].height%bh || mips->mip[0].depth%bd) Con_Printf("%s: mip0 of %i*%i is not a multiple of %i*%i (d3d warning)\n", inname, mips->mip[0].width, mips->mip[0].height, bw, bh); } FS_Remove(raw, FS_SYSTEM); FS_Remove(comp, FS_SYSTEM); return true; } const char *COM_GetFileExtension (const char *in, const char *term) { const char *dot; if (!term) term = in + strlen(in); for (dot = term-1; dot >= in && *dot != '/' && *dot != '\\'; dot--) { if (*dot == '.') return dot; } return term+strlen(term); } static struct pendingtextureinfo *ImgTool_Read(struct opts_s *args, const char *inname) { qbyte *indata; size_t fsize; struct pendingtextureinfo *in; indata = FS_LoadMallocFile(inname, &fsize); if (!indata) Con_Printf("%s: unable to read\n", inname); else { const char *ex = COM_GetFileExtension(inname, NULL); if (!strcasecmp(ex, ".mip")) in = ImgTool_DecodeMiptex(args, (miptex_t*)indata, fsize, NULL); else in = Image_LoadMipsFromMemory(args->flags|IF_NOMIPMAP, inname, inname, indata, fsize); if (!in) { Con_Printf("%s: unsupported format\n", inname); BZ_Free(indata); } else { Con_DPrintf("%s: %s %s, %i*%i, %i mips\n", inname, imagetypename[in->type], Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height, in->mipcount); return in; } } return NULL; } static struct pendingtextureinfo *ImgTool_Combine(struct opts_s *args, const char **namelist, unsigned int filecount) { struct pendingtextureinfo *r, *t; unsigned int i; unsigned int layers = 0; unsigned int j = 0; struct { const char *fname; struct pendingtextureinfo *in; } *srcs, *tmpsrcs; if (args->textype == PTI_3D) args->flags |= IF_NOMIPMAP; //generate any mipmaps after... srcs = alloca(sizeof(*srcs)*filecount); for (i = 0, j = 0; i < filecount; i++) { srcs[j].in = t = ImgTool_Read(args, namelist[i]); if (srcs[j].in) { if (!j) { //get the image loader to massage pixel formats... memset(sh_config.texfmt, 0, sizeof(sh_config.texfmt)); sh_config.texfmt[srcs[j].in->encoding] = true; } else if (!t->mipcount || !t->mip[0].data) { Con_Printf("%s: no valid image data\n", namelist[i]); ImgTool_FreeMips(srcs[j].in); continue; } else if (t->encoding != srcs[0].in->encoding) { Con_Printf("%s: mismatched pixel format, (%s not %s) cannot combine\n", namelist[i], Image_FormatName(t->encoding), Image_FormatName(srcs[0].in->encoding)); ImgTool_FreeMips(srcs[j].in); continue; } else if (t->type == PTI_CUBE && t->mip[0].depth != 6) { Con_Printf("%s: incorrect cubemap data\n", namelist[i]); ImgTool_FreeMips(srcs[j].in); continue; } else if (srcs[0].in->mip[0].width != t->mip[0].width || srcs[0].in->mip[0].height != t->mip[0].height) { Con_Printf("%s: incorrect image size\n", namelist[i]); ImgTool_FreeMips(srcs[j].in); continue; } else if (t->mip[0].depth == 0) { Con_Printf("%s: no layers\n", namelist[i]); ImgTool_FreeMips(srcs[j].in); continue; } layers += t->mip[0].depth; srcs[j++].fname = namelist[i]; } } filecount = j; //FIXME: reorder input images to handle ft/bk/lt/rt/up/dn, and flip+rotate+etc to match quake if (args->textype == PTI_CUBE) { int facetype[6]; static const struct {qboolean flipx, flipy, flipd;} skyboxflips[] ={ {true, false, true}, {false, true, true}, {true, true, false}, {false, false, false}, {true, false, true}, {true, false, true} }; for (i = 0; i < 6; i++) facetype[i] = 0; for (i = 0; i < filecount; i++) { const char *ex = COM_GetFileExtension(srcs[i].fname, NULL); if (ex && ex-srcs[i].fname > 2 && srcs[i].in->mip[0].depth == 1) { if (!strncasecmp(ex-2, "rt", 2)) facetype[0] = -i-1; else if (!strncasecmp(ex-2, "lf", 2)) facetype[1] = -i-1; else if (!strncasecmp(ex-2, "ft", 2)) facetype[2] = -i-1; else if (!strncasecmp(ex-2, "bk", 2)) facetype[3] = -i-1; else if (!strncasecmp(ex-2, "up", 2)) facetype[4] = -i-1; else if (!strncasecmp(ex-2, "dn", 2)) facetype[5] = -i-1; else if (!strncasecmp(ex-2, "px", 2)) facetype[0] = i+1; else if (!strncasecmp(ex-2, "nx", 2)) facetype[1] = i+1; else if (!strncasecmp(ex-2, "py", 2)) facetype[2] = i+1; else if (!strncasecmp(ex-2, "ny", 2)) facetype[3] = i+1; else if (!strncasecmp(ex-2, "pz", 2)) facetype[4] = i+1; else if (!strncasecmp(ex-2, "nz", 2)) facetype[5] = i+1; } } if (facetype[0] && facetype[1] && facetype[2] && facetype[3] && facetype[4] && facetype[5]) { Con_Printf("Reordering images to match cubemap\n"); tmpsrcs = alloca(sizeof(*tmpsrcs)*filecount); memcpy(tmpsrcs, srcs, sizeof(*tmpsrcs)*filecount); for (i = 0; i < 6; i++) { if (facetype[i] < 0) { //flip to match legacy skyboxes unsigned bb,bw,bh,bd; srcs[i] = tmpsrcs[-facetype[i]-1]; t = srcs[i].in; Image_BlockSizeForEncoding(t->encoding, &bb,&bw,&bh,&bd); if (bw == 1 && bh == 1 && bd == 1) { for (j = 0; j < t->mipcount; j++) { void *data = Image_FlipImage(t->mip[j].data, BZ_Malloc(t->mip[j].datasize), &t->mip[j].width, &t->mip[j].height, bb, skyboxflips[i].flipx, skyboxflips[i].flipy, skyboxflips[i].flipd); if (t->mip[j].needfree) Z_Free(t->mip[j].data); t->mip[j].data = data; t->mip[j].needfree = true; } } } else srcs[i] = tmpsrcs[facetype[i]-1]; } } else Con_Printf("WARNING: Cubemap ordering unknown!\n"); } if (!filecount) Con_Printf("no valid input files\n"); else if (!layers) Con_Printf("Images must have at least one layer\n"); else if (args->textype == PTI_2D && layers != 1) Con_Printf("2D images must have one layer exactly, sorry\n"); else if (args->textype == PTI_CUBE && layers != 6) Con_Printf("Cubemaps must have 6 layers exactly\n"); else if (args->textype == PTI_CUBE_ARRAY && layers % 6) Con_Printf("Cubemap arrays must have a multiple of 6 layers exactly\n"); else { t = srcs[0].in; r = Z_Malloc(sizeof(*t)); r->type = args->textype; r->extrafree = NULL; r->encoding = t->encoding; if (args->textype == PTI_3D) r->mipcount = 1; else r->mipcount = t->mipcount; for (j = 0; j < t->mipcount; j++) { r->mip[j].datasize = t->mip[j].datasize*layers; r->mip[j].width = t->mip[j].width; r->mip[j].height = t->mip[j].height; r->mip[j].depth = 0; r->mip[j].needfree = true; r->mip[j].data = BZ_Malloc(r->mip[j].datasize); } for (i = 0, j = 0; i < filecount; i++) { t = srcs[i].in; if (!t) { ImgTool_FreeMips(r); return NULL; } for (j = 0; j < r->mipcount; j++) { if (r->mip[j].width != t->mip[j].width || r->mip[j].height != t->mip[j].height || t->mip[j].depth != 1) { Con_Printf("%s: mismatched mipmap sizes\n", namelist[i]); continue; } memcpy((qbyte*)r->mip[j].data + t->mip[j].datasize*r->mip[j].depth, t->mip[j].data, t->mip[j].datasize); r->mip[j].depth++; } } for (i = 0; i < filecount; i++) ImgTool_FreeMips(srcs[i].in); printf("%s: %s %s, %i*%i, %i mips\n", "combined", imagetypename[r->type], Image_FormatName(r->encoding), r->mip[0].width, r->mip[0].height, r->mipcount); return r; } for (i = 0; i < filecount; i++) ImgTool_FreeMips(srcs[i].in); return NULL; } static void ImgTool_Convert(struct opts_s *args, struct pendingtextureinfo *in, const char *inname, const char *outname) { size_t k; const char *outext; qboolean allowcompressed = false; char newout[MAX_OSPATH]; if (!outname) { outext = COM_GetFileExtension(inname, NULL); k = min(MAX_OSPATH-2-strlen(args->defaultext), outext-inname); memcpy(newout, inname, k); newout[k++] = '.'; strcpy(newout+k, args->defaultext); outname = newout; } outext = COM_GetFileExtension(outname, NULL); if (!strcmp(outext, ".dds") || !strcmp(outext, ".ktx")) allowcompressed = true; if (in) { if (!strcmp(outext, ".ktx") || !strcmp(outext, ".dds") || args->mipnum >= in->mipcount) { if (!(args->flags & IF_NOMIPMAP) && in->mipcount == 1) Image_GenerateMips(in, args->flags); } if (args->mipnum >= in->mipcount) { ImgTool_FreeMips(in); Con_Printf("%s: Requested output mip number was out of bounds %i >= %i\n", outname, args->mipnum, in->mipcount); return; } for (k = 0; k < args->mipnum; k++) { if (in->mip[k].needfree) BZ_Free(in->mip[k].data); } in->mipcount -= k; memmove(in->mip, &in->mip[k], sizeof(in->mip[0])*in->mipcount); Con_Printf("%s(%s)->", inname, Image_FormatName(in->encoding)); if (args->newpixelformat != PTI_INVALID && (args->newpixelformat < PTI_BC1_RGB || allowcompressed) && ImgTool_ConvertPixelFormat(args, inname, in)) Con_Printf("(%s)->", Image_FormatName(in->encoding)); if (!in->mipcount) Con_Printf("%s: no image data\n", inname); else if (!strcasecmp(outext, ".mip")) { vfsfile_t *fs = FS_OpenVFS(outname, "wb", FS_SYSTEM); if (!fs) Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding)); else { if (!ImgTool_MipExport(args, fs, in, outname, 2)) Con_Printf("%s: export failed\n", outname); VFS_CLOSE(fs); } } #ifdef IMAGEFMT_KTX else if (!strcasecmp(outext, ".ktx")) { if (!Image_WriteKTXFile(outname, FS_SYSTEM, in)) Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding)); } #endif #ifdef IMAGEFMT_DDS else if (!strcasecmp(outext, ".dds")) { if (!Image_WriteDDSFile(outname, FS_SYSTEM, in)) Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding)); } #endif else { int bb,bw,bh,bd; if (in->type != PTI_2D) Con_Printf("%s: Unable to write %s file to 2d image format\n", outname, imagetypename[in->type]); #ifdef IMAGEFMT_PNG else if (!strcasecmp(outext, ".png")) { #ifdef AVAIL_PNGLIB qboolean outformats[PTI_MAX]; //force the format, because we can. for (k = 0; k < PTI_MAX; k++) outformats[k] = (k == PTI_RGBA8) || (k == PTI_RGBX8) || (k == PTI_BGRA8) || (k == PTI_BGRX8) || (k == PTI_LLLA8) || (k == PTI_LLLX8) || (k == PTI_RGBA16) || (k == PTI_P8) || (k == PTI_L8) || (k == PTI_L8A8) || /*(k == PTI_L16) ||*/ (k == PTI_BGR8) || (k == PTI_BGR8) || 0; if (!outformats[in->encoding]) Image_ChangeFormat(in, outformats, PTI_INVALID, outname); Image_BlockSizeForEncoding(in->encoding, &bb, &bw,&bh,&bd); if (!Image_WritePNG(outname, FS_SYSTEM, 0, &in->mip[0].data, 1, in->mip[0].width*bb, in->mip[0].width, in->mip[0].height, in->encoding, false)) #endif Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding)); } #endif #ifdef IMAGEFMT_TGA else if (!strcasecmp(outext, ".tga")) { qboolean outformats[PTI_MAX]; for (k = 0; k < PTI_MAX; k++) outformats[k] = (k == PTI_RGBA8) || (k == PTI_RGBX8) || (k == PTI_BGRA8) || (k == PTI_BGRX8) || (k == PTI_LLLA8) || (k == PTI_LLLX8) || (k == PTI_RGBA16F) || (k == PTI_R16F) || //half-float tgas is a format extension, but allow it. (k == PTI_L8) || (k == PTI_L8A8) || /*(k == PTI_L16) ||*/ (k == PTI_BGR8) || (k == PTI_BGR8) || (k == PTI_ARGB1555) || 0; if (!outformats[in->encoding]) Image_ChangeFormat(in, outformats, PTI_INVALID, outname); Image_BlockSizeForEncoding(in->encoding, &bb, &bw,&bh,&bd); if (!WriteTGA(outname, FS_SYSTEM, in->mip[0].data, in->mip[0].width*bb, in->mip[0].width, in->mip[0].height, in->encoding)) Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding)); } #endif #ifdef IMAGEFMT_PCX else if (!strcasecmp(outext, ".pcx")) { qboolean outformats[PTI_MAX]; for (k = 0; k < PTI_MAX; k++) outformats[k] = (k == PTI_P8) || (k == TF_SOLID8) || (k == TF_TRANS8) || (k == TF_H2_TRANS8_0) || 0; if (!outformats[in->encoding]) Image_ChangeFormat(in, outformats, PTI_INVALID, outname); Image_BlockSizeForEncoding(in->encoding, &bb, &bw,&bh,&bd); if (!WritePCXfile(outname, FS_SYSTEM, in->mip[0].data, in->mip[0].width, in->mip[0].height, in->mip[0].width*bb, host_basepal, false)) Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding)); } #endif else if (!strcasecmp(outext, ".lmp")) { int i; vfsfile_t *f = NULL; qboolean outformats[PTI_MAX]; for (k = 0; k < PTI_MAX; k++) outformats[k] = (k == PTI_P8) || (k == TF_SOLID8) || (k == TF_TRANS8) || (k == TF_H2_TRANS8_0) || 0; if (!outformats[in->encoding]) Image_ChangeFormat(in, outformats, PTI_INVALID, outname); Image_BlockSizeForEncoding(in->encoding, &bb, &bw,&bh,&bd); if (!outformats[in->encoding]) Con_Printf("%s(%s): couldn't palettize\n", outname, Image_FormatName(in->encoding)); else { f = FS_OpenVFS(outname, "wb", FS_SYSTEM); if (f) { i = LittleLong(in->mip[0].width); VFS_WRITE(f, &i, sizeof(i)); i = LittleLong(in->mip[0].height); VFS_WRITE(f, &i, sizeof(i)); VFS_WRITE(f, in->mip[0].data, in->mip[0].width*bb*in->mip[0].height); VFS_CLOSE(f); } else Con_Printf("%s(%s): Couldn't open for writing\n", outname, Image_FormatName(in->encoding)); } } else Con_Printf("%s: Unknown output file format\n", outname); } if (in->mipcount > 1) { Con_Printf("%s(%s): %s %i*%i*%i, %i mips\n", outname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, in->mipcount); for (k = 0; k < in->mipcount; k++) if (in->mip[k].depth == 1 && in->type == PTI_2D) Con_DPrintf("\t%u: %i*%i, %u\n", (unsigned)k, in->mip[k].width, in->mip[k].height, (unsigned)in->mip[k].datasize); else Con_DPrintf("\t%u: %i*%i*%i, %u\n", (unsigned)k, in->mip[k].width, in->mip[k].height, in->mip[k].depth, (unsigned)in->mip[k].datasize); } else Con_Printf("%s(%s): %s %i*%i*%i, %u bytes\n", outname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, (unsigned)in->mip[0].datasize); ImgTool_FreeMips(in); } fflush(stdout); } static struct pendingtextureinfo *ImgTool_DecodeMiptex(struct opts_s *args, miptex_t *mip, size_t size, qbyte *pal) { qbyte *data = (qbyte*)mip + (mip->offsets[3]?mip->offsets[3] + (mip->width>>3)*(mip->height>>3):sizeof(miptex_t)); qbyte *dataend = (qbyte*)mip + size; struct pendingtextureinfo *out = Z_Malloc(sizeof(*out)); qbyte *newdata = NULL; int neww=0, newh=0, sz; unsigned int bw,bh,bb,bd, i; out->type = PTI_2D; out->encoding = PTI_INVALID; Con_DPrintf("%s: width %u, height %u, size %u\n", mip->name, mip->width, mip->height, (unsigned int)size); //header [legacymip0 legacymip1 legacymip2] [extsize extcode extdata]*n [legacymip3] //extcode NAME: extdata-8 bytes of replacement name //extdata pixelformats, extdata is: Width Height newmip0...N where N is 1*1 mip, using round-down logic. //compressed and legacy data ommitted means all 4 offsets are 0. //compressed-only data has offset[3] state the termination position, but offset[0,1,2] MUST be 0 still (extension data starts right after the header, offset3 points to the end of the miptex and has no data there. //legacy-only data is densely packed or whatever. //half-life palette data might be glued onto the end. we don't care to handle that. Con_DPrintf("%i bytes of extended data\n", (unsigned)(dataend-data)); Con_DPrintf("offset[0]: %u\n", mip->offsets[0]); Con_DPrintf("offset[1]: %u\n", mip->offsets[1]); Con_DPrintf("offset[2]: %u\n", mip->offsets[2]); Con_DPrintf("offset[3]: %u\n", mip->offsets[3]); if (data+4 < dataend && data[0]==0x00 && data[1]==0xfb&&data[2]==0x2b&&data[3]==0xaf) //magic id to say that there's actually extensions here... { data+=4; for (; data+4 < dataend; data += sz) { //we could recognise more, uploadfmt_t fmt = PTI_INVALID; size_t csz, w, h; sz = (data[0]<<0) | (data[1]<<8) | (data[2]<<16) | (data[3]<<24); if (sz < 4 || sz > dataend-data) { Con_Printf("%s: Invalid miptex extension\n", mip->name); break;} else if (sz > 8 && !strncmp(data+4, "NAME", 4)) continue; //FIXME else if (sz > 16 && !strncmp(data+4, "RGBA", 4)) fmt = PTI_RGBA8; else if (sz > 16 && !strncmp(data+4, "RGB", 4)) fmt = PTI_RGB8; else if (sz > 16 && !strncmp(data+4, "565", 4)) fmt = PTI_RGB565; else if (sz > 16 && !strncmp(data+4, "5551", 4)) fmt = PTI_RGBA5551; else if (sz > 16 && !strncmp(data+4, "4444", 4)) fmt = PTI_RGBA4444; else if (sz > 16 && !strncmp(data+4, "LUM8", 4)) fmt = PTI_L8; //greyscale. because why not. else if (sz > 16 && !strncmp(data+4, "BC1", 4)) fmt = PTI_BC1_RGBA; else if (sz > 16 && !strncmp(data+4, "BC2", 4)) fmt = PTI_BC2_RGBA; else if (sz > 16 && !strncmp(data+4, "BC3", 4)) fmt = PTI_BC3_RGBA; else if (sz > 16 && !strncmp(data+4, "BC4", 4)) fmt = PTI_BC4_R; else if (sz > 16 && !strncmp(data+4, "BC5", 4)) fmt = PTI_BC5_RG; else if (sz > 16 && !strncmp(data+4, "BC6", 4)) fmt = PTI_BC6_RGB_UFLOAT; else if (sz > 16 && !strncmp(data+4, "BC7", 4)) fmt = PTI_BC7_RGBA; else if (sz > 16 && !strncmp(data+4, "ETC1", 4)) fmt = PTI_ETC1_RGB8; else if (sz > 16 && !strncmp(data+4, "ETC2", 4)) fmt = PTI_ETC2_RGB8; else if (sz > 16 && !strncmp(data+4, "ETCP", 4)) fmt = PTI_ETC2_RGB8A1; else if (sz > 16 && !strncmp(data+4, "ETCA", 4)) fmt = PTI_ETC2_RGB8A8; else if (sz > 16 && !strncmp(data+4, "AST4", 4)) fmt = PTI_ASTC_4X4_LDR; else if (sz > 16 && !strncmp(data+4, "AS54", 4)) fmt = PTI_ASTC_5X4_LDR; else if (sz > 16 && !strncmp(data+4, "AST5", 4)) fmt = PTI_ASTC_5X5_LDR; else if (sz > 16 && !strncmp(data+4, "AS65", 4)) fmt = PTI_ASTC_6X5_LDR; else if (sz > 16 && !strncmp(data+4, "AS85", 4)) fmt = PTI_ASTC_8X5_LDR; else if (sz > 16 && !strncmp(data+4, "AS05", 4)) fmt = PTI_ASTC_10X5_LDR; else if (sz > 16 && !strncmp(data+4, "AST6", 4)) fmt = PTI_ASTC_6X6_LDR; else if (sz > 16 && !strncmp(data+4, "AS86", 4)) fmt = PTI_ASTC_8X6_LDR; else if (sz > 16 && !strncmp(data+4, "AS06", 4)) fmt = PTI_ASTC_10X6_LDR; else if (sz > 16 && !strncmp(data+4, "AST8", 4)) fmt = PTI_ASTC_8X8_LDR; else if (sz > 16 && !strncmp(data+4, "AS08", 4)) fmt = PTI_ASTC_10X8_LDR; else if (sz > 16 && !strncmp(data+4, "AST0", 4)) fmt = PTI_ASTC_10X10_LDR; else if (sz > 16 && !strncmp(data+4, "AS20", 4)) fmt = PTI_ASTC_12X10_LDR; else if (sz > 16 && !strncmp(data+4, "AST2", 4)) fmt = PTI_ASTC_12X12_LDR; else if (sz > 16 && !strncmp(data+4, "EXP5", 4)) fmt = PTI_E5BGR9; else {Con_Printf("%s: Unknown miptex extension %4s\n", mip->name, data+4);continue;} if (out->encoding != PTI_INVALID) //use the first format we support, allowing prioritisation. continue; Image_BlockSizeForEncoding(fmt, &bb, &bw, &bh, &bd); w = data[ 8] | (data[ 9]<<8) | (data[10]<<16) | (data[11]<<24); h = data[12] | (data[13]<<8) | (data[14]<<16) | (data[15]<<24); for (csz = 16; w || h; w>>=1, h>>=1) { w = max(1, w); h = max(1, h); csz += bb*((w+bw-1)/bw)*((h+bh-1)/bh); } if (sz == csz) { //mip chain is complete etc out->encoding = fmt; newdata = data+16; neww = data[ 8] | (data[ 9]<<8) | (data[10]<<16) | (data[11]<<24); newh = data[12] | (data[13]<<8) | (data[14]<<16) | (data[15]<<24); } else Con_Printf("%s: Chain size of %u doesn't match expected %u\n", mip->name, (unsigned)csz-16, sz-16); } } //only use our if there were no corrupt sections. if (data == dataend && newdata && neww && newh) { Image_BlockSizeForEncoding(out->encoding, &bb, &bw, &bh, &bd); for (out->mipcount = 0; out->mipcount < countof(out->mip) && neww && newh; out->mipcount++, neww>>=1, newh>>=1) { neww = max(1, neww); newh = max(1, newh); out->mip[out->mipcount].width = neww; out->mip[out->mipcount].height = newh; out->mip[out->mipcount].depth = 1; out->mip[out->mipcount].datasize = bb; out->mip[out->mipcount].datasize *= (out->mip[out->mipcount].width + bw-1)/bw; out->mip[out->mipcount].datasize *= (out->mip[out->mipcount].height + bh-1)/bh; out->mip[out->mipcount].datasize *= (out->mip[out->mipcount].depth + bd-1)/bd; out->mip[out->mipcount].data = newdata; newdata += out->mip[out->mipcount].datasize; } } else { if ((((dataend-data)+3)&~3) == (((256*3+2)+3)&~3) && data[0]==0&&data[1]==1) pal = data+2; //halflife format... if (*mip->name == '{') out->encoding = TF_TRANS8; else if (!strncasecmp(mip->name, "sky", 3)) out->encoding = TF_H2_TRANS8_0; else out->encoding = PTI_P8; if (pal) { //bake the palette directly. rgb(a) data out. qbyte *idx, *rgb, *pi, *tr; size_t s; if (out->encoding == TF_TRANS8) out->encoding = PTI_RGBA8, tr = pal+255; else if (out->encoding == TF_H2_TRANS8_0) out->encoding = PTI_RGBA8, tr = pal+0; else //if (out->encoding == PTI_P8) out->encoding = PTI_RGBX8, tr = NULL; for (out->mipcount = 0; out->mipcount < 4 && mip->offsets[out->mipcount]; out->mipcount++) { out->mip[out->mipcount].width = mip->width>>out->mipcount; out->mip[out->mipcount].height = mip->height>>out->mipcount; out->mip[out->mipcount].depth = 1; s = out->mip[out->mipcount].width*(size_t)out->mip[out->mipcount].height*out->mip[out->mipcount].depth; out->mip[out->mipcount].datasize = s*4; rgb = out->mip[out->mipcount].data = BZ_Malloc(out->mip[out->mipcount].datasize); idx = (char*)mip + mip->offsets[out->mipcount]; if (mip->offsets[out->mipcount]+s > size) { out->mip[out->mipcount].data = mip; out->mip[out->mipcount].datasize = 0; Con_Printf("%s: Mip%i offsets/size exceed size of texture\n", mip->name, out->mipcount); } else while (s-->0) { pi = pal+3**idx++; *rgb++ = pi[0]; *rgb++ = pi[1]; *rgb++ = pi[2]; *rgb++ = (pi==tr)?0:255; } } } else { for (out->mipcount = 0; out->mipcount < 4 && mip->offsets[out->mipcount]; out->mipcount++) { out->mip[out->mipcount].width = mip->width>>out->mipcount; out->mip[out->mipcount].height = mip->height>>out->mipcount; out->mip[out->mipcount].depth = 1; out->mip[out->mipcount].datasize = out->mip[out->mipcount].width*(size_t)out->mip[out->mipcount].height*out->mip[out->mipcount].depth; out->mip[out->mipcount].data = (char*)mip + mip->offsets[out->mipcount]; if (mip->offsets[out->mipcount]+out->mip[out->mipcount].datasize > size) { out->mip[out->mipcount].data = mip; out->mip[out->mipcount].datasize = 0; Con_Printf("%s: Mip%i offsets/size exceed size of texture\n", mip->name, out->mipcount); } } } } if (*mip->name == '*') *mip->name = '#'; //convert from * to #, so its a valid file name. if (args) { if (args->defaultext && !strcasecmp(args->defaultext, "mip")) { char newout[MAX_OSPATH]; size_t k = strlen(mip->name); vfsfile_t *fs; memcpy(newout, mip->name, k); newout[k++] = '.'; strcpy(newout+k, args->defaultext); fs = FS_OpenVFS(newout, "wb", FS_SYSTEM); if (!fs) Con_Printf("%s(%s): Write failed\n", newout, Image_FormatName(out->encoding)); else { VFS_WRITE(fs, mip, size); VFS_CLOSE(fs); } fflush(stdout); } else ImgTool_Convert(args, out, mip->name, NULL); return NULL; } return out; } static void ImgTool_PrintInfo(const char *inname, struct pendingtextureinfo *in) { size_t m; if (in->mipcount == 1 && in->type == PTI_2D && in->mip[0].depth == 1) printf("%-20s(%s): %4i*%-4i\n", inname, Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height); else if (in->mipcount == 1) printf("%-20s(%s): %s, %i*%i*%i, %u bytes\n", inname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, (unsigned)in->mip[0].datasize); else { /*if (mip) printf("%-20s(%s): \"%s\"%s %i*%i, %i mips\n", inname, Image_FormatName(in->encoding), mip->name, mip->offsets[0]?"":" (stripped)", mip->width, mip->height, in->mipcount); else*/ printf("%-20s(%s): %s, %i*%i*%i, %i mips\n", inname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, in->mipcount); if (verbose) for (m = 0; m < in->mipcount; m++) printf("\t%u: %i*%i*%i, %u\n", (unsigned)m, in->mip[m].width, in->mip[m].height, in->mip[m].depth, (unsigned)in->mip[m].datasize); } } static void ImgTool_Enumerate(struct opts_s *args, const char *inname, void(*callback)(const char *name, struct pendingtextureinfo *mips)) { qbyte *indata; size_t fsize; size_t m; struct pendingtextureinfo *in; qbyte *basepal = NULL; indata = FS_LoadMallocFile(inname, &fsize); if (!indata) printf("%s: unable to read\n", inname); else if (fsize >= sizeof(wad2_t) && indata[0] == 'W' && indata[1] == 'A' && indata[2] == 'D') { const wad2_t *w = (const wad2_t *)indata; const wad2entry_t *e = (const wad2entry_t *)(indata+w->offset); printf("%s: wad%c file with %i entries\n", inname, w->magic[3], w->num); for (m = 0; m < w->num; m++, e++) { switch(e->type) { case TYP_QPIC: { size_t sz=min(e->size, e->dsize); unsigned int w=0; unsigned int h=0; in = NULL; if (!strcasecmp(e->name, "CONCHARS") && (e->size==128*128 || e->size==128*128+8)) { //special hack for buggy conchars, which is listed as a miptex for some reason, with no qpic header (it not being a qpic lump) printf("\t%16.16s: corrupt CONCHARS lump - wrongly marked as qpic. Treating as a legacy engine would...\n", e->name); in = Z_Malloc(sizeof(*in)); in->encoding = TF_H2_TRANS8_0; in->mip[0].data = indata+e->offset; in->mip[0].datasize = 128*128; w = 128; h = 128; } else { if (sz >= 8) { w = (indata[e->offset+0]<<0)|(indata[e->offset+1]<<8)|(indata[e->offset+2]<<16)|(indata[e->offset+3]<<24); h = (indata[e->offset+4]<<0)|(indata[e->offset+5]<<8)|(indata[e->offset+6]<<16)|(indata[e->offset+7]<<24); } if (sz == w*h+8) { //quake in = Z_Malloc(sizeof(*in)); in->encoding = TF_TRANS8; in->mip[0].data = indata+e->offset+8; in->mip[0].datasize = w*h; } else if (((sz+3)&~3) == (((w*h+10+768+3)&~3))) { //halflife const unsigned char *src = indata+e->offset+8; const unsigned char *pal = indata+e->offset+8+w*h+2, *p; unsigned char *dst; sz = w*h; in = Z_Malloc(sizeof(*in)+w*h*3); in->encoding = PTI_RGB8; in->mip[0].data = dst = (unsigned char*)(in+1); in->mip[0].datasize = sz*3; while (sz --> 0) { p = pal+*src++*3; *dst++ = *p++; *dst++ = *p++; *dst++ = *p++; } } else printf("\t%16.16s: missized qpic (%u %u, %u bytes)\n", e->name, w, h, (unsigned int)sz); } if (in) { // printf("\n"); in->type = PTI_2D; in->mipcount = 1; in->mip[0].width = w; in->mip[0].height = h; in->mip[0].depth = 1; in->mip[0].needfree = false; callback(e->name, in); ImgTool_FreeMips(in); } } break; case 67: //hl... case TYP_MIPTEX: { miptex_t *mip = (miptex_t *)(indata+e->offset); if (!strcasecmp(e->name, "CONCHARS") && e->size==128*128) { //special hack for conchars, which is listed as a miptex for some reason, with no qpic header (it not being a qpic lump) in = Z_Malloc(sizeof(*in)); in->encoding = TF_H2_TRANS8_0; in->mip[0].data = indata+e->offset+8; in->mip[0].datasize = 128*128; in->type = PTI_2D; in->mipcount = 1; in->mip[0].width = 128; in->mip[0].height = 128; in->mip[0].depth = 1; in->mip[0].needfree = false; } else in = ImgTool_DecodeMiptex(NULL, mip, min(e->size, e->dsize), basepal); if (in) callback(e->name, in); /* //mip name SHOULD match entry name... but gah! if (strcasecmp(e->name, mip->name)) printf("\t%16.16s (%s): ", e->name, mip->name); else printf("\t%16.16s: ", mip->name); printf("%u*%u%s", mip->width, mip->height, mip->offsets[0]?"":" (omitted)"); if (in->encoding != PTI_P8) printf(" (%s %u*%u)", Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height);*/ // printf("\n"); ImgTool_FreeMips(in); } break; case TYP_PALETTE: if (e->size == 768) { basepal = indata+e->offset; if (!memcmp(basepal, host_basepal, 768)) basepal = NULL; } else printf("\t%16.16s: palette - %u bytes\n", e->name, e->size); break; case 70: printf("\t%16.16s: Halflife Font (%u bytes)\n", e->name, e->size); break; default: printf("\t%16.16s: ENTRY TYPE %u (%u bytes)\n", e->name, e->type, e->size); break; } } } else if (fsize >= sizeof(dheader_t) && ( !memcmp(indata, BSPVERSION) || !memcmp(indata, BSPVERSIONHL) || !memcmp(indata, BSPVERSIONPREREL)|| !memcmp(indata, BSPVERSION_LONG1)|| !memcmp(indata, BSPVERSION_LONG2))) { //q1bsp dheader_t *bsp = (dheader_t*)indata; dmiptexlump_t *texlump = (dmiptexlump_t*)(indata + bsp->lumps[LUMP_TEXTURES].fileofs); miptex_t *miptex; size_t i; size_t sz = bsp->lumps[LUMP_TEXTURES].filelen; printf("%-20s: bsp file (%u textures)\n", inname, texlump->nummiptex); for (i = texlump->nummiptex; i --> 0; ) { if (texlump->dataofs[i] < 0 || texlump->dataofs[i] >= bsp->lumps[LUMP_TEXTURES].filelen) { char syn[MAX_QPATH]; sprintf(syn, "unnamed%u", (unsigned)i); printf("%-20s------ %d\n", syn, texlump->dataofs[i]); continue; } miptex = (miptex_t*)((qbyte*)texlump + texlump->dataofs[i]); in = ImgTool_DecodeMiptex(NULL, miptex, sz - texlump->dataofs[i], NULL); sz = texlump->dataofs[i]; callback(miptex->name, in); /* if (in->encoding != PTI_P8) printf("\t%16.16s: %u*%u%s (%s: %i*%i)\n", miptex->name, miptex->width, miptex->height, miptex->offsets[0]?"":" (external data)", Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height); else printf("\t%16.16s: %u*%u%s\n", miptex->name, miptex->width, miptex->height, miptex->offsets[0]?"":" (external data)"); */ ImgTool_FreeMips(in); } } else if (fsize >= sizeof(dmdl_t) && ( !memcmp(indata,IDPOLYHEADER) && (((dmdl_t*)indata)->version == ALIAS_VERSION || ((dmdl_t*)indata)->version == 50))) { //quake's mdl format. also hexen2's missionpack format. int i, j, numframes; char skinname[256]; dmdl_t *mdl = (dmdl_t *)indata; daliasskintype_t *pskintype = (daliasskintype_t*)(indata + sizeof(*mdl)-((((dmdl_t*)indata)->version == 50)?0:sizeof(int))); daliasskingroup_t *pskingroup; daliasskininterval_t *intervals; uploadfmt_t encoding; struct pendingtextureinfo *out = Z_Malloc(sizeof(*out)); if( mdl->flags & MFH2_TRANSPARENT ) encoding = TF_H2_T7G1; //hexen2 else if( mdl->flags & MFH2_HOLEY ) encoding = TF_H2_TRANS8_0; //hexen2 else if( mdl->flags & MFH2_SPECIAL_TRANS ) encoding = TF_H2_T4A4; //hexen2 else encoding = TF_SOLID8; printf("%-20s: mdl file (%u skingroups)\n", inname, mdl->numskins); for (i = 0; i < mdl->numskins; i++) { switch(LittleLong(pskintype->type)) { case ALIAS_SKIN_SINGLE: out = Z_Malloc(sizeof(*out)); out->type = PTI_2D; out->encoding = encoding; out->mipcount = 1; out->mip[0].datasize = mdl->skinwidth*mdl->skinheight; out->mip[0].width = mdl->skinwidth; out->mip[0].height = mdl->skinheight; out->mip[0].depth = 1; out->mip[0].needfree = false; out->mip[0].data = (qbyte*)(pskintype+1); pskintype = (daliasskintype_t *)((char *)out->mip[0].data+out->mip[0].datasize); printf("\t%s_%u: %i*%i\n", inname, i, mdl->skinwidth, mdl->skinheight); snprintf(skinname, sizeof(skinname), "%s_%u", inname, i); callback(skinname, out); ImgTool_FreeMips(out); break; default: pskingroup = (daliasskingroup_t*)(pskintype+1); intervals = (daliasskininterval_t *)(pskingroup+1); numframes = LittleLong(pskingroup->numskins); for (j = 0; j < numframes; j++) { out = Z_Malloc(sizeof(*out)); out->type = PTI_2D; out->encoding = encoding; out->mipcount = 1; out->mip[0].datasize = mdl->skinwidth*mdl->skinheight; out->mip[0].width = mdl->skinwidth; out->mip[0].height = mdl->skinheight; out->mip[0].depth = 1; out->mip[0].needfree = false; out->mip[0].data = (qbyte *)(intervals + numframes) + mdl->skinwidth*mdl->skinheight*j; printf("\t\t%s_%u_%u: %i*%i @ %g\n", inname, i, j, mdl->skinwidth, mdl->skinheight, intervals[j].interval); snprintf(skinname, sizeof(skinname), "%s_%u_%u", inname, i, j); callback(skinname, out); ImgTool_FreeMips(out); } pskintype = (daliasskintype_t *)((qbyte *)(intervals+numframes) + mdl->skinwidth*mdl->skinheight*numframes); break; } } } //else spr else { const miptex_t *mip = NULL; const char *ex = COM_GetFileExtension(inname, NULL); if (!strcasecmp(ex, ".mip")) { in = ImgTool_DecodeMiptex(NULL, (miptex_t*)indata, fsize, NULL); if (fsize >= sizeof(miptex_t)) mip = (const miptex_t*)indata; } else in = Image_LoadMipsFromMemory(args->flags|IF_NOMIPMAP, inname, inname, indata, fsize); if (!in) printf("%-20s: unsupported format\n", inname); else callback(inname, in); (void)mip; ImgTool_FreeMips(in); } fflush(stdout); } struct filelist_s { const char **exts; size_t numfiles; struct { const char *rootpath; //the basepath that was passed to the filelist scan. char *name; size_t baselen; //length up to but not including the filename extension. } *file; size_t maxfiles; //to avoid reallocs }; static void FileList_Release(struct filelist_s *list) { size_t i; for (i = 0; i < list->numfiles; i++) free(list->file[i].name); free(list->file); list->numfiles = 0; list->maxfiles = 0; } static void FileList_Add(struct filelist_s *list, const char *rootpath, const char *fname) { size_t i; size_t baselen; const char *ext = COM_GetFileExtension(fname, NULL); for (i = 0; ; i++) { if (!list->exts[i]) return; //extension wasn't in the list. if (!strcmp(list->exts[i], ext)) break; //one of the accepted extensions } baselen = ext?ext-fname:strlen(fname); for (i = 0; i < list->numfiles; i++) { if (list->file[i].baselen == baselen && !strncasecmp(list->file[i].name, fname, baselen)) { if (strcasecmp(list->file[i].name+baselen, fname+baselen) > 0) { Con_Printf("Ignoring dupe file %s (using %s)\n", list->file[i].name, fname); //use the 'lower' one instead, for consistency between systems. list->file[i].rootpath = rootpath; list->file[i].name = strdup(fname); list->file[i].baselen = baselen; } else Con_Printf("Ignoring dupe file %s (using %s)\n", fname, list->file[i].name); return; //file already listed, but maybe with a different extension } } if (i == list->maxfiles) { list->maxfiles += 64; list->file = realloc(list->file, sizeof(*list->file)*list->maxfiles); } list->file[i].rootpath = rootpath; list->file[i].name = strdup(fname); list->file[i].baselen = baselen; list->numfiles++; } #ifdef _WIN32 static void ImgTool_TreeScan(struct filelist_s *list, const char *rootpath, const char *subpath) { //FIXME: convert to utf-8. HANDLE h; WIN32_FIND_DATAA fd; char file[MAX_OSPATH]; if (subpath && *subpath) Q_snprintfz(file, sizeof(file), "%s/%s", rootpath, subpath); else Q_snprintfz(file, sizeof(file), "%s", rootpath); if (GetFileAttributesA(file) & FILE_ATTRIBUTE_DIRECTORY) //if its a directory then scan it. Q_snprintfz(file+strlen(file), sizeof(file)-strlen(file), "/*"); h = FindFirstFileA(file, &fd); if (h != INVALID_HANDLE_VALUE) { do { if (*fd.cFileName == '.') continue; //skip .. (and unix hidden files, because urgh) if (subpath && *subpath) Q_snprintfz(file, sizeof(file), "%s/%s", subpath, fd.cFileName); else Q_snprintfz(file, sizeof(file), "%s", fd.cFileName); if (fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) ; //don't report hidden entries. else if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ImgTool_TreeScan(list, rootpath, file); else FileList_Add(list, rootpath, file); } while(FindNextFileA(h, &fd)); FindClose(h); } } #else #include #include static void ImgTool_TreeScan(struct filelist_s *list, const char *basepath, const char *subpath) { DIR *dir; char file[MAX_OSPATH]; struct dirent *ent; struct stat sb; if (subpath && *subpath) Q_snprintfz(file, sizeof(file), "%s/%s", basepath, subpath); else Q_snprintfz(file, sizeof(file), "%s", basepath); stat(file, &sb); if ((sb.st_mode & S_IFMT) == S_IFDIR) { dir = opendir(file); if (!dir) { Con_Printf("Failed to open dir %s\n", file); return; } for (;;) { ent = readdir(dir); if (!ent) break; if (*ent->d_name == '.') continue; else if (ent->d_type == DT_DIR) { if (!subpath) continue; if (*subpath) Q_snprintfz(file, sizeof(file), "%s/%s", subpath, ent->d_name); else Q_snprintfz(file, sizeof(file), "%s", ent->d_name); ImgTool_TreeScan(list, basepath, file); } else if (ent->d_type == DT_REG) { if (subpath && *subpath) Q_snprintfz(file, sizeof(file), "%s/%s", subpath, ent->d_name); else Q_snprintfz(file, sizeof(file), "%s", ent->d_name); FileList_Add(list, basepath, file); } } closedir(dir); } else { if (*file == '/') FileList_Add(list, "", file); else FileList_Add(list, ".", file); } } #endif static void ImgTool_TreeConvert(struct opts_s *args, const char *destpath, const char *srcpath) { size_t newfiles=0, skippedfiles=0, processedfiles=0; char file[MAX_OSPATH]; char dest[MAX_OSPATH]; const char *exts[] = {".png", ".bmp", ".tga", ".jpg", ".exr", ".hdr", ".pcx", NULL}; struct filelist_s list = {exts}; size_t i, destlen = strlen(destpath)+1; ImgTool_TreeScan(&list, srcpath, ""); if (!list.numfiles) Con_Printf("No suitable files found in directory: %s\n", srcpath); for (i = 0; i < list.numfiles; i++) { struct stat statsrc, statdst; Q_snprintfz(file, sizeof(file), "%s/%s", srcpath, list.file[i].name); Q_snprintfz(dest, sizeof(dest), "%s/%s", destpath, list.file[i].name); Q_snprintfz(dest+destlen+list.file[i].baselen, sizeof(dest)-destlen-list.file[i].baselen, ".dds"); if (stat(file, &statsrc) < 0) { Con_Printf("stat(\"%s\") failed...\n", file); continue; } if (stat(dest, &statdst) < 0) { statdst.st_mtime = INT_MIN; //make it look old newfiles++; } if (statdst.st_mtime <= statsrc.st_mtime) { processedfiles++; // Con_Printf("Image file %s -> %s\n", file, dest); FS_CreatePath(dest, FS_SYSTEM); ImgTool_Convert(args, ImgTool_Read(args, file), file, dest); } else { skippedfiles++; // Con_Printf("Unmodified image file %s -> %s\n", file, dest); } } Con_Printf("found: %u, processed: %u, skipped: %u, new: %u\n", (unsigned int)list.numfiles, (unsigned int)processedfiles, (unsigned int)skippedfiles, (unsigned int)newfiles); FileList_Release(&list); return; } static void ImgTool_WadExtract(struct opts_s *args, const char *wadname) { qbyte *indata; size_t fsize; size_t m; qbyte *basepal = NULL; indata = FS_LoadMallocFile(wadname, &fsize); if (!indata) printf("%s: unable to read\n", wadname); else if (fsize >= sizeof(wad2_t) && indata[0] == 'W' && indata[1] == 'A' && indata[2] == 'D') { const wad2_t *w = (const wad2_t *)indata; const wad2entry_t *e = (const wad2entry_t *)(indata+w->offset); int i; char clean[sizeof(e->name)+1]; for (m = 0; m < w->num; m++, e++) { switch(e->type) { case 67: //hl... case TYP_MIPTEX: { miptex_t *mip = (miptex_t *)(indata+e->offset); if (!strcasecmp(e->name, "CONCHARS") && e->size==128*128) { //special hack for conchars, which is listed as a miptex for some reason, with no qpic header (it not being a qpic lump) struct pendingtextureinfo *out = Z_Malloc(sizeof(*out)); out->encoding = TF_H2_TRANS8_0; out->type = PTI_2D; out->mip[0].width = 128; out->mip[0].height = 128; out->mip[0].depth = 1; out->mip[0].datasize = out->mip[0].width*out->mip[0].height*out->mip[0].depth; out->mip[0].data = (char*)mip; out->mipcount = 1; ImgTool_Convert(args, out, "conchars", NULL); break; } ImgTool_DecodeMiptex(args, mip, e->dsize, basepal); } break; case TYP_QPIC: { int *qpic = (int *)(indata+e->offset); struct pendingtextureinfo *out = Z_Malloc(sizeof(*out)); size_t sz; qbyte *p; if (e->size < 8 || 8+qpic[0]*qpic[1] != e->size) { printf("invalid size/header for qpic lump: %s\n", e->name); break; } out->type = PTI_2D; out->mip[0].width = LittleLong(qpic[0]); out->mip[0].height = LittleLong(qpic[1]); out->mip[0].depth = 1; out->mip[0].datasize = out->mip[0].width*out->mip[0].height*out->mip[0].depth; out->mip[0].data = (char*)(qpic+2); out->mipcount = 1; for (sz = 0, p = out->mip[0].data; sz < out->mip[0].datasize; sz++) if (p[sz] == 255) break; out->encoding = szmip[0].datasize?TF_TRANS8:PTI_P8; for (i = 0; i < sizeof(e->name); i++) { //lowercase it. if (e->name[i] >= 'A' && e->name[i] <= 'Z') clean[i] = (e->name[i]-'A')+'a'; else clean[i] = e->name[i]; } clean[sizeof(e->name)] = 0; ImgTool_Convert(args, out, clean, NULL); } break; case TYP_PALETTE: if (e->size == 768) { basepal = indata+e->offset; if (!memcmp(basepal, host_basepal, 768)) basepal = NULL; } FALLTHROUGH default: printf("skipping %s\n", e->name); break; } } } else if (fsize >= sizeof(dheader_t) && ( !memcmp(indata, BSPVERSION) || // !memcmp(indata, BSPVERSIONHL) || !memcmp(indata, BSPVERSIONPREREL)|| !memcmp(indata, BSPVERSION_LONG1)|| !memcmp(indata, BSPVERSION_LONG2))) { //q1bsp dheader_t *bsp = (dheader_t*)indata; dmiptexlump_t *texlump = (dmiptexlump_t*)(indata + bsp->lumps[LUMP_TEXTURES].fileofs); miptex_t *miptex; size_t i; size_t sz = bsp->lumps[LUMP_TEXTURES].filelen; for (i = texlump->nummiptex; i --> 0; ) { if (texlump->dataofs[i] < 0 || texlump->dataofs[i] >= bsp->lumps[LUMP_TEXTURES].filelen) continue; miptex = (miptex_t*)((qbyte*)texlump + texlump->dataofs[i]); if (*miptex->name && miptex->width && miptex->height && miptex->offsets[0]>0) { ImgTool_DecodeMiptex(args, miptex, sz - texlump->dataofs[i], basepal); sz = texlump->dataofs[i]; } } } else if (fsize >= sizeof(dmdl_t) && ( !memcmp(indata,IDPOLYHEADER) && ((dmdl_t*)indata)->version == ALIAS_VERSION)) { char imgname[1024]; int i, j, numframes; dmdl_t *mdl = (dmdl_t *)indata; daliasskintype_t *pskintype = (daliasskintype_t*)(indata + sizeof(*mdl)); daliasskingroup_t *pskingroup; daliasskininterval_t *intervals; struct pendingtextureinfo *out = Z_Malloc(sizeof(*out)); out->type = PTI_2D; #ifdef HEXEN2 if( mdl->flags & MFH2_TRANSPARENT ) out->encoding = TF_H2_T7G1; //hexen2 else #endif if( mdl->flags & MFH2_HOLEY ) out->encoding = TF_H2_TRANS8_0; //hexen2 #ifdef HEXEN2 else if( mdl->flags & MFH2_SPECIAL_TRANS ) out->encoding = TF_H2_T4A4; //hexen2 #endif else out->encoding = TF_SOLID8; out->mipcount = 1; out->mip[0].datasize = mdl->skinwidth*mdl->skinheight; out->mip[0].width = mdl->skinwidth; out->mip[0].height = mdl->skinheight; out->mip[0].depth = 1; for (i = 0; i < mdl->numskins; i++) { switch(LittleLong(pskintype->type)) { case ALIAS_SKIN_SINGLE: out->mip[0].data = (qbyte*)(pskintype+1); Q_snprintfz(imgname, sizeof(imgname), "%s_%i.", wadname, i); ImgTool_Convert(args, out, imgname, NULL); pskintype = (daliasskintype_t *)((char *)out->mip[0].data+out->mip[0].datasize); break; default: pskingroup = (daliasskingroup_t*)(pskintype+1); intervals = (daliasskininterval_t *)(pskingroup+1); numframes = LittleLong(pskingroup->numskins); out->mip[0].data = (qbyte *)(intervals + numframes); for (j = 0; j < numframes; j++,out->mip[0].data=(char*)out->mip[0].data+out->mip[0].datasize) { Q_snprintfz(imgname, sizeof(imgname), "%s_%i_%i.", wadname, i, j); ImgTool_Convert(args, out, imgname, NULL); } pskintype = (daliasskintype_t *)out->mip[0].data; break; } } } else printf("%s: does not appear to be a wad file\n", wadname); } int Image_SortPalette(const void *av, const void *bv) { const struct { qbyte r,g,b; int count; } *a=av, *b=bv; return b->count - a->count; } static qbyte *Image_GenPalette(struct pendingtextureinfo *in, const char *name) { unsigned int t; qbyte *pal = Z_Malloc(768); qbyte *d; qboolean hasalpha = false; struct { qbyte r,g,b; int count; } *p = NULL; size_t nump=0, maxp=0, i; static qboolean mippixelformats[PTI_MAX] = {[PTI_RGBA8]=true}; Image_ChangeFormat(in, mippixelformats, PTI_INVALID, name); //make sure its rgbx t = in->mip[0].width*in->mip[0].height*in->mip[0].depth; d = in->mip[0].data; for(; t --> 0; d += 4) { for(i = 0; i < nump; i++) { if (p[i].r == d[0] && p[i].g == d[1] && p[i].b == d[2]) { p[i].count++; break; } } if (i == nump) { //new rgb value. if (nump == maxp) { //urgh. need more. maxp += 64; p = realloc(p, sizeof(*p)*maxp); } p[i].r = d[0]; p[i].g = d[1]; p[i].b = d[2]; p[i].count = 1; nump++; } } qsort(p, nump, sizeof(*p), Image_SortPalette); if (nump > 255) Con_Printf("%s: %u unique colours\n", name, (unsigned)nump); else Con_DPrintf("%s: %u unique colours\n", name, (unsigned)nump); if (nump >= 256) nump = 256; else memset(pal, 0, 768); //make sure its set to something. for (i = 0; i < nump; i++) { //fill in the 256 most common colours. should probably compress them a bit if there's more. pal[i*3+0] = p[i].r; pal[i*3+1] = p[i].g; pal[i*3+2] = p[i].b; } if (hasalpha == true) { pal[255*3+0] = 0; pal[255*3+1] = 0; pal[255*3+2] = 255; } free(p); return pal; } //spits out our extended .mip format static qboolean ImgTool_MipExport(struct opts_s *args, vfsfile_t *outfile, struct pendingtextureinfo *in, const char *mipname, int wadtype) { struct pendingtextureinfo *highcolour = NULL; char *highcode = NULL, *ext; size_t u; unsigned int m, tsz; miptex_t mip; qbyte *pal = host_basepal; static qboolean mippixelformats[PTI_MAX] = {[PTI_P8]=true}; if (!in || !in->mipcount) { Con_Printf("%s: unable to load any mips\n", mipname); return false; } in = ImgTool_DupeMipchain(in); if (in->mipcount < 4) Image_GenerateMips(in, args->flags); if (in->encoding == PTI_P8 || args->newpixelformat == PTI_INVALID) { highcode = NULL; //no, don't store it weirdly... } else { highcolour = ImgTool_DupeMipchain(in); Image_GenerateMips(highcolour, args->flags); for (u = 1; u < countof(sh_config.texfmt); u++) sh_config.texfmt[u] = true; if (!ImgTool_ConvertPixelFormat(args, mipname, highcolour)) { Con_Printf("%s: Unable to convert to requested pixel format\n", mipname); ImgTool_FreeMips(highcolour); highcolour = NULL; } else if (highcolour->mip[highcolour->mipcount-1].width != 1 || highcolour->mip[highcolour->mipcount-1].height != 1) { Con_Printf("%s: Mipchain truncated\n", mipname); ImgTool_FreeMips(highcolour); highcolour = NULL; } else for (u = 1; u < highcolour->mipcount; u++) { //mip chain must round down consistently. if (highcolour->mip[u].width != max(1,highcolour->mip[u-1].width>>1) || highcolour->mip[u].height!= max(1,highcolour->mip[u-1].height>>1)) { Con_Printf("%s: Mipchain sized wrongly\n", mipname); ImgTool_FreeMips(highcolour); highcolour = NULL; break; } } if (highcolour) switch(highcolour->encoding) { case PTI_BC1_RGB: case PTI_BC1_RGBA: highcode = "BC1"; break; //not in any core gl, but uniquitous on desktop, but not mobile. case PTI_BC2_RGBA: highcode = "BC2"; break; case PTI_BC3_RGBA: highcode = "BC3"; break; case PTI_BC4_R: highcode = "BC4"; break; case PTI_BC5_RG: highcode = "BC5"; break; case PTI_BC6_RGB_UFLOAT:highcode = "BC6"; break; //aka bptc, core in gl4.2 (not gles) case PTI_BC7_RGBA: highcode = "BC7"; break; //aka bptc, core in gl4.2 (not gles) case PTI_ETC1_RGB8: highcode = "ETC1"; break; //available on most gles2 devices. case PTI_ETC2_RGB8: highcode = "ETC2"; break; //core in gles3 (or gl4.3) case PTI_ETC2_RGB8A1: highcode = "ETCP"; break; //core in gles3 (or gl4.3) case PTI_ETC2_RGB8A8: highcode = "ETCA"; break; //core in gles3 (or gl4.3) case PTI_ASTC_4X4_LDR: highcode = "AST4"; break; //core in gles3.2 case PTI_ASTC_5X4_LDR: highcode = "AS54"; break; //core in gles3.2 case PTI_ASTC_5X5_LDR: highcode = "AST5"; break; //core in gles3.2 case PTI_ASTC_6X5_LDR: highcode = "AS65"; break; //core in gles3.2 case PTI_ASTC_6X6_LDR: highcode = "AST6"; break; //core in gles3.2 case PTI_ASTC_8X5_LDR: highcode = "AS85"; break; //core in gles3.2 case PTI_ASTC_8X6_LDR: highcode = "AS86"; break; //core in gles3.2 case PTI_ASTC_10X5_LDR: highcode = "AS05"; break; //core in gles3.2 case PTI_ASTC_10X6_LDR: highcode = "AS06"; break; //core in gles3.2 case PTI_ASTC_8X8_LDR: highcode = "AST8"; break; //core in gles3.2 case PTI_ASTC_10X8_LDR: highcode = "AS08"; break; //core in gles3.2 case PTI_ASTC_10X10_LDR:highcode = "AST0"; break; //core in gles3.2 case PTI_ASTC_12X10_LDR:highcode = "AS20"; break; //core in gles3.2 case PTI_ASTC_12X12_LDR:highcode = "AST2"; break; //core in gles3.2 case PTI_RGB565: highcode = "565"; break; case PTI_RGBA5551: highcode = "5551"; break; case PTI_RGBA4444: highcode = "4444"; break; case PTI_RGB8: highcode = "RGB"; break; //generally needs reformatting to rgbx. case PTI_RGBA8: highcode = "RGBA"; break; //bloaty case PTI_L8: highcode = "LUM8"; break; case PTI_E5BGR9: highcode = "EXP5"; break; //gl3+ default: Con_Printf("%s: unsupported pixel format(%s) for miptex\n", mipname, Image_FormatName(highcolour->encoding)); ImgTool_FreeMips(highcolour); highcolour = NULL; break; } } if (args->width && args->height && in->mipcount >= 1) { qbyte *newimg; unsigned int bb, bw, bh, bd; Image_BlockSizeForEncoding(in->encoding, &bb, &bw, &bh, &bd); newimg = Image_ResampleTexture(in->encoding, in->mip[0].data, in->mip[0].width, in->mip[0].height, NULL, args->width, args->height); if (newimg) { in->mipcount = 1; //urgh if (in->mip[0].needfree) BZ_Free(in->mip[0].data); in->mip[0].data = newimg; in->mip[0].needfree = true; in->mip[0].width = args->width; in->mip[0].height = args->height; in->mip[0].depth = 1; in->mip[0].datasize = bb*((in->mip[0].width+bw-1)/bw)*((in->mip[0].height+bh-1)/bh)*((in->mip[0].depth+bd-1)/bd); Image_GenerateMips(in, args->flags); } else Con_Printf("%s: unable to resize %s\n", mipname, Image_FormatName(in->encoding)); } if (args->mipnum >= in->mipcount) { Con_Printf("%s: not enough mips\n", mipname); ImgTool_FreeMips(in); ImgTool_FreeMips(highcolour); return false; } //strip out all but the 4 mip levels we care about. for (u = 0; u < in->mipcount; u++) { if (u >= args->mipnum && u < args->mipnum+4) { if (wadtype<2) { //if we're stripping out the wad data (so that the engine ends up requiring external textures) then do it now before palettizing, for efficiency. if (in->mip[u].needfree) BZ_Free(in->mip[u].data); in->mip[u].data = NULL; in->mip[u].datasize = 0; } } else { if (in->mip[u].needfree) BZ_Free(in->mip[u].data); memset(&in->mip[u], 0, sizeof(in->mip[u])); } } in->mipcount -= args->mipnum; if (in->mipcount > 4) in->mipcount = 4; memmove(&in->mip[0], &in->mip[args->mipnum], sizeof(in->mip[0])*in->mipcount); memset(&in->mip[in->mipcount], 0, sizeof(in->mip[0])*((args->mipnum+4)-in->mipcount)); //null it out, just in case. if (in->mip[0].data) { if (in->encoding != PTI_P8) { qbyte *basepal = host_basepal; if (wadtype == 3) { pal = host_basepal = Image_GenPalette(in, mipname); } if (*mipname=='{') { memset(mippixelformats, 0, sizeof(mippixelformats)); mippixelformats[TF_TRANS8] = true; } Image_ChangeFormat(in, mippixelformats, (*mipname=='{')?TF_TRANS8:PTI_INVALID, mipname); host_basepal = basepal; } if (!(in->encoding == PTI_P8 || in->encoding == TF_TRANS8)) { //erk! we failed to palettize... ImgTool_FreeMips(in); ImgTool_FreeMips(highcolour); Con_Printf("%s: paletizing error (source format %s)\n", mipname, Image_FormatName(in->encoding)); return false; } } if (!in->mip[0].width || (in->mip[0].width & 15)) Con_Printf("%s(%i): WARNING: miptex width is not a multiple of 16 - %i*%i\n", mipname, args->mipnum, in->mip[0].width, in->mip[0].height); if (!in->mip[0].height || (in->mip[0].height & 15)) Con_Printf("%s(%i): WARNING: miptex height is not a multiple of 16 - %i*%i\n", mipname, args->mipnum, in->mip[0].width, in->mip[0].height); memset(mip.name, 0, sizeof(mip.name)); Q_strncpyz(mip.name, mipname, sizeof(mip.name)); ext = (char*)COM_GetFileExtension (mip.name, NULL); while (*ext) *ext++=0; if (*mip.name == '#') *mip.name = '*'; //make it a proper turb mip.width = in->mip[0].width; mip.height = in->mip[0].height; mip.offsets[0] = in->mip[0].datasize?sizeof(mip):0; mip.offsets[1] = in->mip[1].datasize?mip.offsets[0]+in->mip[0].datasize:0; mip.offsets[2] = in->mip[2].datasize?mip.offsets[1]+in->mip[1].datasize:0; mip.offsets[3] = in->mip[3].datasize?mip.offsets[2]+in->mip[2].datasize:0; tsz = sizeof(mip)+in->mip[0].datasize+in->mip[1].datasize+in->mip[2].datasize+in->mip[3].datasize; VFS_WRITE(outfile, &mip, sizeof(mip)); VFS_WRITE(outfile, in->mip[0].data, in->mip[0].datasize); VFS_WRITE(outfile, in->mip[1].data, in->mip[1].datasize); VFS_WRITE(outfile, in->mip[2].data, in->mip[2].datasize); VFS_WRITE(outfile, in->mip[3].data, in->mip[3].datasize); if (wadtype == 3) { tsz += 2 + 256*3; VFS_WRITE(outfile, "\x00\x01", 2); VFS_WRITE(outfile, pal, 256*3); } if (highcolour) { unsigned int highsize; VFS_WRITE(outfile, "\x00\xfb\x2b\xaf", 4); //magic id to say that there's actually extensions here... tsz += 4; //spit out our high-colour lump here for (highsize = 16, m = 0; m < highcolour->mipcount; m++) highsize += highcolour->mip[m].datasize; VFS_WRITE(outfile, &highsize, 4); VFS_WRITE(outfile, highcode, 4); VFS_WRITE(outfile, &highcolour->mip[0].width, 4); VFS_WRITE(outfile, &highcolour->mip[0].height, 4); for (m = 0; m < highcolour->mipcount; m++) VFS_WRITE(outfile, highcolour->mip[m].data, highcolour->mip[m].datasize); tsz += highsize; ImgTool_FreeMips(highcolour); Con_Printf("%s: %ix%i (%s: %ix%i %i)\n", mip.name, mip.width, mip.height, highcode, highcolour->mip[0].width, highcolour->mip[0].height, highcolour->mipcount); } else Con_Printf("%s: %ix%i%s\n", mip.name, mip.width, mip.height, wadtype==3?"+pal":""); //and pad it, just in case. if (tsz & 3) VFS_WRITE(outfile, "\0\0\0\0", 4-(tsz&3)); ImgTool_FreeMips(in); return true; } static void ImgTool_WadConvert(struct opts_s *args, const char *destpath, const char **srcpaths, size_t numpaths, int wadtype/*x,2,3*/) { char file[MAX_OSPATH]; const char *exts[] = {".mip", ".png", ".bmp", ".tga", ".exr", ".hdr", ".dds", ".ktx", ".xcf", ".pcx", ".jpg", NULL}; struct filelist_s list = {exts}; size_t i, u; vfsfile_t *f; char *inname; qbyte *indata; size_t fsize; wad2_t wad2; wad2entry_t *wadentries = NULL, *entry; size_t maxentries = 0; qboolean qpics; struct pendingtextureinfo *in; if (!numpaths) ImgTool_TreeScan(&list, ".", NULL); else while(numpaths --> 0) ImgTool_TreeScan(&list, *srcpaths++, NULL); if (!list.numfiles) { printf("%s: No files specified\n", destpath); return; } if (!strcasecmp(COM_GetFileExtension(destpath, NULL), ".bsp")) { } else { qpics = !strcasecmp("gfx.wad", COM_SkipPath(destpath)); f = FS_OpenVFS(destpath, "wb", FS_SYSTEM); wad2.magic[0] = 'W'; wad2.magic[1] = 'A'; wad2.magic[2] = 'D'; wad2.magic[3] = (wadtype==3)?'3':'2'; //wad3 instead of 2, so we can include a palette for tools to validate against wad2.num = 0; wad2.offset = 0; VFS_WRITE(f, &wad2, 12); if (wadtype == 2 && !qpics) { //WAD2 texture files generally have a palette lump. if (wad2.num == maxentries) { maxentries += 64; wadentries = realloc(wadentries, sizeof(*wadentries)*maxentries); } entry = &wadentries[wad2.num++]; memset(entry, 0, sizeof(*entry)); Q_strncpyz(entry->name, "PALETTE", 16); entry->type = TYP_PALETTE; entry->offset = VFS_TELL(f); //and the lump data. VFS_WRITE(f, host_basepal, 256*3); entry->size = entry->dsize = VFS_TELL(f)-entry->offset; } for (i = 0; i < list.numfiles; i++) { Q_snprintfz(file, sizeof(file), "%s/%s", list.file[i].rootpath, list.file[i].name); inname = list.file[i].name; if (list.file[i].baselen > 15) { Con_Printf("Path too long for wad - %s\n", inname); continue; } indata = FS_LoadMallocFile(file, &fsize); if (!indata) { Con_Printf("Unable to open %s\n", inname); continue; } if (wad2.num == maxentries) { maxentries += 64; wadentries = realloc(wadentries, sizeof(*wadentries)*maxentries); } entry = &wadentries[wad2.num]; memset(entry, 0, sizeof(*entry)); Q_strncpyz(entry->name, inname, 16); if (list.file[i].baselen < sizeof(entry->name)) entry->name[list.file[i].baselen] = 0; //kill any .tga if (*entry->name == '#') *entry->name = '*'; //* is not valid in a filename, yet needed for turbs, so by convention # is used instead. this is only relevant for the first char. entry->type = TYP_MIPTEX; entry->offset = VFS_TELL(f); if (!strcasecmp(COM_GetFileExtension(file, NULL), ".mip")) { //.mip files can just be loaded directly //I just hope they are actually q1 format and not hl, for instance. if (wadtype == 3) { Con_Printf("refusing to inject q1 miptex into halflife wad-3 file\n"); Z_Free(indata); continue; } VFS_WRITE(f, indata, fsize); wad2.num++; entry->size = entry->dsize = VFS_TELL(f)-entry->offset; Z_Free(indata); } else { if (wadtype == 3) { for (u = 0; u < sizeof(entry->name); u++) entry->name[u] = toupper(entry->name[u]); entry->type = 67; //halflife's mips actually use a different type from q1 ones. } //try to decompress everything to a nice friendly palletizable range. for (u = 1; u < countof(sh_config.texfmt); u++) sh_config.texfmt[u] = (u==PTI_RGBA8)||(u==PTI_RGBX8)||(u==PTI_P8)||(u==args->newpixelformat); in = Image_LoadMipsFromMemory(args->flags, inname, file, indata, fsize); if (qpics) { qboolean mippixelformats[PTI_MAX]; memset(mippixelformats, 0, sizeof(mippixelformats)); if (!strcasecmp(entry->name, "CONCHARS") && in->mip[0].width==128&&in->mip[0].height==128) { mippixelformats[TF_H2_TRANS8_0] = true; entry->type = TYP_MIPTEX; //yes, weird. match vanilla quake. explicitly avoid qpic to avoid corruption in the first 8 bytes (due to the engine's early endian swapping) //FIXME: encoding should be pti_trans8_0... } else { mippixelformats[TF_TRANS8] = true; entry->type = TYP_QPIC; //qpics need a header VFS_WRITE(f, &in->mip[0].width, sizeof(int)); VFS_WRITE(f, &in->mip[0].height, sizeof(int)); } if (!mippixelformats[in->encoding]) Image_ChangeFormat(in, mippixelformats, PTI_INVALID, entry->name); if (!mippixelformats[in->encoding]) continue; //and now the 8bit pixel data itself VFS_WRITE(f, in->mip[0].data, in->mip[0].datasize); } else if (!ImgTool_MipExport(args, f, in, entry->name, wadtype)) continue; wad2.num++; entry->size = entry->dsize = VFS_TELL(f)-entry->offset; } } wad2.offset = VFS_TELL(f); VFS_WRITE(f, wadentries, sizeof(*wadentries)*wad2.num); VFS_SEEK(f, 0); VFS_WRITE(f, &wad2, sizeof(wad2)); VFS_CLOSE(f); } free(wadentries); FileList_Release(&list); } #ifdef FTE_SDL static void SDLL_Loop(void); static void ImgTool_View(const char *inname, struct pendingtextureinfo *in); #endif int main(int argc, const char **argv) { static const struct { const char *alias; uploadfmt_t fmt; } fmtaliases[] = { {"BC1", PTI_BC1_RGBA}, {"BC2", PTI_BC2_RGBA}, {"BC3", PTI_BC3_RGBA}, {"BC4", PTI_BC4_R}, {"BC5", PTI_BC5_RG}, {"BC6", PTI_BC6_RGB_UFLOAT}, {"BC7", PTI_BC7_RGBA}, {"ETC1", PTI_ETC1_RGB8}, {"ETC2", PTI_ETC2_RGB8}, {"ETCP", PTI_ETC2_RGB8A1}, {"ETCA", PTI_ETC2_RGB8A8}, {"ASTC4x4", PTI_ASTC_4X4_LDR}, {"ASTC5x4", PTI_ASTC_5X4_LDR}, {"ASTC5x5", PTI_ASTC_5X5_LDR}, {"ASTC6x5", PTI_ASTC_6X5_LDR}, {"ASTC6x6", PTI_ASTC_6X6_LDR}, {"ASTC8x5", PTI_ASTC_8X5_LDR}, {"ASTC8x6", PTI_ASTC_8X6_LDR}, {"ASTC10x5",PTI_ASTC_10X5_LDR}, {"ASTC10x6",PTI_ASTC_10X6_LDR}, {"ASTC8x8", PTI_ASTC_8X8_LDR}, {"ASTC10x8",PTI_ASTC_10X8_LDR}, {"ASTC10x10",PTI_ASTC_10X10_LDR}, {"ASTC12x10",PTI_ASTC_12X10_LDR}, {"ASTC12x12",PTI_ASTC_12X12_LDR}, {"LUM8", PTI_L8}, {"RGBA", PTI_RGBA8}, {"RGB", PTI_RGB8}, }; enum { mode_unspecified, mode_info, #ifdef FTE_SDL mode_view, #endif mode_convert, mode_autotree, mode_genwadx, mode_genwad2, mode_genwad3, mode_extractwad, } mode = mode_unspecified; size_t u, f; qboolean nomoreopts = false; struct opts_s args; size_t files = 0; const char *outname = NULL; for (u = 1; u < countof(sh_config.texfmt); u++) sh_config.texfmt[u] = true; args.flags = 0; args.newpixelformat = PTI_INVALID; args.mipnum = 0; args.textype = PTI_ANY; args.defaultext = NULL; args.width = args.height = 0; if (argc==1) goto showhelp; for (u = 1; u < argc; u++) { if (*argv[u] == '-' && !nomoreopts) { if (!strcmp(argv[u], "--")) nomoreopts = true; else if (!strcmp(argv[u], "-?") || !strcmp(argv[u], "--help")) { showhelp: Con_Printf(DISTRIBUTION " Image Tool\n"); Con_Printf("show info : %s [-i] in.ktx [in2.ext ...]\n", argv[0]); Con_Printf("compress : %s [-c] --ext ktx --astc_6x6_ldr [--nomips] in.png [in2.png ...]\n", argv[0]); Con_Printf("compress : %s [-c] --ext dds --bc3 [--premul] [--nomips] in.png\n\tConvert pixel format (to bc3 aka dxt5) before writing to output file.\n", argv[0]); Con_Printf("convert : %s [-c] --ext png in.exr [in2.pcx ...]\n\tConvert input file(s) to different file format, while trying to preserve pixel formats.\n", argv[0]); Con_Printf("merge : %s -o output [--cube|--3d|--2darray|--cubearray] [--bc1] foo_*.png\n\tConvert to different file format, while trying to preserve pixel formats.\n", argv[0]); Con_Printf("recursive : %s -r [--ext dds] --astc_6x6_ldr destdir srcdir\n\tCompresses the files to dds (writing to an optionally different directory)\n", argv[0]); Con_Printf("decompress : %s --decompress [--exportmip 0] [--nomips] in.ktx out.png\n\tDecompresses any block-compressed pixel data.\n", argv[0]); Con_Printf("create mips: %s [-c] --ext mip [--bc1] [--resize width height] [--exportmip 2] *.dds\n", argv[0]); Con_Printf("create xwad: %s --genwadx [--exportmip 2] [--bc1] out.wad srcdir\n", argv[0]); Con_Printf("create wad : %s -w [--exportmip 2] out.wad *.mipsrcdir\n", argv[0]); Con_Printf("extract wad: %s -x [--ext png] src.wad\n", argv[0]); Con_Printf("extract bsp: %s -x [--ext png] src.bsp\n", argv[0]); Image_PrintInputFormatVersions(); Con_Printf("Supported compressed/interesting pixelformats are:\n"); for (f = 0; f < PTI_MAX; f++) { int bb,bw,bh,bd; Image_BlockSizeForEncoding(f, &bb,&bw,&bh,&bd); if (f >= PTI_ASTC_FIRST && f <= PTI_ASTC_LAST) { if (f >= PTI_ASTC_4X4_SRGB) continue; Con_Printf(" --%-16s %5.3g-bpp (requires astcenc)\n", Image_FormatName(f), 8*(float)bb/(bw*bh*bd)); } else if (f==PTI_BC1_RGB||f==PTI_BC1_RGBA||f==PTI_BC2_RGBA||f==PTI_BC3_RGBA||f==PTI_BC4_R||f==PTI_BC5_RG) Con_Printf(" --%-16s %5.3g-bpp (requires nvcompress)\n", Image_FormatName(f), 8*(float)bb/(bw*bh*bd)); else if (f==PTI_BC6_RGB_UFLOAT || f==PTI_BC6_RGB_SFLOAT || f==PTI_BC7_RGBA) Con_Printf(" --%-16s %5.3g-bpp (requires nvcompress 2.1+)\n", Image_FormatName(f), 8*(float)bb/(bw*bh*bd)); else if ( f==PTI_RGBA16F || f==PTI_RGBA32F || f==PTI_E5BGR9 || f==PTI_B10G11R11F || f==PTI_RGB565 || f==PTI_RGBA4444 || f==PTI_ARGB4444 || f==PTI_RGBA5551 || f==PTI_ARGB1555 || f==PTI_A2BGR10 || // f==PTI_R8 || // f==PTI_R16 || // f==PTI_R16F || // f==PTI_R32F || f==PTI_RG8 || f==PTI_L8 || f==PTI_L8A8 || 0) Con_Printf(" --%-16s %5.3g-bpp\n", Image_FormatName(f), 8*(float)bb/(bw*bh*bd)); // else // Con_DPrintf(" --%-16s %5.3g-bpp (unsupported)\n", Image_FormatName(f), 8*(float)bb/(bw*bh*bd)); } return EXIT_SUCCESS; } else if (!files && (!strcmp(argv[u], "-c") || !strcmp(argv[u], "--convert"))) mode = mode_convert; else if (!files && (!strcmp(argv[u], "-d") || !strcmp(argv[u], "--decompress"))) { //remove any (weird) gpu formats for (f = PTI_BC1_RGB; f < PTI_ASTC_LAST; f++) sh_config.texfmt[f] = false; mode = mode_convert; } else if (!files && (!strcmp(argv[u], "-r") || !strcmp(argv[u], "--auto"))) mode = mode_autotree; else if (!files && (!strcmp(argv[u], "-i") || !strcmp(argv[u], "--info"))) mode = mode_info; #ifdef FTE_SDL else if (!files && (!strcmp(argv[u], "-v") || !strcmp(argv[u], "--view"))) mode = mode_view; #endif else if (!files && (!strcmp(argv[u], "-w") || !strcmp(argv[u], "--genwad2"))) mode = mode_genwad2; else if (!files && (!strcmp(argv[u], "-w") || !strcmp(argv[u], "--genwad3"))) mode = mode_genwad3; else if (!files && (!strcmp(argv[u], "-w") || !strcmp(argv[u], "--genwadx"))) mode = mode_genwadx; else if (!files && (!strcmp(argv[u], "-x") || !strcmp(argv[u], "--extractwad"))) mode = mode_extractwad; else if (!files && (!strcmp(argv[u], "-v") || !strcmp(argv[u], "--verbose"))) verbose = true; else if (!files && (!strcmp(argv[u], "-o") || !strcmp(argv[u], "--outfile"))) { if (u+1 < argc) outname = argv[++u]; else { Con_Printf("--outfile requires output filename\n"); return 1; } } else if (!files && (!strcmp(argv[u], "-p") || !strcmp(argv[u], "--palette"))) { if (u+1 < argc) palette = argv[++u]; else { Con_Printf("--palette requires palette filename\n"); return 1; } } else if (!strcmp(argv[u], "--resize")) { if (u+2 < argc) { args.width = atoi(argv[++u]); args.height = atoi(argv[++u]); } else { Con_Printf("--resize requires width+height values\n"); return 1; } } else if (!strcmp(argv[u], "--2d")) args.textype = PTI_2D; else if (!strcmp(argv[u], "--3d")) args.textype = PTI_3D; else if (!strcmp(argv[u], "--cube")) args.textype = PTI_CUBE; else if (!strcmp(argv[u], "--2darray")) args.textype = PTI_2D_ARRAY; else if (!strcmp(argv[u], "--cubearray")) args.textype = PTI_CUBE_ARRAY; else if (!strcmp(argv[u], "--nomips") ) args.flags |= IF_NOMIPMAP; else if (!strcmp(argv[u], "--mips")) args.flags &= ~IF_NOMIPMAP; else if (!strcmp(argv[u], "--premul") ) args.flags |= IF_PREMULTIPLYALPHA; else if (!strcmp(argv[u], "--nopremul")) args.flags &= ~IF_PREMULTIPLYALPHA; else if (!strcmp(argv[u], "--ext")) { if (mode == mode_unspecified) mode = mode_convert; if (u+1 < argc) args.defaultext = argv[++u]; else { Con_Printf("--ext requires output extension\n"); return 1; } } else if (!strcmp(argv[u], "--exportmip")) { char *e = "erk"; if (u+1 < argc) args.mipnum = strtoul(argv[++u], &e, 10); if (*e) { Con_Printf("--exportmip requires trailing numeric argument\n"); return 1; } } else { if (argv[u][1] == '-') { //try aliases first. for (f = 0; f < countof(fmtaliases); f++) { if (!strcasecmp(argv[u]+2, fmtaliases[f].alias)) { args.newpixelformat = fmtaliases[f].fmt; if (mode == mode_unspecified) mode = mode_convert; break; } } if (f < countof(fmtaliases)) continue; //now try our formal format names for (f = 0; f < PTI_MAX; f++) { if (!strcasecmp(argv[u]+2, Image_FormatName(f))) { args.newpixelformat = f; if (mode == mode_unspecified) mode = mode_convert; break; } } if (f < PTI_MAX) continue; //nope, not a format name } Con_Printf("Unknown arg %s\n", argv[u]); goto showhelp; } argv[u] = NULL; } else argv[files++] = argv[u]; } ImgTool_SetupPalette(); if (mode == mode_unspecified && args.textype!=PTI_ANY) mode = mode_convert; if (!args.defaultext) { if (mode == mode_unspecified) { #ifdef FTE_SDL mode = mode_view; #else mode = mode_info; #endif } if (mode == mode_extractwad) args.defaultext = "png"; //something the user expects to be able to view easily (and lossless) else if (args.newpixelformat >= PTI_BC1_RGB && args.newpixelformat < PTI_BC4_R) args.defaultext = "dds"; else args.defaultext = "ktx"; } else { if (mode == mode_unspecified) mode = mode_convert; } if (mode == mode_info) { //just print info about each listed file. for (u = 0; u < files; u++) ImgTool_Enumerate(&args, argv[u], ImgTool_PrintInfo); } else if (mode == mode_convert && args.textype!=PTI_ANY && outname) //overwrite input { ImgTool_Convert(&args, ImgTool_Combine(&args, argv, files), "combined", outname); } else if (mode == mode_convert && args.textype==PTI_ANY && (!outname||files==1)) //list of files (output filenames will be generated according to -ext arg) { //-c src1 src2 src3 for (u = 0; u < files; u++) ImgTool_Convert(&args, ImgTool_Read(&args, argv[u]), argv[u], NULL); } else if (mode == mode_autotree && files == 2) ImgTool_TreeConvert(&args, argv[0], argv[1]); else if ((mode == mode_genwad2 || mode == mode_genwad3 || mode == mode_genwadx)) ImgTool_WadConvert(&args, argv[0], argv+1, files-1, mode-(mode_genwadx-1)); else if ((mode == mode_extractwad) && files == 1) ImgTool_WadExtract(&args, argv[0]); #ifdef FTE_SDL else if (mode == mode_view) { for (u = 0; u < files; u++) ImgTool_Enumerate(&args, argv[u], ImgTool_View); SDLL_Loop(); } #endif else { printf("%u files\n", (int)files); printf("unsupported arg count for mode\n"); return EXIT_FAILURE; } return EXIT_SUCCESS; } #ifdef FTE_SDL #include struct sdlwindow_s { SDL_Window *w; SDL_Renderer *r; int width; int height; float scale; size_t texshown; size_t texcount; struct { char *name; size_t w, h; uploadfmt_t fmt; SDL_Texture *t; } *tex; }; static struct { qboolean inited; qboolean tried; size_t windowcount; int (SDLCALL *Init) (Uint32 flags); SDL_Window * (SDLCALL *CreateWindow) (const char *title, int x, int y, int w, int h, Uint32 flags); void * (SDLCALL *SetWindowData) (SDL_Window * window, const char *name, void *userdata); void * (SDLCALL *GetWindowData) (SDL_Window * window, const char *name); void (SDLCALL *SetWindowTitle) (SDL_Window * window, const char *title); void (SDLCALL *SetWindowSize) (SDL_Window * window, int w, int h); Uint32 (SDLCALL *GetWindowFlags) (SDL_Window * window); SDL_Renderer * (SDLCALL *CreateRenderer) (SDL_Window * window, int index, Uint32 flags); int (SDLCALL *GetRendererInfo) (SDL_Renderer * renderer, SDL_RendererInfo * info); SDL_Texture * (SDLCALL *CreateTexture) (SDL_Renderer * renderer, Uint32 format, int access, int w, int h); int (SDLCALL *UpdateTexture) (SDL_Texture * texture, const SDL_Rect * rect, const void *pixels, int pitch); int (SDLCALL *RenderCopy) (SDL_Renderer * renderer, SDL_Texture * texture, const SDL_Rect * srcrect, const SDL_Rect * dstrect); void (SDLCALL *RenderPresent) (SDL_Renderer * renderer); int (SDLCALL *WaitEvent) (SDL_Event * event); int (SDLCALL *PollEvent) (SDL_Event * event); SDL_Window * (SDLCALL *GetWindowFromID) (Uint32 id); void (SDLCALL *DestroyTexture) (SDL_Texture * texture); void (SDLCALL *DestroyRenderer) (SDL_Renderer * renderer); void (SDLCALL *DestroyWindow) (SDL_Window * window); void (SDLCALL *Quit) (void); struct sdlwindow_s *texview; } sdl; static qboolean SDLL_Setup(void) { static dllfunction_t funcs[] = { {(void**)&sdl.Init, "SDL_Init"}, {(void**)&sdl.CreateWindow, "SDL_CreateWindow"}, {(void**)&sdl.SetWindowData, "SDL_SetWindowData"}, {(void**)&sdl.GetWindowData, "SDL_GetWindowData"}, {(void**)&sdl.SetWindowTitle, "SDL_SetWindowTitle"}, {(void**)&sdl.SetWindowSize, "SDL_SetWindowSize"}, {(void**)&sdl.GetWindowFlags, "SDL_GetWindowFlags"}, {(void**)&sdl.CreateRenderer, "SDL_CreateRenderer"}, {(void**)&sdl.GetRendererInfo, "SDL_GetRendererInfo"}, {(void**)&sdl.CreateTexture, "SDL_CreateTexture"}, {(void**)&sdl.UpdateTexture, "SDL_UpdateTexture"}, {(void**)&sdl.RenderCopy, "SDL_RenderCopy"}, {(void**)&sdl.RenderPresent, "SDL_RenderPresent"}, {(void**)&sdl.WaitEvent, "SDL_WaitEvent"}, {(void**)&sdl.PollEvent, "SDL_PollEvent"}, {(void**)&sdl.GetWindowFromID, "SDL_GetWindowFromID"}, {(void**)&sdl.DestroyTexture, "SDL_DestroyTexture"}, {(void**)&sdl.DestroyRenderer, "SDL_DestroyRenderer"}, {(void**)&sdl.DestroyWindow, "SDL_DestroyWindow"}, {(void**)&sdl.Quit, "SDL_Quit"}, {NULL,NULL} }; if (!sdl.tried) { sdl.tried = true; #ifdef _WIN32 if (!Sys_LoadLibrary("SDL2.dll", funcs)) #else if (!Sys_LoadLibrary("libSDL2-2.0.so.0", funcs)) if (!Sys_LoadLibrary("libSDL2-2.0.so", funcs)) if (!Sys_LoadLibrary("libSDL2-2.so", funcs)) if (!Sys_LoadLibrary("libSDL2.so", funcs)) if (!Sys_LoadLibrary("SDL2.so", funcs)) #endif { printf("Unable to load SDL2 library\n"); return sdl.inited; } sdl.Init(SDL_INIT_VIDEO); sdl.inited = true; } return sdl.inited; } static void SDLL_KillWindow(struct sdlwindow_s *wc) { while (wc->texcount --> 0) { if (wc->tex[wc->texcount].t) sdl.DestroyTexture(wc->tex[wc->texcount].t); BZ_Free(wc->tex[wc->texcount].name); } BZ_Free(wc->tex); if (wc->r) sdl.DestroyRenderer(wc->r); if (wc->w) sdl.DestroyWindow(wc->w); Z_Free(wc); sdl.windowcount--; if (sdl.texview == wc) sdl.texview = NULL; } static void SDLL_RepaintWindow(struct sdlwindow_s *wc) { if (wc->texshown < wc->texcount) { SDL_Rect dest; dest.x=dest.y=0; dest.w=dest.h=1; sdl.RenderCopy(wc->r, wc->tex[wc->texshown].t, &dest, NULL); dest.w = wc->tex[wc->texshown].w*wc->scale; dest.h = wc->tex[wc->texshown].h*wc->scale; dest.x = (wc->width - dest.w)/2; dest.y = (wc->height - dest.h)/2; sdl.RenderCopy(wc->r, wc->tex[wc->texshown].t, NULL, &dest); sdl.RenderPresent(wc->r); } } static void SDLL_Change(struct sdlwindow_s *wc, size_t newshown) { if (newshown < wc->texcount) { int w, h; char title[512]; wc->texshown = newshown; if (wc->texcount==1) snprintf(title, sizeof(title), "%s %s", wc->tex[wc->texshown].name, Image_FormatName(wc->tex[wc->texshown].fmt)); else snprintf(title, sizeof(title), "[%u/%u] %s %s", 1+(unsigned int)newshown, (unsigned int)wc->texcount, wc->tex[wc->texshown].name, Image_FormatName(wc->tex[wc->texshown].fmt)); sdl.SetWindowTitle(wc->w, title); w = wc->tex[wc->texshown].w * wc->scale; h = wc->tex[wc->texshown].h * wc->scale; if (w > 1024) w = 1024; if (h > 768) h = 768; if (wc->width < w || wc->height < h) if (!(sdl.GetWindowFlags(wc->w) & (SDL_WINDOW_MAXIMIZED|SDL_WINDOW_FULLSCREEN))) //SetWindowSize seems to bug out on linux when its maximized. { wc->width = w; wc->height = h; sdl.SetWindowSize(wc->w, w, h); } SDLL_RepaintWindow(wc); } } static void SDLL_Event(SDL_Event *ev) { struct sdlwindow_s *wc; switch (ev->type) { case SDL_KEYDOWN: wc = sdl.GetWindowData(sdl.GetWindowFromID(ev->key.windowID), "uptr"); if (!wc) break; switch(ev->key.keysym.sym) { case SDLK_ESCAPE: case SDLK_q: SDLL_KillWindow(wc); break; case SDLK_LEFT: wc->scale = 1; SDLL_Change(wc, wc->texshown-1); break; case SDLK_RIGHT: wc->scale = 1; SDLL_Change(wc, wc->texshown+1); break; case SDLK_UP: wc->scale /= 0.9; if (wc->scale > 4) wc->scale = 4; SDLL_Change(wc, wc->texshown); break; case SDLK_DOWN: wc->scale *= 0.9; if (wc->scale < 0.25) wc->scale = 0.25; SDLL_Change(wc, wc->texshown); break; } break; case SDL_WINDOWEVENT: wc = sdl.GetWindowData(sdl.GetWindowFromID(ev->window.windowID), "uptr"); if (!wc) break; switch (ev->window.event) { case SDL_WINDOWEVENT_CLOSE: SDLL_KillWindow(wc); break; case SDL_WINDOWEVENT_SIZE_CHANGED: wc->width = ev->window.data1; wc->height = ev->window.data2; break; case SDL_WINDOWEVENT_EXPOSED: SDLL_RepaintWindow(wc); break; } break; //don't bother with SDL_QUIT, it doesn't get sent if we kill the last window via a keypress. just count live windows instead. default: // printf("event type %x\n", ev->type); break; } } static void SDLL_Loop(void) { SDL_Event ev; if (!sdl.inited) return; if (sdl.texview) SDLL_Change(sdl.texview, sdl.texview->texshown); while (sdl.windowcount && sdl.WaitEvent(&ev)) SDLL_Event(&ev); sdl.Quit(); } static void ImgTool_View(const char *inname, struct pendingtextureinfo *in) { unsigned int sdlfmt; SDL_RendererInfo rinfo; int s; qboolean outformats[PTI_MAX] = {false}; struct sdlwindow_s *wc; SDL_Event ev; uploadfmt_t origencoding; if (!in || in->mipcount < 1 || in->mip[0].width <= 0 || in->mip[0].height <= 0) return; origencoding = in->encoding; if (!SDLL_Setup()) { ImgTool_PrintInfo(inname, in); return; } while (sdl.windowcount>=64 && sdl.WaitEvent(&ev)) SDLL_Event(&ev); if (sdl.texview) wc = sdl.texview; else { sdl.texview = wc = Z_Malloc(sizeof(*wc)); wc->scale = 2; s = 1; while ( (in->mip[0].width*s < 256 && in->mip[0].height*s < 512)|| (in->mip[0].height*s < 256 && in->mip[0].width*s < 512)) s<<=1; wc->w = sdl.CreateWindow("textureview", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 64, 64, SDL_WINDOW_RESIZABLE); sdl.windowcount++; if (wc->w) { sdl.SetWindowData(wc->w, "uptr", wc); //needs a rendering context too wc->r = sdl.CreateRenderer(wc->w, -1, SDL_RENDERER_SOFTWARE); if (!wc->r) { printf("Unable to create rendering context\n"); SDLL_KillWindow(wc); return; } } } if (wc->r) { //figure out which formats we can pass to sdl sdl.GetRendererInfo(wc->r, &rinfo); while (rinfo.num_texture_formats --> 0) { switch(rinfo.texture_formats[rinfo.num_texture_formats]) { //packed formats use hex ordering in both apis. case SDL_PIXELFORMAT_RGB565: outformats[PTI_RGB565] = true; break; // case SDL_PIXELFORMAT_BGR565: outformats[PTI_BGR565] = true; break; case SDL_PIXELFORMAT_RGBA4444: outformats[PTI_RGBA4444] = true; break; case SDL_PIXELFORMAT_ABGR4444: outformats[PTI_ARGB4444] = true; break; case SDL_PIXELFORMAT_RGBA5551: outformats[PTI_RGBA5551] = true; break; case SDL_PIXELFORMAT_ARGB1555: outformats[PTI_ARGB1555] = true; break; // case SDL_PIXELFORMAT_ARGB2101010: outformats[PTI_A2RGB10] = true; break; // case SDL_PIXELFORMAT_ABGR2101010: outformats[PTI_A2BGR10] = true; break; //these sdl aliases are for explicit byte orders, rather than packed. case SDL_PIXELFORMAT_RGBA32: outformats[PTI_RGBA8] = true; break; case SDL_PIXELFORMAT_BGRA32: outformats[PTI_BGRA8] = true; break; case SDL_PIXELFORMAT_BGR24: outformats[PTI_RGB8] = true; break; case SDL_PIXELFORMAT_RGB24: outformats[PTI_BGR8] = true; break; // case SDL_PIXELFORMAT_ARGB32: outformats[PTI_ARGB8] = true; break; // case SDL_PIXELFORMAT_ABGR32: outformats[PTI_ABGR8] = true; break; /*#if SDL_BYTEORDER == SDL_BIG_ENDIAN case SDL_PIXELFORMAT_RGBX8888: outformats[PTI_RGBX8] = true; break; case SDL_PIXELFORMAT_BGRX8888: outformats[PTI_BGRX8] = true; break; #else case SDL_PIXELFORMAT_XBGR8888: outformats[PTI_RGBX8] = true; break; case SDL_PIXELFORMAT_XRGB8888: outformats[PTI_BGRX8] = true; break; #endif*/ } } //convert our image, if needed. if (!outformats[in->encoding]) Image_ChangeFormat(in, outformats, PTI_INVALID, inname); if (!outformats[in->encoding]) sdlfmt = SDL_PIXELFORMAT_UNKNOWN,printf("Unable to convert to usable pixel format\n"); else switch(in->encoding) { //packed formats case PTI_RGB565: sdlfmt = SDL_PIXELFORMAT_RGB565; break; // case PTI_BGR565: sdlfmt = SDL_PIXELFORMAT_BGR565; break; case PTI_RGBA4444: sdlfmt = SDL_PIXELFORMAT_RGBA4444; break; case PTI_ARGB4444: sdlfmt = SDL_PIXELFORMAT_ARGB4444; break; case PTI_RGBA5551: sdlfmt = SDL_PIXELFORMAT_RGBA5551; break; case PTI_ARGB1555: sdlfmt = SDL_PIXELFORMAT_ARGB1555; break; // case PTI_A2RGB10: sdlfmt = SDL_PIXELFORMAT_ARGB2101010; break; // case PTI_A2BGR10: sdlfmt = SDL_PIXELFORMAT_ABGR2101010; break; //byte-ordered formats. case PTI_RGBA8: sdlfmt = SDL_PIXELFORMAT_RGBA32; break; case PTI_BGRA8: sdlfmt = SDL_PIXELFORMAT_BGRA32; break; case PTI_RGB8: sdlfmt = SDL_PIXELFORMAT_RGB24; break; case PTI_BGR8: sdlfmt = SDL_PIXELFORMAT_BGR24; break; /*#if SDL_BYTEORDER == SDL_BIG_ENDIAN case PTI_RGBX8: sdlfmt = SDL_PIXELFORMAT_RGBX8888; break; case PTI_BGRX8: sdlfmt = SDL_PIXELFORMAT_BGRX8888; break; #else case PTI_RGBX8: sdlfmt = SDL_PIXELFORMAT_XBGR8888; break; case PTI_BGRX8: sdlfmt = SDL_PIXELFORMAT_XRGB8888; break; #endif*/ default: sdlfmt = SDL_PIXELFORMAT_UNKNOWN; break; //shouldn't happen. } wc->tex = realloc(wc->tex, sizeof(*wc->tex)*(wc->texcount+1)); wc->tex[wc->texcount].name = Z_StrDup(inname); wc->tex[wc->texcount].w = in->mip[0].width; wc->tex[wc->texcount].h = in->mip[0].height; wc->tex[wc->texcount].fmt = origencoding; wc->tex[wc->texcount].t = sdl.CreateTexture(wc->r, sdlfmt, SDL_TEXTUREACCESS_STATIC, in->mip[0].width, in->mip[0].height); //which needs a texture... if (wc->tex[wc->texcount].t) { sdl.UpdateTexture(wc->tex[wc->texcount].t, NULL, in->mip[0].data, in->mip[0].datasize/in->mip[0].height); //with our image data if (!wc->texcount++) SDLL_Change(wc, 0); while (sdl.PollEvent(&ev)) SDLL_Event(&ev); } else { printf("Unable to create texture %s %u*%u\n", Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height); BZ_Free(wc->tex[wc->texcount].name); } } else { printf("Unable to create window\n"); SDLL_KillWindow(wc); } } #endif #endif ================================================ FILE: iqm/LICENSE ================================================ Copyright (c) 2010-2016 Lee Salzman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: iqm/Makefile ================================================ #Note: This makefile builds the iqm tool without any fte dependancies # This means no .mdl export nor extended model format support (read: no gltf/glb import) CXXFLAGS= -O3 -fomit-frame-pointer override CXXFLAGS+= -Wall -fsigned-char IQM_OBJS= \ iqm.o UPGRADE_OBJS= \ upgrade.o default: all all: iqmtool #upgrade clean: -$(RM) $(IQM_OBJS) $(UPGRADE_OBJS) iqmtool upgrade iqmtool: $(IQM_OBJS) $(CXX) $(CXXFLAGS) -o iqmtool $(IQM_OBJS) upgrade: $(UPGRADE_OBJS) $(CXX) $(CXXFLAGS) -o upgrade $(UPGRADE_OBJS) %.o : %.cpp $(CXX) -o $@ -c $< ================================================ FILE: iqm/Makefile.mingw ================================================ CXXFLAGS= -O3 -fomit-frame-pointer override CXXFLAGS+= -Wall -fsigned-char PLATFORM= $(shell uname -s) ifeq (,$(findstring MINGW,$(PLATFORM))) CXX=i686-w64-mingw32-g++ endif LIBS= -mwindows -static -static-libgcc -static-libstdc++ IQM_OBJS= \ iqm.o UPGRADE_OBJS= \ upgrade.o default: all all: iqmtool.exe #upgrade.exe clean: -$(RM) $(IQM_OBJS) $(UPGRADE_OBJS) iqmtool.exe upgrade.exe iqmtool.exe: $(IQM_OBJS) $(CXX) $(CXXFLAGS) -o iqmtool.exe $(IQM_OBJS) $(LIBS) upgrade.exe: $(UPGRADE_OBJS) $(CXX) $(CXXFLAGS) -o upgrade.exe $(UPGRADE_OBJS) $(LIBS) ================================================ FILE: iqm/README.txt ================================================ FTE's fork of Lee Salzman's commandline IQM exporter, based upon 'IQM Developer Kit 2015-08-03'. there is no blender integration - export your files to a supported format first. Main changes: now utilises command files instead of needing the weird commandline stuff (although that should still mostly work). additional mesh properties and multiple mesh files, providing for proper hitmesh support, as well as some other things. more verbose text output, additional shader prefixes. bone renaming+regrouping animation unpacking, for qc mods that still insist on animating the lame way. Supported import formats: .md5mesh .md5anim .iqe .smd .fbx .obj Unless you're doing complex stuff like any of the above, there's probably not all that much difference. There may be some commandline behaviour differences. Command File Format: output - specifies the output file name. you should only have one of each type of output. output_qmdl - specifies which file to write a quake1-format model. May only occur once. output_md16 - specifies the filename to write a quakeforge 16-bit md16 model to (a upgraded variation of quake's format). May only occur once. output_md3 - specifies the filename to write a quake3 md3 file to. May only occur once. exec - exec the specified command file, before parsing the rest of the current file. hitbox - generates a hitmesh as a bbox centered around the bone in the base pose (the hitbox will rotate/move with animations). The bodynum will be visible to gamecode, and may merge with other hitboxes with the same group. modelflags - enables the specified bit in the iqm header. supported names include q1_rocket, q1_grenade, q1_gib, q1_rotate, q1_tracer1, q1_zomgib, q1_tracer2, q1_tracer3 - defined below and applied as the defaults to the following import lines as well as mesh lines. mesh [MESH PROPERTIES LIST] - provides overrides for a single named mesh (properties used will be those as they're already defined, if not otherwise listed). bone [rename ] [group ] - provides bone renaming and grouping. try to avoid renaming two bones to the same resulting name... groups may have limitations if a parent/child relationship cannot be honoured. lowest group numbers come before higher groups. by default bones will inherit their group from their parent. - defined below and applied as the defaults to the following import lines. import [IMPORT PROPERTIES] [MESH PROPERTIES] - imports the meshes and animations from the specified file. Mesh Properties: contents - 'body' or 'empty' are the two that are most likely to be used. 'solid' may also be desired, or possibly also 'corpse'. surfaceflags - 'q3_nodraw/fte_nodraw' body - this is the 'body' value reported to gamecode when a trace hits this surface. geomset - by configuring this, you can have models display different body parts for different character models. lodrange - not yet implemented by the engine. 0 for both is the default. Anim/Import Properties: name - the imported animations will be assigned this name. May be problematic if the imported file(s) share the same name, so try to avoid using this at global scope. fps - framerate for any imported animations. loop - flags animations as looping. clamp - disables looping. unpack - seperates each pose of the animations into a seperate single-pose animation for compat with q1 or q2-style model animations. pack - disables unpacking again. nomesh <1|0> - discards all meshed from the affected files. noanim <1|0> - discards animations from the affected files, does not disclude the base pose. materialprefix - provides a text prefix on the material name, which should make it easier for editors while still honouring shader paths. start - the fist pose to import. end - the last pose to import. rotate - rotates the model scale - rescales the model origin - moves the thing event [ANIM,] <"EVENTDATA"> - embeds event info within the animation, for stuff like footsteps. How this is used depends on the engine... If used at global scope, can be reset with 'event reset' in order to not apply to later files. ================================================ FILE: iqm/iqm.cpp ================================================ #ifdef IQMTOOL //building as part of fte. we can pull in fte components for extra features. #define FTEPLUGIN #ifndef GLQUAKE #define GLQUAKE //this is shit, but ensures index sizes come out the right size #endif #include "../plugins/plugin.h" #include "com_mesh.h" #define IQMTOOL_MDLEXPORT #else //building standalone. any fte modules cannot be used. #endif #include "util.h" #define IQM_UNPACK (1u<<31) //animations will be unpacked into individual frames-as-animations (ie: no more framegroups) #define IQM_ALLPRIVATE (IQM_UNPACK) bool noext = false; bool verbose = false; bool quiet = false; bool stripbones = false; //strip all bone+anim info. struct ejoint { const char *name; int parent; ejoint() : name(NULL), parent(-1) {} }; struct triangle { uint vert[3]; triangle() {} triangle(uint v0, uint v1, uint v2) { vert[0] = v0; vert[1] = v1; vert[2] = v2; } }; vector triangles, neighbors; struct mesh { uint name, material, numskins; uint firstvert, numverts; uint firsttri, numtris; mesh() : name(0), material(0), firstvert(0), numverts(0), firsttri(0), numtris(0) {} }; vector meshes; struct meshprop { uint contents; uint surfaceflags; uint body; uint geomset; uint geomid; float mindist; float maxdist; meshprop() : contents(0x02000000), surfaceflags(0), body(0), geomset(~0u), geomid(0), mindist(0), maxdist(0) {}; }; vector meshes_fte; //extra crap uint modelflags; //q1 uses this. struct event_fte { uint anim; float timestamp; //pose indexes. uint evcode; const char *evdata_str; uint evdata_idx; }; vector events_fte; vector meshskins; vector skinframes; struct anim { uint name; uint firstframe, numframes; float fps; uint flags; anim() : name(0), firstframe(0), numframes(0), fps(0), flags(0) {} }; vector anims; struct joint { int group; uint name; int parent; float pos[3], orient[4], scale[3]; joint() : name(0), parent(-1) { memset(pos, 0, sizeof(pos)); memset(orient, 0, sizeof(orient)); memset(scale, 0, sizeof(scale)); } }; vector joints; //for meshes struct pose { const char *name; int parent; uint flags; float offset[10], scale[10]; pose() : name(NULL), parent(-1), flags(0) { memset(offset, 0, sizeof(offset)); memset(scale, 0, sizeof(scale)); } }; vector poses; //aka: animation joints struct framebounds { Vec3 bbmin, bbmax; double xyradius, radius; framebounds() : bbmin(0, 0, 0), bbmax(0, 0, 0), xyradius(0), radius(0) {} }; vector bounds; struct transform { Vec3 pos; Quat orient; Vec3 scale; transform() {} transform(const Vec3 &pos, const Quat &orient, const Vec3 &scale = Vec3(1, 1, 1)) : pos(pos), orient(orient), scale(scale) {} }; struct frame { struct framepose { int remap; const char *bonename; int boneparent; transform tr; framepose() : bonename(""),boneparent(-1),tr() {} framepose(ejoint &j, transform t) : bonename(j.name),boneparent(j.parent),tr(t) {} }; vector pose; }; vector frames; vector stringdata, commentdata; uint numfverts; //verts generated so far struct boneoverride { const char *name; bool used; struct prop { const char *rename; int group; prop() : rename(NULL), group(-1) {} } props; boneoverride() : used(false), props(){} }; vector boneoverrides; struct meshoverride { const char *name; meshprop props; }; vector meshoverrides; struct hitbox { int body; const char *bone; Vec3 mins, maxs; }; struct filespec { const char *file; const char *name; double fps; uint flags; int startframe; int endframe; meshprop meshprops; const char *materialprefix; const char *materialsuffix; bool ignoresurfname; Quat rotate; float scale; Vec3 translate; bool nomesh; bool noanim; vector events; filespec() { reset(); } void reset() { file = NULL; name = NULL; fps = 24; flags = 0; startframe = 0; endframe = -1; meshprops = meshprop(); materialprefix = NULL; materialsuffix = NULL; ignoresurfname = false; rotate = Quat(0, 0, 0, 1); scale = 1; translate = Vec3(0,0,0); nomesh = false; noanim = false; events.setsize(0); } }; struct sharedstring { uint offset; sharedstring() {} sharedstring(const char *s) : offset(stringdata.length()) { stringdata.put(s, strlen(s)+1); } }; static inline bool htcmp(const char *x, const sharedstring &s) { return htcmp(x, &stringdata[s.offset]); } hashtable stringoffsets; uint sharestring(const char *s) { if(stringdata.empty()) stringoffsets.access("", 0); return stringoffsets.access(s ? s : "", stringdata.length()); } struct blendcombo { int sorted; double weights[4]; uchar bones[4]; blendcombo() : sorted(0) {} void reset() { sorted = 0; } void addweight(double weight, int bone) { if(weight <= 1e-3) return; loopk(sorted) if(weight > weights[k]) { for(int l = min(sorted-1, 2); l >= k; l--) { weights[l+1] = weights[l]; bones[l+1] = bones[l]; } weights[k] = weight; bones[k] = bone; if(sorted<4) sorted++; return; } if(sorted>=4) return; weights[sorted] = weight; bones[sorted] = bone; sorted++; } void finalize() { loopj(4-sorted) { weights[sorted+j] = 0; bones[sorted+j] = 0; } if(sorted <= 0) return; double total = 0; loopj(sorted) total += weights[j]; total = 1.0/total; loopj(sorted) weights[j] *= total; } void serialize(uchar *vweights) const { int total = 0; loopk(4) total += (vweights[k] = uchar(0.5 + weights[k]*255)); if(sorted <= 0) return; while(total > 255) { loopk(4) if(vweights[k] > 0 && total > 255) { vweights[k]--; total--; } } while(total < 255) { loopk(4) if(vweights[k] < 255 && total < 255) { vweights[k]++; total++; } } } bool operator==(const blendcombo &c) { loopi(4) if(weights[i] != c.weights[i] || bones[i] != c.bones[i]) return false; return true; } bool operator!=(const blendcombo &c) { loopi(4) if(weights[i] != c.weights[i] || bones[i] != c.bones[i]) return true; return false; } }; vector mpositions; vector mblends; static bool parseindex(char *&c, int &val) { while(isspace(*c)) c++; char *end = NULL; int rval = strtol(c, &end, 10); if(c == end) return false; val = rval; c = end; return true; } static double parseattrib(char *&c, double ival = 0) { while(isspace(*c)) c++; char *end = NULL; double val = strtod(c, &end); if(c == end) val = ival; else c = end; return val; } static bool maybeparseattrib(char *&c, double &result) { while(isspace(*c)) c++; char *end = NULL; double val = strtod(c, &end); if(c == end) return false; c = end; result = val; return true; } #if 0 static bool parsename(char *&c, char *buf, int bufsize = sizeof(string)) { while(isspace(*c)) c++; char *end; if(*c == '"') { c++; end = c; while(*end && *end != '"') end++; copystring(buf, c, min(int(end-c+1), bufsize)); if(*end == '"') end++; } else { end = c; while(*end && !isspace(*end)) end++; copystring(buf, c, min(int(end-c+1), bufsize)); } if(c == end) return false; c = end; return true; } #endif static char *trimname(char *&c) { while(isspace(*c)) c++; char *start, *end; if(*c == '"') { c++; start = end = c; while(*end && *end != '"') end++; if(*end) { *end = '\0'; end++; } } else { start = end = c; while(*end && !isspace(*end)) end++; if(*end) { *end = '\0'; end++; } } c = end; return start; } static Vec4 parseattribs4(char *&c, const Vec4 &ival = Vec4(0, 0, 0, 0)) { Vec4 val; loopk(4) val[k] = parseattrib(c, ival[k]); return val; } static Vec3 parseattribs3(char *&c, const Vec3 &ival = Vec3(0, 0, 0)) { Vec3 val; loopk(3) val[k] = parseattrib(c, ival[k]); return val; } static blendcombo parseblends(char *&c) { blendcombo b; int index; while(parseindex(c, index)) { double weight = parseattrib(c, 0); b.addweight(weight, index); } b.finalize(); return b; } struct eanim { const char *name; int startframe, endframe; double fps; uint flags; eanim() : name(NULL), startframe(0), endframe(INT_MAX), fps(0), flags(0) {} }; struct emesh { const char *name, *material; int firsttri; bool used; bool hasexplicits; meshprop explicits; emesh() : name(NULL), material(NULL), firsttri(0), used(false), hasexplicits(false) {} emesh(const char *name, const char *material, int firsttri = 0) : name(name), material(material), firsttri(firsttri), used(false), hasexplicits(false) {} }; struct evarray { string name; uint type, format, size; evarray() : type(IQM_POSITION), format(IQM_FLOAT), size(3) { name[0] = '\0'; } evarray(uint type, uint format, uint size, const char *initname = "") : type(type), format(format), size(size) { copystring(name, initname); } }; struct esmoothgroup { enum { F_USED = 1<<0, F_UVSMOOTH = 1<<1 }; int key; float angle; int flags; esmoothgroup() : key(-1), angle(-1), flags(0) {} }; struct etriangle { int smoothgroup; uint vert[3], weld[3]; etriangle() : smoothgroup(-1) { } etriangle(int v0, int v1, int v2, int smoothgroup = -1) : smoothgroup(smoothgroup) { vert[0] = v0; vert[1] = v1; vert[2] = v2; } }; vector epositions, etexcoords, etangents, ecolors, ecustom[10]; vector enormals, ebitangents; vector eblends; vector etriangles; vector esmoothgroups; vector esmoothindexes; vector esmoothedges; vector ejoints; vector eposes; vector mjoints; vector eframes; vector eanims; vector emeshes; vector evarrays; hashtable enames; const char *getnamekey(const char *name) { char **exists = enames.access(name); if(exists) return *exists; char *key = newstring(name); enames[key] = key; return key; } struct weldinfo { int tri, vert; weldinfo *next; }; void weldvert(const vector &norms, const Vec4 &pos, weldinfo *welds, int &numwelds, unionfind &welder) { welder.clear(); int windex = 0; for(weldinfo *w = welds; w; w = w->next, windex++) { etriangle &wt = etriangles[w->tri]; esmoothgroup &wg = esmoothgroups[wt.smoothgroup]; int vindex = windex + 1; for(weldinfo *v = w->next; v; v = v->next, vindex++) { etriangle &vt = etriangles[v->tri]; esmoothgroup &vg = esmoothgroups[vt.smoothgroup]; if(wg.key != vg.key) continue; if(norms[w->tri].dot(norms[v->tri]) < max(wg.angle, vg.angle)) continue; if(((wg.flags | vg.flags) & esmoothgroup::F_UVSMOOTH) && etexcoords[wt.vert[w->vert]] != etexcoords[vt.vert[v->vert]]) continue; if(esmoothindexes.length() > max(w->vert, v->vert) && esmoothindexes[w->vert] != esmoothindexes[v->vert]) continue; if(esmoothedges.length()) { int w0 = w->vert, w1 = (w->vert+1)%3, w2 = (w->vert+2)%3; const Vec4 &wp1 = epositions[wt.vert[w1]], &wp2 = epositions[wt.vert[w2]]; int v0 = v->vert, v1 = (v->vert+1)%3, v2 = (v->vert+2)%3; const Vec4 &vp1 = epositions[vt.vert[v1]], &vp2 = epositions[vt.vert[v2]]; int wf = esmoothedges[w->tri], vf = esmoothedges[v->tri]; if((wp1 != vp1 || !(((wf>>w0)|(vf>>v0))&1)) && (wp1 != vp2 || !(((wf>>w0)|(vf>>v2))&1)) && (wp2 != vp1 || !(((wf>>w2)|(vf>>v0))&1)) && (wp2 != vp2 || !(((wf>>w2)|(vf>>v2))&1))) continue; } welder.unite(windex, vindex, -1); } } windex = 0; for(weldinfo *w = welds; w; w = w->next, windex++) { etriangle &wt = etriangles[w->tri]; wt.weld[w->vert] = welder.find(windex, -1, numwelds); if(wt.weld[w->vert] == uint(numwelds)) numwelds++; } } void smoothverts(bool areaweight = true) { if(etriangles.empty()) return; if(enormals.length()) { loopv(etriangles) { etriangle &t = etriangles[i]; loopk(3) t.weld[k] = t.vert[k]; } return; } if(etexcoords.empty()) loopv(esmoothgroups) esmoothgroups[i].flags &= ~esmoothgroup::F_UVSMOOTH; if(esmoothedges.length()) while(esmoothedges.length() < etriangles.length()) esmoothedges.add(7); vector tarea, tnorms; loopv(etriangles) { etriangle &t = etriangles[i]; Vec3 v0(epositions[t.vert[0]]), v1(epositions[t.vert[1]]), v2(epositions[t.vert[2]]); tnorms.add(tarea.add((v2 - v0).cross(v1 - v0)).normalize()); } int nextalloc = 0; vector allocs; hashtable welds(1<<12); loopv(etriangles) { etriangle &t = etriangles[i]; loopk(3) { weldinfo **next = &welds.access(epositions[t.vert[k]], NULL); if(! (nextalloc % 1024)) allocs.add(new weldinfo[1024]); weldinfo &w = allocs[nextalloc/1024][nextalloc%1024]; nextalloc++; w.tri = i; w.vert = k; w.next = *next; *next = &w; } } int numwelds = 0; unionfind welder; enumerate(welds, Vec4, vpos, weldinfo *, vwelds, weldvert(tnorms, vpos, vwelds, numwelds, welder)); loopv(allocs) delete[] allocs[i]; loopi(numwelds) enormals.add(Vec3(0, 0, 0)); loopv(etriangles) { etriangle &t = etriangles[i]; loopk(3) enormals[t.weld[k]]+= areaweight ? tarea[i] : tnorms[i]; } loopv(enormals) if(enormals[i] != Vec3(0, 0, 0)) enormals[i] = enormals[i].normalize(); } struct sharedvert { int index, weld; sharedvert() {} sharedvert(int index, int weld) : index(index), weld(weld) {} }; static inline bool htcmp(const sharedvert &v, const sharedvert &s) { if(epositions[v.index] != epositions[s.index]) return false; if(etexcoords.length() && etexcoords[v.index] != etexcoords[s.index]) return false; if(enormals.length() && enormals[v.weld] != enormals[s.weld]) return false; if(eblends.length() && eblends[v.index] != eblends[s.index]) return false; if(ecolors.length() && ecolors[v.index] != ecolors[s.index]) return false; loopi(10) if(ecustom[i].length() && ecustom[i][v.index] != ecustom[i][s.index]) return false; return true; } static inline uint hthash(const sharedvert &v) { return hthash(epositions[v.index]); } const struct vertexarraytype { const char *name; int code; } vatypes[] = { { "position", IQM_POSITION }, { "texcoord", IQM_TEXCOORD }, { "normal", IQM_NORMAL }, { "tangent", IQM_TANGENT }, { "blendindexes", IQM_BLENDINDEXES }, { "blendweights", IQM_BLENDWEIGHTS }, { "color", IQM_COLOR }, { "custom0", IQM_CUSTOM + 0 }, { "custom1", IQM_CUSTOM + 1 }, { "custom2", IQM_CUSTOM + 2 }, { "custom3", IQM_CUSTOM + 3 }, { "custom4", IQM_CUSTOM + 4 }, { "custom5", IQM_CUSTOM + 5 }, { "custom6", IQM_CUSTOM + 6 }, { "custom7", IQM_CUSTOM + 7 }, { "custom8", IQM_CUSTOM + 8 }, { "custom9", IQM_CUSTOM + 9 } }; int findvertexarraytype(const char *name) { loopi(sizeof(vatypes)/sizeof(vatypes[0])) { if(!strcasecmp(vatypes[i].name, name)) return vatypes[i].code; } return -1; } const struct vertexarrayformat { const char *name; int code; int size; } vaformats[] = { { "byte", IQM_BYTE, 1 }, { "ubyte", IQM_UBYTE, 1 }, { "short", IQM_SHORT, 2 }, { "ushort", IQM_USHORT, 2 }, { "int", IQM_INT, 4 }, { "uint", IQM_UINT, 4 }, { "half", IQM_HALF, 2 }, { "float", IQM_FLOAT, 4 }, { "double", IQM_DOUBLE, 8 } }; int findvertexarrayformat(const char *name) { loopi(sizeof(vaformats)/sizeof(vaformats[0])) { if(!strcasecmp(vaformats[i].name, name)) return vaformats[i].code; } return -1; } struct vertexarray { uint type, flags, format, size, offset, count; vector vdata; vertexarray(uint type, uint format, uint size) : type(type), flags(0), format(format), size(size), offset(0), count(0) {} int formatsize() const { return vaformats[format].size; } int bytesize() const { return size * vaformats[format].size; } }; vector vmap; vector varrays; vector vdata; struct halfdata { ushort val; halfdata(double d) { union { ullong i; double d; } conv; conv.d = d; ushort signbit = ushort((conv.i>>63)&1); ushort mantissa = ushort((conv.i>>(52-10))&0x3FF); int exponent = int((conv.i>>52)&0x7FF) - 1023 + 15; if(exponent <= 0) { mantissa |= 0x400; mantissa >>= min(1-exponent, 10+1); exponent = 0; } else if(exponent >= 0x1F) { mantissa = 0; exponent = 0x1F; } val = (signbit<<15) | (ushort(exponent)<<10) | mantissa; } }; template<> inline halfdata endianswap(halfdata n) { n.val = endianswap16(n.val); return n; } template static inline int remapindex(int i, const sharedvert &v) { return v.index; } template<> inline int remapindex(int i, const sharedvert &v) { return v.weld; } template<> inline int remapindex(int i, const sharedvert &v) { return i; } template static inline void putattrib(T &out, const U &val) { out = T(val); } template static inline void uroundattrib(T &out, const U &val, double scale) { out = T(clamp(0.5 + val*scale, 0.0, scale)); } template static inline void sroundattrib(T &out, const U &val, double scale, double low, double high) { double n = val*scale*0.5; out = T(clamp(n < 0 ? ceil(n - 1) : floor(n), low, high)); } template static inline void scaleattrib(T &out, const U &val) { putattrib(out, val); } template static inline void scaleattrib(char &out, const U &val) { sroundattrib(out, val, 255.0, -128.0, 127.0); } template static inline void scaleattrib(short &out, const U &val) { sroundattrib(out, val, 65535.0, -32768.0, 32767.0); } template static inline void scaleattrib(int &out, const U &val) { sroundattrib(out, val, 4294967295.0, -2147483648.0, 2147483647.0); } template static inline void scaleattrib(uchar &out, const U &val) { uroundattrib(out, val, 255.0); } template static inline void scaleattrib(ushort &out, const U &val) { uroundattrib(out, val, 65535.0); } template static inline void scaleattrib(uint &out, const U &val) { uroundattrib(out, val, 4294967295.0); } template static inline bool normalizedattrib() { return true; } template static inline void serializeattrib(const vertexarray &va, T *data, const U &attrib) { if(normalizedattrib()) switch(va.size) { case 4: scaleattrib(data[3], attrib.w); case 3: scaleattrib(data[2], attrib.z); case 2: scaleattrib(data[1], attrib.y); case 1: scaleattrib(data[0], attrib.x); } else switch(va.size) { case 4: putattrib(data[3], attrib.w); case 3: putattrib(data[2], attrib.z); case 2: putattrib(data[1], attrib.y); case 1: putattrib(data[0], attrib.x); } lilswap(data, va.size); } template static inline void serializeattrib(const vertexarray &va, T *data, const Vec3 &attrib) { if(normalizedattrib()) switch(va.size) { case 3: scaleattrib(data[2], attrib.z); case 2: scaleattrib(data[1], attrib.y); case 1: scaleattrib(data[0], attrib.x); } else switch(va.size) { case 3: putattrib(data[2], attrib.z); case 2: putattrib(data[1], attrib.y); case 1: putattrib(data[0], attrib.x); } lilswap(data, va.size); } template static inline void serializeattrib(const vertexarray &va, T *data, const blendcombo &blend) { if(TYPE == IQM_BLENDINDEXES) { switch(va.size) { case 4: putattrib(data[3], blend.bones[3]); case 3: putattrib(data[2], blend.bones[2]); case 2: putattrib(data[1], blend.bones[1]); case 1: putattrib(data[0], blend.bones[0]); } } else if(FMT == IQM_UBYTE) { uchar weights[4]; blend.serialize(weights); switch(va.size) { case 4: putattrib(data[3], weights[3]); case 3: putattrib(data[2], weights[2]); case 2: putattrib(data[1], weights[1]); case 1: putattrib(data[0], weights[0]); } } else { switch(va.size) { case 4: scaleattrib(data[3], blend.weights[3]); case 3: scaleattrib(data[2], blend.weights[2]); case 2: scaleattrib(data[1], blend.weights[1]); case 1: scaleattrib(data[0], blend.weights[0]); } } lilswap(data, va.size); } template void setupvertexarray(const vector &attribs, uint type, uint fmt, uint size, uint first) { const char *name = ""; loopv(evarrays) if(evarrays[i].type == type) { evarray &info = evarrays[i]; fmt = info.format; size = (uint)clamp((int)info.size, 1, 4); name = info.name; break; } if(type >= IQM_CUSTOM) { if(!name[0]) { defformatstring(customname, "custom%d", type-IQM_CUSTOM); type = IQM_CUSTOM + sharestring(customname); } else type = IQM_CUSTOM + sharestring(name); } int k; for (k = 0; k < varrays.length(); k++) { if (varrays[k].type == type && varrays[k].format == fmt && varrays[k].size == size) break; } if (k == varrays.length()) varrays.add(vertexarray(type, fmt, size)); vertexarray &va = varrays[k]; if (va.count != first) fatal("count != first"); //gaps are a problem. va.count += vmap.length(); int totalsize = va.bytesize() * vmap.length(); uchar *data = va.vdata.reserve(totalsize); va.vdata.advance(totalsize); loopv(vmap) { const T &attrib = attribs[remapindex(i, vmap[i])]; switch(va.format) { case IQM_BYTE: serializeattrib(va, (char *)data, attrib); break; case IQM_UBYTE: serializeattrib(va, (uchar *)data, attrib); break; case IQM_SHORT: serializeattrib(va, (short *)data, attrib); break; case IQM_USHORT: serializeattrib(va, (ushort *)data, attrib); break; case IQM_INT: serializeattrib(va, (int *)data, attrib); break; case IQM_UINT: serializeattrib(va, (uint *)data, attrib); break; case IQM_HALF: serializeattrib(va, (halfdata *)data, attrib); break; case IQM_FLOAT: serializeattrib(va, (float *)data, attrib); break; case IQM_DOUBLE: serializeattrib(va, (double *)data, attrib); break; } data += va.bytesize(); } } // linear speed vertex cache optimization from Tom Forsyth #define MAXVCACHE 32 struct triangleinfo { bool used; float score; uint vert[3]; triangleinfo() {} triangleinfo(uint v0, uint v1, uint v2) { vert[0] = v0; vert[1] = v1; vert[2] = v2; } }; struct vertexcache : listnode { int index, rank; float score; int numuses; triangleinfo **uses; vertexcache() : index(-1), rank(-1), score(-1.0f), numuses(0), uses(NULL) {} void calcscore() { if(numuses > 0) { score = 2.0f * powf(numuses, -0.5f); if(rank >= 3) score += powf(1.0f - (rank - 3)/float(MAXVCACHE - 3), 1.5f); else if(rank >= 0) score += 0.75f; } else score = -1.0f; } void removeuse(triangleinfo *t) { loopi(numuses) if(uses[i] == t) { uses[i] = uses[--numuses]; return; } } }; void maketriangles(vector &tris, const vector &mmap) { triangleinfo **uses = new triangleinfo *[3*tris.length()]; vertexcache *verts = new vertexcache[mmap.length()]; list vcache; loopv(tris) { triangleinfo &t = tris[i]; t.used = t.vert[0] == t.vert[1] || t.vert[1] == t.vert[2] || t.vert[2] == t.vert[0]; if(t.used) continue; loopk(3) verts[t.vert[k]].numuses++; } triangleinfo **curuse = uses; loopvrev(tris) { triangleinfo &t = tris[i]; if(t.used) continue; loopk(3) { vertexcache &v = verts[t.vert[k]]; if(!v.uses) { curuse += v.numuses; v.uses = curuse; } *--v.uses = &t; } } loopv(mmap) verts[i].calcscore(); triangleinfo *besttri = NULL; float bestscore = -1e16f; loopv(tris) { triangleinfo &t = tris[i]; if(t.used) continue; t.score = verts[t.vert[0]].score + verts[t.vert[1]].score + verts[t.vert[2]].score; if(t.score > bestscore) { besttri = &t; bestscore = t.score; } } //int reloads = 0; while(besttri) { besttri->used = true; triangle &t = triangles.add(); loopk(3) { vertexcache &v = verts[besttri->vert[k]]; if(v.index < 0) { v.index = vmap.length(); vmap.add(mmap[besttri->vert[k]]); } t.vert[k] = v.index; v.removeuse(besttri); if(v.rank >= 0) vcache.remove(&v)->rank = -1; // else reloads++; if(v.numuses <= 0) continue; vcache.insertfirst(&v); v.rank = 0; } int rank = 0; for(vertexcache *v = vcache.first(); v != vcache.end(); v = v->next) { v->rank = rank++; v->calcscore(); } besttri = NULL; bestscore = -1e16f; for(vertexcache *v = vcache.first(); v != vcache.end(); v = v->next) { loopi(v->numuses) { triangleinfo &t = *v->uses[i]; t.score = verts[t.vert[0]].score + verts[t.vert[1]].score + verts[t.vert[2]].score; if(t.score > bestscore) { besttri = &t; bestscore = t.score; } } } while(vcache.size > MAXVCACHE) vcache.removelast()->rank = -1; if(!besttri) loopv(tris) { triangleinfo &t = tris[i]; if(!t.used && t.score > bestscore) { besttri = &t; bestscore = t.score; } } } // printf("reloads: %d, worst: %d, best: %d\n", reloads, tris.length()*3, mmap.length()); delete[] uses; delete[] verts; } void calctangents(uint priortris, bool areaweight = true) { uint numverts = vmap.length(); Vec3 *tangent = new Vec3[2*numverts], *bitangent = tangent+numverts; for (uint i = 0; i < 2*numverts; i++) tangent[i] = Vec3(0,0,0); for (int i = priortris; i < triangles.length(); i++) { const triangle &t = triangles[i]; sharedvert &i0 = vmap[t.vert[0]], &i1 = vmap[t.vert[1]], &i2 = vmap[t.vert[2]]; Vec3 v0(epositions[i0.index]), e1 = Vec3(epositions[i1.index]) - v0, e2 = Vec3(epositions[i2.index]) - v0; double u1 = etexcoords[i1.index].x - etexcoords[i0.index].x, v1 = etexcoords[i1.index].y - etexcoords[i0.index].y, u2 = etexcoords[i2.index].x - etexcoords[i0.index].x, v2 = etexcoords[i2.index].y - etexcoords[i0.index].y; Vec3 u = e2*v1 - e1*v2, v = e2*u1 - e1*u2; if(e2.cross(e1).dot(v.cross(u)) < 0) { u = -u; v = -v; } if(!areaweight) { u = u.normalize(); v = v.normalize(); } loopj(3) { tangent[t.vert[j]] += u; bitangent[t.vert[j]] += v; } } loopv(vmap) { const Vec3 &n = enormals[vmap[i].weld], &t = tangent[i], &bt = bitangent[i]; etangents.add(Vec4((t - n*n.dot(t)).normalize(), n.cross(t).dot(bt) < 0 ? -1 : 1)); } delete[] tangent; } struct neighborkey { uint e0, e1; neighborkey() {} neighborkey(uint i0, uint i1) { if(epositions[i0] < epositions[i1]) { e0 = i0; e1 = i1; } else { e0 = i1; e1 = i0; } } uint hash() const { return hthash(epositions[e0]) + hthash(epositions[e1]); } bool operator==(const neighborkey &n) const { return epositions[e0] == epositions[n.e0] && epositions[e1] == epositions[n.e1] && (eblends.empty() || (eblends[e0] == eblends[n.e0] && eblends[e1] == eblends[n.e1])); } }; static inline uint hthash(const neighborkey &n) { return n.hash(); } static inline bool htcmp(const neighborkey &x, const neighborkey &y) { return x == y; } struct neighborval { uint tris[2]; neighborval() {} neighborval(uint i) { tris[0] = i; tris[1] = 0xFFFFFFFFU; } void add(uint i) { if(tris[1] != 0xFFFFFFFFU) tris[0] = tris[1] = 0xFFFFFFFFU; else if(tris[0] != 0xFFFFFFFFU) tris[1] = i; } int opposite(uint i) const { return tris[0] == i ? tris[1] : tris[0]; } }; void makeneighbors(uint priortris) { hashtable nhash; for(int i = priortris; iadd(i); else nhash[key] = neighborval(i); } } for(int i = priortris; i mshare(1<<12); vector mmap; vector tinfo; if (!noext) { loopv(emeshes) { if (!emeshes[i].hasexplicits) emeshes[i].explicits = spec.meshprops; loopk(meshoverrides.length()) { if (!strcmp(meshoverrides[k].name, emeshes[i].name)) { emeshes[i].explicits = meshoverrides[k].props; for (; k < meshoverrides.length()-1; k++) meshoverrides[k] = meshoverrides[k+1]; meshoverrides.drop(); break; } } } } if (spec.ignoresurfname) { loopv(emeshes) { emeshes[i].name = getnamekey(""); } } loopv(emeshes) { emesh &em1 = emeshes[i]; if(em1.used) continue; for(int j = i; j < emeshes.length(); j++) { emesh &em = emeshes[j]; if(em.used) continue; if(strcmp(em.name?em.name:"", em1.name?em1.name:"") || strcmp(em.material, em1.material) || memcmp(&em.explicits, &em1.explicits, sizeof(em.explicits))) continue; int lasttri = emeshes.inrange(j+1) ? emeshes[j+1].firsttri : etriangles.length(); for(int k = em.firsttri; k < lasttri; k++) { etriangle &et = etriangles[k]; triangleinfo &t = tinfo.add(); loopl(3) { sharedvert v(et.vert[l], et.weld[l]); t.vert[l] = mshare.access(v, mmap.length()); if(!mmap.inrange(t.vert[l])) mmap.add(v); } } em.used = true; } if(tinfo.empty()) continue; mesh &m = meshes.add(); if (spec.materialprefix || spec.materialsuffix) { char material[512]; formatstring(material, "%s%s%s", spec.materialprefix?spec.materialprefix:"", em1.material, spec.materialsuffix?spec.materialsuffix:""); m.material = sharestring(material); } else m.material = sharestring(em1.material); if (!em1.name) m.name = sharestring(em1.material); else m.name = sharestring(em1.name); m.firsttri = triangles.length(); m.firstvert = numfverts+vmap.length(); maketriangles(tinfo, mmap); m.numtris = triangles.length() - m.firsttri; m.numverts = numfverts+vmap.length() - m.firstvert; m.numskins = 0; //we have a default material, so no worries. if (spec.materialsuffix && !strncmp(spec.materialsuffix, "_00_00", 6)) { //for qex... populate our skins info for (int s = 0; ; s++) { char matname[512]; formatstring(matname, "%s%s_%02d_%02d%s", spec.materialprefix?spec.materialprefix:"", em1.material, s, 0, spec.materialsuffix+6); FILE *f = fopen(matname, "rb"); if (f) { fclose(f); //don't really care. auto &skin = meshskins.add(); m.numskins++; skin.firstframe = skinframes.length(); skin.countframes = 1; skin.interval = 0.2; auto &skinframe = skinframes.add(); skinframe.material_idx = sharestring(matname); skinframe.shadertext_idx = 0; for (skin.countframes = 1; ; skin.countframes++) { formatstring(matname, "%s%s_%02d_%02d%s", spec.materialprefix?spec.materialprefix:"", em1.material, s, skin.countframes, spec.materialsuffix+6); f = fopen(matname, "rb"); if (!f) break; fclose(f); auto &skinframe = skinframes.add(); skinframe.material_idx = sharestring(matname); } } else break; } } meshprop &mf = meshes_fte.add(); mf = em1.explicits; printlastmesh(); mshare.clear(); mmap.setsize(0); tinfo.setsize(0); } numfverts+=vmap.length(); if(triangles.length()) makeneighbors(priortris); if(escale != 1) loopv(epositions) epositions[i] *= escale; if(erotate != Quat(0, 0, 0, 1)) { loopv(epositions) epositions[i].setxyz(erotate.transform(Vec3(epositions[i]))); loopv(enormals) enormals[i] = erotate.transform(enormals[i]); loopv(etangents) etangents[i].setxyz(erotate.transform(Vec3(etangents[i]))); loopv(ebitangents) ebitangents[i] = erotate.transform(ebitangents[i]); } if(emeshtrans != Vec3(0, 0, 0)) loopv(epositions) epositions[i] += emeshtrans; if(epositions.length()) setupvertexarray(epositions, IQM_POSITION, IQM_FLOAT, 3, priorverts); if(etexcoords.length()) setupvertexarray(etexcoords, IQM_TEXCOORD, IQM_FLOAT, 2, priorverts); if(enormals.length()) setupvertexarray(enormals, IQM_NORMAL, IQM_FLOAT, 3, priorverts); if(etangents.length()) { if(ebitangents.length() && enormals.length()) { loopv(etangents) if(ebitangents.inrange(i) && enormals.inrange(i)) etangents[i].w = enormals[i].cross(Vec3(etangents[i])).dot(ebitangents[i]) < 0 ? -1 : 1; } setupvertexarray(etangents, IQM_TANGENT, IQM_FLOAT, 4, priorverts); } else if(enormals.length() && etexcoords.length()) { calctangents(priortris); setupvertexarray(etangents, IQM_TANGENT, IQM_FLOAT, 4, priorverts); } if(eblends.length() && !stripbones) { if (ejoints.length() > 65535) setupvertexarray(eblends, IQM_BLENDINDEXES, IQM_UINT, 4, priorverts); else if (ejoints.length() > 255) setupvertexarray(eblends, IQM_BLENDINDEXES, IQM_USHORT, 4, priorverts); else setupvertexarray(eblends, IQM_BLENDINDEXES, IQM_UBYTE, 4, priorverts); setupvertexarray(eblends, IQM_BLENDWEIGHTS, IQM_UBYTE, 4, priorverts); } if(ecolors.length()) setupvertexarray(ecolors, IQM_COLOR, IQM_UBYTE, 4, priorverts); loopi(10) if(ecustom[i].length()) setupvertexarray(ecustom[i], IQM_CUSTOM + i, IQM_FLOAT, 4, priorverts); //make sure we keep this data in a usable form so that we can calc framebounds. if(epositions.length()) { Vec4 *o = mpositions.reserve(epositions.length()); mpositions.advance(epositions.length()); loopv(epositions) o[i] = epositions[i]; } if(eblends.length()) { blendcombo *o = mblends.reserve(eblends.length()); mblends.advance(eblends.length()); loopv(eblends) o[i] = eblends[i]; } //the generated triangles currently refer to the imported arrays. //make sure they refer to the final verts if (priorverts) for (int i = priortris; i < triangles.length(); i++) { triangles[i].vert[0] += priorverts; triangles[i].vert[1] += priorverts; triangles[i].vert[2] += priorverts; } } void makebounds(framebounds &bb, Matrix3x4 *invbase, frame &frame) { vector buf; buf.growbuf(joints.length()); buf.setsize(joints.length()); //make sure all final bones have some value, even if its gibberish. should probably ignore verts that depend upon bones not defined in this animation. //remap<0 means the bone was dropped. loopv(buf) {buf[i] = Matrix3x4(Quat(0,0,0,1),Vec3(0,0,0));} loopv(frame.pose) if (frame.pose[i].remap>=0) { int bone = frame.pose[i].remap; int jparent = frame.pose[i].boneparent; if (jparent >= 0) jparent = frame.pose[jparent].remap; if(jparent >= 0) buf[bone] = buf[jparent] * Matrix3x4(frame.pose[i].tr.orient, frame.pose[i].tr.pos, frame.pose[i].tr.scale); else buf[bone] = Matrix3x4(frame.pose[i].tr.orient, frame.pose[i].tr.pos, frame.pose[i].tr.scale); } loopv(frame.pose) buf[i] *= invbase[i]; loopv(mpositions) { const blendcombo &c = mblends[i]; Matrix3x4 m(Vec4(0, 0, 0, 0), Vec4(0, 0, 0, 0), Vec4(0, 0, 0, 0)); loopk(4) if(c.weights[k] > 0) m += buf[c.bones[k]] * c.weights[k]; Vec3 p = m.transform(Vec3(mpositions[i])); if(!i) bb.bbmin = bb.bbmax = p; else { bb.bbmin.x = min(bb.bbmin.x, p.x); bb.bbmin.y = min(bb.bbmin.y, p.y); bb.bbmin.z = min(bb.bbmin.z, p.z); bb.bbmax.x = max(bb.bbmax.x, p.x); bb.bbmax.y = max(bb.bbmax.y, p.y); bb.bbmax.z = max(bb.bbmax.z, p.z); } double xyradius = p.x*p.x + p.y*p.y; bb.xyradius = max(bb.xyradius, xyradius); bb.radius = max(bb.radius, xyradius + p.z*p.z); } if(bb.xyradius > 0) bb.xyradius = sqrt(bb.xyradius); if(bb.radius > 0) bb.radius = sqrt(bb.radius); } void makerelativebasepose() { int numbasejoints = min(ejoints.length(), eframes.length() ? eframes[0] : eposes.length()); for(int i = numbasejoints-1; i >= 0; i--) { ejoint &ej = ejoints[i]; if(ej.parent < 0) continue; transform &parent = eposes[ej.parent], &child = eposes[i]; child.pos = (-parent.orient).transform(child.pos - parent.pos); child.orient = (-parent.orient)*child.orient; child.scale = child.scale / parent.scale; if(child.orient.w > 0) child.orient.flip(); } } bool forcejoints = false; void printlastanim(void) { if (quiet) return; anim &a = anims[anims.length()-1]; if (a.numframes == 1) printf(" frame %i:\tname=\"%s\"\tfps=%g, %s\n", anims.length()-1, &stringdata[a.name], a.fps, (a.flags & IQM_LOOP)?"looped":"clamped"); else printf(" anim %i:\tname=\"%s\",\tframes=%i, fps=%g, %s\n", anims.length()-1, &stringdata[a.name], a.numframes, a.fps, (a.flags & IQM_LOOP)?"looped":"clamped"); loopv(events_fte) { if (events_fte[i].anim == (uint)anims.length()-1) printf(" pose %g: %x \"%s\"\n", events_fte[i].timestamp*a.fps, events_fte[i].evcode, events_fte[i].evdata_str); } } void printbones(int parent = -1, size_t ind = 1) { char prefix[256]; if (ind >= sizeof(prefix)) ind = sizeof(prefix)-1; memset(prefix, ' ', ind); prefix[ind] = 0; loopv(joints) { if (joints[i].parent == parent) { //show as 1-based for consistency with quake. if (parent == -1) conoutf("%sbone %i:\tname=\"%s\"\tparent=NONE, group=%i (%f %f %f)", prefix, i+1, &stringdata[joints[i].name], joints[i].group, joints[i].pos[0], joints[i].pos[1], joints[i].pos[2]); else conoutf("%sbone %i:\tname=\"%s\"\tparent=%i, group=%i (%f %f %f)", prefix, i+1, &stringdata[joints[i].name], joints[i].parent+1, joints[i].group, joints[i].pos[0], joints[i].pos[1], joints[i].pos[2]); printbones(i, ind+1); } } } void printbonelist() { loopv(joints) { conoutf("bone %i:\tname=\"%s\"\tparent=%i%s, group=%i", i+1, &stringdata[joints[i].name], joints[i].parent+1, joints[i].parent >= i?"(ERROR)":"", joints[i].group); } } int findjoint(const char *name) { loopv(joints) { if (!strcmp(&stringdata[joints[i].name], name)) return i; } return -1; } bool floatcmp(float f1, float f2) { if (f1 != f2) return true; return false; } void makeanims(const filespec &spec) { if(escale != 1) loopv(eposes) eposes[i].pos *= escale; if(erotate != Quat(0, 0, 0, 1)) loopv(ejoints) { ejoint &ej = ejoints[i]; if(ej.parent < 0) for(int j = i; j < eposes.length(); j += ejoints.length()) { transform &p = eposes[j]; p.orient = erotate * p.orient; p.pos = erotate.transform(p.pos); } } int numbasejoints = eframes.length() ? eframes[0] : eposes.length(); if(forcejoints || emeshes.length()) { bool warned = false; int *jr = new int[ejoints.length()]; loopv(ejoints) { ejoint &ej = ejoints[i]; jr[i] = findjoint(ej.name); if (jr[i] >= 0) { bool rigmismatch = false; if (warned || forcejoints) continue; joint &j = joints[jr[i]]; loopk(3) if (floatcmp(j.pos[k], eposes[i].pos[k] + (ej.parent>=0?0:ejointtrans[k]))) rigmismatch = true; loopk(4) if (floatcmp(j.orient[k], eposes[i].orient[k])) rigmismatch = true; loopk(3) if (floatcmp(j.scale[k], eposes[i].scale[k])) rigmismatch = true; if (rigmismatch) { warned = true; conoutf("warning: rig mismatch (bone %s)", ej.name); } continue; } jr[i] = joints.length(); joint &j = joints.add(); Matrix3x4 &m = mjoints.add(); const char *name = ej.name; int group = -1; loopvk(boneoverrides) { if (!strcmp(boneoverrides[k].name, name)) { boneoverrides[k].used = true; if (boneoverrides[k].props.rename) name = boneoverrides[k].props.rename; if (boneoverrides[k].props.group >= 0) group = boneoverrides[k].props.group; break; } } j.name = sharestring(name); if (ej.parent >= 0) j.parent = findjoint(ejoints[ej.parent].name); else j.parent = -1; if (group < 0 && j.parent >= 0) group = joints[j.parent].group; if (group < 0) group = 0; j.group = group; if(i < numbasejoints) { m.invert(Matrix3x4(eposes[i].orient, eposes[i].pos + (ej.parent>=0?Vec3(0,0,0):ejointtrans), eposes[i].scale)); loopk(3) j.pos[k] = eposes[i].pos[k] + (ej.parent>=0?0:ejointtrans[k]); loopk(4) j.orient[k] = eposes[i].orient[k]; loopk(3) j.scale[k] = eposes[i].scale[k]; } else m.invert(Matrix3x4(Quat(0, 0, 0, 1), Vec3(0, 0, 0), Vec3(1, 1, 1))); if(j.parent >= 0) m *= mjoints[j.parent]; } loopv(eblends) { loopk(eblends[i].sorted) { int b = eblends[i].bones[k]; if (b >= ejoints.length()) b = 0; else b = jr[b]; eblends[i].bones[k] = b; } } delete[] jr; } // loopv(spec.events) // spec.events[i].evdata_idx = 0; loopv(eanims) { eanim &ea = eanims[i]; if (ea.flags & IQM_UNPACK) { //some quake mods suck, and are unable to deal with animations for (int j = ea.startframe, end = eanims.inrange(i+1) ? eanims[i+1].startframe : eframes.length(); j < end && j < ea.endframe; j++) { anim &a = anims.add(); char nname[256]; formatstring(nname, "%s_%i", ea.name, j+1-ea.startframe); a.name = sharestring(nname); a.firstframe = frames.length(); a.numframes = 0; a.fps = ea.fps; a.flags = ea.flags&~IQM_ALLPRIVATE; int offset = eframes[j], range = (eframes.inrange(j+1) ? eframes[j+1] : eposes.length()) - offset; if(range <= 0) continue; frame &fr = frames.add(); loopk(min(range, ejoints.length())) fr.pose.add(frame::framepose(ejoints[k], eposes[offset + k])); loopk(max(ejoints.length() - range, 0)) fr.pose.add(frame::framepose(ejoints[k], transform(Vec3(0, 0, 0), Quat(0, 0, 0, 1), Vec3(1, 1, 1)))); a.numframes++; printlastanim(); } } else { anim &a = anims.add(); a.name = sharestring(ea.name); a.firstframe = frames.length(); a.numframes = 0; a.fps = ea.fps; a.flags = ea.flags&~IQM_ALLPRIVATE; for(int j = ea.startframe, end = eanims.inrange(i+1) ? eanims[i+1].startframe : eframes.length(); j < end && j <= ea.endframe; j++) { int offset = eframes[j], range = (eframes.inrange(j+1) ? eframes[j+1] : eposes.length()) - offset; if(range <= 0) continue; frame &fr = frames.add(); loopk(min(range, ejoints.length())) { if (ejoints[k].parent < 0) eposes[offset+k].pos += ejointtrans; fr.pose.add(frame::framepose(ejoints[k], eposes[offset + k])); } loopk(max(ejoints.length() - range, 0)) { if (ejoints[k].parent < 0) fr.pose.add(frame::framepose(ejoints[k], transform(ejointtrans, Quat(0, 0, 0, 1), Vec3(1, 1, 1)))); else fr.pose.add(frame::framepose(ejoints[k], transform(Vec3(0, 0, 0), Quat(0, 0, 0, 1), Vec3(1, 1, 1)))); } a.numframes++; } loopvj(spec.events) { float p; if (spec.events[j].anim == ~0u) { if (spec.events[j].timestamp < ea.startframe || spec.events[j].timestamp >= ea.startframe + a.numframes) continue; p = spec.events[j].timestamp - ea.startframe; } else { if (spec.events[j].anim != (uint)i) continue; else p = spec.events[j].timestamp; } event_fte &ev = events_fte.add(spec.events[j]); ev.anim = anims.length()-1; ev.timestamp = p / a.fps; } printlastanim(); } } // loopv(spec.events) if (!spec.events[i].evdata_idx) // { // conoutf("event specifies invalid animation from %s", spec.file); // } } bool resetimporter(const filespec &spec, bool reuse = false) { if(reuse) { ejoints.setsize(0); evarrays.setsize(0); return false; } vmap.setsize(0); epositions.setsize(0); etexcoords.setsize(0); enormals.setsize(0); etangents.setsize(0); ebitangents.setsize(0); ecolors.setsize(0); loopi(10) ecustom[i].setsize(0); eblends.setsize(0); etriangles.setsize(0); esmoothindexes.setsize(0); esmoothedges.setsize(0); esmoothgroups.setsize(0); esmoothgroups.add(); ejoints.setsize(0); eposes.setsize(0); eframes.setsize(0); eanims.setsize(0); emeshes.setsize(0); evarrays.setsize(0); emeshtrans = gmeshtrans+spec.translate; ejointtrans = spec.translate; erotate = spec.rotate; escale = gscale*spec.scale; return true; } bool parseiqe(stream *f) { const char *curmesh = getnamekey(""), *curmaterial = getnamekey(""); bool needmesh = true; int fmoffset = 0; char buf[512]; if(!f->getline(buf, sizeof(buf))) return false; if(!strchr(buf, '#') || strstr(buf, "# Inter-Quake Export") != strchr(buf, '#')) return false; while(f->getline(buf, sizeof(buf))) { char *c = buf; while(isspace(*c)) ++c; if(isalpha(c[0]) && isalnum(c[1]) && (!c[2] || isspace(c[2]))) switch(*c++) { case 'v': switch(*c++) { case 'p': epositions.add(parseattribs4(c, Vec4(0, 0, 0, 1))); continue; case 't': etexcoords.add(parseattribs4(c)); continue; case 'n': enormals.add(parseattribs3(c)); continue; case 'x': { Vec4 tangent(parseattribs3(c), 0); Vec3 bitangent(0, 0, 0); bitangent.x = parseattrib(c); if(maybeparseattrib(c, bitangent.y)) { bitangent.z = parseattrib(c); ebitangents.add(bitangent); } else tangent.w = bitangent.x; etangents.add(tangent); continue; } case 'b': eblends.add(parseblends(c)); continue; case 'c': ecolors.add(parseattribs4(c, Vec4(0, 0, 0, 1))); continue; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { int n = c[-1] - '0'; ecustom[n].add(parseattribs4(c)); continue; } case 's': parseindex(c, esmoothindexes.add()); continue; } break; case 'p': { transform t; switch(*c++) { case 'q': { t.pos = parseattribs3(c); loopk(3) t.orient[k] = parseattrib(c); t.orient.restorew(); double w = parseattrib(c, t.orient.w); if(w != t.orient.w) { t.orient.w = w; t.orient.normalize(); // double x2 = f.orient.x*f.orient.x, y2 = f.orient.y*f.orient.y, z2 = f.orient.z*f.orient.z, w2 = f.orient.w*f.orient.w, s2 = x2 + y2 + z2 + w2; // f.orient.x = keepsign(f.orient.x, sqrt(max(1.0 - (w2 + y2 + z2) / s2, 0.0))); // f.orient.y = keepsign(f.orient.y, sqrt(max(1.0 - (w2 + x2 + z2) / s2, 0.0))); // f.orient.z = keepsign(f.orient.z, sqrt(max(1.0 - (w2 + x2 + y2) / s2, 0.0))); // f.orient.w = keepsign(f.orient.w, sqrt(max(1.0 - (x2 + y2 + z2) / s2, 0.0))); } if(t.orient.w > 0) t.orient.flip(); t.scale = parseattribs3(c, Vec3(1, 1, 1)); eposes.add(t); continue; } case 'm': { t.pos = parseattribs3(c); Matrix3x3 m; m.a = parseattribs3(c); m.b = parseattribs3(c); m.c = parseattribs3(c); Vec3 mscale(Vec3(m.a.x, m.b.x, m.c.x).magnitude(), Vec3(m.a.y, m.b.y, m.c.y).magnitude(), Vec3(m.a.z, m.b.z, m.c.z).magnitude()); // check determinant for sign of scaling if(m.determinant() < 0) mscale = -mscale; m.a /= mscale; m.b /= mscale; m.c /= mscale; t.orient = Quat(m); if(t.orient.w > 0) t.orient.flip(); t.scale = parseattribs3(c, Vec3(1, 1, 1)) * mscale; eposes.add(t); continue; } case 'a': { t.pos = parseattribs3(c); Vec3 rot = parseattribs3(c); t.orient = Quat::fromangles(rot); t.scale = parseattribs3(c, Vec3(1, 1, 1)); eposes.add(t); continue; } } break; } case 'f': switch(*c++) { case 'a': { int i1 = 0, i2 = 0, i3 = 0; if(!parseindex(c, i1) || !parseindex(c, i2)) continue; if(needmesh) { emeshes.add(emesh(curmesh, curmaterial, etriangles.length())); needmesh = false; } if(i1 < 0) i1 = max(epositions.length() + i1, 0); if(i2 < 0) i2 = max(epositions.length() + i2, 0); while(parseindex(c, i3)) { if(i3 < 0) i3 = max(epositions.length() + i3, 0); esmoothgroups.last().flags |= esmoothgroup::F_USED; etriangles.add(etriangle(i1, i2, i3, esmoothgroups.length()-1)); i2 = i3; } continue; } case 'm': { int i1 = 0, i2 = 0, i3 = 0; if(!parseindex(c, i1) || !parseindex(c, i2)) continue; if(needmesh) { emeshes.add(emesh(curmesh, curmaterial, etriangles.length())); needmesh = false; } i1 = i1 < 0 ? max(epositions.length() + i1, 0) : (fmoffset + i1); i2 = i2 < 0 ? max(epositions.length() + i2, 0) : (fmoffset + i2); while(parseindex(c, i3)) { i3 = i3 < 0 ? max(epositions.length() + i3, 0) : (fmoffset + i3); esmoothgroups.last().flags |= esmoothgroup::F_USED; etriangles.add(etriangle(i1, i2, i3, esmoothgroups.length()-1)); i2 = i3; } continue; } case 's': { int i1 = 0, i2 = 0, i3 = 0; uchar flags = 0; if(!parseindex(c, i1) || !parseindex(c, i2) || !parseindex(c, i3)) continue; flags |= clamp(i1, 0, 1); flags |= clamp(i2, 0, 1)<<1; flags |= clamp(i3, 0, 1)<<2; esmoothgroups.last().flags |= esmoothgroup::F_USED; while(parseindex(c, i3)) { esmoothedges.add(flags | 4); flags = 1 | ((flags & 4) >> 1) | (clamp(i3, 0, 1)<<2); } esmoothedges.add(flags); continue; } } break; } char *args = c; while(*args && !isspace(*args)) args++; if(!strncmp(c, "smoothgroup", max(int(args-c), 11))) { if(esmoothgroups.last().flags & esmoothgroup::F_USED) esmoothgroups.dup(); parseindex(args, esmoothgroups.last().key); } else if(!strncmp(c, "smoothangle", max(int(args-c), 11))) { if(esmoothgroups.last().flags & esmoothgroup::F_USED) esmoothgroups.dup(); double angle = parseattrib(args, 0); esmoothgroups.last().angle = fabs(cos(clamp(angle, -180.0, 180.0) * M_PI/180)); } else if(!strncmp(c, "smoothuv", max(int(args-c), 8))) { if(esmoothgroups.last().flags & esmoothgroup::F_USED) esmoothgroups.dup(); int val = 1; if(parseindex(args, val) && val <= 0) esmoothgroups.last().flags &= ~esmoothgroup::F_UVSMOOTH; else esmoothgroups.last().flags |= esmoothgroup::F_UVSMOOTH; } else if(!strncmp(c, "mesh", max(int(args-c), 4))) { curmesh = getnamekey(trimname(args)); if(emeshes.empty() || emeshes.last().name != curmesh) needmesh = true; fmoffset = epositions.length(); #if 0 emesh &m = emeshes.add(); m.firsttri = etriangles.length(); fmoffset = epositions.length(); parsename(args, m.name); #endif } else if(!strncmp(c, "material", max(int(args-c), 8))) { curmaterial = getnamekey(trimname(args)); if(emeshes.empty() || emeshes.last().material != curmaterial) needmesh = true; // if(emeshes.length()) parsename(c, emeshes.last().material); } else if(!strncmp(c, "joint", max(int(args-c), 5))) { ejoint &j = ejoints.add(); j.name = getnamekey(trimname(args)); parseindex(args, j.parent); } else if(!strncmp(c, "vertexarray", max(int(args-c), 11))) { evarray &va = evarrays.add(); va.type = findvertexarraytype(trimname(args)); va.format = findvertexarrayformat(trimname(args)); va.size = strtol(args, &args, 10); copystring(va.name, trimname(args)); } else if(!strncmp(c, "animation", max(int(args-c), 9))) { eanim &a = eanims.add(); a.name = getnamekey(trimname(args)); a.startframe = eframes.length(); if(!eframes.length() || eframes.last() != eposes.length()) eframes.add(eposes.length()); } else if(!strncmp(c, "frame", max(int(args-c), 5))) { if(eanims.length() && eframes.length() && eframes.last() != eposes.length()) eframes.add(eposes.length()); } else if(!strncmp(c, "framerate", max(int(args-c), 9))) { if(eanims.length()) { double fps = parseattrib(args); eanims.last().fps = max(fps, 0.0); } } else if(!strncmp(c, "loop", max(int(args-c), 4))) { if(eanims.length()) eanims.last().flags |= IQM_LOOP; } else if(!strncmp(c, "comment", max(int(args-c), 7))) { if(commentdata.length()) break; for(;;) { size_t len = f->read(commentdata.reserve(1024), 1024); commentdata.advance(len); if(len < 1024) { commentdata.add('\0'); break; } } } } return true; } bool loadiqe(const char *filename, const filespec &spec) { int numfiles = 0; while(filename) { const char *endfile = strchr(filename, ','); const char *file = endfile ? newstring(filename, endfile-filename) : filename; stream *f = openfile(file, "r"); if(f) { resetimporter(spec, numfiles > 0); if(parseiqe(f)) numfiles++; delete f; } if(!endfile) break; delete[] file; filename = endfile+1; } if(!numfiles) return false; if(eanims.length() == 1) { eanim &a = eanims.last(); if(spec.name) a.name = spec.name; if(spec.fps > 0) a.fps = spec.fps; a.flags |= spec.flags; if(spec.endframe >= 0) a.endframe = a.startframe + spec.endframe; else if(spec.endframe < -1) a.endframe = a.startframe + max(eframes.length() - a.startframe + spec.endframe + 1, 0); a.startframe += spec.startframe; } makeanims(spec); if(emeshes.length()) { smoothverts(); makemeshes(spec); } return true; } struct md5weight { int joint; double bias; Vec3 pos; }; struct md5vert { double u, v; uint start, count; }; struct md5hierarchy { const char *name; int parent, flags, start; }; vector weightinfo; vector vertinfo; void buildmd5verts() { loopv(vertinfo) { md5vert &v = vertinfo[i]; Vec3 pos(0, 0, 0); loopk(v.count) { md5weight &w = weightinfo[v.start+k]; transform &j = eposes[w.joint]; pos += (j.orient.transform(w.pos) + j.pos)*w.bias; } epositions.add(Vec4(pos, 1)); etexcoords.add(Vec4(v.u, v.v, 0, 0)); blendcombo &c = eblends.add(); loopj(v.count) { md5weight &w = weightinfo[v.start+j]; c.addweight(w.bias, w.joint); } c.finalize(); } } void parsemd5mesh(stream *f, char *buf, size_t bufsize) { md5weight w; md5vert v; etriangle t(0, 0, 0, 0); int index, firsttri = etriangles.length(), firstvert = vertinfo.length(), firstweight = weightinfo.length(), numtris = 0, numverts = 0, numweights = 0; emesh m; while(f->getline(buf, bufsize) && buf[0]!='}') { if(strstr(buf, "// meshes:")) { char *start = strchr(buf, ':')+1; if(*start==' ') start++; char *end = start + strlen(start)-1; while(end >= start && isspace(*end)) end--; end[1] = '\0'; m.name = getnamekey(start); } else if(strstr(buf, "shader")) { char *start = strchr(buf, '"'), *end = start ? strchr(start+1, '"') : NULL; if(start && end) { *end = '\0'; m.material = getnamekey(start+1); } } else if(sscanf(buf, " numverts %d", &numverts)==1) { numverts = max(numverts, 0); if(numverts) { vertinfo.reserve(numverts); vertinfo.advance(numverts); } } else if(sscanf(buf, " numtris %d", &numtris)==1) { numtris = max(numtris, 0); if(numtris) { etriangles.reserve(numtris); etriangles.advance(numtris); } m.firsttri = firsttri; } else if(sscanf(buf, " numweights %d", &numweights)==1) { numweights = max(numweights, 0); if(numweights) { weightinfo.reserve(numweights); weightinfo.advance(numweights); } } else if(sscanf(buf, " vert %d ( %lf %lf ) %u %u", &index, &v.u, &v.v, &v.start, &v.count)==5) { if(index>=0 && index=0 && index=0 && indexgetline(buf, sizeof(buf))) { int tmp; if(sscanf(buf, " MD5Version %d", &tmp)==1) { if(tmp!=10) { delete f; return false; } } else if(sscanf(buf, " numJoints %d", &tmp)==1) { if(tmp<1 || (joints.length() && tmp != joints.length())) { delete f; return false; } } else if(sscanf(buf, " numMeshes %d", &tmp)==1) { if(tmp<1) { delete f; return false; } } else if(strstr(buf, "joints {")) { ejoint j; transform p; while(f->getline(buf, sizeof(buf)) && buf[0]!='}') { char *c = buf; j.name = getnamekey(trimname(c)); if(sscanf(c, " %d ( %lf %lf %lf ) ( %lf %lf %lf )", &j.parent, &p.pos.x, &p.pos.y, &p.pos.z, &p.orient.x, &p.orient.y, &p.orient.z)==7) { p.orient.restorew(); p.scale = Vec3(1, 1, 1); ejoints.add(j); eposes.add(p); } } } else if(strstr(buf, "mesh {")) { parsemd5mesh(f, buf, sizeof(buf)); } } delete f; buildmd5verts(); makerelativebasepose(); makeanims(spec); smoothverts(); makemeshes(spec); return true; } bool loadmd5anim(const char *filename, const filespec &spec) { stream *f = openfile(filename, "r"); if(!f) return false; resetimporter(spec); vector hierarchy; vector baseframe; int animdatalen = 0, animframes = 0, frameoffset = eposes.length(), firstframe = eframes.length(); double framerate = 0; double *animdata = NULL; char buf[512]; while(f->getline(buf, sizeof(buf))) { int tmp; if(sscanf(buf, " MD5Version %d", &tmp)==1) { if(tmp!=10) { delete f; return false; } } else if(sscanf(buf, " numJoints %d", &tmp)==1) { if(tmp<1) { delete f; return false; } } else if(sscanf(buf, " numFrames %d", &animframes)==1) { if(animframes<1) { delete f; return false; } } else if(sscanf(buf, " frameRate %lf", &framerate)==1); else if(sscanf(buf, " numAnimatedComponents %d", &animdatalen)==1) { if(animdatalen>0) animdata = new double[animdatalen]; } else if(strstr(buf, "bounds {")) { while(f->getline(buf, sizeof(buf)) && buf[0]!='}'); } else if(strstr(buf, "hierarchy {")) { while(f->getline(buf, sizeof(buf)) && buf[0]!='}') { char *c = buf; md5hierarchy h; h.name = getnamekey(trimname(c)); if(sscanf(c, " %d %d %d", &h.parent, &h.flags, &h.start)==3) hierarchy.add(h); } if(hierarchy.empty()) { delete f; return false; } loopv(hierarchy) { md5hierarchy &h = hierarchy[i]; ejoint &j = ejoints.add(); j.name = h.name; j.parent = h.parent; } } else if(strstr(buf, "baseframe {")) { while(f->getline(buf, sizeof(buf)) && buf[0]!='}') { transform j; if(sscanf(buf, " ( %lf %lf %lf ) ( %lf %lf %lf )", &j.pos.x, &j.pos.y, &j.pos.z, &j.orient.x, &j.orient.y, &j.orient.z)==6) { j.orient.restorew(); j.scale = Vec3(1, 1, 1); baseframe.add(j); } } if(baseframe.length()!=hierarchy.length()) { delete f; return false; } eposes.reserve(animframes*baseframe.length()); eposes.advance(animframes*baseframe.length()); } else if(sscanf(buf, " frame %d", &tmp)==1) { for(int numdata = 0; f->getline(buf, sizeof(buf)) && buf[0]!='}';) { for(char *src = buf, *next = src; numdata < animdatalen; numdata++, src = next) { animdata[numdata] = strtod(src, &next); if(next <= src) break; } } int offset = frameoffset + tmp*baseframe.length(); eframes.add(offset); loopv(baseframe) { md5hierarchy &h = hierarchy[i]; transform j = baseframe[i]; if(h.start < animdatalen && h.flags) { double *jdata = &animdata[h.start]; if(h.flags&1) j.pos.x = *jdata++; if(h.flags&2) j.pos.y = *jdata++; if(h.flags&4) j.pos.z = *jdata++; if(h.flags&8) j.orient.x = *jdata++; if(h.flags&16) j.orient.y = *jdata++; if(h.flags&32) j.orient.z = *jdata++; j.orient.restorew(); } eposes[offset + i] = j; } } } if(animdata) delete[] animdata; delete f; eanim &a = eanims.add(); if(spec.name) a.name = getnamekey(spec.name); else { string name; copystring(name, filename); char *end = strrchr(name, '.'); if(end) *end = '\0'; a.name = getnamekey(name); } a.startframe = firstframe; a.fps = spec.fps > 0 ? spec.fps : framerate; a.flags = spec.flags; if(spec.endframe >= 0) a.endframe = a.startframe + spec.endframe; else if(spec.endframe < -1) a.endframe = a.startframe + max(eframes.length() - a.startframe + spec.endframe + 1, 0); a.startframe += spec.startframe; makeanims(spec); return true; } namespace smd { bool skipcomment(char *&curbuf) { while(*curbuf && isspace(*curbuf)) curbuf++; switch(*curbuf) { case '#': case ';': case '\r': case '\n': case '\0': return true; case '/': if(curbuf[1] == '/') return true; break; } return false; } void skipsection(stream *f, char *buf, size_t bufsize) { while(f->getline(buf, bufsize)) { char *curbuf = buf; if(skipcomment(curbuf)) continue; if(!strncmp(curbuf, "end", 3)) break; } } void readname(char *&curbuf, char *name, size_t namesize) { char *curname = name; while(*curbuf && isspace(*curbuf)) curbuf++; bool allowspace = false; if(*curbuf == '"') { curbuf++; allowspace = true; } while(*curbuf) { char c = *curbuf++; if(c == '"') break; if(isspace(c) && !allowspace) break; if(curname < &name[namesize-1]) *curname++ = c; } *curname = '\0'; } void readnodes(stream *f, char *buf, size_t bufsize) { while(f->getline(buf, bufsize)) { char *curbuf = buf; if(skipcomment(curbuf)) continue; if(!strncmp(curbuf, "end", 3)) break; int id = strtol(curbuf, &curbuf, 10); string name; readname(curbuf, name, sizeof(name)); int parent = strtol(curbuf, &curbuf, 10); if(id < 0 || id > 255 || parent > 255 || !name[0] || (ejoints.inrange(id) && ejoints[id].name)) continue; ejoint j; j.name = getnamekey(name); j.parent = parent; while(ejoints.length() <= id) ejoints.add(); ejoints[id] = j; } } void readmaterial(char *&curbuf, char *mat, char *name, size_t matsize) { char *curmat = mat; while(*curbuf && isspace(*curbuf)) curbuf++; char *ext = NULL; while(*curbuf) { char c = *curbuf++; if(isspace(c)) break; if(c == '.' && !ext) ext = curmat; if(curmat < &mat[matsize-1]) *curmat++ = c; } *curmat = '\0'; if(!ext) ext = curmat; memcpy(name, mat, ext - mat); name[ext - mat] = '\0'; } void readskeleton(stream *f, char *buf, size_t bufsize) { int frame = -1, firstpose = -1; while(f->getline(buf, bufsize)) { char *curbuf = buf; if(skipcomment(curbuf)) continue; if(sscanf(curbuf, " time %d", &frame) == 1) continue; else if(!strncmp(curbuf, "end", 3)) break; else if(frame != 0) continue; int bone; Vec3 pos, rot; if(sscanf(curbuf, " %d %lf %lf %lf %lf %lf %lf", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7) continue; if(!ejoints.inrange(bone)) continue; if(firstpose < 0) { firstpose = eposes.length(); eposes.reserve(ejoints.length()); eposes.advance(ejoints.length()); } transform p(pos, Quat::fromangles(rot)); eposes[firstpose + bone] = p; } } void readtriangles(stream *f, char *buf, size_t bufsize) { emesh m; while(f->getline(buf, bufsize)) { char *curbuf = buf; if(skipcomment(curbuf)) continue; if(!strncmp(curbuf, "end", 3)) break; string name, material; readmaterial(curbuf, material, name, sizeof(material)); if(!m.name || strcmp(m.name, name)) { if(m.name && etriangles.length() > m.firsttri) emeshes.add(m); m.name = getnamekey(name); m.material = getnamekey(material); m.firsttri = etriangles.length(); } Vec4 *pos = epositions.reserve(3) + 2, *tc = etexcoords.reserve(3) + 2; Vec3 *norm = enormals.reserve(3) + 2; blendcombo *c = eblends.reserve(3) + 2; loopi(3) { char *curbuf; do { if(!f->getline(buf, bufsize)) goto endsection; curbuf = buf; } while(skipcomment(curbuf)); int parent = -1, numlinks = 0, len = 0; if(sscanf(curbuf, " %d %lf %lf %lf %lf %lf %lf %lf %lf %d%n", &parent, &pos->x, &pos->y, &pos->z, &norm->x, &norm->y, &norm->z, &tc->x, &tc->y, &numlinks, &len) < 9) goto endsection; curbuf += len; pos->w = 1; tc->y = 1 - tc->y; tc->z = tc->w = 0; c->reset(); double pweight = 0, tweight = 0; for(; numlinks > 0; numlinks--) { int bone = -1, len = 0; double weight = 0; if(sscanf(curbuf, " %d %lf%n", &bone, &weight, &len) < 2) break; curbuf += len; tweight += weight; if(bone == parent) pweight += weight; else c->addweight(weight, bone); } if(tweight < 1) pweight += 1 - tweight; if(pweight > 0) c->addweight(pweight, parent); c->finalize(); --pos; --tc; --norm; --c; } etriangle &t = etriangles.add(); loopi(3) t.vert[i] = epositions.length() + i; t.smoothgroup = 0; epositions.advance(3); enormals.advance(3); etexcoords.advance(3); eblends.advance(3); } endsection: if(m.name && etriangles.length () > m.firsttri) emeshes.add(m); } int readframes(stream *f, char *buf, size_t bufsize) { int frame = -1, numframes = 0, lastbone = ejoints.length(), frameoffset = eposes.length(); while(f->getline(buf, bufsize)) { char *curbuf = buf; if(skipcomment(curbuf)) continue; int nextframe = -1; if(sscanf(curbuf, " time %d", &nextframe) == 1) { for(; lastbone < ejoints.length(); lastbone++) eposes[frameoffset + frame*ejoints.length() + lastbone] = eposes[frameoffset + lastbone]; if(nextframe >= numframes) { eposes.reserve(ejoints.length() * (nextframe + 1 - numframes)); loopi(nextframe - numframes) { eframes.add(eposes.length()); eposes.put(&eposes[frameoffset], ejoints.length()); } eframes.add(eposes.length()); eposes.advance(ejoints.length()); numframes = nextframe + 1; } frame = nextframe; lastbone = 0; continue; } else if(!strncmp(curbuf, "end", 3)) break; int bone; Vec3 pos, rot; if(sscanf(curbuf, " %d %lf %lf %lf %lf %lf %lf", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7) continue; if(bone < 0 || bone >= ejoints.length()) continue; for(; lastbone < bone; lastbone++) eposes[frameoffset + frame*ejoints.length() + lastbone] = eposes[frameoffset + lastbone]; lastbone++; transform p(pos, Quat::fromangles(rot)); eposes[frameoffset + frame*ejoints.length() + bone] = p; } for(; lastbone < ejoints.length(); lastbone++) eposes[frameoffset + frame*ejoints.length() + lastbone] = eposes[frameoffset + lastbone]; return numframes; } } bool loadsmd(const char *filename, const filespec &spec) { stream *f = openfile(filename, "r"); if(!f) return false; resetimporter(spec); char buf[512]; int version = -1, firstframe = eframes.length(); bool hastriangles = false; while(f->getline(buf, sizeof(buf))) { char *curbuf = buf; if(smd::skipcomment(curbuf)) continue; if(sscanf(curbuf, " version %d", &version) == 1) { if(version != 1) { delete f; return false; } } else if(!strncmp(curbuf, "nodes", 5)) smd::readnodes(f, buf, sizeof(buf)); else if(!strncmp(curbuf, "triangles", 9)) { smd::readtriangles(f, buf, sizeof(buf)); hastriangles = true; } else if(!strncmp(curbuf, "skeleton", 8)) smd::readframes(f, buf, sizeof(buf)); else if(!strncmp(curbuf, "vertexanimation", 15)) smd::skipsection(f, buf, sizeof(buf)); } delete f; if(hastriangles) { eframes.setsize(firstframe); makeanims(spec); smoothverts(); makemeshes(spec); } else { eanim &a = eanims.add(); if(spec.name) a.name = getnamekey(spec.name); else { string name; const char *shortname = filename; shortname = strrchr(filename, '/'); if (shortname) shortname++; else shortname = filename; copystring(name, shortname); char *end = strrchr(name, '.'); if(end) *end = '\0'; a.name = getnamekey(name); } a.startframe = firstframe; a.fps = spec.fps; a.flags = spec.flags; if(spec.endframe >= 0) a.endframe = a.startframe + spec.endframe; else if(spec.endframe < -1) a.endframe = a.startframe + max(eframes.length() - a.startframe + spec.endframe + 1, 0); a.startframe += spec.startframe; makeanims(spec); } return true; } struct objvert { int attrib[3]; objvert() { attrib[0] = attrib[1] = attrib[2] = -1; } }; static inline uint hthash(const objvert &k) { return k.attrib[0] ^ k.attrib[1] ^ k.attrib[2]; }; static inline bool htcmp(const objvert &x, const objvert &y) { return x.attrib[0] == y.attrib[0] && x.attrib[1] == y.attrib[1] && x.attrib[2] == y.attrib[2]; } void parseobjvert(char *s, vector &out) { Vec3 &v = out.add(Vec3(0, 0, 0)); while(isalpha(*s)) s++; loopi(3) { v[i] = strtod(s, &s); while(isspace(*s)) s++; if(!*s) break; } } struct mtlinfo{ const char *name; const char *tex; //often null Vec4 rgba; //often 1,1,1,1 mtlinfo(const char *matname) : name(getnamekey(matname)), tex(NULL),rgba(Vec4(1,1,1,1)) {} }; static vector parsemtl(stream *f) { vector ret; char buf[512]; mtlinfo dummy(""), *curmat = &dummy; if (f) while(f->getline(buf, sizeof(buf))) { char *c = buf; while(isspace(*c)) c++; if (*c == '#') continue; else if(!strncmp(c, "newmtl", 6) && isspace(c[6])) { while(isalpha(*c)) c++; while(isspace(*c)) c++; char *name = c; size_t namelen = strlen(c); while(namelen > 0 && isspace(name[namelen-1])) namelen--; name[namelen] = 0; curmat = &ret.add(mtlinfo(name)); } else if(!strncmp(c, "Kd", 2) && isspace(c[2])) { //diffuse colours... while(isalpha(*c)) c++; while(isspace(*c)) c++; loopi(3) { curmat->rgba[i] = strtod(c, &c); while(isspace(*c)) c++; if(!*c) break; } } else if(!strncmp(c, "D", 1) && isspace(c[1])) { //'disolve', so basically alpha while(isalpha(*c)) c++; while(isspace(*c)) c++; curmat->rgba[3] = strtod(c, &c); } else if(!strncmp(c, "map_Kd", 6) && isspace(c[6])) { while(isalpha(*c) || *c == '_') c++; while(isspace(*c)) c++; char *name = c; size_t namelen = strlen(c); while(namelen > 0 && isspace(name[namelen-1])) namelen--; name[namelen] = 0; curmat->tex = getnamekey(name); } } return ret; } bool parseobj(stream *f) { vector attrib[3]; //coord, tangemt, normal. char buf[512]; hashtable verthash; string meshname = "", matname = "", tmpname=""; int curmesh = -1, smooth = 0; Vec4 col = {1,1,1,1}; vector mtl; while(f->getline(buf, sizeof(buf))) { char *c = buf; while(isspace(*c)) c++; switch(*c) { case '#': continue; case 'v': if(isspace(c[1])) parseobjvert(c, attrib[0]); else if(c[1]=='t') parseobjvert(c, attrib[1]); else if(c[1]=='n') parseobjvert(c, attrib[2]); break; case 'g': { while(isalpha(*c)) c++; while(isspace(*c)) c++; char *name = c; size_t namelen = strlen(name); while(namelen > 0 && isspace(name[namelen-1])) namelen--; copystring(meshname, name, min(namelen+1, sizeof(meshname))); curmesh = -1; break; } case 'm': { if(strncmp(c, "mtllib", 6)) continue; while(isalpha(*c)) c++; while(isspace(*c)) c++; const char *name = c; size_t namelen = strlen(name); while(namelen > 0 && isspace(name[namelen-1])) namelen--; copystring(tmpname, name, min(namelen+1, sizeof(tmpname))); mtl = parsemtl(openfile(tmpname, "r")); break; } case 'u': { if(strncmp(c, "usemtl", 6)) continue; while(isalpha(*c)) c++; while(isspace(*c)) c++; const char *name = c; size_t namelen = strlen(name); while(namelen > 0 && isspace(name[namelen-1])) namelen--; copystring(matname, name, min(namelen+1, sizeof(matname))); col = Vec4(1,1,1,1); loopv(mtl) { //if its an obj material, swap out the vertex colour+mat name for whatever the mtl file specifies. if (!strcmp(matname, mtl[i].name)) { col = mtl[i].rgba; const char *tex = mtl[i].tex; if (!tex) tex = (col[3] < 1)?"whiteskin#VC#BLEND":"whiteskin#VC"; //not me being racist or anything... copystring(matname, tex, sizeof(matname)); break; } } curmesh = -1; break; } case 's': { if(!isspace(c[1])) continue; while(isalpha(*c)) c++; while(isspace(*c)) c++; int key = strtol(c, &c, 10); smooth = -1; loopv(esmoothgroups) if(esmoothgroups[i].key == key) { smooth = i; break; } if(smooth < 0) { smooth = esmoothgroups.length(); esmoothgroups.add().key = key; } break; } case 'f': { if(curmesh < 0) { emesh m; m.name = getnamekey(meshname); m.material = getnamekey(matname); m.firsttri = etriangles.length(); curmesh = emeshes.length(); emeshes.add(m); verthash.clear(); } int v0 = -1, v1 = -1; while(isalpha(*c)) c++; for(;;) { while(isspace(*c)) c++; if(!*c) break; objvert vkey; loopi(3) { vkey.attrib[i] = strtol(c, &c, 10); if(vkey.attrib[i] < 0) vkey.attrib[i] = attrib[i].length() + vkey.attrib[i]; else vkey.attrib[i]--; if(!attrib[i].inrange(vkey.attrib[i])) vkey.attrib[i] = -1; if(*c!='/') break; c++; } int *index = verthash.access(vkey); if(!index) { index = &verthash[vkey]; *index = epositions.length(); epositions.add(Vec4(vkey.attrib[0] < 0 ? Vec3(0, 0, 0) : attrib[0][vkey.attrib[0]].zxy(), 1)); if(vkey.attrib[2] >= 0) enormals.add(attrib[2][vkey.attrib[2]].zxy()); etexcoords.add(vkey.attrib[1] < 0 ? Vec4(0, 0, 0, 0) : Vec4(attrib[1][vkey.attrib[1]].x, 1-attrib[1][vkey.attrib[1]].y, 0, 0)); ecolors.add(col); } if(v0 < 0) v0 = *index; else if(v1 < 0) v1 = *index; else { etriangles.add(etriangle(*index, v1, v0, smooth)); v1 = *index; } } break; } } } return true; } bool loadobj(const char *filename, const filespec &spec) { stream *f = openfile(filename, "r"); if(!f) return false; int numfiles = 0; while(filename) { const char *endfile = strchr(filename, ','); const char *file = endfile ? newstring(filename, endfile-filename) : filename; stream *f = openfile(file, "r"); if(f) { if(resetimporter(spec, numfiles > 0)) { esmoothgroups[0].key = 0; } if(parseobj(f)) numfiles++; delete f; } if(!endfile) break; delete[] file; filename = endfile+1; } if(!numfiles) return false; smoothverts(); makemeshes(spec); return true; } namespace fbx { struct token { enum { NONE, PROP, NUMBER, STRING, ARRAY, BEGIN, END, LINE }; int type; union { char s[64]; double f; int i; }; token() : type(NONE) {} }; struct tokenizer { stream *f; char *pos; char buf[4096]; void reset(stream *s) { f = s; pos = buf; buf[0] = '\0'; } bool parse(token &t) { for(;;) { while(isspace(*pos)) pos++; if(!*pos) { bool more = f->getline(buf, sizeof(buf)); pos = buf; if(!more) { buf[0] = '\0'; return false; } t.type = token::LINE; return true; } size_t slen = 0; switch(*pos) { case ',': pos++; continue; case ';': pos++; while(*pos) pos++; continue; case '{': pos++; t.type = token::BEGIN; return true; case '}': pos++; t.type = token::END; return true; case '"': pos++; for(; *pos && *pos != '"'; pos++) if(slen < sizeof(t.s)-1) t.s[slen++] = *pos; t.s[slen] = '\0'; if(*pos == '"') pos++; t.type = token::STRING; return true; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': case '-': case '+': t.f = strtod(pos, &pos); t.type = token::NUMBER; return true; case '*': pos++; t.i = int(strtol(pos, &pos, 10)); t.type = token::ARRAY; return true; default: for(; *pos && !isspace(*pos) && *pos != ':'; pos++) if(slen < sizeof(t.s)-1) t.s[slen++] = *pos; t.s[slen] = '\0'; if(*pos == ':') pos++; t.type = token::PROP; return true; } } return false; } bool skipprop() { token t; while(parse(t)) switch(t.type) { case token::LINE: return true; case token::BEGIN: while(parse(t)) switch(t.type) { case token::PROP: skipprop(); break; case token::END: return true; } return true; case token::END: return false; } return false; } bool findbegin() { token t; while(parse(t)) switch(t.type) { case token::LINE: return false; case token::BEGIN: return true; } return false; } template bool readarray(vector &vals, int size = 0) { if(!findbegin()) return false; if(size > 0) vals.reserve(min(size, 1<<16)); token t; while(parse(t)) switch(t.type) { case token::NUMBER: if(size <= 0 || vals.length() < size) vals.add(T(t.f)); break; case token::END: return true; } return false; } }; struct node { enum { GEOM = 0, MODEL, MATERIAL, LIMB, CLUSTER, SKIN, CURVE, XFORM, ANIMLAYER, ANIMSTACK }; enum { TRANS = 0, ROT, SCALE }; virtual int type() = 0; virtual ~node() {} virtual void process() {} virtual void finish() {} }; struct namednode : node { string name; namednode() { name[0] = 0; } }; struct geomnode; struct modelnode; struct materialnode; struct limbnode; struct clusternode; struct skinnode; struct curvenode; struct xformnode; struct animlayernode; struct animstacknode; struct geomnode : node { int mesh, firstvert, lastvert, numverts; modelnode *model; vector remap; vector blends; geomnode() : mesh(-1), firstvert(-1), lastvert(-1), numverts(0), model(NULL) {} int type() { return GEOM; } void process(); void finish(); }; struct modelnode : namednode { materialnode *material; Vec3 geomtrans, prerot, lcltrans, lclrot, lclscale; modelnode() : material(NULL), geomtrans(0, 0, 0), prerot(0, 0, 0), lcltrans(0, 0, 0), lclrot(0, 0, 0), lclscale(1, 1, 1) {} int type() { return MODEL; } }; struct materialnode : namednode { int type() { return MATERIAL; } }; struct limbnode : namednode { limbnode *parent; int index; Vec3 trans, rot, prerot, scale; clusternode *cluster; limbnode() : parent(NULL), index(-1), trans(0, 0, 0), rot(0, 0, 0), prerot(0, 0, 0), scale(1, 1, 1), cluster(NULL) {} int type() { return LIMB; } void process() { if(parent) ejoints[index].parent = parent->index; } void finish(); }; struct clusternode : node { skinnode *skin; limbnode *limb; vector indexes; vector weights, transform, transformlink; clusternode() : skin(NULL), limb(NULL) {} int type() { return CLUSTER; } void process(); }; struct skinnode : node { geomnode *geom; skinnode() : geom(NULL) {} int type() { return SKIN; } }; struct curvenode : node { vector vals; int type() { return CURVE; } bool varies() const { loopv(vals) if(vals[i] != vals[0]) return true; return false; } }; struct xformnode : node { limbnode *limb; int xform; Vec3 val; curvenode *curves[3]; xformnode() : limb(NULL), xform(-1), val(0, 0, 0) { curves[0] = curves[1] = curves[2] = NULL; } void setcurve(int i, curvenode *c) { if(c->varies()) curves[i] = c; else if(c->vals.length()) val[i] = c->vals[0]; } int numframes() { int n = 0; loopi(3) if(curves[i]) { if(!n) n = curves[i]->vals.length(); else if(n != curves[i]->vals.length()) n = -1; } return n; } int type() { return XFORM; } }; struct animlayernode : namednode { vector xforms; int numframes() { int n = 0; loopv(xforms) { int xn = xforms[i]->numframes(); if(xn) { if(!n) n = xn; else if(n != xn) n = -1; } } return n; } int type() { return ANIMLAYER; } }; struct animstacknode : namednode { vector layers; double secs; animstacknode() : secs(0) {} int numframes() { int n; loopv(layers) { int ln = layers[i]->numframes(); if(ln) { if(!n) n = ln; else if(n != ln) n = -1; } } return n; } int type() { return ANIMSTACK; } void process(); }; hashtable nodes; tokenizer p; void parsegeometry() { token t; if(!p.parse(t)) return; if(t.type != token::NUMBER) { p.skipprop(); return; } double id = t.f; if(!p.findbegin()) return; vector verts, norms, uvs, colors; vector polyidxs, uvidxs, coloridxs; while(p.parse(t)) switch(t.type) { case token::END: goto endgeometry; case token::PROP: if(!strcmp(t.s, "Vertices")) p.readarray(verts); else if(!strcmp(t.s, "PolygonVertexIndex")) p.readarray(polyidxs); else if(!strcmp(t.s, "LayerElementNormal")) { if(p.findbegin()) { while(p.parse(t)) switch(t.type) { case token::PROP: if(!strcmp(t.s, "Normals")) p.readarray(norms); else p.skipprop(); break; case token::END: goto endnormals; } endnormals:; } } else if(!strcmp(t.s, "LayerElementUV")) { if(p.findbegin()) { while(p.parse(t)) switch(t.type) { case token::PROP: if(!strcmp(t.s, "UV")) p.readarray(uvs); else if(!strcmp(t.s, "UVIndex")) p.readarray(uvidxs); else p.skipprop(); break; case token::END: goto enduvs; } enduvs:; } } else if(!strcmp(t.s, "LayerElementColor")) { if(p.findbegin()) { while(p.parse(t)) switch(t.type) { case token::PROP: if(!strcmp(t.s, "Colors")) p.readarray(colors); else if(!strcmp(t.s, "ColorIndex")) p.readarray(coloridxs); else p.skipprop(); break; case token::END: goto endcolors; } endcolors:; } } else p.skipprop(); break; } endgeometry: int poslen = epositions.length(); geomnode *n = new geomnode; nodes[id] = n; if(polyidxs.empty()) for(int i = 0; i + 2 < verts.length(); i += 3) epositions.add(Vec4(verts[i], verts[i+1], verts[i+2], 1)); else { loopv(polyidxs) { int idx = polyidxs[i]; if(idx < 0) idx = -(idx+1); n->remap.add(idx); idx *= 3; epositions.add(Vec4(verts[idx], verts[idx+1], verts[idx+2], 1)); } } loopi(epositions.length() - poslen) eblends.add(); emesh m; m.name = getnamekey(""); m.material = getnamekey(""); m.firsttri = etriangles.length(); for(int i = poslen; i + 2 < epositions.length(); i += 3) etriangles.add(etriangle(i+1, i, i+2)); emeshes.add(m); n->mesh = emeshes.length()-1; n->firstvert = poslen; n->lastvert = epositions.length(); n->numverts = verts.length()/3; if(uvidxs.empty()) { if(polyidxs.length() && uvs.length()/2 == verts.length()/3) loopv(polyidxs) { int idx = polyidxs[i]; if(idx < 0) idx = -(idx+1); idx *= 2; etexcoords.add(Vec4(uvs[idx], 1-uvs[idx+1], 0, 0)); } else for(int i = 0; i + 1 < uvs.length(); i += 2) etexcoords.add(Vec4(uvs[i], 1-uvs[i+1], 0, 0)); } else loopv(uvidxs) { int idx = 2*uvidxs[i]; etexcoords.add(Vec4(uvs[idx], 1-uvs[idx+1], 0, 0)); } if(polyidxs.length() && norms.length() == verts.length()) loopv(polyidxs) { int idx = polyidxs[i]; if(idx < 0) idx = -(idx+1); idx *= 3; enormals.add(Vec3(norms[idx], norms[idx+1], norms[idx+2])); } else for(int i = 0; i + 2 < norms.length(); i += 3) enormals.add(Vec3(norms[i], norms[i+1], norms[i+2])); if(coloridxs.empty()) { if(polyidxs.length() && colors.length()/4 == verts.length()/3) loopv(polyidxs) { int idx = polyidxs[i]; if(idx < 0) idx = -(idx+1); idx *= 4; ecolors.add(Vec4(colors[idx], colors[idx+1], colors[idx+2], colors[idx+3])); } else for(int i = 0; i + 3 < colors.length(); i += 4) ecolors.add(Vec4(colors[i], colors[i+1], colors[i+2], colors[i+3])); } else loopv(coloridxs) { int idx = 4*coloridxs[i]; ecolors.add(Vec4(colors[idx], colors[idx+1], colors[idx+2], colors[idx+3])); } } void parsemodel() { token id, name, type, t; if(!p.parse(id) || !p.parse(name) || !p.parse(type)) return; if(id.type != token::NUMBER || type.type != token::STRING || name.type != token::STRING) { p.skipprop(); return; } char *str = name.s; if(strstr(str, "Model::") == str) str += strlen("Model::"); if(!strcmp(type.s, "Mesh")) { modelnode *n = new modelnode; copystring(n->name, str); nodes[id.f] = n; if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "Properties70")) { if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: goto endmeshprops; case token::PROP: if(!strcmp(t.s, "P")) { if(!p.parse(t)) return; if(t.type == token::STRING) { if(!strcmp(t.s, "PreRotation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->prerot[i] = t.f; } } else if(!strcmp(t.s, "GeometricTranslation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->geomtrans[i] = t.f; } } else if(!strcmp(t.s, "Lcl Translation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->lcltrans[i] = t.f; } } else if(!strcmp(t.s, "Lcl Rotation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->lclrot[i] = t.f; } } else if(!strcmp(t.s, "Lcl Scaling")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->lclscale[i] = t.f; } } } } p.skipprop(); break; } endmeshprops:; } p.skipprop(); break; } } else if(!strcmp(type.s, "LimbNode")) { limbnode *n = new limbnode; copystring(n->name, str); n->index = ejoints.length(); nodes[id.f] = n; ejoint &j = ejoints.add(); j.name = getnamekey(str); j.parent = -1; eposes.add(transform(Vec3(0, 0, 0), Quat(0, 0, 0, 1))); if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: { transform &x = eposes[n->index]; x.pos = n->trans; x.orient = Quat::fromdegrees(n->rot); x.scale = n->scale; } return; case token::PROP: if(!strcmp(t.s, "Properties70")) { if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: goto endlimbprops; case token::PROP: if(!strcmp(t.s, "P")) { if(!p.parse(t)) return; if(t.type == token::STRING) { if(!strcmp(t.s, "PreRotation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->prerot[i] = t.f; } } else if(!strcmp(t.s, "Lcl Translation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->trans[i] = t.f; } } else if(!strcmp(t.s, "Lcl Rotation")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->rot[i] = t.f; } } else if(!strcmp(t.s, "Lcl Scaling")) { loopi(3) if(!p.parse(t)) return; loopi(3) { if(!p.parse(t)) return; if(t.type != token::NUMBER) break; n->scale[i] = t.f; } } } } p.skipprop(); break; } endlimbprops:; } p.skipprop(); break; } } p.skipprop(); } void parsematerial() { token id, name; if(!p.parse(id) || !p.parse(name)) return; if(id.type == token::NUMBER) { if(name.type == token::STRING) { char *str = name.s; if(strstr(str, "Material::") == str) str += strlen("Material::"); materialnode *n = new materialnode; copystring(n->name, str); nodes[id.f] = n; } } p.skipprop(); } void parsedeformer() { token id, name, type, t; if(!p.parse(id) || !p.parse(name) || !p.parse(type)) return; if(id.type != token::NUMBER || type.type != token::STRING || name.type != token::STRING) { p.skipprop(); return; } if(!strcmp(type.s, "Skin")) { skinnode *n = new skinnode; nodes[id.f] = n; } else if(!strcmp(type.s, "Cluster")) { if(!p.findbegin()) return; clusternode *n = new clusternode; nodes[id.f] = n; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "Indexes")) p.readarray(n->indexes); else if(!strcmp(t.s, "Weights")) p.readarray(n->weights); else if(!strcmp(t.s, "Transform")) p.readarray(n->transform); else if(!strcmp(t.s, "TransformLink")) p.readarray(n->transformlink); else p.skipprop(); break; } return; } p.skipprop(); } void parsecurve() { token id, t; if(!p.parse(id)) return; if(id.type != token::NUMBER) { p.skipprop(); return; } curvenode *n = new curvenode; nodes[id.f] = n; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "KeyValueFloat")) p.readarray(n->vals); else p.skipprop(); break; } } void parsexform() { token id, t; if(!p.parse(id)) return; if(id.type != token::NUMBER) { p.skipprop(); return; } if(!p.findbegin()) return; xformnode *n = new xformnode; nodes[id.f] = n; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "Properties70")) { if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: goto endprops; case token::PROP: if(!strcmp(t.s, "P")) { token name, type, val; if(!p.parse(name) || !p.parse(type) || !p.parse(t) || !p.parse(t)) return; if(name.type == token::STRING) { if(!strcmp(name.s, "d|X")) { if(p.parse(val) && val.type == token::NUMBER) n->val.x = val.f; } else if(!strcmp(name.s, "d|Y")) { if(p.parse(val) && val.type == token::NUMBER) n->val.y = val.f; } else if(!strcmp(name.s, "d|Z")) { if(p.parse(val) && val.type == token::NUMBER) n->val.z = val.f; } } } p.skipprop(); break; } endprops:; } else p.skipprop(); break; } } void parseanimlayer() { token id, name; if(!p.parse(id) || !p.parse(name)) return; if(id.type != token::NUMBER || name.type != token::STRING) { p.skipprop(); return; } char *str = name.s; if(strstr(str, "AnimLayer::") == str) str += strlen("AnimLayer::"); animlayernode *n = new animlayernode; copystring(n->name, str); nodes[id.f] = n; p.skipprop(); } #define FBX_SEC 46186158000.0 void parseanimstack() { token id, name, t; if(!p.parse(id) || !p.parse(name)) return; if(id.type != token::NUMBER || name.type != token::STRING) { p.skipprop(); return; } char *str = name.s; if(strstr(str, "AnimStack::") == str) str += strlen("AnimStack::"); animstacknode *n = new animstacknode; copystring(n->name, str); nodes[id.f] = n; if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "Properties70")) { if(!p.findbegin()) return; while(p.parse(t)) switch(t.type) { case token::END: goto endprops; case token::PROP: if(!strcmp(t.s, "P")) { token name, type, val; if(!p.parse(name) || !p.parse(type) || !p.parse(t) || !p.parse(t)) return; if(name.type == token::STRING) { if(!strcmp(name.s, "LocalStop")) { if(p.parse(val) && val.type == token::NUMBER) n->secs = val.f / FBX_SEC; } } } p.skipprop(); break; } endprops:; } else p.skipprop(); break; } } void parseobjects() { if(!p.findbegin()) return; token t; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "Geometry")) parsegeometry(); else if(!strcmp(t.s, "Model")) parsemodel(); else if(!strcmp(t.s, "Material")) parsematerial(); else if(!strcmp(t.s, "Deformer")) parsedeformer(); else if(!strcmp(t.s, "AnimationCurve")) parsecurve(); else if(!strcmp(t.s, "AnimationCurveNode")) parsexform(); else if(!strcmp(t.s, "AnimationLayer")) parseanimlayer(); else if(!strcmp(t.s, "AnimationStack")) parseanimstack(); else p.skipprop(); break; } } void parseconnection() { token type, from, to, prop; if(!p.parse(type) || !p.parse(from) || !p.parse(to)) return; if(type.type == token::STRING && from.type == token::NUMBER && to.type == token::NUMBER) { node *nf = nodes.find(from.f, NULL), *nt = nodes.find(to.f, NULL); if(!strcmp(type.s, "OO") && nf && nt) { if(nf->type() == node::GEOM && nt->type() == node::MODEL) ((geomnode *)nf)->model = (modelnode *)nt; else if(nf->type() == node::MATERIAL && nt->type() == node::MODEL) ((modelnode *)nt)->material = (materialnode *)nf; else if(nf->type() == node::LIMB && nt->type() == node::LIMB) ((limbnode *)nf)->parent = (limbnode *)nt; else if(nf->type() == node::CLUSTER && nt->type() == node::SKIN) ((clusternode *)nf)->skin = (skinnode *)nt; else if(nf->type() == node::SKIN && nt->type() == node::GEOM) ((skinnode *)nf)->geom = (geomnode *)nt; else if(nf->type() == node::LIMB && nt->type() == node::CLUSTER) { ((clusternode *)nt)->limb = (limbnode *)nf; ((limbnode *)nf)->cluster = (clusternode *)nt; } else if(nf->type() == node::ANIMLAYER && nt->type() == node::ANIMSTACK) ((animstacknode *)nt)->layers.add((animlayernode *)nf); else if(nf->type() == node::XFORM && nt->type() == node::ANIMLAYER) ((animlayernode *)nt)->xforms.add((xformnode *)nf); } else if(!strcmp(type.s, "OP") && nf && nt && p.parse(prop) && prop.type == token::STRING) { if(nf->type() == node::CURVE && nt->type() == node::XFORM) { if(!strcmp(prop.s, "d|X")) ((xformnode *)nt)->setcurve(0, (curvenode *)nf); else if(!strcmp(prop.s, "d|Y")) ((xformnode *)nt)->setcurve(1, (curvenode *)nf); else if(!strcmp(prop.s, "d|Z")) ((xformnode *)nt)->setcurve(2, (curvenode *)nf); } else if(nf->type() == node::XFORM && nt->type() == node::LIMB) { ((xformnode *)nf)->limb = (limbnode *)nt; if(!strcmp(prop.s, "Lcl Translation")) ((xformnode *)nf)->xform = xformnode::TRANS; else if(!strcmp(prop.s, "Lcl Rotation")) ((xformnode *)nf)->xform = xformnode::ROT; else if(!strcmp(prop.s, "Lcl Scaling")) ((xformnode *)nf)->xform = xformnode::SCALE; } } } p.skipprop(); } void parseconnections() { if(!p.findbegin()) return; token t; while(p.parse(t)) switch(t.type) { case token::END: return; case token::PROP: if(!strcmp(t.s, "C")) parseconnection(); else p.skipprop(); break; } } void geomnode::process() { if(model) { emeshes[mesh].name = getnamekey(model->name); if(model->material) emeshes[mesh].material = getnamekey(model->material->name); if(model->geomtrans != Vec3(0, 0, 0)) for(int i = firstvert; i < lastvert; i++) epositions[i] += model->geomtrans; if(model->lclscale != Vec3(1, 1, 1)) { for(int i = firstvert; i < lastvert; i++) { epositions[i].setxyz(model->lclscale * Vec3(epositions[i])); } } if(model->lclrot != Vec3(0, 0, 0)) { Quat lclquat = Quat::fromdegrees(model->lclrot); for(int i = firstvert; i < lastvert; i++) { epositions[i].setxyz(lclquat.transform(Vec3(epositions[i]))); enormals[i] = lclquat.transform(enormals[i]); } } if(model->prerot != Vec3(0, 0, 0)) { Quat prequat = Quat::fromdegrees(model->prerot); for(int i = firstvert; i < lastvert; i++) { epositions[i].setxyz(prequat.transform(Vec3(epositions[i]))); enormals[i] = prequat.transform(enormals[i]); } } if(model->lcltrans != Vec3(0, 0, 0)) for(int i = firstvert; i < lastvert; i++) epositions[i] += model->lcltrans; } } void clusternode::process() { if(!limb || limb->index > 255 || !skin || !skin->geom || indexes.length() != weights.length()) return; geomnode *g = skin->geom; if(g->blends.empty()) loopi(g->numverts) g->blends.add(); loopv(indexes) { int idx = indexes[i]; double weight = weights[i]; g->blends[idx].addweight(weight, limb->index); } } void animstacknode::process() { if(layers.empty()) return; animlayernode *l = layers[0]; int numframes = l->numframes(); if(numframes < 0) return; eanim &a = eanims.add(); a.name = getnamekey(name); a.startframe = eframes.length(); a.fps = secs > 0 ? numframes/secs : 0; transform *poses = eposes.reserve(numframes*ejoints.length()); loopj(numframes) { eframes.add(eposes.length()); eposes.put(eposes.getbuf(), ejoints.length()); } loopv(l->xforms) { xformnode &x = *l->xforms[i]; if(!x.limb) continue; transform *dst = &poses[x.limb->index]; loopj(numframes) { Vec3 val = x.val; loopk(3) if(x.curves[k]) val[k] = x.curves[k]->vals[j]; switch(x.xform) { case xformnode::TRANS: dst->pos = val; break; case xformnode::ROT: dst->orient = Quat::fromdegrees(val); break; case xformnode::SCALE: dst->scale = val; break; } dst += ejoints.length(); } } #if 0 loopv(eposes) { transform &t = eposes[i]; Matrix3x3 m(t.orient, t.scale); Vec3 mscale(Vec3(m.a.x, m.b.x, m.c.x).magnitude(), Vec3(m.a.y, m.b.y, m.c.y).magnitude(), Vec3(m.a.z, m.b.z, m.c.z).magnitude()); if(m.determinant() < 0) mscale = -mscale; m.a /= mscale; m.b /= mscale; m.c /= mscale; Quat morient(m); if(morient.w > 0) morient.flip(); t.orient = morient; t.scale = mscale; } #endif } void geomnode::finish() { if(blends.empty()) return; loopv(blends) blends[i].finalize(); while(eblends.length() < lastvert) eblends.add(); if(remap.length()) loopv(remap) eblends[firstvert + i] = blends[remap[i]]; else loopv(blends) eblends[firstvert + i] = blends[i]; } void limbnode::finish() { if(prerot == Vec3(0, 0, 0)) return; Quat prequat = Quat::fromdegrees(prerot); for(int i = index; i < eposes.length(); i += ejoints.length()) eposes[i].orient = prequat * eposes[i].orient; } bool checkversion(stream *f) { return f->getline(p.buf, sizeof(p.buf)) && strstr(p.buf, "FBX 7"); } void parse(stream *f) { p.reset(f); token t; while(p.parse(t)) switch(t.type) { case token::PROP: if(!strcmp(t.s, "Objects")) parseobjects(); else if(!strcmp(t.s, "Connections")) parseconnections(); else p.skipprop(); break; } enumerate(nodes, double, id, node *, n, { (void)id; n->process(); }); enumerate(nodes, double, id, node *, n, { (void)id; n->finish(); }); enumerate(nodes, double, id, node *, n, { (void)id; delete n; }); nodes.clear(); } } bool loadfbx(const char *filename, const filespec &spec) { int numfiles = 0; while(filename) { const char *endfile = strchr(filename, ','); const char *file = endfile ? newstring(filename, endfile-filename) : filename; stream *f = openfile(file, "r"); if(f) { if(fbx::checkversion(f)) { resetimporter(spec, numfiles > 0); numfiles++; fbx::parse(f); } delete f; } if(!endfile) break; delete[] file; filename = endfile+1; } if(!numfiles) return false; if(eanims.length() == 1) { eanim &a = eanims.last(); if(spec.name) a.name = spec.name; if(spec.fps > 0) a.fps = spec.fps; a.flags |= spec.flags; if(spec.endframe >= 0) a.endframe = a.startframe + spec.endframe; else if(spec.endframe < -1) a.endframe = a.startframe + max(eframes.length() - a.startframe + spec.endframe + 1, 0); a.startframe += spec.startframe; } erotate *= Quat(M_PI/2, Vec3(1, 0, 0)); makeanims(spec); if(emeshes.length()) { smoothverts(); makemeshes(spec); } return true; } #ifdef FTEPLUGIN namespace fte { static vector cvars; static plugfsfuncs_t cppfsfuncs; static plugmodfuncs_t cppmodfuncs; static plugcorefuncs_t cppplugfuncs; static plugcvarfuncs_t cppcvarfuncs; static cvar_t *Cvar_Create(const char *name, const char *defaultval, unsigned int flags, const char *description, const char *groupname) { //could maybe fill with environment settings perhaps? yuck. cvar_t *v = NULL; for (int i = 0; i < cvars.length(); i++) if (!strcmp(cvars[i]->name, name)) return cvars[i]; if (!v) { v = cvars.add() = new cvar_t(); v->name = strdup(name); v->string = strdup(defaultval); v->value = atof(v->string); v->ival = atoi(v->string); } return v; }; static void SetupFTEPluginFuncs(void) { cppfsfuncs.OpenVFS = [](const char *filename, const char *mode, enum fs_relative relativeto) { stream *f = openfile(filename, "rb"); if (!f) return (vfsfile_t*)nullptr; struct cppfile_t : public vfsfile_t { static int ReadBytes (struct vfsfile_s *file, void *buffer, int bytestoread) { auto c = static_cast(file); return c->f->read(buffer, bytestoread); } static qboolean Seek (struct vfsfile_s *file, qofs_t pos) { auto c = static_cast(file); return c->f->seek(pos)?qtrue:qfalse; } static qofs_t Tell (struct vfsfile_s *file) { auto c = static_cast(file); return c->f->tell(); } static qofs_t GetLen (struct vfsfile_s *file) { auto c = static_cast(file); return c->f->size(); } static qboolean Close (struct vfsfile_s *file) { auto c = static_cast(file); c->f->close(); delete c; return qtrue; } cppfile_t(stream *sourcefile):f(sourcefile) { vfsfile_t::ReadBytes = ReadBytes; vfsfile_t::Seek = Seek; vfsfile_t::Tell = Tell; vfsfile_t::GetLen = GetLen; vfsfile_t::Close = Close; } stream *f; }; cppfile_t *c = new cppfile_t(f); return static_cast(c); }; cppmodfuncs.version = MODPLUGFUNCS_VERSION; cppmodfuncs.RegisterModelFormatText = [](const char *formatname, char *magictext, qboolean (QDECL *load) (struct model_s *mod, void *buffer, size_t fsize)) { //called explicitly because we're lame. return 0; }; cppmodfuncs.RegisterModelFormatMagic = [](const char *formatname, qbyte *magic, size_t magicsize, qboolean (QDECL *load) (struct model_s *mod, void *buffer, size_t fsize)) { //called explicitly because we're lame. return 0; }; cppplugfuncs.GMalloc = [](zonegroup_t *ctx, size_t size) { /*leak the memory, because we're lazy*/ void *ret = malloc(size); memset(ret, 0, size); return ret; }; cppmodfuncs.ConcatTransforms = [](const float in1[3][4], const float in2[3][4], float out[3][4]) { out[0][0] = in1[0][0] * in2[0][0] + in1[0][1] * in2[1][0] + in1[0][2] * in2[2][0]; out[0][1] = in1[0][0] * in2[0][1] + in1[0][1] * in2[1][1] + in1[0][2] * in2[2][1]; out[0][2] = in1[0][0] * in2[0][2] + in1[0][1] * in2[1][2] + in1[0][2] * in2[2][2]; out[0][3] = in1[0][0] * in2[0][3] + in1[0][1] * in2[1][3] + in1[0][2] * in2[2][3] + in1[0][3]; out[1][0] = in1[1][0] * in2[0][0] + in1[1][1] * in2[1][0] + in1[1][2] * in2[2][0]; out[1][1] = in1[1][0] * in2[0][1] + in1[1][1] * in2[1][1] + in1[1][2] * in2[2][1]; out[1][2] = in1[1][0] * in2[0][2] + in1[1][1] * in2[1][2] + in1[1][2] * in2[2][2]; out[1][3] = in1[1][0] * in2[0][3] + in1[1][1] * in2[1][3] + in1[1][2] * in2[2][3] + in1[1][3]; out[2][0] = in1[2][0] * in2[0][0] + in1[2][1] * in2[1][0] + in1[2][2] * in2[2][0]; out[2][1] = in1[2][0] * in2[0][1] + in1[2][1] * in2[1][1] + in1[2][2] * in2[2][1]; out[2][2] = in1[2][0] * in2[0][2] + in1[2][1] * in2[1][2] + in1[2][2] * in2[2][2]; out[2][3] = in1[2][0] * in2[0][3] + in1[2][1] * in2[1][3] + in1[2][2] * in2[2][3] + in1[2][3]; }; cppmodfuncs.GenMatrixPosQuat4Scale = [](const vec3_t pos, const vec4_t quat, const vec3_t scale, float result[12]) { Matrix3x4 m = Matrix3x4(Quat(quat), Vec3(pos), Vec3(scale)); result[0] = m.a.x; result[1] = m.b.x; result[2] = m.c.x; result[3] = m.a.w; result[4] = m.a.y; result[5] = m.b.y; result[6] = m.c.y; result[7] = m.b.w; result[8] = m.a.z; result[9] = m.b.z; result[10] = m.c.z; result[11] = m.c.w; }; cppmodfuncs.GetTexture = [](const char *identifier, const char *subpath, unsigned int flags, void *fallbackdata, void *fallbackpalette, int fallbackwidth, int fallbackheight, uploadfmt_t fallbackfmt) { image_t *img = (image_t*)cppplugfuncs.GMalloc(NULL, sizeof(*img)+strlen(identifier)+1); img->ident = (char*)(img+1); strcpy(img->ident, identifier); img->flags = flags; return img; }; cppmodfuncs.AccumulateTextureVectors = [](vecV_t *const vc, vec2_t *const tc, vec3_t *nv, vec3_t *sv, vec3_t *tv, const index_t *idx, int numidx, qboolean calcnorms) { //once per surface that shares the set of verts }; cppmodfuncs.NormaliseTextureVectors = [](vec3_t *n, vec3_t *s, vec3_t *t, int v, qboolean calcnorms) { //once per shared set of verts. }; cppplugfuncs.GetEngineInterface = [](const char *interfacename, size_t structsize) { void *ret = nullptr; if (!strcmp(interfacename, plugfsfuncs_name)) ret = &cppfsfuncs; if (!strcmp(interfacename, plugmodfuncs_name)) ret = &cppmodfuncs; return ret; }; cppcvarfuncs.GetNVFDG = Cvar_Create; } extern "C" { //our plugin-style stuff has a few external dependancies not provided via pointers... void ImgTool_SetupPalette(void); qboolean QDECL Mod_LoadGLTFModel (struct model_s *mod, void *buffer, size_t fsize); qboolean QDECL Mod_LoadGLBModel (struct model_s *mod, void *buffer, size_t fsize); qboolean Plug_GLTF_Init(void); plugcorefuncs_t *plugfuncs = &cppplugfuncs; plugcmdfuncs_t *cmdfuncs; plugcvarfuncs_t *cvarfuncs = &cppcvarfuncs; void Q_strlcpy(char *d, const char *s, int n) { int i; n--; if (n < 0) return; //this could be an error for (i=0; *s; i++) { if (i == n) break; *d++ = *s++; } *d='\0'; } void Q_strlcat(char *d, const char *s, int n) { if (n) { int dlen = strlen(d); int slen = strlen(s)+1; if (slen > (n-1)-dlen) slen = (n-1)-dlen; memcpy(d+dlen, s, slen); d[n - 1] = 0; } } } transform ftetransform(float bm[12], bool invert) { Matrix3x3 m(Vec3(bm[0], bm[1], bm[2]), Vec3(bm[4], bm[5], bm[6]), Vec3(bm[8], bm[9], bm[10])); m.transpose(); Vec3 pos(bm[3], bm[7], bm[11]); transform t; Vec3 mscale(Vec3(m.a.x, m.b.x, m.c.x).magnitude(), Vec3(m.a.y, m.b.y, m.c.y).magnitude(), Vec3(m.a.z, m.b.z, m.c.z).magnitude()); // check determinant for sign of scaling if(Matrix3x3(m).determinant() < 0) mscale = -mscale; m.a /= mscale; m.b /= mscale; m.c /= mscale; t.orient = Quat(m); if(t.orient.w > 0) t.orient.flip(); t.scale = mscale; if (invert) { // invert the translate t.pos[0] = -(pos[0] * m.a[0] + pos[1] * m.a[1] + pos[2] * m.a[2]); t.pos[1] = -(pos[0] * m.b[0] + pos[1] * m.b[1] + pos[2] * m.b[2]); t.pos[2] = -(pos[0] * m.c[0] + pos[1] * m.c[1] + pos[2] * m.c[2]); } else t.pos = pos; return t; } bool loadfte(model_t *mod, const filespec &spec) { //import from fte's structs and convert to iqmtool's c++isms if (mod->type != mod_alias) return false; //err... galiasinfo_t *root = (galiasinfo_t*)mod->meshinfo, *surf=root; if (surf) { resetimporter(spec); if (surf->baseframeofs) { int b2; for (int b = 0; b < surf->numbones; b++) { transform p(ftetransform(surf->ofsbones[b].inverse, true)); //spit out the joint info ejoint &j = ejoints.add(); j.name = surf->ofsbones[b].name; j.parent = surf->ofsbones[b].parent; for (b2 = 0; b2 < b; b2++) { if (!strcmp(surf->ofsbones[b2].name, surf->ofsbones[b].name)) { defformatstring(newname, "b#%i %s", b, j.name); conoutf("warning: Bones %i and %i are both called %s", b2, b, j.name); j.name = getnamekey(newname); break; } } //and the base pose eposes.add(p); } makerelativebasepose(); #if 1 //import the animations for (int animidx = 0; animidx < surf->numanimations; animidx++) { auto &anim = surf->ofsanimations[animidx]; int firstframe = eframes.length(); vector bonebuf; if (!anim.numposes) //drop any animations that don't have any poses defined... continue; for (int f = 0; f < anim.numposes; f++) { skeltype_t sk = anim.skeltype; float time = f/anim.rate; float *bonedata; if (anim.GetRawBones) bonedata = anim.GetRawBones(surf, &anim, time, bonebuf.reserve(surf->numbones)[0], NULL, surf->numbones); else if (anim.boneofs) bonedata = (float*)anim.boneofs; else bonedata = (float*)surf->baseframeofs, sk = SKEL_ABSOLUTE; //abs... if (sk == SKEL_RELATIVE) ; else conoutf("warning: Unusable skeletal type for import - %i", (int)sk); eframes.add(eposes.length()); for (int b = 0; b < surf->numbones; b++, bonedata += 12) eposes.add(ftetransform(bonedata, false)); } eanim &a = eanims.add(); if(spec.name) a.name = getnamekey(spec.name); else if (*anim.name) a.name = getnamekey(anim.name); else { string name; copystring(name, mod->name); char *end = strrchr(name, '.'); if(end) *end = '\0'; a.name = getnamekey(name); } a.startframe = firstframe; a.fps = anim.rate; a.flags = anim.loop?IQM_LOOP:0; a.flags |= spec.flags; a.endframe = eframes.length(); } #endif } for(; surf; surf = surf->nextsurf) { if (surf->shares_bones != root->shares_bones) { conoutf("warning: Ignoring surface %s as it has a different rig", surf->surfacename); continue; } if (surf->numindexes) { unsigned int firstvert=epositions.length(); for (int v = 0; v < surf->numverts; v++) { Vec3 pos(surf->ofs_skel_xyz[v][0], surf->ofs_skel_xyz[v][1], surf->ofs_skel_xyz[v][2]); Vec3 norm; if (surf->ofs_skel_norm) norm = Vec3(surf->ofs_skel_norm[v][0], surf->ofs_skel_norm[v][1], surf->ofs_skel_norm[v][2]); else norm = Vec3(0,0,0); etexcoords.add(Vec4(surf->ofs_st_array[v][0], surf->ofs_st_array[v][1], 0, 0)); if (surf->ofs_rgbaf) ecolors.add(Vec4(surf->ofs_rgbaf[v][0], surf->ofs_rgbaf[v][1], surf->ofs_rgbaf[v][2], surf->ofs_rgbaf[v][3])); else if (surf->ofs_rgbaub) ecolors.add(Vec4(surf->ofs_rgbaub[v][0]/255.0, surf->ofs_rgbaub[v][1]/255.0, surf->ofs_rgbaub[v][2]/255.0, surf->ofs_rgbaub[v][3]/255.0)); // if (surf->ofs_skel_svect) // etangents.add (Vec4(surf->ofs_skel_svect[v][0], surf->ofs_skel_svect[v][1], surf->ofs_skel_svect[v][2], 0)); // if (surf->ofs_skel_tvect) // ebitangents.add(Vec3(surf->ofs_skel_tvect[v][0], surf->ofs_skel_tvect[v][1], surf->ofs_skel_tvect[v][2])); if (surf->shares_bones == root->shares_bones && surf->ofs_skel_weight && surf->ofs_skel_idx) { blendcombo b = {}; //Vec3 newpos(0,0,0); //Vec3 newnorm(0,0,0); for (size_t w = 0; w < 4; w++) { //newpos += bonerepositions[surf->ofs_skel_idx[v][w]].transform(pos) * surf->ofs_skel_weight[v][w]; //newnorm += bonerepositions[surf->ofs_skel_idx[v][w]].transform3(norm) * surf->ofs_skel_weight[v][w]; if (surf->ofs_skel_weight[v][w] > 0) b.addweight(surf->ofs_skel_weight[v][w], surf->ofs_skel_idx[v][w]); } b.finalize(); eblends.add(b); //pos = newpos; //norm = newnorm; } epositions.add(Vec4(pos)); enormals.add(norm); } //iqms don't support skins/skingroups themselves. //we have only surface name and texture(aka material) name. //so use the diffuse texture's name where we can // a) its already processed properly so no ''path/model.gltf/sectionthatdoesntevenexistondisk' locations. // b) its more likely to show something without needing to synthesize shaders/textures. //we should probably cvar this. const char *materialname; if (surf->numskins && surf->ofsskins[0].numframes && surf->ofsskins[0].frame[0].texnums.base && *surf->ofsskins[0].frame[0].texnums.base->ident != '$') materialname = surf->ofsskins[0].frame[0].texnums.base->ident; else if (surf->numskins) materialname = surf->ofsskins[0].name; else materialname = surf->surfacename; if (surf->nummorphs) conoutf("warning: Morph targets on input surface \"%s\" \"%s\" cannot be supported", surf->surfacename, materialname); emesh mesh(surf->surfacename, materialname, etriangles.length()); //add in some extra surface properties. mesh.hasexplicits = true; mesh.explicits.contents = surf->contents; mesh.explicits.surfaceflags = surf->csurface.flags; mesh.explicits.body = surf->surfaceid; mesh.explicits.geomset = surf->geomset; mesh.explicits.geomid = surf->geomid; mesh.explicits.mindist = surf->mindist; mesh.explicits.maxdist = surf->maxdist; emeshes.add(mesh); for (int idx = 0; idx+2 < surf->numindexes; idx+=3) etriangles.add(etriangle(surf->ofs_indexes[idx+0]+firstvert, surf->ofs_indexes[idx+1]+firstvert, surf->ofs_indexes[idx+2]+firstvert)); } } makeanims(spec); if (emeshes.length()) { smoothverts(); makemeshes(spec); } return true; } return false; } bool loadglb(const char *filename, const filespec &spec) { bool ret = false; model_t mod={}; stream *f = openfile(filename, "rb"); Q_strlcpy(mod.name, filename, sizeof(mod.name)); if (f) { size_t sz = f->size(); auto filebuf = new char[sz]; if (sz == f->read(filebuf, sz)) { SetupFTEPluginFuncs(); if (Plug_GLTF_Init()) if (Mod_LoadGLBModel(&mod, filebuf, sz)) ret = loadfte(&mod, spec); } delete[] filebuf; delete f; } return ret; } bool loadgltf(const char *filename, const filespec &spec) { bool ret = false; model_t mod={}; stream *f = openfile(filename, "rb"); Q_strlcpy(mod.name, filename, sizeof(mod.name)); if (f) { size_t sz = f->size(); auto filebuf = new char[sz]; if (sz == f->read(filebuf, sz)) { SetupFTEPluginFuncs(); if (Plug_GLTF_Init()) if (Mod_LoadGLTFModel(&mod, filebuf, sz)) ret = loadfte(&mod, spec); } delete[] filebuf; delete f; } return ret; } } #endif void genhitboxes(vector &hitboxes) { //for half-life weenies that are too lazy to define their own hitmeshes if (!hitboxes.length()) return; filespec inspec; inspec.reset(); resetimporter(inspec); loopv(hitboxes) { hitbox &hb = hitboxes[i]; int bone = -1; for (bone = 0; bone < joints.length(); bone++) if (!strcasecmp(hb.bone, &stringdata[joints[bone].name])) break; if (bone == joints.length()) { fatal("error: hitbox attached to invalid bone %s", hb.bone); continue; //this hitbox is invalid } emesh &m = emeshes.add(); int firstvert = epositions.length(); m.firsttri = etriangles.length(); m.material = "textures/common/hitmesh"; //to be vaugely compatible with q3map2's default shader names string tmp; formatstring(tmp, "hitbox%i", hitboxes[i].body); m.name = newstring(tmp); m.hasexplicits = true; m.explicits = {}; m.explicits.contents = 0x02000000; m.explicits.surfaceflags = 0x80; m.explicits.body = hitboxes[i].body; m.explicits.geomset = ~0u; //spit out some verts Matrix3x4 bm(mjoints[bone]); bm.invert(); for (int j = 0; j < 8; j++) { Vec3 p = Vec3((j&1)?hb.mins[0]:hb.maxs[0], (j&2)?hb.mins[1]:hb.maxs[1], (j&4)?hb.mins[2]:hb.maxs[2]); p = bm.transform(p); epositions.add(Vec4(p, 0)); enormals.add(p); etexcoords.add(Vec4(0,0,0,0)); eblends.add(blendcombo()).addweight(1, bone); } //and some triangles for them etriangles.add(etriangle(firstvert+2, firstvert+1, firstvert+0)); etriangles.add(etriangle(firstvert+2, firstvert+3, firstvert+1)); etriangles.add(etriangle(firstvert+4, firstvert+5, firstvert+6)); etriangles.add(etriangle(firstvert+5, firstvert+7, firstvert+6)); etriangles.add(etriangle(firstvert+0, firstvert+1, firstvert+4)); etriangles.add(etriangle(firstvert+1, firstvert+5, firstvert+4)); etriangles.add(etriangle(firstvert+6, firstvert+3, firstvert+2)); etriangles.add(etriangle(firstvert+6, firstvert+7, firstvert+3)); etriangles.add(etriangle(firstvert+4, firstvert+2, firstvert+0)); etriangles.add(etriangle(firstvert+4, firstvert+6, firstvert+2)); etriangles.add(etriangle(firstvert+1, firstvert+3, firstvert+5)); etriangles.add(etriangle(firstvert+3, firstvert+7, firstvert+5)); } smoothverts(); makemeshes(inspec); } int framesize = 0; vector animdata; #define QUANTIZE(offset, base, scale) ushort(0.5f + (float(offset) - base) / scale) static int jsort(const void *va, const void *vb) { joint &a = joints[*(int*)va]; joint &b = joints[*(int*)vb]; if (a.group == b.group) { if (*(int*)va > *(int*)vb) return 1; else return -1; } else if (a.group < b.group) return -1; else return 1; } void calcanimdata() { hashtable bonewarnings; //reorder the joints according to their groups, including a lookup so we can fix up other mappings int *jointremap = new int[joints.length()]; int *jointremapinv = new int[joints.length()]; loopv(joints) jointremap[i] = i; qsort(jointremap, joints.length(), sizeof(int), jsort); vector oj; joints.swap(oj); bool dodgyorder = false; loopv(oj) jointremapinv[jointremap[i]] = i; loopv(oj) { joint &j = joints.add(oj[jointremap[i]]); if (j.parent >= 0) { j.parent = jointremapinv[j.parent]; if (j.parent >= i) dodgyorder = true; } } if (dodgyorder) { printbonelist(); fatal("Bone group reordering resulted in invalid order"); } //try and ensure that the animation bone order matches the mesh bones loopv(joints) { pose &j = poses.add(); j.name = &stringdata[joints[i].name]; j.parent = joints[i].parent; loopk(10) { j.offset[k] = 1e16f; j.scale[k] = -1e16f; } } loopv(frames) { frame &fr = frames[i]; loopl(fr.pose.length()) { frame::framepose &p = fr.pose[l]; p.remap = -1; loopvk(poses) { // if (poses[k].parent == p.boneparent) if (!strcmp(poses[k].name, p.bonename)) { if (poses[k].parent == -1 || p.boneparent == -1) { if (poses[k].parent != -1 || p.boneparent != -1) fatal("Error: bone %s has inconsistent parents\n", p.bonename); } else if (strcmp(poses[poses[k].parent].name, fr.pose[p.boneparent].bonename)) fatal("Error: bone %s has inconsistent parents (%s vs %s)\n", p.bonename, poses[poses[k].parent].name, fr.pose[p.boneparent].bonename); p.remap = k; break; } } if(p.remap < 0) { //if we have a mesh, then any extra bones are surplus to requirements. //otherwise we play safe and keep all (which is kinda awkward, because there's no way to name them in the output iqm). if (!joints.empty()) { if (!bonewarnings.find(p.bonename, false)) { const char *a = "UNKNOWN"; loopvj(anims) { if ((uint)i >= anims[j].firstframe && (uint)i < anims[j].firstframe+anims[j].numframes) { a = &stringdata[anims[j].name]; break; } } bonewarnings.access(p.bonename, true); if (p.boneparent >= 0) conoutf("warning: ignoring bone %s (parent %s) (surplus in %s)", p.bonename, fr.pose[p.boneparent].bonename, a); else conoutf("warning: ignoring bone %s (root) (surplus in %s)", p.bonename, a); } continue; } if (p.boneparent >= 0) conoutf("bone %s (%s)", p.bonename, poses[p.boneparent].name); else conoutf("bone %s", p.bonename); p.remap = poses.length(); pose &j = poses.add(); j.name = p.bonename; j.parent = -1; if(p.boneparent >= 0) { loopk(p.remap) { if (!strcmp(poses[k].name, fr.pose[p.boneparent].bonename)) { j.parent = k; break; } } } loopk(10) { j.offset[k] = 1e16f; j.scale[k] = -1e16f; } } pose &j = poses[p.remap]; transform &f = p.tr; loopk(3) { j.offset[k] = min(j.offset[k], float(f.pos[k])); j.scale[k] = max(j.scale[k], float(f.pos[k])); } loopk(4) { j.offset[3+k] = min(j.offset[3+k], float(f.orient[k])); j.scale[3+k] = max(j.scale[3+k], float(f.orient[k])); } loopk(3) { j.offset[7+k] = min(j.offset[7+k], float(f.scale[k])); j.scale[7+k] = max(j.scale[7+k], float(f.scale[k])); } } } loopv(poses) { pose &j = poses[i]; loopk(10) { j.scale[k] -= j.offset[k]; if(j.scale[k] >= 1e-10f) { framesize++; j.scale[k] /= 0xFFFF; j.flags |= 1<= 0xFF : runlength || blocksize > 0xFF)) \ { \ animdata.add(0); \ animdata.add(val); \ blocksize = 1; \ runlength = 0; \ blocks++; \ } \ else if(animdata.last() == val) \ { \ animdata[animdata.length()-blocksize-1] += 0x10; \ runlength++; \ } \ else \ { \ animdata[animdata.length()-blocksize-1]++; \ animdata.add(val); \ blocksize++; \ } loopv(joints) { joint &j = joints[i]; loopk(3) if(j.flags & (0x01<=0) {tr[fr.pose[k].remap] = fr.pose[k].tr; def[fr.pose[k].remap] &= ~1;} loopvk(poses) { if (def[k] == 1) { //if this bone didn't have any data and is still in an identity pose, warn about it. def[k] |= 2; const char *a = "UNKNOWN"; loopvj(anims) { if ((uint)i >= anims[j].firstframe && (uint)i < anims[j].firstframe+anims[j].numframes) { a = &stringdata[anims[j].name]; break; } } conoutf("warning: bone %s defaulted (missing in %s)", poses[k].name, a); } pose &j = poses[k]; transform &f = tr[k]; loopk(3) if(j.flags & (0x01< extensions; filestream *f = (filestream *) openfile(filename, "wb"); if(!f) return false; iqmheader hdr; memset(&hdr, 0, sizeof(hdr)); copystring(hdr.magic, IQM_MAGIC, sizeof(hdr.magic)); hdr.version = IQM_VERSION; hdr.filesize = sizeof(hdr); hdr.flags = modelflags; iqmextension *ext_meshes_fte = NULL; if (meshes_fte.length()) { ext_meshes_fte = &extensions.add(); ext_meshes_fte->name = sharestring("FTE_MESH"); } iqmextension *ext_events_fte = NULL; if (events_fte.length()) { ext_events_fte = &extensions.add(); ext_events_fte->name = sharestring("FTE_EVENT"); loopv(events_fte) { event_fte &ev = events_fte[i]; ev.evdata_idx = sharestring(ev.evdata_str); } } iqmextension *ext_skins_fte = NULL; if (meshskins.length()) { ext_skins_fte = &extensions.add(); ext_skins_fte->name = sharestring("FTE_SKINS"); ext_skins_fte->num_data = sizeof(iqmext_fte_skin); ext_skins_fte->num_data += meshes.length()*sizeof(uint); ext_skins_fte->num_data += skinframes.length()*sizeof(iqmext_fte_skin_skinframe); ext_skins_fte->num_data += meshskins.length()*sizeof(iqmext_fte_skin_meshskin); } if(stringdata.length()) hdr.ofs_text = pad_field_ofs(hdr.filesize), hdr.num_text = stringdata.length(), hdr.filesize = hdr.ofs_text + hdr.num_text; hdr.num_meshes = meshes.length(); if(meshes.length()) hdr.ofs_meshes = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_meshes + meshes.length() * sizeof(iqmmesh); hdr.num_vertexarrays = varrays.length(); if(varrays.length()) hdr.ofs_vertexarrays = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_vertexarrays + varrays.length() * sizeof(iqmvertexarray); uint valign = (8 - (hdr.filesize%8))%8; uint voffset = hdr.filesize + valign; hdr.filesize += valign + vdata.length(); hdr.num_vertexes = numfverts; hdr.num_triangles = triangles.length(); if(triangles.length()) hdr.ofs_triangles = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_triangles + triangles.length() * sizeof(iqmtriangle); if(neighbors.length()) hdr.ofs_adjacency = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_adjacency + neighbors.length() * sizeof(iqmtriangle); hdr.num_joints = joints.length(); if(joints.length()) hdr.ofs_joints = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_joints + joints.length() * sizeof(iqmjoint); hdr.num_poses = poses.length(); if(poses.length()) hdr.ofs_poses = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_poses + poses.length() * sizeof(iqmpose); hdr.num_anims = anims.length(); if(anims.length()) hdr.ofs_anims = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_anims + anims.length() * sizeof(iqmanim); hdr.num_frames = frames.length(); hdr.num_framechannels = framesize; if(animdata.length()) hdr.ofs_frames = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_frames + animdata.length() * sizeof(ushort); if(bounds.length()) hdr.ofs_bounds = pad_field_ofs(hdr.filesize), hdr.filesize = hdr.ofs_bounds + bounds.length() * sizeof(float[8]); if(commentdata.length()) hdr.ofs_comment = pad_field_ofs(hdr.filesize), hdr.num_comment = commentdata.length(), hdr.filesize = hdr.ofs_comment + hdr.num_comment; if (extensions.length()) hdr.ofs_extensions = pad_field_ofs(hdr.filesize), hdr.num_extensions = extensions.length(), hdr.filesize = hdr.ofs_extensions + sizeof(iqmextension) * hdr.num_extensions; if (ext_meshes_fte) ext_meshes_fte->ofs_data = pad_field_ofs(hdr.filesize), ext_meshes_fte->num_data = meshes_fte.length()*sizeof(iqmext_fte_mesh), hdr.filesize = ext_meshes_fte->ofs_data + ext_meshes_fte->num_data; if (ext_events_fte) ext_events_fte->ofs_data = pad_field_ofs(hdr.filesize), ext_events_fte->num_data = events_fte.length()*sizeof(iqmext_fte_events), hdr.filesize = ext_events_fte->ofs_data + ext_events_fte->num_data; if (ext_skins_fte) ext_skins_fte->ofs_data = pad_field_ofs(hdr.filesize), hdr.filesize = ext_skins_fte->ofs_data + ext_skins_fte->num_data; lilswap(&hdr.version, (sizeof(hdr) - sizeof(hdr.magic))/sizeof(uint)); f->write(&hdr, sizeof(hdr)); // Move file write position to location specified by ofs_text for(int i = f->tell(); i < hdr.ofs_text; i++) { f->putchar(0); } if(stringdata.length()) f->write(stringdata.getbuf(), stringdata.length()); // Move file write position to location specified by ofs_meshes for(int i = f->tell(); i < hdr.ofs_meshes; i++) { f->putchar(0); } loopv(meshes) { mesh &m = meshes[i]; f->putlil(m.name); f->putlil(m.material); f->putlil(m.firstvert); f->putlil(m.numverts); f->putlil(m.firsttri); f->putlil(m.numtris); } // Move file write position to location specified by ofs_vertexarrays for(int i = f->tell(); i < hdr.ofs_vertexarrays; i++) { f->putchar(0); } loopv(varrays) { vertexarray &v = varrays[i]; f->putlil(v.type); f->putlil(v.flags); f->putlil(v.format); f->putlil(v.size); f->putlil(voffset + v.offset); } loopi(valign) f->putchar(0); f->write(vdata.getbuf(), vdata.length()); // Move file write position to location specified by ofs_triangles for(int i = f->tell(); i < hdr.ofs_triangles; i++) { f->putchar(0); } loopv(triangles) { triangle &t = triangles[i]; loopk(3) f->putlil(t.vert[k]); } // Move file write position to location specified by ofs_adjacency for(int i = f->tell(); i < hdr.ofs_adjacency; i++) { f->putchar(0); } loopv(neighbors) { triangle &t = neighbors[i]; loopk(3) f->putlil(t.vert[k]); } // Move file write position to location specified by ofs_joints for(int i = f->tell(); i < hdr.ofs_joints; i++) { f->putchar(0); } loopv(joints) { joint &j = joints[i]; f->putlil(j.name); f->putlil(j.parent); loopk(3) f->putlil(float(j.pos[k])); loopk(4) f->putlil(float(j.orient[k])); loopk(3) f->putlil(float(j.scale[k])); } // Move file write position to location specified by ofs_poses for(int i = f->tell(); i < hdr.ofs_poses; i++) { f->putchar(0); } loopv(poses) { pose &p = poses[i]; f->putlil(p.parent); f->putlil(p.flags); loopk(10) f->putlil(p.offset[k]); loopk(10) f->putlil(p.scale[k]); } // Move file write position to location specified by ofs_anims for(int i = f->tell(); i < hdr.ofs_anims; i++) { f->putchar(0); } loopv(anims) { anim &a = anims[i]; f->putlil(a.name); f->putlil(a.firstframe); f->putlil(a.numframes); f->putlil(a.fps); f->putlil(a.flags); } // Move file write position to location specified by ofs_frames for(int i = f->tell(); i < hdr.ofs_frames; i++) { f->putchar(0); } loopv(animdata) f->putlil(animdata[i]); // Move file write position to location specified by ofs_bounds for(int i = f->tell(); i < hdr.ofs_bounds; i++) { f->putchar(0); } loopv(bounds) { framebounds &b = bounds[i]; loopk(3) f->putlil(float(b.bbmin[k])); loopk(3) f->putlil(float(b.bbmax[k])); f->putlil(float(b.xyradius)); f->putlil(float(b.radius)); } if(commentdata.length()) { // Move file write position to location specified by ofs_comment for(int i = f->tell(); i < hdr.ofs_comment; i++) { f->putchar(0); } f->write(commentdata.getbuf(), commentdata.length()); } // Move file write position to location specified by ofs_extensions for(int i = f->tell(); i < hdr.ofs_extensions; i++) { f->putchar(0); } loopv (extensions) { iqmextension &ext = extensions[i]; f->putlil(ext.name); f->putlil(ext.num_data); f->putlil(ext.ofs_data); if (i == extensions.length()-1) f->putlil(0); else f->putlil((uint)(hdr.ofs_extensions + (i+1)*sizeof(ext))); } if (ext_meshes_fte) { // Move file write position to location specified by ext_meshes_fte->ofs_data for(int i = f->tell(); i < ext_meshes_fte->ofs_data; i++) { f->putchar(0); } loopv(meshes_fte) { meshprop &mf = meshes_fte[i]; f->putlil(mf.contents); f->putlil(mf.surfaceflags); f->putlil(mf.body); f->putlil(mf.geomset); f->putlil(mf.geomid); f->putlil(mf.mindist); f->putlil(mf.maxdist); } } if (ext_events_fte) { // Move file write position to location specified by ext_events_fte->ofs_data for(int i = f->tell(); i < ext_events_fte->ofs_data; i++) { f->putchar(0); } loopv(events_fte) { event_fte &ev = events_fte[i]; f->putlil(ev.anim); f->putlil(ev.timestamp); f->putlil(ev.evcode); f->putlil(ev.evdata_idx); } } if (ext_skins_fte) { // Move file write position to location specified by ext_skins_fte->ofs_data for(int i = f->tell(); i < ext_skins_fte->ofs_data; i++) { f->putchar(0); } f->putlil(skinframes.length()); f->putlil(meshskins.length()); loopv(meshes) f->putlil(meshes[i].numskins); loopv(skinframes) { f->putlil(skinframes[i].material_idx); f->putlil(skinframes[i].shadertext_idx); } loopv(meshskins) { f->putlil(meshskins[i].firstframe); f->putlil(meshskins[i].countframes); f->putlil(meshskins[i].interval); } } delete f; return true; } #ifdef IQMTOOL_MDLEXPORT static uchar qmdl_bestnorm(Vec3 &v) { #define NUMVERTEXNORMALS 162 static float r_avertexnormals[NUMVERTEXNORMALS][3] = { #include "anorms.h" }; uchar best = 0; float bestdot = -FLT_MAX, dot; for (size_t i = 0; i < countof(r_avertexnormals); i++) { dot = DotProduct(v, r_avertexnormals[i]); if (dot > bestdot) { bestdot = dot; best = i; } } return best; } struct qmdl_vertex_t { unsigned char v[3]; unsigned char normalIndex; }; static bool writemdl(const char *filename, bool md16) { if (meshes.length() != 1) { conoutf("warning: mdl output requires exactly one mesh"); if (meshes.length() < 0) return false; //must have ONE mesh only. else conoutf("using first..."); } auto mesh = meshes[0]; //should probably favour the mesh with the most verts or something. vertexarray *texcoords = NULL; vertexarray *vertcoords = NULL; vertexarray *vertnorm = NULL; vertexarray *vertbones = NULL; vertexarray *vertweights = NULL; uint skinwidth = 0; uint skinheight = 0; Vec3 offset={0,0,0}; Vec3 scale={1,1,1}; uint numskins = 0; vector skindata; unsigned char *paletteddata; loopv(varrays) { if(varrays[i].type == IQM_TEXCOORD && varrays[i].format == IQM_FLOAT && varrays[i].size == 2) texcoords = &varrays[i]; if(varrays[i].type == IQM_POSITION && varrays[i].format == IQM_FLOAT && varrays[i].size == 3) vertcoords = &varrays[i]; if(varrays[i].type == IQM_NORMAL && varrays[i].format == IQM_FLOAT && varrays[i].size == 3) vertnorm = &varrays[i]; if(varrays[i].type == IQM_BLENDINDEXES && varrays[i].format == IQM_UBYTE && varrays[i].size == 4) vertbones = &varrays[i]; if(varrays[i].type == IQM_BLENDWEIGHTS && varrays[i].format == IQM_UBYTE && varrays[i].size == 4) vertweights = &varrays[i]; } if (!texcoords) { conoutf("warning: mdl output requires a float texcoord array"); return false; //must have some vertex coords... } if (!vertcoords) { conoutf("warning: mdl output requires a suitable vertex positions array..."); return false; //must have some vertex coords... } if (!vertnorm) { conoutf("warning: mdl output requires a suitable vertex normals array..."); return false; //must have some vertex coords... } float *tcdata = (float*)texcoords->vdata.getbuf(); //the actual mdl limit is really annoying to calculate. if (mesh.numverts >= 1024) conoutf("Writing mdl %s with %u verts exceeds regular limit of %u", filename, mesh.numverts, 1024); //read the skin... size_t filesize=0; qbyte *filedata = NULL; auto s = openfile(&stringdata[mesh.material], "rb"); if (s) { filesize = s->size(); filedata = (qbyte*)malloc(filesize); s->read(filedata, filesize); delete s; } //decode it... fte::ImgTool_SetupPalette(); struct pendingtextureinfo *tex = NULL; if (filedata) tex = Image_LoadMipsFromMemory(IF_NOMIPMAP, &stringdata[mesh.material], &stringdata[mesh.material], filedata, filesize); else conoutf("could not open file %s", &stringdata[mesh.material]); if (tex) { //okay, we have a valid image! #if 1 //downsize it to work around glquake's limitations. square textures will generally end up 256*256 instead of 512*512 due to that stupid 480 height limit int newwidth = tex->mip[0].width; int newheight = tex->mip[0].height; auto npotup = [](unsigned val) { //convert to npot, rounding up. unsigned scaled = 1; while(scaled < val) scaled<<=1; return scaled; }; while (newwidth*newheight > 640*480/*GL_Upload8 limit*/ || npotup(newwidth)*npotup(newheight) > 1024*512/*GL_Upload32 limit, may be higher thanks to gl_max_size or gl_picmip but really that sucks*/ || newheight > 480/*Mod_LoadAliasModel limit -- weird MAX_LBM_HEIGHT check*/) { newwidth >>= 1; newheight >>= 1; } auto resized = (tex->mip[0].width == newwidth&&tex->mip[0].height == newheight)?NULL:Image_ResampleTexture(tex->encoding, tex->mip[0].data, tex->mip[0].width, tex->mip[0].height, NULL, newwidth, newheight); if (resized) { tex->mip[0].data = resized; tex->mip[0].datasize = 0; /*o.O*/ tex->mip[0].width = newwidth; tex->mip[0].height = newheight; } #endif //palettize it, to match the q1 palette. qboolean allowedformats[PTI_MAX] = {}; allowedformats[PTI_P8]=qtrue; //FIXME: add hexen2's alpha stuff? conoutf("Palettizing \"%s\" (%u*%u)", &stringdata[mesh.material], tex->mip[0].width, tex->mip[0].height); Image_ChangeFormat(tex, allowedformats, PTI_INVALID, "foo"); skinwidth = tex->mip[0].width; skinheight = tex->mip[0].height; } else { //texture coords are ints. if we don't have a large enough texture then we don't have much texture coord precision either, so use something reasonable. skinwidth = 128; skinheight = 128; } paletteddata = skindata.reserve(skinwidth*skinheight); if (tex) { memcpy(paletteddata, tex->mip[0].data, skinwidth*skinheight); *paletteddata^=1; //try to work around glquake's flood fill... } else { //fill with some sort of grey. memset(paletteddata, 8, skinwidth*skinheight); paletteddata[0] = 7; //work around flood filling... } skindata.advance(skinwidth*skinheight); numskins++; //we're going to need the transformed pose data, without any bone weights getting in the way. vector vpos, vnorm; Vec3 min={FLT_MAX,FLT_MAX,FLT_MAX}, max={-FLT_MAX,-FLT_MAX,-FLT_MAX}; if (!anims.length()) { Vec3 *outv = vpos.reserve(mesh.numverts); Vec3 *outn = vnorm.reserve(mesh.numverts); auto invert = (float*)vertcoords->vdata.getbuf(); auto innorm = (float*)vertnorm->vdata.getbuf(); // auto inbones = (uchar*)vertbones->vdata.getbuf(); // auto inweights = (uchar*)vertweights->vdata.getbuf(); //FIXME: generate bone matricies from base pose? or just use the vertex data as-is... for (uint i = mesh.firstvert; i < mesh.firstvert+mesh.numverts; i++, outv++, outn++) { //FIXME: generate vert's matrix //transform each vert *outv = Vec3(invert[i*3+0], invert[i*3+1], invert[i*3+2]); //bound it to find the model's extents for (uint c = 0; c < 3; c++) { if (min.v[c] > outv->v[c]) min.v[c] = outv->v[c]; if (max.v[c] < outv->v[c]) max.v[c] = outv->v[c]; } *outn = Vec3(innorm[i*3+0], innorm[i*3+1], innorm[i*3+2]); } vpos.advance(mesh.numverts); vnorm.advance(mesh.numverts); } else loopv(anims) { anim &a = anims[i]; Vec3 *outv = vpos.reserve(mesh.numverts*a.numframes); Vec3 *outn = vnorm.reserve(mesh.numverts*a.numframes); // Matrix3x4 bonepose[joints.length()]; vector bonepose; bonepose.reserve(joints.length()); auto invert = (float*)vertcoords->vdata.getbuf(); auto innorm = (float*)vertnorm->vdata.getbuf(); auto inbones = vertbones?(uchar*)vertbones->vdata.getbuf():NULL; auto inweights = vertweights?(uchar*)vertweights->vdata.getbuf():NULL; if (!inbones || !inweights) printf("no bone indexes\n"); for (uint j = 0; j < a.numframes; j++) { //build absolute poses. frame &fr = frames[a.firstframe+j]; for (int b = 0; b < fr.pose.length(); b++) { auto &frpose = fr.pose[b]; bonepose[b] = Matrix3x4(Quat(frpose.tr.orient), Vec3(frpose.tr.pos), Vec3(frpose.tr.scale)); if (frpose.boneparent >= 0) bonepose[b] = bonepose[frpose.boneparent] * bonepose[b]; } //done with parents... now we want to invert them for (int b = 0; b < fr.pose.length(); b++) { Matrix3x4 invbind = Matrix3x4(mjoints[b]); //invbind.invert(); bonepose[b] = bonepose[b] * invbind; } for (uint i = mesh.firstvert; i < mesh.numverts; i++, outv++, outn++) { //generate per-vert matrix... Matrix3x4 blend; blend *= 0; if (inweights && inbones) { if (inweights[i*4+0]) blend += bonepose[inbones[i*4+0]] * (inweights[i*4+0]/255.0); if (inweights[i*4+1]) blend += bonepose[inbones[i*4+1]] * (inweights[i*4+1]/255.0); if (inweights[i*4+2]) blend += bonepose[inbones[i*4+2]] * (inweights[i*4+2]/255.0); if (inweights[i*4+3]) blend += bonepose[inbones[i*4+3]] * (inweights[i*4+3]/255.0); } //transform each vert *outv = blend.transform(Vec3(invert[i*3+0], invert[i*3+1], invert[i*3+2])); //bound it to find the model's extents for (uint c = 0; c < 3; c++) { if (min.v[c] > outv->v[c]) min.v[c] = outv->v[c]; if (max.v[c] < outv->v[c]) max.v[c] = outv->v[c]; } *outn = blend.transform3(Vec3(innorm[i*3+0], innorm[i*3+1], innorm[i*3+2])); } vpos.advance(mesh.numverts); vnorm.advance(mesh.numverts); } } offset = min; scale = (max-min)/255; //ignore low order info here stream *f = openfile(filename, "wb"); if(!f) return false; if (md16) f->putlil((uint)(('M'<<0)|('D'<<8)|('1'<<16)|('6'<<24))); else f->putlil((uint)(('I'<<0)|('D'<<8)|('P'<<16)|('O'<<24))); f->putlil((uint)6); //version f->putlil((float)scale[0]); f->putlil((float)scale[1]); f->putlil((float)scale[2]); f->putlil((float)offset[0]); f->putlil((float)offset[1]); f->putlil((float)offset[2]); f->putlil(0.f); //radius f->putlil(0.f); //eyeposx, never used afaik f->putlil(0.f); //eyeposy f->putlil(0.f); //eyeposz f->putlil((uint)numskins); f->putlil((uint)skinwidth); f->putlil((uint)skinheight); f->putlil((uint)mesh.numverts); f->putlil((uint)mesh.numtris); if (!anims.length()) f->putlil((uint)1); //numanims else f->putlil((uint)anims.length()); //numanims f->putlil((uint)0); //synctype f->putlil((uint)modelflags); //flags f->putlil(0.f); //size //skins for (uint i = 0; i < numskins; i++) { f->putlil((uint)0); //ALIAS_SKIN_SINGLE f->write(skindata.getbuf()+i*skinwidth*skinheight, skinwidth*skinheight); } //texcoords for (uint i = mesh.firstvert; i < mesh.firstvert+mesh.numverts; i++) { float s = (tcdata[i*2+0])*skinwidth; float t = (tcdata[i*2+1])*skinheight; s -= 0.5; //glquake has some annoying half-texel offset thing. t -= 0.5; s = bound(0, s, skinwidth); t = bound(0, t, skinheight); f->putlil((uint)(0?32:0)); //onseam. no verts are ever onseam for us, as we don't do that nonsense here. f->putlil((int)s); //mdl texcoords are ints, in texels. which sucks, but what can you do... f->putlil((int)t); } //tris for (uint i = mesh.firsttri; i < mesh.firsttri+mesh.numtris; i++) { f->putlil((uint)1); //faces front. All are effectively front-facing for us. This avoids annoying tc additions. f->putlil((uint)triangles[i].vert[0]); f->putlil((uint)triangles[i].vert[1]); f->putlil((uint)triangles[i].vert[2]); } //animations vector high, low; size_t voffset = 0; if (!anims.length()) { qmdl_vertex_t *th=high.reserve(mesh.numverts),*tl=low.reserve(mesh.numverts); for (uint i = mesh.firstvert; i < mesh.numverts; i++, th++, tl++) { int l; for (uint c = 0; c < 3; c++) { l = (((vpos[voffset][c]-offset[c])*256) / scale[c]); if (l<0) l = 0; if (l > 0xff00) l = 0xff00; //0xffff would exceed the bounds values, so don't use it. th->v[c] = l>>8; tl->v[c] = l&0xff; } tl->normalIndex = th->normalIndex = qmdl_bestnorm(vnorm[voffset]); voffset++; } high.advance(mesh.numverts); low.advance(mesh.numverts); voffset = 0; f->putlil((uint)0); //single-pose type char name[16]="base"; qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}}; for (uint k = 0; k < mesh.numverts; k++) { for (uint c = 0; c < 3; c++) { if (min.v[c] > high[voffset+k].v[c]) min.v[c] = high[voffset+k].v[c]; if (max.v[c] < high[voffset+k].v[c]) max.v[c] = high[voffset+k].v[c]; } } f->put(min); f->put(max); name[countof(name)-1] = 0; for (uint k = 0; k < countof(name); k++) f->put(name[k]); f->write(&high[voffset], sizeof(qmdl_vertex_t)*mesh.numverts); if (md16) f->write(&low[voffset], sizeof(qmdl_vertex_t)*mesh.numverts); voffset += mesh.numverts; } else { loopv(anims) { anim &a = anims[i]; for (uint j = 0; j < a.numframes; j++) { qmdl_vertex_t *th=high.reserve(mesh.numverts),*tl=low.reserve(mesh.numverts); for (uint i = mesh.firstvert; i < mesh.numverts; i++, th++, tl++) { int l; for (uint c = 0; c < 3; c++) { l = (((vpos[voffset][c]-offset[c])*256) / scale[c]); if (l<0) l = 0; if (l > 0xff00) l = 0xff00; //0xffff would exceed the bounds values, so don't use it. th->v[c] = l>>8; tl->v[c] = l&0xff; } tl->normalIndex = th->normalIndex = qmdl_bestnorm(vnorm[voffset]); voffset++; } high.advance(mesh.numverts); low.advance(mesh.numverts); } } voffset = 0; loopv(anims) { anim &a = anims[i]; if (a.numframes == 1) f->putlil((uint)0); //single-pose type else { f->putlil((uint)1); //anim type f->putlil((uint)a.numframes); qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}}; for (uint k = 0; k < mesh.numverts*a.numframes; k++) { for (uint c = 0; c < 3; c++) { if (min.v[c] > high[voffset+k].v[c]) min.v[c] = high[voffset+k].v[c]; if (max.v[c] < high[voffset+k].v[c]) max.v[c] = high[voffset+k].v[c]; } } f->put(min); f->put(max); for (uint j = 0; j < a.numframes; j++) f->putlil(1.0f/a.fps); //intervals. we use the same value for each } for (uint j = 0; j < a.numframes; j++) { char name[16]={0}; qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}}; for (uint k = 0; k < mesh.numverts; k++) { for (uint c = 0; c < 3; c++) { if (min.v[c] > high[voffset+k].v[c]) min.v[c] = high[voffset+k].v[c]; if (max.v[c] < high[voffset+k].v[c]) max.v[c] = high[voffset+k].v[c]; } } f->put(min); f->put(max); strncpy(name, &stringdata[a.name], sizeof(name)); name[countof(name)-1] = 0; for (uint k = 0; k < countof(name); k++) f->put(name[k]); f->write(&high[voffset], sizeof(qmdl_vertex_t)*mesh.numverts); if (md16) f->write(&low[voffset], sizeof(qmdl_vertex_t)*mesh.numverts); voffset += mesh.numverts; } } } delete f; return true; } static bool writeqmdl(const char *filename) { return writemdl(filename, false); } static bool writemd16(const char *filename) { return writemdl(filename, true); } #else static bool writeqmdl(const char *filename) { fatal("(q1)mdl output disabled at compile time"); return false; } static bool writemd16(const char *filename) { fatal("md16 output disabled at compile time"); return false; } #endif static bool writemd3(const char *filename) { fprintf(stderr, "writemd3 is not implemented yet\n"); return false; } static void help(bool exitstatus, bool fullhelp) { fprintf(exitstatus != EXIT_SUCCESS ? stderr : stdout, "-- FTE's Fork of Lee Salzman's iqm exporter --\n" "Usage:\n" "\n" "./iqmtool cmdfile.cmd\n" "./iqmtool [options] output.iqm mesh.iqe anim1.iqe ... animN.iqe\n" "./iqmtool [options] output.iqm mesh.md5mesh anim1.md5anim ... animN.md5anim\n" "./iqmtool [options] output.iqm mesh.smd anim1.smd ... animN.smd\n" "./iqmtool [options] output.iqm mesh.fbx anim1.fbx ... animN.fbx\n" "./iqmtool [options] output.iqm mesh.obj\n" "./iqmtool [options] output.iqm source.gltf\n" "./iqmtool [options] output.iqm --qex sources.md5\n" "\n" "Basic commandline options:\n" " --help Show full help.\n" " -v Verbose\n" " -q Quiet operation\n" " -n Disable output of fte's iqm extensions\n" ); if (fullhelp) fprintf(exitstatus != EXIT_SUCCESS ? stderr : stdout, "\n" "For certain formats, IQE, OBJ, and FBX, it is possible to combine multiple mesh\n" "files of the exact same vertex layout and skeleton by supplying them as\n" "\"mesh1.iqe,mesh2.iqe,mesh3.iqe\", that is, a comma-separated list of the mesh\n" "files (with no spaces) in place of the usual mesh filename.\n" "\n" "Legacy commandline options that affect mesh import for the next file:\n" "\n" " -s N Sets the output scale to N (float).\n" " --meshtrans Z\n" " --meshtrans X,Y,Z Translates a mesh by X,Y,Z (floats).\n" " This does not affect the skeleton.\n" " -j\n" " --forcejoints Forces the exporting of joint information in animation\n" " files without meshes.\n" " --qex Applies a set of fixups to work around the quirks in\n" " the quake rerelease's md5 files.\n" "\n" "Legacy commandline options that affect the following animation file:\n" "\n" " --name A Sets the name of the animation to A.\n" " --fps N Sets the FPS of the animation to N (float).\n" " --loop Sets the loop flag for the animation.\n" " --start N Sets the first frame of the animation to N (integer).\n" " --end N Sets the last frame of the animation to N (integer).\n" " --zup Source model is in quake's orientation.\n" "\n" "You can supply either a mesh file, animation files, or both.\n" "Note that if an input mesh file is supplied, it must come before the animation\n" "files in the file list.\n" "The output IQM file will contain the supplied mesh and any supplied animations.\n" "If no mesh is provided,the IQM file will simply contain the supplied animations\n" "\n" "Command script commands:\n" " hitbox BODYNUM BONENAME x y z x y z\n" " Attaches a hitbox surface around the specified bone\n" " (in base pose). Gamecode knows which part of the model\n" " was hit via the body value.\n" " exec FILENAME Reads additional commands from the specified file.\n" " Handy for shared animations.\n" " modelflags FLAGS Specifies the model's flags, mostly for trail effects.\n" " Known values are rocket, grenade, gib, rotate, tracer1,\n" " zomgib, tracer2, tracer3, or 0xXXXXXXXX\n" " mesh NAME MESHPROPS Overrides properties for a specific source surface.\n" " bone NAME BONEPROPS Overrides properties for a specific bone.\n" " ANIMPROPS Provides default values for following imported files.\n" " import FILENAME [PROPS]\n" " Loads the specified file, with per-file properties.\n" " output FILENAME Specifies the iqm filename to write.\n" " output_mdl FILENAME Specifies the (q1) mdl filename to write.\n" "\n" "Bone Properties:\n" " rename NEWNAME Renames the bone accordingly.\n" " group GROUPID Inherited from parents this allows resorting bones.\n" "\n" "Mesh Properties:\n" " contents a[,b,c] Override surface content values for collisons\n" " Accepted values are empty, solid, lava, slime, water,\n" " fluid, fte_ladder, playerclip, monsterclip, body,\n" " corpse, q2_ladder, fte_sky, q3_nodrop,\n" " or 0xXXXXXXXX for custom values.\n" " surfaceflags a[,b] Specifies explicit surface flags values.\n" " Accepted values are nodraw, or custom 0xXXXXXXXX values.\n" " body BODYNUM Specifies mod-specific body numbers.\n" " geomset GROUP IDX Controls which geomset this surface is part of\n" " lodrange min max Specifies a distance range within which to draw this lod\n" "\n" "Anim Properties:\n" " name NAME Defines the output's name for this animation.\n" " fps RATE Controls the playback rate.\n" " loop Forces the animation(s) to loop.\n" " clamp Forces the animation(s) to not loop.\n" " unpack Extract each pose into its own single-pose 'animation'.\n" " pack Undoes the effect of 'unpack'.\n" " nomesh 1 Skips importing of meshes, getting animations only.\n" " noanim 1 Skips importing of animations, for mesh import only.\n" " materialprefix PRE Prefixes material names with the specified string.\n" " materialsuffix EXT Forces the material's extension as specified.\n" " ignoresurfname 1 Ignores source surface names.\n" " start FRAME The Imported animation starts on this frame...\n" " end FRAME ... and ends on this one.\n" " rotate X Y Z Rotates the input model by the specified angles.\n" " scale FACTOR Scales the input model by some value\n" " origin X Y Z Translates the input model.\n" " event reset Discard previously specified events\n" " event [ANIMIDX:]TIME CODE \"VALUE\"\n" " Inserts a model event into the relevant animation index.\n" ); exit(exitstatus); } struct bitnames { const char *std; const char *name; unsigned int bits; }; //chops up the input string, returning subsections of it, like strtok_r, except with more specific separators. char *mystrtok(char **ctx) { char *ret = NULL; char *p = *ctx; //skip whitespace while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; if (!*p) return NULL; //eof if (*p == '\"') { ret = ++p; while (*p && *p != '\"') p++; if (*p) *p++ = '\0'; *ctx = p; } else { ret = p; //we're screwed if we reach a quote without trailing whitespace, blame the user in that case. while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') p++; if (*p) *p++ = '\0'; *ctx = p; } return ret; } unsigned int parsebits(bitnames *names, char **line) { unsigned int bits = 0; char *comma; for (char *value = mystrtok(line); value; value = comma) { comma = strchr(value, ','); if (comma) *comma++ = 0; char *end; strtoul(value, &end, 0); if (end && !*end) bits |= strtoul(value, NULL, 0); else { size_t i; char *std = value; value = strchr(value, '_'); if (value) *value++ = 0; else { value = std; std += strlen(std); } for (i = 0; names[i].name; i++) { if (!strcasecmp(names[i].std, std)) { if (!strcasecmp(names[i].name, value)) { bits |= names[i].bits; break; } } } if (!names[i].name) { //stuff with no specific standard, mostly for consistency for (i = 0; names[i].name; i++) { if (!strcasecmp(names[i].name, value)) { bits |= names[i].bits; break; } } if (!names[i].name) fatal("Unknown bit name: %s\n", value); } } } return bits; } bool parsebonefield(const char *tok, char **line, boneoverride::prop &spec, bool defaults) { if (!strcasecmp(tok, "rename")) spec.rename = newstring(mystrtok(line)); else if (!strcasecmp(tok, "group")) spec.group = atoi(mystrtok(line)); else return false; return true; } bool parsemeshfield(const char *tok, char **line, meshprop &spec, bool defaults) { if (!strcasecmp(tok, "contents")) { //these should be (mostly) compatible with q2+q3 bitnames contentnames[] = { {"", "empty", 0x00000000}, {"", "solid", 0x00000001}, {"", "lava", 0x00000008}, {"", "slime", 0x00000010}, {"", "water", 0x00000020}, {"", "fluid", 0x00000038}, {"fte", "ladder", 0x00004000}, {"", "playerclip", 0x00010000}, {"", "monsterclip", 0x00010000}, {"", "body", 0x02000000}, {"", "corpse", 0x04000000}, {"q2", "ladder", 0x20000000}, {"fte", "sky", 0x80000000},{"q3", "nodrop", 0x80000000}, {NULL} }; spec.contents = parsebits(contentnames, line); } else if (!strcasecmp(tok, "surfaceflags")) { bitnames surfaceflagnames[] = { {"fte", "nodraw", 0x00000080},{"q3", "nodraw", 0x00000080}, {NULL} }; spec.surfaceflags = parsebits(surfaceflagnames, line); } else if (!strcasecmp(tok, "body")) spec.body = strtoul(mystrtok(line), NULL, 0); else if (!strcasecmp(tok, "geomset")) { spec.geomset = strtoul(mystrtok(line), NULL, 0); spec.geomid = strtoul(mystrtok(line), NULL, 0); } else if (!strcasecmp(tok, "lodrange")) { spec.mindist = atof(mystrtok(line)); spec.maxdist = atof(mystrtok(line)); } else return false; return true; } bool parseanimfield(const char *tok, char **line, filespec &spec, bool defaults) { if (!strcasecmp(tok, "name") && !defaults) spec.name = newstring(mystrtok(line)); else if (!strcasecmp(tok, "fps")) spec.fps = atof(mystrtok(line)); else if (!strcasecmp(tok, "loop")) spec.flags |= IQM_LOOP; else if (!strcasecmp(tok, "clamp")) spec.flags &= ~IQM_LOOP; else if (!strcasecmp(tok, "unpack")) spec.flags |= IQM_UNPACK; else if (!strcasecmp(tok, "pack")) spec.flags &= ~IQM_UNPACK; else if (!strcasecmp(tok, "nomesh")) spec.nomesh = !!strtoul(mystrtok(line), NULL, 0); else if (!strcasecmp(tok, "noanim")) spec.noanim = !!strtoul(mystrtok(line), NULL, 0); else if (!strcasecmp(tok, "materialprefix")) spec.materialprefix = newstring(mystrtok(line)); else if (!strcasecmp(tok, "materialsuffix")) spec.materialsuffix = newstring(mystrtok(line)); else if (!strcasecmp(tok, "ignoresurfname")) spec.ignoresurfname = atoi(mystrtok(line)); else if(!strcasecmp(tok, "start")) spec.startframe = max(atoi(mystrtok(line)), 0); else if(!strcasecmp(tok, "end")) spec.endframe = atoi(mystrtok(line)); else if (!strcasecmp(tok, "rotate")) { Vec3 ang; ang.x = atof(mystrtok(line))*-M_PI/180; ang.z = atof(mystrtok(line))*-M_PI/180; ang.y = atof(mystrtok(line))*-M_PI/180; spec.rotate = Quat::fromangles(ang); } else if (!strcasecmp(tok, "scale")) spec.scale = atof(mystrtok(line)); else if (!strcasecmp(tok, "origin")) { spec.translate.x = atof(mystrtok(line)); spec.translate.y = atof(mystrtok(line)); spec.translate.z = atof(mystrtok(line)); } else if (!strcasecmp(tok, "event")) { const char *poseidx = mystrtok(line); char *dot; if (!strcmp(poseidx, "reset")) { spec.events.setsize(0); return true; } event_fte &ev = spec.events.add(); ev.anim = strtod(poseidx, &dot); if (*dot == ':') ev.timestamp = strtoul(dot+1, NULL, 0); else { ev.timestamp = strtod(poseidx, &dot); ev.anim = ~0u; //fix up according to poses... } ev.evcode = atoi(mystrtok(line)); ev.evdata_str = newstring(mystrtok(line)); } else if (parsemeshfield(tok, line, spec.meshprops, defaults)) ; else return false; return true; } static struct { const char *extname; bool (*write)(const char *filename); const char *cmdname; const char *altcmdname; } outputtypes[] = { {".vvm", writeiqm, "output_vvm"}, {".iqm", writeiqm, "output_iqm"}, {".mdl", writeqmdl, "output_qmdl"}, {".md16", writemd16, "output_md16"}, {".md3", writemd3, "output_md3"}, }; static bitnames modelflagnames[] = { {"q1", "rocket", 1u<<0}, {"q1", "grenade", 1u<<1}, {"q1", "gib", 1u<<2}, {"q1", "rotate", 1u<<3}, {"q1", "tracer1", 1u<<4}, {"q1", "zomgib", 1u<<5}, {"q1", "tracer2", 1u<<6}, {"q1", "tracer3", 1u<<7}, {"q1", "holey", 1u<<14}, //common extension {"h2", "spidergib", 1u<<0}, //conflicts with q1. {"h2", "grenade", 1u<<1}, {"h2", "gib", 1u<<2}, {"h2", "rotate", 1u<<3}, {"h2", "tracer1", 1u<<4}, {"h2", "zomgib", 1u<<5}, {"h2", "tracer2", 1u<<6}, {"h2", "tracer3", 1u<<7}, {"h2", "fireball", 1u<<8}, {"h2", "ice", 1u<<9}, {"h2", "mipmap", 1u<<10}, {"h2", "spit", 1u<<11}, {"h2", "transparent", 1u<<12}, {"h2", "spell", 1u<<13}, {"h2", "holey", 1u<<14}, {"h2", "specialtrans", 1u<<15}, {"h2", "faceview", 1u<<16}, {"h2", "vorpmissile", 1u<<17}, {"h2", "setstaff", 1u<<18}, {"h2", "magicmissle", 1u<<19}, {"h2", "boneshard", 1u<<20}, {"h2", "scarab", 1u<<21}, {"h2", "acidball", 1u<<22}, {"h2", "bloodshot", 1u<<23}, {NULL} }; void parsecommands(char *filename, const char *outfiles[countof(outputtypes)], vector &infiles, vector &hitboxes) { filespec defaultspec; defaultspec.reset(); if (!quiet) conoutf("execing %s", filename); stream *f = openfile(filename, "rt"); if(!f) { fatal("Couldn't open command-file \"%s\"\n", filename); return; } char buf[2048]; while(f->getline(buf, sizeof(buf))) { const char *tok; char *line = buf; tok = mystrtok(&line); if (tok && *tok == '$') tok++; if (!tok) continue; else if (*tok == '#' || !strncasecmp(tok, "//", 2)) { //comments while (mystrtok(&line)) ; } // else if (!strcasecmp(tok, "outputdir")) // (void)mystrtok(&line); else if (!strcasecmp(tok, "hitbox") || !strcasecmp(tok, "hbox")) { hitbox &hb = hitboxes.add(); hb.body = strtoul(mystrtok(&line), NULL, 0); hb.bone = newstring(mystrtok(&line)); for (int i = 0; i < 3; i++) hb.mins[i] = atof(mystrtok(&line)); for (int i = 0; i < 3; i++) hb.maxs[i] = atof(mystrtok(&line)); } else if (!strcasecmp(tok, "exec")) parsecommands(mystrtok(&line), outfiles, infiles, hitboxes); else if (!strcasecmp(tok, "modelflags")) modelflags = parsebits(modelflagnames, &line); else if (!strcasecmp(tok, "static")) stripbones = true; else if (parseanimfield(tok, &line, defaultspec, true)) ; else if (!strcasecmp(tok, "mesh")) { meshoverride &mo = meshoverrides.add(); mo.name = newstring(mystrtok(&line)); mo.props = defaultspec.meshprops; while(( tok = mystrtok(&line))) { //fixme: should probably separate this out. if (parsemeshfield(tok, &line, mo.props, false)) ; else { printf("unknown mesh token \"%s\"\n", tok); break; } } } else if (!strcasecmp(tok, "bone")) { boneoverride &mo = boneoverrides.add(); mo.name = newstring(mystrtok(&line)); mo.props = boneoverride::prop(); while(( tok = mystrtok(&line))) { //fixme: should probably separate this out. if (parsebonefield(tok, &line, mo.props, false)) ; else { printf("unknown mesh token \"%s\"\n", tok); break; } } } else if (!strcasecmp(tok, "import") || !strcasecmp(tok, "model") || !strcasecmp(tok, "scene") || !strcasecmp(tok, "animation")) { filespec inspec = defaultspec; //first token is always the filename(s) inspec.file = newstring(mystrtok(&line)); while(( tok = mystrtok(&line))) { if (parseanimfield(tok, &line, inspec, false)) ; else { printf("unknown scene token \"%s\"\n", tok); break; } } infiles.add(inspec); } else { size_t n, j; if (!strcasecmp(tok, "output")) tok = "output_iqm"; for (n = 0; n < countof(outputtypes); n++) { if (!strcasecmp(tok, outputtypes[n].cmdname)) { outfiles[n] = newstring(mystrtok(&line)); for (j = 0; j < countof(outputtypes); j++) { if (n!=j && outfiles[j] && !strcasecmp(outfiles[n], outfiles[j])) { printf("cancelling %s\n", outputtypes[j].cmdname); outfiles[j] = NULL; break; } } tok = ""; break; } } if (*tok) { printf("unsupported command \"%s\"\n", tok); continue; } } if ((tok=mystrtok(&line))) if (*tok) printf("unexpected junk at end-of-line \"%s\" \"%s\"\n", buf, tok); } delete f; } int main(int argc, char **argv) { if(argc <= 1) help(EXIT_FAILURE, false); vector infiles; vector hitboxes; filespec inspec; const char *outfiles[countof(outputtypes)] = {}; for(int i = 1; i < argc; i++) { if(argv[i][0] == '-') { if(argv[i][1] == '-') { if(!strcasecmp(&argv[i][2], "set")) { #ifdef FTEPLUGIN if(i + 2 < argc) fte::Cvar_Create(argv[i+1], argv[i+2], 0, NULL, "cmdline"); #endif i+=2; } else if(!strcasecmp(&argv[i][2], "cmd")) { if(i + 1 < argc) parsecommands(argv[++i], outfiles, infiles, hitboxes); } else if(!strcasecmp(&argv[i][2], "noext")) noext = true; else if(!strcasecmp(&argv[i][2], "fps")) { if(i + 1 < argc) inspec.fps = atof(argv[++i]); } else if(!strcasecmp(&argv[i][2], "name")) { if(i + 1 < argc) inspec.name = argv[++i]; } else if(!strcasecmp(&argv[i][2], "loop")) { inspec.flags |= IQM_LOOP; } else if(!strcasecmp(&argv[i][2], "unpack")) { inspec.flags |= IQM_UNPACK; } else if(!strcasecmp(&argv[i][2], "start")) { if(i + 1 < argc) inspec.startframe = max(atoi(argv[++i]), 0); } else if(!strcasecmp(&argv[i][2], "end")) { if(i + 1 < argc) inspec.endframe = atoi(argv[++i]); } else if(!strcasecmp(&argv[i][2], "scale")) { if(i + 1 < argc) inspec.scale = clamp(atof(argv[++i]), 1e-8, 1e8); } else if(!strcasecmp(&argv[i][2], "rotate")) { if(i + 3 < argc) inspec.rotate = Quat::fromdegrees(-Vec3(atof(argv[i+1]),atof(argv[i+3]),atof(argv[i+2]))); i+=3;} else if(!strcasecmp(&argv[i][2], "yup")) inspec.rotate = Quat::fromangles(Vec3(0,-M_PI,0)); else if(!strcasecmp(&argv[i][2], "zup")) inspec.rotate = Quat::fromdegrees(Vec3(0,-90,-90)); else if(!strcasecmp(&argv[i][2], "ignoresurfname")) inspec.ignoresurfname = true; else if(!strcasecmp(&argv[i][2], "help")) help(EXIT_SUCCESS,true); else if(!strcasecmp(&argv[i][2], "forcejoints")) forcejoints = true; else if(!strcasecmp(&argv[i][2], "materialprefix")) { if(i + 1 < argc) inspec.materialprefix = argv[++i]; } else if(!strcasecmp(&argv[i][2], "materialsuffix")) { if(i + 1 < argc) inspec.materialsuffix = argv[++i]; } else if(!strcasecmp(&argv[i][2], "qex")) inspec.materialprefix = "progs/", inspec.materialsuffix = "_00_00.lmp", inspec.flags |= IQM_UNPACK; else if(!strcasecmp(&argv[i][2], "modelflags")) { if(i + 1 < argc) { modelflags |= parsebits(modelflagnames, &argv[++i]); }} else if(!strcasecmp(&argv[i][2], "static")) stripbones = true; else if(!strcasecmp(&argv[i][2], "meshtrans")) { if(i + 1 < argc) switch(sscanf(argv[++i], "%lf , %lf , %lf", &gmeshtrans.x, &gmeshtrans.y, &gmeshtrans.z)) { case 1: gmeshtrans = Vec3(0, 0, gmeshtrans.x); break; } } } else switch(argv[i][1]) { case 'h': help(EXIT_SUCCESS,true); break; case 's': if(i + 1 < argc) gscale = clamp(atof(argv[++i]), 1e-8, 1e8); break; case 'j': forcejoints = true; break; case 'v': verbose = true; break; case 'q': quiet = true; break; case 'n': noext = true; break; } } else { const char *type = strrchr(argv[i], '.'); if (type && (!strcasecmp(type, ".cmd")||!strcasecmp(type, ".cfg")||!strcasecmp(type, ".txt")||!strcasecmp(type, ".qc"))) //.qc to humour halflife fanboys parsecommands(argv[i], outfiles, infiles, hitboxes); else { size_t j; for (j = 0; j < countof(outfiles); j++) if (outfiles[j]) break; if(j == countof(outfiles)) { for (j = countof(outfiles)-1; j > 0; j--) if (type && !strcasecmp(type, outputtypes[j].extname)) break; outfiles[j] = argv[i]; //first arg is the output name, if its not an export script thingie. } else { infiles.add(inspec).file = argv[i]; inspec.reset(); } } } } size_t n; for (n = 0; n < countof(outputtypes) && !outfiles[n]; n++); if(n == countof(outfiles)) fatal("no output file specified"); if(infiles.empty()) { if (outfiles[0]) { infiles.add(inspec).file = outfiles[0]; inspec.reset(); outfiles[0] = NULL; } else fatal("no input files specified"); } if(gscale != 1) printf("scale: %f\n", escale); if(gmeshtrans != Vec3(0, 0, 0)) printf("mesh translate: %f, %f, %f\n", gmeshtrans.x, gmeshtrans.y, gmeshtrans.z); loopv(infiles) { const filespec &inspec = infiles[i]; const char *infile = inspec.file, *type = strrchr(infile, '.'); if (verbose) conoutf("importing %s", infile); if(!type) fatal("no file type: %s", infile); else if(!strcasecmp(type, ".md5mesh")) { if(!loadmd5mesh(infile, inspec)) fatal("failed reading: %s", infile); } else if(!strcasecmp(type, ".md5anim")) { if(!loadmd5anim(infile, inspec)) fatal("failed reading: %s", infile); } else if(!strcasecmp(type, ".md5")) { char tmp[MAXSTRLEN]; formatstring(tmp, "%smesh", infile); if(!loadmd5mesh(tmp, inspec)) fatal("failed reading: %s", tmp); formatstring(tmp, "%sanim", infile); if(!loadmd5anim(tmp, inspec)) fatal("failed reading: %s", tmp); } else if(!strcasecmp(type, ".iqe")) { if(!loadiqe(infile, inspec)) fatal("failed reading: %s", infile); } else if(!strcasecmp(type, ".smd")) { if(!loadsmd(infile, inspec)) fatal("failed reading: %s", infile); } else if(!strcasecmp(type, ".fbx")) { if(!loadfbx(infile, inspec)) fatal("failed reading: %s", infile); } else if(!strcasecmp(type, ".obj")) { if(!loadobj(infile, inspec)) fatal("failed reading: %s", infile); } else if(!strcasecmp(type, ".glb")) { #ifdef FTEPLUGIN if(!fte::loadglb(infile, inspec)) fatal("failed reading: %s", infile); #else fatal("GLTF/GLB support was disabled at compile time"); #endif } else if(!strcasecmp(type, ".gltf")) { #ifdef FTEPLUGIN if(!fte::loadgltf(infile, inspec)) fatal("failed reading: %s", infile); #else fatal("GLTF/GLB support was disabled at compile time"); #endif } else fatal("unknown file type: %s", type); } genhitboxes(hitboxes); loopv(boneoverrides) if (!boneoverrides[i].used) conoutf("warning: bone \"%s\" overriden, but not present", boneoverrides[i].name); if (noext && meshoverrides.length()) conoutf("warning: mesh overrides used, but iqm extensions disabled"); else loopv(meshoverrides) conoutf("warning: mesh \"%s\" overriden, but not present", meshoverrides[i].name); if (stripbones) { if (!quiet) conoutf("static bones"); joints.setsize(0); poses.setsize(0); frames.setsize(0); anims.setsize(0); bounds.setsize(0); } calcanimdata(); if (!quiet) { conoutf("bone list:"); printbones(); // printbonelist(); } if (!quiet) conoutf(""); for (size_t n = 0; n < countof(outputtypes); n++) { if (outfiles[n] != NULL) { if(outputtypes[n].write(outfiles[n])) { if (!quiet) conoutf("exported: %s", outfiles[n]); } else fatal("failed writing: %s", outfiles[n]); } } return EXIT_SUCCESS; } ================================================ FILE: iqm/iqm.h ================================================ #ifndef __IQM_H__ #define __IQM_H__ #define IQM_MAGIC "INTERQUAKEMODEL" #define IQM_VERSION 2 struct iqmheader { char magic[16]; unsigned int version; unsigned int filesize; unsigned int flags; unsigned int num_text, ofs_text; unsigned int num_meshes, ofs_meshes; unsigned int num_vertexarrays, num_vertexes, ofs_vertexarrays; unsigned int num_triangles, ofs_triangles, ofs_adjacency; unsigned int num_joints, ofs_joints; unsigned int num_poses, ofs_poses; unsigned int num_anims, ofs_anims; unsigned int num_frames, num_framechannels, ofs_frames, ofs_bounds; unsigned int num_comment, ofs_comment; unsigned int num_extensions, ofs_extensions; }; struct iqmmesh { unsigned int name; unsigned int material; unsigned int first_vertex, num_vertexes; unsigned int first_triangle, num_triangles; }; enum { IQM_POSITION = 0, IQM_TEXCOORD = 1, IQM_NORMAL = 2, IQM_TANGENT = 3, IQM_BLENDINDEXES = 4, IQM_BLENDWEIGHTS = 5, IQM_COLOR = 6, IQM_CUSTOM = 0x10 }; enum { IQM_BYTE = 0, IQM_UBYTE = 1, IQM_SHORT = 2, IQM_USHORT = 3, IQM_INT = 4, IQM_UINT = 5, IQM_HALF = 6, IQM_FLOAT = 7, IQM_DOUBLE = 8 }; struct iqmtriangle { unsigned int vertex[3]; }; struct iqmadjacency { unsigned int triangle[3]; }; struct iqmjointv1 { unsigned int name; int parent; float translate[3], rotate[3], scale[3]; }; struct iqmjoint { unsigned int name; int parent; float translate[3], rotate[4], scale[3]; }; struct iqmposev1 { int parent; unsigned int mask; float channeloffset[9]; float channelscale[9]; }; struct iqmpose { int parent; unsigned int mask; float channeloffset[10]; float channelscale[10]; }; struct iqmanim { unsigned int name; unsigned int first_frame, num_frames; float framerate; unsigned int flags; }; enum { IQM_LOOP = 1<<0 }; struct iqmvertexarray { unsigned int type; unsigned int flags; unsigned int format; unsigned int size; unsigned int offset; }; struct iqmbounds { float bbmin[3], bbmax[3]; float xyradius, radius; }; struct iqmextension { unsigned int name; unsigned int num_data, ofs_data; unsigned int ofs_extensions; // pointer to next extension }; struct iqmext_fte_mesh { unsigned int contents; //default CONTENTS_BODY unsigned int surfaceflags; //propagates to trace_surfaceflags unsigned int body; //the part of the body that this mesh is meant to be from unsigned int geomset; unsigned int geomid; float mindist; float maxdist; }; struct iqmext_fte_events { unsigned int anim; float timestamp; unsigned int evcode; unsigned int evdata_str; }; //skin lump is made of 3 parts struct iqmext_fte_skin { unsigned int nummeshskins; unsigned int numskinframes; //unsigned int numskins[nummeshes]; //iqmext_fte_skin_skinframe[numskinframes]; //iqmext_fte_skin_meshskin mesh0[numskins[0]]; //iqmext_fte_skin_meshskin mesh1[numskins[1]]; etc }; struct iqmext_fte_skin_skinframe { //as many as needed unsigned int material_idx; unsigned int shadertext_idx; }; struct iqmext_fte_skin_meshskin { unsigned int firstframe; //index into skinframes unsigned int countframes; //skinframes float interval; }; #endif ================================================ FILE: iqm/util.h ================================================ #include #include #include #include #include #include #include #include #include #include "iqm.h" #define ASSERT(c) if(c) {} #ifdef NULL #undef NULL #endif #define NULL 0 #ifdef _WIN32 #ifndef M_PI #define M_PI 3.1415926535897932384626433832795 #endif #ifndef strcasecmp #define strcasecmp _stricmp #endif #ifndef strncasecmp #define strncasecmp _strnicmp #endif #endif typedef unsigned char uchar; typedef unsigned short ushort; typedef unsigned int uint; typedef signed long long int llong; typedef unsigned long long int ullong; /*inline void *operator new(size_t size) { void *p = malloc(size); if(!p) abort(); return p; } inline void *operator new[](size_t size) { void *p = malloc(size); if(!p) abort(); return p; } inline void operator delete(void *p) { if(p) free(p); } inline void operator delete[](void *p) { if(p) free(p); } inline void operator delete(void *p, size_t sz) { if(p) free(p); } inline void operator delete[](void *p, size_t sz) { if(p) free(p); } */ inline void *operator new(size_t, void *p) { return p; } inline void *operator new[](size_t, void *p) { return p; } inline void operator delete(void *, void *) {} inline void operator delete[](void *, void *) {} #ifdef swap #undef swap #endif template static inline void swap(T &a, T &b) { T t = a; a = b; b = t; } #ifdef max #undef max #endif #ifdef min #undef min #endif template static inline T max(T a, T b) { return a > b ? a : b; } template static inline T min(T a, T b) { return a < b ? a : b; } #ifndef countof #define countof(n) (sizeof(n)/sizeof(n[0])) #endif #define clamp(a,b,c) (max(b, min(a, c))) #define loop(v,m) for(int v = 0; v inline void formatstring(char (&d)[N], const char *fmt, ...) { va_list v; va_start(v, fmt); vformatstring(d, fmt, v, int(N)); va_end(v); } #define defformatstring(d,...) string d; formatstring(d, __VA_ARGS__) #define defvformatstring(d,last,fmt) string d; { va_list ap; va_start(ap, last); vformatstring(d, fmt, ap); va_end(ap); } inline char *newstring(size_t l) { return new char[l+1]; } inline char *newstring(const char *s, size_t l) { return copystring(newstring(l), s, l+1); } inline char *newstring(const char *s) { size_t l = strlen(s); char *d = newstring(l); memcpy(d, s, l+1); return d; } #define loopv(v) for(int i = 0; i<(v).length(); i++) #define loopvj(v) for(int j = 0; j<(v).length(); j++) #define loopvk(v) for(int k = 0; k<(v).length(); k++) #define loopvrev(v) for(int i = (v).length()-1; i>=0; i--) template struct vector { static const int MINSIZE = 8; T *buf; int alen, ulen; vector() : buf(NULL), alen(0), ulen(0) { } vector(const vector &v) : buf(NULL), alen(0), ulen(0) { *this = v; } ~vector() { setsize(0); if(buf) delete[] (uchar *)buf; } vector &operator=(const vector &v) { setsize(0); if(v.length() > alen) growbuf(v.length()); loopv(v) add(v[i]); return *this; } T &add(const T &x) { if(ulen==alen) growbuf(ulen+1); new (&buf[ulen]) T(x); return buf[ulen++]; } T &add() { if(ulen==alen) growbuf(ulen+1); new (&buf[ulen]) T; return buf[ulen++]; } T &dup() { if(ulen==alen) growbuf(ulen+1); new (&buf[ulen]) T(buf[ulen-1]); return buf[ulen++]; } bool inrange(uint i) const { return i=0 && i=0 && i= 0 && i &v) { ::swap(buf, v.buf); ::swap(ulen, v.ulen); ::swap(alen, v.alen); } T *getbuf() { return buf; } const T *getbuf() const { return buf; } bool inbuf(const T *e) const { return e >= buf && e < &buf[ulen]; } void growbuf(int sz) { int olen = alen; if(!alen) alen = max(MINSIZE, sz); else while(alen < sz) alen *= 2; if(alen <= olen) return; uchar *newbuf = new uchar[alen*sizeof(T)]; if(olen > 0) { memcpy(newbuf, buf, olen*sizeof(T)); delete[] (uchar *)buf; } buf = (T *)newbuf; } T *reserve(int sz) { if(ulen+sz > alen) growbuf(ulen+sz); return &buf[ulen]; } void advance(int sz) { ulen += sz; } void put(const T *v, int n) { memcpy(reserve(n), v, n*sizeof(T)); advance(n); } }; static inline uint hthash(const char *key) { uint h = 5381; for(int i = 0, k; (k = key[i]); i++) h = ((h<<5)+h)^k; // bernstein k=33 xor return h; } static inline bool htcmp(const char *x, const char *y) { return !strcmp(x, y); } static inline uint hthash(int key) { return key; } static inline bool htcmp(int x, int y) { return x==y; } static inline bool htcmp(double x, double y) { return x == y; } static inline uint hthash(double k) { union { double f; uint h[sizeof(double)/sizeof(uint)]; } conv; conv.f = k; uint hash = conv.h[0]; for(size_t i = 1; i < sizeof(conv.h)/sizeof(uint); i++) hash ^= conv.h[i]; return hash; } template struct hashtable { typedef K key; typedef const K const_key; typedef T value; typedef const T const_value; enum { CHUNKSIZE = 64 }; struct chain { T data; K key; chain *next; }; struct chainchunk { chain chains[CHUNKSIZE]; chainchunk *next; }; int size; int numelems; chain **table; chainchunk *chunks; chain *unused; hashtable(int size = 1<<10) : size(size) { numelems = 0; chunks = NULL; unused = NULL; table = new chain *[size]; loopi(size) table[i] = NULL; } ~hashtable() { if(table) delete[] table; deletechunks(); } chain *insert(const K &key, uint h) { if(!unused) { chainchunk *chunk = new chainchunk; chunk->next = chunks; chunks = chunk; loopi(CHUNKSIZE-1) chunk->chains[i].next = &chunk->chains[i+1]; chunk->chains[CHUNKSIZE-1].next = unused; unused = chunk->chains; } chain *c = unused; unused = unused->next; c->key = key; c->next = table[h]; table[h] = c; numelems++; return c; } #define HTFIND(success, fail) \ uint h = hthash(key)&(size-1); \ for(chain *c = table[h]; c; c = c->next) \ { \ if(htcmp(key, c->key)) return (success); \ } \ return (fail); template T *access(const L &key) { HTFIND(&c->data, NULL); } template T &access(const L &key, const T &data) { HTFIND(c->data, insert(key, h)->data = data); } template const T &find(const L &key, const T ¬found) { HTFIND(c->data, notfound); } template T &operator[](const L &key) { HTFIND(c->data, insert(key, h)->data); } #undef HTFIND template bool remove(const L &key) { uint h = hthash(key)&(size-1); for(chain **p = &table[h], *c = table[h]; c; p = &c->next, c = c->next) { if(htcmp(key, c->key)) { *p = c->next; c->data.~T(); c->key.~K(); new (&c->data) T; new (&c->key) K; c->next = unused; unused = c; numelems--; return true; } } return false; } void deletechunks() { for(chainchunk *nextchunk; chunks; chunks = nextchunk) { nextchunk = chunks->next; delete chunks; } } void clear() { if(!numelems) return; loopi(size) table[i] = NULL; numelems = 0; unused = NULL; deletechunks(); } }; #define enumerate(ht,k,e,t,f,b) loopi((ht).size) for(hashtable::chain *enumc = (ht).table[i]; enumc;) { hashtable::const_key &e = enumc->key; t &f = enumc->data; enumc = enumc->next; b; } template struct unionfind { struct ufval { int rank, next; T val; ufval(const T &val) : rank(0), next(-1), val(val) {} }; vector ufvals; void clear() { ufvals.setsize(0); } const T &find(int k, const T &noval, const T &initval) { if(k>=ufvals.length()) return initval; while(ufvals[k].next>=0) k = ufvals[k].next; if(ufvals[k].val == noval) ufvals[k].val = initval; return ufvals[k].val; } int compressfind(int k) { if(ufvals[k].next<0) return k; return ufvals[k].next = compressfind(ufvals[k].next); } void unite (int x, int y, const T &noval) { while(ufvals.length() <= max(x, y)) ufvals.add(ufval(noval)); x = compressfind(x); y = compressfind(y); if(x==y) return; ufval &xval = ufvals[x], &yval = ufvals[y]; if(xval.rank < yval.rank) xval.next = y; else { yval.next = x; if(xval.rank==yval.rank) yval.rank++; } } }; template struct listnode { T *prev, *next; }; template struct list { typedef listnode node; int size; listnode nodes; list() { clear(); } bool empty() const { return nodes.prev == nodes.next; } bool notempty() const { return nodes.prev != nodes.next; } T *first() const { return nodes.next; } T *last() const { return nodes.prev; } T *end() const { return (T *)&nodes; } void clear() { size = 0; nodes.prev = nodes.next = (T *)&nodes; } T *remove(T *node) { size--; node->prev->next = node->next; node->next->prev = node->prev; return node; } T *insertafter(T *node, T *pos) { size++; node->next = pos->next; node->next->prev = node; node->prev = pos; pos->next = node; return node; } T *insertbefore(T *node, T *pos) { size++; node->prev = pos->prev; node->prev->next = node; node->next = pos; pos->prev = node; return node; } T *insertfirst(T *node) { return insertafter(node, end()); } T *insertlast(T *node) { return insertbefore(node, end()); } T *removefirst() { return remove(first()); } T *removelast() { return remove(last()); } }; static inline bool islittleendian() { union { int i; uchar b[sizeof(int)]; } conv; conv.i = 1; return conv.b[0] != 0; } inline ushort endianswap16(ushort n) { return (n<<8) | (n>>8); } inline uint endianswap32(uint n) { return (n<<24) | (n>>24) | ((n>>8)&0xFF00) | ((n<<8)&0xFF0000); } inline ullong endianswap64(ullong n) { return endianswap32(uint(n >> 32)) | ((ullong)endianswap32(uint(n)) << 32); } template inline T endianswap(T n) { union { T t; uint i; } conv; conv.t = n; conv.i = endianswap32(conv.i); return conv.t; } template<> inline uchar endianswap(uchar n) { return n; } template<> inline char endianswap(char n) { return n; } template<> inline ushort endianswap(ushort n) { return endianswap16(n); } template<> inline short endianswap(short n) { return endianswap16(n); } template<> inline uint endianswap(uint n) { return endianswap32(n); } template<> inline int endianswap(int n) { return endianswap32(n); } template<> inline ullong endianswap(ullong n) { return endianswap64(n); } template<> inline llong endianswap(llong n) { return endianswap64(n); } template<> inline double endianswap(double n) { union { double t; uint i; } conv; conv.t = n; conv.i = endianswap64(conv.i); return conv.t; } template inline void endianswap(T *buf, int len) { for(T *end = &buf[len]; buf < end; buf++) *buf = endianswap(*buf); } template inline T endiansame(T n) { return n; } template inline void endiansame(T *buf, int len) {} template inline T lilswap(T n) { return islittleendian() ? n : endianswap(n); } template inline void lilswap(T *buf, int len) { if(!islittleendian()) endianswap(buf, len); } template inline T bigswap(T n) { return islittleendian() ? endianswap(n) : n; } template inline void bigswap(T *buf, int len) { if(islittleendian()) endianswap(buf, len); } /* workaround for some C platforms that have these two functions as macros - not used anywhere */ #ifdef getchar #undef getchar #endif #ifdef putchar #undef putchar #endif struct stream { virtual ~stream() {} virtual void close() = 0; virtual bool end() = 0; virtual long tell() { return -1; } virtual bool seek(long offset, int whence = SEEK_SET) { return false; } virtual long size(); virtual size_t read(void *buf, size_t len) { return 0; } virtual size_t write(const void *buf, size_t len) { return 0; } virtual int getchar() { uchar c; return read(&c, 1) == 1 ? c : -1; } virtual bool putchar(int n) { uchar c = n; return write(&c, 1) == 1; } virtual bool getline(char *str, size_t len); virtual bool putstring(const char *str) { size_t len = strlen(str); return write(str, len) == len; } virtual bool putline(const char *str) { return putstring(str) && putchar('\n'); } virtual int printf(const char *fmt, ...) { return -1; } template bool put(T n) { return write(&n, sizeof(n)) == sizeof(n); } template bool putlil(T n) { return put(lilswap(n)); } template bool putbig(T n) { return put(bigswap(n)); } template T get() { T n; return read(&n, sizeof(n)) == sizeof(n) ? n : 0; } template T getlil() { return lilswap(get()); } template T getbig() { return bigswap(get()); } }; long stream::size() { long pos = tell(), endpos; if(pos < 0 || !seek(0, SEEK_END)) return -1; endpos = tell(); return pos == endpos || seek(pos, SEEK_SET) ? endpos : -1; } bool stream::getline(char *str, size_t len) { loopi(len-1) { if(read(&str[i], 1) != 1) { str[i] = '\0'; return i > 0; } else if(str[i] == '\n') { str[i+1] = '\0'; return true; } } if(len > 0) str[len-1] = '\0'; return true; } struct filestream : stream { FILE *file; filestream() : file(NULL) {} ~filestream() { close(); } bool open(const char *name, const char *mode) { if(file) return false; file = fopen(name, mode); return file!=NULL; } void close() { if(file) { fclose(file); file = NULL; } } bool end() { return feof(file)!=0; } long tell() { return ftell(file); } bool seek(long offset, int whence) { return fseek(file, offset, whence) >= 0; } size_t read(void *buf, size_t len) { return fread(buf, 1, len, file); } size_t write(const void *buf, size_t len) { return fwrite(buf, 1, len, file); } int getchar() { return fgetc(file); } bool putchar(int c) { return fputc(c, file)!=EOF; } bool getline(char *str, int len) { return fgets(str, len, file)!=NULL; } bool putstring(const char *str) { return fputs(str, file)!=EOF; } int printf(const char *fmt, ...) { va_list v; va_start(v, fmt); int result = vfprintf(file, fmt, v); va_end(v); return result; } }; char *path(char *s) { for(char *curpart = s;;) { char *endpart = strchr(curpart, '&'); if(endpart) *endpart = '\0'; if(curpart[0]=='<') { char *file = strrchr(curpart, '>'); if(!file) return s; curpart = file+1; } for(char *t = curpart; (t = strpbrk(t, "/\\")); *t++ = PATHDIV); for(char *prevdir = NULL, *curdir = s;;) { prevdir = curdir[0]==PATHDIV ? curdir+1 : curdir; curdir = strchr(prevdir, PATHDIV); if(!curdir) break; if(prevdir+1==curdir && prevdir[0]=='.') { memmove(prevdir, curdir+1, strlen(curdir+1)+1); curdir = prevdir; } else if(curdir[1]=='.' && curdir[2]=='.' && curdir[3]==PATHDIV) { if(prevdir+2==curdir && prevdir[0]=='.' && prevdir[1]=='.') continue; memmove(prevdir, curdir+4, strlen(curdir+4)+1); curdir = prevdir; } } if(endpart) { *endpart = '&'; curpart = endpart+1; } else break; } return s; } char *path(const char *s, bool copy) { static string tmp; copystring(tmp, s); path(tmp); return tmp; } const char *parentdir(const char *directory) { const char *p = directory + strlen(directory); while(p > directory && *p != '/' && *p != '\\') p--; static string parent; size_t len = p-directory+1; copystring(parent, directory, len); return parent; } stream *openfile(const char *filename, const char *mode) { filestream *file = new filestream; if(!file->open(path(filename, true), mode)) { delete file; return NULL; } return file; } struct Vec4; struct Vec3 { union { struct { double x, y, z; }; double v[3]; uint h[3*sizeof(double)/sizeof(uint)]; }; Vec3() {} Vec3(double x, double y, double z) : x(x), y(y), z(z) {} explicit Vec3(const double *v) : x(v[0]), y(v[1]), z(v[2]) {} explicit Vec3(const float *v) : x(v[0]), y(v[1]), z(v[2]) {} explicit Vec3(const Vec4 &v); double &operator[](int i) { return v[i]; } double operator[](int i) const { return v[i]; } bool operator==(const Vec3 &o) const { return x == o.x && y == o.y && z == o.z; } bool operator!=(const Vec3 &o) const { return x != o.x || y != o.y || z != o.z; } bool operator<(const Vec3 &o) const { return x < o.x || y < o.y || z < o.z; } bool operator>(const Vec3 &o) const { return x > o.x || y > o.y || z > o.z; } Vec3 operator+(const Vec3 &o) const { return Vec3(x+o.x, y+o.y, z+o.z); } Vec3 operator-(const Vec3 &o) const { return Vec3(x-o.x, y-o.y, z-o.z); } Vec3 operator+(double k) const { return Vec3(x+k, y+k, z+k); } Vec3 operator-(double k) const { return Vec3(x-k, y-k, z-k); } Vec3 operator-() const { return Vec3(-x, -y, -z); } Vec3 operator*(const Vec3 &o) const { return Vec3(x*o.x, y*o.y, z*o.z); } Vec3 operator/(const Vec3 &o) const { return Vec3(x/o.x, y/o.y, z/o.z); } Vec3 operator*(double k) const { return Vec3(x*k, y*k, z*k); } Vec3 operator/(double k) const { return Vec3(x/k, y/k, z/k); } Vec3 &operator+=(const Vec3 &o) { x += o.x; y += o.y; z += o.z; return *this; } Vec3 &operator-=(const Vec3 &o) { x -= o.x; y -= o.y; z -= o.z; return *this; } Vec3 &operator+=(double k) { x += k; y += k; z += k; return *this; } Vec3 &operator-=(double k) { x -= k; y -= k; z -= k; return *this; } Vec3 &operator*=(const Vec3 &o) { x *= o.x; y *= o.y; z *= o.z; return *this; } Vec3 &operator/=(const Vec3 &o) { x /= o.x; y /= o.y; z /= o.z; return *this; } Vec3 &operator*=(double k) { x *= k; y *= k; z *= k; return *this; } Vec3 &operator/=(double k) { x /= k; y /= k; z /= k; return *this; } double dot(const Vec3 &o) const { return x*o.x + y*o.y + z*o.z; } double magnitude() const { return sqrt(dot(*this)); } double squaredlen() const { return dot(*this); } double dist(const Vec3 &o) const { return (*this - o).magnitude(); } Vec3 normalize() const { return *this * (1.0 / magnitude()); } Vec3 cross(const Vec3 &o) const { return Vec3(y*o.z-z*o.y, z*o.x-x*o.z, x*o.y-y*o.x); } Vec3 reflect(const Vec3 &n) const { return *this - n*2.0*dot(n); } Vec3 project(const Vec3 &n) const { return *this - n*dot(n); } Vec3 zxy() const { return Vec3(z, x, y); } Vec3 zyx() const { return Vec3(z, y, x); } Vec3 yxz() const { return Vec3(y, x, z); } Vec3 yzx() const { return Vec3(y, z, x); } Vec3 xzy() const { return Vec3(x, z, y); } }; static inline bool htcmp(const Vec3 &x, const Vec3 &y) { return x == y; } static inline uint hthash(const Vec3 &k) { uint hash = k.h[0]; for(size_t i = 1; i < sizeof(k.h)/sizeof(uint); i++) hash ^= k.h[i]; return hash; } struct Vec4 { union { struct { double x, y, z, w; }; double v[4]; uint h[4*sizeof(double)/sizeof(uint)]; }; Vec4() {} Vec4(double x, double y, double z, double w) : x(x), y(y), z(z), w(w) {} explicit Vec4(const Vec3 &p, double w = 0) : x(p.x), y(p.y), z(p.z), w(w) {} explicit Vec4(const double *v) : x(v[0]), y(v[1]), z(v[2]), w(v[3]) {} explicit Vec4(const float *v) : x(v[0]), y(v[1]), z(v[2]), w(v[3]) {} double &operator[](int i) { return v[i]; } double operator[](int i) const { return v[i]; } bool operator==(const Vec4 &o) const { return x == o.x && y == o.y && z == o.z && w == o.w; } bool operator!=(const Vec4 &o) const { return x != o.x || y != o.y || z != o.z || w != o.w; } bool operator<(const Vec4 &o) const { return x < o.x || y < o.y || z < o.z || w < o.w; } bool operator>(const Vec4 &o) const { return x > o.x || y > o.y || z > o.z || w > o.w; } Vec4 operator+(const Vec4 &o) const { return Vec4(x+o.x, y+o.y, z+o.z, w+o.w); } Vec4 operator-(const Vec4 &o) const { return Vec4(x-o.x, y-o.y, z-o.z, w-o.w); } Vec4 operator+(double k) const { return Vec4(x+k, y+k, z+k, w+k); } Vec4 operator-(double k) const { return Vec4(x-k, y-k, z-k, w-k); } Vec4 operator-() const { return Vec4(-x, -y, -z, -w); } Vec4 operator*(double k) const { return Vec4(x*k, y*k, z*k, w*k); } Vec4 operator/(double k) const { return Vec4(x/k, y/k, z/k, w/k); } Vec4 addw(double f) const { return Vec4(x, y, z, w + f); } Vec4 &operator+=(const Vec4 &o) { x += o.x; y += o.y; z += o.z; w += o.w; return *this; } Vec4 &operator+=(const Vec3 &o) { x += o.x; y += o.y; z += o.z; return * this; } Vec4 &operator-=(const Vec4 &o) { x -= o.x; y -= o.y; z -= o.z; w -= o.w; return *this; } Vec4 &operator-=(const Vec3 &o) { x -= o.x; y -= o.y; z -= o.z; return * this; } Vec4 &operator+=(double k) { x += k; y += k; z += k; w += k; return *this; } Vec4 &operator-=(double k) { x -= k; y -= k; z -= k; w -= k; return *this; } Vec4 &operator*=(double k) { x *= k; y *= k; z *= k; w *= k; return *this; } Vec4 &operator/=(double k) { x /= k; y /= k; z /= k; w /= k; return *this; } double dot3(const Vec4 &o) const { return x*o.x + y*o.y + z*o.z; } double dot3(const Vec3 &o) const { return x*o.x + y*o.y + z*o.z; } double dot(const Vec4 &o) const { return dot3(o) + w*o.w; } double dot(const Vec3 &o) const { return x*o.x + y*o.y + z*o.z + w; } double magnitude() const { return sqrt(dot(*this)); } double magnitude3() const { return sqrt(dot3(*this)); } Vec4 normalize() const { return *this * (1.0 / magnitude()); } Vec3 cross3(const Vec4 &o) const { return Vec3(y*o.z-z*o.y, z*o.x-x*o.z, x*o.y-y*o.x); } Vec3 cross3(const Vec3 &o) const { return Vec3(y*o.z-z*o.y, z*o.x-x*o.z, x*o.y-y*o.x); } void setxyz(const Vec3 &o) { x = o.x; y = o.y; z = o.z; } }; inline Vec3::Vec3(const Vec4 &v) : x(v.x), y(v.y), z(v.z) {} static inline bool htcmp(const Vec4 &x, const Vec4 &y) { return x == y; } static inline uint hthash(const Vec4 &k) { uint hash = k.h[0]; for(size_t i = 1; i < sizeof(k.h)/sizeof(uint); i++) hash ^= k.h[i]; return hash; } struct Matrix3x3; struct Matrix3x4; struct Quat : Vec4 { Quat() {} Quat(double x, double y, double z, double w) : Vec4(x, y, z, w) {} Quat(const float *ptr) : Vec4(ptr) {} Quat(double angle, const Vec3 &axis) { double s = sin(0.5*angle); x = s*axis.x; y = s*axis.y; z = s*axis.z; w = cos(0.5*angle); } explicit Quat(const Vec3 &v) : Vec4(v.x, v.y, v.z, -sqrt(max(1.0 - v.squaredlen(), 0.0))) {} explicit Quat(const Matrix3x3 &m) { convertmatrix(m); } explicit Quat(const Matrix3x4 &m) { convertmatrix(m); } void restorew() { w = -sqrt(max(1.0 - dot3(*this), 0.0)); } Quat operator*(const Quat &o) const { return Quat(w*o.x + x*o.w + y*o.z - z*o.y, w*o.y - x*o.z + y*o.w + z*o.x, w*o.z + x*o.y - y*o.x + z*o.w, w*o.w - x*o.x - y*o.y - z*o.z); } Quat &operator*=(const Quat &o) { return (*this = *this * o); } Quat operator+(const Vec4 &o) const { return Quat(x+o.x, y+o.y, z+o.z, w+o.w); } Quat &operator+=(const Vec4 &o) { return (*this = *this + o); } Quat operator-(const Vec4 &o) const { return Quat(x-o.x, y-o.y, z-o.z, w-o.w); } Quat &operator-=(const Vec4 &o) { return (*this = *this - o); } Quat operator-() const { return Quat(-x, -y, -z, w); } void flip() { x = -x; y = -y; z = -z; w = -w; } Vec3 transform(const Vec3 &p) const { return p + cross3(cross3(p) + p*w)*2.0; } template void convertmatrix(const M &m) { double trace = m.a.x + m.b.y + m.c.z; if(trace>0) { double r = sqrt(1 + trace), inv = 0.5/r; w = 0.5*r; x = (m.c.y - m.b.z)*inv; y = (m.a.z - m.c.x)*inv; z = (m.b.x - m.a.y)*inv; } else if(m.a.x > m.b.y && m.a.x > m.c.z) { double r = sqrt(1 + m.a.x - m.b.y - m.c.z), inv = 0.5/r; x = 0.5*r; y = (m.b.x + m.a.y)*inv; z = (m.a.z + m.c.x)*inv; w = (m.c.y - m.b.z)*inv; } else if(m.b.y > m.c.z) { double r = sqrt(1 + m.b.y - m.a.x - m.c.z), inv = 0.5/r; x = (m.b.x + m.a.y)*inv; y = 0.5*r; z = (m.c.y + m.b.z)*inv; w = (m.a.z - m.c.x)*inv; } else { double r = sqrt(1 + m.c.z - m.a.x - m.b.y), inv = 0.5/r; x = (m.a.z + m.c.x)*inv; y = (m.c.y + m.b.z)*inv; z = 0.5*r; w = (m.b.x - m.a.y)*inv; } } static Quat fromangles(const Vec3 &rot) { double cx = cos(rot.x/2), sx = sin(rot.x/2), cy = cos(rot.y/2), sy = sin(rot.y/2), cz = cos(rot.z/2), sz = sin(rot.z/2); Quat q(sx*cy*cz - cx*sy*sz, cx*sy*cz + sx*cy*sz, cx*cy*sz - sx*sy*cz, cx*cy*cz + sx*sy*sz); if(q.w > 0) q.flip(); return q; } static Quat fromdegrees(const Vec3 &rot) { return fromangles(rot * (M_PI / 180)); } }; struct Matrix3x3 { Vec3 a, b, c; Matrix3x3() {} Matrix3x3(const Vec3 &a, const Vec3 &b, const Vec3 &c) : a(a), b(b), c(c) {} explicit Matrix3x3(const Quat &q) { convertquat(q); } explicit Matrix3x3(const Quat &q, const Vec3 &scale) { convertquat(q); a *= scale; b *= scale; c *= scale; } void convertquat(const Quat &q) { double x = q.x, y = q.y, z = q.z, w = q.w, tx = 2*x, ty = 2*y, tz = 2*z, txx = tx*x, tyy = ty*y, tzz = tz*z, txy = tx*y, txz = tx*z, tyz = ty*z, twx = w*tx, twy = w*ty, twz = w*tz; a = Vec3(1 - (tyy + tzz), txy - twz, txz + twy); b = Vec3(txy + twz, 1 - (txx + tzz), tyz - twx); c = Vec3(txz - twy, tyz + twx, 1 - (txx + tyy)); } Matrix3x3 operator*(const Matrix3x3 &o) const { return Matrix3x3( o.a*a.x + o.b*a.y + o.c*a.z, o.a*b.x + o.b*b.y + o.c*b.z, o.a*c.x + o.b*c.y + o.c*c.z); } Matrix3x3 &operator*=(const Matrix3x3 &o) { return (*this = *this * o); } void transpose(const Matrix3x3 &o) { a = Vec3(o.a.x, o.b.x, o.c.x); b = Vec3(o.a.y, o.b.y, o.c.y); c = Vec3(o.a.z, o.b.z, o.c.z); } void transpose() { transpose(Matrix3x3(*this)); } Vec3 transform(const Vec3 &o) const { return Vec3(a.dot(o), b.dot(o), c.dot(o)); } float determinant() { return a.x * b.y * c.z + a.y * b.z * c.x + a.z * b.x * c.y - a.z * b.y * c.x - a.y * b.x * c.z - a.x * b.z * c.y; } }; struct Matrix3x4 { Vec4 a, b, c; Matrix3x4() {} Matrix3x4(const Vec4 &a, const Vec4 &b, const Vec4 &c) : a(a), b(b), c(c) {} explicit Matrix3x4(const Matrix3x3 &rot, const Vec3 &trans) : a(Vec4(rot.a, trans.x)), b(Vec4(rot.b, trans.y)), c(Vec4(rot.c, trans.z)) { } explicit Matrix3x4(const Quat &rot, const Vec3 &trans) { *this = Matrix3x4(Matrix3x3(rot), trans); } explicit Matrix3x4(const Quat &rot, const Vec3 &trans, const Vec3 &scale) { *this = Matrix3x4(Matrix3x3(rot, scale), trans); } Matrix3x4 operator*(float k) const { return Matrix3x4(*this) *= k; } Matrix3x4 &operator*=(float k) { a *= k; b *= k; c *= k; return *this; } Matrix3x4 operator+(const Matrix3x4 &o) const { return Matrix3x4(*this) += o; } Matrix3x4 &operator+=(const Matrix3x4 &o) { a += o.a; b += o.b; c += o.c; return *this; } Matrix3x4 operator+(const Vec3 &o) const { return Matrix3x4(*this) += o; } Matrix3x4 &operator+=(const Vec3 &o) { a[3] += o[0]; b[3] += o[1]; c[3] += o[2]; return *this; } void invert(const Matrix3x4 &o) { Matrix3x3 invrot(Vec3(o.a.x, o.b.x, o.c.x), Vec3(o.a.y, o.b.y, o.c.y), Vec3(o.a.z, o.b.z, o.c.z)); invrot.a /= invrot.a.squaredlen(); invrot.b /= invrot.b.squaredlen(); invrot.c /= invrot.c.squaredlen(); Vec3 trans(o.a.w, o.b.w, o.c.w); a = Vec4(invrot.a, -invrot.a.dot(trans)); b = Vec4(invrot.b, -invrot.b.dot(trans)); c = Vec4(invrot.c, -invrot.c.dot(trans)); } void invert() { invert(Matrix3x4(*this)); } Matrix3x4 operator*(const Matrix3x4 &o) const { return Matrix3x4( (o.a*a.x + o.b*a.y + o.c*a.z).addw(a.w), (o.a*b.x + o.b*b.y + o.c*b.z).addw(b.w), (o.a*c.x + o.b*c.y + o.c*c.z).addw(c.w)); } Matrix3x4 &operator*=(const Matrix3x4 &o) { return (*this = *this * o); } Vec3 transform(const Vec3 &o) const { return Vec3(a.dot(o), b.dot(o), c.dot(o)); } Vec3 transform3(const Vec3 &o) const { return Vec3(a.dot3(o), b.dot3(o), c.dot3(o)); } }; void conoutf(const char *s, ...) { defvformatstring(msg,s,s); printf("%s\n", msg); } void fatal(const char *s, ...) // failure exit { defvformatstring(msg,s,s); fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } // // According to IQM file spec, all field offsets must be 4-byte aligned. // Given a desired destination pointer to write data to, add pad bytes // to ensure 4-byte alignment. // unsigned int pad_field_ofs(unsigned int field_ofs) { return (field_ofs - 1) + 4 - ((field_ofs - 1) % 4); } ================================================ FILE: plugins/Makefile ================================================ #windows is special as always, but we don't support itanium, and microsoft don't support anything else (not even arm with the nt win32 api) ifeq ($(FTE_TARGET),win32) PLUG_NATIVE_EXT=_x86.dll PLUG_LDFLAGS= -static-libgcc PLUG_LDFLAGS_ZLIB=-L../engine/libs/mingw-libs -lz BITS=32 PLUG_LDFLAGS_DL= CMAKERULES=$(OUT_DIR)/toolchain_$(FTE_TARGET).cmake endif ifeq ($(FTE_TARGET),win64) PLUG_NATIVE_EXT=_x64.dll PLUG_LDFLAGS=-Wl,--support-old-code -static-libgcc PLUG_LDFLAGS_ZLIB=-L../engine/libs/mingw64-libs -lz BITS=64 PLUG_LDFLAGS_DL= CMAKERULES=$(OUT_DIR)/toolchain_$(FTE_TARGET).cmake endif PLUG_PREFIX=$(OUT_DIR)/fteplug_ ifeq ($(FTE_TARGET),bsd) PLUG_LDFLAGS_DL?=-ldl -lc else PLUG_LDFLAGS_DL?=-ldl -static-libgcc endif PLUG_LDFLAGS?=-L/usr/local/lib -Wl,-R/usr/local/lib -lm PLUG_LDFLAGS_ZLIB?=-lz ifneq ($(PLUG_NATIVE_EXT),) #if we're on windows, we'll put our windows-specific hacks here. PLUG_DEFFILE=plugin.def PLUG_CFLAGS= PLUG_CXXFLAGS= endif #cygwin uses dll naming. ifeq ($(FTE_TARGET),cygwin) ifeq ($(BITS),64) PLUG_DEFFILE=plugin.def PLUG_NATIVE_EXT=_amd64.dll endif ifneq ($(BITS),64) PLUG_DEFFILE=plugin.def PLUG_NATIVE_EXT=_x86.dll endif endif #if they're not on windows, we'll try asking the compiler directly #the check to see if its already set is to avoid asking msvc, which would probably break things. ifeq ($(PLUG_NATIVE_EXT),) LIBRESOLV=-lresolv ifneq ($(shell echo|$(CC) -E -dM -|grep __amd64__),) #either x32 or x64 ABIs ifneq ($(shell echo|$(CC) -E -dM -|grep __ILP32__|grep 1),) PLUG_NATIVE_EXT=_x32.so else PLUG_NATIVE_EXT=_amd64.so endif endif ifneq ($(shell echo|$(CC) -E -dM -|grep __i386__),) PLUG_NATIVE_EXT=_x86.so endif ifneq ($(shell echo|$(CC) -E -dM -|grep __arm__),) #gnueabi[hf] ifneq ($(shell echo|$(CC) -E -dM -|grep __SOFTFP__),) PLUG_NATIVE_EXT=_arm.so else PLUG_NATIVE_EXT=_armhf.so endif endif ifneq ($(shell echo|$(CC) -E -dM -|grep __ppc__),) PLUG_NATIVE_EXT=_ppc.so #32bit big-endian endif ifneq ($(shell echo|$(CC) -E -dM -|grep __ppc64__),) PLUG_NATIVE_EXT=_ppc64.so #64bit big-endian endif ifneq ($(shell echo|$(CC) -E -dM -|grep __ppc64le__),) PLUG_NATIVE_EXT=_ppc64le.so #64bit little-endian. endif endif ifeq ($(FTE_TARGET),droid) #plugins get written to the tmp build dir, to avoid conflicts PLUG_PREFIX=$(OUT_DIR)/m_droid-$(DROID_ARCH)/libplug_ #don't bother with cpu arch postfixes. they'll be in separate directories anyway. PLUG_NATIVE_EXT=.so #libresolv has no public api on android... LIBRESOLV= #so we know our target. PLUG_CFLAGS=-DANDROID PLUG_CXXFLAGS=-DANDROID endif ARCHLIBS=../engine/libs-$(ARCH) #fallback PLUG_NATIVE_EXT?=.so PLUG_DEFFILE?= PLUG_CFLAGS?=-fPIC -Wl,--no-undefined -Bsymbolic -fvisibility=hidden -static-libgcc PLUG_CXXFLAGS?=-fPIC -Wl,--no-undefined -Bsymbolic -fvisibility=hidden -static-libgcc PLUG_CMAKE?=-DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_COMPILER=$(CC) -DCMAKE_CXX_COMPILER=$(CXX) PLUG_LDFLAGS_ZLIB?= ARCH:=$(shell $(CC) -dumpmachine) PLUG_LDFLAGS:=-L$(ARCHLIBS) $(PLUG_LDFLAGS) PLUG_CFLAGS:=-I$(ARCHLIBS) $(PLUG_CFLAGS) PLUG_CXXFLAGS:=-I$(ARCHLIBS) $(PLUG_CXXFLAGS) #$1 is the plugin name, $2 is its filename, $3 is title, $4 is description define EMBEDMETA @echo "{\\n package fteplug_$1\\n ver \"${SVN_VERSION}\"\\n category Plugins\\n title \"$3\"\\n gamedir \"\"\\n desc \"$4\"\\n}" | zip -q -9 -fz- $2.metazip - @cat $2.metazip >> $2 @zip -q -A $2 @rm $2.metazip endef #legacy build rule, now equivelent to all native: all clean: ezscript-clean qi-clean hud-clean irc-clean .PHONY: all native distclean clean help: @-echo make a subdirectory ###################################### #small script to download+install avformat for windows cross compiles. #linux users are expected to have the library installed locally already. If your version is too old or missing, run the following command to install it (to /usr/local), then delete the gz and directory. #wget http://ffmpeg.org/releases/ffmpeg-1.2.tar.gz && cd tar xvfz ffmpeg-1.2.tar.gz && cd ffmpeg-1.2/ && ./configure --disable-yasm --enable-shared && make && sudo make install #we use ffmpeg's version for some reason, as opposed to libav. not sure what the differences are meant to be, but libav seemed to have non-depricated functions defined, docs that say to use them, and these functions missing. AV_VER=ffmpeg-4.0 ifeq ($(findstring win,$(FTE_TARGET)),win) AV_BASE=$(abspath $(OUT_DIR)/../fte_libav_$(AV_VER))/ else ifeq ($(FTE_TARGET),bsd) AV_BASE=/usr/local/include/ else AV_BASE=/usr/include/ffmpeg/ endif endif ifneq ($(AV_BASE),) AV_DEP=$(AV_BASE)libavformat/avformat.h AV_CFLAGS=-I$(AV_BASE) AV_LDFLAGS=-L$(AV_BASE)lib$(BITS) -lavcodec -lavformat -lavutil -lswscale else AV_LDFLAGS=-lavcodec -lavformat -lavutil -lswscale endif ifeq ($(FTE_TARGET),bsd) AV_LDFLAGS+=-lc endif AVPLUG_OBJS= avplug/avaudio.c avplug/avencode.c avplug/avdecode.c plugin.c ifeq ($(findstring win,$(FTE_TARGET)),win) ifeq (0,1) AV_ARCHIVEEXT=.z7 AV_EXTRACT=7z e -y else AV_ARCHIVEEXT=.zip AV_EXTRACT=unzip -ju endif AV_W32_DEV=$(AV_VER)-win32-dev$(AV_ARCHIVEEXT) AV_W64_DEV=$(AV_VER)-win64-dev$(AV_ARCHIVEEXT) AV_W32_BIN=$(AV_VER)-win32-shared$(AV_ARCHIVEEXT) AV_W64_BIN=$(AV_VER)-win64-shared$(AV_ARCHIVEEXT) AV_URL32_DEV=https://archive.org/download/zeranoe/win32/dev/$(AV_W32_DEV) AV_URL64_DEV=https://archive.org/download/zeranoe/win64/dev/$(AV_W64_DEV) AV_URL32_BIN=https://archive.org/download/zeranoe/win32/shared/$(AV_W32_BIN) AV_URL64_BIN=https://archive.org/download/zeranoe/win64/shared/$(AV_W64_BIN) AV_PRE32_DEV=$(AV_VER)-win32-dev/ AV_PRE64_DEV=$(AV_VER)-win64-dev/ AV_PRE32_BIN=$(AV_VER)-win32-shared/ AV_PRE64_BIN=$(AV_VER)-win64-shared/ ifeq ($(FTE_TARGET),win32) FFMPEG_ZIP=$(AV_BASE)/$(AV_VER)-x86.zip #NATIVE_PLUGINS+=ffmpeg endif ifeq ($(FTE_TARGET),win64) FFMPEG_ZIP=$(AV_BASE)/$(AV_VER)-x64.zip #NATIVE_PLUGINS+=ffmpeg endif $(AV_BASE)$(AV_VER)-win32.zip: mkdir -p $(AV_BASE) cd $(AV_BASE) && wget -N $(AV_URL32_BIN) mkdir -p $(AV_BASE)bin32 && cd $(AV_BASE)bin32 && $(AV_EXTRACT) ../$(AV_W32_BIN) $(AV_PRE32_BIN)bin/avcodec-*.dll $(AV_PRE32_BIN)bin/avutil-*.dll $(AV_PRE32_BIN)bin/swresample-*.dll $(AV_PRE32_BIN)bin/avformat-*.dll $(AV_PRE32_BIN)bin/swscale-*.dll && cd - zip -j9 $@ $(AV_BASE)bin32/*.dll $(AV_BASE)$(AV_VER)-win64.zip: mkdir -p $(AV_BASE) cd $(AV_BASE) && wget -N $(AV_URL64_BIN) mkdir -p $(AV_BASE)bin64 && cd $(AV_BASE)bin64 && $(AV_EXTRACT) ../$(AV_W64_BIN) $(AV_PRE64_BIN)bin/avcodec-*.dll $(AV_PRE64_BIN)bin/avutil-*.dll $(AV_PRE64_BIN)bin/swresample-*.dll $(AV_PRE64_BIN)bin/avformat-*.dll $(AV_PRE64_BIN)bin/swscale-*.dll && cd - zip -j9 $@ $(AV_BASE)bin64/*.dll ifneq ($(FFMPEG_ZIP),) $(FFMPEG_ZIP): $(AV_BASE)$(AV_VER)-$(FTE_TARGET).zip cp $(AV_BASE)$(AV_VER)-$(FTE_TARGET).zip $@ endif $(AV_BASE)libavformat/avformat.h: mkdir -p $(AV_BASE) cd $(AV_BASE) && wget -N $(AV_URL32_DEV) mkdir -p $(AV_BASE)libavformat && cd $(AV_BASE)libavformat && $(AV_EXTRACT) ../$(AV_W32_DEV) $(AV_PRE32_DEV)include/libavformat/* && cd - mkdir -p $(AV_BASE)libavcodec && cd $(AV_BASE)libavcodec && $(AV_EXTRACT) ../$(AV_W32_DEV) $(AV_PRE32_DEV)include/libavcodec/* && cd - mkdir -p $(AV_BASE)libavutil && cd $(AV_BASE)libavutil && $(AV_EXTRACT) ../$(AV_W32_DEV) $(AV_PRE32_DEV)include/libavutil/* && cd - mkdir -p $(AV_BASE)libswscale && cd $(AV_BASE)libswscale && $(AV_EXTRACT) ../$(AV_W32_DEV) $(AV_PRE32_DEV)include/libswscale/* && cd - mkdir -p $(AV_BASE)lib32 && cd $(AV_BASE)lib32 && $(AV_EXTRACT) ../$(AV_W32_DEV) $(AV_PRE32_DEV)lib/avformat.lib $(AV_PRE32_DEV)lib/avcodec.lib $(AV_PRE32_DEV)lib/avutil.lib $(AV_PRE32_DEV)lib/swscale.lib && cd - cd $(AV_BASE) && wget -N $(AV_URL64_DEV) mkdir -p $(AV_BASE)lib64 && cd $(AV_BASE)lib64 && $(AV_EXTRACT) ../$(AV_W64_DEV) $(AV_PRE64_DEV)lib/avformat.lib $(AV_PRE64_DEV)lib/avcodec.lib $(AV_PRE64_DEV)lib/avutil.lib $(AV_PRE64_DEV)lib/swscale.lib && cd - distclean: rm $(AV_BASE)libavformat/avformat.h rm $(AV_BASE)$(AV_VER)-win32.zip rm $(AV_BASE)$(AV_VER)-win64.zip $(PLUG_PREFIX)ffmpeg$(PLUG_NATIVE_EXT): $(AV_DEP) $(AVPLUG_OBJS) $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -s -o $@ -shared $(PLUG_CFLAGS) $(AV_CFLAGS) $(AVPLUG_OBJS) $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(AV_LDFLAGS) $(call EMBEDMETA,ffmpeg,$@,FFMPEG Video Decoding Plugin,Provides support for more audio formats as well as video playback and better capture support.) else # NIX $(PLUG_PREFIX)ffmpeg$(PLUG_NATIVE_EXT): $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -s -o $@ -shared $(PLUG_CFLAGS) $(AV_CFLAGS) $(AVPLUG_OBJS) $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(AV_LDFLAGS) $(call EMBEDMETA,ffmpeg,$@,FFMPEG Video Decoding Plugin,Provides support for more audio formats as well as video playback and better capture support.) endif NATIVE_PLUGINS+=ffmpeg ###################################### ###################################### #small script for ode #FIXME: ode fails to compile under cygwin #FIXME: race condition if you try compiling debug+release at the same time, as this makefile is invoked twice by the engine's one ODE_ARCH=$(FTE_TARGET) ifeq ($(ODE_ARCH),) ODE_ARCH=unknown endif ODE_VER=0.16.5 ODE_URL=https://bitbucket.org/odedevs/ode/downloads/ode-$(ODE_VER).tar.gz ODE_BASE=$(ARCHLIBS)/ode-$(ODE_VER)_$(ODE_ARCH)/ ODE_LIB=$(ODE_BASE)ode-$(ODE_VER)/ode/src/.libs/libode.a $(OUT_DIR)/../ode-$(ODE_VER).tar.gz: mkdir -p $(ODE_BASE) cd $(OUT_DIR)/.. && wget -N $(ODE_URL) $(ODE_LIB): $(OUT_DIR)/../ode-$(ODE_VER).tar.gz mkdir -p $(ODE_BASE) && cd $(ODE_BASE) && tar xvfz $< cd $(ODE_BASE)ode-$(ODE_VER)/ && ./bootstrap && ./configure --enable-double-precision --disable-demos --without-x --with-pic CC="$(CC) $(PLUG_CXXFLAGS)" CXX="$(CC) $(PLUG_CXXFLAGS)" --host=`$(CC) -dumpmachine` && $(MAKE) ODE_FILES=../engine/common/com_phys_ode.c ../engine/common/mathlib.c plugin.c $(ODE_LIB) $(PLUG_PREFIX)ode$(PLUG_NATIVE_EXT): $(ODE_FILES) $(CC) -flto -s $(BASE_CFLAGS) $(CFLAGS) -Os -DFTEPLUGIN -DODE_STATIC -o $@ -shared $(PLUG_CFLAGS) -I$(ODE_BASE)ode-$(ODE_VER)/include $(ODE_FILES) $(PLUG_DEFFILE) $(PLUG_LDFLAGS) -static-libgcc `$(CC) -print-file-name=libstdc++.a` -lpthread $(call EMBEDMETA,ode,$@,ODE Physics,Provides Rigid Body Physics behaviours.) #NATIVE_PLUGINS+=ode ###################################### ###################################### ifneq ($(CMAKERULES),) BULLET_CFLAGS+=-static-libstdc++ PLUG_CMAKE+= -DCMAKE_TOOLCHAIN_FILE="$(CMAKERULES)" $(CMAKERULES): echo "set(CMAKE_SYSTEM_NAME Windows)" > $@ echo "set(TOOLCHAIN_PREFIX `$(CC) -dumpmachine`)" >> $@ # cross compilers to use for C, C++ and Fortran echo "set(CMAKE_C_COMPILER $(CC))" >> $@ echo "set(CMAKE_CXX_COMPILER $(CXX))" >> $@ echo "set(CMAKE_RC_COMPILER $${TOOLCHAIN_PREFIX}-windres)" >> $@ # target environment on the build host system echo "set(CMAKE_FIND_ROOT_PATH /usr/$${TOOLCHAIN_PREFIX})" >> $@ # modify default behavior of FIND_XXX() commands echo "set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)" >> $@ echo "set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)" >> $@ echo "set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)" >> $@ endif BULLET_VER=2.87 BULLET_URL=https://github.com/bulletphysics/bullet3/archive/$(BULLET_VER).tar.gz BULLET_BASE=$(OUT_DIR)/../bullet3-$(BULLET_VER)_$(FTE_TARGET)/ BULLET_LIBS= \ $(BULLET_BASE)bullet3-$(BULLET_VER)/lib/libBulletDynamics.a \ $(BULLET_BASE)bullet3-$(BULLET_VER)/lib/libBulletCollision.a \ $(BULLET_BASE)bullet3-$(BULLET_VER)/lib/libLinearMath.a BULLET_CFLAGS+=-I$(BULLET_BASE)bullet3-$(BULLET_VER)/src $(OUT_DIR)/../bullet3-$(BULLET_VER).tar.gz: mkdir -p $(BULLET_BASE) wget -N $(BULLET_URL) -O $@ BULLET_LIB=$(BULLET_BASE)bullet3-$(BULLET_VER)/lib/libLinearMath.a $(BULLET_BASE)bullet3-$(BULLET_VER)/lib/libBulletDynamics.a $(BULLET_BASE)bullet3-$(BULLET_VER)/lib/libBulletCollision.a: $(BULLET_LIB) $(BULLET_LIB): $(OUT_DIR)/../bullet3-$(BULLET_VER).tar.gz $(CMAKERULES) mkdir -p $(BULLET_BASE) && cd $(BULLET_BASE) && tar xvfz $< rm $(BULLET_BASE)bullet3-$(BULLET_VER)/build3/cmake/FindPythonLibs.cmake #cmake is a pile of shite and fails at cross compiling. oh well, we didn't want any python stuff anyway. cd $(BULLET_BASE)bullet3-$(BULLET_VER)/ && cmake $(PLUG_CMAKE) -DBUILD_PYBULLET:BOOL=OFF -DBUILD_DEMOS:BOOL=OFF -DBUILD_EXTRAS:BOOL=OFF -DLIBRARY_OUTPUT_PATH=$(BULLET_BASE)bullet3-$(BULLET_VER)/lib . && $(MAKE) LinearMath BulletDynamics BulletCollision #./configure --enable-double-precision --disable-demos --without-x CXX="$(CC)" CFLAGS="$(PLUG_CFLAGS)" CXXFLAGS="$(PLUG_CXXFLAGS)" --host=`$(CC) -dumpmachine` && make $(PLUG_PREFIX)bullet$(PLUG_NATIVE_EXT): bullet/bulletplug.cpp plugin.c $(BULLET_LIBS) $(CXX) $(BASE_CXXFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(BULLET_CFLAGS) $(call EMBEDMETA,bullet,$@,Bullet Physics Plugin,Provides Rigid Body Physics.) #NATIVE_PLUGINS+=bullet ###################################### ###################################### #Regarding CEF Versions: Cef 2526 is reportedly the most recent _WORKING_ version of libcef. Later versions have screwed webgl etc, and are just generally unstable. #However, that's now impossible to get hold of since the old cefbuilds server went down. New builds are hosted by spotify and they're all randomly broken, so we might as well just use whatever seems fairly recent. #WARNING: Changing CEF_VER requires updating downloadables.php etc ifeq ($(FTE_TARGET),win32) CEF_ARCH=windows32 endif ifeq ($(FTE_TARGET),win64) CEF_ARCH=windows64 endif ifeq ($(FTE_TARGET),linux32) CEF_ARCH=linux32 endif ifeq ($(FTE_TARGET),linux64) CEF_ARCH=linux64 endif #ifeq ($(FTE_TARGET),macosx64) #CEF_ARCH=macosx64 #endif CEF_VER=95.7.14+g9f72f35+chromium-95.0.4638.69 CEF_NAME=cef_binary_$(CEF_VER)_$(CEF_ARCH)_minimal CEF_URL=https://cef-builds.spotifycdn.com/cef_binary_$(CEF_VER)_$(CEF_ARCH)_minimal.tar.bz2 ifneq ($(CEF_ARCH),) cef/$(CEF_NAME)/include/cef_version.h: cd cef && wget -N $(CEF_URL) cd cef && tar -xjf $(CEF_NAME).tar.bz2 cef/$(CEF_NAME)/rel.zip: cef/$(CEF_NAME)/include/cef_version.h cd cef/$(CEF_NAME)/Release && zip -9 ../rel.zip *.dll *.bin cd cef/$(CEF_NAME)/Resources && zip -r9 ../rel.zip . $(OUT_DIR)/cef_$(CEF_VER).zip: cef/$(CEF_NAME)/rel.zip cp cef/$(CEF_NAME)/rel.zip $@ CEF_SOURCES=cef/cef.c plugin.c $(PLUG_PREFIX)cef$(PLUG_NATIVE_EXT): $(CEF_SOURCES) $(OUT_DIR)/cef_$(CEF_VER).zip cef/$(CEF_NAME)/include/cef_version.h $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $(PLUG_PREFIX)cef$(PLUG_NATIVE_EXT) -shared $(PLUG_CFLAGS) -Icef/$(CEF_NAME) $(CEF_SOURCES) $(PLUG_DEFFILE) $(PLUG_LDFLAGS) -Wl,-rpath,. $(PLUG_LDFLAGS_DL) $(call EMBEDMETA,cef,$@,libcef Browser Plugin,This plugin provides support for an in-game web browser.) #NATIVE_PLUGINS+=cef else .PHONEY: $(PLUG_PREFIX)cef$(PLUG_NATIVE_EXT) $(PLUG_PREFIX)cef$(PLUG_NATIVE_EXT): @echo cef plugin not supported on this arch - $(FTE_TARGET) - $(CEF_ARCH) endif ###################################### #quake3 BOTLIBFILES=quake3/botlib/be_aas_bspq3.c \ quake3/botlib/be_aas_cluster.c \ quake3/botlib/be_aas_debug.c \ quake3/botlib/be_aas_entity.c \ quake3/botlib/be_aas_file.c \ quake3/botlib/be_aas_main.c \ quake3/botlib/be_aas_move.c \ quake3/botlib/be_aas_optimize.c \ quake3/botlib/be_aas_reach.c \ quake3/botlib/be_aas_routealt.c \ quake3/botlib/be_aas_route.c \ quake3/botlib/be_aas_sample.c \ quake3/botlib/be_ai_char.c \ quake3/botlib/be_ai_chat.c \ quake3/botlib/be_ai_gen.c \ quake3/botlib/be_ai_goal.c \ quake3/botlib/be_ai_move.c \ quake3/botlib/be_ai_weap.c \ quake3/botlib/be_ai_weight.c \ quake3/botlib/be_ea.c \ quake3/botlib/be_interface.c \ quake3/botlib/l_crc.c \ quake3/botlib/l_libvar.c \ quake3/botlib/l_log.c \ quake3/botlib/l_memory.c \ quake3/botlib/l_precomp.c \ quake3/botlib/l_script.c \ quake3/botlib/l_struct.c \ quake3/botlib/standalone.c QUAKE3FILES=$(BOTLIBFILES) \ plugin.c \ quake3/clq3_cg.c \ quake3/clq3_ui.c \ quake3/clq3_parse.c \ quake3/svq3_game.c \ quake3/q3common.c $(PLUG_PREFIX)quake3$(PLUG_NATIVE_EXT): ${QUAKE3FILES} $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -DMULTITHREAD -DBOTLIB -DBOTLIB_STATIC -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS_ZLIB) $(PLUG_LDFLAGS) $(call EMBEDMETA,quake3,$@,Quake3 Compat,Quake3 Gamecode Compatibility) NATIVE_PLUGINS+=quake3 ###################################### #for custom/private plugins... -include Makefile.private #small plugins with simpler build rules... ###################################### #Mostly for a joke. $(PLUG_PREFIX)mpq$(PLUG_NATIVE_EXT): mpq/fs_mpq.c mpq/blast.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) -Impq $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS_ZLIB) $(PLUG_LDFLAGS) $(call EMBEDMETA,mpq,$@,MPQ Archives,Provides support for Blizzard's .mpq archive format used in eg Diablo 2) #NATIVE_PLUGINS+=mpq ###################################### ###################################### # XMPP aka Jabber chat-protocol support. requires manual account configuration. $(PLUG_PREFIX)xmpp$(PLUG_NATIVE_EXT): jabber/jabberclient.c jabber/jingle.c jabber/sift.c jabber/xml.c plugin.c ../engine/common/sha1.c ../engine/common/sha2.c emailnot/md5.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) -Ijabber $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(LIBRESOLV) $(call EMBEDMETA,xmpp,$@,XMPP Chat,A slightly more modern alternative to IRC. Requires manual account configuration.) NATIVE_PLUGINS+=xmpp ###################################### ###################################### # Quake Injector plugin. $(PLUG_PREFIX)qi$(PLUG_NATIVE_EXT): qi/qi.c jabber/xml.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) -Ijabber $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(call EMBEDMETA,qi,$@,Quake-Injector Plugin,Provides easy access to the Quaddicted mod database.) NATIVE_PLUGINS+=qi ###################################### ###################################### # Internet Relay Chat (IRC) plugin. $(PLUG_PREFIX)irc$(PLUG_NATIVE_EXT): irc/ircclient.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) -Iirc $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(call EMBEDMETA,irc,$@,IRC Chat,Internet Relay Chat plugin - requires extra server/nick/channel configuration) NATIVE_PLUGINS+=irc ###################################### ###################################### #OpenXR plugin, for use with VR headsets. #the tar.gz has a stupid name OPENXRVER=1.0.33 $(ARCHLIBS)/openxr/openxr.h: cd $(ARCHLIBS)/.. && (test -f release-$(OPENXRVER).tar.gz || wget https://github.com/KhronosGroup/OpenXR-SDK/archive/refs/tags/release-$(OPENXRVER).tar.gz) cd $(ARCHLIBS) && tar -xvzf ../release-$(OPENXRVER).tar.gz --strip-components=2 OpenXR-SDK-release-$(OPENXRVER)/include/openxr/ $(PLUG_PREFIX)openxr$(PLUG_NATIVE_EXT): openxr.c plugin.c $(ARCHLIBS)/openxr/openxr.h $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) -DXR_NO_PROTOTYPES `$(PKGCONFIG) --cflags openxr` -DGLQUAKE -DVKQUAKE -DD3D11QUAKE $(call EMBEDMETA,openxr,$@,OpenXR Support,Enables the use of Virtual Reality headsets and inputs.) NATIVE_PLUGINS+=openxr ###################################### ###################################### #for compat with ezquake $(PLUG_PREFIX)ezhud$(PLUG_NATIVE_EXT): ezhud/ezquakeisms.c ezhud/hud.c ezhud/hud_common.c ezhud/hud_editor.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) -Iezhud $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(call EMBEDMETA,ezhud,$@,EzHud Plugin,Provides compat with ezquake's hud scripts.) NATIVE_PLUGINS+=ezhud ###################################### ###################################### #not really relevant now that gltf was made an internal plugin $(PLUG_PREFIX)models$(PLUG_NATIVE_EXT): models/gltf.c models/exportiqm.c models/models.c plugin.c ../engine/common/json.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) -Imodels $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(call EMBEDMETA,models,$@,Models Plugin,Kinda redundant now that the engine has gltf2 loading) #NATIVE_PLUGINS+=models ###################################### ###################################### #Openssl crypto plugin, to replace microsoft's shoddy dtls implementation. could also be useful on the BSDs, yay system components? ifeq ($(FTE_TARGET),win32) OSSL_ARCH=mingw endif ifeq ($(FTE_TARGET),win64) OSSL_ARCH=mingw64 endif #statically link openssl on the above systems (instead of depending on system libs) ifneq ($(OSSL_ARCH),) OSSL_VERSION=3.0.1 #../engine/libs-$(ARCH)/openssl-$(OSSL_VERSION).tar.gz: ../engine/openssl-$(OSSL_VERSION).tar.gz: wget -O $@ -N https://github.com/openssl/openssl/archive/refs/tags/openssl-$(OSSL_VERSION).tar.gz $(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION)/libssl.a $(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION)/libcrypto.a: ../engine/openssl-$(OSSL_VERSION).tar.gz (cd $(ARCHLIBS) && tar xvfz ../openssl-$(OSSL_VERSION).tar.gz) (cd $(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION) && CFLAGS=-Os ./Configure --release no-filenames no-legacy no-shared no-stdio no-asm $(OSSL_ARCH) --cross-compile-prefix=$(ARCH)- && $(MAKE)) $(PLUG_PREFIX)openssl$(PLUG_NATIVE_EXT): $(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION)/libssl.a $(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION)/libcrypto.a #we should be using openssl's no-sock option, but that also disables core dtls functionality (despite us using our own BIOs). OPENSSL_LDCFLAGS=-I$(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION)/include -L$(ARCHLIBS)/openssl-openssl-$(OSSL_VERSION) -lssl -lcrypto -lws2_32 OPENSSL_AVAILABLE=1 endif OPENSSL_LDCFLAGS?=`$(PKGCONFIG) --libs --cflags openssl` OPENSSL_AVAILABLE?=$(shell $(OPENSSLPKGPATH) $(PKGCONFIG) --atleast-version=3.0.0 openssl && echo 1) $(PLUG_PREFIX)openssl$(PLUG_NATIVE_EXT): net_ssl_openssl.c plugin.c $(CC) -s $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(OPENSSL_LDCFLAGS) $(call EMBEDMETA,openssl,$@,OpenSSL,Provides OpenSSL support for dtls/tls/https support. The crypto library that is actually used is controlled via the tls_provider cvar.) #OpenSSL 3.0.0 is apache2 and thus GPL3-compatible, earlier versions are NOT GPL-compatible at all, so only enable it if its okay. #(you can still force it, but don't distribute) ifeq ($(OPENSSL_AVAILABLE),1) NATIVE_PLUGINS+=openssl endif ###################################### ###################################### #for compat with half-life 2's file formats $(PLUG_PREFIX)hl2$(PLUG_NATIVE_EXT): hl2/fs_vpk.c hl2/fs_vpk_vtmb.c hl2/fs_gma.c hl2/img_tth.c hl2/img_vtf.c hl2/mod_hl2.c hl2/mat_vmt.c hl2/mod_vbsp.c hl2/hl2.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -DMULTITHREAD -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS_ZLIB) $(PLUG_LDFLAGS) $(call EMBEDMETA,hl2,$@,HL2 Formats,Provides support for various formats used by Valve's Source engine.) NATIVE_PLUGINS+=hl2 ###################################### ###################################### #for compat with cod's file formats $(PLUG_PREFIX)cod$(PLUG_NATIVE_EXT): cod/codmod.c cod/codbsp.c cod/codmat.c cod/codiwi.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -DMULTITHREAD -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(call EMBEDMETA,cod,$@,CoD Formats,Provides support for various formats used by Call of Duty.) NATIVE_PLUGINS+=cod ###################################### ifeq ($(findstring win,$(FTE_TARGET)),win) ###################################### #winamp ipc $(PLUG_PREFIX)winamp$(PLUG_NATIVE_EXT): winamp/winamp.c plugin.c $(CC) $(BASE_CFLAGS) $(CFLAGS) -DFTEPLUGIN -o $@ -shared $(PLUG_CFLAGS) $^ $(PLUG_DEFFILE) $(PLUG_LDFLAGS) $(call EMBEDMETA,winamp,$@,Winamp,Provides support for controlling Winamp.) NATIVE_PLUGINS+=winamp ###################################### endif all: $(foreach FOO,$(NATIVE_PLUGINS), $(PLUG_PREFIX)$(FOO)$(PLUG_NATIVE_EXT)) ================================================ FILE: plugins/avplug/avaudio.c ================================================ #include "../plugin.h" #include "../engine.h" #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" static size_t activedecoders; static cvar_t *ffmpeg_audiodecoder, *pdeveloper; #define HAVE_DECOUPLED_API (LIBAVCODEC_VERSION_MAJOR>57 || (LIBAVCODEC_VERSION_MAJOR==57&&LIBAVCODEC_VERSION_MINOR>=36)) struct avaudioctx { //raw file uint8_t *filedata; size_t fileofs; size_t filesize; //avformat stuff AVFormatContext *pFormatCtx; int audioStream; AVCodecContext *pACodecCtx; AVFrame *pAFrame; //decoding int64_t lasttime; //output audio //we throw away data if the format changes. which is awkward, but gah. int64_t samples_framestart; int samples_channels; int samples_speed; qaudiofmt_t samples_format; qbyte *samples_buffer; size_t samples_framecount; size_t samples_maxbytes; }; static void S_AV_Purge(sfx_t *s) { struct avaudioctx *ctx = (struct avaudioctx*)s->decoder.buf; s->loadstate = SLS_NOTLOADED; // Free the audio decoder if (ctx->pACodecCtx) avcodec_free_context(&ctx->pACodecCtx); av_free(ctx->pAFrame); // Close the video file avformat_close_input(&ctx->pFormatCtx); //free the decoded buffer free(ctx->samples_buffer); //file storage will be cleared here too free(ctx); if (s->decoder.ended) activedecoders--; memset(&s->decoder, 0, sizeof(s->decoder)); } #define QAF_U8 0x81 #define QAF_S32 0x04 #ifndef MIXER_F32 #define QAF_F32 0x84 #endif #define QAF_F64 0x88 static void S_AV_ReadFrame(struct avaudioctx *ctx) { //reads an audioframe and spits its data into the output sound file for the game engine to use. qaudiofmt_t outformat = QAF_S16, informat=QAF_S16; int channels = ctx->pACodecCtx->ch_layout.nb_channels; int planes = 1, p; unsigned int auddatasize = av_samples_get_buffer_size(NULL, ctx->pACodecCtx->ch_layout.nb_channels, ctx->pAFrame->nb_samples, ctx->pACodecCtx->sample_fmt, 1); switch(ctx->pACodecCtx->sample_fmt) { //we don't support planar audio. we just treat it as mono instead. default: auddatasize = 0; break; case AV_SAMPLE_FMT_U8P: planes = channels; outformat = QAF_S8; informat = QAF_U8; break; case AV_SAMPLE_FMT_U8: planes = 1; outformat = QAF_S8; informat = QAF_U8; break; case AV_SAMPLE_FMT_S16P: planes = channels; outformat = QAF_S16; informat = QAF_S16; break; case AV_SAMPLE_FMT_S16: planes = 1; outformat = QAF_S16; informat = QAF_S16; break; case AV_SAMPLE_FMT_S32P: planes = channels; outformat = QAF_S16; informat = QAF_S32; break; case AV_SAMPLE_FMT_S32: planes = 1; outformat = QAF_S16; informat = QAF_S32; break; #ifdef MIXER_F32 case AV_SAMPLE_FMT_FLTP: planes = channels; outformat = QAF_F32; informat = QAF_F32; break; case AV_SAMPLE_FMT_FLT: planes = 1; outformat = QAF_F32; informat = QAF_F32; break; case AV_SAMPLE_FMT_DBLP: planes = channels; outformat = QAF_F32; informat = QAF_F64; break; case AV_SAMPLE_FMT_DBL: planes = 1; outformat = QAF_F32; informat = QAF_F64; break; #else case AV_SAMPLE_FMT_FLTP: planes = channels; outformat = QAF_S16; informat = QAF_F32; break; case AV_SAMPLE_FMT_FLT: planes = 1; outformat = QAF_S16; informat = QAF_F32; break; case AV_SAMPLE_FMT_DBLP: planes = channels; outformat = QAF_S16; informat = QAF_F64; break; case AV_SAMPLE_FMT_DBL: planes = 1; outformat = QAF_S16; informat = QAF_F64; break; #endif } if (ctx->samples_channels != channels || ctx->samples_speed != ctx->pACodecCtx->sample_rate || ctx->samples_format != outformat) { //something changed, update ctx->samples_channels = channels; ctx->samples_speed = ctx->pACodecCtx->sample_rate; ctx->samples_format = outformat; //and discard any decoded audio. this might loose some. ctx->samples_framestart += ctx->samples_framecount; ctx->samples_framecount = 0; } if (ctx->samples_maxbytes < (ctx->samples_framecount*QAF_BYTES(ctx->samples_format)*ctx->samples_channels)+auddatasize) { ctx->samples_maxbytes = (ctx->samples_framecount*QAF_BYTES(ctx->samples_format)*ctx->samples_channels)+auddatasize; ctx->samples_maxbytes *= 2; //slop ctx->samples_buffer = realloc(ctx->samples_buffer, ctx->samples_maxbytes); } if (planes==1 && outformat != QAF_S8 && informat==outformat) memcpy(ctx->samples_buffer + ctx->samples_framecount*(QAF_BYTES(ctx->samples_format)*ctx->samples_channels), ctx->pAFrame->data[0], auddatasize); else { void *fte_restrict outv = (ctx->samples_buffer + ctx->samples_framecount*(QAF_BYTES(ctx->samples_format)*ctx->samples_channels)); size_t i, samples = auddatasize / (planes*QAF_BYTES(informat)); if (outformat == QAF_S8 && informat == QAF_U8) { char *out = outv; for (p = 0; p < planes; p++, out++) { unsigned char *in = ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) out[i*planes] = in[i]-128; //convert from u8 to s8. } } else if (outformat == QAF_S16 && informat == QAF_S16) { signed short *out = outv; for (p = 0; p < planes; p++, out++) { signed short *in = (signed short *)ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) out[i*planes] = in[i]; //no conversion needed } } else if (outformat == QAF_S16 && informat == QAF_S32) { signed short *out = outv; for (p = 0; p < planes; p++, out++) { signed int *in = (signed int *)ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) out[i*planes] = in[i]>>16; //just use the MSBs, no clamping needed. } } #ifdef MIXER_F32 else if (outformat == QAF_F32 && informat == QAF_F32) { float *out = outv; for (p = 0; p < planes; p++, out++) { float *in = (float *)ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) out[i*planes] = in[i]; //no conversion needed. } } else if (outformat == QAF_F32 && informat == QAF_F64) { float *out = outv; for (p = 0; p < planes; p++, out++) { double *in = (double *)ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) out[i*planes] = in[i]; //no clamping needed. } } #else else if (outformat == QAF_S16 && informat == QAF_F32) { signed short *out = outv; for (p = 0; p < planes; p++, out++) { float *in = (float *)ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) { int v = in[i] * 32767; if (v < -32768) v = -32768; if (v > 32767) v = 32767; out[i*planes] = v; } } } else if (outformat == QAF_S16 && informat == QAF_F64) { signed short *out = outv; for (p = 0; p < planes; p++, out++) { double *in = (double *)ctx->pAFrame->data[p]; for (i = 0; i < samples; i++) { int v = in[i] * 32767; if (v < -32768) v = -32768; if (v > 32767) v = 32767; out[i*planes] = v; } } } #endif } ctx->samples_framecount += auddatasize/(QAF_BYTES(informat)*ctx->samples_channels); } static sfxcache_t *S_AV_Locate(sfx_t *sfx, sfxcache_t *buf, ssamplepos_t start, int length) { //warning: can be called on a different thread. struct avaudioctx *ctx = (struct avaudioctx*)sfx->decoder.buf; AVPacket packet; int64_t curtime; if (!buf) return NULL; curtime = start + length; while (1) { if (start < ctx->samples_framestart) break; //o.O rewind! if (ctx->samples_framestart+ctx->samples_framecount > curtime) break; //no need yet. #ifdef HAVE_DECOUPLED_API if(0==avcodec_receive_frame(ctx->pACodecCtx, ctx->pAFrame)) { S_AV_ReadFrame(ctx); continue; } #endif // We're ahead of the previous frame. try and read the next. if (av_read_frame(ctx->pFormatCtx, &packet) < 0) break; // Is this a packet from the video stream? if(packet.stream_index==ctx->audioStream) { #ifdef HAVE_DECOUPLED_API avcodec_send_packet(ctx->pACodecCtx, &packet); #else int okay; int len; void *odata = packet.data; while (packet.size > 0) { //this old api only decodes part of the packet with each itteration, so keep reading until we decoded the entire thing. okay = false; len = avcodec_decode_audio4(ctx->pACodecCtx, ctx->pAFrame, &okay, &packet); if (len < 0) break; packet.size -= len; packet.data += len; if (okay) S_AV_ReadFrame(ctx); } packet.data = odata; #endif } // Free the packet that was allocated by av_read_frame av_packet_unref(&packet); } buf->length = ctx->samples_framecount; buf->speed = ctx->samples_speed; buf->format = ctx->samples_format; buf->numchannels = ctx->samples_channels; buf->soundoffset = ctx->samples_framestart; buf->data = ctx->samples_buffer; //if we couldn't return any new data, then we're at an eof, return NULL to signal that. if (start == buf->soundoffset + buf->length && length > 0) return NULL; return buf; } static float S_AV_Query(struct sfx_s *sfx, struct sfxcache_s *buf, char *title, size_t titlesize) { struct avaudioctx *ctx = (struct avaudioctx*)sfx->decoder.buf; if (!ctx) return -1; if (buf) { buf->data = NULL; buf->soundoffset = 0; buf->length = 0; buf->numchannels = ctx->samples_channels; buf->speed = ctx->samples_speed; buf->format = ctx->samples_format; } return ctx->pFormatCtx->duration / (float)AV_TIME_BASE; } static int AVIO_Mem_Read(void *opaque, uint8_t *buf, int buf_size) { struct avaudioctx *ctx = opaque; if (ctx->fileofs > ctx->filesize) buf_size = 0; if (buf_size > ctx->filesize-ctx->fileofs) buf_size = ctx->filesize-ctx->fileofs; if (buf_size > 0) { memcpy(buf, ctx->filedata + ctx->fileofs, buf_size); ctx->fileofs += buf_size; return buf_size; } return 0; } static int64_t AVIO_Mem_Seek(void *opaque, int64_t offset, int whence) { struct avaudioctx *ctx = opaque; whence &= ~AVSEEK_FORCE; switch(whence) { default: return -1; case SEEK_SET: ctx->fileofs = offset; break; case SEEK_CUR: ctx->fileofs += offset; break; case SEEK_END: ctx->fileofs = ctx->filesize + offset; break; case AVSEEK_SIZE: return ctx->filesize; } if (ctx->fileofs < 0) ctx->fileofs = 0; return ctx->fileofs; } /*const char *COM_GetFileExtension (const char *in) { const char *dot; for (dot = in + strlen(in); dot >= in && *dot != '.'; dot--) ; if (dot < in) return ""; in = dot+1; return in; }*/ static qboolean QDECL S_LoadAVSound (sfx_t *s, qbyte *data, size_t datalen, int sndspeed) { struct avaudioctx *ctx; int i; const AVCodec *pCodec; const int iBufSize = 4 * 1024; if (!ffmpeg_audiodecoder) return false; if (!ffmpeg_audiodecoder->ival /* && *ffmpeg_audiodecoder.string */) return false; if (!data || !datalen) return false; //ignore it if it looks like a wav file. that means we don't need to figure out how to calculate loopstart. //FIXME: this also blocks playing the audio from avi files too! if (datalen >= 4 && !strncmp(data, "RIFF", 4)) return false; // if (strcasecmp(COM_GetFileExtension(s->name), "wav")) //don't do .wav - I've no idea how to read the loopstart tag with ffmpeg. // return false; s->decoder.buf = ctx = malloc(sizeof(*ctx) + datalen); if (!ctx) return false; //o.O memset(ctx, 0, sizeof(*ctx)); // Create internal io buffer for FFmpeg ctx->filedata = data; //defer that copy ctx->filesize = datalen; //defer that copy ctx->pFormatCtx = avformat_alloc_context(); ctx->pFormatCtx->pb = avio_alloc_context(av_malloc(iBufSize), iBufSize, 0, ctx, AVIO_Mem_Read, 0, AVIO_Mem_Seek); // Open file if(avformat_open_input(&ctx->pFormatCtx, s->name, NULL, NULL)==0) { // Retrieve stream information if(avformat_find_stream_info(ctx->pFormatCtx, NULL)>=0) { ctx->audioStream=-1; for(i=0; ipFormatCtx->nb_streams; i++) #if LIBAVFORMAT_VERSION_MAJOR >= 57 if(ctx->pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO) #else if(ctx->pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) #endif { ctx->audioStream=i; break; } if(ctx->audioStream!=-1) { #if LIBAVFORMAT_VERSION_MAJOR >= 57 pCodec=avcodec_find_decoder(ctx->pFormatCtx->streams[ctx->audioStream]->codecpar->codec_id); ctx->pACodecCtx = avcodec_alloc_context3(pCodec); if (avcodec_parameters_to_context(ctx->pACodecCtx, ctx->pFormatCtx->streams[ctx->audioStream]->codecpar) < 0) { avcodec_free_context(&ctx->pACodecCtx); pCodec = NULL; } #else ctx->pACodecCtx=ctx->pFormatCtx->streams[ctx->audioStream]->codec; pCodec=avcodec_find_decoder(ctx->pACodecCtx->codec_id); #endif ctx->pAFrame=av_frame_alloc(); if(pCodec!=NULL && ctx->pAFrame && avcodec_open2(ctx->pACodecCtx, pCodec, NULL) >= 0) { //success } else ctx->audioStream = -1; } } if (ctx->audioStream != -1) { //sucky copy ctx->filedata = (uint8_t*)(ctx+1); memcpy(ctx->filedata, data, datalen); s->decoder.ended = S_AV_Purge; s->decoder.purge = S_AV_Purge; s->decoder.decodedata = S_AV_Locate; s->decoder.querydata = S_AV_Query; activedecoders++; return true; } } S_AV_Purge(s); return false; } qboolean AVAudio_MayUnload(void) { return activedecoders==0; } static qboolean AVAudio_Init(void) { if (!plugfuncs->ExportFunction("MayUnload", AVAudio_MayUnload) || !plugfuncs->ExportFunction("S_LoadSound", S_LoadAVSound)) { Con_Printf("ffmpeg: Engine doesn't support audio decoder plugins\n"); return false; } ffmpeg_audiodecoder = cvarfuncs->GetNVFDG("ffmpeg_audiodecoder_wip", "1", 0, "Enables the use of ffmpeg's decoder for pure audio files.", "ffmpeg"); if (!ffmpeg_audiodecoder->ival) Con_Printf("ffmpeg: audio decoding disabled, use \"set %s 1\" to enable ffmpeg audio decoding\n", ffmpeg_audiodecoder->name); return true; } //generic module stuff. this has to go somewhere. static void AVLogCallback(void *avcl, int level, const char *fmt, va_list vl) { //needs to be reenterant #ifdef _DEBUG char string[1024]; if (level >= AV_LOG_INFO) return; //don't care if its just going to be spam. Q_vsnprintf (string, sizeof(string), fmt, vl); if (level >= AV_LOG_WARNING) { if (pdeveloper && pdeveloper->ival) Con_Printf("ffmpeg: %s", string); } else if (level >= AV_LOG_ERROR) Con_Printf(CON_WARNING"ffmpeg: %s", string); else Con_Printf(CON_ERROR"ffmpeg: %s", string); #endif } //get the encoder/decoders to register themselves with the engine, then make sure avformat/avcodec have registered all they have to give. qboolean AVEnc_Init(void); qboolean AVDec_Init(void); qboolean Plug_Init(void) { qboolean okay = false; okay |= AVAudio_Init(); okay |= AVDec_Init(); okay |= AVEnc_Init(); if (okay) { #if ( LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100) ) av_register_all(); avcodec_register_all(); #endif pdeveloper = cvarfuncs->GetNVFDG("developer", "0", 0, "Developer spam.", "ffmpeg"); av_log_set_level(AV_LOG_WARNING); av_log_set_callback(AVLogCallback); } return okay; } ================================================ FILE: plugins/avplug/avdecode.c ================================================ #include "../plugin.h" #include "../engine.h" static plugfsfuncs_t *filefuncs; static plugaudiofuncs_t *audiofuncs; #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include "libavutil/imgutils.h" #define TARGET_FFMPEG (LIBAVFORMAT_VERSION_MICRO >= 100) #define HAVE_DECOUPLED_API (LIBAVCODEC_VERSION_MAJOR>57 || (LIBAVCODEC_VERSION_MAJOR==57&&LIBAVCODEC_VERSION_MINOR>=36)) //between av 52.31 and 54.35, lots of constants etc got renamed to gain an extra AV_ prefix. /* #define AV_PIX_FMT_BGRA PIX_FMT_BGRA #define AVMEDIA_TYPE_AUDIO CODEC_TYPE_AUDIO #define AVMEDIA_TYPE_VIDEO CODEC_TYPE_VIDEO #define AV_PIX_FMT_BGRA PIX_FMT_BGRA #define AV_SAMPLE_FMT_U8 SAMPLE_FMT_U8 #define AV_SAMPLE_FMT_S16 SAMPLE_FMT_S16 #define AV_SAMPLE_FMT_FLT SAMPLE_FMT_FLT #define AVIOContext ByteIOContext #define avio_alloc_context av_alloc_put_byte */ #define DECODERNAME "ffmpeg" /*should probably try threading this, though I suppose it should be the engine doing that.*/ /*timing is based upon the start time. this means overflow issues with rtsp etc*/ struct decctx { unsigned int width, height; vfsfile_t *file; int64_t fileofs; int64_t filelen; AVFormatContext *pFormatCtx; int audioStream; AVCodecContext *pACodecCtx; AVFrame *pAFrame; int videoStream; AVCodecContext *pVCodecCtx; AVFrame *pVFrame; int64_t num, denum; int64_t lasttime; int64_t timeoffset; //timestamp of first video frame uint8_t *rgb_data; int rgb_linesize; struct SwsContext *pScaleCtx; }; static qboolean AVDec_SetSize (void *vctx, int width, int height) { struct decctx *ctx = (struct decctx*)vctx; uint8_t *rgb_data[4]; //av_image_alloc requires at least 4 entries for certain pix formats (libav (but not ffmpeg) zero-fills, so this is important). int rgb_linesize[4]; //colourspace conversions will be fastest if we // if (width > ctx->pCodecCtx->width) width = ctx->pVCodecCtx->width; // if (height > ctx->pCodecCtx->height) height = ctx->pVCodecCtx->height; //is this a no-op? if (width == ctx->width && height == ctx->height && ctx->pScaleCtx) return true; if (av_image_alloc(rgb_data, rgb_linesize, width, height, AV_PIX_FMT_BGRA, 1) >= 0) { //update the scale context as required //clear the old stuff out av_free(ctx->rgb_data); ctx->width = width; ctx->height = height; ctx->rgb_data = rgb_data[0]; ctx->rgb_linesize = rgb_linesize[0]; return qtrue; } return qfalse; //unsupported } static int AVIO_Read(void *opaque, uint8_t *buf, int buf_size) { struct decctx *ctx = opaque; int ammount; ammount = VFS_READ(ctx->file, buf, buf_size); if (ammount > 0) ctx->fileofs += ammount; return ammount; } static int64_t AVIO_Seek(void *opaque, int64_t offset, int whence) { struct decctx *ctx = opaque; whence &= ~AVSEEK_FORCE; switch(whence) { default: return -1; case SEEK_SET: ctx->fileofs = offset; break; case SEEK_CUR: ctx->fileofs += offset; break; case SEEK_END: ctx->fileofs = ctx->filelen + offset; break; case AVSEEK_SIZE: return ctx->filelen; } VFS_SEEK(ctx->file, ctx->fileofs); return ctx->fileofs; } static void AVDec_Destroy(void *vctx) { struct decctx *ctx = (struct decctx*)vctx; // Free the video stuff av_free(ctx->rgb_data); if (ctx->pVCodecCtx) avcodec_free_context(&ctx->pVCodecCtx); av_free(ctx->pVFrame); // Free the audio decoder if (ctx->pACodecCtx) avcodec_free_context(&ctx->pACodecCtx); av_free(ctx->pAFrame); // Close the video file avformat_close_input(&ctx->pFormatCtx); if (ctx->file) VFS_CLOSE(ctx->file); free(ctx); } static void *AVDec_Create(const char *medianame) { struct decctx *ctx; unsigned int i; const AVCodec *pCodec; qboolean useioctx = false; // const char *extension = strrchr(medianame, '.'); /*always respond to av: media prefixes*/ if (!strncmp(medianame, "av:", 3) || !strncmp(medianame, "ff:", 3)) { medianame = medianame + 3; useioctx = true; } else if (!strncmp(medianame, "avs:", 4) || !strncmp(medianame, "ffs:", 4)) { medianame = medianame + 4; //let avformat do its own avio context stuff } else if (strchr(medianame, ':')) //block other types of url/prefix. return NULL; // else if (!strcasecmp(extension, ".roq") || !strcasecmp(extension, ".roq") || !strcasecmp(extension, ".cin")) // return NULL; //roq+cin should be played back via the engine instead... else useioctx = true; ctx = malloc(sizeof(*ctx)); memset(ctx, 0, sizeof(*ctx)); ctx->lasttime = -1; ctx->file = NULL; if (useioctx) { // Create internal Buffer for FFmpeg: const int iBufSize = 32 * 1024; char *pBuffer = av_malloc(iBufSize); AVIOContext *ioctx; ctx->file = filefuncs->OpenVFS(medianame, "rb", FS_GAME); if (!ctx->file) ctx->file = filefuncs->OpenVFS(va("video/%s", medianame), "rb", FS_GAME); if (!ctx->file) { Con_Printf("Unable to open %s\n", medianame); free(ctx); av_free(pBuffer); return NULL; } ctx->filelen = VFS_GETLEN(ctx->file); ioctx = avio_alloc_context(pBuffer, iBufSize, 0, ctx, AVIO_Read, 0, AVIO_Seek); ctx->pFormatCtx = avformat_alloc_context(); ctx->pFormatCtx->pb = ioctx; } /* small how-to note for if I ever try to add support for voice-and-video rtp decoding. this stuff is presumably needed to handle ICE+stun+ports etc. I prolly need to hack around with adding rtcp too. :s rtsp: Add support for depacketizing RTP data via custom IO To use this, set sdpflags=custom_io to the sdp demuxer. During the avformat_open_input call, the SDP is read from the AVFormatContext AVIOContext (ctx->pb) - after the avformat_open_input call, during the av_read_frame() calls, the same ctx->pb is used for reading packets (and sending back RTCP RR packets). Normally, one would use this with a read-only AVIOContext for the SDP during the avformat_open_input call, then close that one and replace it with a read-write one for the packets after the avformat_open_input call has returned. This allows using the RTP depacketizers as "pure" demuxers, without having them tied to the libavformat network IO. */ // Open video file if(avformat_open_input(&ctx->pFormatCtx, medianame, NULL, NULL)==0) { // Retrieve stream information if(avformat_find_stream_info(ctx->pFormatCtx, NULL)>=0) { ctx->audioStream=-1; for(i=0; ipFormatCtx->nb_streams && ctx->audioStream==-1; i++) { #if LIBAVFORMAT_VERSION_MAJOR >= 57 if(ctx->pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO) #else if(ctx->pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) #endif ctx->audioStream=i; } if(ctx->audioStream!=-1) { #if LIBAVFORMAT_VERSION_MAJOR >= 57 pCodec=avcodec_find_decoder(ctx->pFormatCtx->streams[ctx->audioStream]->codecpar->codec_id); ctx->pACodecCtx = avcodec_alloc_context3(pCodec); if (avcodec_parameters_to_context(ctx->pACodecCtx, ctx->pFormatCtx->streams[ctx->audioStream]->codecpar) < 0) { avcodec_free_context(&ctx->pACodecCtx); pCodec = NULL; } #else ctx->pACodecCtx=ctx->pFormatCtx->streams[ctx->audioStream]->codec; pCodec=avcodec_find_decoder(ctx->pACodecCtx->codec_id); #endif ctx->pAFrame=av_frame_alloc(); if(pCodec!=NULL && ctx->pAFrame && avcodec_open2(ctx->pACodecCtx, pCodec, NULL) >= 0) { } else ctx->audioStream = -1; } ctx->videoStream=-1; for(i=0; ipFormatCtx->nb_streams && ctx->videoStream==-1; i++) { #if LIBAVFORMAT_VERSION_MAJOR >= 57 if(ctx->pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) #else if(ctx->pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) #endif ctx->videoStream=i; } if(ctx->videoStream!=-1) { #if LIBAVFORMAT_VERSION_MAJOR >= 57 pCodec=avcodec_find_decoder(ctx->pFormatCtx->streams[ctx->videoStream]->codecpar->codec_id); ctx->pVCodecCtx = avcodec_alloc_context3(pCodec); if (avcodec_parameters_to_context(ctx->pVCodecCtx, ctx->pFormatCtx->streams[ctx->videoStream]->codecpar) < 0) { avcodec_free_context(&ctx->pVCodecCtx); pCodec = NULL; } #else ctx->pVCodecCtx=ctx->pFormatCtx->streams[ctx->videoStream]->codec; pCodec=avcodec_find_decoder(ctx->pVCodecCtx->codec_id); #endif ctx->num = ctx->pFormatCtx->streams[ctx->videoStream]->time_base.num; ctx->denum = ctx->pFormatCtx->streams[ctx->videoStream]->time_base.den; if (ctx->pFormatCtx->streams[ctx->videoStream]->start_time != AV_NOPTS_VALUE) ctx->timeoffset = ctx->pFormatCtx->streams[ctx->videoStream]->start_time; else ctx->timeoffset = 0; //should probably guess. // Open codec if(pCodec!=NULL && avcodec_open2(ctx->pVCodecCtx, pCodec, NULL) >= 0) { // Allocate video frame ctx->pVFrame=av_frame_alloc(); if(ctx->pVFrame!=NULL) { if (AVDec_SetSize(ctx, ctx->pVCodecCtx->width, ctx->pVCodecCtx->height)) { return ctx; } } } } } } AVDec_Destroy(ctx); return NULL; } static qboolean VARGS AVDec_DisplayFrame(void *vctx, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ectx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ectx) { struct decctx *ctx = (struct decctx*)vctx; AVPacket packet; #if HAVE_DECOUPLED_API #else int frameFinished; #endif qboolean repainted = false; int64_t curtime; curtime = (mediatime * ctx->denum) / ctx->num; nosound |= !audiofuncs; while (1) { if (ctx->lasttime >= curtime) break; // We're ahead of the previous frame. try and read the next. //FIXME: when streaming, av_read_frame will _block_ and that sucks big hairy donkey balls. if (av_read_frame(ctx->pFormatCtx, &packet) < 0) { if (repainted) break; return false; } #if HAVE_DECOUPLED_API if(packet.stream_index==ctx->videoStream) { avcodec_send_packet(ctx->pVCodecCtx, &packet); while(0==avcodec_receive_frame(ctx->pVCodecCtx, ctx->pVFrame)) { //rescale+convert it to what we're rendering (no more yuv) ctx->pScaleCtx = sws_getCachedContext(ctx->pScaleCtx, ctx->pVCodecCtx->width, ctx->pVCodecCtx->height, ctx->pVCodecCtx->pix_fmt, ctx->width, ctx->height, AV_PIX_FMT_BGRA, SWS_POINT, 0, 0, 0); sws_scale(ctx->pScaleCtx, (void*)ctx->pVFrame->data, ctx->pVFrame->linesize, 0, ctx->pVCodecCtx->height, &ctx->rgb_data, &ctx->rgb_linesize); ctx->lasttime = ctx->pVFrame->best_effort_timestamp-ctx->timeoffset; if (!ctx->file && curtime - ctx->lasttime > (1 * ctx->denum) / ctx->num) { ctx->timeoffset = ctx->pVFrame->best_effort_timestamp-curtime; ctx->lasttime = curtime; } repainted = true; } } else if(packet.stream_index==ctx->audioStream && !nosound) { avcodec_send_packet(ctx->pACodecCtx, &packet); while(0==avcodec_receive_frame(ctx->pACodecCtx, ctx->pAFrame)) { int width = 2; int channels = ctx->pACodecCtx->ch_layout.nb_channels; unsigned int auddatasize = av_samples_get_buffer_size(NULL, ctx->pACodecCtx->ch_layout.nb_channels, ctx->pAFrame->nb_samples, ctx->pACodecCtx->sample_fmt, 1); void *auddata = ctx->pAFrame->data[0]; switch(ctx->pACodecCtx->sample_fmt) { default: auddatasize = 0; break; case AV_SAMPLE_FMT_U8P: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_U8: width = 1; break; case AV_SAMPLE_FMT_S16P: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_S16: width = 2; break; case AV_SAMPLE_FMT_FLTP: if (channels == 2) { #ifdef MIXER_F32 float *l = (float*)ctx->pAFrame->data[0], *r = (float*)ctx->pAFrame->data[1], *t; unsigned int i; unsigned int frames = ctx->pAFrame->nb_samples; width = sizeof(*t); auddatasize = frames*width*channels; t = malloc(auddatasize); for (i = 0; i < frames; i++) { t[2*i+0] = l[i]; t[2*i+1] = r[i]; } audiofuncs->RawAudio(-1, t, ctx->pACodecCtx->sample_rate, auddatasize/(channels*width), channels, width, 1); free(t); continue; #else //note that we can only reformat in place because we are NOT outputting floats. float *in[2] = {(float*)ctx->pAFrame->data[0],(float*)ctx->pAFrame->data[1]}; signed short *out = (void*)auddata; int v; unsigned int i, c; unsigned int frames = ctx->pAFrame->nb_samples; for (i = 0; i < frames; i++) { for (c = 0; c < 2; c++) { v = (short)(in[c][i]*32767); if (v < -32767) v = -32767; else if (v > 32767) v = 32767; *out++ = v; } } width = sizeof(*out); auddatasize = frames*width*channels; break; #endif } auddatasize /= channels; channels = 1; //fallthrough, using just the first channel as mono case AV_SAMPLE_FMT_FLT: #ifdef MIXER_F32 width = 4; #else { float *in = (void*)auddata; signed short *out = (void*)auddata; int v; unsigned int i; for (i = 0; i < auddatasize/sizeof(*in); i++) { v = (short)(in[i]*32767); if (v < -32767) v = -32767; else if (v > 32767) v = 32767; out[i] = v; } auddatasize/=2; width = 2; } #endif break; case AV_SAMPLE_FMT_DBLP: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_DBL: { double *in = (double*)auddata; signed short *out = (void*)auddata; int v; unsigned int i; for (i = 0; i < auddatasize/sizeof(*in); i++) { v = (short)(in[i]*32767); if (v < -32767) v = -32767; else if (v > 32767) v = 32767; out[i] = v; } auddatasize/=4; width = 2; } break; } audiofuncs->RawAudio(-1, auddata, ctx->pACodecCtx->sample_rate, auddatasize/(channels*width), channels, width, 1); } } av_packet_unref(&packet); #else // Is this a packet from the video stream? if(packet.stream_index==ctx->videoStream) { // Decode video frame avcodec_decode_video2(ctx->pVCodecCtx, ctx->pVFrame, &frameFinished, &packet); // Did we get a video frame? if(frameFinished) { ctx->pScaleCtx = sws_getCachedContext(ctx->pScaleCtx, ctx->pVCodecCtx->width, ctx->pVCodecCtx->height, ctx->pVCodecCtx->pix_fmt, ctx->width, ctx->height, AV_PIX_FMT_BGRA, SWS_POINT, 0, 0, 0); // Convert the image from its native format to RGB sws_scale(ctx->pScaleCtx, (void*)ctx->pVFrame->data, ctx->pVFrame->linesize, 0, ctx->pVCodecCtx->height, &ctx->rgb_data, &ctx->rgb_linesize); repainted = true; } #if TARGET_FFMPEG ctx->lasttime = av_frame_get_best_effort_timestamp(ctx->pVFrame); #else if(frameFinished) { if (ctx->pVFrame->pkt_pts != AV_NOPTS_VALUE) ctx->lasttime = ctx->pVFrame->pkt_pts; else ctx->lasttime = ctx->pVFrame->pkt_dts; } #endif } else if(packet.stream_index==ctx->audioStream && !nosound) { int okay; int len; void *odata = packet.data; while (packet.size > 0) { okay = false; len = avcodec_decode_audio4(ctx->pACodecCtx, ctx->pAFrame, &okay, &packet); if (len < 0) break; packet.size -= len; packet.data += len; if (okay) { int width = 2; int channels = ctx->pACodecCtx->ch_layout.nb_channels; unsigned int auddatasize = av_samples_get_buffer_size(NULL, ctx->pACodecCtx->ch_layout.nb_channels, ctx->pAFrame->nb_samples, ctx->pACodecCtx->sample_fmt, 1); void *auddata = ctx->pAFrame->data[0]; switch(ctx->pACodecCtx->sample_fmt) { default: auddatasize = 0; break; case AV_SAMPLE_FMT_U8P: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_U8: width = 1; break; case AV_SAMPLE_FMT_S16P: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_S16: width = 2; break; case AV_SAMPLE_FMT_FLTP: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_FLT: //FIXME: support float audio internally. { float *in = (void*)auddata; signed short *out = (void*)auddata; int v; unsigned int i; for (i = 0; i < auddatasize/sizeof(*in); i++) { v = (short)(in[i]*32767); if (v < -32767) v = -32767; else if (v > 32767) v = 32767; out[i] = v; } auddatasize/=2; width = 2; } case AV_SAMPLE_FMT_DBLP: auddatasize /= channels; channels = 1; case AV_SAMPLE_FMT_DBL: { double *in = (double*)auddata; signed short *out = (void*)auddata; int v; unsigned int i; for (i = 0; i < auddatasize/sizeof(*in); i++) { v = (short)(in[i]*32767); if (v < -32767) v = -32767; else if (v > 32767) v = 32767; out[i] = v; } auddatasize/=4; width = 2; } break; } audiofuncs->RawAudio(-1, auddata, ctx->pACodecCtx->sample_rate, auddatasize/(channels*width), channels, width, 1); } } packet.data = odata; } // Free the packet that was allocated by av_read_frame av_packet_unref(&packet); #endif } if (forcevideo || repainted) uploadtexture(ectx, TF_BGRA32, ctx->width, ctx->height, ctx->rgb_data, NULL); return true; } static void AVDec_GetSize (void *vctx, int *width, int *height) { struct decctx *ctx = (struct decctx*)vctx; *width = ctx->width; *height = ctx->height; } /*static void AVDec_CursorMove (void *vctx, float posx, float posy) { //its a video, dumbass } static void AVDec_Key (void *vctx, int code, int unicode, int isup) { //its a video, dumbass } static void AVDec_ChangeStream(void *vctx, char *newstream) { } */ static void AVDec_Rewind(void *vctx) { struct decctx *ctx = (struct decctx*)vctx; if (ctx->lasttime != -1) { av_seek_frame(ctx->pFormatCtx, -1, 0, AVSEEK_FLAG_FRAME|AVSEEK_FLAG_BACKWARD); avcodec_flush_buffers(ctx->pVCodecCtx); } ctx->lasttime = -1; } /* //avcodec has no way to shut down properly. static qboolean AVDec_Shutdown(void) { return 0; } */ static media_decoder_funcs_t decoderfuncs = { sizeof(media_decoder_funcs_t), DECODERNAME, AVDec_Create, AVDec_DisplayFrame, AVDec_Destroy, AVDec_Rewind, NULL,//AVDec_CursorMove, NULL,//AVDec_Key, NULL,//AVDec_SetSize, AVDec_GetSize, NULL,//AVDec_ChangeStream }; qboolean AVDec_Init(void) { filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs)); audiofuncs = plugfuncs->GetEngineInterface(plugaudiofuncs_name, sizeof(*audiofuncs)); if (!filefuncs || !plugfuncs->ExportInterface("Media_VideoDecoder", &decoderfuncs, sizeof(decoderfuncs))) { Con_Printf(DECODERNAME": Engine doesn't support media decoder plugins\n"); return false; } return true; } ================================================ FILE: plugins/avplug/avencode.c ================================================ #include "../plugin.h" #include "../engine.h" #include "libavformat/avformat.h" //#include "libavformat/avio.h" #include "libavcodec/avcodec.h" #include "libswscale/swscale.h" #include "libavutil/imgutils.h" #include "libavutil/opt.h" #include "libavutil/channel_layout.h" #define TARGET_FFMPEG (LIBAVFORMAT_VERSION_MICRO >= 100) #if TARGET_FFMPEG #define ENCODERNAME "ffmpeg" #else #define ENCODERNAME "libav" #endif #define HAVE_DECOUPLED_API (LIBAVCODEC_VERSION_MAJOR>57 || (LIBAVCODEC_VERSION_MAJOR==57&&LIBAVCODEC_VERSION_MINOR>=36)) //crappy compat crap #ifndef AV_CODEC_FLAG_GLOBAL_HEADER #define AV_CODEC_FLAG_GLOBAL_HEADER CODEC_FLAG_GLOBAL_HEADER #endif #ifndef AV_ERROR_MAX_STRING_SIZE #define AV_ERROR_MAX_STRING_SIZE 64 #endif static plugfsfuncs_t *filefuncs; /* Most of the logic in here came from here: http://svn.gnumonks.org/tags/21c3-video/upstream/ffmpeg-0.4.9-pre1/output_example.c */ static cvar_t *ffmpeg_format_force; static cvar_t *ffmpeg_videocodec; static cvar_t *ffmpeg_videobitrate; static cvar_t *ffmpeg_videoforcewidth; static cvar_t *ffmpeg_videoforceheight; static cvar_t *ffmpeg_videopreset; static cvar_t *ffmpeg_video_crf; static cvar_t *ffmpeg_audiocodec; static cvar_t *ffmpeg_audiobitrate; struct encctx { char abspath[MAX_OSPATH]; AVFormatContext *fc; qboolean doneheaders; AVCodecContext *video_codec; AVStream *video_st; struct SwsContext *scale_ctx; AVFrame *picture; uint8_t *video_outbuf; int video_outbuf_size; AVCodecContext *audio_codec; AVStream *audio_st; AVFrame *audio; uint8_t *audio_outbuf; uint32_t audio_outcount; int64_t audio_pts; }; #define VARIABLE_AUDIO_FRAME_MIN_SIZE 512 //audio frames smaller than a certain size are just wasteful #define VARIABLE_AUDIO_FRAME_MAX_SIZE 1024 #if !TARGET_FFMPEG #define av_make_error_string qav_make_error_string static inline char *av_make_error_string(char *errbuf, size_t errbuf_size, int errnum) { av_strerror(errnum, errbuf, errbuf_size); return errbuf; } #endif static void AVEnc_End (void *ctx); static AVFrame *alloc_frame(enum AVPixelFormat pix_fmt, int width, int height) { AVFrame *picture; uint8_t *picture_buf; int size; picture = av_frame_alloc(); if(!picture) return NULL; #if TARGET_FFMPEG size = av_image_get_buffer_size(pix_fmt, width, height, 1); #else size = avpicture_get_size(pix_fmt, width, height); #endif picture_buf = (uint8_t*)(av_malloc(size)); if (!picture_buf) { av_free(picture); return NULL; } #if TARGET_FFMPEG av_image_fill_arrays(picture->data, picture->linesize, picture_buf, pix_fmt, width, height, 1/*fixme: align*/); #else avpicture_fill((AVPicture*)picture, picture_buf, pix_fmt, width, height); #endif picture->width = width; picture->height = height; return picture; } static AVStream *add_video_stream(struct encctx *ctx, const AVCodec *codec, int fps, int width, int height) { AVCodecContext *c; AVStream *st; int bitrate = ffmpeg_videobitrate->value; int forcewidth = ffmpeg_videoforcewidth->value; int forceheight = ffmpeg_videoforceheight->value; #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101) st = avformat_new_stream(ctx->fc, NULL); if (!st) return NULL; st->id = ctx->fc->nb_streams-1; c = avcodec_alloc_context3(codec); #else st = avformat_new_stream(ctx->fc, codec); if (!st) return NULL; c = st->codec; st->id = ctx->fc->nb_streams-1; #endif ctx->video_codec = c; c->codec_id = codec->id; c->codec_type = codec->type; /* put sample parameters */ if (bitrate) c->bit_rate = bitrate; // c->rc_max_rate = bitrate; // c->rc_min_rate = bitrate; /* resolution must be a multiple of two */ c->width = forcewidth?forcewidth:width; c->height = forceheight?forceheight:height; /* frames per second */ c->time_base.num = 1; c->time_base.den = fps; //c->gop_size = 12; /* emit one intra frame every twelve frames at most */ c->pix_fmt = AV_PIX_FMT_YUV420P; if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO) { /* just for testing, we also add B frames */ c->max_b_frames = 2; } if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO) { /* needed to avoid using macroblocks in which some coeffs overflow this doesnt happen with normal video, it just happens here as the motion of the chroma plane doesnt match the luma plane */ // c->mb_decision=2; } // some formats want stream headers to be seperate if (ctx->fc->oformat->flags & AVFMT_GLOBALHEADER) c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; if (*ffmpeg_videopreset->string) av_opt_set(c->priv_data, "preset", ffmpeg_videopreset->string, AV_OPT_SEARCH_CHILDREN); if (*ffmpeg_video_crf->string) av_opt_set(c->priv_data, "crf", ffmpeg_video_crf->string, AV_OPT_SEARCH_CHILDREN); return st; } static void close_video(struct encctx *ctx) { if (!ctx->video_st) return; avcodec_free_context(&ctx->video_codec); if (ctx->picture) { av_free(ctx->picture->data[0]); av_free(ctx->picture); } av_free(ctx->video_outbuf); } #if HAVE_DECOUPLED_API //frame can be null on eof. static void AVEnc_DoEncode(AVFormatContext *fc, AVStream *stream, AVCodecContext *codec, AVFrame *frame) { AVPacket *pkt = av_packet_alloc(); int err = avcodec_send_frame(codec, frame); if (err) { char buf[512]; Con_Printf("avcodec_send_frame: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } while (!(err=avcodec_receive_packet(codec, pkt))) { av_packet_rescale_ts(pkt, codec->time_base, stream->time_base); pkt->stream_index = stream->index; err = av_interleaved_write_frame(fc, pkt); if (err) { char buf[512]; Con_Printf("av_interleaved_write_frame: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } av_packet_unref(pkt); } if (err && err != AVERROR(EAGAIN) && err != AVERROR_EOF) { char buf[512]; Con_Printf("avcodec_receive_packet: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } av_packet_free(&pkt); } #endif static void AVEnc_Video (void *vctx, int frameno, void *data, int bytestride, int width, int height, enum uploadfmt qpfmt) { struct encctx *ctx = vctx; const uint8_t *srcslices[4]; int srcstride[4]; int avpfmt; if (!ctx->video_st) return; switch(qpfmt) { case TF_BGRA32: avpfmt = AV_PIX_FMT_BGRA; break; case TF_RGBA32: avpfmt = AV_PIX_FMT_RGBA; break; #if TARGET_FFMPEG case TF_BGRX32: avpfmt = AV_PIX_FMT_BGR0; break; case TF_RGBX32: avpfmt = AV_PIX_FMT_RGB0; break; #endif case TF_BGR24: avpfmt = AV_PIX_FMT_BGR24; break; case TF_RGB24: avpfmt = AV_PIX_FMT_RGB24; break; default: return; } if (bytestride < 0) //fix up the buffers so callers don't have to. data = (char*)data - bytestride*(height-1); //weird maths to flip it. srcslices[0] = (uint8_t*)data; srcstride[0] = bytestride; srcslices[1] = NULL; srcstride[1] = 0; srcslices[2] = NULL; //libav's version probably needs this excess srcstride[2] = 0; srcslices[3] = NULL; srcstride[3] = 0; //fixme: it would be nice to avoid copies here if possible... //convert RGB to whatever the codec needs (ie: yuv...). //also rescales, but only if the user resizes the video while recording. which is a stupid thing to do. ctx->scale_ctx = sws_getCachedContext(ctx->scale_ctx, width, height, avpfmt, ctx->picture->width, ctx->picture->height, ctx->video_codec->pix_fmt, SWS_POINT, 0, 0, 0); sws_scale(ctx->scale_ctx, srcslices, srcstride, 0, height, ctx->picture->data, ctx->picture->linesize); ctx->picture->pts = frameno; ctx->picture->format = ctx->video_codec->pix_fmt; #if HAVE_DECOUPLED_API AVEnc_DoEncode(ctx->fc, ctx->video_st, ctx->video_codec, ctx->picture); #else { int success; int err; AVPacket *pkt = av_packet_alloc(); pkt->data = ctx->video_outbuf; pkt->size = ctx->video_outbuf_size; success = 0; err = avcodec_encode_video2(ctx->video_codec, pkt, ctx->picture, &success); if (err) { char buf[512]; Con_Printf("avcodec_encode_video2: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } else if (err == 0 && success) { av_packet_rescale_ts(pkt, ctx->video_codec->time_base, ctx->video_st->time_base); pkt->stream_index = ctx->video_st->index; err = av_interleaved_write_frame(ctx->fc, pkt); if (err) { char buf[512]; Con_Printf("av_interleaved_write_frame: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } } av_packet_free(&pkt); } #endif } static AVStream *add_audio_stream(struct encctx *ctx, const AVCodec *codec, int *samplerate, int *bits, int channels) { AVCodecContext *c; AVStream *st; int bitrate = ffmpeg_audiobitrate->value; int num_sample_fmts = 0; enum AVSampleFormat *sample_fmts; #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 48, 101) st = avformat_new_stream(ctx->fc, codec); if (!st) return NULL; c = st->codec; #else st = avformat_new_stream(ctx->fc, NULL); if (!st) return NULL; c = avcodec_alloc_context3(codec); if(avcodec_parameters_to_context(c, st->codecpar)) return NULL; #endif st->id = ctx->fc->nb_streams-1; ctx->audio_codec = c; c->codec_id = codec->id; c->codec_type = codec->type; // if (c->codec_id == AV_CODEC_ID_OPUS) //opus is strictly 48khz. force that here. // *samplerate = 48000; //FIXME: the engine can't cope with this. /* put sample parameters */ c->bit_rate = bitrate; /* frames per second */ c->time_base.num = 1; c->time_base.den = *samplerate; c->sample_rate = *samplerate; c->ch_layout.nb_channels = channels; av_channel_layout_default(&c->ch_layout, channels); avcodec_get_supported_config(c, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void **)&sample_fmts, &num_sample_fmts); c->sample_fmt = sample_fmts[0]; // if (c->sample_fmt == AV_SAMPLE_FMT_FLTP || c->sample_fmt == AV_SAMPLE_FMT_FLT) // *bits = 32; //get the engine to mix 32bit audio instead of whatever its currently set to. // else if (c->sample_fmt == AV_SAMPLE_FMT_U8P || c->sample_fmt == AV_SAMPLE_FMT_U8) // *bits = 8; //get the engine to mix 32bit audio instead of whatever its currently set to. // else if (c->sample_fmt == AV_SAMPLE_FMT_S16P || c->sample_fmt == AV_SAMPLE_FMT_S16) // *bits = 16; // else *bits = 32; //ask for float audio. c->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; // some formats want stream headers to be seperate if (ctx->fc->oformat->flags & AVFMT_GLOBALHEADER) c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // avcodec_parameters_from_context(st->codecpar, c); return st; } static void close_audio(struct encctx *ctx) { if (!ctx->audio_st) return; avcodec_free_context(&ctx->audio_codec); } static void AVEnc_Audio (void *vctx, void *data, int bytes) { struct encctx *ctx = vctx; if (!ctx->audio_st) return; while (bytes) { int i, p, chans = ctx->audio_codec->ch_layout.nb_channels; int blocksize = sizeof(float)*chans; int count = bytes / blocksize; int planesize = ctx->audio_codec->frame_size; float *in; int offset; if (!planesize) //variable-sized frames. yay { planesize = count; if (count > VARIABLE_AUDIO_FRAME_MAX_SIZE - ctx->audio_outcount) count = VARIABLE_AUDIO_FRAME_MAX_SIZE - ctx->audio_outcount; } else if (count > ctx->audio_codec->frame_size - ctx->audio_outcount) count = ctx->audio_codec->frame_size - ctx->audio_outcount; in = (float*)data; offset = ctx->audio_outcount; ctx->audio_outcount += count; data = (qbyte*)data + count * blocksize; bytes -= count * blocksize; //input is always float audio, because I'm lazy. //output is whatever the codec needs (may be packed or planar, gah). //the engine's mixer will do all rate scaling for us, as well as channel selection switch(ctx->audio_codec->sample_fmt) { case AV_SAMPLE_FMT_DBL: offset *= chans; count *= chans; planesize *= chans; chans = 1; case AV_SAMPLE_FMT_DBLP: for (p = 0; p < chans; p++) { double *f = (double*)ctx->audio_outbuf + p*planesize + offset; for (i = 0; i < count*chans; i+=chans) *f++ = in[i]; in++; } break; case AV_SAMPLE_FMT_FLT: offset *= chans; count *= chans; planesize *= chans; chans = 1; case AV_SAMPLE_FMT_FLTP: for (p = 0; p < chans; p++) { float *f = (float *)ctx->audio_outbuf + p*planesize + offset; for (i = 0; i < count*chans; i+=chans) *f++ = in[i]; in++; } break; case AV_SAMPLE_FMT_S32: offset *= chans; count *= chans; planesize *= chans; chans = 1; case AV_SAMPLE_FMT_S32P: for (p = 0; p < chans; p++) { int32_t *f = (int32_t *)ctx->audio_outbuf + p*planesize + offset; for (i = 0; i < count*chans; i+=chans) *f++ = bound(0x80000000, (int)(in[i] * (float)0x7fffffff), 0x7fffffff); in++; } break; case AV_SAMPLE_FMT_S16: offset *= chans; count *= chans; planesize *= chans; chans = 1; case AV_SAMPLE_FMT_S16P: for (p = 0; p < chans; p++) { int16_t *f = (int16_t *)ctx->audio_outbuf + p*planesize + offset; for (i = 0; i < count*chans; i+=chans) *f++ = bound(-32768, (int)(in[i] * 32767), 32767); //sin((ctx->audio_pts+ctx->audio_outcount-count+i/chans)*0.1) * 32767;// in++; } break; case AV_SAMPLE_FMT_U8: offset *= chans; count *= chans; planesize *= chans; chans = 1; case AV_SAMPLE_FMT_U8P: for (p = 0; p < chans; p++) { uint8_t *f = (uint8_t*)ctx->audio_outbuf + p*planesize + offset; for (i = 0; i < count*chans; i+=chans) *f++ = bound(0, 128+(int)(in[i] * 127), 255); in++; } break; default: return; } if (ctx->audio_codec->frame_size) { if (ctx->audio_outcount < ctx->audio_codec->frame_size) break; //not enough data yet. } else { if (ctx->audio_outcount < VARIABLE_AUDIO_FRAME_MIN_SIZE) break; //not enough data yet. } ctx->audio->nb_samples = ctx->audio_outcount; avcodec_fill_audio_frame(ctx->audio, ctx->audio_codec->ch_layout.nb_channels, ctx->audio_codec->sample_fmt, ctx->audio_outbuf, av_get_bytes_per_sample(ctx->audio_codec->sample_fmt)*ctx->audio_outcount*ctx->audio_codec->ch_layout.nb_channels, 1); ctx->audio->pts = ctx->audio_pts; ctx->audio_pts += ctx->audio_outcount; ctx->audio_outcount = 0; #if HAVE_DECOUPLED_API AVEnc_DoEncode(ctx->fc, ctx->audio_st, ctx->audio_codec, ctx->audio); #else { int success; int err; AVPacket *pkt = av_packet_alloc(); pkt->data = NULL; pkt->size = 0; success = 0; err = avcodec_encode_audio2(ctx->audio_codec, pkt, ctx->audio, &success); if (err) { char buf[512]; Con_Printf("avcodec_encode_audio2: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } else if (success) { // pkt.pts = ctx->audio_codec->coded_frame->pts; // if(ctx->audio_codec->coded_frame->key_frame) // pkt.flags |= AV_PKT_FLAG_KEY; av_packet_rescale_ts(pkt, ctx->audio_codec->time_base, ctx->audio_st->time_base); pkt->stream_index = ctx->audio_st->index; err = av_interleaved_write_frame(ctx->fc, pkt); if (err) { char buf[512]; Con_Printf("av_interleaved_write_frame: error: %s\n", av_make_error_string(buf, sizeof(buf), err)); } } av_packet_free(&pkt); } #endif } } static void *AVEnc_Begin (char *streamname, int videorate, int width, int height, int *sndkhz, int *sndchannels, int *sndbits) { struct encctx *ctx; const AVOutputFormat *fmt = NULL; const AVCodec *videocodec = NULL; const AVCodec *audiocodec = NULL; int err; char errtxt[AV_ERROR_MAX_STRING_SIZE] = {0}; if (*ffmpeg_format_force->string) { fmt = av_guess_format(ffmpeg_format_force->string, NULL, NULL); if (!fmt) { Con_Printf("Unknown format specified: %s.\n", ffmpeg_format_force->string); return NULL; } } if (!fmt) fmt = av_guess_format(NULL, streamname, NULL); if (!fmt) { Con_DPrintf("Could not deduce output format from file extension: using MPEG.\n"); fmt = av_guess_format("mpeg", NULL, NULL); } if (!fmt) { Con_Printf("Format not known\n"); return NULL; } if (videorate) { if (strcmp(ffmpeg_videocodec->string, "none")) { if (ffmpeg_videocodec->string[0]) { videocodec = avcodec_find_encoder_by_name(ffmpeg_videocodec->string); if (!videocodec) { Con_Printf("Unsupported %s \"%s\"\n", ffmpeg_videocodec->name, ffmpeg_videocodec->string); return NULL; } } if (!videocodec && fmt->video_codec != AV_CODEC_ID_NONE) videocodec = avcodec_find_encoder(fmt->video_codec); } } if (*sndkhz) { if (strcmp(ffmpeg_audiocodec->string, "none")) { if (ffmpeg_audiocodec->string[0]) { audiocodec = avcodec_find_encoder_by_name(ffmpeg_audiocodec->string); if (!audiocodec) { Con_Printf(ENCODERNAME": Unsupported %s \"%s\"\n", ffmpeg_audiocodec->name, ffmpeg_audiocodec->string); return NULL; } } if (!audiocodec && fmt->audio_codec != AV_CODEC_ID_NONE) audiocodec = avcodec_find_encoder(fmt->audio_codec); } } Con_DPrintf(ENCODERNAME": Using format \"%s\"\n", fmt->name); if (videocodec) Con_DPrintf(ENCODERNAME": Using Video Codec \"%s\"\n", videocodec->name); else Con_DPrintf(ENCODERNAME": Not encoding video\n"); if (audiocodec) Con_DPrintf(ENCODERNAME": Using Audio Codec \"%s\"\n", audiocodec->name); else Con_DPrintf(ENCODERNAME": Not encoding audio\n"); if (!videocodec && !audiocodec) { Con_DPrintf(ENCODERNAME": Nothing to encode!\n"); return NULL; } if (!audiocodec) *sndkhz = 0; ctx = malloc(sizeof(*ctx)); if (!ctx) return NULL; memset(ctx, 0, sizeof(*ctx)); ctx->fc = avformat_alloc_context(); ctx->fc->oformat = fmt; #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 6, 100) Q_strncatz(ctx->fc->filename, streamname, sizeof(ctx->fc->filename)); #else ctx->fc->url = av_strdup(streamname); #endif //pick default codecs ctx->video_st = NULL; ctx->audio_st = NULL; if (videocodec) ctx->video_st = add_video_stream(ctx, videocodec, videorate, width, height); if (audiocodec) ctx->audio_st = add_audio_stream(ctx, audiocodec, sndkhz, sndbits, *sndchannels); if (ctx->video_st) { AVCodecContext *c = ctx->video_codec; err = avcodec_open2(c, videocodec, NULL); if (err < 0) { Con_Printf(ENCODERNAME": Could not init codec instance \"%s\" - %s\nMaybe try a different framerate/resolution/bitrate\n", videocodec->name, av_make_error_string(errtxt, sizeof(errtxt), err)); AVEnc_End(ctx); return NULL; } ctx->picture = alloc_frame(c->pix_fmt, c->width, c->height); ctx->video_outbuf_size = 200000; ctx->video_outbuf = av_malloc(ctx->video_outbuf_size); if (!ctx->video_outbuf) ctx->video_outbuf_size = 0; #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101) //copy the avcodec parameters over to avformat err = avcodec_parameters_from_context(ctx->video_st->codecpar, c); if(err < 0) { AVEnc_End(ctx); return NULL; } #endif } if (ctx->audio_st) { int sz; AVCodecContext *c = ctx->audio_codec; err = avcodec_open2(c, audiocodec, NULL); if (err < 0) { Con_Printf(ENCODERNAME": Could not init codec instance \"%s\" - %s\n", audiocodec->name, av_make_error_string(errtxt, sizeof(errtxt), err)); AVEnc_End(ctx); return NULL; } ctx->audio = av_frame_alloc(); sz = ctx->audio_codec->frame_size; if (!sz) sz = VARIABLE_AUDIO_FRAME_MAX_SIZE; sz *= av_get_bytes_per_sample(ctx->audio_codec->sample_fmt) * ctx->audio_codec->ch_layout.nb_channels; ctx->audio_outbuf = av_malloc(sz); #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101) //copy the avcodec parameters over to avformat err = avcodec_parameters_from_context(ctx->audio_st->codecpar, c); if(err < 0) { AVEnc_End(ctx); return NULL; } #endif } av_dump_format(ctx->fc, 0, streamname, 1); if (!(fmt->flags & AVFMT_NOFILE)) { //okay, this is annoying, but I'm too lazy to figure out the issue I was having with avio stuff. if (!filefuncs->NativePath(streamname, FS_GAMEONLY, ctx->abspath, sizeof(ctx->abspath))) { Con_Printf("Couldn't find system path for '%s'\n", streamname); AVEnc_End(ctx); return NULL; } err = avio_open(&ctx->fc->pb, ctx->abspath, AVIO_FLAG_WRITE); if (err < 0) { Con_Printf("Could not open '%s' - %s\n", ctx->abspath, av_make_error_string(errtxt, sizeof(errtxt), err)); AVEnc_End(ctx); return NULL; } } else { strncpy(ctx->abspath, "", sizeof(ctx->abspath)-1); } //different formats have different metadata formats. there's no standards here. //av_dict_set(&ctx->fc- >metadata, "TPFL", "testtest", 0); //FIXME: use ffmpeg's sidedata stuff, which should handle it in a generic way //nearly complete, can make the file dirty now. err = avformat_write_header(ctx->fc, NULL); if (err < 0) { Con_Printf("avformat_write_header(%s): failed %s\n", ctx->abspath, av_make_error_string(errtxt, sizeof(errtxt), err)); if (ctx->video_st) Con_Printf(" Video %s: %i * %i\n", ctx->video_codec->codec->name, width, height); if (ctx->audio_st) Con_Printf(" Audio %s: %i channels, %ibit @ %ikhz\n", ctx->audio_codec->codec->name, *sndchannels, *sndbits, *sndkhz); AVEnc_End(ctx); return NULL; } ctx->doneheaders = true; return ctx; } static void AVEnc_End (void *vctx) { struct encctx *ctx = vctx; unsigned int i; #if HAVE_DECOUPLED_API if (ctx->doneheaders) { //terminate the codecs properly, flushing all unwritten packets if (ctx->video_st) AVEnc_DoEncode(ctx->fc, ctx->video_st, ctx->video_codec, NULL); if (ctx->audio_st) AVEnc_DoEncode(ctx->fc, ctx->audio_st, ctx->audio_codec, NULL); } #endif //don't write trailers if this is an error case and we never even wrote the headers. if (ctx->doneheaders) { av_write_trailer(ctx->fc); if (*ctx->abspath) Con_Printf("Finished writing %s\n", ctx->abspath); } close_video(ctx); close_audio(ctx); for(i = 0; i < ctx->fc->nb_streams; i++) av_freep(&ctx->fc->streams[i]); // if (!(fmt->flags & AVFMT_NOFILE)) avio_close(ctx->fc->pb); av_free(ctx->audio_outbuf); avformat_free_context(ctx->fc); free(ctx); } static media_encoder_funcs_t encoderfuncs = { sizeof(media_encoder_funcs_t), "ffmpeg", "Use ffmpeg's various codecs. Various settings are configured with the "ENCODERNAME"_* cvars.", ".mp4;.*", AVEnc_Begin, AVEnc_Video, AVEnc_Audio, AVEnc_End }; static void AVEnc_Preset_Nvidia_f(void) { cvarfuncs->SetString("capturedriver", ENCODERNAME); //be sure to use our encoder cvarfuncs->SetString(ENCODERNAME"_videocodec", "h264_nvenc"); cvarfuncs->SetString("capturerate", "60"); //we should be able to cope with it, and the default of 30 sucks cvarfuncs->SetString("capturedemowidth", "1920"); //force a specific size, some codecs need multiples of 16 or whatever. cvarfuncs->SetString("capturedemoheight", "1080"); //so this avoids issues with various video codecs. cvarfuncs->SetString("capturesound", "1"); cvarfuncs->SetString("capturesoundchannels", "2"); cvarfuncs->SetString("capturesoundbits", "16"); Con_Printf(ENCODERNAME": now configured for nvidia's hardware encoder\n"); Con_Printf(ENCODERNAME": use ^[/capture foo.mp4^] or ^[/capturedemo foo.mvd foo.mkv^] commands to begin capturing\n"); } void AVEnc_Preset_Defaults_f(void) { //most formats will end up using the x264 encoder or something cvarfuncs->SetString(ENCODERNAME"_format_force", ""); cvarfuncs->SetString(ENCODERNAME"_videocodec", ""); cvarfuncs->SetString(ENCODERNAME"_videobitrate", ""); cvarfuncs->SetString(ENCODERNAME"_videoforcewidth", ""); cvarfuncs->SetString(ENCODERNAME"_videoforceheight", ""); cvarfuncs->SetString(ENCODERNAME"_videopreset", "veryfast"); cvarfuncs->SetString(ENCODERNAME"_video_crf", ""); cvarfuncs->SetString(ENCODERNAME"_audiocodec", ""); cvarfuncs->SetString(ENCODERNAME"_audiobitrate", ""); cvarfuncs->SetString("capturedriver", ENCODERNAME); cvarfuncs->SetString("capturerate", "30"); cvarfuncs->SetString("capturedemowidth", "0"); cvarfuncs->SetString("capturedemoheight", "0"); cvarfuncs->SetString("capturesound", "1"); cvarfuncs->SetString("capturesoundchannels", "2"); cvarfuncs->SetString("capturesoundbits", "16"); Con_Printf(ENCODERNAME": capture settings reset to "ENCODERNAME" defaults\n"); Con_Printf(ENCODERNAME": Note that some codecs may have restrictions on video sizes\n"); } qboolean AVEnc_Init(void) { filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs)); if (!cvarfuncs || !cmdfuncs || !filefuncs || !plugfuncs->ExportInterface("Media_VideoEncoder", &encoderfuncs, sizeof(encoderfuncs))) { Con_Printf(ENCODERNAME": Engine doesn't support media encoder plugins\n"); return false; } ffmpeg_format_force = cvarfuncs->GetNVFDG(ENCODERNAME"_format_force", "", 0, "Forces the output container format. If blank, will guess based upon filename extension.", ENCODERNAME); ffmpeg_videocodec = cvarfuncs->GetNVFDG(ENCODERNAME"_videocodec", "", 0, "Forces which video encoder to use. If blank, guesses based upon container defaults.\nCommon names are libx264 (software), x264_nvenc (hardware accelerated)", ENCODERNAME); ffmpeg_videobitrate = cvarfuncs->GetNVFDG(ENCODERNAME"_videobitrate", "", 0, "Specifies the target video bitrate", ENCODERNAME); ffmpeg_videoforcewidth = cvarfuncs->GetNVFDG(ENCODERNAME"_videoforcewidth", "", 0, "Rescales the input video width. Best to leave blank in order to record the video at the native resolution.", ENCODERNAME); ffmpeg_videoforceheight = cvarfuncs->GetNVFDG(ENCODERNAME"_videoforceheight", "", 0, "Rescales the input video height. Best to leave blank in order to record the video at the native resolution.", ENCODERNAME); ffmpeg_videopreset = cvarfuncs->GetNVFDG(ENCODERNAME"_videopreset", "veryfast", 0, "Specifies which codec preset to use, for codecs that support such presets.", ENCODERNAME); ffmpeg_video_crf = cvarfuncs->GetNVFDG(ENCODERNAME"_video_crf", "", 0, "Specifies the 'Constant Rate Factor' codec setting.\nA value of 0 is 'lossless', a value of 51 is 'worst quality posible', a value of 23 is default.", ENCODERNAME); ffmpeg_audiocodec = cvarfuncs->GetNVFDG(ENCODERNAME"_audiocodec", "", 0, "Forces which audio encoder to use. If blank, guesses based upon container defaults.", ENCODERNAME); ffmpeg_audiobitrate = cvarfuncs->GetNVFDG(ENCODERNAME"_audiobitrate", "", 0, "Specifies the target audio bitrate", ENCODERNAME); // cmdfuncs->AddCommand(ENCODERNAME"_configure", AVEnc_LoadPreset_f); cmdfuncs->AddCommand(ENCODERNAME"_nvidia", AVEnc_Preset_Nvidia_f, "Attempts to reconfigure video capture to use nvidia's hardware encoder."); cmdfuncs->AddCommand(ENCODERNAME"_defaults", AVEnc_Preset_Defaults_f, "Reconfigures video capture to the "ENCODERNAME" plugin's default settings."); //cmdfuncs->AddCommand(ENCODERNAME"_twitch", AVEnc_Preset_Twitch_f, "Reconfigures video capture to stream to twitch."); return true; } ================================================ FILE: plugins/avplug/msvc_libc/inttypes.h ================================================ typedef unsigned char uint8_t; typedef signed char int8_t; typedef unsigned short uint16_t; typedef signed short int16_t; typedef unsigned int uint32_t; typedef signed int int32_t; typedef unsigned long long uint64_t; typedef signed long long int64_t; #define inline _inline ================================================ FILE: plugins/avplug/msvc_libc/stdint.h ================================================ #define UINT64_C(n) n##ull ================================================ FILE: plugins/avplug/readme.txt ================================================ video encoder/decoder using the ffmpeg avformat/avcodec libraries. The video decoder plugs into the media decoder functionality on media with an 'av:' or 'avs:' prefix, specifically: The console command 'playfilm av:foo.mpg' will start playing back $BASEDIR/$GAMEDIR/foo.mpg fullscreen (or from inside paks/pk3s, but make sure seeking is fast, so avoid compression in pk3s...). The console command 'playfilm avs:c:\foo.mpg' will start playing back c:\foo.mpg fullscreen. The shader term 'videomap avs:c:\foo.mpg' will play the video upon the shader. This can be used with csqc+drawpic, csqc+beginpolygon, or placed upon walls. It theoretically supports any file that the avformat/avcodec libraries support, but has no ability to pass arguments, thus playback is likely limited only to files which require no explicit overrides. The video encoder plugs into the existing capture command. Or something. I don't know. Its all basically junk. The container type is guessed by the file name used. avplug_format says which container format to use. If empty, will be guessed based upon file extension. See ffmpeg docs for more info. avplug_videocodec says which video codec to use. If empty, will be guessed based upon the container type ('libx264' for h264 compression). See ffmpeg docs for a full list. avplug_videobitrate says what bitrate to encode at. Default 400000. At the time of writing, audio is not implemented. To check if the plugin is loaded, use the plug_list command. For streaming, you can try this: avplug_format mpegts avplug_videocodec mpeg4 capture udp://PLAYERIP:1234 You can then run VLC or some such app and tell it to listen on port 1234. You might be able to find some other protocol/codec that works better for you. Consult the ffmpeg documentation for that, if you can find something that's actually readable. ================================================ FILE: plugins/berkelium/Makefile ================================================ all: linux32 linux64 linux32: gcc -fvisibility=hidden -shared -fPIC -m32 -lstdc++ -Llib32 -llibberkelium plugapi.cpp ../plugin.c ../qvm_api.c -oberkeliumx86.so -I. linux64: gcc -fvisibility=hidden -shared -fPIC -m64 -lstdc++ -Llib64 -llibberkelium plugapi.cpp ../plugin.c ../qvm_api.c -oberkeliumamd.so -I. ================================================ FILE: plugins/berkelium/plugapi.cpp ================================================ #include "../plugin.h" #include "../engine.h" #include "berkelium/Berkelium.hpp" #include "berkelium/Window.hpp" #include "berkelium/WindowDelegate.hpp" #include "berkelium/Context.hpp" #include qboolean inited; class decctx { public: Berkelium::Window *wnd; int width; int height; unsigned int *buffer; bool repainted; int paintedwidth; int paintedheight; }; class MyDelegate : public Berkelium::WindowDelegate { private: decctx *ctx; virtual void onCrashedWorker(Berkelium::Window *win) { int i; Con_Printf("Berkelium worker crashed\n"); /*black it out*/ for (i = 0; i < ctx->width*ctx->height; i++) { ctx->buffer[i] = 0xff000000; } ctx->repainted = true; } virtual void onCrashed(Berkelium::Window *win) { int i; Con_Printf("Berkelium window crashed\n"); /*black it out*/ for (i = 0; i < ctx->width*ctx->height; i++) { ctx->buffer[i] = 0xff000000; } ctx->repainted = true; } virtual void onUnresponsive(Berkelium::Window *win) { Con_Printf("Berkelium window unresponsive\n"); } virtual void onResponsive(Berkelium::Window *win) { Con_Printf("Berkelium window responsive again, yay\n"); } virtual void onPaint(Berkelium::Window *wini, const unsigned char *bitmap_in, const Berkelium::Rect &bitmap_rect, size_t num_copy_rects, const Berkelium::Rect *copy_rects, int dx, int dy, const Berkelium::Rect& scroll_rect) { int i; // handle paint events... if (dx || dy) { int y, m; int dt = scroll_rect.top(); int dl = scroll_rect.left(); int w = scroll_rect.width(); int h = scroll_rect.height(); int st = dt - dy; int sl = dl - dx; /*bound the output rect*/ if (dt < 0) { st -= dt; h += dt; dt = 0; } if (dl < 0) { sl -= dl; w += dl; dl = 0; } /*bound the source rect*/ if (st < 0) { dt -= st; h += st; st = 0; } if (sl < 0) { dl -= sl; w += sl; sl = 0; } /*bound the width*/ m = (dl>sl)?dl:sl; if (m + w > ctx->width) w = ctx->width - m; m = (dt>st)?dt:st; if (m + h > ctx->height) h = ctx->height - m; if (w > 0 && h > 0) { if (dy > 0) { //if we're moving downwards, we need to write the bottom before the top (so we don't overwrite the data before its copied) for (y = h-1; y >= 0; y--) { memmove(ctx->buffer + (dl + (dt+y)*ctx->width), ctx->buffer + (sl + (st+y)*ctx->width), w*4); } } else { //moving upwards requires we write the top row first for (y = 0; y < h; y++) { memmove(ctx->buffer + (dl + (dt+y)*ctx->width), ctx->buffer + (sl + (st+y)*ctx->width), w*4); } } } } for (i = 0; i < num_copy_rects; i++) { unsigned int *out = ctx->buffer; const unsigned int *in = (const unsigned int*)bitmap_in; int x, y; int t = copy_rects[i].top(); int l = copy_rects[i].left(); int r = copy_rects[i].width() + l; int b = copy_rects[i].height() + t; int w, h; //Clip the rect to the display. This should generally happen anyway, but resizes can be lagged a bit with the whole multi-process/thread thing. //don't need to clip to the bitmap rect, that should be correct. if (l < 0) l = 0; if (t < 0) t = 0; if (r > ctx->width) r = ctx->width; if (b > ctx->height) b = ctx->height; w = r - l; h = b - t; unsigned int instride = bitmap_rect.width() - (w); unsigned int outstride = ctx->width - (w); out += l; out += t * ctx->width; in += (l-bitmap_rect.left()); in += (t-bitmap_rect.top()) * bitmap_rect.width(); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { *out++ = *in++; } in += instride; out += outstride; } } ctx->repainted = true; } public: MyDelegate(decctx *_ctx) : ctx(_ctx) {}; }; static void *Dec_Create(const char *medianame) { /*only respond to berkelium: media prefixes*/ if (!strncmp(medianame, "berkelium:", 10)) medianame = medianame + 10; else if (!strcmp(medianame, "berkelium")) medianame = (char*)"about:blank"; else if (!strncmp(medianame, "http:", 5) || !strncmp(medianame, "https:", 6)) medianame = medianame; //and direct http requests. else return NULL; if (!inited) { //linux lags behind and apparently returns void, so don't bother checking return values on windows, cos I'm lazy. Berkelium::init(Berkelium::FileString::empty()); inited = qtrue; } decctx *ctx = new decctx(); Berkelium::Context* context = Berkelium::Context::create(); ctx->paintedwidth = ctx->width = 1024; ctx->paintedheight = ctx->height = 1024; ctx->repainted = false; ctx->buffer = (unsigned int*)malloc(ctx->width * ctx->height * 4); ctx->wnd = Berkelium::Window::create(context); delete context; ctx->wnd->setDelegate(new MyDelegate(ctx)); ctx->wnd->resize(ctx->width, ctx->height); std::string url = medianame; ctx->wnd->navigateTo(Berkelium::URLString::point_to(url.data(), url.length())); return ctx; } static qboolean VARGS Dec_DisplayFrame(void *vctx, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ectx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ectx) { decctx *ctx = (decctx*)vctx; if (forcevideo || ctx->repainted) { uploadtexture(ectx, TF_BGRA32, ctx->width, ctx->height, ctx->buffer, NULL); ctx->paintedwidth = ctx->width; ctx->paintedheight = ctx->height; ctx->repainted = false; } return qtrue; } static void Dec_Destroy(void *vctx) { decctx *ctx = (decctx*)vctx; if (inited) //make sure things don't happen in the wrong order. we can still leak though ctx->wnd->destroy(); delete ctx; } static void Dec_GetSize (void *vctx, int *width, int *height) { decctx *ctx = (decctx*)vctx; if (ctx->repainted) { *width = ctx->width; *height = ctx->height; } else { *width = ctx->paintedwidth; *height = ctx->paintedheight; } } static qboolean Dec_SetSize (void *vctx, int width, int height) { decctx *ctx = (decctx*)vctx; if (width < 4) width = 4; if (height < 4) height = 4; if (ctx->width == width && ctx->height == height) return qtrue; //no point //there's no resize notification. apparently javascript cannot resize windows. yay. unsigned int *newbuf = (unsigned int*)realloc(ctx->buffer, width * height * sizeof(*newbuf)); if (!newbuf) return qfalse; //failed?!? ctx->width = width; ctx->height = height; ctx->buffer = newbuf; ctx->repainted = false; ctx->wnd->resize(ctx->width, ctx->height); return qtrue; } static void Dec_CursorMove (void *vctx, float posx, float posy) { decctx *ctx = (decctx*)vctx; ctx->wnd->mouseMoved((int)(posx * ctx->width), (int)(posy * ctx->height)); } static void Dec_Key (void *vctx, int code, int unicode, int isup) { decctx *ctx = (decctx*)vctx; wchar_t wchr = unicode; if (code >= 178 && code < 178+6) { code = code - 178; //swap mouse2+3 if (code == 1) code = 2; else if (code == 2) code = 1; ctx->wnd->mouseButton(code, !isup); } else if (code == 188 || code == 189) { if (!isup) ctx->wnd->mouseWheel(0, (code==189)?-30:30); } else { int mods = 0; if (code == 127) code = 0x08; else if (code == 140) //del code = 0x2e; else if (code == 143) //home code = 0x24; else if (code == 144) //end code = 0x23; else if (code == 141) //pgdn code = 0x22; else if (code == 142) //pgup code = 0x21; else if (code == 139) //ins code = 0x2d; else if (code == 132) //up code = 0x26; else if (code == 133) //down code = 0x28; else if (code == 134) //left code = 0x25; else if (code == 135) //right code = 0x27; if (code) ctx->wnd->keyEvent(!isup, mods, code, 0); if (unicode && !isup) { wchar_t chars[2] = {unicode}; if (unicode == 127 || unicode == 8 || unicode == 9 || unicode == 27) return; ctx->wnd->textEvent(chars, 1); } } } static void Dec_ChangeStream(void *vctx, const char *newstream) { decctx *ctx = (decctx*)vctx; if (!strncmp(newstream, "cmd:", 4)) { if (!strcmp(newstream+4, "refresh")) ctx->wnd->refresh(); else if (!strcmp(newstream+4, "transparent")) ctx->wnd->setTransparent(true); else if (!strcmp(newstream+4, "focus")) ctx->wnd->focus(); else if (!strcmp(newstream+4, "unfocus")) ctx->wnd->unfocus(); else if (!strcmp(newstream+4, "opaque")) ctx->wnd->setTransparent(false); else if (!strcmp(newstream+4, "stop")) ctx->wnd->stop(); else if (!strcmp(newstream+4, "back")) ctx->wnd->goBack(); else if (!strcmp(newstream+4, "forward")) ctx->wnd->goForward(); else if (!strcmp(newstream+4, "cut")) ctx->wnd->cut(); else if (!strcmp(newstream+4, "copy")) ctx->wnd->copy(); else if (!strcmp(newstream+4, "paste")) ctx->wnd->paste(); else if (!strcmp(newstream+4, "del")) ctx->wnd->del(); else if (!strcmp(newstream+4, "selectall")) ctx->wnd->selectAll(); } else if (!strncmp(newstream, "javascript:", 11)) { newstream+=11; int len = mblen(newstream, MB_CUR_MAX); wchar_t *wchrs = (wchar_t *)malloc((len+1)*2); len = mbstowcs(wchrs, newstream, len); ctx->wnd->executeJavascript(Berkelium::WideString::point_to(wchrs, len)); free(wchrs); } else { std::string url = newstream; ctx->wnd->navigateTo(Berkelium::URLString::point_to(url.data(), url.length())); } } static bool Dec_Init(void) { return true; } static qintptr_t Dec_Tick(qintptr_t *args) { //need to keep it ticking over, if any work is to be done. if (inited) Berkelium::update(); return 0; } static qintptr_t Dec_Shutdown(qintptr_t *args) { //force-kill all. if (inited) Berkelium::destroy(); inited = qfalse; return 0; } static media_decoder_funcs_t decoderfuncs = { sizeof(media_decoder_funcs_t), "berkelium", Dec_Create, Dec_DisplayFrame, Dec_Destroy, NULL,//rewind Dec_CursorMove, Dec_Key, Dec_SetSize, Dec_GetSize, Dec_ChangeStream }; extern "C" qintptr_t Plug_Init(qintptr_t *args) { if (!Plug_Export("Tick", Dec_Tick)) { Con_Printf("Berkelium plugin failed: Engine doesn't support Tick feature\n"); return false; } if (!Plug_Export("Shutdown", Dec_Shutdown)) { Con_Printf("Berkelium plugin failed: Engine doesn't support Shutdown feature\n"); return false; } if (!pPlug_ExportNative("Media_VideoDecoder", &decoderfuncs)) { Con_Printf("Berkelium plugin failed: Engine doesn't support media decoder plugins\n"); return false; } return Dec_Init(); } ================================================ FILE: plugins/berkelium/readme.txt ================================================ simple media decoding plugin that decodes web pages instead of videos... See installation instructions at the end of this file. This means you can do: playfilm berkelium:http://google.com And you'll get google.com displayed as if it were a cinematic or so. The engine will pass mouse+keyboard events to such cinematics so you can interact with it. You can also use the 'videomap' term within a shader, if you want to put some youtube video on a wall or something. Here's an example of such a shader: muh_bad { { map $lightmap } { videomap berkelium:http://google.com blendfunc filter } } CSQC is able to interact with videos by: 1: find the name of the shader string texname = getsurfacetexture(trace_ent, getsurfacenearpoint(trace_ent, trace_endpos)); 2: send a key event gecko_keyevent(texname, keycode, keydown); 3: move the mouse cursor gecko_mousemove(texname, x, y); note that the x and y values should be between 0 and 1. Surface resolution is not relevent here. 4: resize the image gecko_resize(texname, 1024, 1024); if you're using it in 2d, make sure it matches the pixel width of the screen where it'll be displayed. it'll get fuzzy otherwise. Size matters. Beware of sites that do not resize images for different resolutions (ie: most of them). 5: you can query the image with: vector v = gecko_get_texture_extent(texname); 6: you can purge resources with: gecko_destroy(texname); this will 'end' the video. when the shader is next displayed it'll reset from the original url. 7: you can change the url with: gecko_navigate(texname, "http://fteqw.com"); 8: you can send it these navigation commands (in the place of a url). You'll likely want a focus command. cmd:refresh cmd:transparent cmd:focus cmd:unfocus cmd:opaque cmd:stop cmd:back cmd:forward cmd:cut cmd:copy cmd:paste cmd:del cmd:selectall Compiling the plugin for Windows: In the berkelium 7z file that you should have already downloaded, there'll be an includes and a lib directory. Stick the contents of both of those in the empty doubled berkelium directory on the fte svn. The msvc project file should now be usable. You will likely want to change the linker target path to get picked up by the engine automatically. Windows Installation instructions: Download Berkelium from here: http://berkelium.org/ (http://github.com/sirikata/berkelium/downloads) You should get a 7z file that contains a bin directory. Copy the contents of that bin directory to your quake directory. The FTE-specific plugin 'berkeliumx86.dll' should be placed as 'quake/fte/plugins/berkeliumx86.dll'. ================================================ FILE: plugins/botlib/makebotlibdll.bat ================================================ REM Quite clearly this is a stupid way to do this. REM But it keeps it all in one file, so that's always fun. REM To use: Stick in the q3 quake3-1.32b/code/botlib directory. REM Install gcc as either cygwin or mingw REM Double click it your batch file. REM Copy the resultant botlib.dll to your quake directory. REM Run FTE in Q3 with bots. REM note that botlib doesn't work the same as other addons, and must be compiled to native. REM it could potentially be compiled in, but that results in a lot of conflicts and much bloat when the engine is primarily targeted for q1. REM botlib works as a dll, and FTE links dynamically. botlib calls fail without the dll. REM The bots_enabled q3 cvar is readonly and forced to 0 without the dll. REM The primary reason for the library as a dll is to stop the damn thing from crashing on cached memory references on map changes. REM FTE likes freeing memory, while q3 reuses it. Botlib has some really unhealthy reads. The only ways around that I could find were rewriting botlib or closing the dll between maps. DLLs help combat q1 bloat though, and requires less maintainence in botlib. echo off echo. >standalone.c echo #include "../game/q_shared.h" >>standalone.c echo void Com_Memset (void* dest, const int val, const size_t count) >>standalone.c echo { >>standalone.c echo memset(dest, val, count); >>standalone.c echo } >>standalone.c echo void Com_Memcpy (void* dest, const void* src, const size_t count) >>standalone.c echo { >>standalone.c echo memcpy(dest, src, count); >>standalone.c echo } >>standalone.c echo void QDECL Com_Error( int level, const char *error, ... ) >>standalone.c echo { >>standalone.c echo exit(0); >>standalone.c echo } >>standalone.c gcc -mno-cygwin *.c ../game/q_shared.c ../game/q_math.c -DBOTLIB -D__LCC__ -shared -o botlib.dll -DCom_Printf=printf ================================================ FILE: plugins/bullet/bulletplug.cpp ================================================ /* Copyright (C) 1996-1997 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. */ //if we're not building as an fte-specific plugin, we must be being built as part of the fte engine itself. //(no, we don't want to act as a plugin for ezquake...) #ifndef FTEPLUGIN #define FTEENGINE #define FTEPLUGIN #define pCvar_Register Cvar_Get #define pCvar_GetNVFDG Cvar_Get2 #define pCvar_GetFloat(x) Cvar_FindVar(x)->value #define pSys_Error Sys_Error #define Plug_Init Plug_Bullet_Init #pragma comment(lib,"../../plugins/bullet/libs/bullet_dbg.lib") #endif #include "quakedef.h" #include "../plugin.h" #include "../engine.h" #include "pr_common.h" #include "com_mesh.h" #ifndef FTEENGINE #define BZ_Malloc malloc #define BZ_Free free #define Z_Free BZ_Free //#define vec3_origin vec3_origin_ //static vec3_t vec3_origin; #define VectorCompare VectorCompare_ static int VectorCompare (const pvec3_t v1, const pvec3_t v2) { int i; for (i=0 ; i<3 ; i++) if (v1[i] != v2[i]) return 0; return 1; } #endif static rbeplugfuncs_t *rbefuncs; //============================================================================ // physics engine support //============================================================================ #define DEG2RAD(d) (d * M_PI * (1/180.0f)) #define RAD2DEG(d) ((d*180) / M_PI) #include "btBulletDynamicsCommon.h" //not sure where these are going. seems to be an issue only on windows. #ifndef max #define max(a,b) ((a) > (b) ? (a) : (b)) #endif #ifndef min #define min(a,b) ((a) < (b) ? (a) : (b)) #endif static void World_Bullet_RunCmd(world_t *world, rbecommandqueue_t *cmd); static cvar_t *physics_bullet_maxiterationsperframe; static cvar_t *physics_bullet_framerate; static cvar_t *pr_meshpitch; void World_Bullet_Init(void) { physics_bullet_maxiterationsperframe = cvarfuncs->GetNVFDG("physics_bullet_maxiterationsperframe", "10", 0, "FIXME: should be 1 when CCD is working properly.", "Bullet"); physics_bullet_framerate = cvarfuncs->GetNVFDG("physics_bullet_framerate", "60", 0, "Bullet physics run at a fixed framerate in order to preserve numerical stability (interpolation is used to smooth out the result). Higher framerates are of course more demanding.", "Bullet"); pr_meshpitch = cvarfuncs->GetNVFDG("r_meshpitch", "-1", 0, "", "Bullet"); } typedef struct bulletcontext_s { rigidbodyengine_t funcs; bool hasextraobjs; // void *ode_space; // void *ode_contactgroup; // number of constraint solver iterations to use (for dWorldStepFast) // int ode_iterations; // actual step (server frametime / ode_iterations) // vec_t ode_step; // max velocity for a 1-unit radius object at current step to prevent // missed collisions // vec_t ode_movelimit; rbecommandqueue_t *cmdqueuehead; rbecommandqueue_t *cmdqueuetail; world_t *gworld; btBroadphaseInterface *broadphase; btDefaultCollisionConfiguration *collisionconfig; btCollisionDispatcher *collisiondispatcher; btSequentialImpulseConstraintSolver *solver; btDiscreteDynamicsWorld *dworld; btOverlapFilterCallback *ownerfilter; } bulletcontext_t; class QCFilterCallback : public btOverlapFilterCallback { // return true when pairs need collision virtual bool needBroadphaseCollision(btBroadphaseProxy* proxy0,btBroadphaseProxy* proxy1) const { //dimensions don't collide bool collides = (proxy0->m_collisionFilterGroup & proxy1->m_collisionFilterMask) != 0; collides = collides && (proxy1->m_collisionFilterGroup & proxy0->m_collisionFilterMask); //owners don't collide (unless one is world, obviouslyish) if (collides) { auto b1 = (btRigidBody*)proxy0->m_clientObject; auto b2 = (btRigidBody*)proxy1->m_clientObject; //don't let two qc-controlled entities collide in Bullet, that's the job of quake. if (b1->isStaticOrKinematicObject() && b2->isStaticOrKinematicObject()) return false; auto e1 = (wedict_t*)b1->getUserPointer(); auto e2 = (wedict_t*)b2->getUserPointer(); if (e1&&e2) { if ((e1->v->solid == SOLID_TRIGGER && e2->v->solid != SOLID_BSP) || (e2->v->solid == SOLID_TRIGGER && e1->v->solid != SOLID_BSP)) return false; //triggers only collide with bsp objects. if (e1->entnum && e2->entnum) collides = e1->v->owner != e2->entnum && e2->v->owner != e1->entnum; } } return collides; } }; static void QDECL World_Bullet_End(world_t *world) { auto ctx = reinterpret_cast(world->rbe); world->rbe = nullptr; delete ctx->dworld; delete ctx->solver; delete ctx->collisionconfig; delete ctx->collisiondispatcher; delete ctx->broadphase; delete ctx->ownerfilter; Z_Free(ctx); } static void QDECL World_Bullet_RemoveJointFromEntity(world_t *world, wedict_t *ed) { ed->rbe.joint_type = 0; // if(ed->rbe.joint) // dJointDestroy((dJointID)ed->rbe.joint); ed->rbe.joint.joint = NULL; } static void QDECL World_Bullet_RemoveFromEntity(world_t *world, wedict_t *ed) { auto ctx = reinterpret_cast(world->rbe); btRigidBody *body; btCollisionShape *geom; if (!ed->rbe.physics) return; // entity is not physics controlled, free any physics data ed->rbe.physics = qfalse; body = (btRigidBody*)ed->rbe.body.body; ed->rbe.body.body = NULL; if (body) ctx->dworld->removeRigidBody (body); geom = (btCollisionShape*)ed->rbe.body.geom; ed->rbe.body.geom = NULL; if (ed->rbe.body.geom) delete geom; //FIXME: joints rbefuncs->ReleaseCollisionMesh(ed); if(ed->rbe.massbuf) BZ_Free(ed->rbe.massbuf); ed->rbe.massbuf = NULL; } static bool NegativeMeshPitch(world_t *world, wedict_t *ent) { if (ent->v->modelindex) { model_t *model = world->Get_CModel(world, ent->v->modelindex); if (model && (model->type == mod_alias || model->type == mod_halflife)) return pr_meshpitch->value < 0; return false; } return false; } static btTransform transformFromQuake(world_t *world, wedict_t *ent) { vec3_t forward, left, up; if (NegativeMeshPitch(world, ent)) { pvec3_t iangles = {-ent->v->angles[0], ent->v->angles[1], ent->v->angles[2]}; rbefuncs->AngleVectors(iangles, forward, left, up); } else rbefuncs->AngleVectors(ent->v->angles, forward, left, up); VectorNegate(left, left); return btTransform(btMatrix3x3(forward[0], forward[1], forward[2], left[0], left[1], left[2], up[0], up[1], up[2]), btVector3(ent->v->origin[0], ent->v->origin[1], ent->v->origin[2])); } static void World_Bullet_Frame_JointFromEntity(world_t *world, wedict_t *ed) { auto rbe = reinterpret_cast(world->rbe); btTypedConstraint *j = nullptr; btRigidBody *b1 = nullptr; btRigidBody *b2 = nullptr; int movetype = 0; int jointtype = 0; int enemy = 0, aiment = 0; wedict_t *e1, *e2; // vec_t CFM, ERP, FMax; vec_t Stop; // vec_t Vel; vec3_t forward; movetype = ed->v->movetype; jointtype = ed->xv->jointtype; enemy = ed->v->enemy; aiment = ed->v->aiment; btVector3 origin(ed->v->origin[0], ed->v->origin[1], ed->v->origin[2]); btVector3 velocity(ed->v->velocity[0], ed->v->velocity[1], ed->v->velocity[2]); btVector3 movedir(ed->v->movedir[0], ed->v->movedir[1], ed->v->movedir[2]); if(movetype == MOVETYPE_PHYSICS) jointtype = 0; // can't have both e1 = (wedict_t*)PROG_TO_EDICT(world->progs, enemy); b1 = (btRigidBody*)e1->rbe.body.body; if(ED_ISFREE(e1) || !b1) enemy = 0; e2 = (wedict_t*)PROG_TO_EDICT(world->progs, aiment); b2 = (btRigidBody*)e2->rbe.body.body; if(ED_ISFREE(e2) || !b2) aiment = 0; // see http://www.ode.org/old_list_archives/2006-January/017614.html // we want to set ERP? make it fps independent and work like a spring constant // note: if movedir[2] is 0, it becomes ERP = 1, CFM = 1.0 / (H * K) if(movedir[0] > 0 && movedir[1] > 0) { // float K = movedir[0]; // float D = movedir[1]; // float R = 2.0 * D * sqrt(K); // we assume D is premultiplied by sqrt(sprungMass) // CFM = 1.0 / (rbe->ode_step * K + R); // always > 0 // ERP = rbe->ode_step * K * CFM; // Vel = 0; // FMax = 0; Stop = movedir[2]; } else if(movedir[1] < 0) { // CFM = 0; // ERP = 0; // Vel = movedir[0]; // FMax = -movedir[1]; // TODO do we need to multiply with world.physics.ode_step? Stop = movedir[2] > 0 ? movedir[2] : BT_INFINITY; } else // movedir[0] > 0, movedir[1] == 0 or movedir[0] < 0, movedir[1] >= 0 { // CFM = 0; // ERP = 0; // Vel = 0; // FMax = 0; Stop = BT_INFINITY; } if(jointtype == ed->rbe.joint_type && VectorCompare(origin, ed->rbe.joint_origin) && VectorCompare(velocity, ed->rbe.joint_velocity) && VectorCompare(ed->v->angles, ed->rbe.joint_angles) && enemy == ed->rbe.joint_enemy && aiment == ed->rbe.joint_aiment && VectorCompare(movedir, ed->rbe.joint_movedir)) return; // nothing to do if(ed->rbe.joint.joint) { j = (btTypedConstraint*)ed->rbe.joint.joint; rbe->dworld->removeConstraint(j); ed->rbe.joint.joint = nullptr; delete j; } if (!jointtype) return; btVector3 b1org(0,0,0), b2org(0,0,0); if(enemy) b1org.setValue(e1->v->origin[0], e1->v->origin[1], e1->v->origin[2]); if(aiment) b2org.setValue(e2->v->origin[0], e2->v->origin[1], e2->v->origin[2]); ed->rbe.joint_type = jointtype; ed->rbe.joint_enemy = enemy; ed->rbe.joint_aiment = aiment; VectorCopy(origin, ed->rbe.joint_origin); VectorCopy(velocity, ed->rbe.joint_velocity); VectorCopy(ed->v->angles, ed->rbe.joint_angles); VectorCopy(movedir, ed->rbe.joint_movedir); rbefuncs->AngleVectors(ed->v->angles, forward, nullptr, nullptr); //Con_Printf("making new joint %i\n", (int) (ed - prog->edicts)); switch(jointtype) { case JOINTTYPE_POINT: j = new btPoint2PointConstraint(*b1, *b2, btVector3(b1org - origin), btVector3(b2org - origin)); break; /* case JOINTTYPE_HINGE: btHingeConstraint *h = new btHingeConstraint(*b1, *b2, btVector3(b1org - origin), btVector3(b2org - origin), aa, ab, ref); j = h; if (h) { h->setLimit(-Stop, Stop, softness, bias, relaxation); h->setAxis(btVector3(forward[0], forward[1], forward[2])); // h->dJointSetHingeParam(j, dParamFMax, FMax); // h->dJointSetHingeParam(j, dParamHiStop, Stop); // h->dJointSetHingeParam(j, dParamLoStop, -Stop); // h->dJointSetHingeParam(j, dParamStopCFM, CFM); // h->dJointSetHingeParam(j, dParamStopERP, ERP); // h->setMotorTarget(vel); } break;*/ case JOINTTYPE_SLIDER: { btTransform jointtransform = transformFromQuake(world, ed); btTransform b1transform = transformFromQuake(world, e1).inverseTimes(jointtransform); btTransform b2transform = transformFromQuake(world, e2).inverseTimes(jointtransform); btSliderConstraint *s = new btSliderConstraint(*b1, *b2, b1transform, b2transform, false); j = s; if (s) { // s->dJointSetSliderAxis(j, forward[0], forward[1], forward[2]); // s->dJointSetSliderParam(j, dParamFMax, FMax); s->setLowerLinLimit(-Stop); s->setUpperLinLimit(Stop); s->setLowerAngLimit(0); s->setUpperAngLimit(0); // s->dJointSetSliderParam(j, dParamHiStop, Stop); // s->dJointSetSliderParam(j, dParamLoStop, -Stop); // s->dJointSetSliderParam(j, dParamStopCFM, CFM); // s->dJointSetSliderParam(j, dParamStopERP, ERP); // s->setTargetLinMotorVelocity(vel); // s->setPoweredLinMotor(true); } } break; /* case JOINTTYPE_UNIVERSAL: btGeneric6DofConstraint j = dJointCreateUniversal(rbe->ode_world, 0); if (j) { dJointSetUniversalAnchor(j, origin[0], origin[1], origin[2]); dJointSetUniversalAxis1(j, forward[0], forward[1], forward[2]); dJointSetUniversalAxis2(j, up[0], up[1], up[2]); dJointSetUniversalParam(j, dParamFMax, FMax); dJointSetUniversalParam(j, dParamHiStop, Stop); dJointSetUniversalParam(j, dParamLoStop, -Stop); dJointSetUniversalParam(j, dParamStopCFM, CFM); dJointSetUniversalParam(j, dParamStopERP, ERP); dJointSetUniversalParam(j, dParamVel, Vel); dJointSetUniversalParam(j, dParamFMax2, FMax); dJointSetUniversalParam(j, dParamHiStop2, Stop); dJointSetUniversalParam(j, dParamLoStop2, -Stop); dJointSetUniversalParam(j, dParamStopCFM2, CFM); dJointSetUniversalParam(j, dParamStopERP2, ERP); dJointSetUniversalParam(j, dParamVel2, Vel); } break;*/ /* case JOINTTYPE_HINGE2: j = dJointCreateHinge2(rbe->ode_world, 0); if (j) { dJointSetHinge2Anchor(j, origin[0], origin[1], origin[2]); dJointSetHinge2Axis1(j, forward[0], forward[1], forward[2]); dJointSetHinge2Axis2(j, velocity[0], velocity[1], velocity[2]); dJointSetHinge2Param(j, dParamFMax, FMax); dJointSetHinge2Param(j, dParamHiStop, Stop); dJointSetHinge2Param(j, dParamLoStop, -Stop); dJointSetHinge2Param(j, dParamStopCFM, CFM); dJointSetHinge2Param(j, dParamStopERP, ERP); dJointSetHinge2Param(j, dParamVel, Vel); dJointSetHinge2Param(j, dParamFMax2, FMax); dJointSetHinge2Param(j, dParamHiStop2, Stop); dJointSetHinge2Param(j, dParamLoStop2, -Stop); dJointSetHinge2Param(j, dParamStopCFM2, CFM); dJointSetHinge2Param(j, dParamStopERP2, ERP); dJointSetHinge2Param(j, dParamVel2, Vel); } break;*/ case JOINTTYPE_FIXED: { btTransform jointtransform = transformFromQuake(world, ed); btTransform b1transform = transformFromQuake(world, e1).inverseTimes(jointtransform); btTransform b2transform = transformFromQuake(world, e2).inverseTimes(jointtransform); j = new btFixedConstraint(*b1, *b2, b1transform, b2transform); } break; case 0: default: j = nullptr; break; } ed->rbe.joint.joint = (void *) j; if (j) { j->setUserConstraintPtr((void *) ed); rbe->dworld->addConstraint(j, false); } } static void MatToTransform(const float *mat, btTransform &tr) { tr.setBasis(btMatrix3x3( mat[0], mat[1], mat[2], mat[4], mat[5], mat[6], mat[8], mat[9], mat[10])); tr.setOrigin(btVector3(mat[3], mat[7], mat[11])); } static void MatFromTransform(float *mat, const btTransform &tr) { const btMatrix3x3 &m = tr.getBasis(); const btVector3 &o = tr.getOrigin(); const btVector3 &r0 = m.getRow(0); const btVector3 &r1 = m.getRow(1); const btVector3 &r2 = m.getRow(2); mat[0] = r0[0]; mat[1] = r0[1]; mat[2] = r0[2]; mat[3] =o[0]; mat[4] = r1[0]; mat[5] = r1[1]; mat[6] = r1[2]; mat[7] =o[1]; mat[8] = r2[0]; mat[9] = r2[1]; mat[10]= r2[2]; mat[11]=o[2]; } static qboolean QDECL World_Bullet_RagMatrixToBody(rbebody_t *bodyptr, float *mat) { //mat is a 4*3 matrix btTransform tr; auto body = reinterpret_cast(bodyptr->body); MatToTransform(mat, tr); body->setWorldTransform(tr); return qtrue; } static qboolean QDECL World_Bullet_RagCreateBody(world_t *world, rbebody_t *bodyptr, rbebodyinfo_t *bodyinfo, float *mat, wedict_t *ent) { btRigidBody *body = nullptr; btCollisionShape *geom = nullptr; float radius; float threshold; // float length; auto ctx = reinterpret_cast(world->rbe); // int axisindex; ctx->hasextraobjs = true; switch(bodyinfo->geomshape) { /* case GEOMTYPE_TRIMESH: // foo Matrix4x4_Identity(ed->rbe.offsetmatrix); geom = NULL; if (!model) { Con_Printf("entity %i (classname %s) has no model\n", NUM_FOR_EDICT(world->progs, (edict_t*)ed), PR_GetString(world->progs, ed->v->classname)); if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } if (!rbefuncs->GenerateCollisionMesh(world, model, ed, geomcenter)) { if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } // foo Matrix4x4_RM_CreateTranslate(ed->rbe.offsetmatrix, geomcenter[0], geomcenter[1], geomcenter[2]); { btTriangleIndexVertexArray *tiva = new btTriangleIndexVertexArray(); btIndexedMesh mesh; mesh.m_vertexType = PHY_FLOAT; mesh.m_indexType = PHY_INTEGER; mesh.m_numTriangles = ed->rbe.numtriangles; mesh.m_numVertices = ed->rbe.numvertices; mesh.m_triangleIndexBase = (const unsigned char*)ed->rbe.element3i; mesh.m_triangleIndexStride = sizeof(*ed->rbe.element3i)*3; mesh.m_vertexBase = (const unsigned char*)ed->rbe.vertex3f; mesh.m_vertexStride = sizeof(*ed->rbe.vertex3f)*3; tiva->addIndexedMesh(mesh); geom = new btBvhTriangleMeshShape(tiva, true); } break; */ default: Con_DPrintf("World_Bullet_RagCreateBody: unsupported geomshape %i\n", bodyinfo->geomshape); case GEOMTYPE_BOX: geom = new btBoxShape(btVector3(bodyinfo->dimensions[0], bodyinfo->dimensions[1], bodyinfo->dimensions[2]) * 0.5); radius = sqrt(DotProduct(bodyinfo->dimensions,bodyinfo->dimensions)); threshold = min(bodyinfo->dimensions[0], min(bodyinfo->dimensions[1], bodyinfo->dimensions[2])); break; case GEOMTYPE_SPHERE: threshold = radius = bodyinfo->dimensions[0] * 0.5f; geom = new btSphereShape(radius); break; case GEOMTYPE_CAPSULE: // case GEOMTYPE_CAPSULE_X: // case GEOMTYPE_CAPSULE_Y: case GEOMTYPE_CAPSULE_Z: radius = (bodyinfo->dimensions[0]+bodyinfo->dimensions[1]) * 0.5f; geom = new btCapsuleShapeZ(radius, bodyinfo->dimensions[2]); threshold = min(radius, bodyinfo->dimensions[2]*0.5); radius = max(radius, bodyinfo->dimensions[2]*0.5); break; case GEOMTYPE_CYLINDER: // case GEOMTYPE_CYLINDER_X: // case GEOMTYPE_CYLINDER_Y: case GEOMTYPE_CYLINDER_Z: radius = (bodyinfo->dimensions[0] + bodyinfo->dimensions[1]) * 0.5; geom = new btCylinderShapeZ(btVector3(radius, radius, bodyinfo->dimensions[2])*0.5); threshold = min(radius, bodyinfo->dimensions[2]*0.5); radius = max(radius, bodyinfo->dimensions[2]*0.5); break; } bodyptr->geom = geom; //now create the body too btVector3 fallInertia(0, 0, 0); geom->calculateLocalInertia(bodyinfo->mass, fallInertia); btRigidBody::btRigidBodyConstructionInfo fallRigidBodyCI(bodyinfo->mass, nullptr, geom, fallInertia); MatToTransform(mat, fallRigidBodyCI.m_startWorldTransform); body = new btRigidBody(fallRigidBodyCI); body->setUserPointer(ent); bodyptr->body = reinterpret_cast(body); //motion threshhold should be speed/physicsframerate. //FIXME: recalculate... body->setCcdMotionThreshold(threshold/64); //radius should be the body's radius body->setCcdSweptSphereRadius(radius); ctx->dworld->addRigidBody(body, ent?ent->xv->dimension_solid:~0, ent?ent->xv->dimension_hit:~0); return qtrue; } static void QDECL World_Bullet_RagMatrixFromJoint(rbejoint_t *joint, rbejointinfo_t *info, float *mat) { /* dVector3 dr3; switch(info->type) { case JOINTTYPE_POINT: dJointGetBallAnchor(joint->ode_joint, dr3); mat[3] = dr3[0]; mat[7] = dr3[1]; mat[11] = dr3[2]; VectorClear(mat+4); VectorClear(mat+8); break; case JOINTTYPE_HINGE: dJointGetHingeAnchor(joint->ode_joint, dr3); mat[3] = dr3[0]; mat[7] = dr3[1]; mat[11] = dr3[2]; dJointGetHingeAxis(joint->ode_joint, dr3); VectorCopy(dr3, mat+4); VectorClear(mat+8); CrossProduct(mat+4, mat+8, mat+0); return; break; case JOINTTYPE_HINGE2: dJointGetHinge2Anchor(joint->ode_joint, dr3); mat[3] = dr3[0]; mat[7] = dr3[1]; mat[11] = dr3[2]; dJointGetHinge2Axis1(joint->ode_joint, dr3); VectorCopy(dr3, mat+4); dJointGetHinge2Axis2(joint->ode_joint, dr3); VectorCopy(dr3, mat+8); break; case JOINTTYPE_SLIDER: //no anchor point... //get the two bodies and average their origin for a somewhat usable representation of an anchor. { const dReal *p1, *p2; dReal n[3]; dBodyID b1 = dJointGetBody(joint->ode_joint, 0), b2 = dJointGetBody(joint->ode_joint, 1); if (b1) p1 = dBodyGetPosition(b1); else { p1 = n; VectorClear(n); } if (b2) p2 = dBodyGetPosition(b2); else p2 = p1; dJointGetSliderAxis(joint->ode_joint, dr3 + 0); VectorInterpolate(p1, 0.5, p2, dr3); mat[3] = dr3[0]; mat[7] = dr3[1]; mat[11] = dr3[2]; VectorClear(mat+4); VectorClear(mat+8); } break; case JOINTTYPE_UNIVERSAL: dJointGetUniversalAnchor(joint->ode_joint, dr3); mat[3] = dr3[0]; mat[7] = dr3[1]; mat[11] = dr3[2]; dJointGetUniversalAxis1(joint->ode_joint, dr3); VectorCopy(dr3, mat+4); dJointGetUniversalAxis2(joint->ode_joint, dr3); VectorCopy(dr3, mat+8); CrossProduct(mat+4, mat+8, mat+0); return; break; } rbefuncs->AngleVectors(vec3_origin, mat+0, mat+4, mat+8); VectorNegate((mat+4), (mat+4)); */ } static void QDECL World_Bullet_RagMatrixFromBody(world_t *world, rbebody_t *bodyptr, float *mat) { //auto ctx = reinterpret_cast(world->rbe); auto body = reinterpret_cast(bodyptr->body); MatFromTransform(mat, body->getCenterOfMassTransform()); } static void QDECL World_Bullet_RagEnableJoint(rbejoint_t *joint, qboolean enabled) { auto j = reinterpret_cast(joint->joint); j->setEnabled(enabled); } static void QDECL World_Bullet_RagCreateJoint(world_t *world, rbejoint_t *joint, rbejointinfo_t *info, rbebody_t *body1, rbebody_t *body2, vec3_t aaa2[3]) { auto ctx = reinterpret_cast(world->rbe); btTypedConstraint *j; btVector3 org(aaa2[0][0], aaa2[0][1], aaa2[0][2]); btVector3 axis1(aaa2[1][0], aaa2[1][1], aaa2[1][2]); btVector3 axis2(aaa2[2][0], aaa2[2][1], aaa2[2][2]); auto rb1 = reinterpret_cast(body1->body); auto rb2 = reinterpret_cast(body2->body); switch(info->type) { case JOINTTYPE_POINT: { auto point = new btPoint2PointConstraint(*rb1, *rb2, rb1->getWorldTransform().getOrigin()-org, rb2->getWorldTransform().getOrigin()-org); // point->setParam(BT_P2P_FLAGS_ERP, info->ERP); // point->setParam(BT_P2P_FLAGS_CFM, info->CFM); j = point; } break; case JOINTTYPE_HINGE: { auto hinge = new btHingeConstraint(*rb1, *rb2, org, org, axis1, axis2); hinge->setLimit(info->LoStop, info->HiStop /*other stuff!*/); // hinge->setParam(BT_HINGE_FLAGS_CFM_NORM, info->CFM); // hinge->setParam(BT_HINGE_FLAGS_CFM_STOP, info->CFM2); // hinge->setParam(BT_HINGE_FLAGS_ERP_NORM, info->ERP); // hinge->setParam(BT_HINGE_FLAGS_ERP_STOP, info->ERP2); j = hinge; } break; // case JOINTTYPE_SLIDER: // j = btSliderConstraint(rb1, rb2, tr1, tr2, false); // break; case JOINTTYPE_UNIVERSAL: { auto uni = new btUniversalConstraint(*rb1, *rb2, org, axis1, axis2); // uni->setParam(BT_6DOF_FLAGS_CFM_NORM, info->CFM); // uni->setParam(BT_6DOF_FLAGS_CFM_STOP, info->CFM2); // uni->setParam(BT_6DOF_FLAGS_ERP_STOP, info->ERP); uni->setUpperLimit(info->HiStop, info->HiStop2); uni->setLowerLimit(info->LoStop, info->LoStop2); j = uni; } break; case JOINTTYPE_HINGE2: { auto hinge2 = new btHinge2Constraint(*rb1, *rb2, org, axis1, axis2); hinge2->setUpperLimit(info->HiStop); hinge2->setLowerLimit(info->LoStop); j = hinge2; } break; // case JOINTTYPE_FIXED: // j = btFixedConstraint(rb1, rb2, tr1, tr2); // break; default: Con_Printf("Bullet: joint type %i not supported\n", info->type); j = nullptr; break; } /* if (joint->ode_joint) { //Con_Printf("made new joint %i\n", (int) (ed - prog->edicts)); // dJointSetData(joint->ode_joint, NULL); dJointAttach(joint->ode_joint, body1?body1->ode_body:NULL, body2?body2->ode_body:NULL); switch(info->type) { case JOINTTYPE_POINT: dJointSetBallAnchor(joint->ode_joint, aaa2[0][0], aaa2[0][1], aaa2[0][2]); break; case JOINTTYPE_HINGE: dJointSetHingeAnchor(joint->ode_joint, aaa2[0][0], aaa2[0][1], aaa2[0][2]); dJointSetHingeAxis(joint->ode_joint, aaa2[1][0], aaa2[1][1], aaa2[1][2]); dJointSetHingeParam(joint->ode_joint, dParamFMax, info->FMax); dJointSetHingeParam(joint->ode_joint, dParamHiStop, info->HiStop); dJointSetHingeParam(joint->ode_joint, dParamLoStop, info->LoStop); dJointSetHingeParam(joint->ode_joint, dParamStopCFM, info->CFM); dJointSetHingeParam(joint->ode_joint, dParamStopERP, info->ERP); dJointSetHingeParam(joint->ode_joint, dParamVel, info->Vel); break; case JOINTTYPE_SLIDER: dJointSetSliderAxis(joint->ode_joint, aaa2[1][0], aaa2[1][1], aaa2[1][2]); dJointSetSliderParam(joint->ode_joint, dParamFMax, info->FMax); dJointSetSliderParam(joint->ode_joint, dParamHiStop, info->HiStop); dJointSetSliderParam(joint->ode_joint, dParamLoStop, info->LoStop); dJointSetSliderParam(joint->ode_joint, dParamStopCFM, info->CFM); dJointSetSliderParam(joint->ode_joint, dParamStopERP, info->ERP); dJointSetSliderParam(joint->ode_joint, dParamVel, info->Vel); break; case JOINTTYPE_UNIVERSAL: dJointSetUniversalAnchor(joint->ode_joint, aaa2[0][0], aaa2[0][1], aaa2[0][2]); dJointSetUniversalAxis1(joint->ode_joint, aaa2[1][0], aaa2[1][1], aaa2[1][2]); dJointSetUniversalAxis2(joint->ode_joint, aaa2[2][0], aaa2[2][1], aaa2[2][2]); dJointSetUniversalParam(joint->ode_joint, dParamFMax, info->FMax); dJointSetUniversalParam(joint->ode_joint, dParamHiStop, info->HiStop); dJointSetUniversalParam(joint->ode_joint, dParamLoStop, info->LoStop); dJointSetUniversalParam(joint->ode_joint, dParamStopCFM, info->CFM); dJointSetUniversalParam(joint->ode_joint, dParamStopERP, info->ERP); dJointSetUniversalParam(joint->ode_joint, dParamVel, info->Vel); dJointSetUniversalParam(joint->ode_joint, dParamFMax2, info->FMax2); dJointSetUniversalParam(joint->ode_joint, dParamHiStop2, info->HiStop2); dJointSetUniversalParam(joint->ode_joint, dParamLoStop2, info->LoStop2); dJointSetUniversalParam(joint->ode_joint, dParamStopCFM2, info->CFM2); dJointSetUniversalParam(joint->ode_joint, dParamStopERP2, info->ERP2); dJointSetUniversalParam(joint->ode_joint, dParamVel2, info->Vel2); break; case JOINTTYPE_HINGE2: dJointSetHinge2Anchor(joint->ode_joint, aaa2[0][0], aaa2[0][1], aaa2[0][2]); dJointSetHinge2Axis1(joint->ode_joint, aaa2[1][0], aaa2[1][1], aaa2[1][2]); dJointSetHinge2Axis2(joint->ode_joint, aaa2[2][0], aaa2[2][1], aaa2[2][2]); dJointSetHinge2Param(joint->ode_joint, dParamFMax, info->FMax); dJointSetHinge2Param(joint->ode_joint, dParamHiStop, info->HiStop); dJointSetHinge2Param(joint->ode_joint, dParamLoStop, info->LoStop); dJointSetHinge2Param(joint->ode_joint, dParamStopCFM, info->CFM); dJointSetHinge2Param(joint->ode_joint, dParamStopERP, info->ERP); dJointSetHinge2Param(joint->ode_joint, dParamVel, info->Vel); dJointSetHinge2Param(joint->ode_joint, dParamFMax2, info->FMax2); dJointSetHinge2Param(joint->ode_joint, dParamHiStop2, info->HiStop2); dJointSetHinge2Param(joint->ode_joint, dParamLoStop2, info->LoStop2); dJointSetHinge2Param(joint->ode_joint, dParamStopCFM2, info->CFM2); dJointSetHinge2Param(joint->ode_joint, dParamStopERP2, info->ERP2); dJointSetHinge2Param(joint->ode_joint, dParamVel2, info->Vel2); break; case JOINTTYPE_FIXED: dJointSetFixed(joint->ode_joint); break; } } */ if (j) ctx->dworld->addConstraint(j, true); joint->joint = reinterpret_cast(j); } static void QDECL World_Bullet_RagDestroyBody(world_t *world, rbebody_t *bodyptr) { auto ctx = reinterpret_cast(world->rbe); auto body = reinterpret_cast(bodyptr->body); auto geom = reinterpret_cast(bodyptr->geom); bodyptr->body = nullptr; bodyptr->geom = nullptr; if (body) { ctx->dworld->removeRigidBody(body); delete body; } if (geom) delete geom; } static void QDECL World_Bullet_RagDestroyJoint(world_t *world, rbejoint_t *joint) { auto ctx = reinterpret_cast(world->rbe); auto j = reinterpret_cast(joint->joint); if (j) { ctx->dworld->removeConstraint(j); delete j; } joint->joint = nullptr; } //bullet gives us a handy way to get/set motion states. we can cheesily update entity fields this way. class QCMotionState : public btMotionState { wedict_t *edict; qboolean dirty; btTransform trans; world_t *world; public: void ReloadMotionState(void) { vec3_t offset; vec3_t axis[3]; btVector3 org; rbefuncs->AngleVectors(edict->v->angles, axis[0], axis[1], axis[2]); VectorNegate(axis[1], axis[1]); VectorAvg(edict->rbe.mins, edict->rbe.maxs, offset); VectorMA(edict->v->origin, offset[0]*1, axis[0], org); org[3] = 0;//for sse. VectorMA(org, offset[1]*1, axis[1], org); VectorMA(org, offset[2]*1, axis[2], org); trans.setBasis(btMatrix3x3(axis[0][0], axis[1][0], axis[2][0], axis[0][1], axis[1][1], axis[2][1], axis[0][2], axis[1][2], axis[2][2])); trans.setOrigin(org); } QCMotionState(wedict_t *ed, world_t *w) { dirty = qtrue; edict = ed; world = w; ReloadMotionState(); } virtual ~QCMotionState() { } virtual void getWorldTransform(btTransform &worldTrans) const { worldTrans = trans; } virtual void setWorldTransform(const btTransform &worldTrans) { vec3_t fwd, left, up, offset; trans = worldTrans; btVector3 pos = worldTrans.getOrigin(); VectorCopy(worldTrans.getBasis().getColumn(0), fwd); VectorCopy(worldTrans.getBasis().getColumn(1), left); VectorCopy(worldTrans.getBasis().getColumn(2), up); VectorAvg(edict->rbe.mins, edict->rbe.maxs, offset); VectorMA(pos, offset[0]*-1, fwd, pos); VectorMA(pos, offset[1]*-1, left, pos); VectorMA(pos, offset[2]*-1, up, edict->v->origin); rbefuncs->VectorAngles(fwd, up, edict->v->angles, (qboolean)NegativeMeshPitch(world, edict)); const btVector3 &vel = ((btRigidBody*)edict->rbe.body.body)->getLinearVelocity(); VectorCopy(vel.m_floats, edict->v->velocity); //so it doesn't get rebuilt VectorCopy(edict->v->origin, edict->rbe.origin); VectorCopy(edict->v->angles, edict->rbe.angles); VectorCopy(edict->v->velocity, edict->rbe.velocity); //FIXME: relink the ent into the areagrid } }; static void World_Bullet_Frame_BodyFromEntity(world_t *world, wedict_t *ed) { auto ctx = reinterpret_cast(world->rbe); btRigidBody *body = nullptr; // btScalar mass; float test; // void *dataID; model_t *model; int axisindex; int modelindex = 0; int movetype = MOVETYPE_NONE; int solid = SOLID_NOT; int geomtype = GEOMTYPE_SOLID; qboolean modified = qfalse; vec3_t angles; vec3_t avelocity; vec3_t entmaxs; vec3_t entmins; vec3_t forward; vec3_t geomcenter; vec3_t geomsize; vec3_t left; vec3_t origin; vec3_t spinvelocity; vec3_t up; vec3_t velocity; // vec_t f; vec_t length; vec_t massval = 1.0f; // vec_t movelimit; vec_t radius; vec_t scale; // vec_t spinlimit; qboolean gravity; geomtype = ed->xv->geomtype; solid = ed->v->solid; movetype = ed->v->movetype; scale = ed->xv->scale?ed->xv->scale:1; modelindex = 0; model = nullptr; if (!geomtype) { switch(solid) { case SOLID_NOT: geomtype = GEOMTYPE_NONE; break; case SOLID_TRIGGER: geomtype = GEOMTYPE_NONE; break; case SOLID_BSP: geomtype = GEOMTYPE_TRIMESH; break; case SOLID_PHYSICS_TRIMESH: geomtype = GEOMTYPE_TRIMESH; break; case SOLID_PHYSICS_BOX: geomtype = GEOMTYPE_BOX; break; case SOLID_PHYSICS_SPHERE: geomtype = GEOMTYPE_SPHERE; break; case SOLID_PHYSICS_CAPSULE: geomtype = GEOMTYPE_CAPSULE; break; case SOLID_PHYSICS_CYLINDER:geomtype = GEOMTYPE_CYLINDER; break; default: geomtype = GEOMTYPE_BOX; break; } } switch(geomtype) { case GEOMTYPE_TRIMESH: modelindex = ed->v->modelindex; model = world->Get_CModel(world, modelindex); if (!model || model->loadstate != MLS_LOADED) { model = nullptr; modelindex = 0; } if (model) { VectorScale(model->mins, scale, entmins); VectorScale(model->maxs, scale, entmaxs); if (ed->xv->mass) massval = ed->xv->mass; } else { modelindex = 0; massval = 1.0f; } massval = 0; //bullet does not support trisoup moving through the world. break; case GEOMTYPE_BOX: case GEOMTYPE_SPHERE: case GEOMTYPE_CAPSULE: case GEOMTYPE_CAPSULE_X: case GEOMTYPE_CAPSULE_Y: case GEOMTYPE_CAPSULE_Z: case GEOMTYPE_CYLINDER: case GEOMTYPE_CYLINDER_X: case GEOMTYPE_CYLINDER_Y: case GEOMTYPE_CYLINDER_Z: VectorCopy(ed->v->mins, entmins); VectorCopy(ed->v->maxs, entmaxs); if (ed->xv->mass) massval = ed->xv->mass; break; default: // case GEOMTYPE_NONE: if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } VectorSubtract(entmaxs, entmins, geomsize); if (DotProduct(geomsize,geomsize) == 0) { // we don't allow point-size physics objects... if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } // check if we need to create or replace the geom if (!ed->rbe.physics || !VectorCompare(ed->rbe.mins, entmins) || !VectorCompare(ed->rbe.maxs, entmaxs) || ed->rbe.modelindex != modelindex) { btCollisionShape *geom; modified = qtrue; World_Bullet_RemoveFromEntity(world, ed); ed->rbe.physics = qtrue; VectorCopy(entmins, ed->rbe.mins); VectorCopy(entmaxs, ed->rbe.maxs); ed->rbe.modelindex = modelindex; VectorAvg(entmins, entmaxs, geomcenter); ed->rbe.movelimit = min(geomsize[0], min(geomsize[1], geomsize[2])); /* memset(ed->rbe.offsetmatrix, 0, sizeof(ed->rbe.offsetmatrix)); ed->rbe.offsetmatrix[0] = 1; ed->rbe.offsetmatrix[5] = 1; ed->rbe.offsetmatrix[10] = 1; ed->rbe.offsetmatrix[3] = -geomcenter[0]; ed->rbe.offsetmatrix[7] = -geomcenter[1]; ed->rbe.offsetmatrix[11] = -geomcenter[2]; */ ed->rbe.mass = massval; switch(geomtype) { case GEOMTYPE_TRIMESH: // foo Matrix4x4_Identity(ed->rbe.offsetmatrix); geom = nullptr; if (!model) { Con_Printf("entity %i (classname %s) has no model\n", NUM_FOR_EDICT(world->progs, (edict_t*)ed), PR_GetString(world->progs, ed->v->classname)); if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } if (!rbefuncs->GenerateCollisionMesh(world, model, ed, geomcenter)) { if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } // foo Matrix4x4_RM_CreateTranslate(ed->rbe.offsetmatrix, geomcenter[0], geomcenter[1], geomcenter[2]); { btTriangleIndexVertexArray *tiva = new btTriangleIndexVertexArray(); btIndexedMesh mesh; mesh.m_vertexType = PHY_FLOAT; mesh.m_indexType = PHY_INTEGER; mesh.m_numTriangles = ed->rbe.numtriangles; mesh.m_numVertices = ed->rbe.numvertices; mesh.m_triangleIndexBase = (const unsigned char*)ed->rbe.element3i; mesh.m_triangleIndexStride = sizeof(*ed->rbe.element3i)*3; mesh.m_vertexBase = (const unsigned char*)ed->rbe.vertex3f; mesh.m_vertexStride = sizeof(*ed->rbe.vertex3f)*3; tiva->addIndexedMesh(mesh); geom = new btBvhTriangleMeshShape(tiva, true); } break; case GEOMTYPE_BOX: geom = new btBoxShape(btVector3(geomsize[0], geomsize[1], geomsize[2]) * 0.5); break; case GEOMTYPE_SPHERE: geom = new btSphereShape(geomsize[0] * 0.5f); break; case GEOMTYPE_CAPSULE: case GEOMTYPE_CAPSULE_X: case GEOMTYPE_CAPSULE_Y: case GEOMTYPE_CAPSULE_Z: if (geomtype == GEOMTYPE_CAPSULE) { // the qc gives us 3 axis radius, the longest axis is the capsule axis axisindex = 0; if (geomsize[axisindex] < geomsize[1]) axisindex = 1; if (geomsize[axisindex] < geomsize[2]) axisindex = 2; } else axisindex = geomtype-GEOMTYPE_CAPSULE_X; if (axisindex == 0) radius = min(geomsize[1], geomsize[2]) * 0.5f; else if (axisindex == 1) radius = min(geomsize[0], geomsize[2]) * 0.5f; else radius = min(geomsize[0], geomsize[1]) * 0.5f; length = geomsize[axisindex] - radius*2; if (length <= 0) { radius -= (1 - length)*0.5; length = 1; } if (axisindex == 0) geom = new btCapsuleShapeX(radius, length); else if (axisindex == 1) geom = new btCapsuleShape(radius, length); else geom = new btCapsuleShapeZ(radius, length); break; case GEOMTYPE_CYLINDER: case GEOMTYPE_CYLINDER_X: case GEOMTYPE_CYLINDER_Y: case GEOMTYPE_CYLINDER_Z: if (geomtype == GEOMTYPE_CYLINDER) { // the qc gives us 3 axis radius, the longest axis is the capsule axis axisindex = 0; if (geomsize[axisindex] < geomsize[1]) axisindex = 1; if (geomsize[axisindex] < geomsize[2]) axisindex = 2; } else axisindex = geomtype-GEOMTYPE_CYLINDER_X; if (axisindex == 0) geom = new btCylinderShapeX(btVector3(geomsize[0], geomsize[1], geomsize[2])*0.5); else if (axisindex == 1) geom = new btCylinderShape(btVector3(geomsize[0], geomsize[1], geomsize[2])*0.5); else geom = new btCylinderShapeZ(btVector3(geomsize[0], geomsize[1], geomsize[2])*0.5); break; default: // Con_Printf("World_Bullet_BodyFromEntity: unrecognized solid value %i was accepted by filter\n", solid); if (ed->rbe.physics) World_Bullet_RemoveFromEntity(world, ed); return; } // Matrix3x4_InvertTo4x4_Simple(ed->rbe.offsetmatrix, ed->rbe.offsetimatrix); // ed->rbe.massbuf = BZ_Malloc(sizeof(dMass)); // memcpy(ed->rbe.massbuf, &mass, sizeof(dMass)); ed->rbe.body.geom = (void *)geom; } //non-moving objects need to be static objects (and thus need 0 mass) if (movetype != MOVETYPE_PHYSICS && movetype != MOVETYPE_WALK) //enabling kinematic objects for everything else destroys framerates (!movetype || movetype == MOVETYPE_PUSH) massval = 0; //if the mass changes, we'll need to create a new body (but not the shape, so invalidate the current one) if (ed->rbe.mass != massval) { ed->rbe.mass = massval; body = (btRigidBody*)ed->rbe.body.body; if (body) ctx->dworld->removeRigidBody(body); ed->rbe.body.body = NULL; } // if(ed->rbe.body.geom) // dGeomSetData(ed->rbe.body.geom, (void*)ed); if (movetype == MOVETYPE_PHYSICS && ed->rbe.mass) { if (ed->rbe.body.body == NULL) { // ed->rbe.body.body = (void *)(body = dBodyCreate(world->rbe.world)); // dGeomSetBody(ed->rbe.body.geom, body); // dBodySetData(body, (void*)ed); // dBodySetMass(body, (dMass *) ed->rbe.massbuf); btVector3 fallInertia(0, 0, 0); ((btCollisionShape*)ed->rbe.body.geom)->calculateLocalInertia(ed->rbe.mass, fallInertia); btRigidBody::btRigidBodyConstructionInfo fallRigidBodyCI(ed->rbe.mass, new QCMotionState(ed,world), (btCollisionShape*)ed->rbe.body.geom, fallInertia); body = new btRigidBody(fallRigidBodyCI); body->setUserPointer(ed); // btTransform trans; // trans.setFromOpenGLMatrix(ed->rbe.offsetmatrix); // body->setCenterOfMassTransform(trans); ed->rbe.body.body = (void*)body; //motion threshhold should be speed/physicsframerate. //Threshhold enables CCD when the object moves faster than X //FIXME: recalculate... body->setCcdMotionThreshold((geomsize[0]+geomsize[1]+geomsize[2])*(4/3)); //radius should be the body's radius, or smaller. body->setCcdSweptSphereRadius((geomsize[0]+geomsize[1]+geomsize[2])*(0.5/3)); ctx->dworld->addRigidBody(body, ed->xv->dimension_solid, ed->xv->dimension_hit); modified = qtrue; } } else { if (ed->rbe.body.body == nullptr) { btRigidBody::btRigidBodyConstructionInfo rbci(ed->rbe.mass, new QCMotionState(ed,world), (btCollisionShape*)ed->rbe.body.geom, btVector3(0, 0, 0)); body = new btRigidBody(rbci); body->setUserPointer(ed); // btTransform trans; // trans.setFromOpenGLMatrix(ed->rbe.offsetmatrix); // body->setCenterOfMassTransform(trans); ed->rbe.body.body = (void*)body; if (ed->rbe.mass) body->setCollisionFlags(body->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT); else body->setCollisionFlags(body->getCollisionFlags() | btCollisionObject::CF_STATIC_OBJECT); ctx->dworld->addRigidBody(body, ed->xv->dimension_solid, ed->xv->dimension_hit); modified = qtrue; } } body = (btRigidBody*)ed->rbe.body.body; // get current data from entity gravity = qtrue; VectorCopy(ed->v->origin, origin); VectorCopy(ed->v->velocity, velocity); VectorCopy(ed->v->angles, angles); VectorCopy(ed->v->avelocity, avelocity); if (ed == world->edicts || (ed->xv->gravity && ed->xv->gravity <= 0.01)) gravity = qfalse; // compatibility for legacy entities // if (!DotProduct(forward,forward) || solid == SOLID_BSP) { vec3_t qangles, qavelocity; VectorCopy(angles, qangles); VectorCopy(avelocity, qavelocity); if (ed->v->modelindex) { model = world->Get_CModel(world, ed->v->modelindex); if (!model || model->type == mod_alias) { qangles[PITCH] *= -1; qavelocity[PITCH] *= -1; } } rbefuncs->AngleVectors(qangles, forward, left, up); VectorNegate(left,left); // convert single-axis rotations in avelocity to spinvelocity // FIXME: untested math - check signs VectorSet(spinvelocity, DEG2RAD(qavelocity[PITCH]), DEG2RAD(qavelocity[ROLL]), DEG2RAD(qavelocity[YAW])); } // compatibility for legacy entities switch (solid) { case SOLID_BBOX: case SOLID_SLIDEBOX: case SOLID_CORPSE: VectorSet(forward, 1, 0, 0); VectorSet(left, 0, 1, 0); VectorSet(up, 0, 0, 1); VectorSet(spinvelocity, 0, 0, 0); break; } // we must prevent NANs... test = DotProduct(origin,origin) + DotProduct(forward,forward) + DotProduct(left,left) + DotProduct(up,up) + DotProduct(velocity,velocity) + DotProduct(spinvelocity,spinvelocity); if (IS_NAN(test)) { modified = qtrue; //Con_Printf("Fixing NAN values on entity %i : .classname = \"%s\" .origin = '%f %f %f' .velocity = '%f %f %f' .axis_forward = '%f %f %f' .axis_left = '%f %f %f' .axis_up = %f %f %f' .spinvelocity = '%f %f %f'\n", PRVM_NUM_FOR_EDICT(ed), PRVM_GetString(PRVM_EDICTFIELDVALUE(ed, prog->fieldoffsets.classname)->string), origin[0], origin[1], origin[2], velocity[0], velocity[1], velocity[2], forward[0], forward[1], forward[2], left[0], left[1], left[2], up[0], up[1], up[2], spinvelocity[0], spinvelocity[1], spinvelocity[2]); Con_Printf("Fixing NAN values on entity %i : .classname = \"%s\" .origin = '%f %f %f' .velocity = '%f %f %f' .angles = '%f %f %f' .avelocity = '%f %f %f'\n", NUM_FOR_EDICT(world->progs, (edict_t*)ed), PR_GetString(world->progs, ed->v->classname), origin[0], origin[1], origin[2], velocity[0], velocity[1], velocity[2], angles[0], angles[1], angles[2], avelocity[0], avelocity[1], avelocity[2]); test = DotProduct(origin,origin); if (IS_NAN(test)) VectorClear(origin); test = DotProduct(forward,forward) * DotProduct(left,left) * DotProduct(up,up); if (IS_NAN(test)) { VectorSet(angles, 0, 0, 0); VectorSet(forward, 1, 0, 0); VectorSet(left, 0, 1, 0); VectorSet(up, 0, 0, 1); } test = DotProduct(velocity,velocity); if (IS_NAN(test)) VectorClear(velocity); test = DotProduct(spinvelocity,spinvelocity); if (IS_NAN(test)) { VectorClear(avelocity); VectorClear(spinvelocity); } } // check if the qc edited any position data if ( 0//!VectorCompare(origin, ed->rbe.origin) || !VectorCompare(velocity, ed->rbe.velocity) //|| !VectorCompare(angles, ed->rbe.angles) || !VectorCompare(avelocity, ed->rbe.avelocity) || gravity != ed->rbe.gravity) modified = qtrue; // store the qc values into the physics engine body = (btRigidBody*)ed->rbe.body.body; if (modified && body) { // dVector3 r[3]; // float entitymatrix[16]; // float bodymatrix[16]; #if 0 Con_Printf("entity %i got changed by QC\n", (int) (ed - prog->edicts)); if(!VectorCompare(origin, ed->rbe.origin)) Con_Printf(" origin: %f %f %f -> %f %f %f\n", ed->rbe.origin[0], ed->rbe.origin[1], ed->rbe.origin[2], origin[0], origin[1], origin[2]); if(!VectorCompare(velocity, ed->rbe.velocity)) Con_Printf(" velocity: %f %f %f -> %f %f %f\n", ed->rbe.velocity[0], ed->rbe.velocity[1], ed->rbe.velocity[2], velocity[0], velocity[1], velocity[2]); if(!VectorCompare(angles, ed->rbe.angles)) Con_Printf(" angles: %f %f %f -> %f %f %f\n", ed->rbe.angles[0], ed->rbe.angles[1], ed->rbe.angles[2], angles[0], angles[1], angles[2]); if(!VectorCompare(avelocity, ed->rbe.avelocity)) Con_Printf(" avelocity: %f %f %f -> %f %f %f\n", ed->rbe.avelocity[0], ed->rbe.avelocity[1], ed->rbe.avelocity[2], avelocity[0], avelocity[1], avelocity[2]); if(gravity != ed->rbe.gravity) Con_Printf(" gravity: %i -> %i\n", ed->ide.ode_gravity, gravity); #endif // values for BodyFromEntity to check if the qc modified anything later VectorCopy(origin, ed->rbe.origin); VectorCopy(velocity, ed->rbe.velocity); VectorCopy(angles, ed->rbe.angles); VectorCopy(avelocity, ed->rbe.avelocity); ed->rbe.gravity = gravity; // foo Matrix4x4_RM_FromVectors(entitymatrix, forward, left, up, origin); // foo Matrix4_Multiply(ed->rbe.offsetmatrix, entitymatrix, bodymatrix); // foo Matrix3x4_RM_ToVectors(bodymatrix, forward, left, up, origin); // r[0][0] = forward[0]; // r[1][0] = forward[1]; // r[2][0] = forward[2]; // r[0][1] = left[0]; // r[1][1] = left[1]; // r[2][1] = left[2]; // r[0][2] = up[0]; // r[1][2] = up[1]; // r[2][2] = up[2]; auto ms = (QCMotionState*)body->getMotionState(); ms->ReloadMotionState(); body->setMotionState(ms); body->setLinearVelocity(btVector3(velocity[0], velocity[1], velocity[2])); body->setAngularVelocity(btVector3(spinvelocity[0], spinvelocity[1], spinvelocity[2])); // body->setGravity(btVector3(ed->xv->gravitydir[0], ed->xv->gravitydir[1], ed->xv->gravitydir[2]) * ed->xv->gravity); //something changed. make sure it still falls over appropriately body->setActivationState(1); } /* FIXME: check if we actually need this insanity with bullet (ode sucks). if(body) { // limit movement speed to prevent missed collisions at high speed btVector3 ovelocity = body->getLinearVelocity(); btVector3 ospinvelocity = body->getAngularVelocity(); movelimit = ed->rbe.movelimit * world->rbe.movelimit; test = DotProduct(ovelocity,ovelocity); if (test > movelimit*movelimit) { // scale down linear velocity to the movelimit // scale down angular velocity the same amount for consistency f = movelimit / sqrt(test); VectorScale(ovelocity, f, velocity); VectorScale(ospinvelocity, f, spinvelocity); body->setLinearVelocity(btVector3(velocity[0], velocity[1], velocity[2])); body->setAngularVelocity(btVector3(spinvelocity[0], spinvelocity[1], spinvelocity[2])); } // make sure the angular velocity is not exploding spinlimit = physics_bullet_spinlimit.value; test = DotProduct(ospinvelocity,ospinvelocity); if (test > spinlimit) { body->setAngularVelocity(btVector3(0, 0, 0)); } } */ } /* #define MAX_CONTACTS 16 static void VARGS nearCallback (void *data, dGeomID o1, dGeomID o2) { world_t *world = (world_t *)data; dContact contact[MAX_CONTACTS]; // max contacts per collision pair dBodyID b1; dBodyID b2; dJointID c; int i; int numcontacts; float bouncefactor1 = 0.0f; float bouncestop1 = 60.0f / 800.0f; float bouncefactor2 = 0.0f; float bouncestop2 = 60.0f / 800.0f; float erp; dVector3 grav; wedict_t *ed1, *ed2; if (dGeomIsSpace(o1) || dGeomIsSpace(o2)) { // colliding a space with something dSpaceCollide2(o1, o2, data, &nearCallback); // Note we do not want to test intersections within a space, // only between spaces. //if (dGeomIsSpace(o1)) dSpaceCollide(o1, data, &nearCallback); //if (dGeomIsSpace(o2)) dSpaceCollide(o2, data, &nearCallback); return; } b1 = dGeomGetBody(o1); b2 = dGeomGetBody(o2); // at least one object has to be using MOVETYPE_PHYSICS or we just don't care if (!b1 && !b2) return; // exit without doing anything if the two bodies are connected by a joint if (b1 && b2 && dAreConnectedExcluding(b1, b2, dJointTypeContact)) return; ed1 = (wedict_t *) dGeomGetData(o1); ed2 = (wedict_t *) dGeomGetData(o2); if (ed1 == ed2 && ed1) { //ragdolls don't make contact with the bbox of the doll entity //the origional entity should probably not be solid anyway. //these bodies should probably not collide against bboxes of other entities with ragdolls either, but meh. if (ed1->rbe.body.body == b1 || ed2->rbe.body == b2) return; } if(!ed1 || ed1->isfree) ed1 = world->edicts; if(!ed2 || ed2->isfree) ed2 = world->edicts; // generate contact points between the two non-space geoms numcontacts = dCollide(o1, o2, MAX_CONTACTS, &(contact[0].geom), sizeof(contact[0])); if (numcontacts) { if(ed1 && ed1->v->touch) { world->Event_Touch(world, ed1, ed2); } if(ed2 && ed2->v->touch) { world->Event_Touch(world, ed2, ed1); } // if either ent killed itself, don't collide if ((ed1&&ed1->isfree) || (ed2&&ed2->isfree)) return; } if(ed1) { if (ed1->xv->bouncefactor) bouncefactor1 = ed1->xv->bouncefactor; if (ed1->xv->bouncestop) bouncestop1 = ed1->xv->bouncestop; } if(ed2) { if (ed2->xv->bouncefactor) bouncefactor2 = ed2->xv->bouncefactor; if (ed2->xv->bouncestop) bouncestop2 = ed2->xv->bouncestop; } if ((ed2->entnum&&ed1->v->owner == ed2->entnum) || (ed1->entnum&&ed2->v->owner == ed1->entnum)) return; // merge bounce factors and bounce stop if(bouncefactor2 > 0) { if(bouncefactor1 > 0) { // TODO possibly better logic to merge bounce factor data? if(bouncestop2 < bouncestop1) bouncestop1 = bouncestop2; if(bouncefactor2 > bouncefactor1) bouncefactor1 = bouncefactor2; } else { bouncestop1 = bouncestop2; bouncefactor1 = bouncefactor2; } } dWorldGetGravity(world->rbe.world, grav); bouncestop1 *= fabs(grav[2]); erp = (DotProduct(ed1->v->velocity, ed1->v->velocity) > DotProduct(ed2->v->velocity, ed2->v->velocity)) ? ed1->xv->erp : ed2->xv->erp; // add these contact points to the simulation for (i = 0;i < numcontacts;i++) { contact[i].surface.mode = (physics_bullet_contact_mu.value != -1 ? dContactApprox1 : 0) | (physics_bullet_contact_erp.value != -1 ? dContactSoftERP : 0) | (physics_bullet_contact_cfm.value != -1 ? dContactSoftCFM : 0) | (bouncefactor1 > 0 ? dContactBounce : 0); contact[i].surface.mu = physics_bullet_contact_mu.value; if (ed1->xv->friction) contact[i].surface.mu *= ed1->xv->friction; if (ed2->xv->friction) contact[i].surface.mu *= ed2->xv->friction; contact[i].surface.mu2 = 0; contact[i].surface.soft_erp = physics_bullet_contact_erp.value + erp; contact[i].surface.soft_cfm = physics_bullet_contact_cfm.value; contact[i].surface.bounce = bouncefactor1; contact[i].surface.bounce_vel = bouncestop1; c = dJointCreateContact(world->rbe.world, world->rbe.contactgroup, contact + i); dJointAttach(c, b1, b2); } } */ static void QDECL World_Bullet_Frame(world_t *world, double frametime, double gravity) { auto ctx = reinterpret_cast(world->rbe); if (world->rbe_hasphysicsents || ctx->hasextraobjs) { int iters; unsigned int i; wedict_t *ed; // world->rbe.iterations = bound(1, physics_bullet_iterationsperframe.ival, 1000); // world->rbe.step = frametime / world->rbe.iterations; // world->rbe.movelimit = physics_bullet_movelimit.value / world->rbe.step; // copy physics properties from entities to physics engine for (i = 0;i < world->num_edicts;i++) { ed = WEDICT_NUM_PB(world->progs, i); if (!ED_ISFREE(ed)) World_Bullet_Frame_BodyFromEntity(world, ed); } // oh, and it must be called after all bodies were created for (i = 0;i < world->num_edicts;i++) { ed = WEDICT_NUM_PB(world->progs, i); if (!ED_ISFREE(ed)) World_Bullet_Frame_JointFromEntity(world, ed); } while(ctx->cmdqueuehead) { auto cmd = ctx->cmdqueuehead; ctx->cmdqueuehead = cmd->next; if (!cmd->next) ctx->cmdqueuetail = nullptr; World_Bullet_RunCmd(world, cmd); Z_Free(cmd); } ctx->dworld->setGravity(btVector3(0, 0, -gravity)); iters=physics_bullet_maxiterationsperframe->value; if (iters < 0) iters = 0; ctx->dworld->stepSimulation(frametime, iters, 1/bound(1, physics_bullet_framerate->value, 500)); // set the tolerance for closeness of objects // dWorldSetContactSurfaceLayer(world->rbe.world, max(0, physics_bullet_contactsurfacelayer.value)); // run collisions for the current world state, creating JointGroup // dSpaceCollide(world->rbe.space, (void *)world, nearCallback); // run physics (move objects, calculate new velocities) // if (physics_bullet_worldquickstep.ival) // { // dWorldSetQuickStepNumIterations(world->rbe.world, bound(1, physics_bullet_worldquickstep_iterations.ival, 200)); // dWorldQuickStep(world->rbe.world, world->rbe.step); // } // else // dWorldStep(world->rbe.world, world->rbe.step); // clear the JointGroup now that we're done with it // dJointGroupEmpty(world->rbe.contactgroup); } } static void World_Bullet_RunCmd(world_t *world, rbecommandqueue_t *cmd) { auto body = (btRigidBody*)(cmd->edict->rbe.body.body); switch(cmd->command) { case RBECMD_ENABLE: if (body) body->setActivationState(1); break; case RBECMD_DISABLE: if (body) body->setActivationState(0); break; case RBECMD_FORCE: if (body) { btVector3 relativepos; const btVector3 ¢er = body->getCenterOfMassPosition(); VectorSubtract(cmd->v2, center, relativepos); body->setActivationState(1); body->applyImpulse(btVector3(cmd->v1[0], cmd->v1[1], cmd->v1[2]), relativepos); } break; case RBECMD_TORQUE: if (cmd->edict->rbe.body.body) { body->setActivationState(1); body->applyTorque(btVector3(cmd->v1[0], cmd->v1[1], cmd->v1[2])); } break; } } static void QDECL World_Bullet_PushCommand(world_t *world, rbecommandqueue_t *val) { auto ctx = reinterpret_cast(world->rbe); auto cmd = (rbecommandqueue_t*)BZ_Malloc(sizeof(rbecommandqueue_t)); world->rbe_hasphysicsents = qtrue; //just in case. memcpy(cmd, val, sizeof(*cmd)); cmd->next = nullptr; //add on the end of the queue, so that order is preserved. if (ctx->cmdqueuehead) { auto ot = ctx->cmdqueuetail; ot->next = ctx->cmdqueuetail = cmd; } else ctx->cmdqueuetail = ctx->cmdqueuehead = cmd; } static void QDECL World_Bullet_TraceEntity(world_t *world, wedict_t *ed, vec3_t start, vec3_t end, trace_t *trace) { auto ctx = reinterpret_cast(world->rbe); auto shape = (btCollisionShape*)ed->rbe.body.geom; //btCollisionAlgorithm class myConvexResultCallback : public btCollisionWorld::ConvexResultCallback { public: void *m_impactent; btVector3 m_impactpos; btVector3 m_impactnorm; virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) { if (m_closestHitFraction > convexResult.m_hitFraction) { m_closestHitFraction = convexResult.m_hitFraction; m_impactpos = convexResult.m_hitPointLocal; m_impactnorm = convexResult.m_hitNormalLocal; m_impactent = convexResult.m_hitCollisionObject->getUserPointer(); } return 0; } } result; result.m_impactent = nullptr; result.m_closestHitFraction = trace->fraction; btTransform from(btMatrix3x3(1, 0, 0, 0, 1, 0, 0, 0, 1), btVector3(start[0], start[1], start[2])); btTransform to(btMatrix3x3(1, 0, 0, 0, 1, 0, 0, 0, 1), btVector3(end[0], end[1], end[2])); ctx->dworld->convexSweepTest((btConvexShape*)shape, from, to, result, 1); if (result.m_impactent) { memset(trace, 0, sizeof(*trace)); trace->fraction = trace->truefraction = result.m_closestHitFraction; VectorInterpolate(start, result.m_closestHitFraction, end, trace->endpos); // VectorCopy(result.m_impactpos, trace->endpos); VectorCopy(result.m_impactnorm, trace->plane.normal); trace->ent = result.m_impactent; trace->startsolid = qfalse; //FIXME: we don't really know } } static void QDECL World_Bullet_Start(world_t *world) { bulletcontext_t *ctx; if (world->rbe) return; //no thanks, we already have one. somehow. ctx = reinterpret_cast(BZ_Malloc(sizeof(*ctx))); memset(ctx, 0, sizeof(*ctx)); ctx->gworld = world; ctx->funcs.End = World_Bullet_End; ctx->funcs.RemoveJointFromEntity = World_Bullet_RemoveJointFromEntity; ctx->funcs.RemoveFromEntity = World_Bullet_RemoveFromEntity; ctx->funcs.RagMatrixToBody = World_Bullet_RagMatrixToBody; ctx->funcs.RagCreateBody = World_Bullet_RagCreateBody; ctx->funcs.RagMatrixFromJoint = World_Bullet_RagMatrixFromJoint; ctx->funcs.RagMatrixFromBody = World_Bullet_RagMatrixFromBody; ctx->funcs.RagEnableJoint = World_Bullet_RagEnableJoint; ctx->funcs.RagCreateJoint = World_Bullet_RagCreateJoint; ctx->funcs.RagDestroyBody = World_Bullet_RagDestroyBody; ctx->funcs.RagDestroyJoint = World_Bullet_RagDestroyJoint; ctx->funcs.RunFrame = World_Bullet_Frame; ctx->funcs.PushCommand = World_Bullet_PushCommand; ctx->funcs.Trace = World_Bullet_TraceEntity; world->rbe = &ctx->funcs; ctx->broadphase = new btDbvtBroadphase(); ctx->collisionconfig = new btDefaultCollisionConfiguration(); ctx->collisiondispatcher = new btCollisionDispatcher(ctx->collisionconfig); ctx->solver = new btSequentialImpulseConstraintSolver; ctx->dworld = new btDiscreteDynamicsWorld(ctx->collisiondispatcher, ctx->broadphase, ctx->solver, ctx->collisionconfig); ctx->ownerfilter = new QCFilterCallback(); ctx->dworld->getPairCache()->setOverlapFilterCallback(ctx->ownerfilter); ctx->dworld->setGravity(btVector3(0, -10, 0)); /* if(physics_bullet_world_erp.value >= 0) dWorldSetERP(world->rbe.world, physics_bullet_world_erp.value); if(physics_bullet_world_cfm.value >= 0) dWorldSetCFM(world->rbe.world, physics_bullet_world_cfm.value); if (physics_bullet_world_damping.ival) { dWorldSetLinearDamping(world->rbe.world, (physics_bullet_world_damping_linear.value >= 0) ? (physics_bullet_world_damping_linear.value * physics_bullet_world_damping.value) : 0); dWorldSetLinearDampingThreshold(world->rbe.world, (physics_bullet_world_damping_linear_threshold.value >= 0) ? (physics_bullet_world_damping_linear_threshold.value * physics_bullet_world_damping.value) : 0); dWorldSetAngularDamping(world->rbe.world, (physics_bullet_world_damping_angular.value >= 0) ? (physics_bullet_world_damping_angular.value * physics_bullet_world_damping.value) : 0); dWorldSetAngularDampingThreshold(world->rbe.world, (physics_bullet_world_damping_angular_threshold.value >= 0) ? (physics_bullet_world_damping_angular_threshold.value * physics_bullet_world_damping.value) : 0); } else { dWorldSetLinearDamping(world->rbe.world, 0); dWorldSetLinearDampingThreshold(world->rbe.world, 0); dWorldSetAngularDamping(world->rbe.world, 0); dWorldSetAngularDampingThreshold(world->rbe.world, 0); } if (physics_bullet_autodisable.ival) { dWorldSetAutoDisableSteps(world->rbe.world, bound(1, physics_bullet_autodisable_steps.ival, 100)); dWorldSetAutoDisableTime(world->rbe.world, physics_bullet_autodisable_time.value); dWorldSetAutoDisableAverageSamplesCount(world->rbe.world, bound(1, physics_bullet_autodisable_threshold_samples.ival, 100)); dWorldSetAutoDisableLinearThreshold(world->rbe.world, physics_bullet_autodisable_threshold_linear.value); dWorldSetAutoDisableAngularThreshold(world->rbe.world, physics_bullet_autodisable_threshold_angular.value); dWorldSetAutoDisableFlag (world->rbe.world, true); } else dWorldSetAutoDisableFlag (world->rbe.world, false); */ } static void QDECL World_Bullet_Shutdown(void) { if (rbefuncs) rbefuncs->UnregisterPhysicsEngine("Bullet"); } static bool World_Bullet_DoInit(void) { if (!rbefuncs || !rbefuncs->RegisterPhysicsEngine) { rbefuncs = nullptr; Con_Printf("Bullet plugin failed: Engine is incompatible.\n"); } else if (!rbefuncs->RegisterPhysicsEngine("Bullet", World_Bullet_Start)) Con_Printf("Bullet plugin failed: Engine already has a physics plugin active.\n"); else { World_Bullet_Init(); return true; } return false; } extern "C" qboolean Plug_Init(void) { rbefuncs = (rbeplugfuncs_t*)plugfuncs->GetEngineInterface("RBE", sizeof(*rbefuncs)); if (rbefuncs && ( rbefuncs->version < RBEPLUGFUNCS_VERSION || rbefuncs->wedictsize != sizeof(wedict_t))) rbefuncs = nullptr; plugfuncs->ExportFunction("Shutdown", (funcptr_t)World_Bullet_Shutdown); return World_Bullet_DoInit()?qtrue:qfalse; } ================================================ FILE: plugins/cef/cef.c ================================================ //fte defs #include "../plugin.h" #include "../engine.h" static plugfsfuncs_t *fsfuncs; static plugsubconsolefuncs_t *confuncs; static plugclientfuncs_t *clientfuncs; #pragma GCC diagnostic ignored "-Wstrict-prototypes" //not my bug. //libcef defs #include "include/cef_version.h" #include "include/capi/cef_app_capi.h" #include "include/capi/cef_client_capi.h" #include "include/capi/cef_parser_capi.h" #include "include/capi/cef_request_context_handler_capi.h" //#include "include/capi/cef_url_capi.h" #include "assert.h" #if defined(_DEBUG) && defined(_MSC_VER) #include #endif #ifndef _WIN32 #include #endif #include #include #define EXPECTED_COMMIT_NUMBER 2179 //last version of libcef we tried building against... #if EXPECTED_COMMIT_NUMBER != EXPECTED_COMMIT_NUMBER #warning "libcef version different from expected. expect problems with libcef's unstable API." #endif #define cef_addref(ptr) (ptr)->base.add_ref(&(ptr)->base) #define cef_release(ptr) (((ptr)->base.release)(&(ptr)->base)) #if !defined(LIBCEF_STATIC) && !defined(LIBCEF_DYNAMIC) #define LIBCEF_DYNAMIC #endif #ifdef LIBCEF_DYNAMIC //avoid conflicts with cef headers #define cef_version_info pcef_version_info #define cef_initialize pcef_initialize #define cef_do_message_loop_work pcef_do_message_loop_work #define cef_shutdown pcef_shutdown #define cef_execute_process pcef_execute_process #define cef_browser_host_create_browser_sync pcef_browser_host_create_browser_sync #define cef_string_utf8_to_utf16 pcef_string_utf8_to_utf16 #define cef_string_utf16_to_utf8 pcef_string_utf16_to_utf8 #define cef_string_utf16_clear pcef_string_utf16_clear #define cef_string_utf16_set pcef_string_utf16_set #define cef_string_utf8_clear pcef_string_utf8_clear #define cef_string_utf8_set pcef_string_utf8_set #define cef_string_userfree_utf16_free pcef_string_userfree_utf16_free #define cef_register_scheme_handler_factory pcef_register_scheme_handler_factory #define cef_get_mime_type pcef_get_mime_type #define cef_v8value_create_function pcef_v8value_create_function #define cef_v8value_create_string pcef_v8value_create_string #define cef_process_message_create pcef_process_message_create #define cef_v8context_get_current_context pcef_v8context_get_current_context #define cef_post_task pcef_post_task #define cef_request_context_create_context pcef_request_context_create_context #define cef_string_multimap_alloc pcef_string_multimap_alloc #define cef_string_multimap_append pcef_string_multimap_append #define cef_string_multimap_size pcef_string_multimap_size #define cef_string_multimap_key pcef_string_multimap_key #define cef_string_multimap_value pcef_string_multimap_value #define cef_string_multimap_free pcef_string_multimap_free #define cef_string_list_size pcef_string_list_size #define cef_string_list_value pcef_string_list_value static int (*cef_version_info)(int entry); static int (*cef_initialize)(const struct _cef_main_args_t* args, const cef_settings_t* settings, cef_app_t* application, void* windows_sandbox_info); static void (*cef_do_message_loop_work)(void); static void (*cef_shutdown)(void); static int (*cef_execute_process)(const cef_main_args_t* args, cef_app_t* application, void* windows_sandbox_info); static cef_browser_t* (*cef_browser_host_create_browser_sync)(const cef_window_info_t* windowInfo, cef_client_t* client, const cef_string_t* url, const cef_browser_settings_t* settings, cef_dictionary_value_t* extra_info, cef_request_context_t* request_context); static int (*cef_string_utf8_to_utf16)(const char* src, size_t src_len, cef_string_utf16_t* output); static int (*cef_string_utf16_to_utf8)(const char16* src, size_t src_len, cef_string_utf8_t* output); static void (*cef_string_utf16_clear)(cef_string_utf16_t* str); static int (*cef_string_utf16_set)(const char16* src, size_t src_len, cef_string_utf16_t* output, int copy); static void (*cef_string_utf8_clear)(cef_string_utf8_t* str); static int (*cef_string_utf8_set)(const char* src, size_t src_len, cef_string_utf8_t* output, int copy); static void (*cef_string_userfree_utf16_free)(cef_string_userfree_utf16_t str); static int (*cef_register_scheme_handler_factory)(const cef_string_t* scheme_name, const cef_string_t* domain_name, cef_scheme_handler_factory_t* factory); static cef_string_userfree_t (*cef_get_mime_type)(const cef_string_t* extension); static cef_v8value_t* (*cef_v8value_create_function)(const cef_string_t* name, cef_v8handler_t* handler); static cef_v8value_t* (*cef_v8value_create_string)(const cef_string_t* value); static cef_process_message_t* (*cef_process_message_create)(const cef_string_t* name); static cef_v8context_t* (*cef_v8context_get_current_context)(void); //typical C++ programmers omitted the void. static int (*cef_post_task)(cef_thread_id_t threadId, cef_task_t* task); static cef_request_context_t* (*cef_request_context_create_context)(const cef_request_context_settings_t* settings, cef_request_context_handler_t* handler); static cef_string_multimap_t (*cef_string_multimap_alloc)(void); static int (*cef_string_multimap_append)(cef_string_multimap_t map, const cef_string_t* key, const cef_string_t* value); static size_t (*cef_string_multimap_size)(cef_string_multimap_t map); static int (*cef_string_multimap_key)(cef_string_multimap_t map, size_t index, cef_string_t* key); static int (*cef_string_multimap_value)(cef_string_multimap_t map, size_t index, cef_string_t* value); static void (*cef_string_multimap_free)(cef_string_multimap_t map); static size_t (*cef_string_list_size)(cef_string_list_t list); static int (*cef_string_list_value)(cef_string_list_t list, size_t index, cef_string_t* value); #endif static cvar_t *cef_incognito; static cvar_t *cef_allowplugins; static cvar_t *cef_allowcvars; static cvar_t *cef_devtools; static char plugname[MAX_OSPATH]; static char *newconsole; /*static void setcefstring(char *str, cef_string_t *r) { cef_string_from_utf8(str, strlen(str), r); }*/ static cef_string_t makecefstring(char *str) { cef_string_t r = {NULL}; cef_string_from_utf8(str, strlen(str), &r); return r; } static cef_string_t *makecefstringptr(char *str, cef_string_t *ptr) { cef_string_from_utf8(str, strlen(str), ptr); return ptr; } static char *Info_JSONify (char *s, char *o, size_t outlen) { outlen--; //so we don't have to consider nulls if (*s == '\\') s++; while (*s) { //min overhead if (outlen < 6) break; outlen -= 6; *o++ = ','; *o++ = '\"'; for (; *s && *s != '\\'; s++) { if (*s != '\"' && outlen) { outlen--; *o++ = *s; } } *o++ = '\"'; *o++ = ':'; *o++ = '\"'; if (!*s++) { //should never happen. *o++ = '\"'; *o = 0; return o; } for (; *s && *s != '\\'; s++) { if (*s != '\"' && outlen) { outlen--; *o++ = *s; } } *o++ = '\"'; if (*s) s++; } *o = 0; return o; } #ifdef _MSC_VER #define atomic_fetch_add(p,v) (InterlockedIncrement(p)-1) #define atomic_fetch_sub(p,v) (InterlockedDecrement(p)+1) #define atomic_uint32_t LONG #else #define atomic_fetch_add __sync_fetch_and_add #define atomic_fetch_sub __sync_fetch_and_sub #define atomic_uint32_t unsigned int #endif typedef struct { atomic_uint32_t refcount; //this needs to be atomic to avoid multiple threads adding at the same time //cef interface objects cef_client_t client; cef_render_handler_t render_handler; cef_display_handler_t display_handler; cef_request_handler_t request_handler; cef_life_span_handler_t life_span_handler; cef_context_menu_handler_t context_menu_handler; cef_browser_t *thebrowser; void *videodata; int videowidth; int videoheight; qboolean updated; int desiredwidth; int desiredheight; char *consolename; //for internal plugin use. qboolean fullscreen; cef_string_utf8_t currenturl; cef_string_utf8_t currenticon; cef_string_utf8_t currenttitle; cef_string_utf8_t currentstatus; cef_mouse_event_t mousepos; unsigned char keystate[K_MAX]; } browser_t; unsigned int numbrowsers; static void browser_addref(browser_t *br) { atomic_fetch_add(&br->refcount, 1); } static int browser_release(browser_t *br) { if (atomic_fetch_sub(&br->refcount, 1) == 1) { if (br->consolename) free(br->consolename); if (br->videodata) free(br->videodata); cef_string_utf8_clear(&br->currenturl); cef_string_utf8_clear(&br->currenticon); cef_string_utf8_clear(&br->currenttitle); cef_string_utf8_clear(&br->currentstatus); numbrowsers--; free(br); return true; } return false; } #define browser_subs(sub) \ static void CEF_CALLBACK browser_##sub##_addref(cef_base_ref_counted_t* self) {browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, sub.base)); browser_addref(br);}; \ static int CEF_CALLBACK browser_##sub##_release(cef_base_ref_counted_t* self) {browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, sub.base)); return browser_release(br);}; \ static int CEF_CALLBACK browser_##sub##_hasoneref(cef_base_ref_counted_t* self) {browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, sub.base)); return br->refcount == 1;}; browser_subs(client); browser_subs(render_handler); browser_subs(display_handler); browser_subs(request_handler); browser_subs(life_span_handler); browser_subs(context_menu_handler); #undef browser_subs //client methods static cef_render_handler_t *CEF_CALLBACK browser_get_render_handler(cef_client_t *self) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, client)); cef_addref(&br->render_handler); return &br->render_handler; } static cef_life_span_handler_t *CEF_CALLBACK browser_get_life_span_handler(cef_client_t *self) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, client)); cef_addref(&br->life_span_handler); return &br->life_span_handler; } static cef_context_menu_handler_t *CEF_CALLBACK browser_get_context_menu_handler(cef_client_t *self) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, client)); cef_addref(&br->context_menu_handler); return &br->context_menu_handler; } static cef_display_handler_t *CEF_CALLBACK browser_get_display_handler(cef_client_t *self) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, client)); cef_addref(&br->display_handler); return &br->display_handler; } static cef_request_handler_t *CEF_CALLBACK browser_get_request_handler(cef_client_t *self) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, client)); cef_addref(&br->request_handler); return &br->request_handler; } static qboolean browser_handle_query(const char *req, char *buffer, size_t buffersize) { if (!req) return false; else if (!strncmp(req, "getcvar_", 8)) { if (cef_allowcvars->value && cvarfuncs->GetString(req+8, buffer, buffersize)) return true; } else if (!strncmp(req, "setcvar_", 8)) { const char *eq = strchr(req+8, '='); if (eq) *(char*)eq++ = 0; else eq = req+strlen(req); if (cef_allowcvars->value) { cvarfuncs->SetString(req+8, eq); *buffer = 0; return true; } } else if (!strcmp(req, "getstats")) { //1 [, one sign, 10 chars, one ], one comma //FIXME: should be more than just a one-off. unsigned int stats[256], i, m; char *e = buffer; m = clientfuncs->GetStats(0, stats, countof(stats)); if (!m) { m = 0; stats[m++] = 0; } *e++ = '['; for (i = 0; i < m; i++) { if (i) *e++ = ','; sprintf(e, "%i", (int)stats[i]); e += strlen(e); } *e++ = ']'; *e = 0; assert(e <= buffer + buffersize); return true; } else if (!strcmp(req, "getseats")) { int i; char *e = buffer; int players[MAX_SPLITS]; int tracks[MAX_SPLITS]; int seats = clientfuncs->GetLocalPlayerNumbers(0, MAX_SPLITS, players, tracks); *e++ = '['; for (i = 0; i < seats; i++) { if (i) *e++ = ','; sprintf(e, "{\"player\":%i,\"track\":%i}", players[i], tracks[i]); e += strlen(e); } *e++ = ']'; *e = 0; assert(e <= buffer + buffersize); return true; } else if (!strcmp(req, "getserverinfo")) { char serverinfo[4096]; char *e = buffer; clientfuncs->GetServerInfoRaw(serverinfo, sizeof(serverinfo)); e = Info_JSONify(serverinfo, e, buffer + buffersize - e-1); if (e == buffer) e++; *buffer = '{'; *e++ = '}'; *e = 0; assert(e <= buffer + buffersize); return true; } else if (!strcmp(req, "getplayers")) { unsigned int i; char *e = buffer; plugclientinfo_t info; *e++ = '['; for (i = 0; ; i++) { clientfuncs->GetPlayerInfo(i, &info); if (buffer + buffersize - e-1 < 100) break; if (i) *e++ = ','; *e++ = '{'; //splurge the specific info sprintf(e, "\"frags\":%i,\"ping\":%i,\"pl\":%i,\"active\":%i,\"userid\":%i", info.frags, info.ping, info.pl, info.activetime, info.userid); e += strlen(e); //splurge the generic info (colours, name, team) e = Info_JSONify(info.userinfo, e, buffer + buffersize - e-1); *e++ = '}'; } *e++ = ']'; *e = 0; assert(e <= buffer + buffersize); return true; } return false; } static int CEF_CALLBACK browser_on_process_message_received(cef_client_t* self, cef_browser_t* browser, cef_frame_t* frame, cef_process_id_t source_process, cef_process_message_t* message) { int handled = false; // browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, request_handler)); cef_string_userfree_t msgnameunusable = message->get_name(message); cef_string_utf8_t name = {NULL}; cef_string_to_utf8(msgnameunusable->str, msgnameunusable->length, &name); if (!strcmp(name.str, "fte_query")) { char buffer[8192]; int id1, id2; cef_process_message_t *reply; cef_string_utf8_t queryname = {NULL}; cef_string_t str = {NULL}; cef_list_value_t *args = message->get_argument_list(message); cef_string_userfree_t cmdunusable = args->get_string(args, 0); cef_string_to_utf8(cmdunusable?cmdunusable->str:NULL, cmdunusable?cmdunusable->length:0, &queryname); id1 = args->get_int(args, 2); id2 = args->get_int(args, 3); cef_release(args); reply = cef_process_message_create(msgnameunusable); args = reply->get_argument_list(reply); args->set_string(args, 0, cmdunusable); args->set_int(args, 2, id1); args->set_int(args, 3, id2); if (browser_handle_query(queryname.str, buffer, sizeof(buffer))) { str = makecefstring(buffer); args->set_string(args, 1, &str); cef_string_clear(&str); } else args->set_null(args, 1); cef_release(args); cef_string_utf8_clear(&queryname); if (cmdunusable) cef_string_userfree_free(cmdunusable); frame->send_process_message(frame, source_process, reply); handled = true; } cef_release(message); cef_release(browser); cef_string_utf8_clear(&name); cef_string_userfree_free(msgnameunusable); // if (messagerouter->OnProcessMessageReceived(browser, source_process, message)) // return true; return handled; } //render_handler methods static void CEF_CALLBACK browser_get_view_rect(cef_render_handler_t *self, cef_browser_t *browser, cef_rect_t *rect) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, render_handler)); rect->x = 0; rect->y = 0; rect->width = br->desiredwidth; rect->height = br->desiredheight; cef_release(browser); } static void CEF_CALLBACK browser_on_paint(cef_render_handler_t *self, cef_browser_t *browser, cef_paint_element_type_t type, size_t dirtyRectsCount, cef_rect_t const* dirtyRects, const void* buffer, int width, int height) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, render_handler)); cef_release(browser); //popups are gonna be so awkward... if (type != PET_VIEW) return; /* if (pbocontext) { if (!PBO_Lock(pbocontext, width, height, &lost, rgba)) return; if (lost) PBO_Update(pbocontext, buffer, width, height, stride); else while (dirtyRectsCount --> 0) PBO_Update(pbocontext, (char*)buffer+(width*dirtyRects->y + dirtyRects->x)*4), dirtyRects->width, dirtyRects->height, width*4); PBO_Unlock(pbocontext); } else */ if (br->videowidth != width || br->videoheight != height) { //copy the entire thing. if (br->videodata) free(br->videodata); br->videowidth = width; br->videoheight = height; br->videodata = malloc(width * height * 4); memcpy(br->videodata, buffer, width * height * 4); } else { //try to save cpu time by copying only the dirty parts while (dirtyRectsCount --> 0) { if (width == dirtyRects->width && height == dirtyRects->height) memcpy(br->videodata, buffer, width * height * 4); else { int y; const unsigned int *src; unsigned int *dst; src = buffer; src += width * dirtyRects->y + dirtyRects->x; dst = br->videodata; dst += width * dirtyRects->y + dirtyRects->x; for (y = 0; y < dirtyRects->height; y++) { memcpy(dst, src, dirtyRects->width*4); src += width; dst += width; } } dirtyRects++; } } br->updated = true; } static void CEF_CALLBACK browser_on_before_close(cef_life_span_handler_t* self, cef_browser_t* browser) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, life_span_handler)); if (br->thebrowser) { //we may have already released our reference to this, if something else was blocking for some reason. cef_release(br->thebrowser); br->thebrowser = NULL; } cef_release(browser); } //context_menu_handler methods static void CEF_CALLBACK browser_on_before_context_menu(struct _cef_context_menu_handler_t* self, struct _cef_browser_t* browser, struct _cef_frame_t* frame, struct _cef_context_menu_params_t* params, struct _cef_menu_model_t* model) { // browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, context_menu_handler)); //wipe whatever elements libcef thinks it should add model->clear(model); //don't bother adding any new ones. cef_release(browser); cef_release(frame); cef_release(params); cef_release(model); } //display_handler methods //redirect console.log messages to quake's console, but only display them if we've got developer set. static int CEF_CALLBACK browser_on_console_message(cef_display_handler_t* self, cef_browser_t* browser, cef_log_severity_t level, const cef_string_t* message, const cef_string_t* source, int line) { cef_string_utf8_t u8_source = {NULL}; cef_string_utf8_t u8_message = {NULL}; if (source) cef_string_to_utf8(source->str, source->length, &u8_source); if (message) cef_string_to_utf8(message->str, message->length, &u8_message); Con_DPrintf("%s:%i: %s\n", u8_source.str, line, u8_message.str); cef_string_utf8_clear(&u8_source); cef_string_utf8_clear(&u8_message); cef_release(browser); return true; } static void CEF_CALLBACK browser_on_title_change(cef_display_handler_t* self, cef_browser_t* browser, const cef_string_t* title) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, display_handler)); if (title) cef_string_to_utf8(title->str, title->length, &br->currenttitle); else cef_string_utf8_copy(br->currenturl.str, br->currenturl.length, &br->currenttitle); if (br->consolename) confuncs->SetConsoleString(br->consolename, "title", br->currenttitle.str?br->currenttitle.str:""); cef_release(browser); } static void CEF_CALLBACK browser_on_favicon_urlchange(cef_display_handler_t* self, cef_browser_t* browser, cef_string_list_t favicon) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, display_handler)); cef_string_t str = {NULL}; if (favicon) { //size_t opts = cef_string_list_size(favicon); cef_string_list_value(favicon, 0, &str); } cef_string_to_utf8(str.str, str.length, &br->currenticon); cef_string_clear(&str); if (br->consolename) confuncs->SetConsoleString(br->consolename, "icon", br->currenticon.str?br->currenticon.str:""); cef_release(browser); } static void CEF_CALLBACK browser_on_fullscreenmode_change(cef_display_handler_t* self, cef_browser_t* browser, int fullscreen) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, display_handler)); br->fullscreen = fullscreen; if (br->consolename) confuncs->SetConsoleFloat(br->consolename, "fullscreen", br->fullscreen); cef_release(browser); } static void CEF_CALLBACK browser_on_address_change(cef_display_handler_t* self, cef_browser_t* browser, cef_frame_t* frame, const cef_string_t* url) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, display_handler)); cef_string_to_utf8(url->str, url->length, &br->currenturl); if (br->currenticon.length) { cef_string_utf8_clear(&br->currenticon); if (br->consolename) confuncs->SetConsoleString(br->consolename, "icon", br->currenticon.str?br->currenticon.str:""); } //FIXME: should probably make sure its the root frame // Con_Printf("new url: %s\n", url.ToString().c_str()); cef_release(browser); cef_release(frame); } static int CEF_CALLBACK browser_on_tooltip(cef_display_handler_t* self, cef_browser_t* browser, cef_string_t* text) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, display_handler)); if (br->consolename) { cef_string_utf8_t u8_text = {NULL}; cef_string_to_utf8(text->str, text->length, &u8_text); confuncs->SetConsoleString(br->consolename, "tooltip", u8_text.str?u8_text.str:""); cef_string_utf8_clear(&u8_text); } cef_release(browser); return true; //cef won't draw tooltips when running like this } static void CEF_CALLBACK browser_on_status_message(cef_display_handler_t* self, cef_browser_t* browser, const cef_string_t* value) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, display_handler)); if (br->consolename) { cef_string_utf8_t u8_value = {NULL}; if (value) cef_string_to_utf8(value->str, value->length, &u8_value); confuncs->SetConsoleString(br->consolename, "footer", u8_value.str?u8_value.str:""); cef_string_utf8_clear(&u8_value); } cef_release(browser); } //request_handler methods static int CEF_CALLBACK browser_on_before_browse(cef_request_handler_t* self, cef_browser_t* browser, cef_frame_t* frame, cef_request_t* request, int user_gesture, int is_redirect) { // browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, request_handler)); cef_release(browser); cef_release(frame); cef_release(request); return false; //false = allow navigation, true = block } static void CEF_CALLBACK browser_on_render_process_terminated(cef_request_handler_t* self, cef_browser_t* browser, cef_termination_status_t status) { browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, request_handler)); if (br->videodata) free(br->videodata); br->videowidth = 1; br->videoheight = 1; br->videodata = malloc(br->videowidth * br->videoheight * 4); memset(br->videodata, 0, br->videowidth * br->videoheight * 4); br->updated = true; cef_release(browser); } static browser_t *browser_create(void) { browser_t *nb = malloc(sizeof(*nb)); memset(nb, 0, sizeof(*nb)); nb->refcount = 1; #define browser_subs(sub) \ nb->sub.base.add_ref = browser_##sub##_addref; \ nb->sub.base.release = browser_##sub##_release; \ nb->sub.base.has_one_ref = browser_##sub##_hasoneref; \ nb->sub.base.size = sizeof(nb->sub); browser_subs(client); browser_subs(render_handler); browser_subs(display_handler); browser_subs(request_handler); browser_subs(life_span_handler); browser_subs(context_menu_handler); #undef browser_subs nb->client.get_context_menu_handler = browser_get_context_menu_handler; nb->client.get_dialog_handler = NULL; nb->client.get_display_handler = browser_get_display_handler; nb->client.get_download_handler = NULL; nb->client.get_drag_handler = NULL; nb->client.get_find_handler = NULL; nb->client.get_focus_handler = NULL; nb->client.get_jsdialog_handler = NULL; nb->client.get_keyboard_handler = NULL; nb->client.get_life_span_handler = browser_get_life_span_handler; nb->client.get_load_handler = NULL; nb->client.get_render_handler = browser_get_render_handler; nb->client.get_request_handler = browser_get_request_handler; nb->client.on_process_message_received = browser_on_process_message_received; // nb->render_handler.get_accessibility_handler = NULL; nb->render_handler.get_root_screen_rect = NULL; nb->render_handler.get_view_rect = browser_get_view_rect; nb->render_handler.get_screen_point = NULL; nb->render_handler.get_screen_info = NULL; nb->render_handler.on_popup_show = NULL; nb->render_handler.on_popup_size = NULL; nb->render_handler.on_paint = browser_on_paint; // nb->render_handler.on_cursor_change = NULL; nb->render_handler.start_dragging = NULL; nb->render_handler.update_drag_cursor = NULL; nb->render_handler.on_scroll_offset_changed = NULL; // nb->render_handler.on_ime_composition_range_changed = NULL; nb->display_handler.on_address_change = browser_on_address_change; nb->display_handler.on_title_change = browser_on_title_change; nb->display_handler.on_favicon_urlchange = browser_on_favicon_urlchange; nb->display_handler.on_fullscreen_mode_change = browser_on_fullscreenmode_change; nb->display_handler.on_tooltip = browser_on_tooltip; nb->display_handler.on_status_message = browser_on_status_message; nb->display_handler.on_console_message = browser_on_console_message; nb->request_handler.on_before_browse = browser_on_before_browse; nb->request_handler.on_open_urlfrom_tab = NULL; // nb->request_handler.on_before_resource_load = NULL; // nb->request_handler.get_resource_handler = NULL; // nb->request_handler.on_resource_redirect = NULL; // nb->request_handler.on_resource_response = NULL; // nb->request_handler.get_resource_response_filter = NULL; // nb->request_handler.on_resource_load_complete = NULL; nb->request_handler.get_auth_credentials = NULL; nb->request_handler.on_quota_request = NULL; // nb->request_handler.on_protocol_execution = NULL; //FIXME: should implement. nb->request_handler.on_certificate_error = NULL; // nb->request_handler.on_select_client_certificate = NULL; //we have no such certs nb->request_handler.on_plugin_crashed = NULL; nb->request_handler.on_render_view_ready = NULL; nb->request_handler.on_render_process_terminated = browser_on_render_process_terminated; nb->life_span_handler.on_before_popup = NULL; nb->life_span_handler.on_after_created = NULL; nb->life_span_handler.do_close = NULL; nb->life_span_handler.on_before_close = browser_on_before_close; nb->context_menu_handler.on_before_context_menu = browser_on_before_context_menu; nb->context_menu_handler.run_context_menu = NULL; //fixme: implement a working context menu somehow nb->context_menu_handler.on_context_menu_command = NULL; //for custom context things, like opening in a new window... nb->context_menu_handler.on_context_menu_dismissed = NULL; // nb->desiredwidth = 640; nb->desiredheight = 480; if (newconsole) nb->consolename = strdup(newconsole); else nb->consolename = NULL; //make it white until there's actually something to draw nb->videowidth = 1; nb->videoheight = 1; nb->videodata = malloc(nb->videowidth * nb->videoheight * 4); *(int*)nb->videodata = 0x00ffffff; memset(nb->videodata, 0xff, nb->videowidth * nb->videoheight * 4); nb->updated = true; numbrowsers++; return nb; } //request contexts are per-session things. eg, incognito tabs would have their own private instance static cef_request_context_t *request_context; static cef_request_context_handler_t request_context_handler; //request_context_handler methods static int CEF_CALLBACK request_context_handler_on_before_plugin_load(cef_request_context_handler_t* self, const cef_string_t* mime_type, const cef_string_t* plugin_url, int is_main_frame, const cef_string_t* top_origin_url, cef_web_plugin_info_t* plugin_info, cef_plugin_policy_t* plugin_policy) { // Con_DPrintf("%s (%s), \"%s\" \"%s\" \"%s\" \"%s\"\n", policy_url.ToString().c_str(), url.ToString().c_str(), // info->GetName().ToString().c_str(), info->GetPath().ToString().c_str(), info->GetVersion().ToString().c_str(), info->GetDescription().ToString().c_str()); *plugin_policy = PLUGIN_POLICY_BLOCK; //block by default (user can manually override supposedly). most plugins are unlikely to cope well with our offscreen rendering stuff, and flash sucks. cef_release(plugin_info); if (!cef_allowplugins->value) { *plugin_policy = PLUGIN_POLICY_DISABLE; // Con_Printf("Blocking plugin: %s (%s)\n", info->GetName().ToString().c_str(), url.ToString().c_str()); } else return false; //false to use the 'recommended' policy return true; } //there's only one of these, so I'm not going to bother making separate objects for all of the interfaces, nor ref counting static cef_app_t app; static cef_browser_process_handler_t browser_process_handler; static cef_render_process_handler_t render_process_handler; static cef_v8handler_t v8handler_query; //window.fte_query static cef_scheme_handler_factory_t scheme_handler_factory; static cef_browser_process_handler_t* CEF_CALLBACK app_get_browser_process_handler(cef_app_t* self) { //cef_addref(&browser_process_handler); return &browser_process_handler; } static cef_render_process_handler_t* CEF_CALLBACK app_get_render_process_handler(cef_app_t* self) { //cef_addref(&render_process_handler); return &render_process_handler; } static void CEF_CALLBACK app_on_register_custom_schemes(struct _cef_app_t* self, cef_scheme_registrar_t* registrar) { cef_string_t fte = makecefstring("fte"); registrar->add_custom_scheme(registrar, &fte, CEF_SCHEME_OPTION_NONE |CEF_SCHEME_OPTION_STANDARD /*|CEF_SCHEME_OPTION_LOCAL*/ |CEF_SCHEME_OPTION_DISPLAY_ISOLATED |CEF_SCHEME_OPTION_SECURE |CEF_SCHEME_OPTION_CORS_ENABLED /*|CEF_SCHEME_OPTION_CSP_BYPASSING*/ /*|CEF_SCHEME_OPTION_FETCH_ENABLED*/ ); cef_string_clear(&fte); } static void CEF_CALLBACK browser_process_handler_on_context_initialized(cef_browser_process_handler_t* self) { cef_string_t fte = makecefstring("fte"); cef_register_scheme_handler_factory(&fte, NULL, &scheme_handler_factory); cef_string_clear(&fte); } static void CEF_CALLBACK browser_process_handler_on_before_child_process_launch(cef_browser_process_handler_t* self, cef_command_line_t* command_line) { char *arg = "--plugwrapper"; char *funcname = "CefSubprocessInit"; cef_string_t cefisannoying = {NULL}; cef_string_from_utf8(arg, strlen(arg), &cefisannoying); command_line->append_argument(command_line, &cefisannoying); cef_string_from_utf8(plugname, strlen(plugname), &cefisannoying); command_line->append_argument(command_line, &cefisannoying); cef_string_from_utf8(funcname, strlen(funcname), &cefisannoying); command_line->append_argument(command_line, &cefisannoying); // MessageBoxW(NULL, command_line->GetCommandLineString().c_str(), L"CEF", 0); cef_string_clear(&cefisannoying); cef_release(command_line); } static void CEF_CALLBACK render_process_handler_on_context_created(cef_render_process_handler_t* self, cef_browser_t* browser, cef_frame_t* frame, cef_v8context_t* context) { cef_v8value_t *jswindow = context->get_global(context); //eg: window.fte_query("getstats", function(req,res){console.log("health: "+JSON.parse(res)[0/*STAT_HEALTH*/]);}); cef_string_t key = makecefstring("fte_query"); jswindow->set_value_bykey(jswindow, &key, cef_v8value_create_function(&key, &v8handler_query), V8_PROPERTY_ATTRIBUTE_READONLY | V8_PROPERTY_ATTRIBUTE_DONTENUM | V8_PROPERTY_ATTRIBUTE_DONTDELETE); cef_string_clear(&key); cef_release(browser); cef_release(frame); cef_release(context); } //only use these in the 'renderer' thread / javascript thread. typedef struct activequery_s { cef_v8value_t *callbackfunc; //this is the js function to call when the result is available. cef_v8context_t *context; cef_frame_t *frame; int64 queryid; struct activequery_s *next; } activequery_t; static activequery_t *queries; static int64 next_queryid; typedef struct { cef_task_t task; cef_string_userfree_t request; cef_string_userfree_t result; int64 queryid; atomic_uint32_t refcount; } queryresponse_t; static void CEF_CALLBACK queryresponse_addref(cef_base_ref_counted_t* self) { queryresponse_t *qr = (queryresponse_t*)((char*)self - offsetof(queryresponse_t, task.base)); atomic_fetch_add(&qr->refcount, 1); } static int CEF_CALLBACK queryresponse_release(cef_base_ref_counted_t* self) { queryresponse_t *qr = (queryresponse_t*)((char*)self - offsetof(queryresponse_t, task.base)); if (atomic_fetch_sub(&qr->refcount, 1) == 1) { if (qr->request) cef_string_userfree_free(qr->request); if (qr->result) cef_string_userfree_free(qr->result); free(qr); return true; } return false; } static void CEF_CALLBACK queryresponse_execute(struct _cef_task_t* self) { //lethal injection. queryresponse_t *qr = (queryresponse_t*)((char*)self - offsetof(queryresponse_t, task.base)); activequery_t **link, *q; for (link = &queries; (q=*link); link = &(*link)->next) { if (q->queryid == qr->queryid) { if (q->callbackfunc) { cef_v8value_t *args[2] = {cef_v8value_create_string(qr->request), cef_v8value_create_string(qr->result)}; cef_v8value_t *r; cef_addref(q->context); r = q->callbackfunc->execute_function_with_context(q->callbackfunc, q->context, NULL, 2, args); cef_release(r); } //and clear up the request context too. *link = q->next; if (q->callbackfunc) cef_release(q->callbackfunc); cef_release(q->frame); cef_release(q->context); free(q); return; } } } static void CEF_CALLBACK render_process_handler_on_context_released(cef_render_process_handler_t* self, cef_browser_t* browser, cef_frame_t* frame, cef_v8context_t* context) { activequery_t **link, *q; for (link = &queries; (q=*link); ) { if (q->context == context)// && q->frame == frame) { *link = q->next; if (q->callbackfunc) cef_release(q->callbackfunc); cef_release(q->frame); cef_release(q->context); free(q); continue; } link = &(*link)->next; } cef_release(browser); cef_release(frame); cef_release(context); } /*javascript methods for the guest code to call*/ static cef_v8value_t *makev8string(char *str) { cef_v8value_t *r; cef_string_t cs = makecefstring(str); r = cef_v8value_create_string(&cs); cef_string_clear(&cs); return r; } static int CEF_CALLBACK fsfunc_execute(cef_v8handler_t* self, const cef_string_t* name, cef_v8value_t* object, size_t argumentsCount, cef_v8value_t* const* arguments, cef_v8value_t** retval, cef_string_t* exception) { cef_process_message_t *msg; cef_list_value_t *args; cef_v8context_t *v8ctx = cef_v8context_get_current_context(); cef_browser_t *browser = v8ctx->get_browser(v8ctx); cef_frame_t *frame = v8ctx->get_frame(v8ctx); // int64 frame_id = frame->get_identifier(frame); // cef_string_t key = {L"omgwtfitkindaworks"}; // key.length = wcslen(key.str); // *exception = makecefstring("SOME KIND OF EXCEPTION!"); *retval = makev8string(""); if (argumentsCount) { cef_string_userfree_t setting = arguments[0]->get_string_value(arguments[0]); activequery_t *q = malloc(sizeof(*q)); memset(q, 0, sizeof(*q)); q->context = v8ctx; //hold on to these q->frame = frame; q->queryid = ++next_queryid; q->next = queries; q->callbackfunc = arguments[1]; queries = q; cef_addref(q->callbackfunc); msg = cef_process_message_create(name); args = msg->get_argument_list(msg); args->set_string(args, 0, setting); args->set_null(args, 1); args->set_int(args, 2, q->queryid & 0xffffffff); args->set_int(args, 3, (q->queryid>>32) & 0xffffffff); cef_release(args); frame->send_process_message(frame, PID_BROWSER, msg); if (setting) cef_string_userfree_free(setting); } else { cef_release(frame); cef_release(v8ctx); } cef_release(browser); return 1; } static int CEF_CALLBACK render_process_handler_on_process_message_received(cef_render_process_handler_t* self,cef_browser_t* browser, cef_frame_t* frame, cef_process_id_t source_process, cef_process_message_t* message) { int handled = false; // browser_t *br = (browser_t*)((char*)self - offsetof(browser_t, request_handler)); cef_string_userfree_t msgnameunusable = message->get_name(message); cef_string_utf8_t name = {NULL}; cef_string_to_utf8(msgnameunusable->str, msgnameunusable->length, &name); if (!strcmp(name.str, "fte_query")) { cef_list_value_t *args = message->get_argument_list(message); queryresponse_t *task = malloc(sizeof(*task)); memset(task, 0, sizeof(*task)); task->refcount = 1; task->task.base.size = sizeof(task->task); task->task.base.add_ref = queryresponse_addref; task->task.base.release = queryresponse_release; task->task.execute = queryresponse_execute; task->request = args->get_string(args, 0); task->result = args->get_string(args, 1); task->queryid = args->get_int(args, 2) | ((int64)args->get_int(args, 3)<<32u); cef_release(args); cef_post_task(TID_RENDERER, &task->task); handled = true; } cef_string_utf8_clear(&name); cef_string_userfree_free(msgnameunusable); cef_release(browser); cef_release(message); return handled; } /* fte://file/path scheme handler */ typedef struct { cef_resource_handler_t rh; atomic_uint32_t refcount; vfsfile_t *fh; char *data; size_t offset; size_t datasize; unsigned int resultcode; char *responseheaders; } fteresource_t; static void CEF_CALLBACK resource_handler_addref(cef_base_ref_counted_t* self) { fteresource_t *rh = (fteresource_t*)((char*)self - offsetof(fteresource_t, rh.base)); atomic_fetch_add(&rh->refcount, 1); } static int CEF_CALLBACK resource_handler_release(cef_base_ref_counted_t* self) { fteresource_t *rh = (fteresource_t*)((char*)self - offsetof(fteresource_t, rh.base)); if (atomic_fetch_sub(&rh->refcount, 1) == 1) { if (rh->fh) VFS_CLOSE(rh->fh); if (rh->data) free(rh->data); free(rh->responseheaders); free(rh); return true; } return false; } static void res_catfield_l(char **const orig, const char *key, int kl, const char *val, int vl) { size_t ol = *orig?strlen(*orig):0; char *n; if (ol) { n = malloc(ol+1+kl+1+vl+1); memcpy(n, *orig, ol); n[ol++] = '\n'; } else n = malloc(kl+1+vl+1); memcpy(n+ol, key, kl); ol+=kl; n[ol++] = '\n'; memcpy(n+ol, val, vl); ol+=vl; n[ol++] = 0; free(*orig); *orig = n; } static void res_catfield(char **const orig, const char *key, const char *val) { res_catfield_l(orig, key, strlen(key), val, strlen(val)); } static void res_catfield_csuf(char **const orig, const char *key, cef_string_userfree_t cs) { cef_string_utf8_t u8 = {NULL}; cef_string_to_utf8(cs->str, cs->length, &u8); res_catfield_l(orig, key, strlen(key), u8.str, u8.length); cef_string_utf8_clear(&u8); } static void res_catfield_cs(char **const orig, cef_string_t *key, cef_string_t *val) { cef_string_utf8_t keyu8 = {NULL}; cef_string_utf8_t valu8 = {NULL}; cef_string_to_utf8(key->str, key->length, &keyu8); cef_string_to_utf8(val->str, val->length, &valu8); res_catfield_l(orig, keyu8.str, keyu8.length, valu8.str, valu8.length); cef_string_utf8_clear(&keyu8); cef_string_utf8_clear(&valu8); } static int CEF_CALLBACK resource_handler_process_request(cef_resource_handler_t* self, cef_request_t* request, cef_callback_t* callback) { fteresource_t *rh = (fteresource_t*)((char*)self - offsetof(fteresource_t, rh)); cef_string_userfree_t url = request->get_url(request), method; cef_post_data_t *postdata; size_t numelements; cef_post_data_element_t *elements[1]; size_t postsize; char *postbytes; char *q; char *e; cef_string_utf8_t u8_url = {NULL}, u8={NULL}; cef_string_t ext = {NULL}; cef_string_to_utf8(url->str, url->length, &u8_url); rh->resultcode = 404; //hack at the url to hide the q = strchr(u8_url.str, '?'); if (q) *q = 0; for(e = q?q:u8_url.str+strlen(u8_url.str); e > u8_url.str; ) { e--; if (*e == '/') break; //no extension if (*e == '.') { e++; cef_string_from_utf8(e, strlen(e), &ext); break; } } res_catfield(&rh->responseheaders, "Access-Control-Allow-Origin", "fte://data"); res_catfield(&rh->responseheaders, "Access-Control-Allow-Origin", "fte://csqc"); res_catfield(&rh->responseheaders, "Access-Control-Allow-Origin", "fte://ssqc"); res_catfield(&rh->responseheaders, "Access-Control-Allow-Origin", "fte://menu"); //sandboxed to the same dir that qc can fopen/fwrite. //(also blocks any fancy http:// parsing that an engine might do) if (!strncmp(u8_url.str, "fte://data/", 11)) { rh->fh = fsfuncs->OpenVFS(u8_url.str+6, "rb", FS_GAME); if (rh->fh) { cef_string_userfree_t mt = cef_get_mime_type(&ext); if (mt) { res_catfield_csuf(&rh->responseheaders, "Content-Type", mt); cef_string_userfree_free(mt); } rh->resultcode = 200; } else { rh->resultcode = 404; res_catfield(&rh->responseheaders, "Content-Type", "text/html"); rh->data = strdup("not foundFile not found within game filesystem."); rh->datasize = strlen(rh->data); } } else if (!strncmp(u8_url.str, "fte://ssqc/", 11) || !strncmp(u8_url.str, "fte://csqc/", 11) || !strncmp(u8_url.str, "fte://menu/", 11)) { struct pubprogfuncs_s *progs; const char *page; const char *respheaders = NULL; const char *reqheaders = NULL; if (ext.str) { cef_string_userfree_t mt = cef_get_mime_type(&ext); if (mt) { res_catfield_csuf((char**)&respheaders, "Content-Type", mt); cef_string_userfree_free(mt); } } if(q) *q = '?'; //put it back so the qc can get the full url. rh->resultcode = 404; if (!strncmp(u8_url.str, "fte://ssqc/", 11)) progs = plugfuncs->GetEngineInterface("SSQCVM", sizeof(*progs)); //WARNING: goes direct rather than via the server, so basically single-player only. else if (!strncmp(u8_url.str, "fte://csqc/", 11)) progs = plugfuncs->GetEngineInterface("CSQCVM", sizeof(*progs)); else if (!strncmp(u8_url.str, "fte://menu/", 11)) progs = plugfuncs->GetEngineInterface("MenuQCVM", sizeof(*progs)); else progs = NULL; if (progs) { func_t func = progs->FindFunction(progs, "Cef_GeneratePage", PR_ANY); if (func) { void *pr_globals = PR_globals(progs, PR_CURRENT); ((string_t *)pr_globals)[OFS_PARM0] = progs->TempString(progs, u8_url.str+11); method = request->get_method(request); cef_string_to_utf8(method->str, method->length, &u8); ((string_t *)pr_globals)[OFS_PARM1] = progs->TempString(progs, u8.str); cef_string_userfree_free(method); cef_string_utf8_clear(&u8); postdata = request->get_post_data(request); if (postdata) { numelements = countof(elements); memset(elements, 0, sizeof(elements)); postdata->get_elements(postdata, &numelements, elements); postsize = elements[0]->get_bytes_count(elements[0]); postbytes = malloc(postsize+1); elements[0]->get_bytes(elements[0], postsize, postbytes); postbytes[postsize] = 0; ((string_t *)pr_globals)[OFS_PARM2] = progs->TempString(progs, postbytes); free(postbytes); cef_release(elements[0]); } else ((string_t *)pr_globals)[OFS_PARM2] = 0; { size_t i, elems; cef_string_t key = {NULL}, val = {NULL}; cef_string_multimap_t hmap = cef_string_multimap_alloc(); request->get_header_map(request, hmap); elems = cef_string_multimap_size(hmap); for (i = 0; i < elems; i++) { cef_string_multimap_key(hmap, i, &key); cef_string_multimap_key(hmap, i, &val); res_catfield_cs(&rh->responseheaders, &key, &val); cef_string_clear(&key); cef_string_clear(&val); } cef_string_multimap_free(hmap); } ((string_t *)pr_globals)[OFS_PARM3] = reqheaders?progs->TempString(progs, reqheaders):0; //request heders ((string_t *)pr_globals)[OFS_PARM4] = rh->responseheaders?progs->TempString(progs, rh->responseheaders):0; //response headers ((string_t *)pr_globals)[OFS_PARM5] = 0; ((string_t *)pr_globals)[OFS_PARM6] = 0; ((string_t *)pr_globals)[OFS_PARM7] = 0; progs->ExecuteProgram(progs, func); if (((string_t *)pr_globals)[OFS_RETURN]) { page = progs->StringToNative(progs, ((string_t *)pr_globals)[OFS_RETURN]); respheaders = progs->StringToNative(progs, ((string_t *)pr_globals)[OFS_PARM4]); rh->resultcode = 200; } else page = "not foundCef_GeneratePage returned null"; } else page = "not foundCef_GeneratePage not implemented by mod"; } else page = "not foundThat QCVM is not running"; if (*respheaders == '\n') respheaders++; rh->responseheaders = strdup(respheaders); //FIXME: only return any data if we were successful OR the mime is text/html rh->data = strdup(page); rh->datasize = strlen(rh->data); } else { rh->resultcode = 403; res_catfield(&rh->responseheaders, "Content-Type", "text/html"); rh->data = strdup("forbiddenTry here Or try here"); rh->datasize = strlen(rh->data); } cef_string_userfree_free(url); cef_string_utf8_clear(&u8_url); callback->cont(callback); //headers are now known... should be delayed. cef_release(callback); cef_release(request); return 1; //failure is reported as an http error code rather than an exception } static char *strseps(char *str, char *chars) { //find the next separator char *best = str+strlen(str); char *c; if (*str) while(*chars) { c = strchr(str, *chars++); if (c && c < best) best = c; } return best; } static void CEF_CALLBACK resource_handler_get_response_headers(cef_resource_handler_t* self, cef_response_t* response, int64* response_length, cef_string_t* redirectUrl) { fteresource_t *rh = (fteresource_t*)((char*)self - offsetof(fteresource_t, rh)); cef_string_multimap_t *hmap; cef_string_t key = {NULL}, val={NULL}; if (rh->fh) *response_length = VFS_GETLEN(rh->fh); else if (rh->data) *response_length = rh->datasize; else *response_length = -1; hmap = cef_string_multimap_alloc(); if (rh->responseheaders) { char *start; char *sep; char *nl; for (start = rh->responseheaders; *start; ) { sep = strseps(start, ":\n"); nl = strseps(sep+1, "\n"); cef_string_from_utf8(start, sep-start, &key); if (*sep) sep++; cef_string_from_utf8(sep, nl-sep, &val); cef_string_multimap_append(hmap, &key, &val); if (*nl) start = nl+1; else break; } } cef_string_multimap_append(hmap, makecefstringptr("Access-Control-Allow-Origin", &key), makecefstringptr("fte://data", &val)); response->set_header_map(response, hmap); // response->set_mime_type(response, &rh->mimetype); response->set_status(response, rh->resultcode); cef_string_clear(&key); cef_string_clear(&val); cef_release(response); } static int CEF_CALLBACK resource_handler_read_response(cef_resource_handler_t* self, void* data_out, int bytes_to_read, int* bytes_read, cef_callback_t* callback) { fteresource_t *rh = (fteresource_t*)((char*)self - offsetof(fteresource_t, rh)); if (rh->fh) *bytes_read = VFS_READ(rh->fh, data_out, bytes_to_read); else if (rh->data) { if (bytes_to_read > rh->datasize - rh->offset) bytes_to_read = rh->datasize - rh->offset; *bytes_read = bytes_to_read; memcpy(data_out, rh->data + rh->offset, bytes_to_read); rh->offset += bytes_to_read; } else *bytes_read = 0; //callback->cont(callback); //headers are now known... should be delayed. cef_release(callback); if (*bytes_read <= 0) { *bytes_read = 0; return 0; } return true; //more to come } static void CEF_CALLBACK resource_handler_cancel(cef_resource_handler_t* self) { fteresource_t *rh = (fteresource_t*)((char*)self - offsetof(fteresource_t, rh)); if (rh->fh) VFS_CLOSE(rh->fh); rh->fh = NULL; if (rh->data) free(rh->data); rh->data = NULL; rh->offset = 0; rh->datasize = 0; } static cef_resource_handler_t* CEF_CALLBACK scheme_handler_factory_create(cef_scheme_handler_factory_t* self, cef_browser_t* browser, cef_frame_t* frame, const cef_string_t* scheme_name, cef_request_t* request) { fteresource_t *rh = malloc(sizeof(*rh)); memset(rh, 0, sizeof(*rh)); rh->rh.base.size = sizeof(*rh); rh->rh.base.add_ref = resource_handler_addref; rh->rh.base.release = resource_handler_release; rh->rh.process_request = resource_handler_process_request; rh->rh.get_response_headers = resource_handler_get_response_headers; rh->rh.read_response = resource_handler_read_response; rh->rh.cancel = resource_handler_cancel; cef_addref(&rh->rh); cef_release(browser); cef_release(frame); cef_release(request); return &rh->rh; } static void app_initialize(void) { app.base.size = sizeof(app); app.get_browser_process_handler = app_get_browser_process_handler; app.get_render_process_handler = app_get_render_process_handler; app.on_register_custom_schemes = app_on_register_custom_schemes; browser_process_handler.base.size = sizeof(browser_process_handler); browser_process_handler.on_before_child_process_launch = browser_process_handler_on_before_child_process_launch; browser_process_handler.on_context_initialized = browser_process_handler_on_context_initialized; render_process_handler.base.size = sizeof(render_process_handler); render_process_handler.on_context_created = render_process_handler_on_context_created; render_process_handler.on_context_released = render_process_handler_on_context_released; render_process_handler.on_process_message_received = render_process_handler_on_process_message_received; v8handler_query.base.size = sizeof(v8handler_query); v8handler_query.execute = fsfunc_execute; scheme_handler_factory.base.size = sizeof(scheme_handler_factory); scheme_handler_factory.create = scheme_handler_factory_create; request_context_handler.base.size = sizeof(request_context_handler); request_context_handler.on_before_plugin_load = request_context_handler_on_before_plugin_load; } static int cefwasinitialised; cef_request_context_t *Cef_GetRequestContext(void) { char utf8[MAX_OSPATH]; cef_request_context_t *ret = NULL; qboolean incog; incog = cef_incognito->value; if (!incog) ret = request_context; if (!ret) { cef_request_context_settings_t csettings = {sizeof(csettings)}; csettings.persist_user_preferences = !incog; if (!incog && fsfuncs->NativePath("cefcache", FS_ROOT, utf8, sizeof(utf8))) cef_string_from_utf8(utf8, strlen(utf8), &csettings.cache_path); //should be empty for incognito. ret = cef_request_context_create_context(&csettings, &request_context_handler); cef_string_clear(&csettings.cache_path); } else cef_addref(ret); if (!incog && !request_context) { request_context = ret; cef_addref(request_context); } return ret; } static qboolean Cef_Init(qboolean engineprocess); struct mediacallbacks_s; //todo... static void *Cef_Create(const char *name, struct mediacallbacks_s *callbacks) { cef_window_info_t window_info = {0}; cef_browser_settings_t browserSettings = {sizeof(browserSettings)}; browser_t *newbrowser; cef_string_t url = {NULL}; if (!strcmp(name, "cef")) name += 3; else if (!strncmp(name, "cef:", 4)) name += 4; else if (!strcmp(name, "http")) name += 4; else if (!strncmp(name, "http:", 5)) ; else if (!strncmp(name, "https:", 6)) ; else if (!strncmp(name, "ftp:", 4)) ; else return NULL; if (!cefwasinitialised) { char utf8[MAX_OSPATH]; cef_main_args_t mainargs = {0}; cef_settings_t settings = {sizeof(settings)}; if (!Cef_Init(true)) return NULL; //const char *ua = "FTEBrowser"; //cef_string_from_utf8(ua, strlen(ua), &settings.user_agent); if (fsfuncs->NativePath("cefcache", FS_ROOT, utf8, sizeof(utf8))) cef_string_from_utf8(utf8, strlen(utf8), &settings.cache_path); if (fsfuncs->NativePath("cef_debug.log", FS_ROOT, utf8, sizeof(utf8))) cef_string_from_utf8(utf8, strlen(utf8), &settings.log_file); if (fsfuncs->NativePath("", FS_BINARYPATH, utf8, sizeof(utf8))) cef_string_from_utf8(utf8, strlen(utf8), &settings.resources_dir_path); if (fsfuncs->NativePath("locales", FS_BINARYPATH, utf8, sizeof(utf8))) { struct stat statbuf; if (stat(utf8, &statbuf) < 0) { Con_Printf("%s not found\n", utf8); return NULL; } cef_string_from_utf8(utf8, strlen(utf8), &settings.locales_dir_path); } #ifdef _WIN32 { wchar_t omgwtfamonkey[MAX_OSPATH]; if (GetModuleFileNameW(NULL, omgwtfamonkey, countof(omgwtfamonkey))) cef_string_from_utf16(omgwtfamonkey, wcslen(omgwtfamonkey), &settings.browser_subprocess_path); mainargs.instance = GetModuleHandle(NULL); } #endif #ifdef _DEBUG settings.log_severity = LOGSEVERITY_VERBOSE; #else settings.log_severity = LOGSEVERITY_DISABLE; #endif settings.background_color = 0x00ffffff; // settings.single_process = true; #ifdef _WIN32 // settings.multi_threaded_message_loop = true; //fixme: use this. #endif settings.windowless_rendering_enabled = true; // settings.command_line_args_disabled = true; // settings.persist_session_cookies = false; /* { char *s; strcpy(utf8, FULLENGINENAME "/" STRINGIFY(FTE_VER_MAJOR) "." STRINGIFY(FTE_VER_MINOR)); while((s = strchr(utf8, ' '))) *s = '_'; cef_string_from_utf8(utf8, strlen(utf8), &settings.product_version); } */ cefwasinitialised = !!cef_initialize(&mainargs, &settings, &app, NULL); cef_string_clear(&settings.browser_subprocess_path); // cef_string_clear(&settings.product_version); cef_string_clear(&settings.cache_path); cef_string_clear(&settings.log_file); } if (!cefwasinitialised) return NULL; //tbh, web browser's are so horribly insecure that it seems pointless to even try disabling stuff that might be useful browserSettings.windowless_frame_rate = 60; browserSettings.javascript_close_windows = STATE_DISABLED; browserSettings.javascript_access_clipboard = STATE_DISABLED; // browserSettings.universal_access_from_file_urls = STATE_DISABLED; // browserSettings.file_access_from_file_urls = STATE_DISABLED; browserSettings.remote_fonts = STATE_DISABLED; browserSettings.plugins = STATE_DISABLED; browserSettings.background_color = 0x00ffffff; window_info.windowless_rendering_enabled = true; memset(&window_info.parent_window, 0, sizeof(window_info.parent_window)); newbrowser = browser_create(); if (!newbrowser) return NULL; if (!*name || !strcmp(name, "http:") || !strcmp(name, "https:")) name = "about:blank"; cef_string_from_utf8(name, strlen(name), &url); cef_addref(&newbrowser->client); newbrowser->thebrowser = cef_browser_host_create_browser_sync(&window_info, &newbrowser->client, &url, &browserSettings, NULL, Cef_GetRequestContext()); cef_string_to_utf8(url.str, url.length, &newbrowser->currenturl); cef_string_clear(&url); if (!newbrowser->thebrowser) { browser_release(newbrowser); return NULL; //cef fucked up. } if (cef_devtools->value) { cef_browser_host_t *host = newbrowser->thebrowser->get_host(newbrowser->thebrowser); browser_t *devtools = browser_create(); #ifdef _WIN32 window_info.style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE; window_info.parent_window = NULL; window_info.bounds.x = CW_USEDEFAULT; window_info.bounds.y = CW_USEDEFAULT; window_info.bounds.width = CW_USEDEFAULT; window_info.bounds.height = CW_USEDEFAULT; window_info.window_name = makecefstring("CEF Dev Tools"); #else memset(&window_info.parent_window, 0, sizeof(window_info.parent_window)); window_info.x = 0; window_info.y = 0; window_info.width = 320; window_info.height = 240; #endif window_info.windowless_rendering_enabled = false; cef_addref(&devtools->client); host->show_dev_tools(host, &window_info, &devtools->client, &browserSettings, NULL); cef_release(host); browser_release(devtools); //cef should continue to hold a reference to it while its visible, but its otherwise out of engine now. #ifdef _WIN32 cef_string_clear(&window_info.window_name); #endif } return (void*)newbrowser; } static void *Cef_CreateOld(const char *name) { return Cef_Create(name, NULL); } static qboolean VARGS Cef_DisplayFrame(void *ctx, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ectx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ectx) { browser_t *browser = (browser_t*)ctx; if (browser->updated || forcevideo) { uploadtexture(ectx, TF_BGRA32, browser->videowidth, browser->videoheight, browser->videodata, NULL); browser->updated = false; } return true; } static void Cef_Destroy(void *ctx) { //engine isn't allowed to talk about the browser any more. kill it. browser_t *br = (browser_t*)ctx; cef_browser_host_t *host = br->thebrowser->get_host(br->thebrowser); host->close_browser(host, true); cef_release(host); if (br->thebrowser) { cef_release(br->thebrowser); br->thebrowser = NULL; } //now release our reference to it. browser_release(br); //hopefully this should be the last reference, but we might be waiting for something on the cef side. hopefully nothing blocking on an unload event... } static void VARGS Cef_CursorMove (void *ctx, float posx, float posy) { browser_t *br = (browser_t*)ctx; cef_browser_host_t *host = br->thebrowser->get_host(br->thebrowser); br->mousepos.x = (int)(posx * br->desiredwidth); br->mousepos.y = (int)(posy * br->desiredheight); br->mousepos.modifiers = 0; host->send_mouse_move_event(host, &br->mousepos, false); cef_release(host); } static void VARGS Cef_Key (void *ctx, int code, int unicode, int event) { browser_t *browser = (browser_t*)ctx; cef_browser_host_t *host = browser->thebrowser->get_host(browser->thebrowser); //handle mouse buttons if (code >= K_MOUSE1 && code <= K_MOUSE3) { int buttons[] = {MBT_LEFT, MBT_RIGHT, MBT_MIDDLE}; if (!event || browser->keystate[code]) host->send_mouse_click_event(host, &browser->mousepos, buttons[code-K_MOUSE1], event?true:false, 1); if (event) browser->keystate[code] = 0; else browser->keystate[code] = 1; cef_release(host); return; } if (code == K_TOUCH) { //FIXME cef_release(host); return; } if (code == K_TOUCHSLIDE || code == K_TOUCHTAP || code == K_TOUCHLONG) { cef_release(host); return; //has to do its own } //handle mouse wheels if (code == K_MWHEELUP || code == K_MWHEELDOWN) { if (!event) host->send_mouse_wheel_event(host, &browser->mousepos, 0, (code == K_MWHEELDOWN)?-32:32); cef_release(host); return; } //handle keypress/release events if (code) { cef_key_event_t kev = {0}; if (event && !browser->keystate[code]) { cef_release(host); return; //releasing a key that is already released is weird. } kev.type = event?KEYEVENT_KEYUP:KEYEVENT_RAWKEYDOWN; kev.modifiers = 0; switch(code) { case 0: kev.windows_key_code = 0; break; case K_UPARROW: kev.windows_key_code = 0x26/*VK_UP*/; break; case K_DOWNARROW: kev.windows_key_code = 0x28/*VK_DOWN*/; break; case K_LEFTARROW: kev.windows_key_code = 0x25/*VK_LEFT*/; break; case K_RIGHTARROW: kev.windows_key_code = 0x27/*VK_RIGHT*/; break; case K_ESCAPE: kev.windows_key_code = 0x1b/*VK_ESCAPE*/; break; case K_SPACE: kev.windows_key_code = 0x20/*VK_SPACE*/; break; case K_RSHIFT: kev.windows_key_code = 0x10/*VK_SHIFT*/; break; case K_LSHIFT: kev.windows_key_code = 0x10/*VK_SHIFT*/; break; case K_RCTRL: kev.windows_key_code = 0x11/*VK_CONTROL*/; break; case K_LCTRL: kev.windows_key_code = 0x11/*VK_CONTROL*/; break; case K_RALT: kev.windows_key_code = 0x12/*VK_MENU*/; break; case K_LALT: kev.windows_key_code = 0x12/*VK_MENU*/; break; case K_TAB: kev.windows_key_code = 0x09/*VK_TAB*/; break; case K_RWIN: kev.windows_key_code = 0x5c/*VK_RWIN*/; break; case K_LWIN: kev.windows_key_code = 0x5b/*VK_LWIN*/; break; case K_APP: kev.windows_key_code = 0x5d/*VK_APPS*/; break; case K_F1: kev.windows_key_code = 0x70/*VK_F1*/; break; case K_F2: kev.windows_key_code = 0x71/*VK_F2*/; break; case K_F3: kev.windows_key_code = 0x72/*VK_F3*/; break; case K_F4: kev.windows_key_code = 0x73/*VK_F4*/; break; case K_F5: kev.windows_key_code = 0x74/*VK_F5*/; break; case K_F6: kev.windows_key_code = 0x75/*VK_F6*/; break; case K_F7: kev.windows_key_code = 0x76/*VK_F7*/; break; case K_F8: kev.windows_key_code = 0x77/*VK_F8*/; break; case K_F9: kev.windows_key_code = 0x78/*VK_F9*/; break; case K_F10: kev.windows_key_code = 0x79/*VK_F10*/; break; case K_F11: kev.windows_key_code = 0x81/*VK_F11*/; break; case K_F12: kev.windows_key_code = 0x82/*VK_F12*/; break; case K_BACKSPACE: kev.windows_key_code = 0x08/*VK_BACK*/; break; case K_DEL: kev.windows_key_code = 0x2e/*VK_DELETE*/; break; case K_HOME: kev.windows_key_code = 0x24/*VK_HOME*/; break; case K_END: kev.windows_key_code = 0x23/*VK_END*/; break; case K_INS: kev.windows_key_code = 0x2d/*VK_INSERT*/; break; case K_PGUP: kev.windows_key_code = 0x21/*VK_PRIOR*/; break; case K_PGDN: kev.windows_key_code = 0x22/*VK_NEXT*/; break; default: if ((code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5a)) kev.windows_key_code = code; else if (code >= 'a' && code <= 'z') kev.windows_key_code = (code-'a') + 'A'; else kev.windows_key_code = 0; break; } kev.native_key_code = unicode<<16; if (browser->keystate[code]) kev.native_key_code |= 1<<30; if (event) kev.native_key_code |= 1u<<31; if (event) browser->keystate[code] = 0; else browser->keystate[code] = 1; kev.is_system_key = 0; kev.character = unicode; kev.unmodified_character = unicode; kev.focus_on_editable_field = true; host->send_key_event(host, &kev); } //handle text input events (down events only) if (unicode && !event) { cef_key_event_t kev; kev.type = KEYEVENT_CHAR; kev.modifiers = 0; kev.windows_key_code = unicode; kev.native_key_code = unicode<<16; if (browser->keystate[code]) kev.native_key_code |= 1<<30; if (event) kev.native_key_code |= 1u<<31; kev.is_system_key = 0; kev.character = unicode; kev.unmodified_character = unicode; kev.focus_on_editable_field = true; host->send_key_event(host, &kev); } cef_release(host); } static qboolean VARGS Cef_SetSize (void *ctx, int width, int height) { browser_t *browser = (browser_t*)ctx; cef_browser_host_t *host = browser->thebrowser->get_host(browser->thebrowser); if (browser->desiredwidth != width || browser->desiredheight != height) { browser->desiredwidth = width; browser->desiredheight = height; host->was_resized(host); } cef_release(host); return qtrue; } static void VARGS Cef_GetSize (void *ctx, int *width, int *height) { //this specifies the logical size/aspect of the browser object browser_t *browser = (browser_t*)ctx; *width = browser->desiredwidth; *height = browser->desiredheight; } static void VARGS Cef_ChangeStream (void *ctx, const char *streamname) { browser_t *browser = (browser_t*)ctx; cef_browser_host_t *host = browser->thebrowser->get_host(browser->thebrowser); cef_frame_t *frame = NULL; if (!strncmp(streamname, "cmd:", 4)) { const char *cmd = streamname+4; if (!strcmp(cmd, "focus")) host->set_focus(host, true); else if (!strcmp(cmd, "unfocus")) host->set_focus(host, false); else if (!strcmp(cmd, "refresh")) browser->thebrowser->reload(browser->thebrowser); else if (!strcmp(cmd, "transparent")) ; else if (!strcmp(cmd, "opaque")) ; else if (!strcmp(cmd, "stop")) browser->thebrowser->stop_load(browser->thebrowser); else if (!strcmp(cmd, "back")) browser->thebrowser->go_back(browser->thebrowser); else if (!strcmp(cmd, "forward")) browser->thebrowser->go_forward(browser->thebrowser); else if (!strcmp(cmd, "home")) Cef_ChangeStream(ctx, "http://fte.triptohell.info"); else { frame = browser->thebrowser->get_focused_frame(browser->thebrowser); if (!strcmp(cmd, "undo")) frame->undo(frame); else if (!strcmp(cmd, "redo")) frame->redo(frame); else if (!strcmp(cmd, "cut")) frame->cut(frame); else if (!strcmp(cmd, "copy")) frame->copy(frame); else if (!strcmp(cmd, "paste")) ;//frame->paste(frame); //possible security hole, as this uses the system clipboard else if (!strcmp(cmd, "del")) frame->del(frame); else if (!strcmp(cmd, "selectall")) frame->select_all(frame); else Con_Printf("unrecognised cmd: %s\n", cmd); } } else if (!strncmp(streamname, "javascript:", 11)) { cef_string_t thescript = {NULL}; cef_string_t url = {NULL}; cef_string_from_utf8(streamname+11, strlen(streamname+11), &thescript); cef_string_from_utf8("http://localhost/", strlen("http://localhost/"), &url); frame = browser->thebrowser->get_main_frame(browser->thebrowser); frame->execute_java_script(frame, &thescript, &url, 1); cef_string_clear(&thescript); cef_string_clear(&url); } /*else if (!strncmp(streamname, "raw:", 4)) { cef_string_t thehtml = {NULL}; cef_string_t url = {NULL}; cef_string_from_utf8(streamname+4, strlen(streamname+4), &thehtml); cef_string_from_utf8("http://localhost/", strlen("http://localhost/"), &url); frame = browser->thebrowser->get_main_frame(browser->thebrowser); frame->load_string(frame, &thehtml, &url); cef_string_clear(&thehtml); cef_string_clear(&url); }*/ else if (*streamname && strcmp(streamname, "http:") && strcmp(streamname, "https:")) { cef_string_t url = {NULL}; cef_string_from_utf8(streamname, strlen(streamname), &url); frame = browser->thebrowser->get_main_frame(browser->thebrowser); frame->load_url(frame, &url); cef_string_clear(&url); } if (frame) cef_release(frame); cef_release(host); } qboolean VARGS Cef_GetProperty (void *ctx, const char *field, char *out, size_t *outsize) { browser_t *browser = (browser_t*)ctx; const char *ret = NULL; if (!strcmp(field, "url")) ret = browser->currenturl.str; else if (!strcmp(field, "title")) ret = browser->currenttitle.str; else if (!strcmp(field, "status")) ret = browser->currentstatus.str; else if (!strcmp(field, "icon")) ret = browser->currenticon.str; if (ret) { size_t retsize = strlen(ret); if (out) { if (*outsize < retsize) return false; //caller fucked up memcpy(out, ret, retsize); } *outsize = retsize; return true; } return false; } static media_decoder_funcs_t decoderfuncs; static qintptr_t Cef_Tick(qintptr_t *args) { if (cefwasinitialised) { cef_do_message_loop_work(); /* libcef can't cope with this. if (!numbrowsers) { if (request_context) { cef_release(request_context); request_context = NULL; } cef_shutdown(); cefwasinitialised = false; } */ } return 0; } static qintptr_t Cef_Shutdown(qintptr_t *args) { if (cefwasinitialised) { int tries = 1000*10;//60*5; //keep trying for a duration (in ms)... give up after then as it just isn't working. while(numbrowsers && tries > 0) { cef_do_message_loop_work(); tries -= 10; #ifdef _WIN32 Sleep(10); #else usleep(10*1000); #endif } #ifdef _WIN32 if (numbrowsers) { //this really should NOT be happening. MessageBox(NULL, "Browsers are still open", "CEF Fuckup", 0); } #endif if (request_context) { cef_release(request_context); request_context = NULL; } cef_shutdown(); cefwasinitialised = false; numbrowsers = 0; } #if defined(_DEBUG) && defined(_MSC_VER) // _CrtDumpMemoryLeaks(); #endif return 0; } #ifndef _WIN32 static int argc=0; static char *argv[64]; static char commandline[8192]; static void SetupArgv(cef_main_args_t *a) { FILE *f; if (!argc) { f = fopen("/proc/self/cmdline", "r"); if (f) { char *s = commandline; char *e = commandline+fread(commandline, 1, sizeof(commandline), f); fclose(f); while(s < e) { argv[argc++] = s; while(*s) s++; s++; } } } a->argc = argc; a->argv = argv; } #endif //if we're a subprocess and somehow failed to add the --plugwrapper arg to the engine, then make sure we're not starting endless processes. static qboolean Cef_Init(qboolean engineprocess) { static qboolean cefwasloaded = qfalse; #ifdef _WIN32 cef_main_args_t args = {GetModuleHandle(NULL)}; #else cef_main_args_t args; SetupArgv(&args); #endif if (cefwasloaded) return qtrue; { int result; #ifdef LIBCEF_DYNAMIC dllfunction_t ceffuncs[] = { {(void **)&cef_version_info, "cef_version_info"}, {(void **)&cef_initialize, "cef_initialize"}, {(void **)&cef_do_message_loop_work, "cef_do_message_loop_work"}, {(void **)&cef_shutdown, "cef_shutdown"}, {(void **)&cef_execute_process, "cef_execute_process"}, {(void **)&cef_browser_host_create_browser_sync,"cef_browser_host_create_browser_sync"}, {(void **)&cef_string_utf8_to_utf16, "cef_string_utf8_to_utf16"}, {(void **)&cef_string_utf16_to_utf8, "cef_string_utf16_to_utf8"}, {(void **)&cef_string_utf16_clear, "cef_string_utf16_clear"}, {(void **)&cef_string_utf16_set, "cef_string_utf16_set"}, {(void **)&cef_string_utf8_clear, "cef_string_utf8_clear"}, {(void **)&cef_string_utf8_set, "cef_string_utf8_set"}, {(void **)&cef_string_userfree_utf16_free, "cef_string_userfree_utf16_free"}, {(void **)&cef_register_scheme_handler_factory, "cef_register_scheme_handler_factory"}, {(void **)&cef_v8value_create_function, "cef_v8value_create_function"}, {(void **)&cef_v8value_create_string, "cef_v8value_create_string"}, {(void **)&cef_process_message_create, "cef_process_message_create"}, {(void **)&cef_v8context_get_current_context, "cef_v8context_get_current_context"}, {(void **)&cef_post_task, "cef_post_task"}, {(void **)&cef_request_context_create_context, "cef_request_context_create_context"}, {(void **)&cef_string_multimap_alloc, "cef_string_multimap_alloc"}, {(void **)&cef_string_multimap_append, "cef_string_multimap_append"}, {(void **)&cef_string_multimap_size, "cef_string_multimap_size"}, {(void **)&cef_string_multimap_key, "cef_string_multimap_key"}, {(void **)&cef_string_multimap_value, "cef_string_multimap_value"}, {(void **)&cef_string_multimap_free, "cef_string_multimap_free"}, {(void **)&cef_string_list_size, "cef_string_list_size"}, {(void **)&cef_string_list_value, "cef_string_list_value"}, {NULL} }; #ifdef _WIN32 if (plugfuncs && !plugfuncs->LoadDLL("libcef", ceffuncs)) #else if (plugfuncs && !plugfuncs->LoadDLL("./libcef", ceffuncs)) #endif { if (engineprocess) Con_Printf("Unable to load libcef (version "CEF_VERSION")\n"); return false; } #endif if (engineprocess) { Con_DPrintf("libcef %i.%i.%i.%i (chrome %i.%i.%i.%i)\n", cef_version_info(0), cef_version_info(1), cef_version_info(2), cef_version_info(3), cef_version_info(4), cef_version_info(5), cef_version_info(6), cef_version_info(7)); if (cef_version_info(0) != CEF_VERSION_MAJOR|| cef_version_info(1) != CEF_VERSION_MINOR|| cef_version_info(2) != CEF_VERSION_PATCH|| cef_version_info(3) != CEF_COMMIT_NUMBER|| cef_version_info(4) != CHROME_VERSION_MAJOR|| cef_version_info(5) != CHROME_VERSION_MINOR|| cef_version_info(6) != CHROME_VERSION_BUILD|| cef_version_info(7) != CHROME_VERSION_PATCH) { //the libcef api hash can be used to see if there's an api change that might break stuff. //refuse to load it if the api changed. Con_Printf("libcef outdated. Please install libcef version "CEF_VERSION"\n"); return false; } } app_initialize(); if (!engineprocess) { result = cef_execute_process(&args, &app, 0); if (result >= 0 || !engineprocess) { //result is meant to be the exit code that the child process is meant to exit with //either way, we really don't want to return to the engine because that would run a second instance of it. exit(result); return qfalse; } } } return cefwasloaded=qtrue; } //works with the --plugwrapper engine argument int NATIVEEXPORT CefSubprocessInit(plugcorefuncs_t *corefuncs) { plugfuncs = corefuncs; return Cef_Init(false); } void Cef_ExecuteCommand(void) { if (confuncs && Cef_Init(true)) { static int sequence; char f[128]; char videomap[8192]; Q_snprintf(f, sizeof(f), "libcef:%i", ++sequence); newconsole = f; strcpy(videomap, "cef:"); cmdfuncs->Argv(1, videomap+4, sizeof(videomap)-4); if (!videomap[4]) strcpy(videomap, "cef:https://fte.triptohell.info"); confuncs->SetConsoleString(f, "title", videomap+4); confuncs->SetConsoleFloat(f, "iswindow", true); confuncs->SetConsoleFloat(f, "forceutf8", true); confuncs->SetConsoleFloat(f, "wnd_w", 640+16); confuncs->SetConsoleFloat(f, "wnd_h", 480+16+8); confuncs->SetConsoleString(f, "backvideomap", videomap); confuncs->SetConsoleFloat(f, "linebuffered", 2); confuncs->SetActive(f); newconsole = NULL; } } static qboolean QDECL Cef_PluginMayUnload(void) { if (cefwasinitialised) return false; //cef is a piece of shite. we have to leave it running or the threads it spawns will crash+burn... return true; } qboolean Plug_Init(void) { fsfuncs = (plugfsfuncs_t*)plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*fsfuncs)); //for fte://data/ scheme confuncs = (plugsubconsolefuncs_t*)plugfuncs->GetEngineInterface(plugsubconsolefuncs_name, sizeof(*confuncs)); //for cef command etc. clientfuncs = (plugclientfuncs_t*)plugfuncs->GetEngineInterface(plugclientfuncs_name, sizeof(*clientfuncs)); //for weird people trying to use xml requests to query game status (for hud stuff) if (!fsfuncs || !confuncs || !clientfuncs || !plugfuncs->GetPluginName(-1, plugname, sizeof(plugname)) || !plugfuncs->ExportFunction("MayUnload", Cef_PluginMayUnload) || !plugfuncs->ExportFunction("Tick", Cef_Tick) || !plugfuncs->ExportFunction("Shutdown", Cef_Shutdown)) { Con_Printf("CEF plugin failed: Required engine feature missing.\n"); return false; } decoderfuncs.structsize = sizeof(media_decoder_funcs_t); decoderfuncs.drivername = "cef"; decoderfuncs.createdecoder = Cef_CreateOld; // decoderfuncs.createdecoderCB = Cef_Create; decoderfuncs.decodeframe = Cef_DisplayFrame; decoderfuncs.shutdown = Cef_Destroy; decoderfuncs.cursormove = Cef_CursorMove; decoderfuncs.key = Cef_Key; decoderfuncs.setsize = Cef_SetSize; decoderfuncs.getsize = Cef_GetSize; decoderfuncs.changestream = Cef_ChangeStream; decoderfuncs.getproperty = Cef_GetProperty; if (!plugfuncs->ExportInterface("Media_VideoDecoder", &decoderfuncs, sizeof(decoderfuncs))) { Con_Printf("CEF plugin failed: Engine doesn't support media decoder plugins\n"); return false; } cmdfuncs->AddCommand("cef", Cef_ExecuteCommand, "Open a web page!"); cef_incognito = cvarfuncs->GetNVFDG("cef_incognito", "0", 0, NULL, "browser settings"); cef_allowplugins = cvarfuncs->GetNVFDG("cef_allowplugins", "0", 0, NULL, "browser settings"); cef_allowcvars = cvarfuncs->GetNVFDG("cef_allowcvars", "0", 0, NULL, "browser settings"); cef_devtools = cvarfuncs->GetNVFDG("cef_devtools", "0", 0, NULL, "browser settings"); return true; } ================================================ FILE: plugins/cod/codbsp.c ================================================ // https://wiki.zeroy.com/index.php?title=Call_of_Duty_1:_d3dbsp // https://wiki.zeroy.com/index.php?title=Call_of_Duty_2:_d3dbsp //yes, shockingly badly documented. that's half the challenge though, right? //vaguely derived from quake3. #include "../plugin.h" #include "../engine/common/com_mesh.h" #include "../engine/common/com_bih.h" static plugfsfuncs_t *filefuncs; static plugmodfuncs_t *modfuncs; static plugthreadfuncs_t *threadfuncs; typedef struct { //materials are used for rendering and collisions q2mapsurface_t *surfaces; //for collision properties, texturing info, not actual surfaces. //generally useful stuff unsigned int codbspver; mplane_t *planes; size_t num_planes; struct codnode_s { mplane_t *plane; int childnum[2]; ivec3_t mins, maxs; } *nodes; size_t numnodes; struct codleaf_s { int cluster; int area; //we don't care about what we don't understand. //unsigned int firstleafbrush; //unsigned int numleafbrushes; //unsigned int firstleafsurface; //unsigned int numleafsurfaces; //int cell; unsigned int firstlightindex; unsigned int numlightindexes; } *leaf; size_t numleafs; qbyte *pvsdata; //rendering stuff. this is pretty simple as its all soup. mesh_t soupverts; //don't trust the indexes! struct codsoup_s { unsigned int vertex_offset; unsigned int index_offset; unsigned int index_fixup; } *soups; //aka surfs //brush collision... this stuff all goes away once we've built our BIH. const struct codbsp_brushside_s { //unswapped... union{ unsigned int plane; float dist; }; unsigned int material_idx; } *brushsides; size_t num_brushsides; q2cbrush_t *brushes; size_t num_brushes; //patch/soup collision nightmare. const struct codpatch_s { //unswapped... short mat; //material? short mode; //mode union { struct { //patch short w; //width short h; //height int unknown; //unknown. flatness? int firstvert; //first CODLUMP_COLLISIONVERTS } mode0; struct { //soup short numverts; short numidx; int firstvert; //first CODLUMP_COLLISIONVERTS int firstidx; //first CODLUMP_COLLISIONINDEXES } mode1; }; } *patches; size_t numpatches; const unsigned int *leafpatches; //loadtime only, unswapped... size_t numleafpatches; vecV_t *patchvertexes; size_t numpatchvertexes; index_t *patchindexes; size_t numpatchindexes; unsigned short *lightindexes; size_t numlightindexes; struct codlight_s { int type; vec3_t xyz; vec3_t rgb; vec3_t dir; float scale; float fov; } *lights; size_t numlights; } codbspinfo_t; #define COD1BSP_VERSION 0x0000003b #define COD2BSP_VERSION 0x00000004 enum { COD1LUMP_MATERIALS=0, //names, surfaceflags, and contentbits for said materials (so dedicated servers don't need to parse anything extra). COD1LUMP_LIGHTMAPS=1, //just 2d images. 512*512 RGB ones. COD1LUMP_PLANES=2, //used for nodes and brushsides COD1LUMP_BRUSHSIDES=3, //which planes+materials to use for each side of the brushes... COD1LUMP_BRUSHES=4, //defines which sets of said sides to group, and their material(contents). // COD1LUMP_UNKNOWN=5, COD1LUMP_SOUPS=6, //q3 would call em surfaces. except they're ALL trisoup, none are planar/patches, they're prebatched (within their 'cells'). COD1LUMP_SOUPVERTS=7, //the vertex attributes for the soup COD1LUMP_SOUPINDEXES=8, //the indexes, woo. how fancy. it ain't soup without this! COD1LUMP_CULLGROUPS=9, COD1LUMP_CULLGROUPINDEXES=10, COD1LUMP_PORTALVERTS=11, COD1LUMP_OCCLUDERS=12, COD1LUMP_OCCLUDERPLANES=13, COD1LUMP_OCCLUDEREDGES=14, COD1LUMP_OCCLUDERINDEXES=15, COD1LUMP_AABBTREES=16, //some sort of tree... COD1LUMP_CELLS=17, //part of the portal rendering system (reduced number vs leafs so there's less to compute) COD1LUMP_PORTALS=18, //for walking between cells? COD1LUMP_LIGHTINDEXES=19, //indexes into LIGHTVALUES COD1LUMP_NODES=20, //tree leading to leafs. COD1LUMP_LEAFS=21, //describes a small convex area COD1LUMP_LEAFBRUSHES=22, //list of brushes so leafs can share them, for collision+pointcontents. COD1LUMP_LEAFPATCHES=23, //list of patches and embedded meshes per leaf, for collision COD1LUMP_PATCHCOLLISION=24, //defines which verts/topology etc to use for each mesh/patch COD1LUMP_COLLISIONVERTS=25, //simple verts used for patch/embedded-mesh collision COD1LUMP_COLLISIONINDEXES=26,//indexes for the collision verts, only used for embedded meshes COD1LUMP_MODELS=27, //sub (aka inline) models - the first entry is the worldmodel, the second is your "*1" model and upwards. COD1LUMP_VISIBILITY=28, //numclusters, numrowbytes, then just packed rows per cluster (no compression like q3 unlike q1). COD1LUMP_ENTITIES=29, COD1LUMP_LIGHTS=30, // COD1LUMP_UNKNOWN=31, // COD1LUMP_UNKNOWN=32, COD1LUMP_COUNT=33 }; enum { COD2LUMP_MATERIALS=0, COD2LUMP_LIGHTMAPS=1, COD2LUMP_LIGHTGRIDHASH=2, COD2LUMP_LIGHTGRIDVALUES=3, COD2LUMP_PLANES=4, COD2LUMP_BRUSHSIDES=5, COD2LUMP_BRUSHES=6, COD2LUMP_SOUPS=7, COD2LUMP_SOUPVERTS=8, COD2LUMP_SOUPINDEXES=9, COD2LUMP_CULLGROUPS=10, COD2LUMP_CULLGROUPINDEXES=11, // COD2LUMP_UNKNOWN=12, // COD2LUMP_UNKNOWN=13, // COD2LUMP_UNKNOWN=14, // COD2LUMP_UNKNOWN=15, // COD2LUMP_UNKNOWN=16, COD2LUMP_PORTALVERTS=17, COD2LUMP_OCCLUDERS=18, COD2LUMP_OCCLUDERPLANES=19, COD2LUMP_OCCLUDEREDGES=20, COD2LUMP_OCCLUDERINDEXES=21, COD2LUMP_AABBTREES=22, COD2LUMP_CELLS=23, COD2LUMP_PORTALS=24, COD2LUMP_NODES=25, COD2LUMP_LEAFS=26, COD2LUMP_LEAFBRUSHES=27, // COD2LUMP_LEAFPATCHES=28, //o.O COD2LUMP_COLLISIONVERTS=29, COD2LUMP_COLLISIONEDGES=30, COD2LUMP_COLLISIONINDEXES=31, COD2LUMP_COLLISIONBORDERS=32, COD2LUMP_COLLISIONPARTS=33, COD2LUMP_COLLISIONAABBS=34, COD2LUMP_MODELS=35, COD2LUMP_VISIBILITY=36, COD2LUMP_ENTITIES=37, COD2LUMP_PATHS=38, COD2LUMP_COUNT=39 }; static struct codleaf_s *CODBSP_LeafForPoint (model_t *mod, const vec3_t p, int num) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; float d; struct codnode_s *node; mplane_t *plane; while (num >= 0) { node = prv->nodes + num; plane = node->plane; if (plane->type < 3) d = p[plane->type] - plane->dist; else d = DotProduct (plane->normal, p) - plane->dist; if (d < 0) num = node->childnum[1]; else num = node->childnum[0]; } return &prv->leaf[-1 - num]; } static int CODBSP_ClusterForPoint (struct model_s *model, const vec3_t point, int *areaout) { struct codleaf_s *leaf = CODBSP_LeafForPoint(model, point, 0); if (areaout) *areaout = leaf->area; return leaf->cluster; } static qbyte *CODBSP_ClusterPVS (struct model_s *model, int cluster, pvsbuffer_t *pvsbuffer, pvsmerge_t merge) { codbspinfo_t *prv = (codbspinfo_t*)model->meshinfo; size_t i; if (cluster >= 0 && cluster < model->numclusters) { qbyte *pvs = prv->pvsdata + cluster*model->pvsbytes; //packed, without compresion. if (merge == PVM_FAST) return pvs; else { if (pvsbuffer->buffersize < model->pvsbytes) pvsbuffer->buffer = plugfuncs->Realloc(pvsbuffer->buffer, pvsbuffer->buffersize = model->pvsbytes); if (merge==PVM_REPLACE) memcpy(pvsbuffer->buffer, pvs, model->pvsbytes); else for (i = 0; i < model->pvsbytes; i++) pvsbuffer->buffer[i] |= pvs[i]; //slooooow return pvsbuffer->buffer; } } if (pvsbuffer) { if (pvsbuffer->buffersize < model->pvsbytes) pvsbuffer->buffer = plugfuncs->Realloc(pvsbuffer->buffer, pvsbuffer->buffersize = model->pvsbytes); if (merge!=PVM_MERGE) memset(pvsbuffer->buffer, 0, model->pvsbytes); return pvsbuffer->buffer; } return NULL; } //static qbyte *CODBSP_ClusterPHS (struct model_s *model, int cluster, pvsbuffer_t *pvsbuffer){return "\xff";} //static void CODBSP_PurgeModel(struct model_s *mod){} //static qbyte *CODBSP_ClustersInSphere(struct model_s *model, const vec3_t point, float radius, pvsbuffer_t *pvsbuffer, const qbyte *fte_restrict unionwith){} //static size_t CODBSP_WriteAreaBits(struct model_s *model, qbyte *buffer, size_t maxbytes, int area, qboolean merge){} //static qboolean CODBSP_AreasConnected(struct model_s *model, unsigned int area1, unsigned int area2){} //static void CODBSP_SetAreaPortalState(struct model_s *model, unsigned int portal, unsigned int area1, unsigned int area2, qboolean open){} //static size_t CODBSP_SaveAreaPortalBlob(struct model_s *model, void **ptr){} //static size_t CODBSP_LoadAreaPortalBlob(struct model_s *model, void *ptr, size_t size){} #ifdef HAVE_SERVER static int leaf_count, leaf_maxcount; static struct codleaf_s **leaf_list; static const float *leaf_mins, *leaf_maxs; static int leaf_topnode; #define BoxOnPlaneSide CodBoxOnPlaneSide static int BoxOnPlaneSide (const vec3_t emins, const vec3_t emaxs, const mplane_t *p) { float dist1, dist2; int sides; // general case switch (p->signbits) { default: case 0: dist1 = p->normal[0]*emaxs[0] + p->normal[1]*emaxs[1] + p->normal[2]*emaxs[2]; dist2 = p->normal[0]*emins[0] + p->normal[1]*emins[1] + p->normal[2]*emins[2]; break; case 1: dist1 = p->normal[0]*emins[0] + p->normal[1]*emaxs[1] + p->normal[2]*emaxs[2]; dist2 = p->normal[0]*emaxs[0] + p->normal[1]*emins[1] + p->normal[2]*emins[2]; break; case 2: dist1 = p->normal[0]*emaxs[0] + p->normal[1]*emins[1] + p->normal[2]*emaxs[2]; dist2 = p->normal[0]*emins[0] + p->normal[1]*emaxs[1] + p->normal[2]*emins[2]; break; case 3: dist1 = p->normal[0]*emins[0] + p->normal[1]*emins[1] + p->normal[2]*emaxs[2]; dist2 = p->normal[0]*emaxs[0] + p->normal[1]*emaxs[1] + p->normal[2]*emins[2]; break; case 4: dist1 = p->normal[0]*emaxs[0] + p->normal[1]*emaxs[1] + p->normal[2]*emins[2]; dist2 = p->normal[0]*emins[0] + p->normal[1]*emins[1] + p->normal[2]*emaxs[2]; break; case 5: dist1 = p->normal[0]*emins[0] + p->normal[1]*emaxs[1] + p->normal[2]*emins[2]; dist2 = p->normal[0]*emaxs[0] + p->normal[1]*emins[1] + p->normal[2]*emaxs[2]; break; case 6: dist1 = p->normal[0]*emaxs[0] + p->normal[1]*emins[1] + p->normal[2]*emins[2]; dist2 = p->normal[0]*emins[0] + p->normal[1]*emaxs[1] + p->normal[2]*emaxs[2]; break; case 7: dist1 = p->normal[0]*emins[0] + p->normal[1]*emins[1] + p->normal[2]*emins[2]; dist2 = p->normal[0]*emaxs[0] + p->normal[1]*emaxs[1] + p->normal[2]*emaxs[2]; break; } sides = 0; if (dist1 >= p->dist) sides = 1; if (dist2 < p->dist) sides |= 2; return sides; } static void CODBSP_BoxLeafs_r (codbspinfo_t *prv, int nodenum) { mplane_t *plane; struct codnode_s *node; int s; while (1) { if (nodenum < 0) { if (leaf_count >= leaf_maxcount) return; leaf_list[leaf_count++] = &prv->leaf[-1 - nodenum]; return; } node = &prv->nodes[nodenum]; plane = node->plane; s = BOX_ON_PLANE_SIDE(leaf_mins, leaf_maxs, plane); if (s == 1) nodenum = node->childnum[0]; else if (s == 2) nodenum = node->childnum[1]; else { // go down both if (leaf_topnode == -1) leaf_topnode = nodenum; CODBSP_BoxLeafs_r (prv, node->childnum[0]); nodenum = node->childnum[1]; } } } static int CODBSP_BoxLeafs (model_t *mod, const vec3_t mins, const vec3_t maxs, struct codleaf_s **list, int listsize, int *topnode) { leaf_list = list; leaf_count = 0; leaf_maxcount = listsize; leaf_mins = mins; leaf_maxs = maxs; leaf_topnode = -1; CODBSP_BoxLeafs_r (mod->meshinfo, 0); if (topnode) *topnode = leaf_topnode; return leaf_count; } static unsigned int CODBSP_FatPVS (struct model_s *mod, const vec3_t org, pvsbuffer_t *result, qboolean merge) { struct codleaf_s *leafs[64]; int i, j, count; vec3_t mins, maxs; for (i=0 ; i<3 ; i++) { mins[i] = org[i] - 8; maxs[i] = org[i] + 8; } count = CODBSP_BoxLeafs (mod, mins, maxs, leafs, countof(leafs), NULL); if (count < 1) Sys_Errorf ("CODBSP_FatPVS: count < 1"); //grow the buffer if needed if (result->buffersize < mod->pvsbytes) result->buffer = plugfuncs->Realloc(result->buffer, result->buffersize=mod->pvsbytes); if (count == 1 && leafs[0]->cluster == -1) { //if the only leaf is the outside then broadcast it. memset(result->buffer, 0xff, mod->pvsbytes); i = count; } else { i = 0; if (!merge) mod->funcs.ClusterPVS(mod, leafs[i++]->cluster, result, PVM_REPLACE); // or in all the other leaf bits for ( ; ifuncs.ClusterPVS(mod, leafs[i]->cluster, result, PVM_MERGE); } } return mod->pvsbytes; } static qboolean CODBSP_HeadnodeVisible (codbspinfo_t *prv, int nodenum, const qbyte *visbits) { int leafnum; int cluster; struct codnode_s *node; if (nodenum < 0) { leafnum = -1-nodenum; cluster = prv->leaf[leafnum].cluster; if (cluster == -1) return false; if (visbits[cluster>>3] & (1<<(cluster&7))) return true; return false; } node = &prv->nodes[nodenum]; if (CODBSP_HeadnodeVisible(prv, node->childnum[0], visbits)) return true; return CODBSP_HeadnodeVisible(prv, node->childnum[1], visbits); } static qboolean CODBSP_EdictInFatPVS (struct model_s *mod, const struct pvscache_s *ent, const qbyte *pvs, const int *areas) { int i,l; /*int nullarea = -1; if (areas) { //areas[0] is the count of areas the camera is in, if valid. requires us to track portal states... for (i = 1; ; i++) { if (i > areas[0]) return false; //none of the camera's areas could see the entity if (areas[i] == ent->areanum) { if (areas[i] != nullarea) break; //else entity is fully outside the world, invisible to all... } else if (CODBSP_AreasConnected (mod, areas[i], ent->areanum)) break; // doors can legally straddle two areas, so // we may need to check another one else if (ent->areanum2 != nullarea && CODBSP_AreasConnected (mod, areas[i], ent->areanum2)) break; } }*/ if (ent->num_leafs == -1) { // too many leafs for individual check, go by headnode if (!CODBSP_HeadnodeVisible (mod->meshinfo, ent->headnode, pvs)) return false; } else { // check individual leafs for (i=0 ; i < ent->num_leafs ; i++) { l = ent->leafnums[i]; if (pvs[l >> 3] & (1 << (l&7) )) break; } if (i == ent->num_leafs) return false; // not visible } return true; } static void CODBSP_FindTouchedLeafs (struct model_s *model, struct pvscache_s *ent, const vec3_t mins, const vec3_t maxs) { #define MAX_TOTAL_ENT_LEAFS MAX_ENT_LEAFS+1 struct codleaf_s *leafs[MAX_TOTAL_ENT_LEAFS]; int clusters[MAX_TOTAL_ENT_LEAFS]; int num_leafs; int topnode; int i, j; int area; int nullarea = -1; //ent->num_leafs == q2's ent->num_clusters ent->num_leafs = 0; ent->areanum = nullarea; ent->areanum2 = nullarea; if (!mins || !maxs) return; //get all leafs, including solids num_leafs = CODBSP_BoxLeafs (model, mins, maxs, leafs, MAX_TOTAL_ENT_LEAFS, &topnode); // set areas for (i=0 ; icluster; //could dedupe these. area = leafs[i]->area; if (area != nullarea) { // doors may legally straggle two areas, // but nothing should ever need more than that if (ent->areanum != nullarea && ent->areanum != area) ent->areanum2 = area; else ent->areanum = area; } } if (num_leafs >= MAX_TOTAL_ENT_LEAFS) { // assume we missed some leafs, and mark by headnode ent->num_leafs = -1; ent->headnode = topnode; } else { ent->num_leafs = 0; for (i=0 ; inum_leafs == MAX_ENT_LEAFS) { // assume we missed some leafs, and mark by headnode ent->num_leafs = -1; ent->headnode = topnode; break; } ent->leafnums[ent->num_leafs++] = clusters[i]; } } } } #endif #ifdef HAVE_CLIENT static void CODBSP_LightPointValues (struct model_s *mod, const vec3_t point, vec3_t res_diffuse, vec3_t res_ambient, vec3_t res_dir) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct codleaf_s *leaf = CODBSP_LeafForPoint(mod, point, 0); unsigned short *lightindexes = prv->lightindexes + leaf->firstlightindex; size_t i; struct codlight_s *light; // vec3_t move; // float d; float scale; if (!leaf->numlightindexes) { VectorSet(res_diffuse, 128,128,128); VectorSet(res_ambient, 128,128,128); VectorSet(res_dir, 1,0,0); return; } VectorSet(res_diffuse, 0,0,0); VectorSet(res_ambient, 0,0,0); VectorSet(res_dir, 0,0,0); for (i = 0; i < leaf->numlightindexes; i++, lightindexes++) { if (*lightindexes >= prv->numlights) { // :( don't know what this is meant to signify, happens with the first index more often than not. if (!prv->numlights) continue; light = prv->lights; } else light = prv->lights + *lightindexes; switch (light->type) { case 1: //sun... scale = 256; break; /* case 4: VectorSubtract(point, light->xyz, move); d = DotProduct(move,move); if (d > light->scale) continue; scale = (light->scale-d)/d; scale *= 256; break;*/ /* case 5: break;*/ /* case 7: break;*/ default: continue; } VectorMA(res_diffuse, scale, light->rgb, res_diffuse); VectorMA(res_ambient, scale, light->rgb, res_ambient); VectorMA(res_dir, scale, light->dir, res_dir); break; //:( } scale = DotProduct(res_dir,res_dir); if (scale <= 0) VectorSet(res_dir, 1,0,0); else VectorScale(res_dir, 1/scale, res_dir); } //static void CODBSP_GenerateShadowMesh (struct model_s *model, dlight_t *dl, const qbyte *lvis, qbyte *truevis, void(*callback)(struct msurface_s*)){} //static void CODBSP_StainNode (struct model_s *model, float *parms){} //static void CODBSP_MarkLights (struct dlight_s *light, dlightbitmask_t bit, struct mnode_s *node){} static void CODBSP_BuildSurfMesh(model_t *mod, msurface_t *surf, builddata_t *bd) { //just builds the actual mesh data, now that it has per-batch storage allocated. codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; mesh_t *mesh = surf->mesh; struct codsoup_s *soup = &prv->soups[surf-mod->surfaces]; int i; mesh->istrifan = false; memcpy(mesh->xyz_array, prv->soupverts.xyz_array+soup->vertex_offset, sizeof(*mesh->xyz_array)*mesh->numvertexes); memcpy(mesh->normals_array, prv->soupverts.normals_array+soup->vertex_offset, sizeof(*mesh->normals_array)*mesh->numvertexes); for (i = 0; i < mesh->numvertexes; i++) Vector4Scale(prv->soupverts.colors4b_array[soup->vertex_offset+i], 1.f/255, mesh->colors4f_array[0][i]); //memcpy(mesh->colors4b_array, prv->soupverts.colors4b_array+soup->vertex_offset, sizeof(*mesh->colors4b_array)*mesh->numvertexes); memcpy(mesh->st_array, prv->soupverts.st_array+soup->vertex_offset, sizeof(*mesh->st_array)*mesh->numvertexes); memcpy(mesh->lmst_array[0], prv->soupverts.lmst_array[0]+soup->vertex_offset, sizeof(*mesh->lmst_array[0])*mesh->numvertexes); if (soup->index_fixup) { for (i = 0; i < mesh->numindexes; i++) mesh->indexes[i] = prv->soupverts.indexes[soup->index_offset+i] - soup->index_fixup; } else memcpy(mesh->indexes, prv->soupverts.indexes+soup->index_offset, sizeof(*mesh->indexes)*mesh->numindexes); if (prv->soupverts.snormals_array) { //cod2 made them explicit. yay. memcpy(mesh->snormals_array, prv->soupverts.snormals_array+soup->vertex_offset, sizeof(*mesh->snormals_array)*mesh->numvertexes); memcpy(mesh->tnormals_array, prv->soupverts.tnormals_array+soup->vertex_offset, sizeof(*mesh->tnormals_array)*mesh->numvertexes); } else { //compute the tangents for rtlights. modfuncs->AccumulateTextureVectors(mesh->xyz_array, mesh->st_array, mesh->normals_array, mesh->snormals_array, mesh->tnormals_array, mesh->indexes, mesh->numindexes, false); modfuncs->NormaliseTextureVectors(mesh->normals_array, mesh->snormals_array, mesh->tnormals_array, mesh->numvertexes, false); } } static void CODBSP_GenerateMaterials(void *ctx, void *data, size_t a, size_t b) { model_t *mod = ctx; const char *script; if (!a) { //submodels share textures, so only do this if 'a' is 0 (inline index, 0 = world). for(a = 0; a < mod->numtextures; a++) { script = NULL; if (!strncmp(mod->textures[a]->name, "sky/", 4)) script = "{\n" "sort sky\n" "surfaceparm nodlight\n" "skyparms - - -\n" "}\n"; mod->textures[a]->shader = modfuncs->RegisterBasicShader(mod, mod->textures[a]->name, SUF_LIGHTMAP, script, PTI_INVALID, 0, 0, NULL, NULL); } } modfuncs->Batches_Build(mod, data); if (data) plugfuncs->Free(data); } static void CODBSP_PrepareFrame(struct model_s *mod, refdef_t *refdef, int area, int clusters[2], pvsbuffer_t *vis, qbyte **entvis_out, qbyte **surfvis_out) { *entvis_out = *surfvis_out = CODBSP_ClusterPVS(mod, clusters[0], vis, false); if (clusters[1] != -1) CODBSP_ClusterPVS(mod, clusters[1], vis, true); /*if (!refdef->areabitsknown) { //generate the info each frame, as the gamecode didn't tell us what to use. int leafnum = CODBSP_PointLeafnum (mod, refdef->vieworg); int clientarea = CODBSP_LeafArea (mod, leafnum); CODBSP_WriteAreaBits(mod, refdef->areabits, clientarea, false); refdef->areabitsknown = true; }*/ if (0) { size_t i; msurface_t *surf; for (i = mod->firstmodelsurface+mod->nummodelsurfaces; i --> mod->firstmodelsurface; ) { surf = &mod->surfaces[i]; surf->sbatch->mesh[surf->sbatch->meshes++] = surf->mesh; } } else { size_t i; msurface_t *surf; for (i = mod->firstmodelsurface; i < mod->nummodelsurfaces; i++) { surf = &mod->surfaces[i]; surf->sbatch->mesh[surf->sbatch->meshes++] = surf->mesh; } } //for static props... //ent = modfuncs->NewSceneEntity(); } static void CODBSP_InfoForPoint(struct model_s *mod, vec3_t pos, int *area, int *cluster, unsigned int *contentbits) { struct codleaf_s *leaf = CODBSP_LeafForPoint(mod, pos, 0); *area = leaf->area; *cluster = leaf->cluster; *contentbits = mod->funcs.PointContents(mod, NULL, pos); //needs a proper pointcontents. } #endif static qboolean CODBSP_LoadShaders (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { char shadername[64]; unsigned int surfflags; unsigned int contents; } *in = (void *)(mod_base + l->fileofs); q2mapsurface_t *out; int i, count; texture_t *tex; if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadShaders: funny lump size\n"); return false; } count = l->filelen / sizeof(*in); if (count < 1) { Con_Printf (CON_ERROR "CODBSP_LoadShaders: Map with no shaders\n"); return false; } mod->numtexinfo = count; out = prv->surfaces = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); mod->textures = plugfuncs->GMalloc(&mod->memgroup, (sizeof(texture_t*)+sizeof(mtexinfo_t)+sizeof(texture_t))*count); //+1 is 'noshader' for flares. mod->texinfo = (mtexinfo_t*)(mod->textures+count); tex = (texture_t*)(mod->texinfo+count); mod->numtextures = count; for ( i=0 ; ic.flags = LittleLong ( in->surfflags ); out->c.value = LittleLong ( in->contents ); Q_strlcpy(out->rname, in->shadername, sizeof(out->rname)); mod->texinfo[i].texture = tex+i; mod->texinfo[i].flags = prv->surfaces[i].c.flags; Q_strlcpy(mod->texinfo[i].texture->name, in->shadername, sizeof(mod->texinfo[i].texture->name)); mod->textures[i] = mod->texinfo[i].texture; } return true; } static qboolean COD1BSP_LoadLightmap(model_t *mod, qbyte *mod_base, lump_t *l) { // codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; int overbright; int bytes; size_t i, lev; qbyte *in = mod_base+l->fileofs; mod->lightmaps.width = 512; mod->lightmaps.height = 512; mod->lightmaps.prebaked = PTI_RGB8; mod->lightmaps.fmt = LM_RGB8; bytes = 3; mod->lightmaps.surfstyles = 1; //always style 0... mod->lightmaps.maxstyle = 0; mod->lightmaps.deluxemapping = false; mod->lightmaps.deluxemapping_modelspace = false; mod->lightmaps.first = 0; mod->lightmaps.count = l->filelen / (mod->lightmaps.width*mod->lightmaps.height*bytes); if (l->filelen != mod->lightmaps.count * (mod->lightmaps.width*mod->lightmaps.height*bytes)) { Con_Printf (CON_ERROR "CODBSP_LoadLighting: funny lump size\n"); return false; //err... rounded badly. } mod->lightdata = plugfuncs->GMalloc(&mod->memgroup, l->filelen); mod->lightdatasize = l->filelen; overbright = cvarfuncs->GetFloat("gl_overbright"); mod->engineflags = MDLF_NEEDOVERBRIGHT; if (overbright == 2) memcpy(mod->lightdata, in, l->filelen); else { qbyte *out = mod->lightdata; overbright = (1<<(2-overbright)); for (i = 0; i < l->filelen; i++, in++) { lev = *in * overbright; *out++ = min(255, lev); } } return true; } static qboolean COD2BSP_LoadLightmap(model_t *mod, qbyte *mod_base, lump_t *l) { //seems to be sets of 4 images (3 normalmaps and some extra discoloured one). more deluxemap than lightmap. this is not useful to us. //cod2 bundles some hlsl code, which may reveal clues. // codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; mod->lightmaps.width = 512; mod->lightmaps.height = 512; mod->lightmaps.prebaked = PTI_RGBA8; //needs glsl to use properly. mod->lightmaps.surfstyles = 1; //always style 0... mod->lightmaps.maxstyle = 0; mod->lightmaps.deluxemapping = false; //fixme: uses 4 lightmap textures at a time. mod->lightmaps.deluxemapping_modelspace = false; mod->lightmaps.first = 0; mod->lightmaps.count = 0; Con_Printf(CON_WARNING"COD2 lightmaps are not supported\n"); return true; } static qboolean COD1BSP_LoadSoupVertices (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { vec3_t position; vec2_t tc; vec2_t lmtc; vec3_t normal; byte_vec4_t rgba; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); mesh_t *mesh = &prv->soupverts; if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadSoupVertices: funny lump size\n"); return false; } //allocate lots of space. stoopid separate arrays. prv->soupverts.istrifan = false; prv->soupverts.xyz_array = plugfuncs->GMalloc(&mod->memgroup, count*( sizeof(*mesh->xyz_array)+ sizeof(*mesh->normals_array)+ sizeof(*mesh->colors4b_array)+ sizeof(*mesh->st_array)+ sizeof(*mesh->lmst_array[0]))); mesh->normals_array = (void*)(mesh->xyz_array + count); mesh->colors4b_array = (void*)(mesh->normals_array + count); mesh->st_array = (void*)(mesh->colors4b_array + count); mesh->lmst_array[0] = (void*)(mesh->st_array + count); //copy it all over. for (i = 0; i < count; i++, in++) { mesh->xyz_array[i][0] = LittleFloat(in->position[0]); mesh->xyz_array[i][1] = LittleFloat(in->position[1]); mesh->xyz_array[i][2] = LittleFloat(in->position[2]); mesh->st_array[i][0] = LittleFloat(in->tc[0]); mesh->st_array[i][1] = LittleFloat(in->tc[1]); mesh->lmst_array[0][i][0] = LittleFloat(in->lmtc[0]); mesh->lmst_array[0][i][1] = LittleFloat(in->lmtc[1]); mesh->normals_array[i][0] = LittleFloat(in->normal[0]); mesh->normals_array[i][1] = LittleFloat(in->normal[1]); mesh->normals_array[i][2] = LittleFloat(in->normal[2]); mesh->colors4b_array[i][0] = in->rgba[0]; mesh->colors4b_array[i][1] = in->rgba[1]; mesh->colors4b_array[i][2] = in->rgba[2]; mesh->colors4b_array[i][3] = in->rgba[3]; } return true; } static qboolean COD2BSP_LoadSoupVertices (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { //slightly rearranged for some reason (plus addition of tangents at the end) vec3_t position; vec3_t normal; byte_vec4_t rgba; vec2_t tc; vec2_t lmtc; vec3_t sdir; vec3_t tdir; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); mesh_t *mesh = &prv->soupverts; if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadSoupVertices: funny lump size\n"); return false; } //allocate lots of space. stoopid separate arrays. prv->soupverts.istrifan = false; prv->soupverts.xyz_array = plugfuncs->GMalloc(&mod->memgroup, count*( sizeof(*mesh->xyz_array)+ sizeof(*mesh->normals_array)+ sizeof(*mesh->snormals_array)+ sizeof(*mesh->tnormals_array)+ sizeof(*mesh->colors4b_array)+ sizeof(*mesh->st_array)+ sizeof(*mesh->lmst_array[0]))); mesh->normals_array = (void*)(mesh->xyz_array + count); mesh->snormals_array = (void*)(mesh->normals_array + count); mesh->tnormals_array = (void*)(mesh->snormals_array + count); mesh->colors4b_array = (void*)(mesh->tnormals_array + count); mesh->st_array = (void*)(mesh->colors4b_array + count); mesh->lmst_array[0] = (void*)(mesh->st_array + count); //copy it all over. for (i = 0; i < count; i++, in++) { mesh->xyz_array[i][0] = LittleFloat(in->position[0]); mesh->xyz_array[i][1] = LittleFloat(in->position[1]); mesh->xyz_array[i][2] = LittleFloat(in->position[2]); mesh->st_array[i][0] = LittleFloat(in->tc[0]); mesh->st_array[i][1] = LittleFloat(in->tc[1]); mesh->lmst_array[0][i][0] = LittleFloat(in->lmtc[0]); mesh->lmst_array[0][i][1] = LittleFloat(in->lmtc[1]); mesh->normals_array[i][0] = LittleFloat(in->normal[0]); mesh->normals_array[i][1] = LittleFloat(in->normal[1]); mesh->normals_array[i][2] = LittleFloat(in->normal[2]); mesh->snormals_array[i][0] = LittleFloat(in->sdir[0]); mesh->snormals_array[i][1] = LittleFloat(in->sdir[1]); mesh->snormals_array[i][2] = LittleFloat(in->sdir[2]); mesh->tnormals_array[i][0] = LittleFloat(in->tdir[0]); mesh->tnormals_array[i][1] = LittleFloat(in->tdir[1]); mesh->tnormals_array[i][2] = LittleFloat(in->tdir[2]); mesh->colors4b_array[i][0] = in->rgba[0]; mesh->colors4b_array[i][1] = in->rgba[1]; mesh->colors4b_array[i][2] = in->rgba[2]; mesh->colors4b_array[i][3] = in->rgba[2]; } return true; } static qboolean CODBSP_LoadSoupIndexes (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; unsigned short *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadSoupIndexes: funny lump size\n"); return false; } prv->soupverts.indexes = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*prv->soupverts.indexes)); for (i = 0; i < count; i++, in++) prv->soupverts.indexes[i] = LittleShort(*in); return true; } static qboolean CODBSP_LoadSoups (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { unsigned short material_idx; unsigned short lightmap_idx; unsigned int vertex_offset; unsigned short vertex_count; unsigned short index_count; unsigned int index_offset; } *in = (void*)((qbyte*)mod_base + l->fileofs); struct codsoup_s *out; mesh_t *mesh; size_t j, i, count = l->filelen / sizeof(*in); unsigned int mn,mx, idx; if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadSoups: funny lump size\n"); return false; } prv->soups = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); mod->numsurfaces = count; mod->nummodelsurfaces = count; mod->surfaces = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*mod->surfaces) + count*sizeof(*mesh)); mesh = (void*)(mod->surfaces+count); for (i = 0; i < count; i++, in++, out++) { unsigned short tex = LittleShort(in->material_idx); unsigned short lmap = LittleShort(in->lightmap_idx); out->vertex_offset = LittleLong (in->vertex_offset); out->index_offset = LittleLong (in->index_offset); if (tex >= mod->numtexinfo) return false; for (j = 0; j < MAXCPULIGHTMAPS; j++) mod->surfaces[i].styles[j] = INVALID_LIGHTSTYLE; for (j = 0; j < MAXRLIGHTMAPS; j++) { mod->surfaces[i].vlstyles[j] = INVALID_VLIGHTSTYLE; mod->surfaces[i].lightmaptexturenums[j] = -1; } mod->surfaces[i].styles[0] = 0; mod->surfaces[i].lightmaptexturenums[0] = lmap==(unsigned short)~0u?INVALID_LIGHTSTYLE:lmap; mod->surfaces[i].texinfo = &mod->texinfo[tex]; mod->surfaces[i].mesh = &mesh[i]; mesh[i].numindexes = LittleShort(in->index_count); mesh[i].numvertexes = LittleShort(in->vertex_count); //cod2 sucks and is way out and results in horrible memory use. calculate what it should have been. mn = ~0u; mx = 0; for (j = 0; j < mesh[i].numindexes; j++) { idx = LittleLong(prv->soupverts.indexes[out->index_offset+j]); if (mx<= idx) mx = idx+1; if (mn > idx) mn = idx; } if (mx 65535) return false; out->index_fixup = mn; out->vertex_offset += mn; } return true; } static qboolean CODBSP_LoadLights (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { //18... int type; //1=sunlight? //4=omni? //5=oriented non-spot? //7=oriented spot light? vec3_t rgb; //these values are completely fucked in most modes. :( vec3_t xyz; //actually matches a lightsource vec3_t dir; float pointnineish; //some sort of exponent? falloff? float scale; //?big float. radius? sometimes denormalised? float fov; //very fovy int naught0; //no info. could be floats. int naught1; //no info. could be floats. int naught2; //no info. could be floats. int naught3; //no info. could be floats. int naught4; //no info. could be floats. } *in = (void*)((qbyte*)mod_base + l->fileofs); struct codlight_s *out; size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadLightValues: funny lump size\n"); return false; } prv->lights = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); prv->numlights = count; for (i = 0; i < count; i++, in++, out++) { out->type = LittleLong(in->type); out->xyz[0] = LittleFloat(in->xyz[0]); out->xyz[1] = LittleFloat(in->xyz[1]); out->xyz[2] = LittleFloat(in->xyz[2]); out->rgb[0] = LittleFloat(in->rgb[0]); out->rgb[1] = LittleFloat(in->rgb[1]); out->rgb[2] = LittleFloat(in->rgb[2]); out->dir[0] = LittleFloat(in->dir[0]); out->dir[1] = LittleFloat(in->dir[1]); out->dir[2] = LittleFloat(in->dir[2]); out->scale = LittleFloat(in->scale); out->fov = LittleFloat(in->fov); } return true; } static qboolean CODBSP_LoadLightIndexes (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; unsigned short *in = (void*)((qbyte*)mod_base + l->fileofs), *out, v; size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadLightIndexes: funny lump size\n"); return false; } prv->lightindexes = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); prv->numlightindexes = count; for (i = 0; i < count; i++) { v = LittleShort(*in++); if (v == 0xffff) ; //o.O else if (v >= prv->numlights) { Con_Printf (CON_ERROR "CODBSP_LoadLightIndexes: invalid index %i\n", v); return false; } *out++ = v; } return true; } static qboolean CODBSP_LoadEntities (model_t *mod, qbyte *mod_base, lump_t *l) { //just quake-style { "field" "value" "field2" "value2" } blocks. return modfuncs->LoadEntities(mod, mod_base+l->fileofs, l->filelen); } static qboolean CODBSP_LoadPlanes (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; vec4_t *in = (void*)((qbyte*)mod_base + l->fileofs); mplane_t *out; size_t j, i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadPlanes: funny lump size\n"); return false; } prv->planes = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); prv->num_planes = count; for (i = 0; i < count; i++, out++) { out->normal[0] = LittleFloat(in[i][0]); out->normal[1] = LittleFloat(in[i][1]); out->normal[2] = LittleFloat(in[i][2]); out->dist = LittleFloat(in[i][3]); out->type = PLANE_ANYZ; out->signbits = 0; for (j=0 ; j<3 ; j++) { if (out->normal[j] < 0) out->signbits |= 1<normal[j] == 1) out->type = j; } } return true; } static qboolean CODBSP_LoadBrushSides (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct codbsp_brushside_s *in = (void*)((qbyte*)mod_base + l->fileofs); size_t count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadBrushSides: funny lump size\n"); return false; } prv->brushsides = in; //used elsewhere in the loader prv->num_brushsides = count; return true; } static qboolean CODBSP_LoadBrushes (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; const struct { unsigned short sides; unsigned short material; } *in = (void*)((qbyte*)mod_base + l->fileofs); const struct codbsp_brushside_s *inside = prv->brushsides; q2cbrush_t *out; q2cbrushside_t *outside; mplane_t *aplane; size_t j, i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadBrushes: funny lump size\n"); return false; } prv->brushes = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); aplane = plugfuncs->GMalloc(&mod->memgroup, count*6*sizeof(*aplane)); //read the data for (i = 0, j = 0; i < count; i++, in++) { unsigned int mat = LittleShort(in->material); out[i].numsides = (unsigned short)LittleShort(in->sides); out[i].contents = prv->surfaces[mat].c.value; //is this right? seems to kinda work? feels wrong though. j += out[i].numsides; } //fix up the planes... outside = plugfuncs->GMalloc(&mod->memgroup, j*sizeof(*outside)); prv->num_brushes = count; for (i = 0, j = 0; i < count; i++, out++) { out->brushside = outside; for (j = 0; j < out->numsides; j++, inside++, outside++) { unsigned int mat = LittleLong(inside->material_idx); if (j < 6) { aplane->dist = LittleFloat(inside->dist); if (j&1) { //stored nx px ny py nz pz aplane->normal[j>>1] = 1; out->absmaxs[j>>1] = aplane->dist; } else { aplane->normal[j>>1] = -1; out->absmins[j>>1] = aplane->dist; aplane->dist *= -1; } outside->plane = aplane++; } else outside->plane = prv->planes + LittleLong(inside->plane); outside->surface = &prv->surfaces[mat]; } } return true; } /*static qboolean CODBSP_LoadLeafBrushes (model_t *mod, qbyte *mod_base, lump_t *l) { //we don't really care about this, as we're using our BIH stuff for collisions instead. // codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { int a; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); int highest=0; int lowest=0; if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadLeafBrushes: funny lump size\n"); return false; } for (i = 0; i < count; i++, in++) { if (lowest > in->a) lowest = in->a; if (highest < in->a) highest = in->a; // Con_Printf("%i: %i\n", (int)i, in->a); } Con_Printf("leaf brushes: %i - %i\n", lowest, highest); return true; }*/ static qboolean CODBSP_LoadPatchVertexes (model_t *mod, qbyte *mod_base, lump_t *l) { //for collision codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; const vec3_t *in = (void*)((qbyte*)mod_base + l->fileofs); vecV_t *out; size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadPatchVertexes: funny lump size\n"); return false; } prv->patchvertexes = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); prv->numpatchvertexes = count; for (i = 0; i < count; i++) { out[i][0] = LittleFloat(in[i][0]); out[i][1] = LittleFloat(in[i][1]); out[i][2] = LittleFloat(in[i][2]); } return true; } static qboolean CODBSP_LoadPatchIndexes (model_t *mod, qbyte *mod_base, lump_t *l) { //for collision codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; unsigned short *in = (void*)((qbyte*)mod_base + l->fileofs); index_t *out; size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadPatchIndexes: funny lump size\n"); return false; } prv->patchindexes = out = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*out)); prv->numpatchindexes = count; for (i = 0; i < count; i++) { *out++ = (unsigned short)LittleShort(*in++); } return true; } static qboolean CODBSP_LoadPatchCollision (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct codpatch_s *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadPatchesCollision: funny lump size\n"); return false; } prv->patches = in; prv->numpatches = count; for (i = 0; i < count; i++, in++) { if (in->mode==0) ;//Con_Printf("p%i: %s ?+%i*%-4i v%i+%i %i\n", (int)i, mod->textures[in->mat]->name, in->mode0.w, in->mode0.h, in->mode0.firstvert,in->mode0.w*in->mode0.h, in->mode0.unknown); else if (in->mode==1) ;//Con_Printf("s%i: %s %4i+%-4i v%i+%i\n", (int)i, mod->textures[in->mat]->name, in->mode1.firstidx,in->mode1.numidx, in->mode1.firstvert,in->mode1.numverts); else { Con_Printf("?%i: %s %i ?!?!?!?!?\n", (int)i, mod->textures[in->mat]->name, in->mode); return false; //nope. } } return true; } static qboolean CODBSP_LoadLeafPatches (model_t *mod, qbyte *mod_base, lump_t *l) { //for collision codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; unsigned int *in = (void*)((qbyte*)mod_base + l->fileofs); size_t count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadLeafPatches: funny lump size\n"); return false; } prv->leafpatches = in; prv->numleafpatches = count; return true; } /* static qboolean CODBSP_LoadAABBs (model_t *mod, qbyte *mod_base, lump_t *l) { // codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { int a; //surfaceindex (submodel0 only, so stops short of the full range implied by the lump's count) int b; //numsurfaces int c; //some sort of offset? } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadAABBs: funny lump size\n"); return false; } for (i = 0; i < count; i++, in++) { // Con_Printf("%i: %i+%i %i\n", (int)i, in->a, in->b, in->c); } return true; } static qboolean CODBSP_LoadCells (model_t *mod, qbyte *mod_base, lump_t *l) { // codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct { vec3_t mins; vec3_t maxs; int aabtree; //lump 16ish? CODLUMP_AABBTREES int firstportal; //lump 18? CODLUMP_PORTALS int numportals; int firstcullgroupindex; //lump 10? CODLUMP_CULLGROUPINDEXES int numcullgroupindexes; int firstoccluderindex; int numoccluders; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t i, count = l->filelen / sizeof(*in); if (l->filelen % sizeof(*in)) { Con_Printf (CON_ERROR "CODBSP_LoadCells: funny lump size\n"); return false; } for (i = 0; i < count; i++, in++) { Con_Printf("%i: [%f %f %f] [%f %f %f] %i %i+%i %i+%i %i+%i\n", (int)i, in->mins[0], in->mins[1], in->mins[2], in->maxs[0], in->maxs[1], in->maxs[2], in->aabtree, in->firstportal, in->numportals, in->firstcullgroupindex, in->numcullgroupindexes, in->firstoccluderindex, in->numoccluders); } return true; }*/ static qboolean CODBSP_LoadLeafs (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct codinlinemodel_s{ int cluster; //-1 for invalid int area; //-1 for invalid unsigned int firstleafsurfs; unsigned int numsurfaces; unsigned int firstleafbrushes; unsigned int numbrushes; int cell; //-1 for invalid unsigned int firstlightindex; unsigned int numlightindexes; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t count = l->filelen / sizeof(*in); size_t i; if (l->filelen % sizeof(*in) || count < 1) { Con_Printf (CON_ERROR "CODBSP_LoadLeafs: funny lump size\n"); return false; } prv->leaf = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*prv->leaf)); prv->numleafs = count; for (i = 0; i < count; i++, in++) { prv->leaf[i].cluster = LittleLong(in->cluster); prv->leaf[i].area = LittleLong(in->area); prv->leaf[i].firstlightindex = LittleLong(in->firstlightindex); prv->leaf[i].numlightindexes = LittleLong(in->numlightindexes); } return true; } static qboolean CODBSP_LoadNodes (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; struct codinlinemodel_s{ unsigned int plane; int child[2]; //negative for leaf ivec3_t mins; ivec3_t maxs; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t count = l->filelen / sizeof(*in); size_t i; if (l->filelen % sizeof(*in) || count < 1) { Con_Printf (CON_ERROR "CODBSP_LoadNodes: funny lump size\n"); return false; } prv->nodes = plugfuncs->GMalloc(&mod->memgroup, count*sizeof(*prv->nodes)); prv->numnodes = count; for (i = 0; i < count; i++, in++) { prv->nodes[i].plane = &prv->planes[LittleLong(in->plane)]; prv->nodes[i].childnum[0] = LittleLong(in->child[0]); prv->nodes[i].childnum[1] = LittleLong(in->child[1]); prv->nodes[i].mins[0] = LittleLong(in->mins[0]); prv->nodes[i].mins[1] = LittleLong(in->mins[1]); prv->nodes[i].mins[2] = LittleLong(in->mins[2]); prv->nodes[i].maxs[0] = LittleLong(in->maxs[0]); prv->nodes[i].maxs[1] = LittleLong(in->maxs[1]); prv->nodes[i].maxs[2] = LittleLong(in->maxs[2]); } return true; } static qboolean CODBSP_LoadVisibility (model_t *mod, qbyte *mod_base, lump_t *l) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; qbyte *in = (void*)((qbyte*)mod_base + l->fileofs); if (!l->filelen) { //unvised. mod->numclusters = 0; mod->pvsbytes = 0; prv->pvsdata = NULL; return true; } if (l->filelen < 8) { Con_Printf (CON_ERROR "CODBSP_LoadVisibility: funny lump size\n"); return false; } mod->numclusters = LittleLong(((int*)in)[0]); mod->pvsbytes = LittleLong(((int*)in)[1]); if (l->filelen != 8 + mod->numclusters*mod->pvsbytes) { Con_Printf (CON_ERROR "CODBSP_LoadVisibility: funny lump size\n"); return false; } prv->pvsdata = plugfuncs->GMalloc(&mod->memgroup, l->filelen-8); memcpy(prv->pvsdata, in+8, mod->numclusters*mod->pvsbytes); return true; } void AddPointToBounds (const vec3_t v, vec3_t mins, vec3_t maxs) { int i; vec_t val; for (i=0 ; i<3 ; i++) { val = v[i]; if (val < mins[i]) mins[i] = val; if (val > maxs[i]) maxs[i] = val; } } static void CODBSP_BuildBIH (model_t *mod, size_t firstbrush, size_t numbrushes, size_t firstleafpatch, size_t numleafpatches) { codbspinfo_t *prv = (codbspinfo_t*)mod->meshinfo; size_t numtriangles = 0; size_t numquads = 0; index_t *silly; struct bihleaf_s *bihleaf, *l; size_t i, j, lp; qbyte *patches = memset(alloca((prv->numpatches+7)>>3), 0, (prv->numpatches+7)>>3); for (i = firstleafpatch; i < numleafpatches; i++) { //de-dupe them... *sigh* lp = LittleLong(prv->leafpatches[i]); if (lp < prv->numpatches) patches[lp>>3] |= 1u<<(lp&7); } for (i = 0; i < prv->numpatches; i++) { if (patches[i>>3] & (1u<<(i&7))) { if (prv->patches[i].mode) numtriangles+=(unsigned short)LittleShort(prv->patches[i].mode1.numidx)/3; else numquads+=(unsigned int)((unsigned short)LittleShort(prv->patches[i].mode0.w)-1) * ((unsigned short)LittleShort(prv->patches[i].mode0.h)-1); } } bihleaf = l = plugfuncs->Malloc(sizeof(*bihleaf)*(numbrushes+numtriangles+numquads*2)); silly = plugfuncs->GMalloc(&mod->memgroup, sizeof(*silly)*6*numquads); //now we have enough storage, spit them out providing bounds info. for (i = 0; i < prv->numpatches; i++) { if (patches[i>>3] & (1u<<(i&7))) { if (prv->patches[i].mode) { unsigned int numidx = (unsigned short)LittleShort(prv->patches[i].mode1.numidx); for (j = 0; j < numidx; j+=3) { vec_t *v1,*v2,*v3; l->type = BIH_TRIANGLE; l->data.contents = prv->surfaces[LittleLong(prv->patches[i].mat)].c.value; l->data.tri.xyz = prv->patchvertexes + (unsigned int)LittleLong(prv->patches[i].mode1.firstvert); l->data.tri.indexes = prv->patchindexes + (unsigned int)LittleLong(prv->patches[i].mode1.firstidx) + j; v1 = l->data.tri.xyz[l->data.tri.indexes[0]]; v2 = l->data.tri.xyz[l->data.tri.indexes[1]]; v3 = l->data.tri.xyz[l->data.tri.indexes[2]]; VectorCopy(v1, l->mins); VectorCopy(v1, l->maxs); AddPointToBounds(v2, l->mins, l->maxs); AddPointToBounds(v3, l->mins, l->maxs); l++; } } else { unsigned int w = (unsigned short)LittleShort(prv->patches[i].mode0.w); unsigned int h = (unsigned short)LittleShort(prv->patches[i].mode0.h); unsigned int x, y; for (y = 0; y < h-1; y++) for (x = 0; x < w-1; x++) { const vec_t *v1,*v2,*v3; silly[0] = x+y*w; silly[1] = silly[0]+1; silly[2] = silly[0]+w; silly[3] = silly[1]; silly[4] = silly[1]+w; silly[5] = silly[2]; l->type = BIH_TRIANGLE; l->data.contents = FTECONTENTS_SOLID; //prv->surfaces[LittleLong(prv->patches[i].mat)].c.value; l->data.tri.xyz = prv->patchvertexes + (unsigned int)LittleLong(prv->patches[i].mode0.firstvert); l->data.tri.indexes = silly; v1 = l->data.tri.xyz[l->data.tri.indexes[0]]; v2 = l->data.tri.xyz[l->data.tri.indexes[1]]; v3 = l->data.tri.xyz[l->data.tri.indexes[2]]; VectorCopy(v1, l->mins); VectorCopy(v1, l->maxs); AddPointToBounds(v2, l->mins, l->maxs); AddPointToBounds(v3, l->mins, l->maxs); l++; silly+=3; l->type = BIH_TRIANGLE; l->data.contents = FTECONTENTS_SOLID; //prv->surfaces[LittleLong(prv->patches[i].mat)].c.value; l->data.tri.xyz = prv->patchvertexes + (unsigned int)LittleLong(prv->patches[i].mode0.firstvert); l->data.tri.indexes = silly; v1 = l->data.tri.xyz[l->data.tri.indexes[0]]; v2 = l->data.tri.xyz[l->data.tri.indexes[1]]; v3 = l->data.tri.xyz[l->data.tri.indexes[2]]; VectorCopy(v1, l->mins); VectorCopy(v1, l->maxs); AddPointToBounds(v2, l->mins, l->maxs); AddPointToBounds(v3, l->mins, l->maxs); l++; silly+=3; } } } } //now we have enough storage, spit them out providing bounds info. for (i = 0; i < numbrushes; i++) { q2cbrush_t *b = &prv->brushes[firstbrush+i]; l->type = BIH_BRUSH; l->data.brush = b; l->data.contents = b->contents; VectorCopy(b->absmins, l->mins); VectorCopy(b->absmaxs, l->maxs); l++; } modfuncs->BIH_Build(mod, bihleaf, l-bihleaf); plugfuncs->Free(bihleaf); } static qboolean CODBSP_LoadInlineModels (model_t *wmod, qbyte *mod_base, lump_t *l) { // codbspinfo_t *prv = (codbspinfo_t*)wmod->meshinfo; struct codinlinemodel_s{ vec3_t mins; vec3_t maxs; unsigned int firstsurf; unsigned int numsurfs; unsigned int firstleafpatch; //seems to match lump 23, unsigned int numleafpatches; unsigned int firstbrush; unsigned int numbrushes; } *in = (void*)((qbyte*)mod_base + l->fileofs); size_t count = l->filelen / sizeof(*in); size_t i, j; if (l->filelen % sizeof(*in) || count < 1) { Con_Printf (CON_ERROR "CODBSP_LoadInlineModels: funny lump size\n"); return false; } for (i = 0; i < count; i++, in++) { char name[MAX_QPATH]; model_t *mod; if (i) { //submodels Q_snprintfz (name, sizeof(name), "*%u:%s", (unsigned int)i, wmod->publicname); mod = modfuncs->BeginSubmodelLoad(name); *mod = *wmod; mod->archive = NULL; mod->entities_raw = NULL; mod->submodelof = wmod; Q_strlcpy(mod->publicname, name, sizeof(mod->publicname)); Q_snprintfz (mod->name, sizeof(mod->name), "*%u:%s", (unsigned int)i, wmod->name); memset(&mod->memgroup, 0, sizeof(mod->memgroup)); } else //handle the world model here too mod = wmod; mod->hulls[0].firstclipnode = i?-1:0; for (j = 1; j < countof(mod->hulls); j++) mod->hulls[j].firstclipnode = -1; //no nodes, mod->nodes = mod->rootnode = NULL; mod->leafs = NULL; mod->nummodelsurfaces = LittleLong(in->numsurfs); mod->firstmodelsurface = LittleLong(in->firstsurf); VectorCopy(in->mins, mod->mins); VectorCopy(in->maxs, mod->maxs); CODBSP_BuildBIH(mod, LittleLong(in->firstbrush), LittleLong(in->numbrushes), LittleLong(in->firstleafpatch), LittleLong(in->numleafpatches)); memset(&mod->batches, 0, sizeof(mod->batches)); mod->vbos = NULL; #ifdef HAVE_CLIENT // mod->radius = RadiusFromBounds (mod->mins, mod->maxs); // if (qrenderer != QR_NONE) { builddata_t *bd = plugfuncs->Malloc(sizeof(*bd)); bd->buildfunc = CODBSP_BuildSurfMesh; bd->paintlightmaps = false; //q3like with prebaked lightmaps. threadfuncs->AddWork(WG_MAIN, CODBSP_GenerateMaterials, mod, bd, 0, 0); } #endif if (mod != wmod) modfuncs->EndSubmodelLoad(mod, MLS_LOADED); } return true; } static qboolean QDECL Mod_LoadCodBSP(struct model_s *mod, void *buffer, size_t fsize) { codbspinfo_t *prv; qboolean okay = true; int i; int ver = LittleLong(((int*)buffer)[1]); lump_t lumps[max((int)COD1LUMP_COUNT, (int)COD2LUMP_COUNT)]; mod->fromgame = fg_new; #ifdef HAVE_SERVER mod->funcs.FatPVS = CODBSP_FatPVS; mod->funcs.EdictInFatPVS = CODBSP_EdictInFatPVS; mod->funcs.FindTouchedLeafs = CODBSP_FindTouchedLeafs; #endif #ifdef HAVE_CLIENT mod->funcs.LightPointValues = CODBSP_LightPointValues; // mod->funcs.StainNode = CODBSP_StainNode; // mod->funcs.MarkLights = CODBSP_MarkLights; // mod->funcs.GenerateShadowMesh = CODBSP_GenerateShadowMesh; #endif mod->funcs.ClusterPVS = CODBSP_ClusterPVS; // mod->funcs.ClusterPHS = CODBSP_ClusterPHS; mod->funcs.ClusterForPoint = CODBSP_ClusterForPoint; // mod->funcs.SetAreaPortalState = CODBSP_SetAreaPortalState; // mod->funcs.AreasConnected = CODBSP_AreasConnected; // mod->funcs.LoadAreaPortalBlob = CODBSP_LoadAreaPortalBlob; // mod->funcs.SaveAreaPortalBlob = CODBSP_SaveAreaPortalBlob; mod->funcs.PrepareFrame = CODBSP_PrepareFrame; mod->funcs.InfoForPoint = CODBSP_InfoForPoint; if (ver == COD1BSP_VERSION) { memcpy(lumps, (char*)buffer+8, sizeof(*lumps)*COD1LUMP_COUNT); for (i = 0; i < COD1LUMP_COUNT; i++) { int ffs = lumps[i].filelen; //ffs lumps[i].filelen = LittleLong(lumps[i].fileofs); lumps[i].fileofs = LittleLong(ffs); if (lumps[i].filelen && lumps[i].fileofs+(size_t)lumps[i].filelen > fsize) { Con_Printf(CON_ERROR"Truncated BSP file\n"); return false; } } mod->meshinfo = prv = plugfuncs->GMalloc(&mod->memgroup, sizeof(codbspinfo_t)); prv->codbspver = ver; //basic trisoup info okay = okay && CODBSP_LoadShaders(mod, buffer, &lumps[COD1LUMP_MATERIALS]); okay = okay && COD1BSP_LoadLightmap(mod, buffer, &lumps[COD1LUMP_LIGHTMAPS]); okay = okay && COD1BSP_LoadSoupVertices(mod, buffer, &lumps[COD1LUMP_SOUPVERTS]); okay = okay && CODBSP_LoadSoupIndexes(mod, buffer, &lumps[COD1LUMP_SOUPINDEXES]); okay = okay && CODBSP_LoadSoups(mod, buffer, &lumps[COD1LUMP_SOUPS]); //gamecode needs to know what's around okay = okay && CODBSP_LoadLights(mod, buffer, &lumps[COD1LUMP_LIGHTS]); okay = okay && CODBSP_LoadLightIndexes(mod, buffer, &lumps[COD1LUMP_LIGHTINDEXES]); okay = okay && CODBSP_LoadEntities(mod, buffer, &lumps[COD1LUMP_ENTITIES]); //basic collision okay = okay && CODBSP_LoadPlanes(mod, buffer, &lumps[COD1LUMP_PLANES]); okay = okay && CODBSP_LoadBrushSides(mod, buffer, &lumps[COD1LUMP_BRUSHSIDES]); okay = okay && CODBSP_LoadBrushes(mod, buffer, &lumps[COD1LUMP_BRUSHES]); //okay = okay && CODBSP_LoadLeafBrushes(mod, buffer, &lumps[COD1LUMP_LEAFBRUSHES]); //patch collision... okay = okay && CODBSP_LoadPatchVertexes(mod, buffer, &lumps[COD1LUMP_COLLISIONVERTS]); okay = okay && CODBSP_LoadPatchIndexes(mod, buffer, &lumps[COD1LUMP_COLLISIONINDEXES]); okay = okay && CODBSP_LoadPatchCollision(mod, buffer, &lumps[COD1LUMP_PATCHCOLLISION]); okay = okay && CODBSP_LoadLeafPatches(mod, buffer, &lumps[COD1LUMP_LEAFPATCHES]); //seems like cod is a portal engine? //okay = okay && CODBSP_LoadAABBs(mod, buffer, &lumps[COD1LUMP_AABBTREES]); //okay = okay && CODBSP_LoadCells(mod, buffer, &lumps[COD1LUMP_CELLS]); //but we still have pvs with its clusters+areas that are node based (also required to determine which 'cell' we're inside). okay = okay && CODBSP_LoadVisibility(mod, buffer, &lumps[COD1LUMP_VISIBILITY]); okay = okay && CODBSP_LoadLeafs(mod, buffer, &lumps[COD1LUMP_LEAFS]); okay = okay && CODBSP_LoadNodes(mod, buffer, &lumps[COD1LUMP_NODES]); okay = okay && CODBSP_LoadInlineModels(mod, buffer, &lumps[COD1LUMP_MODELS]); } else if (ver == COD2BSP_VERSION) { memcpy(lumps, (char*)buffer+8, sizeof(*lumps)*COD2LUMP_COUNT); for (i = 0; i < COD2LUMP_COUNT; i++) { int ffs = lumps[i].filelen; //ffs lumps[i].filelen = LittleLong(lumps[i].fileofs); lumps[i].fileofs = LittleLong(ffs); if (lumps[i].filelen && lumps[i].fileofs+(size_t)lumps[i].filelen > fsize) { Con_Printf(CON_ERROR"Truncated BSP file\n"); return false; } } mod->meshinfo = prv = plugfuncs->GMalloc(&mod->memgroup, sizeof(codbspinfo_t)); prv->codbspver = ver; //basic trisoup info okay = okay && CODBSP_LoadShaders(mod, buffer, &lumps[COD2LUMP_MATERIALS]); okay = okay && COD2BSP_LoadLightmap(mod, buffer, &lumps[COD2LUMP_LIGHTMAPS]); okay = okay && COD2BSP_LoadSoupVertices(mod, buffer, &lumps[COD2LUMP_SOUPVERTS]); okay = okay && CODBSP_LoadSoupIndexes(mod, buffer, &lumps[COD2LUMP_SOUPINDEXES]); okay = okay && CODBSP_LoadSoups(mod, buffer, &lumps[COD2LUMP_SOUPS]); //gamecode needs to know what's around okay = okay && CODBSP_LoadEntities(mod, buffer, &lumps[COD2LUMP_ENTITIES]); //basic collision okay = okay && CODBSP_LoadPlanes(mod, buffer, &lumps[COD2LUMP_PLANES]); okay = okay && CODBSP_LoadBrushSides(mod, buffer, &lumps[COD2LUMP_BRUSHSIDES]); okay = okay && CODBSP_LoadBrushes(mod, buffer, &lumps[COD2LUMP_BRUSHES]); //okay = okay && CODBSP_LoadLeafBrushes(mod, buffer, &lumps[COD2LUMP_LEAFBRUSHES]); //patch collision... // okay = okay && CODBSP_LoadPatchVertexes(mod, buffer, &lumps[COD1LUMP_COLLISIONVERTS]); // okay = okay && CODBSP_LoadPatchIndexes(mod, buffer, &lumps[COD1LUMP_COLLISIONINDEXES]); // okay = okay && CODBSP_LoadPatchCollision(mod, buffer, &lumps[COD2LUMP_PATCHCOLLISION]); // okay = okay && CODBSP_LoadLeafPatches(mod, buffer, &lumps[COD2LUMP_LEAFPATCHES]); //seems like cod is a portal engine? //okay = okay && CODBSP_LoadAABBs(mod, buffer, &lumps[COD2LUMP_AABBTREES]); //okay = okay && CODBSP_LoadCells(mod, buffer, &lumps[COD2LUMP_CELLS]); //but we still have pvs with its clusters+areas that are node based (also required to determine which 'cell' we're inside). okay = okay && CODBSP_LoadVisibility(mod, buffer, &lumps[COD2LUMP_VISIBILITY]); okay = okay && CODBSP_LoadLeafs(mod, buffer, &lumps[COD2LUMP_LEAFS]); okay = okay && CODBSP_LoadNodes(mod, buffer, &lumps[COD2LUMP_NODES]); okay = okay && CODBSP_LoadInlineModels(mod, buffer, &lumps[COD2LUMP_MODELS]); } else { Con_Printf(CON_ERROR"Bad COD Version...\n"); //should have already been checked, so this ain't possible. okay = false; } return okay; } qboolean CODBSP_Init(void) { filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs)); modfuncs = plugfuncs->GetEngineInterface(plugmodfuncs_name, sizeof(*modfuncs)); threadfuncs = plugfuncs->GetEngineInterface(plugthreadfuncs_name, sizeof(*threadfuncs)); if (modfuncs && modfuncs->version != MODPLUGFUNCS_VERSION) modfuncs = NULL; if (modfuncs && filefuncs && threadfuncs) { modfuncs->RegisterModelFormatMagic("CoD(1) Maps", "IBSP\x3b\0\0\0",8, Mod_LoadCodBSP); modfuncs->RegisterModelFormatMagic("CoD2 Maps", "IBSP\x04\0\0\0",8, Mod_LoadCodBSP); return true; } return false; } ================================================ FILE: plugins/cod/codiwi.c ================================================ #include "../plugin.h" static plugimagefuncs_t *imagefuncs; static struct pendingtextureinfo *Image_ReadIWIFile(unsigned int imgflags, const char *fname, qbyte *filedata, size_t filesize) { if (filesize >= 12 && filedata[0]=='I'&&filedata[1]=='W'&&filedata[2]=='i' && (filedata[3]==5/*cod2*/||filedata[3]==6/*cod4*/)) { static const enum uploadfmt fmts[] = {PTI_INVALID/*0*/, PTI_RGBA8/*1*/, PTI_RGB8/*2*/, PTI_L8A8/*3, VALIDATE!*/, PTI_INVALID/*4,PTI_A8*/, PTI_INVALID/*5*/,PTI_INVALID/*6*/,PTI_INVALID/*7*/,PTI_INVALID/*8*/,PTI_INVALID/*9*/,PTI_INVALID/*10*/,PTI_BC1_RGBA/*11*/, PTI_BC2_RGBA/*12*/, PTI_BC3_RGBA/*13*/}; enum uploadfmt fmt = PTI_INVALID; unsigned int bb,bw,bh,bd; unsigned int iw,ih,id, l; struct pendingtextureinfo *mips; unsigned int offsets[4]; qbyte *end = filedata+filesize; enum { IWI_STANDARD=0, IWI_MIPLESS=3, IWI_CUBEMAP=6, // IWI_NORMALMAP=0x20, // IWI_UNKNOWN=0x40, // IWI_UNKNOWN=0x80, } usage = filedata[5]; if (filedata[4] < countof(fmts)) fmt = fmts[filedata[4]]; if (fmt == PTI_INVALID) { //bail. with warning Con_Printf(CON_WARNING"Image_ReadIWIFile(%s): unsupported iwi pixelformat %x\n", fname, filedata[4]); return NULL; } iw = filedata[6] | (filedata[7]<<8); ih = filedata[8] | (filedata[9]<<8); id = filedata[10] | (filedata[11]<<8); imagefuncs->BlockSizeForEncoding(fmt, &bb, &bw, &bh, &bd); if (!bb) { Con_Printf(CON_WARNING"Image_ReadIWIFile(%s): unsupported fte pixelformat %x(%i)\n", fname, filedata[4], fmt); return NULL; } mips = plugfuncs->Malloc(sizeof(*mips)); mips->encoding = fmt; if ((filedata[5]&0xf) == IWI_CUBEMAP && id==1) { mips->type = PTI_CUBE; id *= 6; } else mips->type = (id>1)?PTI_3D:PTI_2D; mips->extrafree = filedata; filedata += 12; for (l = 0; l < countof(offsets); l++, filedata+=4) offsets[l] = filedata[0] | (filedata[1]<<8) | (filedata[2]<<16) | (filedata[3]<<24); //dunno what this 4 values are for. looks like descending ends? if (mips->type != PTI_2D || (usage&0xf)==IWI_MIPLESS) mips->mipcount = l = 1; else for (l = 0; l < countof(mips->mip); l++) { if ((iw >> l) || (ih >> l) || (id >> l)) mips->mipcount++; else break; } //these are smallest to biggest. while (l --> 0) { unsigned int w = iw>>l; unsigned int h = ih>>l; unsigned int d = (mips->type==PTI_3D)?id>>l:id; size_t datasize; if (!w && !h && ((mips->type==PTI_3D)?!d:true)) break; if (!w) w = 1; if (!h) h = 1; if (!d) d = 1; datasize = ((w+bw-1)/bw) * ((h+bh-1)/bh) * ((d+bd-1)/bd) * bb; if (filedata + datasize > end) { Con_Printf(CON_WARNING"%s: truncated\n", fname); Con_Printf("%s: %#x\n", fname, usage); plugfuncs->Free(mips); return NULL; //doesn't fit... } mips->mip[l].width = w; mips->mip[l].height = h; mips->mip[l].depth = d; mips->mip[l].data = filedata; mips->mip[l].datasize = datasize; mips->mip[l].needfree = false; filedata += datasize; } if (filedata != end) { Con_Printf(CON_WARNING"%s: trailing data\n", fname); Con_Printf("%s: %#x\n", fname, usage); plugfuncs->Free(mips); return NULL; //doesn't fit... } return mips; } return NULL; } static plugimageloaderfuncs_t iwifuncs = { "InfinityWard Image", sizeof(struct pendingtextureinfo), false, Image_ReadIWIFile, }; qboolean IWI_Init(void) { imagefuncs = plugfuncs->GetEngineInterface(plugimagefuncs_name, sizeof(*imagefuncs)); if (!imagefuncs) return false; return plugfuncs->ExportInterface(plugimageloaderfuncs_name, &iwifuncs, sizeof(iwifuncs)); } ================================================ FILE: plugins/cod/codmat.c ================================================ #include "../plugin.h" #include "shader.h" typedef struct shaderparsestate_s parsestate_t; static plugfsfuncs_t *fsfuncs; static qboolean COD2_DecodeMaterial(parsestate_t *ps, const char *filename, void (*LoadMaterialString)(parsestate_t *ps, const char *script)) { size_t sz; qbyte *base = fsfuncs->LoadFile(va("materials/%s", filename), &sz); struct { unsigned int ofs_materialname; //usually matches filename. dunno why this needs to be here. for aliases? does the qbsp read this instead? unsigned int ofs_texturename; //simplification for tools? dunno. unsigned int z1; qbyte unk10; qbyte sort; //sort: //0x00: distortion //0x01: opaquewater //0x02: boathull //0x03: opaque //0x04: sky //0x04: skybox_sunmoon //0x06: skybox_clouds //0x08: decal_bottom1 //0x09: decal_bottom2 //0x0a: decal_bottom3 //0x0b: decal_world //0x0c: decal_middle1 //0x0d: decal_middle2 //0x0e: decal_middle3 //0x0f: decal_gunimpact //0x10: decal_top1 //0x11: decal_top2 //0x12: decal_top3 //0x12: decal_multiplicative //0x14: banner //0x15: hair //0x16: underwater //0x17: transparentwater //0x18: corona //0x19: windowinside //0x1a: windowoutside //0x1b: blend //0x1c: viewmodel qbyte unk12; qbyte unk13; unsigned int z2; unsigned short unk2[2]; unsigned int unk3; unsigned short width, height; unsigned int z3; unsigned int surfaceflags; //lower bits seem like they might be wrong. upper bits are consistentish with cod1 surfaceflags though. unsigned int contentbits; //pure guess. probably wrong. unsigned int blendbits; //0x00ff00ff bits are src/dst color/alpha blendfuncs matching the D3DBLEND enum. unsigned int unk7; unsigned int unk8; unsigned int ofs_program; unsigned int ofs_table; //0x44 unsigned int ofs_table_end; //regarding the various unknowns, we can expect editor locale+usage (which are useless to us), blendfunc+alphafunc++cullface+depthtest+depthwrite+polygonoffset, maybe some tessellation settings and envmapping stuff. struct { unsigned int ofs_sampler; unsigned int flags; //maybe? 0x200 for rgb, 0x300 for normals, 0x400 for spec...? there's probably clamping options here. //&7==0: default //&7==1: nearest... or trilinear?!? //&7==2: linear //&7==3: ??? //&7==4: ??? //&7==5: ??? //&7==6: bilinear //&7==7: anisotropic unsigned int ofs_texname; } maps[1]; } *header = (void*)base; unsigned int m; if (!base) return false; //nope, bad filename if (header->ofs_table == 0x44) //make sure we know what it is... { size_t ofs = 0; char shad[8192]; const char *srcfac, *dstfac; //no initial { cos we're weird. Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\n"),ofs+=strlen(shad+ofs); //not actually useful to us. maybe for the hud so it doesn't have to wait for the image data too? Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t//material='%s'\n\t//tooltex='%s'\n", base+header->ofs_materialname, base+header->ofs_texturename),ofs+=strlen(shad+ofs); Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t//z1=%#x z2=%#x z3=%#x\n\t//unk1=%#x,%#x,%#x,%#x\n\t//unk2=%#x,%#x\n\t//unk3=%#x\n\t//surfaceflags=%#x\n\t//contentbits=%#x\n\t//unk6=%#x\n\t//unk7=%#x\n\t//unk8=%#x\n", header->z1, header->z2, header->z3, header->unk10,header->sort,header->unk12,header->unk13, header->unk2[0],header->unk2[1], header->unk3, header->surfaceflags, header->contentbits, header->blendbits, header->unk7, header->unk8),ofs+=strlen(shad+ofs); Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\timagesize %i %i\n", header->width, header->height),ofs+=strlen(shad+ofs); //not actually useful to us. maybe for the hud so it doesn't have to wait for the image data too? for (m = 0; m < (header->ofs_table_end-header->ofs_table)/sizeof(header->maps[0]); m++) { const char *sampler = base+header->maps[m].ofs_sampler; if (!strcmp(sampler, "colorMap")) Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\tdiffusemap images/%s.iwi //%#x\n", base+header->maps[m].ofs_texname, header->maps[m].flags),ofs+=strlen(shad+ofs); else if (!strcmp(sampler, "normalMap")) Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\tnormalmap images/%s.iwi //%#x\n", base+header->maps[m].ofs_texname, header->maps[m].flags),ofs+=strlen(shad+ofs); else if (!strcmp(sampler, "detailMap")) Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\tdisplacementmap images/%s.iwi //%#x\n", base+header->maps[m].ofs_texname, header->maps[m].flags),ofs+=strlen(shad+ofs); //hack. might as well use this one as detail. else if (!strcmp(sampler, "specularMap")) Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\tspecularmap images/%s.iwi //%#x\n", base+header->maps[m].ofs_texname, header->maps[m].flags),ofs+=strlen(shad+ofs); else Con_Printf("\t%s:%#x<-%s\n", base+header->maps[m].ofs_sampler, header->maps[m].flags, base+header->maps[m].ofs_texname); } //spit out a pass... Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t{\n"),ofs+=strlen(shad+ofs); Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t\tprogram %s\n", base+header->ofs_program),ofs+=strlen(shad+ofs); Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t\tmap $diffuse\n"),ofs+=strlen(shad+ofs); //just in case. switch((header->blendbits>>0)&0xf) { //source factors default: case 1: srcfac="zero"; break; case 2: srcfac="one"; break; case 3: srcfac="src_color"; break; case 4: srcfac="one_minus_src_color"; break; case 5: srcfac="src_alpha"; break; case 6: srcfac="one_minus_src_alpha"; break; case 7: srcfac="dst_alpha"; break; case 8: srcfac="one_minus_dst_alpha"; break; case 9: srcfac="dst_color"; break; case 10: srcfac="one_minus_dst_color"; break; case 11: srcfac="src_alpha_sat"; break; case 12: srcfac="both_src_alpha"; break; case 13: srcfac="both_one_minus_src_alpha"; break; case 14: srcfac="blend_factor"; break; case 15: srcfac="one_minus_blend_factor"; break; } switch((header->blendbits>>4)&0xf) { //dest factors default: case 1: dstfac="zero"; break; case 2: dstfac="one"; break; case 3: dstfac="src_color"; break; case 4: dstfac="one_minus_src_color"; break; case 5: dstfac="src_alpha"; break; case 6: dstfac="one_minus_src_alpha"; break; case 7: dstfac="dst_alpha"; break; case 8: dstfac="one_minus_dst_alpha"; break; case 9: dstfac="dst_color"; break; case 10: dstfac="one_minus_dst_color"; break; case 11: dstfac="src_alpha_sat"; break; case 12: dstfac="both_src_alpha"; break; case 13: dstfac="both_one_minus_src_alpha"; break; case 14: dstfac="blend_factor"; break; case 15: dstfac="one_minus_blend_factor"; break; } Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t\tblendfunc %s %s\n", srcfac, dstfac),ofs+=strlen(shad+ofs); //just in case. Q_snprintf(shad+ofs,sizeof(shad)-ofs, "\t}\n"),ofs+=strlen(shad+ofs); Q_snprintf(shad+ofs,sizeof(shad)-ofs, "}\n"); plugfuncs->Free(base); LoadMaterialString(ps, shad); return true; } else Con_Printf(CON_WARNING"%s doesn't seem to be a material? table=%#x..%#x\n", filename, header->ofs_table, header->ofs_table_end); plugfuncs->Free(base); return false; } static qboolean SType_LoadShader(parsestate_t *ps, const char *filename, void (*LoadMaterialString)(parsestate_t *ps, const char *script)) { char stypefname[MAX_QPATH]; char *path; const char *sep; const char *at; size_t pre; char *base, *in, *out; sep = strrchr(filename, '/'); if (!sep++) return COD2_DecodeMaterial(ps, filename, LoadMaterialString); at = strrchr(sep, '@'); if (!at) return false; //nope, no shadertype specified. depend on the engine/loader/etc defaults. if (!strncmp(filename, "skins/", 5)) path = "shadertypes/model/"; else if (!strncmp(filename, "textures/", 9)) path = "shadertypes/world/"; else if (!strncmp(filename, "gfx/", 4)) path = "shadertypes/2d/"; else return false; //nope, not gonna try. pre = strlen(path); if (pre+(at-sep)+7 > sizeof(stypefname)) return false; //nope, too long... memcpy(stypefname, path, pre); memcpy(stypefname+pre, sep, at-sep); memcpy(stypefname+pre+(at-sep), ".stype", 7); in = out = base = fsfuncs->LoadFile(stypefname, &pre); if (!base) return false; //nope, bad filename //yay we got something, but we need to fix up the $texturename strings to $diffuse because $reasons while (*in) { if (*in == '$' && !strncmp(in, "$texturename", 12)) { memcpy(out, "$diffuse", 8); out += 8; in += 12; } else *out++ = *in++; } *out = 0; //now hand over the fteised shader script. in = base; while(*in == ' ' || *in == '\t' || *in == '\n' || *in == '\r') in++; if (*in == '{') in++; LoadMaterialString(ps, in); plugfuncs->Free(base); //done with it now. return true; } /*static struct sbuiltin_s codprograms[] = { {QR_NONE} };*/ static plugmaterialloaderfuncs_t stypefuncs = { "Cod Shader Types", SType_LoadShader, // codprograms, }; qboolean STypes_Init(void) { fsfuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*fsfuncs)); if (!fsfuncs) return false; return plugfuncs->ExportInterface(plugmaterialloaderfuncs_name, &stypefuncs, sizeof(stypefuncs)); } ================================================ FILE: plugins/cod/codmod.c ================================================ #include "../plugin.h" #include "../engine/common/com_mesh.h" static plugfsfuncs_t *filefuncs; static plugmodfuncs_t *modfuncs; //Utility functions. silly plugins. float Length(const vec3_t v) {return sqrt(DotProduct(v,v));} float RadiusFromBounds (const vec3_t mins, const vec3_t maxs) { int i; vec3_t corner; for (i=0 ; i<3 ; i++) corner[i] = fabs(mins[i]) > fabs(maxs[i]) ? fabs(mins[i]) : fabs(maxs[i]); return Length(corner); } struct fstream_s { void *start; size_t len; size_t ofs; size_t numbones; galiasbone_t *bones; float *basepose; }; struct lod_s { float coverage; const char *name; unsigned short numtex; const char *tex[64]; }; static qboolean ReadEOF(struct fstream_s *f) { return (f->ofs >= f->len); } static qbyte ReadByte(struct fstream_s *f) { if (f->ofs+1 > f->len) { f->ofs++; return 0; } return ((qbyte*)f->start)[f->ofs++]; } static const char *ReadBytes(struct fstream_s *f, size_t len) { f->ofs+=len; if (f->ofs > f->len) return NULL; return &((const char*)f->start)[f->ofs-len]; } static const char *ReadString(struct fstream_s *f) { size_t len; for (len = 0; f->ofs+len < f->len && ((qbyte*)f->start)[f->ofs+len]; len++) ; len++; //for the null f->ofs += len; return &((const qbyte*)f->start)[f->ofs-len]; } static unsigned short ReadUInt16(struct fstream_s *f) { unsigned short r; r = ReadByte(f); r|= ReadByte(f) << 8; return r; } static short ReadSInt16(struct fstream_s *f) { return (signed short)ReadUInt16(f); } static unsigned int ReadUInt32(struct fstream_s *f) { unsigned int r; r = ReadByte(f); r|= ReadByte(f) << 8; r|= ReadByte(f) << 16; r|= ReadByte(f) << 24; return r; } static float ReadFloat(struct fstream_s *f) { union { unsigned int u; float f; } r; r.u = ReadByte(f); r.u|= ReadByte(f) << 8; r.u|= ReadByte(f) << 16; r.u|= ReadByte(f) << 24; return r.f; } static qboolean Mod_XModel_LoadPart (struct model_s *mod, struct fstream_s *f) { unsigned short ver = ReadUInt16(f); unsigned short b, nboner = ReadUInt16(f); unsigned short nbonea = ReadUInt16(f); float rel[12]; switch(ver) { case 0x0e: break; case 0x14: break; default: Con_Printf(CON_ERROR"%s: Unknown version %#x\n", mod->name, ver); return false; } // Con_Printf("%s: version %x rb:%i ab:%i\n", mod->name, ver, nboner, nbonea); f->numbones = nbonea+nboner; f->basepose = plugfuncs->GMalloc(&mod->memgroup, sizeof(*f->basepose)*12 * f->numbones); f->bones = plugfuncs->GMalloc(&mod->memgroup, sizeof(*f->bones) * f->numbones); for (b = 0; b < nbonea; b++) { //root bones, with identity position. for some reason. f->bones[b].parent = -1; VectorClear(f->bones[b].ref.org); Vector4Set(f->bones[b].ref.quat, 0, 0, 0, 1); VectorSet(f->bones[b].ref.scale, 1, 1, 1); modfuncs->GenMatrixPosQuat4Scale(f->bones[b].ref.org, f->bones[b].ref.quat, f->bones[b].ref.scale, f->basepose + b*12); } for (; b < f->numbones; b++) { f->bones[b].parent = ReadByte(f); if (f->bones[b].parent >= b) Con_Printf(CON_ERROR"b%i (%s) has parent %i\n", b, f->bones[b].name, f->bones[b].parent); f->bones[b].ref.org[0] = ReadFloat(f); f->bones[b].ref.org[1] = ReadFloat(f); f->bones[b].ref.org[2] = ReadFloat(f); f->bones[b].ref.quat[0] = ReadSInt16(f)/32767.0f; f->bones[b].ref.quat[1] = ReadSInt16(f)/32767.0f; f->bones[b].ref.quat[2] = ReadSInt16(f)/32767.0f; f->bones[b].ref.quat[3] = 1.0-DotProduct(f->bones[b].ref.quat,f->bones[b].ref.quat); //reconstruct the w part. if (f->bones[b].ref.quat[3]>0) f->bones[b].ref.quat[3] = sqrt(f->bones[b].ref.quat[3]); else f->bones[b].ref.quat[3] = 0; VectorSet(f->bones[b].ref.scale, 1, 1, 1); modfuncs->GenMatrixPosQuat4Scale(f->bones[b].ref.org, f->bones[b].ref.quat, f->bones[b].ref.scale, rel); modfuncs->ConcatTransforms((void*)(f->basepose + f->bones[b].parent*12), (void*)rel, (void*)(f->basepose + b*12)); // Con_Printf("b%i: p:%i, [%f %f %f] [%f %f %f %f]\n", b, f->bones[b].parent, pos[0],pos[1],pos[2], quat[0],quat[1],quat[2],quat[3]); } for (b = 0; b < f->numbones; b++) { vec3_t mins, maxs; const char *n = ReadString(f); if (ver >= 0x14) { //omitted. VectorClear(mins); VectorClear(maxs); } else { mins[0] = ReadFloat(f); mins[1] = ReadFloat(f); mins[2] = ReadFloat(f); maxs[0] = ReadFloat(f); maxs[1] = ReadFloat(f); maxs[2] = ReadFloat(f); } //hack... I assume the (game)code does a skel_setbone so this doesn't actually matter? if (!strcmp(n, "torso_stabilizer")) Vector4Set(f->bones[b].ref.quat, 0,0,0,1); Q_strlcpy(f->bones[b].name, n, sizeof(f->bones[b].name)); modfuncs->M3x4_Invert(f->basepose+12*b, f->bones[b].inverse); // if (ver >= 0x14) // Con_Printf("b%i: %-20s\n", b, n); // else // Con_Printf("b%i: %-20s [%f %f %f] [%f %f %f]\n", b, n, mins[0], mins[1], mins[2], maxs[0], maxs[1], maxs[2]); } for (b = 0; b < f->numbones; b++) f->bones[b].group = ReadByte(f); //presumed. return true; } static qboolean Mod_XModel_LoadSurfs (struct model_s *mod, struct fstream_s *f, struct lod_s *lod, float mincov) { galiasinfo_t *surf; galiasskin_t *skins; skinframe_t *skinframe; unsigned short ver = ReadUInt16(f); unsigned short n, nsurfs = ReadUInt16(f); switch(ver) { case 0x0e: break; case 0x14: break; default: Con_Printf("%s: Unknown version %#x\n", mod->name, ver); return false; } if (!nsurfs) return true; //nothing to do... surf = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf)*nsurfs + sizeof(*skins)*nsurfs + sizeof(*skinframe)*nsurfs); skins = (galiasskin_t*)(surf+nsurfs); skinframe = (skinframe_t*)(skins+nsurfs); for (n = 0; n < nsurfs; n++) { surf[n].numbones = f->numbones; surf[n].ofsbones = f->bones; surf[n].baseframeofs = f->basepose; surf[n].mindist = mincov; surf[n].maxdist = lod->coverage; surf[n].shares_verts = n; surf[n].shares_bones = 0; surf[n].nextsurf = &surf[n+1]; surf[n].ofsskins = &skins[n]; surf[n].numskins = 1; skins[n].skinspeed = 0.1; skins[n].frame = &skinframe[n]; skins[n].numframes = 1; Q_snprintf(surf[n].surfacename, sizeof(surf[n].surfacename), "%s/%i", lod->name, n); Q_strlcpy(skins[n].name, lod->name, sizeof(skins[n].name)); if (n < lod->numtex) Q_snprintf(skinframe[n].shadername, sizeof(skinframe[n].shadername), (ver==0x0e)?"skins/%s":"%s", lod->tex[n]); } surf[nsurfs-1].nextsurf = mod->meshinfo; mod->meshinfo = surf; for(surf = surf[nsurfs-1].nextsurf; surf; surf = surf->nextsurf) surf->shares_verts += nsurfs; surf = mod->meshinfo; for (n = 0; n < nsurfs; n++, surf++) { int flags = ReadByte(f); int v, nverts = ReadUInt16(f); int t, ntris = ReadUInt16(f); int r, runs = (ver==0xe)?ReadUInt16(f):0; unsigned short wcount = 0; qbyte run; unsigned int idx1, idx2, idx3; unsigned short boneidx = ReadUInt16(f); qboolean boney = boneidx == (unsigned short)~0u; int exweights = 0; unsigned short *vw = NULL; // unsigned short bunk2; vec3_t norm; vec4_t xyzw; const float *matrix; (void)flags; if (boney) { if (ver == 0x0e) exweights = ReadUInt16(f); /*bunk2 =*/ ReadUInt16(f); //seems to be vaguely related to vert/triangle counts. } surf->numverts = nverts; if (ver == 0xe) { //cod1 surf->ofs_indexes = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_indexes ) * ntris*3); for(t = 0, r = 0; r < runs; r++) { run = ReadByte(f); idx1 = ReadUInt16(f); idx2 = ReadUInt16(f); run-=2; for (;;) { //strip if(!run--) break; idx3 = ReadUInt16(f); if (idx1 != idx2 && idx1 != idx3 && idx2 != idx3 && t < ntris) { surf->ofs_indexes[t*3+0] = idx1; surf->ofs_indexes[t*3+1] = idx2; surf->ofs_indexes[t*3+2] = idx3; t++; } idx1 = idx2; idx2 = idx3; //alternating triangles flip the order if(!run--) break; idx3 = ReadUInt16(f); if (idx1 != idx2 && idx1 != idx3 && idx2 != idx3 && t < ntris) { surf->ofs_indexes[t*3+2] = idx1; surf->ofs_indexes[t*3+1] = idx2; surf->ofs_indexes[t*3+0] = idx3; t++; } idx1 = idx2; idx2 = idx3; } } if (ntris != t) { Con_Printf(CON_ERROR"Expected %i tris, got %i from %i runs\n", ntris, t, r); return false; } surf->numindexes = t*3; //lazy and a bit slower. surf->ofs_st_array = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_st_array ) * surf->numverts); surf->ofs_skel_xyz = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_xyz ) * surf->numverts); surf->ofs_skel_norm = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_norm ) * surf->numverts); if (boney || f->numbones>0) { surf->ofs_skel_idx = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_idx ) * surf->numverts); surf->ofs_skel_weight = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_weight ) * surf->numverts); vw = memset(alloca(sizeof(*vw)*nverts), 0, sizeof(*vw)*nverts); } boneidx = min(boneidx,f->numbones-1); for (v = 0; v < nverts; v++) { norm[0] = ReadFloat(f); norm[1] = ReadFloat(f); norm[2] = ReadFloat(f); surf->ofs_st_array[v][0] = ReadFloat(f); surf->ofs_st_array[v][1] = ReadFloat(f); if (boney) { wcount = ReadUInt16(f); boneidx = ReadUInt16(f); boneidx = min(boneidx,f->numbones-1); } xyzw[0] = ReadFloat(f); xyzw[1] = ReadFloat(f); xyzw[2] = ReadFloat(f); if (wcount) { xyzw[3] = ReadFloat(f); VectorScale(xyzw, xyzw[3], xyzw); } else xyzw[3] = 1; if (surf->ofs_skel_idx) { vw[v] = wcount; //urgh. surf->ofs_skel_idx[v][0] = surf->ofs_skel_idx[v][1] = surf->ofs_skel_idx[v][2] = surf->ofs_skel_idx[v][3] = boneidx; //set all of them, cache might thank us. surf->ofs_skel_weight[v][0] = xyzw[3]; } //calculate the correct position in the base pose matrix = f->basepose + boneidx*12; surf->ofs_skel_xyz[ v][0] = xyzw[0] * matrix[0] + xyzw[1] * matrix[1] + xyzw[2] * matrix[ 2] + xyzw[3] * matrix[ 3]; surf->ofs_skel_xyz[ v][1] = xyzw[0] * matrix[4] + xyzw[1] * matrix[5] + xyzw[2] * matrix[ 6] + xyzw[3] * matrix[ 7]; surf->ofs_skel_xyz[ v][2] = xyzw[0] * matrix[8] + xyzw[1] * matrix[9] + xyzw[2] * matrix[10] + xyzw[3] * matrix[11]; surf->ofs_skel_norm[v][0] = norm[0] * matrix[0] + norm[1] * matrix[1] + norm[2] * matrix[ 2]; surf->ofs_skel_norm[v][1] = norm[0] * matrix[4] + norm[1] * matrix[5] + norm[2] * matrix[ 6]; surf->ofs_skel_norm[v][2] = norm[0] * matrix[8] + norm[1] * matrix[9] + norm[2] * matrix[10]; } if (vw) { float lowestv; int lowesti, j; for (v = 0; v < nverts; v++) { for (; vw[v] > 0; vw[v]--) { if (--exweights < 0) break; boneidx = ReadUInt16(f); boneidx = min(boneidx,f->numbones-1); xyzw[0] = ReadFloat(f); xyzw[1] = ReadFloat(f); xyzw[2] = ReadFloat(f); xyzw[3] = ReadFloat(f); VectorScale(xyzw, xyzw[3], xyzw); matrix = f->basepose + boneidx*12; surf->ofs_skel_xyz[ v][0] += xyzw[0] * matrix[0] + xyzw[1] * matrix[1] + xyzw[2] * matrix[ 2] + xyzw[3] * matrix[ 3]; surf->ofs_skel_xyz[ v][1] += xyzw[0] * matrix[4] + xyzw[1] * matrix[5] + xyzw[2] * matrix[ 6] + xyzw[3] * matrix[ 7]; surf->ofs_skel_xyz[ v][2] += xyzw[0] * matrix[8] + xyzw[1] * matrix[9] + xyzw[2] * matrix[10] + xyzw[3] * matrix[11]; lowesti = 0; lowestv = surf->ofs_skel_weight[v][0]; for (j = 1; j < countof(surf->ofs_skel_idx[v]); j++) { if (surf->ofs_skel_weight[v][j] < lowestv) lowestv = surf->ofs_skel_weight[v][lowesti=j]; } if (lowestv < xyzw[3]) { surf->ofs_skel_idx[v][lowesti] = boneidx; surf->ofs_skel_weight[v][lowesti] = xyzw[3]; } } //compensate for any missing weights xyzw[3] = surf->ofs_skel_weight[v][0]+surf->ofs_skel_weight[v][1]+surf->ofs_skel_weight[v][2]+surf->ofs_skel_weight[v][3]; if (xyzw[3]>0) Vector4Scale(surf->ofs_skel_weight[v], 1/xyzw[3], surf->ofs_skel_weight[v]); } } if (exweights) { //something was misread surf->numverts = 0; surf->numindexes = 0; return false; } //compute the tangents that are not stored in the file. surf->ofs_skel_svect = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_svect ) * surf->numverts); surf->ofs_skel_tvect = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_tvect ) * surf->numverts); modfuncs->AccumulateTextureVectors(surf->ofs_skel_xyz, surf->ofs_st_array, surf->ofs_skel_norm, surf->ofs_skel_svect, surf->ofs_skel_tvect, surf->ofs_indexes, surf->numindexes, false); modfuncs->NormaliseTextureVectors(surf->ofs_skel_norm, surf->ofs_skel_svect, surf->ofs_skel_tvect, surf->numverts, false); } else if (ver == 0x14) { //cod2 surf->ofs_st_array = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_st_array ) * surf->numverts); surf->ofs_skel_xyz = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_xyz ) * surf->numverts); surf->ofs_skel_norm = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_norm ) * surf->numverts); surf->ofs_skel_svect = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_svect ) * surf->numverts); surf->ofs_skel_tvect = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_tvect ) * surf->numverts); surf->ofs_rgbaub = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_rgbaub ) * surf->numverts); if (boney || f->numbones) { surf->ofs_skel_idx = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_idx ) * surf->numverts); surf->ofs_skel_weight = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_skel_weight ) * surf->numverts); } boneidx = min(boneidx,f->numbones-1); for (v = 0; v < nverts; v++) { surf->ofs_skel_norm[v][0] = ReadFloat(f); surf->ofs_skel_norm[v][1] = ReadFloat(f); surf->ofs_skel_norm[v][2] = ReadFloat(f); surf->ofs_rgbaub[v][0] = ReadByte(f); surf->ofs_rgbaub[v][1] = ReadByte(f); surf->ofs_rgbaub[v][2] = ReadByte(f); surf->ofs_rgbaub[v][3] = ReadByte(f); surf->ofs_st_array[v][0] = ReadFloat(f); surf->ofs_st_array[v][1] = ReadFloat(f); surf->ofs_skel_svect[v][0] = ReadFloat(f); surf->ofs_skel_svect[v][1] = ReadFloat(f); surf->ofs_skel_svect[v][2] = ReadFloat(f); surf->ofs_skel_tvect[v][0] = ReadFloat(f); surf->ofs_skel_tvect[v][1] = ReadFloat(f); surf->ofs_skel_tvect[v][2] = ReadFloat(f); if (boney) { int w; wcount = ReadByte(f); boneidx = ReadUInt16(f); boneidx = min(boneidx,f->numbones-1); xyzw[0] = ReadFloat(f); xyzw[1] = ReadFloat(f); xyzw[2] = ReadFloat(f); xyzw[3] = wcount?ReadByte(f)/255.f:1; VectorScale(xyzw, xyzw[3], xyzw); matrix = f->basepose + boneidx*12; surf->ofs_skel_xyz[ v][0] = xyzw[0] * matrix[0] + xyzw[1] * matrix[1] + xyzw[2] * matrix[ 2] + xyzw[3] * matrix[ 3]; surf->ofs_skel_xyz[ v][1] = xyzw[0] * matrix[4] + xyzw[1] * matrix[5] + xyzw[2] * matrix[ 6] + xyzw[3] * matrix[ 7]; surf->ofs_skel_xyz[ v][2] = xyzw[0] * matrix[8] + xyzw[1] * matrix[9] + xyzw[2] * matrix[10] + xyzw[3] * matrix[11]; surf->ofs_skel_idx[v][0] = surf->ofs_skel_idx[v][1] = surf->ofs_skel_idx[v][2] = surf->ofs_skel_idx[v][3] = boneidx; surf->ofs_skel_weight[v][0] = xyzw[3]; surf->ofs_skel_weight[v][1] = surf->ofs_skel_weight[v][2] = surf->ofs_skel_weight[v][3] = 0; for (w = 1; w <= wcount; w++) { boneidx = ReadUInt16(f); boneidx = min(boneidx,f->numbones-1); xyzw[0] = ReadFloat(f); xyzw[1] = ReadFloat(f); xyzw[2] = ReadFloat(f); xyzw[3] = ReadUInt16(f)/65535.f; VectorScale(xyzw, xyzw[3], xyzw); matrix = f->basepose + boneidx*12; surf->ofs_skel_xyz[ v][0] += xyzw[0] * matrix[0] + xyzw[1] * matrix[1] + xyzw[2] * matrix[ 2] + xyzw[3] * matrix[ 3]; surf->ofs_skel_xyz[ v][1] += xyzw[0] * matrix[4] + xyzw[1] * matrix[5] + xyzw[2] * matrix[ 6] + xyzw[3] * matrix[ 7]; surf->ofs_skel_xyz[ v][2] += xyzw[0] * matrix[8] + xyzw[1] * matrix[9] + xyzw[2] * matrix[10] + xyzw[3] * matrix[11]; if (w < 4) { surf->ofs_skel_idx[v][w] = boneidx; surf->ofs_skel_weight[v][w] = xyzw[3]; } } //compensate for any missing weights xyzw[3] = surf->ofs_skel_weight[v][0]+surf->ofs_skel_weight[v][1]+surf->ofs_skel_weight[v][2]+surf->ofs_skel_weight[v][3]; if (xyzw[3]>0&&xyzw[3]!=1) Vector4Scale(surf->ofs_skel_weight[v], 1/xyzw[3], surf->ofs_skel_weight[v]); } else { surf->ofs_skel_xyz[v][0] = ReadFloat(f); surf->ofs_skel_xyz[v][1] = ReadFloat(f); surf->ofs_skel_xyz[v][2] = ReadFloat(f); if (surf->ofs_skel_idx) { surf->ofs_skel_idx[v][0] = surf->ofs_skel_idx[v][1] = surf->ofs_skel_idx[v][2] = surf->ofs_skel_idx[v][3] = boneidx; surf->ofs_skel_weight[v][0] = 1; surf->ofs_skel_weight[v][1] = surf->ofs_skel_weight[v][2] = surf->ofs_skel_weight[v][3] = 0; } } } //indexes moved to AFTER. also triangles instead of weird strips. much nicer. surf->numindexes = ntris*3; surf->ofs_indexes = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf->ofs_indexes ) * surf->numindexes); for (v = 0; v < ntris*3; v++) surf->ofs_indexes[v] = ReadUInt16(f); } } return true; } struct codbone_s { //source data is the number of samples, the pose index for each sample and THEN the actual samples at the specified sparse poses. // const char *name; unsigned int numquats; qboolean flip; struct { unsigned short ts; signed short v[3]; } *quats; unsigned int numcoords; struct { unsigned int ts; vec3_t v; } *coord; }; static void Mod_XAnim_LoadQuats (struct model_s *mod, struct fstream_s *f, struct codbone_s *b, unsigned int maxts, qboolean flipquat, qboolean zonly) { unsigned int i, n = ReadUInt16(f); b->flip = flipquat; if (n) { b->numquats = n; b->quats = plugfuncs->GMalloc(&mod->memgroup, sizeof(*b->quats)*n); if (n == 1 || n == maxts) for (i = 0; i < n; i++) b->quats[i].ts = i; else if (maxts > 0xff) for (i = 0; i < n; i++) b->quats[i].ts = ReadUInt16(f); else for (i = 0; i < n; i++) b->quats[i].ts = ReadByte(f); for (i = 0; i < n; i++) { b->quats[i].v[0] = zonly?0:ReadSInt16(f); b->quats[i].v[1] = zonly?0:ReadSInt16(f); b->quats[i].v[2] = ReadSInt16(f); } } } static void Mod_XAnim_LoadCoords (struct model_s *mod, struct fstream_s *f, struct codbone_s *b, unsigned int maxts) { unsigned int i, n = ReadUInt16(f); if (n) { b->numcoords = n; b->coord = plugfuncs->GMalloc(&mod->memgroup, sizeof(*b->coord)*n); if (n == 1 || n == maxts) for (i = 0; i < n; i++) b->coord[i].ts = i; else if (maxts > 0xff) for (i = 0; i < n; i++) b->coord[i].ts = ReadUInt16(f); else for (i = 0; i < n; i++) b->coord[i].ts = ReadByte(f); for (i = 0; i < n; i++) { b->coord[i].v[0] = ReadFloat(f); b->coord[i].v[1] = ReadFloat(f); b->coord[i].v[2] = ReadFloat(f); } } } static float *QDECL Mod_XAnim_GetRawBones(const struct galiasinfo_s *animmesh, const struct galiasanimation_s *a, float time, float *bonematrixstorage, const struct galiasbone_s *boneinf, int numbones) { const struct codbone_s *bone = (const void*)(a+1); size_t b, i, j; int ts; float frac; galiasrefpose_t ref; time *= a->rate; ts = time; frac = time - ts; //FIXME: negative time! if (a->loop) { ts %= a->numposes; if (ts < 0) ts += a->numposes; } ts = bound(0, ts, a->numposes); if (!boneinf) { //our own bones?!? that'll be a horrible mess, but if you're really asking for that... boneinf = animmesh->ofsbones; numbones = min(numbones,animmesh->numbones); } for (b = 0; b < numbones; b++, boneinf++) { ref = boneinf->ref; for (j = 0; j < animmesh->numbones; j++) { if (!strcmp(boneinf->name, animmesh->ofsbones[j].name)) { //okay this is the one we want. replace the reference pose with the animated data. bone = (const struct codbone_s*)(a+1) + j; for (i = 0; i < bone->numquats; i++) if (ts < bone->quats[i].ts) break; //we flew too close to the sun! if (!i--) ; //use the value from the model. else if (i < bone->numquats-1 && bone->quats[i+1].ts == ts+1) { vec4_t oq,nq; VectorScale(bone->quats[i].v, 1.f/32767, oq); oq[3] = 1 - DotProduct(oq,oq); if (oq[3] > 0) oq[3] = sqrt(oq[3]); VectorScale(bone->quats[i+1].v, 1.f/32767, nq); nq[3] = 1 - DotProduct(nq,nq); if (nq[3] > 0) nq[3] = sqrt(nq[3]); modfuncs->QuaternionSlerp(oq, nq, frac, ref.quat); } else { VectorScale(bone->quats[i].v, 1.f/32767, ref.quat); ref.quat[3] = 1 - DotProduct(ref.quat,ref.quat); if (ref.quat[3] > 0) ref.quat[3] = sqrt(ref.quat[3]); } for (i = 0; i < bone->numcoords; i++) if (ts < bone->coord[i].ts) break; if (!i--) ; //use the value from the model. else if (i < bone->numcoords-1 && bone->coord[i+1].ts == ts+1) VectorInterpolate(bone->coord[i].v, frac, bone->coord[i+1].v, ref.org); else VectorCopy(bone->coord[i].v, ref.org); break; //don't look for others. } } modfuncs->GenMatrixPosQuat4Scale(ref.org, ref.quat, ref.scale, bonematrixstorage + b*12); } return bonematrixstorage; } static int Mod_XAnim_CompareEvents (const void *av, const void *bv) { const galiasevent_t *a=av; const galiasevent_t *b=bv; return b->timestamp-a->timestamp; } static qboolean Mod_XAnim_Load (struct model_s *mod, void *buffer, size_t fsize) { struct fstream_s f = {buffer, fsize}; unsigned short ver = ReadUInt16(&f); unsigned short numposes = ReadUInt16(&f); unsigned short numabones=0, numrbones = ReadUInt16(&f); unsigned char flags = ReadByte(&f); unsigned short framerate = ReadUInt16(&f); unsigned int i; const qbyte *flip, *tiny; struct codbone_s *bone; galiasinfo_t *surf; galiasanimation_t *anim; galiasbone_t *gbones; int numev; switch(ver) { case 0x0e: //cod1 break; case 0x14: break; default: Con_Printf(CON_ERROR"%s: unknown version %x\n", mod->name, ver); return false; } // Con_Printf(CON_DEBUG"Poses:%i bones:%i flags:%i rate:%i\n", numposes, numrbones, flags, framerate); if (flags & 2) numabones = 1; mod->type = mod_alias; mod->fromgame = fg_new; mod->meshinfo = surf = plugfuncs->GMalloc(&mod->memgroup, sizeof(*surf) + sizeof(*anim) + sizeof(*bone)*(numabones+numrbones)); surf->numbones = numabones+numrbones; surf->ofsbones = gbones = plugfuncs->GMalloc(&mod->memgroup, sizeof(*gbones)*(numabones+numrbones)); surf->numanimations = 1; surf->ofsanimations = anim = (void*)(surf+1); anim->loop = !!(flags&1); anim->numposes = numposes; anim->skeltype = SKEL_RELATIVE; //no parents info stored here, so these are screwy unless you're importing them into a model that DOES provide the parents info for you. bone = (void*)(anim+1); anim->rate = framerate; Q_strlcpy(anim->name, mod->name, sizeof(anim->name)); anim->GetRawBones = Mod_XAnim_GetRawBones; for (i = 0; i < numabones; i++) { gbones[i].parent = -1; VectorClear(gbones[i].ref.org); Vector4Set(gbones[i].ref.quat,0,0,0,1); VectorSet(gbones[i].ref.scale,1,1,1); Q_strlcpy(gbones[i].name, "tag_origin", sizeof(gbones[i].name)); Mod_XAnim_LoadQuats(mod, &f, &bone[i], numposes, false, true); Mod_XAnim_LoadCoords(mod, &f, &bone[i], numposes); } flip = ReadBytes(&f, (numrbones+7)>>3); tiny = ReadBytes(&f, (numrbones+7)>>3); if (anim->loop) numposes++; //is this the right place? for (i = 0; i < numrbones; i++) { const char *n = ReadString(&f); gbones[numabones+i].parent = -1; //no information. oh noes. VectorClear(gbones[i].ref.org); Vector4Set(gbones[i].ref.quat,0,0,0,1); VectorSet(gbones[i].ref.scale,1,1,1); Q_strlcpy(gbones[numabones+i].name, n, sizeof(gbones[numabones+i].name)); // Con_Printf(CON_DEBUG"Part %i = %s (%i %i)\n", numabones+i, bone[numabones+i].name, !!(flip[i>>3]&(1u<<(i&7))), !!(tiny[i>>3]&(1u<<(i&7)))); } for (i = 0; i < numrbones; i++) { // Con_Printf(CON_WARNING"%s:\n", gbones[numabones+i].name); Mod_XAnim_LoadQuats(mod, &f, &bone[numabones+i], numposes, !!(flip[i>>3]&(1u<<(i&7))), !!(tiny[i>>3]&(1u<<(i&7)))); Mod_XAnim_LoadCoords(mod, &f, &bone[numabones+i], numposes); } if (anim->loop) numposes--; numev = ReadByte(&f); if (numev) { anim->events = plugfuncs->GMalloc(&mod->memgroup, sizeof(*anim->events)*(numev)); for (i = 0; i < numev; i++) { const char *txt = ReadString(&f); anim->events[i].code = 0; //this format doesn't provide event ids, just pure strings. anim->events[i].data = strcpy(plugfuncs->GMalloc(&mod->memgroup, strlen(txt)+1), txt); anim->events[i].timestamp = ReadUInt16(&f) / (float)numposes; } qsort(anim->events, numev, sizeof(*anim->events), Mod_XAnim_CompareEvents); //make sure they're sorted by timestamps. for (i = 0; i < numev-1; i++) anim->events[i].next = &anim->events[i+1]; } if (f.ofs != f.len) Con_Printf(CON_WARNING"Misread %s (%u bytes of %u)\n", mod->name, (unsigned)f.ofs, (unsigned)f.len); return true; } qboolean QDECL Mod_XModel_Load (struct model_s *mod, void *buffer, size_t fsize) { struct fstream_s f = {buffer, fsize}; struct fstream_s pf = {NULL,0}; unsigned short ver; unsigned i; unsigned int clod, clodnsurf; unsigned short t; struct lod_s lod[4]; float mincov = 0; int nlod; //I fucking hate this. if (!strncmp(mod->publicname, "xanim/", 6)) //anims are not linked in any way, we treat them as separate precachable models so that gamecode can selectively load them. return Mod_XAnim_Load(mod, buffer, fsize); if (!strncmp(mod->publicname, "xmodelparts/", 12)) //loaded as part of models. don't get confused. return false; if (!strncmp(mod->publicname, "xmodelsurfs/", 12)) //loaded as part of models. don't get confused. return false; //"xmodels/" is okay though! ver = ReadUInt16(&f); switch(ver) { case 0x0e: //cod1 nlod = 3; break; case 0x14: //cod2 /*type =*/ ReadByte(&f); nlod = 4; break; default: Con_Printf(CON_ERROR"%s: Unknown version %x\n", mod->name, ver); return false; } mod->mins[0] = ReadFloat(&f); mod->mins[1] = ReadFloat(&f); mod->mins[2] = ReadFloat(&f); mod->maxs[0] = ReadFloat(&f); mod->maxs[1] = ReadFloat(&f); mod->maxs[2] = ReadFloat(&f); for (i = 0; i < nlod; i++) { lod[i].coverage = ReadFloat(&f); //coverage lod[i].name = ReadString(&f); } clod = ReadUInt32(&f); clodnsurf = ReadUInt32(&f); (void)clod; for (i = 0; i < clodnsurf && !ReadEOF(&f); i++) { unsigned int t, ntris = ReadUInt32(&f); for (t = 0; t < ntris && !ReadEOF(&f); t++) { vec4_t p, sd, td; //plane p[0] = ReadFloat(&f); p[1] = ReadFloat(&f); p[2] = ReadFloat(&f); p[3] = ReadFloat(&f); //?splane? sd[0] = ReadFloat(&f); sd[1] = ReadFloat(&f); sd[2] = ReadFloat(&f); sd[3] = ReadFloat(&f); //?tplane? td[0] = ReadFloat(&f); td[1] = ReadFloat(&f); td[2] = ReadFloat(&f); td[3] = ReadFloat(&f); (void)p; (void)sd; (void)td; // Con_Printf("%i: %f %f %f %f : %f %f %f %f : %f %f %f %f\n", t, p[0],p[1],p[2],p[3], sd[0],sd[1],sd[2],sd[3], td[0],td[1],td[2],td[3]); //dunno how to use this for collision... a triangle has 3 side faces AND a front! } //bounds ReadFloat(&f); ReadFloat(&f); ReadFloat(&f); ReadFloat(&f); ReadFloat(&f); ReadFloat(&f); /*boneidx = */ ReadUInt32(&f); /*contents = */ ReadUInt32(&f); /*surfaceflags = */ ReadUInt32(&f); } pf.ofs = 0; pf.start = filefuncs->LoadFile(va("xmodelparts/%s", lod[0].name), &pf.len); if (pf.start) { if (!ReadEOF(&pf) && Mod_XModel_LoadPart(mod, &pf)) { if (pf.ofs != pf.len) Con_Printf(CON_WARNING"Misread xmodelparts/%s (%u bytes of %u)\n", lod[0].name, (unsigned)f.ofs, (unsigned)f.len); } else return false; plugfuncs->Free(pf.start); } for (i = 0; i < nlod; i++) { if (!*lod[i].name) break; lod[i].numtex = ReadUInt16(&f); for (t = 0; t < lod[i].numtex; t++) lod[i].tex[t] = ReadString(&f); pf.ofs = 0; pf.start = filefuncs->LoadFile(va("xmodelsurfs/%s", lod[i].name), &pf.len); if (pf.start) { if (!ReadEOF(&pf) && Mod_XModel_LoadSurfs(mod, &pf, &lod[i], mincov)) { if (pf.ofs != pf.len) Con_Printf(CON_WARNING"Misread xmodelsurfs/%s (%u bytes of %u)\n", lod[i].name, (unsigned)f.ofs, (unsigned)f.len); } else return false; mincov = lod[i].coverage; plugfuncs->Free(pf.start); } } mod->type = mod_alias; mod->fromgame = fg_new; mod->radius = RadiusFromBounds(mod->mins, mod->maxs); // if (mod->meshinfo) // modfuncs->BIH_BuildAlias(mod, mod->meshinfo); return !!mod->meshinfo; } static qboolean XMODEL_Init(void) { filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs)); modfuncs = plugfuncs->GetEngineInterface(plugmodfuncs_name, sizeof(*modfuncs)); if (modfuncs && modfuncs->version != MODPLUGFUNCS_VERSION) modfuncs = NULL; if (modfuncs && filefuncs) { modfuncs->RegisterModelFormatMagic("XMODEL", "\x0e\0",2, Mod_XModel_Load); //cod1 modfuncs->RegisterModelFormatMagic("XMODEL", "\x14\0",2, Mod_XModel_Load); //cod2 return true; } return false; } qboolean CODBSP_Init(void); qboolean STypes_Init(void); qboolean IWI_Init(void); //qboolean IWFF_Init(void); qboolean Plug_Init(void) { qboolean somethingisokay = false; char plugname[128]; strcpy(plugname, "cod"); plugfuncs->GetPluginName(0, plugname, sizeof(plugname)); if (!STypes_Init()) Con_Printf(CON_ERROR"%s: Shader Types support unavailable\n", plugname); else somethingisokay = true; if (!IWI_Init()) Con_Printf(CON_ERROR"%s: IWI support unavailable\n", plugname); else somethingisokay = true; if (!XMODEL_Init()) Con_Printf(CON_ERROR"%s: XModel support unavailable\n", plugname); else somethingisokay = true; if (!CODBSP_Init()) Con_Printf(CON_ERROR"%s: CODBSP support unavailable\n", plugname); else somethingisokay = true; // if (!IWFF_Init()) Con_Printf(CON_ERROR"%s: FF support unavailable\n", plugname); else somethingisokay = true; return somethingisokay; } ================================================ FILE: plugins/emailnot/emailnot.q3asm ================================================ -o "emailnot" plugin qvm_api memory imapnoti pop3noti md5 ================================================ FILE: plugins/emailnot/imapnoti.c ================================================ /* Copyright (C) 2005 David Walton. 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. As a special exception, you may incorpotate patents and libraries regarding only hashing and security, on the conditions that it is also open source. This means md4/5, rsa, ssl and similar. */ #include "../plugin.h" //code to sit on an imap server and check for new emails every now and then. char *STR_Parse(char *str, char *out, int outlen, char *punctuation) { char *s = str; char *f; skipwhite: //skip over the whitespace while (*s <= ' ' && *s) s++; if (*s == '/') { if (s[1] == '/') //c++ style comment { while(*s != '\n' && *s) s++; goto skipwhite; } if (s[1] == '*') { s+=2; while(*s) { if (s[0] == '*' && s[1] == '/') { s+=2; break; } s++; } goto skipwhite; } } if (*s == '\"') { s++; while(*s && outlen>1) { if (*s == '\"') { s++; break; } *out++ = *s++; outlen--; } *out++ = '\0'; return s; } if (strchr(punctuation, *s)) { //starts with punctuation, so return only the first char if (outlen < 2) return NULL; //aaaah! *out++ = *s; *out++ = '\0'; s++; return s; } //skip over non-white for (f = s; outlen>1 && *(unsigned char*)f > ' '; f++, outlen--) { if (strchr(punctuation, *f)) { //found punctuation, so return up to here break; } *out++ = *f; } *out++ = '\0'; return f; } //exported. void IMAP_CreateConnection(char *servername, char *username, char *password); int imap_checkfrequency=60*1000; void IMAP_Think (void); //end export list. #define IMAP_PORT 143 typedef struct imap_con_s { char server[128]; char username[128]; char password[128]; unsigned int lastnoop; //these are used so we can fail a send. //or recieve only part of an input. //FIXME: make dynamically sizable, as it could drop if the send is too small (That's okay. // but if the read is bigger than one command we suddenly fail entirly. int sendlen; int sendbuffersize; char *sendbuffer; int readlen; int readbuffersize; char *readbuffer; qboolean drop; qhandle_t socket; enum { IMAP_WAITINGFORINITIALRESPONCE, IMAP_AUTHING, IMAP_AUTHED, IMAP_INBOX } state; struct imap_con_s *next; } imap_con_t; static imap_con_t *imapsv; void IMAP_CreateConnection(char *addy, char *username, char *password) { unsigned long _true = true; imap_con_t *con; for (con = imapsv; con; con = con->next) { if (!strcmp(con->server, addy)) { Con_Printf("Already connected to that imap server\n"); return; } } con = malloc(sizeof(imap_con_t)); con->socket = Net_TCPConnect(addy, IMAP_PORT); if (!con->socket) { Con_Printf ("IMAP_CreateConnection: connect failed\n"); free(con); return; } strlcpy(con->server, addy, sizeof(con->server)); strlcpy(con->username, username, sizeof(con->username)); strlcpy(con->password, password, sizeof(con->password)); con->next = imapsv; imapsv = con; Con_Printf ("Connected to %s (%s)\n", addy, username); } static void IMAP_EmitCommand(imap_con_t *imap, char *text) { int newlen; //makes a few things easier though newlen = imap->sendlen + 2 + strlen(text) + 2; if (newlen >= imap->sendbuffersize || !imap->sendbuffer) //pre-length check. { char *newbuf; imap->sendbuffersize = newlen*2; newbuf = malloc(imap->sendbuffersize); //the null terminator comes from the >= if (!newbuf) { Con_Printf("Memory is low\n"); imap->drop = true; //failed. return; } if (imap->sendbuffer) { memcpy(newbuf, imap->sendbuffer, imap->sendlen); free(imap->sendbuffer); } imap->sendbuffer = newbuf; } snprintf(imap->sendbuffer+imap->sendlen, newlen+1, "* %s\r\n", text); imap->sendlen = newlen; } static char *IMAP_AddressStructure(char *msg, char *out, int outsize) { char name[256]; char mailbox[64]; char hostname[128]; char route[128]; int indents=0; while(*msg == ' ') msg++; while(*msg == '(') //do it like this, we can get 2... I'm not sure if that's always true.. { msg++; indents++; } msg = STR_Parse(msg, name, sizeof(name), ""); //name msg = STR_Parse(msg, route, sizeof(route), ""); //smtp route (ignored normally) msg = STR_Parse(msg, mailbox, sizeof(mailbox), ""); //mailbox msg = STR_Parse(msg, hostname, sizeof(hostname), ""); //hostname while(indents && *msg == ')') msg++; if (out) { if (!strcmp(name, "NIL")) snprintf(out, outsize, "%s@%s", mailbox, hostname); else snprintf(out, outsize, "%s <%s@%s>", name, mailbox, hostname); } return msg; } static qboolean IMAP_ThinkCon(imap_con_t *imap) //false means drop the connection. { char *ending; int len; //get the buffer, stick it in our read holder if (imap->readlen+32 >= imap->readbuffersize || !imap->readbuffer) { len = imap->readbuffersize; if (!imap->readbuffer) imap->readbuffersize = 256; else imap->readbuffersize*=2; ending = malloc(imap->readbuffersize); if (!ending) { Con_Printf("Memory is low\n"); return false; } if (imap->readbuffer) { memcpy(ending, imap->readbuffer, len); free(imap->readbuffer); } imap->readbuffer = ending; } len = Net_Recv(imap->socket, imap->readbuffer+imap->readlen, imap->readbuffersize-imap->readlen-1); if (len>0) { imap->readlen+=len; imap->readbuffer[imap->readlen] = '\0'; } if (imap->readlen>0) { ending = strstr(imap->readbuffer, "\r\n"); if (ending) //pollable text. { *ending = '\0'; // Con_Printf("%s\n", imap->readbuffer); ending+=2; if (imap->state == IMAP_WAITINGFORINITIALRESPONCE) { //can be one of two things. if (!strncmp(imap->readbuffer, "* OK", 4)) { IMAP_EmitCommand(imap, va("LOGIN %s %s", imap->username, imap->password)); imap->state = IMAP_AUTHING; } else if (!strncmp(imap->readbuffer, "* PREAUTH", 9)) { Con_Printf("Logged on to %s\n", imap->server); IMAP_EmitCommand(imap, "SELECT INBOX"); imap->state = IMAP_AUTHED; imap->lastnoop = Sys_Milliseconds(); } else { Con_Printf("Unexpected response from IMAP server\n"); return false; } } else if (imap->state == IMAP_AUTHING) { if (!strncmp(imap->readbuffer, "* OK", 4)) { Con_Printf("Logged on to %s\n", imap->server); IMAP_EmitCommand(imap, "SELECT INBOX"); imap->state = IMAP_AUTHED; imap->lastnoop = Sys_Milliseconds(); } else { Con_Printf("Unexpected response from IMAP server\n"); return false; } } else if (imap->state == IMAP_AUTHED) { char *num; num = imap->readbuffer; if (!strncmp(imap->readbuffer, "* SEARCH ", 8)) //we only ever search for recent messages. So we fetch them and get sender and subject. { char *s; s = imap->readbuffer+8; num = NULL; while(*s) { s++; num = s; while (*s >= '0' && *s <= '9') s++; IMAP_EmitCommand(imap, va("FETCH %i ENVELOPE", atoi(num))); //envelope so that it's all one line. } } else if (imap->readbuffer[0] == '*' && imap->readbuffer[1] == ' ') { num = imap->readbuffer+2; while(*num >= '0' && *num <= '9') { num++; } if (!strcmp(num, " RECENT")) { if (atoi(imap->readbuffer+2) > 0) { IMAP_EmitCommand(imap, "SEARCH RECENT"); } } else if (!strncmp(num, " FETCH (ENVELOPE (", 18)) { char from[256]; char subject[256]; char date[256]; num += 18; num = STR_Parse(num, date, sizeof(date), ""); num = STR_Parse(num, subject, sizeof(subject), ""); // Con_Printf("Date/Time: %s\n", date); num = IMAP_AddressStructure(num, from, sizeof(from)); if ((rand() & 3) == 3) { if (rand()&1) Con_Printf("\n^2New spam has arrived\n"); else Con_Printf("\n^2You have new spam\n"); } else if (rand()&1) Con_Printf("\n^2New mail has arrived\n"); else Con_Printf("\n^2You have new mail\n"); Con_Printf("Subject: %s\n", subject); Con_Printf("From: %s\n", from); SCR_CenterPrint(va("NEW MAIL HAS ARRIVED\n\nTo: %s@%s\nFrom: %s\nSubject: %s", imap->username, imap->server, from, subject)); //throw the rest away. } } } else { Con_Printf("Bad client state\n"); return false; } imap->readlen -= ending - imap->readbuffer; memmove(imap->readbuffer, ending, strlen(ending)+1); } } if (imap->drop) return false; if (imap->state == IMAP_AUTHED) { if (imap->lastnoop + imap_checkfrequency < Sys_Milliseconds()) { //we need to keep the connection reasonably active IMAP_EmitCommand(imap, "SELECT INBOX"); //this causes the recent flags to be reset. This is the only way I found. imap->lastnoop = Sys_Milliseconds(); } } if (imap->sendlen) { len = Net_Send(imap->socket, imap->sendbuffer, imap->sendlen); if (len>0) { imap->sendlen-=len; memmove(imap->sendbuffer, imap->sendbuffer+len, imap->sendlen+1); } } return true; } void IMAP_Think (void) { imap_con_t *prev = NULL; imap_con_t *imap; for (imap = imapsv; imap; imap = imap->next) { if (imap->drop || !IMAP_ThinkCon(imap)) { if (!prev) imapsv = imap->next; else prev->next = imap->next; Net_Close(imap->socket); free(imap); if (!prev) break; } prev = imap; } } ================================================ FILE: plugins/emailnot/md5.c ================================================ //used by pop3. #include "../plugin.h" /* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm */ /* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All rights reserved. License to copy and use this software is granted provided that it is identified as the "RSA Data Security, Inc. MD5 Message-Digest Algorithm" in all material mentioning or referencing this software or this function. License is also granted to make and use derivative works provided that such works are identified as "derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm" in all material mentioning or referencing the derived work. RSA Data Security, Inc. makes no representations concerning either the merchantability of this software or the suitability of this software for any particular purpose. It is provided "as is" without express or implied warranty of any kind. These notices must be retained in any copies of any part of this documentation and/or software. */ /* GLOBAL.H - RSAREF types and constants */ /* POINTER defines a generic pointer type */ typedef unsigned char *POINTER; /* UINT2 defines a two byte word */ typedef unsigned short int UINT2; /* UINT4 defines a four byte word */ typedef unsigned int UINT4; /* MD5.H - header file for MD5C.C */ /* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All rights reserved. License to copy and use this software is granted provided that it is identified as the "RSA Data Security, Inc. MD5 Message-Digest Algorithm" in all material mentioning or referencing this software or this function. License is also granted to make and use derivative works provided that such works are identified as "derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm" in all material mentioning or referencing the derived work. RSA Data Security, Inc. makes no representations concerning either the merchantability of this software or the suitability of this software for any particular purpose. It is provided "as is" without express or implied warranty of any kind. These notices must be retained in any copies of any part of this documentation and/or software. */ /* MD5 context. */ typedef struct { UINT4 state[4]; /* state (ABCD) */ UINT4 count[2]; /* number of bits, modulo 2^64 (lsb first) */ unsigned char buffer[64]; /* input buffer */ } MD5_CTX; void MD5Init (MD5_CTX *ctx); void MD5Update (MD5_CTX *, unsigned char *, unsigned int); void MD5Final (unsigned char [16], MD5_CTX *); /* Constants for MD5Transform routine. */ #define S11 7 #define S12 12 #define S13 17 #define S14 22 #define S21 5 #define S22 9 #define S23 14 #define S24 20 #define S31 4 #define S32 11 #define S33 16 #define S34 23 #define S41 6 #define S42 10 #define S43 15 #define S44 21 static void MD5Transform (UINT4 [4], unsigned char [64]); static void Encode (unsigned char *, UINT4 *, unsigned int); static void Decode (UINT4 *, unsigned char *, unsigned int); static unsigned char PADDING[64] = { 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; /* F, G, H and I are basic MD5 functions. */ #define F(x, y, z) (((x) & (y)) | ((~x) & (z))) #define G(x, y, z) (((x) & (z)) | ((y) & (~z))) #define H(x, y, z) ((x) ^ (y) ^ (z)) #define I(x, y, z) ((y) ^ ((x) | (~z))) /* ROTATE_LEFT rotates x left n bits. */ #define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n)))) /* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4. Rotation is separate from addition to prevent recomputation. */ #define FF(a, b, c, d, x, s, ac) { \ (a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \ (a) = ROTATE_LEFT ((a), (s)); \ (a) += (b); \ } #define GG(a, b, c, d, x, s, ac) { \ (a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \ (a) = ROTATE_LEFT ((a), (s)); \ (a) += (b); \ } #define HH(a, b, c, d, x, s, ac) { \ (a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \ (a) = ROTATE_LEFT ((a), (s)); \ (a) += (b); \ } #define II(a, b, c, d, x, s, ac) { \ (a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \ (a) = ROTATE_LEFT ((a), (s)); \ (a) += (b); \ } /* MD5 initialization. Begins an MD5 operation, writing a new context. */ void MD5Init (MD5_CTX *context) { context->count[0] = context->count[1] = 0; /* Load magic initialization constants. */ context->state[0] = 0x67452301; context->state[1] = 0xefcdab89; context->state[2] = 0x98badcfe; context->state[3] = 0x10325476; } /* MD5 block update operation. Continues an MD5 message-digest operation, processing another message block, and updating the context. */ void MD5Update (MD5_CTX *context, unsigned char *input, unsigned int inputLen) { unsigned int i, index, partLen; /* Compute number of bytes mod 64 */ index = (unsigned int)((context->count[0] >> 3) & 0x3F); /* Update number of bits */ if ((context->count[0] += ((UINT4)inputLen << 3)) < ((UINT4)inputLen << 3)) context->count[1]++; context->count[1] += ((UINT4)inputLen >> 29); partLen = 64 - index; /* Transform as many times as possible. */ if (inputLen >= partLen) { memcpy ((POINTER)&context->buffer[index], (POINTER)input, partLen); MD5Transform (context->state, context->buffer); for (i = partLen; i + 63 < inputLen; i += 64) MD5Transform (context->state, &input[i]); index = 0; } else i = 0; /* Buffer remaining input */ memcpy ((POINTER)&context->buffer[index], (POINTER)&input[i], inputLen-i); } /* MD5 finalization. Ends an MD5 message-digest operation, writing the the message digest and zeroizing the context. */ void MD5Final (unsigned char digest[16], MD5_CTX *context) { unsigned char bits[8]; unsigned int index, padLen; /* Save number of bits */ Encode (bits, context->count, 8); /* Pad out to 56 mod 64. */ index = (unsigned int)((context->count[0] >> 3) & 0x3f); padLen = (index < 56) ? (56 - index) : (120 - index); MD5Update (context, PADDING, padLen); /* Append length (before padding) */ MD5Update (context, bits, 8); /* Store state in digest */ Encode (digest, context->state, 16); /* Zeroize sensitive information. */ memset ((POINTER)context, 0, sizeof (*context)); } /* MD5 basic transformation. Transforms state based on block. */ static void MD5Transform (UINT4 state[4], unsigned char block[64]) { UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16]; Decode (x, block, 64); /* Round 1 */ FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */ FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */ FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */ FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */ FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */ FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */ FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */ FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */ FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */ FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */ FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ /* Round 2 */ GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */ GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */ GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */ GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */ GG (d, a, b, c, x[10], S22, 0x2441453); /* 22 */ GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */ GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */ GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */ GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */ GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */ GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */ GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ /* Round 3 */ HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */ HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */ HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */ HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */ HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */ HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */ HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */ HH (b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */ HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */ HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */ /* Round 4 */ II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */ II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */ II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */ II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */ II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */ II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */ II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */ II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */ II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */ II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; /* Zeroize sensitive information. */ memset ((POINTER)x, 0, sizeof (x)); } /* Encodes input (UINT4) into output (unsigned char). Assumes len is a multiple of 4. */ static void Encode (unsigned char *output, UINT4 *input, unsigned int len) { unsigned int i, j; for (i = 0, j = 0; j < len; i++, j += 4) { output[j] = (unsigned char)(input[i] & 0xff); output[j+1] = (unsigned char)((input[i] >> 8) & 0xff); output[j+2] = (unsigned char)((input[i] >> 16) & 0xff); output[j+3] = (unsigned char)((input[i] >> 24) & 0xff); } } /* Decodes input (unsigned char) into output (UINT4). Assumes len is a multiple of 4. */ static void Decode (UINT4 *output, unsigned char *input, unsigned int len) { unsigned int i, j; for (i = 0, j = 0; j < len; i++, j += 4) output[i] = ((UINT4)input[j]) | (((UINT4)input[j+1]) << 8) | (((UINT4)input[j+2]) << 16) | (((UINT4)input[j+3]) << 24); } char *MD5_ToHex(char *input, int inputlen, char *ret, int retlen) { int v, i; unsigned char digest[16]; MD5_CTX ctx; if (retlen < 33) return NULL; MD5Init (&ctx); MD5Update (&ctx, (unsigned char *)input, inputlen); MD5Final ( (unsigned char *)digest, &ctx); for (i = 0; i < 16; i++) { v = digest[i]>>4; if (v >= 10) ret[i*2+0] = (v-10) + 'a'; else ret[i*2+0] = v + '0'; v = digest[i]&0xf; if (v >= 10) ret[i*2+1] = (v-10) + 'a'; else ret[i*2+1] = v + '0'; } ret[i*2] = '\0'; return ret; } char *MD5_ToBinary(char *input, int inputlen, char *ret, int retlen) { MD5_CTX ctx; if (retlen < 16) return NULL; MD5Init (&ctx); MD5Update (&ctx, (unsigned char *)input, inputlen); MD5Final ( (unsigned char *)ret, &ctx); return ret; } char *MD5_GetPop3APOPString(char *timestamp, char *secrit) { int v, i; static char ret[33]; unsigned char digest[16]; MD5_CTX ctx; MD5Init (&ctx); MD5Update (&ctx, (unsigned char *)timestamp, strlen(timestamp)); MD5Update (&ctx, (unsigned char *)secrit, strlen(secrit)); MD5Final ( (unsigned char *)digest, &ctx); for (i = 0; i < 16; i++) { v = digest[i]>>4; if (v >= 10) ret[i*2+0] = (v-10) + 'a'; else ret[i*2+0] = v + '0'; v = digest[i]&0xf; if (v >= 10) ret[i*2+1] = (v-10) + 'a'; else ret[i*2+1] = v + '0'; } ret[i*2] = '\0'; return ret; } ================================================ FILE: plugins/emailnot/pop3noti.c ================================================ /* Copyright (C) 2005 David Walton. 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. As a special exception, you may incorpotate patents and libraries regarding only hashing and security, on the conditions that it is also open source. This means md4/5, rsa, ssl and similar. */ #include "../plugin.h" //the idea is to send a UIDL request, and compare against the previous list. //this list will be stored on disk on quit. //be aware that we cannot stay connected. POP3 mailboxes are not refreshable without disconnecting. //so we have a special state. char *MD5_GetPop3APOPString(char *timestamp, char *secrit); void IMAP_CreateConnection(char *servername, char *username, char *password); void IMAP_Think (void); //exported. void POP3_CreateConnection(char *servername, char *username, char *password); int pop3_checkfrequency=60*1000; void POP3_Think (void); void POP3_WriteCache (void); //end export list. typedef struct msglist_s { struct msglist_s *next; char name[4]; } msglist_t; msglist_t *msglist; qboolean POP3_IsMessageUnique(char *name) { msglist_t *msg; for (msg = msglist; msg; msg = msg->next) { if (!strcmp(msg->name, name)) return false; } msg = malloc(sizeof(msglist_t) + strlen(name)+1); if (!msg) return false; strcpy(msg->name, name); msg->next = msglist; msglist = msg; return true; } #define POP3_PORT 110 typedef struct pop3_con_s { char server[128]; char username[128]; char password[128]; unsigned int lastnoop; //these are used so we can fail a send. //or recieve only part of an input. //FIXME: make dynamically sizable, as it could drop if the send is too small (That's okay. // but if the read is bigger than one command we suddenly fail entirly.) int sendlen; int sendbuffersize; char *sendbuffer; int readlen; int readbuffersize; char *readbuffer; qboolean drop; qhandle_t socket; //we have a certain number of stages. enum { POP3_NOTCONNECTED, POP3_WAITINGFORINITIALRESPONCE, //waiting for an initial response. POP3_AUTHING, //wating for a response from USER POP3_AUTHING2, //Set PASS, waiting to see if we passed. POP3_LISTING, //Sent UIDL, waiting to see POP3_RETRIEVING, //sent TOP, waiting for message headers to print info. POP3_HEADER, POP3_BODY, POP3_QUITTING } state; int retrlist[256]; //unrecognised uidls are added to this list. int numtoretrieve; char msgsubject[256]; char msgfrom[256]; struct pop3_con_s *next; } pop3_con_t; static pop3_con_t *pop3sv; void POP3_CreateConnection(char *addy, char *username, char *password) { pop3_con_t *con; for (con = pop3sv; con; con = con->next) { if (!strcmp(con->server, addy) && !strcmp(con->username, username)) { if (con->state == POP3_NOTCONNECTED && !con->socket) break; Con_Printf("Already connected to that pop3 server\n"); return; } } if (!con) { con = malloc(sizeof(pop3_con_t)); if (!con) { Con_Printf ("POP3_CreateConnection: out of plugin memory\n"); return; } memset(con, 0, sizeof(*con)); } else con->state = POP3_WAITINGFORINITIALRESPONCE; con->socket = Net_TCPConnect(addy, POP3_PORT); if (!con->socket) { Con_Printf ("POP3_CreateConnection: connect failed\n"); free(con); return; } strlcpy(con->server, addy, sizeof(con->server)); strlcpy(con->username, username, sizeof(con->username)); strlcpy(con->password, password, sizeof(con->password)); if (!con->state) { con->state = POP3_WAITINGFORINITIALRESPONCE; con->next = pop3sv; pop3sv = con; Con_Printf("Connected to %s\n", con->server); } } static void POP3_EmitCommand(pop3_con_t *pop3, char *text) { int newlen; newlen = pop3->sendlen + strlen(text) + 2; if (newlen >= pop3->sendbuffersize || !pop3->sendbuffer) //pre-length check. { char *newbuf; pop3->sendbuffersize = newlen*2; newbuf = malloc(pop3->sendbuffersize); if (!newbuf) { Con_Printf("Memory is low\n"); pop3->drop = true; //failed. return; } if (pop3->sendbuffer) { memcpy(newbuf, pop3->sendbuffer, pop3->sendlen); free(pop3->sendbuffer); } pop3->sendbuffer = newbuf; } snprintf(pop3->sendbuffer+pop3->sendlen, newlen+1, "%s\r\n", text); pop3->sendlen = newlen; // Con_Printf("^3%s\n", text); } static qboolean POP3_ThinkCon(pop3_con_t *pop3) //false means drop the connection. { char *ending; int len; if (pop3->state == POP3_NOTCONNECTED && !pop3->socket) { if (pop3->lastnoop + pop3_checkfrequency < Sys_Milliseconds()) { //we need to recreate the connection now. pop3->lastnoop = Sys_Milliseconds(); POP3_CreateConnection(pop3->server, pop3->username, pop3->password); } return true; } //get the buffer, stick it in our read holder if (pop3->readlen+32 >= pop3->readbuffersize || !pop3->readbuffer) { len = pop3->readbuffersize; if (!pop3->readbuffer) pop3->readbuffersize = 256; else pop3->readbuffersize*=2; ending = malloc(pop3->readbuffersize); if (!ending) { Con_Printf("Memory is low\n"); return false; } if (pop3->readbuffer) { memcpy(ending, pop3->readbuffer, len); free(pop3->readbuffer); } pop3->readbuffer = ending; } len = Net_Recv(pop3->socket, pop3->readbuffer+pop3->readlen, pop3->readbuffersize-pop3->readlen-1); if (len>0) { pop3->readlen+=len; pop3->readbuffer[pop3->readlen] = '\0'; } if (pop3->readlen>0) { ending = strstr(pop3->readbuffer, "\r\n"); if (ending) //pollable text. { *ending = '\0'; // Con_Printf("^2%s\n", pop3->readbuffer); ending+=2; if (pop3->state == POP3_WAITINGFORINITIALRESPONCE) { if (!strncmp(pop3->readbuffer, "+OK", 3)) { char *angle1; char *angle2 = NULL; angle1 = strchr(pop3->readbuffer, '<'); if (angle1) { angle2 = strchr(angle1+1, '>'); } if (angle2) { //just in case angle2[1] = '\0'; POP3_EmitCommand(pop3, va("APOP %s %s", pop3->username, MD5_GetPop3APOPString(angle1, pop3->password))); pop3->state = POP3_AUTHING2; } else { POP3_EmitCommand(pop3, va("USER %s", pop3->username)); pop3->state = POP3_AUTHING; } } else { Con_Printf("Unexpected response from POP3 server\n"); return false; //some sort of error. } } else if (pop3->state == POP3_AUTHING) { if (!strncmp(pop3->readbuffer, "+OK", 3)) { POP3_EmitCommand(pop3, va("PASS %s", pop3->password)); pop3->state = POP3_AUTHING2; } else { Con_Printf("Unexpected response from POP3 server.\nCheck username/password\n"); return false; //some sort of error. } } else if (pop3->state == POP3_AUTHING2) { if (!strncmp(pop3->readbuffer, "+OK", 3)) { POP3_EmitCommand(pop3, "UIDL"); pop3->state = POP3_LISTING; pop3->lastnoop = Sys_Milliseconds(); } else { Con_Printf("Unexpected response from POP3 server.\nCheck username/password\n"); return false; } } else if (pop3->state == POP3_LISTING) { if (!strncmp(pop3->readbuffer, "-ERR", 4)) { Con_Printf("Unexpected response from POP3 server.\nUIDL not supported?\n"); return false; } else if (!strncmp(pop3->readbuffer, "+OK", 3)) { } else if (!strncmp(pop3->readbuffer, ".", 1)) //we only ever search for recent messages. So we fetch them and get sender and subject. { if (!pop3->numtoretrieve) { pop3->state = POP3_QUITTING; POP3_EmitCommand(pop3, "QUIT"); } else { pop3->state = POP3_RETRIEVING; POP3_EmitCommand(pop3, va("RETR %i", pop3->retrlist[--pop3->numtoretrieve])); } } else { char *s; s = pop3->readbuffer; if (*s) { s++; while (*s >= '0' && *s <= '9') s++; while (*s == ' ') s++; } if (POP3_IsMessageUnique(s)) if (pop3->numtoretrieve < sizeof(pop3->retrlist)/sizeof(pop3->retrlist[0])) pop3->retrlist[pop3->numtoretrieve++] = atoi(pop3->readbuffer); } } else if (pop3->state == POP3_RETRIEVING) { if (!strncmp(pop3->readbuffer, "+OK", 3)) { pop3->msgsubject[0] = '\0'; pop3->msgfrom[0] = '\0'; pop3->state = POP3_HEADER; } else { //erm... go for the next? if (!pop3->numtoretrieve) { pop3->state = POP3_QUITTING; POP3_EmitCommand(pop3, "QUIT"); } else POP3_EmitCommand(pop3, va("RETR %i", pop3->retrlist[--pop3->numtoretrieve])); } } else if (pop3->state == POP3_HEADER) { if (!strnicmp(pop3->readbuffer, "From: ", 6)) strlcpy(pop3->msgfrom, pop3->readbuffer + 6, sizeof(pop3->msgfrom)); else if (!strnicmp(pop3->readbuffer, "Subject: ", 9)) strlcpy(pop3->msgsubject, pop3->readbuffer + 9, sizeof(pop3->msgsubject)); else if (!strncmp(pop3->readbuffer, ".", 1)) { Con_Printf("New message:\nFrom: %s\nSubject: %s\n", pop3->msgfrom, pop3->msgsubject); if (BUILTINISVALID(SCR_CenterPrint)) SCR_CenterPrint(va("NEW MAIL HAS ARRIVED\n\nTo: %s@%s\nFrom: %s\nSubject: %s", pop3->username, pop3->server, pop3->msgfrom, pop3->msgsubject)); if (!pop3->numtoretrieve) { pop3->state = POP3_QUITTING; POP3_EmitCommand(pop3, "QUIT"); } else { pop3->state = POP3_RETRIEVING; POP3_EmitCommand(pop3, va("RETR %i", pop3->retrlist[--pop3->numtoretrieve])); } } else if (!*pop3->readbuffer) pop3->state = POP3_BODY; } else if (pop3->state == POP3_BODY) { if (!strncmp(pop3->readbuffer, "..", 2)) { //line of text, skipping first '.' Con_Printf("%s\n", pop3->readbuffer+1); } else if (!strncmp(pop3->readbuffer, ".", 1)) { Con_Printf("New message:\nFrom: %s\nSubject: %s\n", pop3->msgfrom, pop3->msgsubject); if (BUILTINISVALID(SCR_CenterPrint)) SCR_CenterPrint(va("NEW MAIL HAS ARRIVED\n\nTo: %s@%s\nFrom: %s\nSubject: %s", pop3->username, pop3->server, pop3->msgfrom, pop3->msgsubject)); if (!pop3->numtoretrieve) { pop3->state = POP3_QUITTING; POP3_EmitCommand(pop3, "QUIT"); } else { pop3->state = POP3_RETRIEVING; POP3_EmitCommand(pop3, va("RETR %i", pop3->retrlist[--pop3->numtoretrieve])); } } else { //normal line of text Con_Printf("%s\n", pop3->readbuffer); } } else if (pop3->state == POP3_QUITTING) { pop3->state = POP3_NOTCONNECTED; Net_Close(pop3->socket); pop3->lastnoop = Sys_Milliseconds(); pop3->socket = 0; pop3->readlen = 0; pop3->sendlen = 0; return true; } else { Con_Printf("Bad client state\n"); return false; } pop3->readlen -= ending - pop3->readbuffer; memmove(pop3->readbuffer, ending, strlen(ending)+1); } } if (pop3->drop) return false; if (pop3->sendlen) { len = Net_Send(pop3->socket, pop3->sendbuffer, pop3->sendlen); if (len>0) { pop3->sendlen-=len; memmove(pop3->sendbuffer, pop3->sendbuffer+len, pop3->sendlen+1); } } return true; } void POP3_Think (void) { pop3_con_t *prev = NULL; pop3_con_t *pop3; for (pop3 = pop3sv; pop3; pop3 = pop3->next) { if (pop3->drop || !POP3_ThinkCon(pop3)) { if (!prev) pop3sv = pop3->next; else prev->next = pop3->next; if (pop3->socket) Net_Close(pop3->socket); free(pop3); if (!prev) break; } prev = pop3; } } int EmailNotification_Frame(int *args) { POP3_Think(); IMAP_Think(); return 0; } void IMAP_Account(void) { char arg1[64]; char arg2[64]; char arg3[64]; Cmd_Argv(1, arg1, sizeof(arg1)); Cmd_Argv(2, arg2, sizeof(arg2)); Cmd_Argv(3, arg3, sizeof(arg3)); if (!*arg1) { Con_Printf("imapaccount \n"); } else IMAP_CreateConnection(arg1, arg2, arg3); } void POP3_Account(void) { char arg1[64]; char arg2[64]; char arg3[64]; Cmd_Argv(1, arg1, sizeof(arg1)); Cmd_Argv(2, arg2, sizeof(arg2)); Cmd_Argv(3, arg3, sizeof(arg3)); if (!*arg1) { Con_Printf("pop3account \n"); Con_Printf("Say you had an acount called \"foo\" at yahoo's mail servers\n"); Con_Printf("Yahoo's pop3 servers are named \"pop.mail.yahoo.co.uk\"\n"); Con_Printf("Then if your password was bar, this is the command you would use\n"); Con_Printf("pop3account pop.mail.yahoo.co.uk foo bar\n"); Con_Printf("Of course, different pop3 servers have different naming conventions\n"); Con_Printf("So read your provider's documentation\n"); } else POP3_CreateConnection(arg1, arg2, arg3); } int EmailNotification_ExecuteCommand(int *args) { char cmd[64]; Cmd_Argv(0, cmd, sizeof(cmd)); if (!strcmp(cmd, "imapaccount")) { IMAP_Account(); return true; } if (!strcmp(cmd, "pop3account")) { POP3_Account(); return true; } return false; } int Plug_Init(int *args) { if (!Plug_Export("Tick", EmailNotification_Frame) || !Plug_Export("ExecuteCommand", EmailNotification_ExecuteCommand)) { Con_Print("email notification plugin failed\n"); return false; } Cmd_AddCommand("imapaccount"); Cmd_AddCommand("pop3account"); Con_Print("email notification plugin loaded\n"); return true; } ================================================ FILE: plugins/engine.h ================================================ #ifndef FTEPLUGIN #ifndef VARGS #define VARGS QDECL #endif typedef enum uploadfmt_e { TF_INVALID, TF_RGBA32, TF_BGRA32, TF_RGBX32, TF_BGRX32, TF_RGB24, TF_BGR24 } uploadfmt_t; typedef struct { size_t structsize; const char *drivername; void *(VARGS *createdecoder)(const char *name); qboolean (VARGS *decodeframe)(void *ctx, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ectx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ectx); void (VARGS *shutdown)(void *ctx); void (VARGS *rewind)(void *ctx); //these are any interactivity functions you might want... void (VARGS *cursormove) (void *ctx, float posx, float posy); //pos is 0-1 void (VARGS *key) (void *ctx, int code, int unicode, int event); qboolean (VARGS *setsize) (void *ctx, int width, int height); void (VARGS *getsize) (void *ctx, int *width, int *height); void (VARGS *changestream) (void *ctx, const char *streamname); size_t (VARGS *gettext) (void *ctx, const char *field, char *out, size_t outlen); //if out is null, returns required buffer size. returns 0 on failure / buffer too small } media_decoder_funcs_t; typedef struct { size_t structsize; const char *drivername; const char *description; const char *defaultextension; void *(VARGS *capture_begin) (char *streamname, int videorate, int width, int height, int *sndkhz, int *sndchannels, int *sndbits); void (VARGS *capture_video) (void *ctx, void *data, int frame, int width, int height, enum uploadfmt fmt); void (VARGS *capture_audio) (void *ctx, void *data, int bytes); void (VARGS *capture_end) (void *ctx); } media_encoder_funcs_t; #endif ================================================ FILE: plugins/ezhud/builtin_huds.h ================================================ /* WARNING: THIS FILE IS GENERATED BY 'generatebuiltin.c'. YOU SHOULD NOT EDIT THIS FILE BY HAND */ const char *builtin_hud_nquake = //fixme: sanitise markup "hud_ammo1_align \"center\"\n" "hud_ammo1_align_x \"center\"\n" "hud_ammo1_align_y \"after\"\n" "hud_ammo1_digits \"3\"\n" "hud_ammo1_frame \"0\"\n" "hud_ammo1_frame_color \"0 0 0\"\n" "hud_ammo1_item_opacity \"0.5\"\n" "hud_ammo1_order \"16\"\n" "hud_ammo1_place \"gun2\"\n" "hud_ammo1_pos_x \"0\"\n" "hud_ammo1_pos_y \"2\"\n" "hud_ammo1_scale \"0.4\"\n" "hud_ammo1_show \"1\"\n" "hud_ammo1_style \"0\"\n" "hud_ammo2_align \"center\"\n" "hud_ammo2_align_x \"center\"\n" "hud_ammo2_align_y \"after\"\n" "hud_ammo2_digits \"3\"\n" "hud_ammo2_frame \"0\"\n" "hud_ammo2_frame_color \"0 0 0\"\n" "hud_ammo2_item_opacity \"0.5\"\n" "hud_ammo2_order \"14\"\n" "hud_ammo2_place \"gun4\"\n" "hud_ammo2_pos_x \"0\"\n" "hud_ammo2_pos_y \"2\"\n" "hud_ammo2_scale \"0.4\"\n" "hud_ammo2_show \"1\"\n" "hud_ammo2_style \"0\"\n" "hud_ammo3_align \"center\"\n" "hud_ammo3_align_x \"center\"\n" "hud_ammo3_align_y \"after\"\n" "hud_ammo3_digits \"3\"\n" "hud_ammo3_frame \"0\"\n" "hud_ammo3_frame_color \"0 0 0\"\n" "hud_ammo3_item_opacity \"0.5\"\n" "hud_ammo3_order \"14\"\n" "hud_ammo3_place \"gun6\"\n" "hud_ammo3_pos_x \"0\"\n" "hud_ammo3_pos_y \"2\"\n" "hud_ammo3_scale \"0.4\"\n" "hud_ammo3_show \"1\"\n" "hud_ammo3_style \"0\"\n" "hud_ammo4_align \"center\"\n" "hud_ammo4_align_x \"center\"\n" "hud_ammo4_align_y \"after\"\n" "hud_ammo4_digits \"3\"\n" "hud_ammo4_frame \"0\"\n" "hud_ammo4_frame_color \"0 0 0\"\n" "hud_ammo4_item_opacity \"0.5\"\n" "hud_ammo4_order \"16\"\n" "hud_ammo4_place \"gun8\"\n" "hud_ammo4_pos_x \"0\"\n" "hud_ammo4_pos_y \"2\"\n" "hud_ammo4_scale \"0.4\"\n" "hud_ammo4_show \"1\"\n" "hud_ammo4_style \"0\"\n" "hud_ammo_align \"right\"\n" "hud_ammo_align_x \"after\"\n" "hud_ammo_align_y \"bottom\"\n" "hud_ammo_digits \"3\"\n" "hud_ammo_frame \"0\"\n" "hud_ammo_frame_color \"0 0 0\"\n" "hud_ammo_item_opacity \"0.99\"\n" "hud_ammo_order \"30\"\n" "hud_ammo_place \"health\"\n" "hud_ammo_pos_x \"5\"\n" "hud_ammo_pos_y \"0\"\n" "hud_ammo_scale \"0.9\"\n" "hud_ammo_show \"0\"\n" "hud_ammo_style \"0\"\n" "hud_armor_align \"right\"\n" "hud_armor_align_x \"right\"\n" "hud_armor_align_y \"center\"\n" "hud_armor_digits \"3\"\n" "hud_armor_frame \"0\"\n" "hud_armor_frame_color \"0 0 0\"\n" "hud_armor_item_opacity \"0.95\"\n" "hud_armor_order \"5\"\n" "hud_armor_pent_666 \"1\"\n" "hud_armor_place \"bar_armor\"\n" "hud_armor_pos_x \"-33\"\n" "hud_armor_pos_y \"2\"\n" "hud_armor_scale \"1\"\n" "hud_armor_show \"1\"\n" "hud_armor_style \"0\"\n" /* "hud_armordamage_align \"right\"\n" "hud_armordamage_align_x \"left\"\n" "hud_armordamage_align_y \"before\"\n" "hud_armordamage_digits \"3\"\n" "hud_armordamage_duration \"0.8\"\n" "hud_armordamage_frame \"0\"\n" "hud_armordamage_frame_color \"0 0 0\"\n" "hud_armordamage_item_opacity \"0.99\"\n" "hud_armordamage_order \"6\"\n" "hud_armordamage_place \"armor\"\n" "hud_armordamage_pos_x \"0\"\n" "hud_armordamage_pos_y \"0\"\n" "hud_armordamage_scale \"1\"\n" "hud_armordamage_show \"0\"\n" "hud_armordamage_style \"0\"\n" */ "hud_bar_armor_align_x \"before\"\n" "hud_bar_armor_align_y \"center\"\n" "hud_bar_armor_color_ga \"32 128 0 128\"\n" "hud_bar_armor_color_noarmor \"128 128 128 64\"\n" "hud_bar_armor_color_ra \"128 0 0 128\"\n" "hud_bar_armor_color_unnatural \"128 128 128\"\n" "hud_bar_armor_color_ya \"192 128 0 128\"\n" "hud_bar_armor_direction \"1\"\n" "hud_bar_armor_frame \"0\"\n" "hud_bar_armor_frame_color \"0 0 0\"\n" "hud_bar_armor_height \"16\"\n" "hud_bar_armor_item_opacity \"0.99\"\n" "hud_bar_armor_order \"4\"\n" "hud_bar_armor_place \"group1\"\n" "hud_bar_armor_pos_x \"-10\"\n" "hud_bar_armor_pos_y \"0\"\n" "hud_bar_armor_show \"1\"\n" "hud_bar_armor_width \"200\"\n" "hud_bar_health_align_x \"after\"\n" "hud_bar_health_align_y \"center\"\n" "hud_bar_health_color_mega \"64 96 128 128\"\n" "hud_bar_health_color_nohealth \"128 128 128 64\"\n" "hud_bar_health_color_normal \"32 64 128 128\"\n" "hud_bar_health_color_twomega \"128 128 255 128\"\n" "hud_bar_health_color_unnatural \"255 255 255 128\"\n" "hud_bar_health_direction \"0\"\n" "hud_bar_health_frame \"0\"\n" "hud_bar_health_frame_color \"0 0 0\"\n" "hud_bar_health_height \"16\"\n" "hud_bar_health_item_opacity \"0.99\"\n" "hud_bar_health_order \"2\"\n" "hud_bar_health_place \"group1\"\n" "hud_bar_health_pos_x \"10\"\n" "hud_bar_health_pos_y \"0\"\n" "hud_bar_health_show \"1\"\n" "hud_bar_health_width \"200\"\n" "hud_clock_align_x \"right\"\n" "hud_clock_align_y \"console\"\n" "hud_clock_big \"0\"\n" "hud_clock_blink \"0\"\n" "hud_clock_format \"3\"\n" "hud_clock_frame \"0\"\n" "hud_clock_frame_color \"0 0 0\"\n" "hud_clock_item_opacity \"0.99\"\n" "hud_clock_order \"8\"\n" "hud_clock_place \"screen\"\n" "hud_clock_pos_x \"1\"\n" "hud_clock_pos_y \"0\"\n" "hud_clock_scale \"1.000013\"\n" "hud_clock_show \"1\"\n" "hud_clock_style \"0\"\n" "hud_democlock_align_x \"right\"\n" "hud_democlock_align_y \"middle\"\n" "hud_democlock_big \"0\"\n" "hud_democlock_blink \"0\"\n" "hud_democlock_frame \"0\"\n" "hud_democlock_frame_color \"0 0 0\"\n" "hud_democlock_item_opacity \"0.99\"\n" "hud_democlock_order \"11\"\n" "hud_democlock_place \"fps\"\n" "hud_democlock_pos_x \"0\"\n" "hud_democlock_pos_y \"8\"\n" "hud_democlock_scale \"1\"\n" "hud_democlock_show \"0\"\n" "hud_democlock_style \"0\"\n" "hud_digits_trim \"1\"\n" "hud_editor_allowalign \"1\"\n" "hud_editor_allowmove \"1\"\n" "hud_editor_allowplace \"1\"\n" "hud_editor_allowresize \"1\"\n" "hud_face_align_x \"before\"\n" "hud_face_align_y \"bottom\"\n" "hud_face_frame \"0\"\n" "hud_face_frame_color \"0 0 0\"\n" "hud_face_item_opacity \"0.99\"\n" "hud_face_order \"30\"\n" "hud_face_place \"health\"\n" "hud_face_pos_x \"0\"\n" "hud_face_pos_y \"0\"\n" "hud_face_scale \"0.84\"\n" "hud_face_show \"0\"\n" "hud_fps_align_x \"before\"\n" "hud_fps_align_y \"center\"\n" /* "hud_fps_decimals \"0\"\n" */ "hud_fps_drop \"70\"\n" "hud_fps_frame \"0\"\n" "hud_fps_frame_color \"0 0 0\"\n" "hud_fps_item_opacity \"0.99\"\n" "hud_fps_order \"10\"\n" "hud_fps_place \"ping\"\n" "hud_fps_pos_x \"-10\"\n" "hud_fps_pos_y \"0\"\n" "hud_fps_show \"1\"\n" "hud_fps_show_min \"0\"\n" "hud_fps_style \"0\"\n" "hud_fps_title \"1\"\n" "hud_frags_align_x \"center\"\n" "hud_frags_align_y \"bottom\"\n" "hud_frags_bignum \"0\"\n" "hud_frags_cell_height \"8\"\n" "hud_frags_cell_width \"28\"\n" "hud_frags_colors_alpha \"1.0\"\n" "hud_frags_cols \"8\"\n" "hud_frags_extra_spec_info \"ALL\"\n" "hud_frags_fliptext \"0\"\n" "hud_frags_frame \"0\"\n" "hud_frags_frame_color \"0 0 0\"\n" "hud_frags_item_opacity \"0.99\"\n" "hud_frags_maxname \"16\"\n" "hud_frags_notintp \"0\"\n" "hud_frags_order \"0\"\n" "hud_frags_padtext \"1\"\n" "hud_frags_place \"screen\"\n" "hud_frags_pos_x \"0\"\n" "hud_frags_pos_y \"-80\"\n" "hud_frags_rows \"1\"\n" "hud_frags_show \"0\"\n" "hud_frags_shownames \"0\"\n" "hud_frags_showself_always \"1\"\n" "hud_frags_showteams \"0\"\n" "hud_frags_space_x \"1\"\n" "hud_frags_space_y \"1\"\n" "hud_frags_strip \"1\"\n" "hud_frags_style \"2\"\n" "hud_frags_teamsort \"1\"\n" "hud_frags_vertical \"0\"\n" "hud_gameclock_align_x \"center\"\n" "hud_gameclock_align_y \"top\"\n" "hud_gameclock_big \"1\"\n" "hud_gameclock_blink \"0\"\n" "hud_gameclock_countdown \"1\"\n" "hud_gameclock_frame \"0\"\n" "hud_gameclock_frame_color \"0 0 0\"\n" "hud_gameclock_item_opacity \"0.95\"\n" "hud_gameclock_offset \"0\"\n" "hud_gameclock_order \"8\"\n" "hud_gameclock_place \"screen\"\n" "hud_gameclock_pos_x \"0\"\n" "hud_gameclock_pos_y \"10\"\n" "hud_gameclock_scale \"1\"\n" "hud_gameclock_show \"1\"\n" "hud_gameclock_style \"0\"\n" "hud_group1_align_x \"center\"\n" "hud_group1_align_y \"bottom\"\n" "hud_group1_frame \"0.1\"\n" "hud_group1_frame_color \"100 100 100\"\n" "hud_group1_height \"26\"\n" "hud_group1_item_opacity \"0.99\"\n" "hud_group1_name \"group1\"\n" "hud_group1_order \"1\"\n" "hud_group1_pic_alpha \"1.0\"\n" "hud_group1_pic_scalemode \"0\"\n" "hud_group1_picture \"\"\n" "hud_group1_place \"screen\"\n" "hud_group1_pos_x \"0\"\n" "hud_group1_pos_y \"-5\"\n" "hud_group1_show \"1\"\n" "hud_group1_width \"2\"\n" "hud_group2_align_x \"center\"\n" "hud_group2_align_y \"before\"\n" "hud_group2_frame \"0.1\"\n" "hud_group2_frame_color \"100 100 100\"\n" "hud_group2_height \"2\"\n" "hud_group2_item_opacity \"0.99\"\n" "hud_group2_name \"group2\"\n" "hud_group2_order \"2\"\n" "hud_group2_pic_alpha \"1.0\"\n" "hud_group2_pic_scalemode \"0\"\n" "hud_group2_picture \"\"\n" "hud_group2_place \"group1\"\n" "hud_group2_pos_x \"0\"\n" "hud_group2_pos_y \"-6\"\n" "hud_group2_show \"1\"\n" "hud_group2_width \"325\"\n" "hud_group3_align_x \"center\"\n" "hud_group3_align_y \"before\"\n" "hud_group3_frame \"0\"\n" "hud_group3_frame_color \"0 0 0\"\n" "hud_group3_height \"32\"\n" "hud_group3_item_opacity \"0.99\"\n" "hud_group3_name \"group2\"\n" "hud_group3_order \"3\"\n" "hud_group3_pic_alpha \"0.1\"\n" "hud_group3_pic_scalemode \"2\"\n" "hud_group3_picture \"weaplist_full\"\n" "hud_group3_place \"group2\"\n" "hud_group3_pos_x \"0\"\n" "hud_group3_pos_y \"0\"\n" "hud_group3_show \"1\"\n" "hud_group3_width \"336\"\n" "hud_group4_align_x \"center\"\n" "hud_group4_align_y \"center\"\n" "hud_group4_frame \".5\"\n" "hud_group4_frame_color \"0 0 0\"\n" "hud_group4_height \"10\"\n" "hud_group4_item_opacity \"0.99\"\n" "hud_group4_name \"group2\"\n" "hud_group4_order \"14\"\n" "hud_group4_pic_alpha \"1.0\"\n" "hud_group4_pic_scalemode \"0\"\n" "hud_group4_picture \"\"\n" "hud_group4_place \"gun6\"\n" "hud_group4_pos_x \"-8\"\n" "hud_group4_pos_y \"-1\"\n" "hud_group4_show \"0\"\n" "hud_group4_width \"60\"\n" "hud_group5_align_x \"center\"\n" "hud_group5_align_y \"center\"\n" "hud_group5_frame \".5\"\n" "hud_group5_frame_color \"0 0 0\"\n" "hud_group5_height \"10\"\n" "hud_group5_item_opacity \"0.99\"\n" "hud_group5_name \"group2\"\n" "hud_group5_order \"16\"\n" "hud_group5_pic_alpha \"1.0\"\n" "hud_group5_pic_scalemode \"0\"\n" "hud_group5_picture \"\"\n" "hud_group5_place \"gun8\"\n" "hud_group5_pos_x \"-8\"\n" "hud_group5_pos_y \"-1\"\n" "hud_group5_show \"0\"\n" "hud_group5_width \"60\"\n" "hud_group6_align_x \"left\"\n" "hud_group6_align_y \"top\"\n" "hud_group6_frame \".5\"\n" "hud_group6_frame_color \"0 0 0\"\n" "hud_group6_height \"64\"\n" "hud_group6_item_opacity \"0.99\"\n" "hud_group6_name \"group6\"\n" "hud_group6_order \"0\"\n" "hud_group6_pic_alpha \"1.0\"\n" "hud_group6_pic_scalemode \"0\"\n" "hud_group6_picture \"\"\n" "hud_group6_place \"screen\"\n" "hud_group6_pos_x \"0\"\n" "hud_group6_pos_y \"0\"\n" "hud_group6_show \"0\"\n" "hud_group6_width \"64\"\n" "hud_group7_align_x \"left\"\n" "hud_group7_align_y \"top\"\n" "hud_group7_frame \".5\"\n" "hud_group7_frame_color \"0 0 0\"\n" "hud_group7_height \"64\"\n" "hud_group7_item_opacity \"0.99\"\n" "hud_group7_name \"group7\"\n" "hud_group7_order \"0\"\n" "hud_group7_pic_alpha \"1.0\"\n" "hud_group7_pic_scalemode \"0\"\n" "hud_group7_picture \"\"\n" "hud_group7_place \"screen\"\n" "hud_group7_pos_x \"0\"\n" "hud_group7_pos_y \"0\"\n" "hud_group7_show \"0\"\n" "hud_group7_width \"64\"\n" "hud_group8_align_x \"left\"\n" "hud_group8_align_y \"top\"\n" "hud_group8_frame \".5\"\n" "hud_group8_frame_color \"0 0 0\"\n" "hud_group8_height \"64\"\n" "hud_group8_item_opacity \"0.99\"\n" "hud_group8_name \"group8\"\n" "hud_group8_order \"0\"\n" "hud_group8_pic_alpha \"1.0\"\n" "hud_group8_pic_scalemode \"0\"\n" "hud_group8_picture \"\"\n" "hud_group8_place \"screen\"\n" "hud_group8_pos_x \"0\"\n" "hud_group8_pos_y \"0\"\n" "hud_group8_show \"0\"\n" "hud_group8_width \"64\"\n" "hud_group9_align_x \"left\"\n" "hud_group9_align_y \"top\"\n" "hud_group9_frame \".5\"\n" "hud_group9_frame_color \"0 0 0\"\n" "hud_group9_height \"64\"\n" "hud_group9_item_opacity \"0.99\"\n" "hud_group9_name \"group9\"\n" "hud_group9_order \"0\"\n" "hud_group9_pic_alpha \"1.0\"\n" "hud_group9_pic_scalemode \"0\"\n" "hud_group9_picture \"\"\n" "hud_group9_place \"screen\"\n" "hud_group9_pos_x \"0\"\n" "hud_group9_pos_y \"0\"\n" "hud_group9_show \"0\"\n" "hud_group9_width \"64\"\n" "hud_gun2_align_x \"before\"\n" "hud_gun2_align_y \"center\"\n" "hud_gun2_frame \"0\"\n" "hud_gun2_frame_color \"0 0 0\"\n" "hud_gun2_item_opacity \"0.99\"\n" "hud_gun2_order \"15\"\n" "hud_gun2_place \"gun3\"\n" "hud_gun2_pos_x \"0\"\n" "hud_gun2_pos_y \"0\"\n" "hud_gun2_scale \"2\"\n" "hud_gun2_show \"1\"\n" "hud_gun2_style \"0\"\n" "hud_gun3_align_x \"before\"\n" "hud_gun3_align_y \"center\"\n" "hud_gun3_frame \"0\"\n" "hud_gun3_frame_color \"0 0 0\"\n" "hud_gun3_item_opacity \"0.99\"\n" "hud_gun3_order \"14\"\n" "hud_gun3_place \"gun4\"\n" "hud_gun3_pos_x \"0\"\n" "hud_gun3_pos_y \"0\"\n" "hud_gun3_scale \"2\"\n" "hud_gun3_show \"1\"\n" "hud_gun3_style \"0\"\n" "hud_gun4_align_x \"before\"\n" "hud_gun4_align_y \"center\"\n" "hud_gun4_frame \"0\"\n" "hud_gun4_frame_color \"0 0 0\"\n" "hud_gun4_item_opacity \"0.99\"\n" "hud_gun4_order \"13\"\n" "hud_gun4_place \"gun5\"\n" "hud_gun4_pos_x \"0\"\n" "hud_gun4_pos_y \"0\"\n" "hud_gun4_scale \"2\"\n" "hud_gun4_show \"1\"\n" "hud_gun4_style \"0\"\n" "hud_gun5_align_x \"center\"\n" "hud_gun5_align_y \"center\"\n" "hud_gun5_frame \"0\"\n" "hud_gun5_frame_color \"0 0 0\"\n" "hud_gun5_item_opacity \"0.99\"\n" "hud_gun5_order \"12\"\n" "hud_gun5_place \"group3\"\n" "hud_gun5_pos_x \"0\"\n" "hud_gun5_pos_y \"0\"\n" "hud_gun5_scale \"2\"\n" "hud_gun5_show \"1\"\n" "hud_gun5_style \"0\"\n" "hud_gun6_align_x \"after\"\n" "hud_gun6_align_y \"center\"\n" "hud_gun6_frame \"0\"\n" "hud_gun6_frame_color \"0 0 0\"\n" "hud_gun6_item_opacity \"0.99\"\n" "hud_gun6_order \"13\"\n" "hud_gun6_place \"gun5\"\n" "hud_gun6_pos_x \"0\"\n" "hud_gun6_pos_y \"0\"\n" "hud_gun6_scale \"2\"\n" "hud_gun6_show \"1\"\n" "hud_gun6_style \"0\"\n" "hud_gun7_align_x \"after\"\n" "hud_gun7_align_y \"center\"\n" "hud_gun7_frame \"0\"\n" "hud_gun7_frame_color \"0 0 0\"\n" "hud_gun7_item_opacity \"0.99\"\n" "hud_gun7_order \"14\"\n" "hud_gun7_place \"gun6\"\n" "hud_gun7_pos_x \"0\"\n" "hud_gun7_pos_y \"0\"\n" "hud_gun7_scale \"2\"\n" "hud_gun7_show \"1\"\n" "hud_gun7_style \"0\"\n" "hud_gun8_align_x \"after\"\n" "hud_gun8_align_y \"center\"\n" "hud_gun8_frame \"0\"\n" "hud_gun8_frame_color \"0 0 0\"\n" "hud_gun8_item_opacity \"0.99\"\n" "hud_gun8_order \"15\"\n" "hud_gun8_place \"gun7\"\n" "hud_gun8_pos_x \"0\"\n" "hud_gun8_pos_y \"0\"\n" "hud_gun8_scale \"2\"\n" "hud_gun8_show \"1\"\n" "hud_gun8_style \"0\"\n" "hud_gun8_wide \"0\"\n" "hud_gun_align_x \"center\"\n" "hud_gun_align_y \"bottom\"\n" "hud_gun_frame \"0\"\n" "hud_gun_frame_color \"0 0 0\"\n" "hud_gun_item_opacity \"0.99\"\n" "hud_gun_order \"0\"\n" "hud_gun_place \"screen\"\n" "hud_gun_pos_x \"center\"\n" "hud_gun_pos_y \"-43\"\n" "hud_gun_scale \"0.5\"\n" "hud_gun_show \"0\"\n" "hud_gun_style \"1\"\n" "hud_gun_wide \"0\"\n" "hud_health_align \"left\"\n" "hud_health_align_x \"left\"\n" "hud_health_align_y \"center\"\n" "hud_health_digits \"3\"\n" "hud_health_frame \"0\"\n" "hud_health_frame_color \"0 0 0\"\n" "hud_health_item_opacity \"0.95\"\n" "hud_health_order \"29\"\n" "hud_health_place \"bar_health\"\n" "hud_health_pos_x \"20\"\n" "hud_health_pos_y \"0\"\n" "hud_health_scale \"1\"\n" "hud_health_show \"1\"\n" "hud_health_style \"0\"\n" /* "hud_healthdamage_align \"right\"\n" "hud_healthdamage_align_x \"left\"\n" "hud_healthdamage_align_y \"before\"\n" "hud_healthdamage_digits \"3\"\n" "hud_healthdamage_duration \"0.8\"\n" "hud_healthdamage_frame \"0\"\n" "hud_healthdamage_frame_color \"0 0 0\"\n" "hud_healthdamage_item_opacity \"0.99\"\n" "hud_healthdamage_order \"30\"\n" "hud_healthdamage_place \"health\"\n" "hud_healthdamage_pos_x \"0\"\n" "hud_healthdamage_pos_y \"0\"\n" "hud_healthdamage_scale \"1\"\n" "hud_healthdamage_show \"0\"\n" "hud_healthdamage_style \"0\"\n" */ "hud_iammo1_align_x \"after\"\n" "hud_iammo1_align_y \"top\"\n" "hud_iammo1_frame \"0\"\n" "hud_iammo1_frame_color \"0 0 0\"\n" "hud_iammo1_item_opacity \"0.99\"\n" "hud_iammo1_order \"17\"\n" "hud_iammo1_place \"ammo1\"\n" "hud_iammo1_pos_x \"1\"\n" "hud_iammo1_pos_y \"1\"\n" "hud_iammo1_scale \"0.25\"\n" "hud_iammo1_show \"0\"\n" "hud_iammo1_style \"0\"\n" "hud_iammo2_align_x \"after\"\n" "hud_iammo2_align_y \"top\"\n" "hud_iammo2_frame \"0\"\n" "hud_iammo2_frame_color \"0 0 0\"\n" "hud_iammo2_item_opacity \"0.99\"\n" "hud_iammo2_order \"18\"\n" "hud_iammo2_place \"ammo2\"\n" "hud_iammo2_pos_x \"1\"\n" "hud_iammo2_pos_y \"1\"\n" "hud_iammo2_scale \"0.25\"\n" "hud_iammo2_show \"0\"\n" "hud_iammo2_style \"0\"\n" "hud_iammo3_align_x \"after\"\n" "hud_iammo3_align_y \"top\"\n" "hud_iammo3_frame \"0\"\n" "hud_iammo3_frame_color \"0 0 0\"\n" "hud_iammo3_item_opacity \"0.99\"\n" "hud_iammo3_order \"19\"\n" "hud_iammo3_place \"ammo3\"\n" "hud_iammo3_pos_x \"1\"\n" "hud_iammo3_pos_y \"1\"\n" "hud_iammo3_scale \"0.25\"\n" "hud_iammo3_show \"0\"\n" "hud_iammo3_style \"0\"\n" "hud_iammo4_align_x \"after\"\n" "hud_iammo4_align_y \"top\"\n" "hud_iammo4_frame \"0\"\n" "hud_iammo4_frame_color \"0 0 0\"\n" "hud_iammo4_item_opacity \"0.99\"\n" "hud_iammo4_order \"20\"\n" "hud_iammo4_place \"ammo4\"\n" "hud_iammo4_pos_x \"1\"\n" "hud_iammo4_pos_y \"1\"\n" "hud_iammo4_scale \"0.25\"\n" "hud_iammo4_show \"0\"\n" "hud_iammo4_style \"0\"\n" "hud_iammo_align_x \"after\"\n" "hud_iammo_align_y \"bottom\"\n" "hud_iammo_frame \"0\"\n" "hud_iammo_frame_color \"0 0 0\"\n" "hud_iammo_item_opacity \"0.99\"\n" "hud_iammo_order \"31\"\n" "hud_iammo_place \"ammo\"\n" "hud_iammo_pos_x \"0\"\n" "hud_iammo_pos_y \"0\"\n" "hud_iammo_scale \"0.84\"\n" "hud_iammo_show \"0\"\n" "hud_iammo_style \"0\"\n" "hud_iarmor_align_x \"before\"\n" "hud_iarmor_align_y \"bottom\"\n" "hud_iarmor_frame \"0\"\n" "hud_iarmor_frame_color \"0 0 0\"\n" "hud_iarmor_item_opacity \"0.99\"\n" "hud_iarmor_order \"6\"\n" "hud_iarmor_place \"armor\"\n" "hud_iarmor_pos_x \"0\"\n" "hud_iarmor_pos_y \"0\"\n" "hud_iarmor_scale \"0.84\"\n" "hud_iarmor_show \"0\"\n" "hud_iarmor_style \"0\"\n" /* "hud_itemsclock_align_x \"right\"\n" "hud_itemsclock_align_y \"center\"\n" "hud_itemsclock_frame \"0\"\n" "hud_itemsclock_frame_color \"0 0 0\"\n" "hud_itemsclock_item_opacity \"0.99\"\n" "hud_itemsclock_order \"1\"\n" "hud_itemsclock_place \"screen\"\n" "hud_itemsclock_pos_x \"0\"\n" "hud_itemsclock_pos_y \"0\"\n" "hud_itemsclock_show \"0\"\n" "hud_itemsclock_style \"0\"\n" "hud_itemsclock_timelimit \"5\"\n" */ "hud_key1_align_x \"center\"\n" "hud_key1_align_y \"bottom\"\n" "hud_key1_frame \"0\"\n" "hud_key1_frame_color \"0 0 0\"\n" "hud_key1_item_opacity \"0.99\"\n" "hud_key1_order \"0\"\n" "hud_key1_place \"top\"\n" "hud_key1_pos_x \"155\"\n" "hud_key1_pos_y \"-32\"\n" "hud_key1_scale \"2\"\n" "hud_key1_show \"1\"\n" "hud_key1_style \"0\"\n" "hud_key2_align_x \"center\"\n" "hud_key2_align_y \"bottom\"\n" "hud_key2_frame \"0\"\n" "hud_key2_frame_color \"0 0 0\"\n" "hud_key2_item_opacity \"0.99\"\n" "hud_key2_order \"1\"\n" "hud_key2_place \"key1\"\n" "hud_key2_pos_x \"-34\"\n" "hud_key2_pos_y \"0\"\n" "hud_key2_scale \"2\"\n" "hud_key2_show \"1\"\n" "hud_key2_style \"0\"\n" /* "hud_keys_align_x \"right\"\n" "hud_keys_align_y \"center\"\n" "hud_keys_frame \"0.5\"\n" "hud_keys_frame_color \"20 20 20\"\n" "hud_keys_item_opacity \"0.99\"\n" "hud_keys_order \"1\"\n" "hud_keys_place \"screen\"\n" "hud_keys_pos_x \"0\"\n" "hud_keys_pos_y \"0\"\n" "hud_keys_scale \"2\"\n" "hud_keys_show \"0\"\n" */ "hud_mouserate_align_x \"left\"\n" "hud_mouserate_align_y \"bottom\"\n" "hud_mouserate_frame \"0\"\n" "hud_mouserate_frame_color \"0 0 0\"\n" "hud_mouserate_interval \"1\"\n" "hud_mouserate_item_opacity \"0.99\"\n" "hud_mouserate_order \"9\"\n" "hud_mouserate_place \"screen\"\n" "hud_mouserate_pos_x \"0\"\n" "hud_mouserate_pos_y \"0\"\n" "hud_mouserate_show \"1\"\n" "hud_mouserate_style \"0\"\n" "hud_mouserate_title \"1\"\n" "hud_mp3_time_align_x \"left\"\n" "hud_mp3_time_align_y \"bottom\"\n" "hud_mp3_time_frame \"0\"\n" "hud_mp3_time_frame_color \"0 0 0\"\n" "hud_mp3_time_item_opacity \"0.99\"\n" "hud_mp3_time_on_scoreboard \"0\"\n" "hud_mp3_time_order \"0\"\n" "hud_mp3_time_place \"top\"\n" "hud_mp3_time_pos_x \"0\"\n" "hud_mp3_time_pos_y \"0\"\n" "hud_mp3_time_show \"0\"\n" "hud_mp3_time_style \"0\"\n" "hud_mp3_title_align_x \"right\"\n" "hud_mp3_title_align_y \"bottom\"\n" "hud_mp3_title_frame \"0\"\n" "hud_mp3_title_frame_color \"0 0 0\"\n" "hud_mp3_title_height \"8\"\n" "hud_mp3_title_item_opacity \"0.99\"\n" "hud_mp3_title_on_scoreboard \"0\"\n" "hud_mp3_title_order \"0\"\n" "hud_mp3_title_place \"top\"\n" "hud_mp3_title_pos_x \"0\"\n" "hud_mp3_title_pos_y \"0\"\n" "hud_mp3_title_scroll \"1\"\n" "hud_mp3_title_scroll_delay \"0.5\"\n" "hud_mp3_title_show \"0\"\n" "hud_mp3_title_style \"2\"\n" "hud_mp3_title_width \"512\"\n" "hud_mp3_title_wordwrap \"0\"\n" "hud_net_align_x \"left\"\n" "hud_net_align_y \"center\"\n" "hud_net_frame \"0.2\"\n" "hud_net_frame_color \"0 0 0\"\n" "hud_net_item_opacity \"0.99\"\n" "hud_net_order \"7\"\n" "hud_net_period \"1\"\n" "hud_net_place \"top\"\n" "hud_net_pos_x \"0\"\n" "hud_net_pos_y \"0\"\n" "hud_net_show \"1\"\n" /* "hud_netgraph_align_x \"left\"\n" "hud_netgraph_align_y \"bottom\"\n" "hud_netgraph_alpha \"1\"\n" "hud_netgraph_frame \"0\"\n" "hud_netgraph_frame_color \"0 0 0\"\n" "hud_netgraph_full \"0\"\n" "hud_netgraph_height \"32\"\n" "hud_netgraph_inframes \"0\"\n" "hud_netgraph_item_opacity \"0.99\"\n" "hud_netgraph_lostscale \"1\"\n" "hud_netgraph_order \"0\"\n" "hud_netgraph_place \"top\"\n" "hud_netgraph_ploss \"1\"\n" "hud_netgraph_pos_x \"0\"\n" "hud_netgraph_pos_y \"0\"\n" "hud_netgraph_scale \"256\"\n" "hud_netgraph_show \"0\"\n" "hud_netgraph_swap_x \"0\"\n" "hud_netgraph_swap_y \"0\"\n" "hud_netgraph_width \"256\"\n" */ "hud_netproblem_align_x \"left\"\n" "hud_netproblem_align_y \"top\"\n" "hud_netproblem_frame \"0\"\n" "hud_netproblem_frame_color \"0 0 0\"\n" "hud_netproblem_item_opacity \"0.99\"\n" "hud_netproblem_order \"0\"\n" "hud_netproblem_place \"top\"\n" "hud_netproblem_pos_x \"0\"\n" "hud_netproblem_pos_y \"0\"\n" "hud_netproblem_scale \"1\"\n" "hud_netproblem_show \"0\"\n" /* "hud_notify_align_x \"left\"\n" "hud_notify_align_y \"top\"\n" "hud_notify_cols \"30\"\n" "hud_notify_frame \"0\"\n" "hud_notify_frame_color \"0 0 0\"\n" "hud_notify_item_opacity \"0.99\"\n" "hud_notify_order \"8\"\n" "hud_notify_place \"top\"\n" "hud_notify_pos_x \"0\"\n" "hud_notify_pos_y \"0\"\n" "hud_notify_rows \"4\"\n" "hud_notify_scale \"1\"\n" "hud_notify_show \"0\"\n" "hud_notify_time \"4\"\n" */ /* "hud_ownfrags_align_x \"center\"\n" "hud_ownfrags_align_y \"top\"\n" "hud_ownfrags_frame \"0\"\n" "hud_ownfrags_frame_color \"0 0 100\"\n" "hud_ownfrags_item_opacity \"0.99\"\n" "hud_ownfrags_order \"1\"\n" "hud_ownfrags_place \"screen\"\n" "hud_ownfrags_pos_x \"0\"\n" "hud_ownfrags_pos_y \"75\"\n" "hud_ownfrags_scale \"1\"\n" "hud_ownfrags_show \"1\"\n" "hud_ownfrags_timeout \"3\"\n" */ "hud_pent_align_x \"center\"\n" "hud_pent_align_y \"before\"\n" "hud_pent_frame \"0\"\n" "hud_pent_frame_color \"0 0 0\"\n" "hud_pent_item_opacity \"0.99\"\n" "hud_pent_order \"12\"\n" "hud_pent_place \"quad\"\n" "hud_pent_pos_x \"0\"\n" "hud_pent_pos_y \"0\"\n" "hud_pent_scale \"2\"\n" "hud_pent_show \"1\"\n" "hud_pent_style \"0\"\n" "hud_ping_align_x \"center\"\n" "hud_ping_align_y \"center\"\n" "hud_ping_blink \"0\"\n" "hud_ping_frame \"0\"\n" "hud_ping_frame_color \"0 0 0\"\n" "hud_ping_item_opacity \"0.99\"\n" "hud_ping_order \"9\"\n" "hud_ping_period \"1\"\n" "hud_ping_place \"clock\"\n" "hud_ping_pos_x \"1\"\n" "hud_ping_pos_y \"9\"\n" "hud_ping_show \"1\"\n" "hud_ping_show_dev \"0\"\n" "hud_ping_show_max \"0\"\n" "hud_ping_show_min \"0\"\n" "hud_ping_show_pl \"0\"\n" "hud_ping_style \"0\"\n" "hud_quad_align_x \"right\"\n" "hud_quad_align_y \"center\"\n" "hud_quad_frame \"0\"\n" "hud_quad_frame_color \"0 0 0\"\n" "hud_quad_item_opacity \"0.99\"\n" "hud_quad_order \"11\"\n" "hud_quad_place \"screen\"\n" "hud_quad_pos_x \"0\"\n" "hud_quad_pos_y \"0\"\n" "hud_quad_scale \"2\"\n" "hud_quad_show \"1\"\n" "hud_quad_style \"0\"\n" /* "hud_radar_align_x \"left\"\n" "hud_radar_align_y \"bottom\"\n" "hud_radar_autosize \"0\"\n" "hud_radar_fade_players \"1\"\n" "hud_radar_frame \"0\"\n" "hud_radar_frame_color \"0 0 0\"\n" "hud_radar_height \"25%\"\n" "hud_radar_highlight \"0\"\n" "hud_radar_highlight_color \"yellow\"\n" "hud_radar_item_opacity \"0.99\"\n" "hud_radar_itemfilter \"backpack quad pent armor mega\"\n" "hud_radar_onlytp \"0\"\n" "hud_radar_opacity \"0.5\"\n" "hud_radar_order \"0\"\n" "hud_radar_otherfilter \"projectiles gibs explosions shotgun\"\n" "hud_radar_place \"top\"\n" "hud_radar_player_size \"10\"\n" "hud_radar_pos_x \"0\"\n" "hud_radar_pos_y \"0\"\n" "hud_radar_show \"0\"\n" "hud_radar_show_height \"1\"\n" "hud_radar_show_hold \"0\"\n" "hud_radar_show_names \"0\"\n" "hud_radar_show_powerups \"1\"\n" "hud_radar_show_stats \"1\"\n" "hud_radar_weaponfilter \"gl rl lg\"\n" "hud_radar_width \"30%\"\n" */ "hud_ring_align_x \"center\"\n" "hud_ring_align_y \"after\"\n" "hud_ring_frame \"0\"\n" "hud_ring_frame_color \"0 0 0\"\n" "hud_ring_item_opacity \"0.99\"\n" "hud_ring_order \"12\"\n" "hud_ring_place \"quad\"\n" "hud_ring_pos_x \"0\"\n" "hud_ring_pos_y \"0\"\n" "hud_ring_scale \"2\"\n" "hud_ring_show \"1\"\n" "hud_ring_style \"0\"\n" "hud_score_bar_align_x \"center\"\n" "hud_score_bar_align_y \"console\"\n" "hud_score_bar_format_big \"%t:%e:%Z\"\n" "hud_score_bar_format_small \"&c69f%T&r:%t &cf10%E&r:%e $[%D$]\"\n" "hud_score_bar_frame \"0.5\"\n" "hud_score_bar_frame_color \"0 0 0\"\n" "hud_score_bar_item_opacity \"0.99\"\n" "hud_score_bar_order \"0\"\n" "hud_score_bar_place \"screen\"\n" "hud_score_bar_pos_x \"0\"\n" "hud_score_bar_pos_y \"0\"\n" "hud_score_bar_scale \"1\"\n" "hud_score_bar_show \"0\"\n" "hud_score_bar_style \"0\"\n" "hud_score_difference_align \"right\"\n" "hud_score_difference_align_x \"after\"\n" "hud_score_difference_align_y \"bottom\"\n" "hud_score_difference_colorize \"1\"\n" "hud_score_difference_digits \"0\"\n" "hud_score_difference_frame \"0.5\"\n" "hud_score_difference_frame_color \"0 0 0\"\n" "hud_score_difference_item_opacity \"0.99\"\n" "hud_score_difference_order \"2\"\n" "hud_score_difference_place \"score_enemy\"\n" "hud_score_difference_pos_x \"0\"\n" "hud_score_difference_pos_y \"0\"\n" "hud_score_difference_scale \"1\"\n" "hud_score_difference_show \"0\"\n" "hud_score_difference_style \"0\"\n" "hud_score_enemy_align \"right\"\n" "hud_score_enemy_align_x \"center\"\n" "hud_score_enemy_align_y \"before\"\n" "hud_score_enemy_colorize \"0\"\n" "hud_score_enemy_digits \"3\"\n" "hud_score_enemy_frame \"0.25\"\n" "hud_score_enemy_frame_color \"200 75 75\"\n" "hud_score_enemy_item_opacity \"0.99\"\n" "hud_score_enemy_order \"1\"\n" "hud_score_enemy_place \"score_team\"\n" "hud_score_enemy_pos_x \"0\"\n" "hud_score_enemy_pos_y \"-1\"\n" "hud_score_enemy_scale \"0.5\"\n" "hud_score_enemy_show \"1\"\n" "hud_score_enemy_style \"2\"\n" "hud_score_position_align \"right\"\n" "hud_score_position_align_x \"after\"\n" "hud_score_position_align_y \"bottom\"\n" "hud_score_position_colorize \"1\"\n" "hud_score_position_digits \"0\"\n" "hud_score_position_frame \"0.5\"\n" "hud_score_position_frame_color \"0 0 0\"\n" "hud_score_position_item_opacity \"0.99\"\n" "hud_score_position_order \"3\"\n" "hud_score_position_place \"score_difference\"\n" "hud_score_position_pos_x \"0\"\n" "hud_score_position_pos_y \"0\"\n" "hud_score_position_scale \"1\"\n" "hud_score_position_show \"0\"\n" "hud_score_position_style \"0\"\n" "hud_score_team_align \"right\"\n" "hud_score_team_align_x \"right\"\n" "hud_score_team_align_y \"bottom\"\n" "hud_score_team_colorize \"0\"\n" "hud_score_team_digits \"3\"\n" "hud_score_team_frame \"0.25\"\n" "hud_score_team_frame_color \"75 75 200\"\n" "hud_score_team_item_opacity \"0.99\"\n" "hud_score_team_order \"0\"\n" "hud_score_team_place \"screen\"\n" "hud_score_team_pos_x \"-5\"\n" "hud_score_team_pos_y \"-5\"\n" "hud_score_team_scale \"0.5\"\n" "hud_score_team_show \"1\"\n" "hud_score_team_style \"2\"\n" "hud_sigil1_align_x \"center\"\n" "hud_sigil1_align_y \"bottom\"\n" "hud_sigil1_frame \"0\"\n" "hud_sigil1_frame_color \"0 0 0\"\n" "hud_sigil1_item_opacity \"0.99\"\n" "hud_sigil1_order \"0\"\n" "hud_sigil1_place \"screen\"\n" "hud_sigil1_pos_x \"-160\"\n" "hud_sigil1_pos_y \"-80\"\n" "hud_sigil1_scale \"2\"\n" "hud_sigil1_show \"1\"\n" "hud_sigil1_style \"0\"\n" "hud_sigil2_align_x \"center\"\n" "hud_sigil2_align_y \"bottom\"\n" "hud_sigil2_frame \"0\"\n" "hud_sigil2_frame_color \"0 0 0\"\n" "hud_sigil2_item_opacity \"0.99\"\n" "hud_sigil2_order \"1\"\n" "hud_sigil2_place \"sigil1\"\n" "hud_sigil2_pos_x \"17\"\n" "hud_sigil2_pos_y \"0\"\n" "hud_sigil2_scale \"2\"\n" "hud_sigil2_show \"1\"\n" "hud_sigil2_style \"0\"\n" "hud_sigil3_align_x \"center\"\n" "hud_sigil3_align_y \"bottom\"\n" "hud_sigil3_frame \"0\"\n" "hud_sigil3_frame_color \"0 0 0\"\n" "hud_sigil3_item_opacity \"0.99\"\n" "hud_sigil3_order \"2\"\n" "hud_sigil3_place \"sigil2\"\n" "hud_sigil3_pos_x \"17\"\n" "hud_sigil3_pos_y \"0\"\n" "hud_sigil3_scale \"2\"\n" "hud_sigil3_show \"1\"\n" "hud_sigil3_style \"0\"\n" "hud_sigil4_align_x \"center\"\n" "hud_sigil4_align_y \"bottom\"\n" "hud_sigil4_frame \"0\"\n" "hud_sigil4_frame_color \"0 0 0\"\n" "hud_sigil4_item_opacity \"0.99\"\n" "hud_sigil4_order \"3\"\n" "hud_sigil4_place \"sigil3\"\n" "hud_sigil4_pos_x \"17\"\n" "hud_sigil4_pos_y \"0\"\n" "hud_sigil4_scale \"2\"\n" "hud_sigil4_show \"1\"\n" "hud_sigil4_style \"0\"\n" "hud_speed2_align_x \"center\"\n" "hud_speed2_align_y \"bottom\"\n" "hud_speed2_color_fast \"72\"\n" "hud_speed2_color_fastest \"216\"\n" "hud_speed2_color_insane \"229\"\n" "hud_speed2_color_normal \"100\"\n" "hud_speed2_color_stopped \"52\"\n" "hud_speed2_frame \"0\"\n" "hud_speed2_frame_color \"0 0 0\"\n" "hud_speed2_item_opacity \"0.99\"\n" "hud_speed2_opacity \"1.0\"\n" "hud_speed2_order \"7\"\n" "hud_speed2_orientation \"0\"\n" "hud_speed2_place \"top\"\n" "hud_speed2_pos_x \"0\"\n" "hud_speed2_pos_y \"0\"\n" "hud_speed2_radius \"50.0\"\n" "hud_speed2_show \"0\"\n" "hud_speed2_wrapspeed \"500\"\n" "hud_speed2_xyz \"0\"\n" /* "hud_speed_align_x \"center\"\n" "hud_speed_align_y \"center\"\n" "hud_speed_color_fast \"72\"\n" "hud_speed_color_fastest \"216\"\n" "hud_speed_color_insane \"229\"\n" "hud_speed_color_normal \"100\"\n" "hud_speed_color_stopped \"52\"\n" "hud_speed_frame \"0\"\n" "hud_speed_frame_color \"0 0 0\"\n" "hud_speed_height \"15\"\n" "hud_speed_item_opacity \"0\"\n" "hud_speed_opacity \"1.0\"\n" "hud_speed_order \"7\"\n" "hud_speed_place \"screen\"\n" "hud_speed_pos_x \"100\"\n" "hud_speed_pos_y \"0\"\n" "hud_speed_show \"0\"\n" "hud_speed_style \"0\"\n" "hud_speed_text_align \"3\"\n" "hud_speed_tick_spacing \"0.2\"\n" "hud_speed_vertical \"0\"\n" "hud_speed_vertical_text \"1\"\n" "hud_speed_width \"160\"\n" "hud_speed_xyz \"0\"\n" */ "hud_suit_align_x \"center\"\n" "hud_suit_align_y \"after\"\n" "hud_suit_frame \"0\"\n" "hud_suit_frame_color \"0 0 0\"\n" "hud_suit_item_opacity \"0.99\"\n" "hud_suit_order \"13\"\n" "hud_suit_place \"ring\"\n" "hud_suit_pos_x \"0\"\n" "hud_suit_pos_y \"0\"\n" "hud_suit_scale \"2\"\n" "hud_suit_show \"1\"\n" "hud_suit_style \"0\"\n" "hud_teamfrags_align_x \"center\"\n" "hud_teamfrags_align_y \"bottom\"\n" "hud_teamfrags_bignum \"0\"\n" "hud_teamfrags_cell_height \"8\"\n" "hud_teamfrags_cell_width \"35\"\n" "hud_teamfrags_colors_alpha \"1.0\"\n" "hud_teamfrags_cols \"2\"\n" "hud_teamfrags_extra_spec_info \"0\"\n" "hud_teamfrags_fliptext \"1\"\n" "hud_teamfrags_frame \"0\"\n" "hud_teamfrags_frame_color \"0 0 0\"\n" "hud_teamfrags_item_opacity \"0.99\"\n" "hud_teamfrags_maxname \"16\"\n" "hud_teamfrags_onlytp \"0\"\n" "hud_teamfrags_order \"9\"\n" "hud_teamfrags_padtext \"1\"\n" "hud_teamfrags_place \"gameclock\"\n" "hud_teamfrags_pos_x \"0\"\n" "hud_teamfrags_pos_y \"11\"\n" "hud_teamfrags_rows \"1\"\n" "hud_teamfrags_show \"0\"\n" "hud_teamfrags_shownames \"0\"\n" "hud_teamfrags_space_x \"0\"\n" "hud_teamfrags_space_y \"1\"\n" "hud_teamfrags_strip \"1\"\n" "hud_teamfrags_style \"3\"\n" "hud_teamfrags_vertical \"0\"\n" /* "hud_teamholdbar_align_x \"left\"\n" "hud_teamholdbar_align_y \"bottom\"\n" "hud_teamholdbar_frame \"0\"\n" "hud_teamholdbar_frame_color \"0 0 0\"\n" "hud_teamholdbar_height \"8\"\n" "hud_teamholdbar_item_opacity \"0.99\"\n" "hud_teamholdbar_onlytp \"0\"\n" "hud_teamholdbar_opacity \"0.8\"\n" "hud_teamholdbar_order \"0\"\n" "hud_teamholdbar_place \"top\"\n" "hud_teamholdbar_pos_x \"0\"\n" "hud_teamholdbar_pos_y \"0\"\n" "hud_teamholdbar_show \"0\"\n" "hud_teamholdbar_show_text \"1\"\n" "hud_teamholdbar_vertical \"0\"\n" "hud_teamholdbar_vertical_text \"0\"\n" "hud_teamholdbar_width \"200\"\n" */ /* "hud_teamholdinfo_align_x \"left\"\n" "hud_teamholdinfo_align_y \"bottom\"\n" "hud_teamholdinfo_frame \"0\"\n" "hud_teamholdinfo_frame_color \"0 0 0\"\n" "hud_teamholdinfo_height \"8\"\n" "hud_teamholdinfo_item_opacity \"0.99\"\n" "hud_teamholdinfo_itemfilter \"quad ra ya ga mega pent rl quad\"\n" "hud_teamholdinfo_onlytp \"0\"\n" "hud_teamholdinfo_opacity \"0.8\"\n" "hud_teamholdinfo_order \"0\"\n" "hud_teamholdinfo_place \"top\"\n" "hud_teamholdinfo_pos_x \"0\"\n" "hud_teamholdinfo_pos_y \"0\"\n" "hud_teamholdinfo_show \"0\"\n" "hud_teamholdinfo_style \"1\"\n" "hud_teamholdinfo_width \"200\"\n" */ /* "hud_teaminfo_align_right \"0\"\n" "hud_teaminfo_align_x \"right\"\n" "hud_teaminfo_align_y \"center\"\n" "hud_teaminfo_armor_style \"3\"\n" "hud_teaminfo_frame \"0.25\"\n" "hud_teaminfo_frame_color \"20 20 20\"\n" "hud_teaminfo_item_opacity \"1.0\"\n" "hud_teaminfo_layout \"%p%n $x10%l$x11 %a/%H %w\"\n" "hud_teaminfo_loc_width \"5\"\n" "hud_teaminfo_low_health \"25\"\n" "hud_teaminfo_name_width \"6\"\n" "hud_teaminfo_order \"0\"\n" "hud_teaminfo_place \"\"\n" "hud_teaminfo_pos_x \"0\"\n" "hud_teaminfo_pos_y \"45\"\n" "hud_teaminfo_powerup_style \"1\"\n" "hud_teaminfo_scale \"1\"\n" "hud_teaminfo_show \"1\"\n" "hud_teaminfo_show_enemies \"0\"\n" "hud_teaminfo_show_self \"1\"\n" "hud_teaminfo_weapon_style \"0\"\n" */ "hud_tp_need \"0\"\n" "hud_tracking_align_x \"center\"\n" "hud_tracking_align_y \"bottom\"\n" "hud_tracking_format \"^mTracking:^m %t %n, ^mJUMP^m for next\"\n" "hud_tracking_frame \"0\"\n" "hud_tracking_frame_color \"0 0 0\"\n" "hud_tracking_item_opacity \"0.99\"\n" "hud_tracking_order \"31\"\n" "hud_tracking_place \"top\"\n" "hud_tracking_pos_x \"0\"\n" "hud_tracking_pos_y \"-25\"\n" "hud_tracking_scale \"1\"\n" "hud_tracking_show \"1\"\n" "hud_vidlag_align_x \"right\"\n" "hud_vidlag_align_y \"top\"\n" "hud_vidlag_frame \"0\"\n" "hud_vidlag_frame_color \"0 0 0\"\n" "hud_vidlag_item_opacity \"0.99\"\n" "hud_vidlag_order \"9\"\n" "hud_vidlag_place \"top\"\n" "hud_vidlag_pos_x \"0\"\n" "hud_vidlag_pos_y \"0\"\n" "hud_vidlag_show \"0\"\n" "hud_vidlag_style \"0\"\n" ; ================================================ FILE: plugins/ezhud/ezquakeisms.c ================================================ #include "ezquakeisms.h" #include "hud.h" #include "hud_editor.h" #ifdef FTEENGINE #define Plug_Init Plug_EZHud_Init #endif plug2dfuncs_t *drawfuncs; plugclientfuncs_t *clientfuncs; plugfsfuncs_t *filefuncs; pluginputfuncs_t *inputfuncs; struct ezcl_s cl; struct ezcls_s cls; struct ezvid_s vid; int sb_lines; float scr_con_current; int sb_showteamscores; int sb_showscores; int host_screenupdatecount; float alphamul; cvar_t *scr_newHud; void HUD_InitSbarImages(void); static void QDECL EZHud_UpdateVideo(int width, int height, qboolean restarted) { vid.width = width; vid.height = height; if (restarted) HUD_InitSbarImages(); } char *Cmd_Argv(int arg) { static char buf[4][128]; if (arg >= 4) return ""; cmdfuncs->Argv(arg, buf[arg], sizeof(buf[arg])); return buf[arg]; } float infofloat(char *info, char *findkey, float def); void Draw_SetOverallAlpha(float a) { alphamul = a; } void Draw_AlphaFillRGB(float x, float y, float w, float h, qbyte r, qbyte g, qbyte b, qbyte a) { drawfuncs->Colour4f(r/255.0, g/255.0, b/255.0, a/255.0 * alphamul); drawfuncs->Fill(x, y, w, h); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_Fill(float x, float y, float w, float h, qbyte pal) { drawfuncs->Colourpa(pal, alphamul); drawfuncs->Fill(x, y, w, h); drawfuncs->Colour4f(1, 1, 1, 1); } const char *ColorNameToRGBString (const char *newval) { return newval; } byte *StringToRGB(const char *str) { static byte rgba[4]; int i; for (i = 0; i < 4; i++) { while(*str && *str <= ' ') str++; if (!*str) rgba[i] = 255; else rgba[i] = strtoul(str, (char**)&str, 0); } return rgba; } void Draw_TextBox (int x, int y, int width, int lines) { } char *TP_LocationName (const vec3_t location) { static char locname[256]; clientfuncs->GetLocationName(location, locname, sizeof(locname)); return locname; } void Draw_SPic(float x, float y, mpic_t *pic, float scale) { qhandle_t image = (intptr_t)pic; float w=64, h=64; drawfuncs->ImageSize(image, &w, &h); drawfuncs->Image(x, y, w*scale, h*scale, 0, 0, 1, 1, image); } void Draw_SSubPic(float x, float y, mpic_t *pic, float s1, float t1, float s2, float t2, float scale) { qhandle_t image = (intptr_t)pic; float w=64, h=64; drawfuncs->ImageSize(image, &w, &h); drawfuncs->Image(x, y, (s2-s1)*scale, (t2-t1)*scale, s1/w, t1/h, s2/w, t2/h, image); } void Draw_EZString(float x, float y, char *str, float scale, qboolean red) { unsigned int flags = 0; if (red) flags |= 1; drawfuncs->StringH(x, y, scale, flags, str); } #define Draw_STransPic Draw_SPic void Draw_Character(float x, float y, unsigned int ch) { drawfuncs->Character(x, y, 0xe000|ch); } void Draw_SCharacter(float x, float y, unsigned int ch, float scale) { drawfuncs->CharacterH(x, y, 8*scale, 0, 0xe000|ch); } void SCR_DrawWadString(float x, float y, float scale, char *str) { drawfuncs->String(x, y, str); //FIXME } void Draw_SAlphaSubPic2(float x, float y, mpic_t *pic, float s1, float t1, float s2, float t2, float sw, float sh, float alpha) { qhandle_t image = (intptr_t)pic; float w=64, h=64; drawfuncs->ImageSize(image, &w, &h); drawfuncs->Colour4f(1, 1, 1, alpha * alphamul); drawfuncs->Image(x, y, (s2-s1)*sw, (t2-t1)*sh, s1/w, t1/h, s2/w, t2/h, image); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_AlphaFill(float x, float y, float w, float h, unsigned int pal, float alpha) { if (pal >= 256) drawfuncs->Colour4f(((pal>>16)&0xff)/255.0, ((pal>>8)&0xff)/255.0, ((pal>>0)&0xff)/255.0, alpha * alphamul); else drawfuncs->Colourpa(pal, alpha * alphamul); drawfuncs->Fill(x, y, w, h); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_AlphaPic(float x, float y, mpic_t *pic, float alpha) { qhandle_t image = (intptr_t)pic; float w, h; drawfuncs->ImageSize(image, &w, &h); drawfuncs->Colour4f(1, 1, 1, alpha * alphamul); drawfuncs->Image(x, y, w, h, 0, 0, 1, 1, image); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_AlphaSubPic(float x, float y, mpic_t *pic, float s1, float t1, float s2, float t2, float alpha) { qhandle_t image = (intptr_t)pic; float w, h; drawfuncs->ImageSize(image, &w, &h); drawfuncs->Colour4f(1, 1, 1, alpha * alphamul); drawfuncs->Image(x, y, s2-s1, t2-t1, s1/w, t1/h, s2/w, t2/h, image); drawfuncs->Colour4f(1, 1, 1, 1); } void SCR_HUD_DrawBar(int direction, int value, float max_value, float *rgba, int x, int y, int width, int height) { int amount; if(direction >= 2) // top-down amount = Q_rint(fabs((height * value) / max_value)); else// left-right amount = Q_rint(fabs((width * value) / max_value)); drawfuncs->Colour4f(rgba[0]/255.0, rgba[1]/255.0, rgba[2]/255.0, rgba[3]/255.0 * alphamul); if(direction == 0) // left->right drawfuncs->Fill(x, y, amount, height); else if (direction == 1) // right->left drawfuncs->Fill(x + width - amount, y, amount, height); else if (direction == 2) // down -> up drawfuncs->Fill(x, y + height - amount, width, amount); else // up -> down drawfuncs->Fill(x, y, width, amount); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_Polygon(int x, int y, vec3_t *vertices, int num_vertices, qbool fill, byte r, byte g, byte b, byte a) { drawfuncs->Colour4f(r/255.0, g/255.0, b/255.0, a/255.0 * alphamul); // drawfuncs->Line(x1, y1, x2, y1); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_ColoredString3(float x, float y, const char *str, clrinfo_t *clr, int huh, int wut) { drawfuncs->Colour4f(clr->c[0]/255.0, clr->c[1]/255.0, clr->c[2]/255.0, clr->c[3]/255.0 * alphamul); drawfuncs->String(x, y, str); drawfuncs->Colour4f(1, 1, 1, 1); } void UI_PrintTextBlock(float x, float y, float w, float h, char *str, int flags) { } void Draw_AlphaRectangleRGB(int x, int y, int w, int h, int foo, int bar, byte r, byte g, byte b, byte a) { float x1 = x; float x2 = x+w; float y1 = y; float y2 = y+h; drawfuncs->Colour4f(r/255.0, g/255.0, b/255.0, a/255.0 * alphamul); drawfuncs->Line(x1, y1, x2, y1); drawfuncs->Line(x2, y1, x2, y2); drawfuncs->Line(x1, y2, x2, y2); drawfuncs->Line(x1, y1, x1, y2); drawfuncs->Colour4f(1, 1, 1, 1); } void Draw_AlphaLineRGB(float x1, float y1, float x2, float y2, float width, byte r, byte g, byte b, byte a) { drawfuncs->Colour4f(r/255.0, g/255.0, b/255.0, a/255.0 * alphamul); drawfuncs->Line(x1, y1, x2, y2); drawfuncs->Colour4f(1, 1, 1, 1); } mpic_t *Draw_CachePicSafe(const char *name, qbool crash, qbool ignorewad) { if (!*name) return NULL; return (mpic_t*)(qintptr_t)drawfuncs->LoadImage(name); } mpic_t *Draw_CacheWadPic(const char *name) { char ftename[MAX_QPATH]; Q_snprintf(ftename, sizeof(ftename), "gfx/%s", name); return (mpic_t*)(qintptr_t)drawfuncs->LoadImage(ftename); } mpic_t *SCR_LoadCursorImage(char *cursorimage) { return Draw_CachePicSafe(cursorimage, false, true); } unsigned int Sbar_ColorForMap (unsigned int m) { if (m >= 16) return m; m = (m < 0) ? 0 : ((m > 13) ? 13 : m); m *= 16; return m < 128 ? m + 8 : m + 8; } int Sbar_TopColor(player_info_t *pi) { return Sbar_ColorForMap(pi->topcolour); } int Sbar_BottomColor(player_info_t *pi) { return Sbar_ColorForMap(pi->bottomcolour); } int dehex(char nib) { if (nib >= '0' && nib <= '9') return nib - '0'; if (nib >= 'a' && nib <= 'f') return nib - 'a' + 10; if (nib >= 'A' && nib <= 'F') return nib - 'A' + 10; return 0; } char *TP_ParseFunChars(char *str) { static char resultbuf[1024]; char *out = resultbuf, *end = resultbuf+sizeof(resultbuf)-1; while (out < end) { if (str[0] == '$' && str[1] == 'x' && str[2] && str[3]) { *out++ = (dehex(str[2]) << 4) | dehex(str[3]); str+=4; } else if (str[0] == '$') { int c = 0; switch (str[1]) { case '\\': c = 0x0D; break; case ':': c = 0x0A; break; case '[': c = 0x10; break; case ']': c = 0x11; break; case 'G': c = 0x86; break; case 'R': c = 0x87; break; case 'Y': c = 0x88; break; case 'B': c = 0x89; break; case '(': c = 0x80; break; case '=': c = 0x81; break; case ')': c = 0x82; break; case 'a': c = 0x83; break; case '<': c = 0x1d; break; case '-': c = 0x1e; break; case '>': c = 0x1f; break; case ',': c = 0x1c; break; case '.': c = 0x9c; break; case 'b': c = 0x8b; break; case 'c': case 'd': c = 0x8d; break; case '$': c = '$'; break; case '^': c = '^'; break; case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': c = str[1] -'0' + 0x12;break; } if (c) { *out++ = c; str++; } str++; } else if (*str) *out++ = *str++; else break; } *out = 0; return resultbuf; } char *TP_ItemName(unsigned int itbit) { cvar_t *var = NULL; switch (itbit) { #define ITEMNAME(it, nam) case it: var = cvarfuncs->GetNVFDG("tp_name_"#nam, #nam, 0, NULL, "Item Names"); break ITEMNAME(IT_SHOTGUN, sg); ITEMNAME(IT_SUPER_SHOTGUN, ssg); ITEMNAME(IT_NAILGUN, ng); ITEMNAME(IT_SUPER_NAILGUN, sng); ITEMNAME(IT_GRENADE_LAUNCHER, gl); ITEMNAME(IT_ROCKET_LAUNCHER, rl); ITEMNAME(IT_LIGHTNING, lg); // ITEMNAME(IT_SUPER_LIGHTNING, ???); ITEMNAME(IT_SHELLS, shells); ITEMNAME(IT_NAILS, nails); ITEMNAME(IT_ROCKETS, rockets); ITEMNAME(IT_CELLS, cells); ITEMNAME(IT_AXE, axe); ITEMNAME(IT_ARMOR1, ga); ITEMNAME(IT_ARMOR2, ya); ITEMNAME(IT_ARMOR3, ra); ITEMNAME(IT_SUPERHEALTH, mh); // ITEMNAME(IT_KEY1, ); // ITEMNAME(IT_KEY2, ); ITEMNAME(IT_INVISIBILITY, ring); ITEMNAME(IT_INVULNERABILITY, pent); ITEMNAME(IT_SUIT, suit); ITEMNAME(IT_QUAD, quad); // ITEMNAME(IT_SIGIL1, ); // ITEMNAME(IT_SIGIL2, ); // ITEMNAME(IT_SIGIL3, ); // ITEMNAME(IT_SIGIL4, ); } if (var) return var->string; return va("it%#x", itbit); } void Replace_In_String(char *src, size_t strsize, char leadchar, int patterns, ...) { char orig[1024]; char *out, *outstop; va_list ap; int i; strlcpy(orig, src, sizeof(orig)); out = src; outstop = out + strsize-1; src = orig; while(*src) { if (out == outstop) break; if (*src != leadchar) *out++ = *src++; else if (*++src == leadchar) *out++ = leadchar; else { va_start(ap, patterns); for (i = 0; i < patterns; i++) { const char *arg = va_arg(ap, const char *); const char *val = va_arg(ap, const char *); size_t alen = strlen(arg); if (!strncmp(src, arg, strlen(arg))) { strlcpy(out, val, (outstop-out)+1); out += strlen(out); src += alen; break; } } if (i == patterns) { strlcpy(out, "unknown", (outstop-out)+1); out += strlen(out); } va_end(ap); } } *out = 0; } int SCR_GetClockStringWidth(const char *s, qbool big, float scale) { int w = 0; if (big) { while(*s) w += ((*s++==':')?16:24); } else w = strlen(s) * 8; return w * scale; } int SCR_GetClockStringHeight(qbool big, float scale) { return (big?24:8)*scale; } char *SecondsToMinutesString(int print_time, char *buffer, size_t buffersize) { int tens_minutes, minutes, tens_seconds, seconds; tens_minutes = fmod (print_time / 600, 6); minutes = fmod (print_time / 60, 10); tens_seconds = fmod (print_time / 10, 6); seconds = fmod (print_time, 10); snprintf (buffer, buffersize, "%i%i:%i%i", tens_minutes, minutes, tens_seconds, seconds); return buffer; } char *SCR_GetGameTime(int t, char *buffer, size_t buffersize) { float timelimit; timelimit = (t == TIMETYPE_GAMECLOCKINV) ? 60 * infofloat(cl.serverinfo, "timelimit", 0) + 1: 0; if (cl.countdown || cl.standby) SecondsToMinutesString(timelimit, buffer, buffersize); else SecondsToMinutesString((int) fabs(timelimit - (cl.time - cl.matchstart)), buffer, buffersize); return buffer; } const char* SCR_GetTimeString(int timetype, const char *format) { static char buffer[256]; switch(timetype) { case TIMETYPE_CLOCK: { time_t t; struct tm *ptm; time (&t); ptm = localtime (&t); if (!ptm) return "-:-"; if (!strftime(buffer, sizeof(buffer)-1, format, ptm)) return "-:-"; return buffer; } case TIMETYPE_GAMECLOCK: case TIMETYPE_GAMECLOCKINV: return SCR_GetGameTime(timetype, buffer, sizeof(buffer)); case TIMETYPE_DEMOCLOCK: return SecondsToMinutesString(infofloat(cl.serverinfo, "demotime", 0), buffer, sizeof(buffer)); default: return "01234"; } } void SCR_DrawBigClock(int x, int y, int style, int blink, float scale, const char *t) { extern mpic_t *sb_nums[2][11]; extern mpic_t *sb_colon/*, *sb_slash*/; qbool lblink = (int)(cls.realtime*2) & 1; if (style > 1) style = 1; if (style < 0) style = 0; while (*t) { if (*t >= '0' && *t <= '9') { Draw_STransPic(x, y, sb_nums[style][*t-'0'], scale); x += 24*scale; } else if (*t == ':') { if (lblink || !blink) Draw_STransPic (x, y, sb_colon, scale); x += 16*scale; } else { Draw_SCharacter(x, y, *t+(style?128:0), 3*scale); x += 24*scale; } t++; } } void SCR_DrawSmallClock(int x, int y, int style, int blink, float scale, const char *t) { qbool lblink = (int)(cls.realtime*2) & 1; int c; if (style > 3) style = 3; if (style < 0) style = 0; while (*t) { c = (int) *t; if (c >= '0' && c <= '9') { if (style == 1) c += 128; else if (style == 2 || style == 3) c -= 30; } else if (c == ':') { if (style == 1 || style == 3) c += 128; if (lblink || !blink) ; else c = ' '; } Draw_SCharacter(x, y, c, scale); x+= 8*scale; t++; } } #include "builtin_huds.h" void EZHud_UseNquake_f(void) { const char *hudstr = builtin_hud_nquake; cmdfuncs->AddText(hudstr, true); } int IN_BestWeapon(void) { return 0; } qbool VID_VSyncIsOn(void){return false;} double vid_vsync_lag; vrect_t scr_vrect; void EZHud_Tick(double realtime, double gametime) { cls.realtime = realtime; cl.time = gametime; } char *findinfo(char *info, char *findkey) { int kl = strlen(findkey); char *key, *value; while(*info) { key = strchr(info, '\\'); if (!key) break; key++; value = strchr(key, '\\'); if (!value) break; info = value+1; if (!strncmp(key, findkey, kl) && key[kl] == '\\') return info; } return NULL; } char *infostring(char *info, char *findkey, char *buffer, size_t bufsize) { char *value = findinfo(info, findkey); char *end; if (value) { end = strchr(value, '\\'); if (!end) end = value + strlen(value); bufsize--; if (bufsize > (end - value)) bufsize = (end - value); memcpy(buffer, value, bufsize); buffer[bufsize] = 0; return buffer; } *buffer = 0; return buffer; } float infofloat(char *info, char *findkey, float def) { char *value = findinfo(info, findkey); if (value) return atof(value); return def; } int EZHud_Draw(int seat, float viewx, float viewy, float viewwidth, float viewheight, int showscores) { char serverinfo[4096]; char val[64]; int i; static float lasttime, lasttime_min = 99999; static int framecount; static float oldtime; if (cls.realtime - lasttime > 1) { cls.fps = framecount/(cls.realtime - lasttime); lasttime = cls.realtime; framecount = 0; if (cls.realtime - lasttime_min > 30) { cls.min_fps = cls.fps; lasttime_min = cls.realtime; } else if (cls.min_fps > cls.fps) cls.min_fps = cls.fps; } if (!seat) { cls.frametime = cls.realtime - oldtime; framecount++; oldtime = cls.realtime; } cl.splitscreenview = seat; scr_vrect.x = viewx; scr_vrect.y = viewy; scr_vrect.width = viewwidth; scr_vrect.height = viewheight; sb_showscores = showscores & 1; sb_showteamscores = showscores & 2; clientfuncs->GetStats(0, cl.stats, sizeof(cl.stats)/sizeof(cl.stats[0])); for (i = 0; i < 32; i++) clientfuncs->GetPlayerInfo(i, &cl.players[i]); clientfuncs->GetLocalPlayerNumbers(cl.splitscreenview, 1, &cl.playernum, &cl.tracknum); clientfuncs->GetServerInfoRaw(cl.serverinfo, sizeof(serverinfo)); cl.deathmatch = infofloat(cl.serverinfo, "deathmatch", 0); cl.teamplay = infofloat(cl.serverinfo, "teamplay", 0); cl.intermission = infofloat(cl.serverinfo, "intermission", 0); cl.spectator = (cl.playernum>=32)||cl.players[cl.playernum].spectator; infostring(cl.serverinfo, "status", val, sizeof(val)); cl.standby = !strcmp(val, "standby"); cl.countdown = !strcmp(val, "countdown"); cl.matchstart = infofloat(cl.serverinfo, "matchstart", 0); cls.state = ca_active; infostring(cl.serverinfo, "demotype", val, sizeof(val)); cls.mvdplayback = !strcmp(val, "mvd"); cls.demoplayback = strcmp(val, ""); { static cvar_t *pscr_viewsize = NULL; int size; if (!pscr_viewsize) pscr_viewsize = cvarfuncs->GetNVFDG("viewsize", "100", 0, NULL, NULL); size = cl.intermission ? 120 : pscr_viewsize->value; if (size >= 120) sb_lines = 0; // no status bar at all else if (size >= 110) sb_lines = 24; // no inventory else sb_lines = 24 + 16 + 8; } clientfuncs->GetPredInfo(seat, cl.simvel); //cl.faceanimtime //cl.item_gettime //cls.state; //cls.min_fps; //cls.fps; ////cls.realtime; ////cls.trueframetime; host_screenupdatecount++; HUD_Draw(); return true; } unsigned int keydown[K_MAX]; float cursor_x; float cursor_y; float mouse_x; float mouse_y; mpic_t *scr_cursor_icon = NULL; qboolean QDECL EZHud_MenuEvent(int eventtype, int keyparam, int unicodeparm, float mousecursor_x, float mousecursor_y, float vidwidth, float vidheight) { mouse_x += mousecursor_x - cursor_x; //FIXME: the hud editor should NOT need this sort of thing mouse_y += mousecursor_y - cursor_y; cursor_x = mousecursor_x; cursor_y = mousecursor_y; HUD_Editor_MouseEvent(cursor_x, cursor_y); switch(eventtype) { case 0: //draw host_screenupdatecount++; HUD_Draw(); HUD_Editor_Draw(); mouse_x = 0; mouse_y = 0; break; case 1: if (keyparam < K_MAX) keydown[keyparam] = true; HUD_Editor_Key(keyparam, 0, true); break; case 2: if (keyparam < K_MAX) keydown[keyparam] = false; HUD_Editor_Key(keyparam, 0, false); break; } return 1; } qboolean Plug_Init(void) { drawfuncs = plugfuncs->GetEngineInterface(plug2dfuncs_name, sizeof(*drawfuncs)); clientfuncs = plugfuncs->GetEngineInterface(plugclientfuncs_name, sizeof(*clientfuncs)); filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs)); inputfuncs = plugfuncs->GetEngineInterface(pluginputfuncs_name, sizeof(*inputfuncs)); plugfuncs->ExportFunction("UpdateVideo", EZHud_UpdateVideo); if (cvarfuncs && drawfuncs && clientfuncs && filefuncs && inputfuncs && plugfuncs->ExportFunction("SbarBase", EZHud_Draw) && plugfuncs->ExportFunction("MenuEvent", EZHud_MenuEvent) && plugfuncs->ExportFunction("Tick", EZHud_Tick)) { Cmd_AddCommand("ezhud_nquake", EZHud_UseNquake_f); HUD_Init(); HUD_Editor_Init(); return true; } Con_Printf("EZHud: Unable to initiailise\n"); return false; } ================================================ FILE: plugins/ezhud/ezquakeisms.h ================================================ //ezquake likes this #ifndef FTEPLUGIN #include "quakedef.h" #define FTEENGINE //we're getting statically linked. lucky us. #define FTEPLUGIN #endif #include "../plugin.h" #include #include #ifdef FTEENGINE #define drawfuncs ezhud_drawfuncs #define filefuncs ezhud_filefuncs #define clientfuncs ezhud_clientfuncs #define inputfuncs ezhud_inputfuncs #endif extern plug2dfuncs_t *drawfuncs; extern plugfsfuncs_t *filefuncs; extern plugclientfuncs_t *clientfuncs; extern pluginputfuncs_t *inputfuncs; //ezquake sucks. I'd fix these, but that'd make diffs more messy. #ifdef __GNUC__ #pragma GCC diagnostic ignored "-Wold-style-definition" #pragma GCC diagnostic ignored "-Wstrict-prototypes" #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif //ezquake types. #define byte qbyte #define qbool qboolean #define Com_Printf Con_Printf #define Com_DPrintf Con_DPrintf #define Cvar_Find(n) cvarfuncs->GetNVFDG(n,NULL,0,NULL,NULL) #define Cvar_SetValue(var,val) cvarfuncs->SetFloat(var->name,val) #define Cvar_Set(var,val) cvarfuncs->SetString(var->name,val) #define Cmd_AddCommand(nam,ptr) cmdfuncs->AddCommand(nam,ptr,NULL) #define Cmd_Argc cmdfuncs->Argc #define Cbuf_AddText(x) cmdfuncs->AddText(x,false) #define Sys_Error(x) plugfuncs->Error(x) #define Q_calloc calloc #define Q_malloc malloc #define Q_strdup strdup #define Q_free free #define Q_rint(x) ((int)(x+0.5)) #define Q_atoi atoi #ifdef FTEENGINE #define strlcpy Q_strncpyz #define strlcat Q_strncatz #else #define strlcpy Q_strlcpy #define strlcat Q_strlcat #endif //ezhud has a number of common symbol conflicts, which matter when sttatically linking into the engine #define Cmd_Argv ezCmd_Argv #define TP_LocationName ezTP_LocationName #define TP_ParseFunChars ezTP_ParseFunChars #define Sbar_ColorForMap ezSbar_ColorForMap #define scr_vrect ezscr_vrect #define sb_lines ezsb_lines #define keydown ezkeydown #define scr_con_current ezscr_con_current #undef mpic_t #define mpic_t void #define MV_VIEWS 4 extern float cursor_x; extern float cursor_y; extern int host_screenupdatecount; extern cvar_t *scr_newHud; extern cvar_t *cl_multiview; #define Cam_TrackNum() cl.tracknum #define spec_track cl.tracknum #define autocam ((spec_track==-1)?CAM_NONE:CAM_TRACK) #define CAM_TRACK true #define CAM_NONE false //#define HAXX #define vid plugvid #define cls plugcls #define cl plugcl #define player_info_t plugclientinfo_t extern struct ezcl_s{ int intermission; int teamplay; int deathmatch; int stats[MAX_CL_STATS]; int item_gettime[32]; char serverinfo[4096]; player_info_t players[MAX_CLIENTS]; int playernum; int tracknum; vec3_t simvel; float time; float matchstart; float faceanimtime; qboolean spectator; qboolean standby; qboolean countdown; int splitscreenview; } cl; extern struct ezcls_s{ int state; float min_fps; float fps; float realtime; float frametime; qbool mvdplayback; int demoplayback; } cls; extern struct ezvid_s{ int width; int height; // float displayFrequency; } vid; //reimplementations of ezquake functions void Draw_SetOverallAlpha(float a); void Draw_AlphaFillRGB(float x, float y, float w, float h, qbyte r, qbyte g, qbyte b, qbyte a); void Draw_Fill(float x, float y, float w, float h, qbyte pal); const char *ColorNameToRGBString (const char *newval); byte *StringToRGB(const char *str); #define Draw_String drawfuncs->String void Draw_EZString(float x, float y, char *str, float scale, qboolean red); #define Draw_Alt_String(x,y,s) Draw_EZString(x,y,s,8,true) #define Draw_ColoredString(x,y,str,alt) Draw_EZString(x,y,str,8,alt) #define Draw_SString(x,y,str,sc) Draw_EZString(x,y,str,8*sc,false) #define Draw_SAlt_String(x,y,str,sc) Draw_EZString(x,y,str,8*sc,true) void Draw_SPic(float x, float y, mpic_t *pic, float scale); void Draw_SSubPic(float x, float y, mpic_t *pic, float s1, float t1, float s2, float t2, float scale); #define Draw_STransPic Draw_SPic void Draw_Character(float x, float y, unsigned int ch); void Draw_SCharacter(float x, float y, unsigned int ch, float scale); void SCR_DrawWadString(float x, float y, float scale, char *str); void Draw_SAlphaSubPic2(float x, float y, mpic_t *pic, float s1, float t1, float s2, float t2, float w, float h, float alpha); void Draw_AlphaFill(float x, float y, float w, float h, unsigned int pal, float alpha); void Draw_AlphaPic(float x, float y, mpic_t *pic, float alpha); void Draw_AlphaSubPic(float x, float y, mpic_t *pic, float s1, float t1, float s2, float t2, float alpha); void SCR_HUD_DrawBar(int direction, int value, float max_value, float *rgba, int x, int y, int width, int height); mpic_t *Draw_CachePicSafe(const char *name, qbool crash, qbool ignorewad); mpic_t *Draw_CacheWadPic(const char *name); int Sbar_TopColor(player_info_t *pi); int Sbar_BottomColor(player_info_t *pi); char *TP_ParseFunChars(char*); char *TP_ItemName(unsigned int itbit); char* TP_LocationName (const vec3_t location); char *Cmd_Argv(int arg); extern float scr_con_current; //current console lines shown #define Util_SkipChars(src,strip,dst,dstlen) strlcpy(dst,src,dstlen) #define Util_SkipEZColors(src,dst,dstlen) strlcpy(dst,src,dstlen) void Replace_In_String(char *string, size_t strsize, char leadchar, int patterns, ...); //static qbool Utils_RegExpMatch(char *regexp, char *term) {return true;} #define Utils_RegExpMatch(regexp,term) (true) #define clamp(v,min,max) v=bound(min,v,max) #define strlen_color(line) (drawfuncs->StringWidth(8, 0, line)/8.0) #define TIMETYPE_CLOCK 0 #define TIMETYPE_GAMECLOCK 1 #define TIMETYPE_GAMECLOCKINV 2 #define TIMETYPE_DEMOCLOCK 3 int SCR_GetClockStringWidth(const char *s, qbool big, float scale); int SCR_GetClockStringHeight(qbool big, float scale); const char* SCR_GetTimeString(int timetype, const char *format); void SCR_DrawBigClock(int x, int y, int style, int blink, float scale, const char *t); void SCR_DrawSmallClock(int x, int y, int style, int blink, float scale, const char *t); typedef struct { qbyte c[4]; } clrinfo_t; void Draw_ColoredString3(float x, float y, const char *str, clrinfo_t *clr, int huh, int wut); void UI_PrintTextBlock(float x, float y, float w, float h, char *str, int flags); void Draw_AlphaRectangleRGB(int x, int y, int w, int h, int foo, int bar, byte r, byte g, byte b, byte a); void Draw_AlphaLineRGB(float x1, float y1, float x2, float y2, float width, byte r, byte g, byte b, byte a); void Draw_Polygon(int x, int y, vec3_t *vertices, int num_vertices, qbool fill, byte r, byte g, byte b, byte a); extern int sb_lines; // scan lines to draw #ifndef SBAR_HEIGHT #define SBAR_HEIGHT 24 #define STAT_HEALTH 0 #define STAT_WEAPONMODELI 2 #define STAT_AMMO 3 #define STAT_ARMOR 4 #define STAT_WEAPONFRAME 5 #define STAT_SHELLS 6 #define STAT_NAILS 7 #define STAT_ROCKETS 8 #define STAT_CELLS 9 #define STAT_ACTIVEWEAPON 10 #define STAT_TOTALSECRETS 11 #define STAT_TOTALMONSTERS 12 #define STAT_SECRETS 13 // bumped on client side by svc_foundsecret #define STAT_MONSTERS 14 // bumped by svc_killedmonster #define STAT_ITEMS 15 #define STAT_VIEWHEIGHT 16 //same as zquake #define STAT_TIME 17 //zquake #define STAT_MATCHSTARTTIME 18 #define IT_SHOTGUN (1u<<0) #define IT_SUPER_SHOTGUN (1u<<1) #define IT_NAILGUN (1u<<2) #define IT_SUPER_NAILGUN (1u<<3) #define IT_GRENADE_LAUNCHER (1u<<4) #define IT_ROCKET_LAUNCHER (1u<<5) #define IT_LIGHTNING (1u<<6) #define IT_SUPER_LIGHTNING (1u<<7) #define IT_SHELLS (1u<<8) #define IT_NAILS (1u<<9) #define IT_ROCKETS (1u<<10) #define IT_CELLS (1u<<11) #define IT_AXE (1u<<12) #define IT_ARMOR1 (1u<<13) #define IT_ARMOR2 (1u<<14) #define IT_ARMOR3 (1u<<15) #define IT_SUPERHEALTH (1u<<16) #define IT_KEY1 (1u<<17) #define IT_KEY2 (1u<<18) #define IT_INVISIBILITY (1u<<19) #define IT_INVULNERABILITY (1u<<20) #define IT_SUIT (1u<<21) #define IT_QUAD (1u<<22) #define IT_SIGIL1 (1u<<28) #define IT_SIGIL2 (1u<<29) #define IT_SIGIL3 (1u<<30) #define IT_SIGIL4 (1u<<31) #endif ================================================ FILE: plugins/ezhud/hud.c ================================================ /* Copyright (C) 2011 azazello and ezQuake team 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, see . */ // // HUD commands // #include "ezquakeisms.h" //#include "common_draw.h" //#include "keys.h" #include "hud.h" #include "hud_common.h" #include "hud_editor.h" //#include "utils.h" //#include "sbar.h" #define sbar_last_width 320 // yeah yeah I know, *garbage* -> leave it be :> char *align_strings_x[] = { "left", "center", "right", "before", "after" }; #define num_align_strings_x (sizeof(align_strings_x) / sizeof(align_strings_x[0])) char *align_strings_y[] = { "top", "center", "bottom", "before", "after", "console" }; #define num_align_strings_y (sizeof(align_strings_y) / sizeof(align_strings_y[0])) char *snap_strings[] = { "screen", "top", "view", "sbar", "ibar", "hbar", "sfree", "ifree", "hfree", }; #define num_snap_strings (sizeof(snap_strings) / sizeof(snap_strings[0])) // Hud elements list. hud_t *hud_huds = NULL; qbool doreorder; // // Hud plus func - show element. // void HUD_Plus_f(void) { char *t; hud_t *hud; if (Cmd_Argc() < 1) return; t = Cmd_Argv(0); if (strncmp(t, "+hud_", 5)) return; hud = HUD_Find(t + 5); if (!hud) { // This should never happen... return; } if (!hud->show) { // This should never happen... return; } Cvar_Set(hud->show, "1"); } // // Hud minus func - hide element. // void HUD_Minus_f(void) { char *t; hud_t *hud; if (Cmd_Argc() < 1) return; t = Cmd_Argv(0); if (strncmp(t, "-hud_", 5)) return; hud = HUD_Find(t + 5); if (!hud) { // this should never happen... return; } if (!hud->show) { // this should never happen... return; } Cvar_Set(hud->show, "0"); } // // Hud element func - describe it // this also solves the TAB completion problem // void HUD_Func_f(void) { int i; hud_t *hud; hud = HUD_Find(Cmd_Argv(0)); if (!hud) { // This should never happen... Com_Printf("Hud element not found\n"); return; } if (Cmd_Argc() > 1) { char buf[512]; snprintf(buf, sizeof(buf), "hud_%s_%s", hud->name, Cmd_Argv(1)); if (Cvar_Find(buf) != NULL) { Cbuf_AddText(buf); if (Cmd_Argc() > 2) { Cbuf_AddText(" "); Cbuf_AddText(Cmd_Argv(2)); } Cbuf_AddText("\n"); } else { Com_Printf("Trying \"%s\" - no such variable\n", buf); } return; } // Description. Com_Printf("%s\n\n", hud->description); // Status. if (hud->show != NULL) { Com_Printf("Current status: %s\n", hud->show->value ? "shown" : "hidden"); } if (hud->frame != NULL) { Com_Printf("Frame: %s\n\n", hud->frame->string); } if (hud->frame_color != NULL) { Com_Printf("Frame color: %s\n\n", hud->frame_color->string); } // Placement. Com_Printf("Placement: %s\n", hud->place->string); // Alignment. Com_Printf("Alignment (x y): %s %s\n", hud->align_x->string, hud->align_y->string); // Position. Com_Printf("Offset (x y): %d %d\n", (int)(hud->pos_x->value), (int)(hud->pos_y->value)); // Ordering. Com_Printf("Draw Order (z): %d\n", (int)hud->order->value); // Additional parameters. if (hud->num_params > 0) { int prefix_l = strlen(va("hud_%s_", hud->name)); Com_Printf("\nParameters:\n"); for (i=0; i < hud->num_params; i++) { if (strlen(hud->params[i]->name) > prefix_l) Com_Printf(" %-15s %s\n", hud->params[i]->name + prefix_l, hud->params[i]->string); } } } // // Find the elements with the max and min z-order. // void HUD_FindMaxMinOrder(int *max, int *min) { hud_t *hud = hud_huds; while(hud) { (*min) = ((int)hud->order->value < (*min)) ? (int)hud->order->value : (*min); (*max) = ((int)hud->order->value > (*max)) ? (int)hud->order->value : (*max); hud = hud->next; } } // // Find hud placement by string // return 0 if error // int HUD_FindPlace(hud_t *hud) { int i; hud_t *par; qbool out; char *t; // First try standard strings. for (i=0; i < num_snap_strings; i++) { if (!strcasecmp(hud->place->string, snap_strings[i])) { break; } } if (i < num_snap_strings) { // Found. hud->place_num = i+1; hud->place_hud = NULL; return 1; } // then try another HUD element out = true; t = hud->place->string; if (hud->place->string[0] == '@') { // place inside out = false; t++; } par = hud_huds; while (par) { if (par != hud && !strcmp(t, par->name)) { hud->place_outside = out; hud->place_hud = par; hud->place_num = HUD_PLACE_SCREEN; return 1; } par = par->next; } // No way. hud->place_num = HUD_PLACE_SCREEN; hud->place_hud = NULL; return 0; } // // Find hud alignment by strings // return 0 if error // int HUD_FindAlignX(hud_t *hud) { int i; // First try standard strings. for (i=0; i < num_align_strings_x; i++) { if (!strcasecmp(hud->align_x->string, align_strings_x[i])) { break; } } if (i < num_align_strings_x) { // Found. hud->align_x_num = i+1; return 1; } else { // Error. hud->align_x_num = HUD_ALIGN_LEFT; // left return 0; } } // // Find the alignment for a hud element. // int HUD_FindAlignY(hud_t *hud) { int i; // First try standard strings. for (i=0; i < num_align_strings_y; i++) { if (!strcasecmp(hud->align_y->string, align_strings_y[i])) { break; } } if (i < num_align_strings_y) { // Found. hud->align_y_num = i + 1; return 1; } else { // Error. hud->align_y_num = HUD_ALIGN_TOP; // Left. return 0; } } int Hud_HudCompare (const void *p1, const void *p2) { return strcmp((*((hud_t **) p1))->name, (*((hud_t **) p2))->name); } // // List hud elements // void HUD_List (void) { static hud_t *sorted_huds[256]; int i, count; hud_t *hud; #define MAX_SORTED_HUDS (sizeof (sorted_huds) / sizeof (sorted_huds[0])) for (hud = hud_huds, count = 0; hud && count < MAX_SORTED_HUDS; hud = hud->next, count++) sorted_huds[count] = hud; qsort (sorted_huds, count, sizeof (hud_t *), Hud_HudCompare); if (count == MAX_SORTED_HUDS) assert(!"count == MAX_SORTED_HUDS"); Com_Printf("name status\n"); Com_Printf("--------------- ------\n"); for (i = 0; i < count; i++) { hud = sorted_huds[i]; Com_Printf("%-15s %s\n", hud->name, hud->show->value ? "shown" : "hidden"); } } // // Show the specified hud element. // void HUD_Show_f (void) { hud_t *hud; if (Cmd_Argc() != 2) { Com_Printf("Usage: show [ | all]\n"); Com_Printf("Show given HUD element.\n"); Com_Printf("use \"show all\" to show all elements.\n"); Com_Printf("Current elements status:\n\n"); HUD_List(); return; } if (!strcasecmp(Cmd_Argv(1), "all")) { hud = hud_huds; while (hud) { Cvar_SetValue(hud->show, 1); hud = hud->next; } } else { hud = HUD_Find(Cmd_Argv(1)); if (!hud) { Com_Printf("No such element: %s\n", Cmd_Argv(1)); return; } Cvar_SetValue(hud->show, 1); } } // // Hide the specified hud element. // void HUD_Hide_f (void) { hud_t *hud; if (Cmd_Argc() != 2) { Com_Printf("Usage: hide [ | all]\n"); Com_Printf("Hide given HUD element\n"); Com_Printf("use \"hide all\" to hide all elements.\n"); Com_Printf("Current elements status:\n\n"); HUD_List(); return; } if (!strcasecmp(Cmd_Argv(1), "all")) { hud = hud_huds; while (hud) { Cvar_SetValue(hud->show, 0); hud = hud->next; } } else { hud = HUD_Find(Cmd_Argv(1)); if (!hud) { Com_Printf("No such element: %s\n", Cmd_Argv(1)); return; } Cvar_SetValue(hud->show, 0); } } // // Toggles specified hud element. // void HUD_Toggle_f (void) { hud_t *hud; if (Cmd_Argc() != 2) { Com_Printf("Usage: togglehud | \n"); Com_Printf("Show/hide given HUD element, or toggles variable value.\n"); return; } hud = HUD_Find(Cmd_Argv(1)); if (!hud) { // look for cvar cvar_t *var = Cvar_Find(Cmd_Argv(1)); if (!var) { Com_Printf("No such element or variable: %s\n", Cmd_Argv(1)); return; } Cvar_Set (var, var->value ? "0" : "1"); return; } Cvar_Set (hud->show, hud->show->value ? "0" : "1"); } // // Move the specified hud element relative to placement/alignment. // void HUD_Move_f (void) { hud_t *hud; if (Cmd_Argc() != 4 && Cmd_Argc() != 2) { Com_Printf("Usage: move [ ]\n"); Com_Printf("Set offset for given HUD element\n"); return; } hud = HUD_Find(Cmd_Argv(1)); if (!hud) { Com_Printf("No such element: %s\n", Cmd_Argv(1)); return; } if (Cmd_Argc() == 2) { Com_Printf("Current %s offset is:\n", Cmd_Argv(1)); Com_Printf(" x: %s\n", hud->pos_x->string); Com_Printf(" y: %s\n", hud->pos_y->string); return; } Cvar_SetValue(hud->pos_x, atof(Cmd_Argv(2))); Cvar_SetValue(hud->pos_y, atof(Cmd_Argv(3))); } // // Resets a hud item to the center of the screen. // void HUD_Reset_f (void) { hud_t *hud = NULL; char *hudname = NULL; if (Cmd_Argc() != 2) { Com_Printf("Usage: reset \n"); Com_Printf("Resets the position of the given HUD element to the center of the screen.\n"); return; } hudname = Cmd_Argv(1); hud = HUD_Find(hudname); if (!hud) { Com_Printf("No such HUD element %s.\n", hudname); return; } Cbuf_AddText(va("place %s screen\n", hudname)); Cbuf_AddText(va("move %s 0 0\n", hudname)); Cbuf_AddText(va("align %s center center\n", hudname)); } // // Reorders children so that they are place infront of their parent. // void HUD_ReorderChildren(void) { hud_t *hud = hud_huds; // Give all children a higher Z-order. while(hud) { if(hud->place_hud && hud->order->value <= hud->place_hud->order->value) { Cvar_SetValue(hud->order, hud->place_hud->order->value + 1); } hud = hud->next; } } // // Place the specified hud element. // void HUD_Place_f (void) { hud_t *hud; char temp[512]; if (Cmd_Argc() < 2 || Cmd_Argc() > 3) { Com_Printf("Usage: move []\n"); Com_Printf("Place HUD element at given area.\n"); Com_Printf("\nPossible areas are:\n"); Com_Printf(" screen - screen area\n"); Com_Printf(" top - screen minus status bar\n"); Com_Printf(" view - view\n"); Com_Printf(" sbar - status bar\n"); Com_Printf(" ibar - inventory bar\n"); Com_Printf(" hbar - health bar\n"); Com_Printf(" sfree - status bar free area\n"); Com_Printf(" ifree - inventory bar free area\n"); Com_Printf(" hfree - health bar free area\n"); Com_Printf("You can also use any other HUD element as a base alignment. In such case you should specify area as:\n"); Com_Printf(" @elem - if you want to place\n"); Com_Printf(" it inside elem\n"); Com_Printf(" elem - if you want to place\n"); Com_Printf(" it outside elem\n"); Com_Printf("Examples:\n"); Com_Printf(" place fps view\n"); Com_Printf(" place fps @ping\n"); return; } hud = HUD_Find(Cmd_Argv(1)); if (!hud) { Com_Printf("No such element: %s\n", Cmd_Argv(1)); return; } if (Cmd_Argc() == 2) { Com_Printf("Current %s placement: %s\n", hud->name, hud->place->string); return; } // Place with helper. strlcpy(temp, hud->place->string, sizeof(temp)); Cvar_Set(hud->place, Cmd_Argv(2)); if (!HUD_FindPlace(hud)) { Com_Printf("place: invalid area argument: %s\n", Cmd_Argv(2)); Cvar_Set(hud->place, temp); // Restore old value. } else { HUD_ReorderChildren(); } } // // Sets the z-order of a HUD element. // void HUD_Order_f (void) { int max = 0; int min = 0; char *option = NULL; hud_t *hud = NULL; if (Cmd_Argc() < 2 || Cmd_Argc() > 3) { Com_Printf("Usage: order [