Index: ps/trunk/build/premake/extern_libs4.lua =================================================================== --- ps/trunk/build/premake/extern_libs4.lua (revision 14331) +++ ps/trunk/build/premake/extern_libs4.lua (revision 14332) @@ -1,711 +1,723 @@ -- this file provides project_add_extern_libs, which takes care of the -- dirty details of adding the libraries' include and lib paths. -- -- TYPICAL TASK: add new library. Instructions: -- 1) add a new extern_lib_defs entry -- 2) add library name to extern_libs tables in premake.lua for all 'projects' that want to use it -- directory in which OS-specific library subdirectories reside. if os.is("macosx") then libraries_dir = rootdir.."/libraries/osx/" elseif os.is("windows") then libraries_dir = rootdir.."/libraries/win32/" else -- No Unix-specific libs yet (use source directory instead!) end -- directory for shared, bundled libraries libraries_source_dir = rootdir.."/libraries/source/" local function add_default_lib_paths(extern_lib) libdirs { libraries_dir .. extern_lib .. "/lib" } end local function add_source_lib_paths(extern_lib) libdirs { libraries_source_dir .. extern_lib .. "/lib" } end local function add_default_include_paths(extern_lib) includedirs { libraries_dir .. extern_lib .. "/include" } end local function add_source_include_paths(extern_lib) includedirs { libraries_source_dir .. extern_lib .. "/include" } end -- For unixes: add buildflags and linkflags using pkg-config or another similar command. -- By default, pkg-config is used. Other commands can be passed as "alternative_cmd". -- Running such commands at build/linktime does not work on all environments. -- For those environments where it does not work, we already run it now. -- Any better ideas? local function pkgconfig_cflags(lib, alternative_cmd) local cmd_cflags = "" local result_cflags if not alternative_cmd then cmd_cflags = "pkg-config "..lib.." --cflags" else cmd_cflags = alternative_cmd end if _ACTION == "xcode3" or _ACTION == "xcode4" then result_cflags = string.gsub(os.capture(cmd_cflags), "\n", "") buildoptions { result_cflags } else buildoptions { "`"..cmd_cflags.."`" } end end local function pkgconfig_libs(lib, alternative_cmd) local cmd_libs = "" local result_libs if not alternative_cmd then cmd_libs = "pkg-config "..lib.." --libs" else cmd_libs = alternative_cmd end if _ACTION == "xcode3" or _ACTION == "xcode4" then -- The syntax of -Wl with the comma separated list doesn't work and -Wl apparently isn't needed in xcode. -- This is a hack, but it works... result_libs = string.gsub(os.capture(cmd_libs), "-Wl", "") result_libs = string.gsub(result_libs, ",", " ") result_libs = string.gsub(result_libs, "\n", "") linkoptions { result_libs } elseif _ACTION == "gmake" then gnuexternals { "`"..cmd_libs.."`" } else linkoptions { "`"..cmd_libs.."`" } end end function os.capture(cmd) local f = io.popen(cmd, 'r') local s = f:read('*a') return s end local function add_delayload(name, suffix, def) if def["no_delayload"] then return end -- currently only supported by VC; nothing to do on other platforms. if not os.is("windows") then return end -- no extra debug version; use same library in all configs if suffix == "" then linkoptions { "/DELAYLOAD:"..name..".dll" } -- extra debug version available; use in debug config else local dbg_cmd = "/DELAYLOAD:" .. name .. suffix .. ".dll" local cmd = "/DELAYLOAD:" .. name .. ".dll" configuration "Debug" linkoptions { dbg_cmd } configuration "Release" linkoptions { cmd } configuration { } end end local function add_default_links(def) -- careful: make sure to only use *_names when on the correct platform. local names = {} if os.is("windows") then if def.win_names then names = def.win_names end elseif _OPTIONS["android"] and def.android_names then names = def.android_names elseif os.is("linux") and def.linux_names then names = def.linux_names elseif os.is("macosx") and def.osx_names then names = def.osx_names elseif os.is("bsd") and def.bsd_names then names = def.bsd_names elseif def.unix_names then names = def.unix_names end local suffix = "d" -- library is overriding default suffix (typically "" to indicate there is none) if def["dbg_suffix"] then suffix = def["dbg_suffix"] end -- non-Windows doesn't have the distinction of debug vs. release libraries -- (to be more specific, they do, but the two are binary compatible; -- usually only one type - debug or release - is installed at a time). if not os.is("windows") then suffix = "" end -- OS X "Frameworks" need to be added in a special way to the link -- i.e. by linkoptions += "-framework ..." if os.is("macosx") and def.osx_frameworks then for i,name in pairs(def.osx_frameworks) do linkoptions { "-framework " .. name } end else for i,name in pairs(names) do configuration "Debug" links { name .. suffix } configuration "Release" links { name } configuration { } add_delayload(name, suffix, def) end end end -- Library definitions -- In a perfect world, libraries would have a common installation template, -- i.e. location of include directory, naming convention for .lib, etc. -- this table provides a means of working around each library's differences. -- -- The basic approach is defining two functions per library: -- -- 1. compile_settings -- This function should set all settings requred during the compile-phase like -- includedirs, defines etc... -- -- 2. link_settings -- This function should set all settings required during the link-phase like -- libdirs, linkflag etc... -- -- The main reason for separating those settings is different linking behaviour -- on osx and xcode. For more details, read the comment in project_add_extern_libs. -- -- There are some helper functions for the most common tasks. You should use them -- if they can be used in your situation to make the definitions more consistent and -- use their default beviours as a guideline. -- -- -- add_default_lib_paths(extern_lib) -- Description: Add '//lib'to the libpaths -- Parameters: -- * extern_lib: to be used in the libpath. -- -- add_default_include_paths(extern_lib) -- Description: Add '//include' to the includepaths -- Parameters: -- * extern_lib: to be used in the libpath. -- -- add_default_links -- Description: Adds links to libraries and configures delayloading. -- If the *_names parameter for a plattform is missing, no linking will be done -- on that plattform. -- The default assumptions are: -- * debug import library and DLL are distinguished with a "d" suffix -- * the library should be marked for delay-loading. -- Parameters: -- * win_names: table of import library / DLL names (no extension) when -- running on Windows. -- * unix_names: as above; shared object names when running on non-Windows. -- * osx_names: as above; for OS X specifically (overrides unix_names if both are -- specified) -- * bsd_names: as above; for BSD specifically (overrides unix_names if both are -- specified) -- * linux_names: ditto for Linux (overrides unix_names if both given) -- * dbg_suffix: changes the debug suffix from the above default. -- can be "" to indicate the library doesn't have a debug build; -- in that case, the same library (without suffix) is used in -- all build configurations. -- * no_delayload: indicate the library is not to be delay-loaded. -- this is necessary for some libraries that do not support it, -- e.g. Xerces (which is so stupid as to export variables). extern_lib_defs = { boost = { compile_settings = function() if os.is("windows") then add_default_include_paths("boost") elseif os.is("macosx") then -- Suppress all the Boost warnings on OS X by including it as a system directory buildoptions { "-isystem../" .. libraries_dir .. "boost/include" } end if os.getversion().description == "OpenBSD" then includedirs { "/usr/local/include" } end end, link_settings = function() if os.is("windows") or os.is("macosx") then add_default_lib_paths("boost") end add_default_links({ -- The following are not strictly link dependencies on all systems, but -- are included for compatibility with different versions of Boost android_names = { "boost_filesystem-gcc-mt", "boost_system-gcc-mt" }, unix_names = { os.findlib("boost_filesystem-mt") and "boost_filesystem-mt" or "boost_filesystem", os.findlib("boost_system-mt") and "boost_system-mt" or "boost_system" }, osx_names = { "boost_filesystem-mt", "boost_system-mt" }, }) end, }, boost_signals = { link_settings = function() add_default_links({ android_names = { "boost_signals-gcc-mt" }, unix_names = { os.findlib("boost_signals-mt") and "boost_signals-mt" or "boost_signals" }, osx_names = { "boost_signals-mt" }, }) end, }, cxxtest = { compile_settings = function() add_source_include_paths("cxxtest") end, link_settings = function() add_source_lib_paths("cxxtest") end, }, + miniupnpc = { + compile_settings = function() + add_source_include_paths("miniupnpc") + end, + link_settings = function() + add_source_lib_paths("miniupnpc") + add_default_links({ + win_names = { "miniupnpc" }, + unix_names = { "miniupnpc" }, + }) + end, + }, comsuppw = { link_settings = function() add_default_links({ win_names = { "comsuppw" }, dbg_suffix = "d", no_delayload = 1, }) end, }, enet = { compile_settings = function() if not _OPTIONS["with-system-enet"] then add_source_include_paths("enet") end end, link_settings = function() if not _OPTIONS["with-system-enet"] then add_source_lib_paths("enet") end add_default_links({ win_names = { "enet" }, unix_names = { "enet" }, }) end, }, fcollada = { compile_settings = function() add_source_include_paths("fcollada") end, link_settings = function() add_source_lib_paths("fcollada") if os.is("windows") then configuration "Debug" links { "FColladaD" } configuration "Release" links { "FCollada" } configuration { } else configuration "Debug" links { "FColladaSD" } configuration "Release" links { "FColladaSR" } configuration { } end end, }, ffmpeg = { compile_settings = function() if os.is("windows") then add_default_include_paths("ffmpeg") end end, link_settings = function() if os.is("windows") then add_default_lib_paths("ffmpeg") end add_default_links({ win_names = { "avcodec-51", "avformat-51", "avutil-49", "swscale-0" }, unix_names = { "avcodec", "avformat", "avutil" }, dbg_suffix = "", }) end, }, gloox = { compile_settings = function() if os.is("windows") then add_default_include_paths("gloox") elseif os.is("macosx") then -- Support GLOOX_CONFIG for overriding the default PATH-based gloox-config gloox_config_path = os.getenv("GLOOX_CONFIG") if not gloox_config_path then gloox_config_path = "gloox-config" end pkgconfig_cflags(nil, gloox_config_path.." --cflags") end end, link_settings = function() if os.is("windows") then add_default_lib_paths("gloox") end if os.is("macosx") then gloox_config_path = os.getenv("GLOOX_CONFIG") if not gloox_config_path then gloox_config_path = "gloox-config" end pkgconfig_libs(nil, gloox_config_path.." --libs") else -- TODO: consider using pkg-config on non-Windows (for compile_settings too) add_default_links({ win_names = { "gloox-1.0" }, unix_names = { "gloox" }, }) end end, }, libcurl = { compile_settings = function() if os.is("windows") or os.is("macosx") then add_default_include_paths("libcurl") end end, link_settings = function() if os.is("windows") or os.is("macosx") then add_default_lib_paths("libcurl") end add_default_links({ win_names = { "libcurl" }, unix_names = { "curl" }, }) end, }, libjpg = { compile_settings = function() if os.is("windows") or os.is("macosx") then add_default_include_paths("libjpg") end end, link_settings = function() if os.is("windows") or os.is("macosx") then add_default_lib_paths("libjpg") end add_default_links({ win_names = { "jpeg-6b" }, unix_names = { "jpeg" }, }) end, }, libpng = { compile_settings = function() if os.is("windows") or os.is("macosx") then add_default_include_paths("libpng") end if os.getversion().description == "OpenBSD" then includedirs { "/usr/local/include/libpng" } end end, link_settings = function() if os.is("windows") or os.is("macosx") then add_default_lib_paths("libpng") end add_default_links({ win_names = { "libpng15" }, unix_names = { "png" }, -- Otherwise ld will sometimes pull in ancient 1.2 from the SDK, which breaks the build :/ -- TODO: Figure out why that happens osx_names = { "png15" }, }) end, }, libxml2 = { compile_settings = function() if os.is("windows") then add_default_include_paths("libxml2") elseif os.is("macosx") then -- Support XML2_CONFIG for overriding for the default PATH-based xml2-config xml2_config_path = os.getenv("XML2_CONFIG") if not xml2_config_path then xml2_config_path = "xml2-config" end -- use xml2-config instead of pkg-config on OS X pkgconfig_cflags(nil, xml2_config_path.." --cflags") -- libxml2 needs _REENTRANT or __MT__ for thread support; -- OS X doesn't get either set by default, so do it manually defines { "_REENTRANT" } else pkgconfig_cflags("libxml-2.0") end end, link_settings = function() if os.is("windows") then add_default_lib_paths("libxml2") configuration "Debug" links { "libxml2" } configuration "Release" links { "libxml2" } configuration { } elseif os.is("macosx") then xml2_config_path = os.getenv("XML2_CONFIG") if not xml2_config_path then xml2_config_path = "xml2-config" end pkgconfig_libs(nil, xml2_config_path.." --libs") else pkgconfig_libs("libxml-2.0") end end, }, nvtt = { compile_settings = function() if not _OPTIONS["with-system-nvtt"] then add_source_include_paths("nvtt") end defines { "NVTT_SHARED=1" } end, link_settings = function() if not _OPTIONS["with-system-nvtt"] then add_source_lib_paths("nvtt") end add_default_links({ win_names = { "nvtt" }, unix_names = { "nvcore", "nvmath", "nvimage", "nvtt" }, osx_names = { "nvcore", "nvmath", "nvimage", "nvtt", "squish" }, dbg_suffix = "", -- for performance we always use the release-mode version }) end, }, openal = { compile_settings = function() if os.is("windows") then add_default_include_paths("openal") end end, link_settings = function() if os.is("windows") then add_default_lib_paths("openal") end add_default_links({ win_names = { "openal32" }, unix_names = { "openal" }, osx_frameworks = { "OpenAL" }, dbg_suffix = "", no_delayload = 1, -- delayload seems to cause errors on startup }) end, }, opengl = { compile_settings = function() if os.is("windows") then add_default_include_paths("opengl") end end, link_settings = function() if os.is("windows") then add_default_lib_paths("opengl") end if _OPTIONS["gles"] then add_default_links({ unix_names = { "GLESv2" }, dbg_suffix = "", }) else add_default_links({ win_names = { "opengl32", "gdi32" }, unix_names = { "GL" }, osx_frameworks = { "OpenGL" }, dbg_suffix = "", no_delayload = 1, -- delayload seems to cause errors on startup }) end end, }, sdl = { compile_settings = function() if os.is("windows") then includedirs { libraries_dir .. "sdl/include/SDL" } elseif not _OPTIONS["android"] then -- Support SDL_CONFIG for overriding for the default PATH-based sdl-config sdl_config_path = os.getenv("SDL_CONFIG") if not sdl_config_path then sdl_config_path = "sdl-config" end -- "pkg-config sdl --libs" appears to include both static and dynamic libs -- when on MacPorts, which is bad, so use sdl-config instead pkgconfig_cflags(nil, sdl_config_path.." --cflags") end end, link_settings = function() if os.is("windows") then add_default_lib_paths("sdl") elseif not _OPTIONS["android"] then sdl_config_path = os.getenv("SDL_CONFIG") if not sdl_config_path then sdl_config_path = "sdl-config" end pkgconfig_libs(nil, sdl_config_path.." --libs") end end, }, spidermonkey = { compile_settings = function() if _OPTIONS["with-system-mozjs185"] then if not _OPTIONS["android"] then pkgconfig_cflags("mozjs185") end defines { "WITH_SYSTEM_MOZJS185" } else if os.is("windows") then include_dir = "include-win32" elseif os.is("macosx") then include_dir = "include" else include_dir = "include-unix" end configuration "Debug" includedirs { libraries_source_dir.."spidermonkey/"..include_dir } configuration "Release" includedirs { libraries_source_dir.."spidermonkey/"..include_dir } configuration { } end end, link_settings = function() if _OPTIONS["with-system-mozjs185"] then if _OPTIONS["android"] then links { "mozjs185-1.0" } else pkgconfig_libs("mozjs185") end else configuration "Debug" links { "mozjs185-ps-debug" } configuration "Release" links { "mozjs185-ps-release" } configuration { } add_source_lib_paths("spidermonkey") end end, }, valgrind = { compile_settings = function() add_source_include_paths("valgrind") end, link_settings = function() add_source_lib_paths("valgrind") end, }, vorbis = { compile_settings = function() if os.is("windows") or os.is("macosx") then add_default_include_paths("vorbis") end end, link_settings = function() if os.is("windows") or os.is("macosx") then add_default_lib_paths("vorbis") end -- TODO: We need to force linking with these as currently -- they need to be loaded explicitly on execution if os.getversion().description == "OpenBSD" then add_default_links({ unix_names = { "ogg", "vorbis" }, }) end add_default_links({ win_names = { "vorbisfile" }, unix_names = { "vorbisfile" }, osx_names = { "vorbis", "vorbisenc", "vorbisfile", "ogg" }, dbg_suffix = "_d", }) end, }, wxwidgets = { compile_settings = function() if os.is("windows") then includedirs { libraries_dir.."wxwidgets/include/msvc" } add_default_include_paths("wxwidgets") else -- Support WX_CONFIG for overriding for the default PATH-based wx-config wx_config_path = os.getenv("WX_CONFIG") if not wx_config_path then wx_config_path = "wx-config" end pkgconfig_cflags(nil, wx_config_path.." --unicode=yes --cxxflags") end end, link_settings = function() if os.is("windows") then libdirs { libraries_dir.."wxwidgets/lib/vc_lib" } configuration "Debug" links { "wxmsw28ud_gl" } configuration "Release" links { "wxmsw28u_gl" } configuration { } else wx_config_path = os.getenv("WX_CONFIG") if not wx_config_path then wx_config_path = "wx-config" end pkgconfig_libs(nil, wx_config_path.." --unicode=yes --libs std,gl") end end, }, x11 = { link_settings = function() add_default_links({ win_names = { }, unix_names = { "X11" }, }) end, }, xcursor = { link_settings = function() add_default_links({ unix_names = { "Xcursor" }, }) end, }, zlib = { compile_settings = function() if os.is("windows") or os.is("macosx") then add_default_include_paths("zlib") end end, link_settings = function() if os.is("windows") or os.is("macosx") then add_default_lib_paths("zlib") end add_default_links({ win_names = { "zlib1" }, unix_names = { "z" }, }) end, }, } -- add a set of external libraries to the project; takes care of -- include / lib path and linking against the import library. -- extern_libs: table of library names [string] -- target_type: String defining the projects kind [string] function project_add_extern_libs(extern_libs, target_type) for i,extern_lib in pairs(extern_libs) do local def = extern_lib_defs[extern_lib] assert(def, "external library " .. extern_lib .. " not defined") if def.compile_settings then def.compile_settings() end -- Linking to external libraries will only be done in the main executable and not in the -- static libraries. Premake would silently skip linking into static libraries for some -- actions anyway (e.g. vs2010). -- On osx using xcode, if this linking would be defined in the static libraries, it would fail to -- link if only dylibs are available. If both *.a and *.dylib are available, it would link statically. -- I couldn't find any problems with that approach. if target_type ~= "StaticLib" and def.link_settings then def.link_settings() end end end Index: ps/trunk/build/premake/premake4.lua =================================================================== --- ps/trunk/build/premake/premake4.lua (revision 14331) +++ ps/trunk/build/premake/premake4.lua (revision 14332) @@ -1,1424 +1,1426 @@ newoption { trigger = "android", description = "Use non-working Android cross-compiling mode" } newoption { trigger = "atlas", description = "Include Atlas scenario editor projects" } newoption { trigger = "collada", description = "Include COLLADA projects (requires FCollada library)" } newoption { trigger = "coverage", description = "Enable code coverage data collection (GCC only)" } newoption { trigger = "gles", description = "Use non-working OpenGL ES 2.0 mode" } newoption { trigger = "icc", description = "Use Intel C++ Compiler (Linux only; should use either \"--cc icc\" or --without-pch too, and then set CXX=icpc before calling make)" } newoption { trigger = "outpath", description = "Location for generated project files" } newoption { trigger = "without-audio", description = "Disable use of OpenAL/Ogg/Vorbis APIs" } newoption { trigger = "minimal-flags", description = "Only set compiler/linker flags that are really needed. Has no effect on Windows builds" } newoption { trigger = "without-nvtt", description = "Disable use of NVTT" } newoption { trigger = "without-tests", description = "Disable generation of test projects" } newoption { trigger = "without-pch", description = "Disable generation and usage of precompiled headers" } newoption { trigger = "without-lobby", description = "Disable the use of gloox and the multiplayer lobby" } newoption { trigger = "with-system-nvtt", description = "Search standard paths for nvidia-texture-tools library, instead of using bundled copy" } newoption { trigger = "with-system-enet", description = "Search standard paths for libenet, instead of using bundled copy" } newoption { trigger = "with-system-mozjs185", description = "Search standard paths for libmozjs185, instead of using bundled copy" } newoption { trigger = "with-c++11", description = "Enable C++11 on GCC" } newoption { trigger = "sysroot", description = "Set compiler system root path, used for building against a non-system SDK. For example /usr/local becomes SYSROOT/user/local" } newoption { trigger = "macosx-version-min", description = "Set minimum required version of the OS X API, the build will possibly fail if an older SDK is used, while newer API functions will be weakly linked (i.e. resolved at runtime)" } newoption { trigger = "macosx-bundle", description = "Enable OSX bundle, the argument is the bundle identifier string (e.g. com.wildfiregames.0ad)" } newoption { trigger = "build-shared-glooxwrapper", description = "Rebuild glooxwrapper DLL for Windows. Requires the same compiler version that gloox was built with" } newoption { trigger = "use-shared-glooxwrapper", description = "Use prebuilt glooxwrapper DLL for Windows" } newoption { trigger = "bindir", description = "Directory for executables (typically '/usr/games'); default is to be relocatable" } newoption { trigger = "datadir", description = "Directory for data files (typically '/usr/share/games/0ad'); default is ../data/ relative to executable" } newoption { trigger = "libdir", description = "Directory for libraries (typically '/usr/lib/games/0ad'); default is ./ relative to executable" } -- Root directory of project checkout relative to this .lua file rootdir = "../.." dofile("extern_libs4.lua") -- detect CPU architecture (simplistic, currently only supports x86, amd64 and ARM) arch = "x86" if _OPTIONS["android"] then arch = "arm" elseif os.is("windows") then if os.getenv("PROCESSOR_ARCHITECTURE") == "amd64" or os.getenv("PROCESSOR_ARCHITEW6432") == "amd64" then arch = "amd64" end else arch = os.getenv("HOSTTYPE") if arch == "x86_64" or arch == "amd64" then arch = "amd64" else os.execute("gcc -dumpmachine > .gccmachine.tmp") local f = io.open(".gccmachine.tmp", "r") local machine = f:read("*line") f:close() if string.find(machine, "x86_64") == 1 or string.find(machine, "amd64") == 1 then arch = "amd64" elseif string.find(machine, "i.86") == 1 then arch = "x86" elseif string.find(machine, "arm") == 1 then arch = "arm" else print("WARNING: Cannot determine architecture from GCC, assuming x86") end end end -- Set up the Solution solution "pyrogenesis" targetdir(rootdir.."/binaries/system") libdirs(rootdir.."/binaries/system") if not _OPTIONS["outpath"] then error("You must specify the 'outpath' parameter") end location(_OPTIONS["outpath"]) configurations { "Release", "Debug" } -- Get some environement specific information used later. if os.is("windows") then nasmpath(rootdir.."/build/bin/nasm.exe") lcxxtestpath = rootdir.."/build/bin/cxxtestgen.exe" has_broken_pch = false else lcxxtestpath = rootdir.."/build/bin/cxxtestgen.pl" if os.is("linux") and arch == "amd64" then nasmformat "elf64" elseif os.is("macosx") and arch == "amd64" then nasmformat "macho64" elseif os.is("macosx") then nasmformat "macho" else nasmformat "elf" end -- GCC bug (http://gcc.gnu.org/bugzilla/show_bug.cgi?id=10591) - PCH breaks anonymous namespaces -- Fixed in 4.2.0, but we have to disable PCH for earlier versions, else -- it conflicts annoyingly with wx 2.8 headers. -- It's too late to do this test by the time we start compiling the PCH file, so -- do the test in this build script instead (which is kind of ugly - please fix if -- you have a better idea) if not _OPTIONS["icc"] then os.execute("gcc -dumpversion > .gccver.tmp") local f = io.open(".gccver.tmp", "r") major, dot, minor = f:read(1, 1, 1) f:close() major = 0+major -- coerce to number minor = 0+minor has_broken_pch = (major < 4 or (major == 4 and minor < 2)) if has_broken_pch then print("WARNING: Detected GCC <4.2 -- disabling PCH for Atlas (will increase build times)") end end end source_root = rootdir.."/source/" -- default for most projects - overridden by local in others -- Rationale: projects should not have any additional include paths except for -- those required by external libraries. Instead, we should always write the -- full relative path, e.g. #include "maths/Vector3d.h". This avoids confusion -- ("which file is meant?") and avoids enormous include path lists. -- projects: engine static libs, main exe, atlas, atlas frontends, test. -------------------------------------------------------------------------------- -- project helper functions -------------------------------------------------------------------------------- function project_set_target(project_name) -- Note: On Windows, ".exe" is added on the end, on unices the name is used directly local obj_dir_prefix = _OPTIONS["outpath"].."/obj/"..project_name.."_" configuration "Debug" objdir(obj_dir_prefix.."Debug") targetsuffix("_dbg") configuration "Release" objdir(obj_dir_prefix.."Release") configuration { } end function project_set_build_flags() flags { "Symbols", "NoEditAndContinue" } if not _OPTIONS["icc"] and (os.is("windows") or not _OPTIONS["minimal-flags"]) then -- adds the -Wall compiler flag flags { "ExtraWarnings" } -- this causes far too many warnings/remarks on ICC end -- disable Windows debug heap, since it makes malloc/free hugely slower when -- running inside a debugger if os.is("windows") then flags { "NoDebugHeap" } end configuration "Debug" defines { "DEBUG" } configuration "Release" if os.is("windows") or not _OPTIONS["minimal-flags"] then flags { "OptimizeSpeed" } end defines { "NDEBUG", "CONFIG_FINAL=1" } configuration { } if _OPTIONS["gles"] then defines { "CONFIG2_GLES=1" } end if _OPTIONS["without-audio"] then defines { "CONFIG2_AUDIO=0" } end if _OPTIONS["without-nvtt"] then defines { "CONFIG2_NVTT=0" } end if _OPTIONS["without-lobby"] then defines { "CONFIG2_LOBBY=0" } end -- required for the lowlevel library. must be set from all projects that use it, otherwise it assumes it is -- being used as a DLL (which is currently not the case in 0ad) defines { "LIB_STATIC_LINK" } -- various platform-specific build flags if os.is("windows") then -- use native wchar_t type (not typedef to unsigned short) flags { "NativeWChar" } flags { "EnableSSE2" } -- Enable SSE2 code generation for VS -- VC++ 2008 has implied FPO as the default (newer versions default to /Oy-) -- disable it explicitly since it breaks our stack walker in release build if _ACTION == "vs2008" then buildoptions { "/Oy-" } end else -- *nix if _OPTIONS["icc"] and not _OPTIONS["minimal-flags"] then buildoptions { "-w1", -- "-Wabi", -- "-Wp64", -- complains about OBJECT_TO_JSVAL which is annoying "-Wpointer-arith", "-Wreturn-type", -- "-Wshadow", "-Wuninitialized", "-Wunknown-pragmas", "-Wunused-function", "-wd1292" -- avoid lots of 'attribute "__nonnull__" ignored' } configuration "Debug" buildoptions { "-O0" } -- ICC defaults to -O2 configuration { } if os.is("macosx") then linkoptions { "-multiply_defined","suppress" } end else -- exclude most non-essential build options for minimal-flags if not _OPTIONS["minimal-flags"] then buildoptions { -- enable most of the standard warnings "-Wno-switch", -- enumeration value not handled in switch (this is sometimes useful, but results in lots of noise) "-Wno-reorder", -- order of initialization list in constructors (lots of noise) "-Wno-invalid-offsetof", -- offsetof on non-POD types (see comment in renderer/PatchRData.cpp) "-Wextra", "-Wno-missing-field-initializers", -- (this is common in external headers we can't fix) -- add some other useful warnings that need to be enabled explicitly "-Wunused-parameter", "-Wredundant-decls", -- (useful for finding some multiply-included header files) -- "-Wformat=2", -- (useful sometimes, but a bit noisy, so skip it by default) -- "-Wcast-qual", -- (useful for checking const-correctness, but a bit noisy, so skip it by default) "-Wnon-virtual-dtor", -- (sometimes noisy but finds real bugs) "-Wundef", -- (useful for finding macro name typos) -- enable security features (stack checking etc) that shouldn't have -- a significant effect on performance and can catch bugs "-fstack-protector-all", "-U_FORTIFY_SOURCE", -- (avoid redefinition warning if already defined) "-D_FORTIFY_SOURCE=2", -- always enable strict aliasing (useful in debug builds because of the warnings) "-fstrict-aliasing", -- don't omit frame pointers (for now), because performance will be impacted -- negatively by the way this breaks profilers more than it will be impacted -- positively by the optimisation "-fno-omit-frame-pointer" } if not _OPTIONS["without-pch"] then buildoptions { -- do something (?) so that ccache can handle compilation with PCH enabled -- (ccache 3.1+ also requires CCACHE_SLOPPINESS=time_macros for this to work) "-fpch-preprocess" } end if arch == "x86" or arch == "amd64" then buildoptions { -- enable SSE intrinsics "-msse" } end if os.is("linux") or os.is("bsd") then linkoptions { "-Wl,--no-undefined", "-Wl,--as-needed" } end if arch == "x86" then buildoptions { -- To support intrinsics like __sync_bool_compare_and_swap on x86 -- we need to set -march to something that supports them "-march=i686" } end end if _OPTIONS["with-c++11"] then buildoptions { -- Enable C++11 standard. VS2010 and higher automatically support C++11 -- but we have to enable it manually on GNU C++ and Intel C++ "-std=c++0x" } end if arch == "arm" then -- disable warnings about va_list ABI change and use -- compile-time flags for futher configuration. buildoptions { "-Wno-psabi" } if _OPTIONS["android"] then -- Android uses softfp, so we should too. buildoptions { "-mfloat-abi=softfp" } end end if _OPTIONS["coverage"] then buildoptions { "-fprofile-arcs", "-ftest-coverage" } links { "gcov" } end -- We don't want to require SSE2 everywhere yet, but OS X headers do -- require it (and Intel Macs always have it) so enable it here if os.is("macosx") then buildoptions { "-msse2" } end -- Check if SDK path should be used if _OPTIONS["sysroot"] then buildoptions { "-isysroot " .. _OPTIONS["sysroot"] } linkoptions { "-Wl,-syslibroot," .. _OPTIONS["sysroot"] } end -- On OS X, sometimes we need to specify the minimum API version to use if _OPTIONS["macosx-version-min"] then buildoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] } -- clang and llvm-gcc look at mmacosx-version-min to determine link target -- and CRT version, and use it to set the macosx_version_min linker flag linkoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] } end -- Check if we're building a bundle if _OPTIONS["macosx-bundle"] then defines { "BUNDLE_IDENTIFIER=" .. _OPTIONS["macosx-bundle"] } end end if not _OPTIONS["minimal-flags"] then buildoptions { -- Hide symbols in dynamic shared objects by default, for efficiency and for equivalence with -- Windows - they should be exported explicitly with __attribute__ ((visibility ("default"))) "-fvisibility=hidden" } end if _OPTIONS["bindir"] then defines { "INSTALLED_BINDIR=" .. _OPTIONS["bindir"] } end if _OPTIONS["datadir"] then defines { "INSTALLED_DATADIR=" .. _OPTIONS["datadir"] } end if _OPTIONS["libdir"] then defines { "INSTALLED_LIBDIR=" .. _OPTIONS["libdir"] } end if os.is("linux") or os.is("bsd") then -- To use our local shared libraries, they need to be found in the -- runtime dynamic linker path. Add their path to -rpath. if _OPTIONS["libdir"] then linkoptions {"-Wl,-rpath," .. _OPTIONS["libdir"] } else -- On FreeBSD we need to allow use of $ORIGIN if os.is("bsd") then linkoptions { "-Wl,-z,origin" } end -- Adding the executable path and taking care of correct escaping if _ACTION == "gmake" then linkoptions { "-Wl,-rpath,'$$ORIGIN'" } elseif _ACTION == "codeblocks" then linkoptions { "-Wl,-R\\\\$$ORIGIN" } end end end end end -- add X11 includes paths after all the others so they don't conflict with -- bundled libs function project_add_x11_dirs() if not os.is("windows") and not os.is("macosx") then -- X11 includes may be installed in one of a gadzillion of three places -- Famous last words: "You can't include too much! ;-)" includedirs { "/usr/X11R6/include/X11", "/usr/X11R6/include", "/usr/include/X11" } libdirs { "/usr/X11R6/lib" } end end -- create a project and set the attributes that are common to all projects. function project_create(project_name, target_type) project(project_name) language "C++" kind(target_type) project_set_target(project_name) project_set_build_flags() end -- OSX creates a .app bundle if the project type of the main application is set to "WindowedApp". -- We don't want this because this bundle would be broken (it lacks all the resources and external dependencies, Info.plist etc...) -- Windows opens a console in the background if it's set to ConsoleApp, which is not what we want. -- I didn't check if this setting matters for linux, but WindowedApp works there. function get_main_project_target_type() if _OPTIONS["android"] then return "SharedLib" elseif os.is("macosx") then return "ConsoleApp" else return "WindowedApp" end end -- source_root: rel_source_dirs and rel_include_dirs are relative to this directory -- rel_source_dirs: A table of subdirectories. All source files in these directories are added. -- rel_include_dirs: A table of subdirectories to be included. -- extra_params: table including zero or more of the following: -- * no_pch: If specified, no precompiled headers are used for this project. -- * pch_dir: If specified, this directory will be used for precompiled headers instead of the default -- /pch//. -- * extra_files: table of filenames (relative to source_root) to add to project -- * extra_links: table of library names to add to link step function project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params) for i,v in pairs(rel_source_dirs) do local prefix = source_root..v.."/" files { prefix.."*.cpp", prefix.."*.h", prefix.."*.inl", prefix.."*.js", prefix.."*.asm", prefix.."*.mm" } end -- Put the project-specific PCH directory at the start of the -- include path, so '#include "precompiled.h"' will look in -- there first local pch_dir if not extra_params["pch_dir"] then pch_dir = source_root .. "pch/" .. project().name .. "/" else pch_dir = extra_params["pch_dir"] end includedirs { pch_dir } -- Precompiled Headers -- rationale: we need one PCH per static lib, since one global header would -- increase dependencies. To that end, we can either include them as -- "projectdir/precompiled.h", or add "source/PCH/projectdir" to the -- include path and put the PCH there. The latter is better because -- many projects contain several dirs and it's unclear where there the -- PCH should be stored. This way is also a bit easier to use in that -- source files always include "precompiled.h". -- Notes: -- * Visual Assist manages to use the project include path and can -- correctly open these files from the IDE. -- * precompiled.cpp (needed to "Create" the PCH) also goes in -- the abovementioned dir. if (not _OPTIONS["without-pch"] and not extra_params["no_pch"]) then pchheader(pch_dir.."precompiled.h") pchsource(pch_dir.."precompiled.cpp") defines { "USING_PCH" } files { pch_dir.."precompiled.h", pch_dir.."precompiled.cpp" } else flags { "NoPCH" } end -- next is source root dir, for absolute (nonrelative) includes -- (e.g. "lib/precompiled.h") includedirs { source_root } for i,v in pairs(rel_include_dirs) do includedirs { source_root .. v } end if extra_params["extra_files"] then for i,v in pairs(extra_params["extra_files"]) do -- .rc files are only needed on Windows if path.getextension(v) ~= ".rc" or os.is("windows") then files { source_root .. v } end end end if extra_params["extra_links"] then links { extra_params["extra_links"] } end end -- Add command-line options to set up the manifest dependencies for Windows -- (See lib/sysdep/os/win/manifest.cpp) function project_add_manifest() linkoptions { "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='X86' publicKeyToken='6595b64144ccf1df'\"" } configuration "Debug" linkoptions { "\"/manifestdependency:type='win32' name='Microsoft.VC80.DebugCRT' version='8.0.50727.4053' processorArchitecture='x86' publicKeyToken='1fc8b3b9a1e18e3b'\"" } configuration "Release" linkoptions { "\"/manifestdependency:type='win32' name='Microsoft.VC80.CRT' version='8.0.50727.4053' processorArchitecture='x86' publicKeyToken='1fc8b3b9a1e18e3b'\"" } configuration { } end -------------------------------------------------------------------------------- -- engine static libraries -------------------------------------------------------------------------------- -- the engine is split up into several static libraries. this eases separate -- distribution of those components, reduces dependencies a bit, and can -- also speed up builds. -- more to the point, it is necessary to efficiently support a separate -- test executable that also includes much of the game code. -- names of all static libs created. automatically added to the -- main app project later (see explanation at end of this file) static_lib_names = {} static_lib_names_debug = {} static_lib_names_release = {} -- set up one of the static libraries into which the main engine code is split. -- extra_params: -- no_default_link: If specified, linking won't be done by default. -- For the rest of extra_params, see project_add_contents(). -- note: rel_source_dirs and rel_include_dirs are relative to global source_root. function setup_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) local target_type = "StaticLib" project_create(project_name, target_type) project_add_contents(source_root, rel_source_dirs, {}, extra_params) project_add_extern_libs(extern_libs, target_type) project_add_x11_dirs() if not extra_params["no_default_link"] then table.insert(static_lib_names, project_name) end if os.is("windows") then flags { "NoRTTI" } end end function setup_shared_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) local target_type = "SharedLib" project_create(project_name, target_type) project_add_contents(source_root, rel_source_dirs, {}, extra_params) project_add_extern_libs(extern_libs, target_type) project_add_x11_dirs() if not extra_params["no_default_link"] then table.insert(static_lib_names, project_name) end if os.is("windows") then flags { "NoRTTI" } links { "delayimp" } end end -- this is where the source tree is chopped up into static libs. -- can be changed very easily; just copy+paste a new setup_static_lib_project, -- or remove existing ones. static libs are automagically added to -- main_exe link step. function setup_all_libs () -- relative to global source_root. local source_dirs = {} -- names of external libraries used (see libraries_dir comment) local extern_libs = {} source_dirs = { "network", } extern_libs = { "spidermonkey", "enet", "boost", -- dragged in via server->simulation.h->random + "miniupnpc" } setup_static_lib_project("network", source_dirs, extern_libs, {}) if not _OPTIONS["without-lobby"] then source_dirs = { "lobby", "lobby/scripting", } extern_libs = { "spidermonkey", "boost", "gloox", } setup_static_lib_project("lobby", source_dirs, extern_libs, {}) if _OPTIONS["use-shared-glooxwrapper"] and not _OPTIONS["build-shared-glooxwrapper"] then table.insert(static_lib_names_debug, "glooxwrapper_dbg") table.insert(static_lib_names_release, "glooxwrapper") else source_dirs = { "lobby/glooxwrapper", } extern_libs = { "boost", "gloox", } if _OPTIONS["build-shared-glooxwrapper"] then setup_shared_lib_project("glooxwrapper", source_dirs, extern_libs, {}) else setup_static_lib_project("glooxwrapper", source_dirs, extern_libs, {}) end end else source_dirs = { "lobby/scripting", } extern_libs = { "spidermonkey", "boost" } setup_static_lib_project("lobby", source_dirs, extern_libs, {}) files { source_root.."lobby/Globals.cpp" } end source_dirs = { "simulation2", "simulation2/components", "simulation2/helpers", "simulation2/scripting", "simulation2/serialization", "simulation2/system", "simulation2/testcomponents", } extern_libs = { "boost", "opengl", "spidermonkey", } setup_static_lib_project("simulation2", source_dirs, extern_libs, {}) source_dirs = { "scriptinterface", } extern_libs = { "boost", "spidermonkey", "valgrind", "sdl", } setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {}) source_dirs = { "ps", "ps/scripting", "ps/Network", "ps/GameSetup", "ps/XML", "soundmanager", "soundmanager/data", "soundmanager/items", "soundmanager/scripting", "scripting", "maths", "maths/scripting", } extern_libs = { "spidermonkey", "sdl", -- key definitions "libxml2", "opengl", "zlib", "boost", "enet", "libcurl", } if not _OPTIONS["without-audio"] then table.insert(extern_libs, "openal") table.insert(extern_libs, "vorbis") end setup_static_lib_project("engine", source_dirs, extern_libs, {}) source_dirs = { "graphics", "graphics/scripting", "renderer", "renderer/scripting", "third_party/mikktspace" } extern_libs = { "opengl", "sdl", -- key definitions "spidermonkey", -- for graphics/scripting "boost" } if not _OPTIONS["without-nvtt"] then table.insert(extern_libs, "nvtt") end setup_static_lib_project("graphics", source_dirs, extern_libs, {}) source_dirs = { "tools/atlas/GameInterface", "tools/atlas/GameInterface/Handlers" } extern_libs = { "boost", "sdl", -- key definitions "opengl", "spidermonkey" } setup_static_lib_project("atlas", source_dirs, extern_libs, {}) source_dirs = { "gui", "gui/scripting" } extern_libs = { "spidermonkey", "sdl", -- key definitions "opengl", "boost", } setup_static_lib_project("gui", source_dirs, extern_libs, {}) source_dirs = { "lib", "lib/adts", "lib/allocators", "lib/external_libraries", "lib/file", "lib/file/archive", "lib/file/common", "lib/file/io", "lib/file/vfs", "lib/pch", "lib/posix", "lib/res", "lib/res/graphics", "lib/sysdep", "lib/tex" } extern_libs = { "boost", "sdl", "opengl", "libpng", "zlib", "libjpg", "valgrind", "cxxtest", } -- CPU architecture-specific if arch == "amd64" then table.insert(source_dirs, "lib/sysdep/arch/amd64"); table.insert(source_dirs, "lib/sysdep/arch/x86_x64"); elseif arch == "x86" then table.insert(source_dirs, "lib/sysdep/arch/ia32"); table.insert(source_dirs, "lib/sysdep/arch/x86_x64"); elseif arch == "arm" then table.insert(source_dirs, "lib/sysdep/arch/arm"); end -- OS-specific sysdep_dirs = { linux = { "lib/sysdep/os/linux", "lib/sysdep/os/unix" }, -- note: RC file must be added to main_exe project. -- note: don't add "lib/sysdep/os/win/aken.cpp" because that must be compiled with the DDK. windows = { "lib/sysdep/os/win", "lib/sysdep/os/win/wposix", "lib/sysdep/os/win/whrt" }, macosx = { "lib/sysdep/os/osx", "lib/sysdep/os/unix" }, bsd = { "lib/sysdep/os/bsd", "lib/sysdep/os/unix", "lib/sysdep/os/unix/x" }, } for i,v in pairs(sysdep_dirs[os.get()]) do table.insert(source_dirs, v); end if os.is("linux") then if _OPTIONS["android"] then table.insert(source_dirs, "lib/sysdep/os/android") else table.insert(source_dirs, "lib/sysdep/os/unix/x") end end -- runtime-library-specific if _ACTION == "vs2005" or _ACTION == "vs2008" or _ACTION == "vs2010" or _ACTION == "vs2012" or _ACTION == "vs2013" then table.insert(source_dirs, "lib/sysdep/rtl/msc"); else table.insert(source_dirs, "lib/sysdep/rtl/gcc"); end setup_static_lib_project("lowlevel", source_dirs, extern_libs, {}) -- Third-party libraries that are built as part of the main project, -- not built externally and then linked source_dirs = { "third_party/mongoose", } extern_libs = { } setup_static_lib_project("mongoose", source_dirs, extern_libs, { no_pch = 1 }) -- CxxTest mock function support extern_libs = { "boost", "cxxtest", } -- 'real' implementations, to be linked against the main executable -- (files are added manually and not with setup_static_lib_project -- because not all files in the directory are included) setup_static_lib_project("mocks_real", {}, extern_libs, { no_default_link = 1, no_pch = 1 }) files { "mocks/*.h", source_root.."mocks/*_real.cpp" } -- 'test' implementations, to be linked against the test executable setup_static_lib_project("mocks_test", {}, extern_libs, { no_default_link = 1, no_pch = 1 }) files { source_root.."mocks/*.h", source_root.."mocks/*_test.cpp" } end -------------------------------------------------------------------------------- -- main EXE -------------------------------------------------------------------------------- -- used for main EXE as well as test used_extern_libs = { "opengl", "sdl", "libjpg", "libpng", "zlib", "spidermonkey", "libxml2", "boost", "cxxtest", "comsuppw", "enet", "libcurl", "valgrind", + "miniupnpc", } if not os.is("windows") and not _OPTIONS["android"] and not os.is("macosx") then -- X11 should only be linked on *nix table.insert(used_extern_libs, "x11") table.insert(used_extern_libs, "xcursor") end if not _OPTIONS["without-audio"] then table.insert(used_extern_libs, "openal") table.insert(used_extern_libs, "vorbis") end if not _OPTIONS["without-nvtt"] then table.insert(used_extern_libs, "nvtt") end if not _OPTIONS["without-lobby"] then table.insert(used_extern_libs, "gloox") end -- Bundles static libs together with main.cpp and builds game executable. function setup_main_exe () local target_type = get_main_project_target_type() project_create("pyrogenesis", target_type) links { "mocks_real" } local extra_params = { extra_files = { "main.cpp" }, no_pch = 1 } project_add_contents(source_root, {}, {}, extra_params) project_add_extern_libs(used_extern_libs, target_type) project_add_x11_dirs() -- Platform Specifics if os.is("windows") then files { source_root.."lib/sysdep/os/win/icon.rc" } -- from "lowlevel" static lib; must be added here to be linked in files { source_root.."lib/sysdep/os/win/error_dialog.rc" } flags { "NoRTTI" } linkoptions { -- wraps main thread in a __try block(see wseh.cpp). replace with mainCRTStartup if that's undesired. "/ENTRY:wseh_EntryPoint", -- see wstartup.h "/INCLUDE:_wstartup_InitAndRegisterShutdown", -- allow manual unload of delay-loaded DLLs "/DELAY:UNLOAD", } -- see manifest.cpp project_add_manifest() elseif os.is("linux") or os.is("bsd") then if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then links { "rt" } end if _OPTIONS["android"] then -- NDK's STANDALONE-TOOLCHAIN.html says this is required linkoptions { "-Wl,--fix-cortex-a8" } links { "log" } end if os.is("linux") or os.getversion().description == "GNU/kFreeBSD" then links { -- Dynamic libraries (needed for linking for gold) "dl", } elseif os.is("bsd") then links { -- Needed for backtrace* on BSDs "execinfo", } end -- Threading support buildoptions { "-pthread" } if not _OPTIONS["android"] then linkoptions { "-pthread" } end -- For debug_resolve_symbol configuration "Debug" linkoptions { "-rdynamic" } configuration { } elseif os.is("macosx") then links { "pthread" } linkoptions { "-framework ApplicationServices", "-framework Cocoa", "-framework CoreFoundation" } end end -------------------------------------------------------------------------------- -- atlas -------------------------------------------------------------------------------- -- setup a typical Atlas component project -- extra_params, rel_source_dirs and rel_include_dirs: as in project_add_contents; function setup_atlas_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params) local source_root = rootdir.."/source/tools/atlas/" .. project_name .. "/" project_create(project_name, target_type) -- if not specified, the default for atlas pch files is in the project root. if not extra_params["pch_dir"] then extra_params["pch_dir"] = source_root end project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params) project_add_extern_libs(extern_libs, target_type) project_add_x11_dirs() -- Platform Specifics if os.is("windows") then defines { "_UNICODE" } -- Link to required libraries links { "winmm", "comctl32", "rpcrt4", "delayimp", "ws2_32" } -- required to use WinMain() on Windows, otherwise will default to main() flags { "WinMain" } elseif os.is("linux") or os.is("bsd") then buildoptions { "-rdynamic", "-fPIC" } linkoptions { "-fPIC", "-rdynamic" } elseif os.is("macosx") then -- install_name settings aren't really supported yet by premake, but there are plans for the future. -- we currently use this hack to work around some bugs with wrong install_names. if target_type == "SharedLib" then if _OPTIONS["macosx-bundle"] then -- If we're building a bundle, it will be in ../Frameworks linkoptions { "-install_name @executable_path/../Frameworks/lib"..project_name..".dylib" } else linkoptions { "-install_name @executable_path/lib"..project_name..".dylib" } end end end end -- build all Atlas component projects function setup_atlas_projects() setup_atlas_project("AtlasObject", "StaticLib", { -- src "." },{ -- include },{ -- extern_libs "boost", "libxml2", "spidermonkey", "wxwidgets" },{ -- extra_params no_pch = 1 }) setup_atlas_project("AtlasScript", "StaticLib", { -- src "." },{ -- include ".." },{ -- extern_libs "boost", "spidermonkey", "valgrind", "wxwidgets", },{ -- extra_params no_pch = 1 }) atlas_src = { "ActorEditor", "CustomControls/Buttons", "CustomControls/Canvas", "CustomControls/ColourDialog", "CustomControls/DraggableListCtrl", "CustomControls/EditableListCtrl", "CustomControls/FileHistory", "CustomControls/HighResTimer", "CustomControls/MapDialog", "CustomControls/SnapSplitterWindow", "CustomControls/VirtualDirTreeCtrl", "CustomControls/Windows", "ErrorReporter", "General", "General/VideoRecorder", "Misc", "ScenarioEditor", "ScenarioEditor/Sections/Common", "ScenarioEditor/Sections/Cinematic", "ScenarioEditor/Sections/Environment", "ScenarioEditor/Sections/Map", "ScenarioEditor/Sections/Object", "ScenarioEditor/Sections/Player", "ScenarioEditor/Sections/Terrain", "ScenarioEditor/Sections/Trigger", "ScenarioEditor/Tools", "ScenarioEditor/Tools/Common", } atlas_extra_links = { "AtlasObject", "AtlasScript", } atlas_extern_libs = { "boost", "boost_signals", "comsuppw", --"ffmpeg", -- disabled for now because it causes too many build difficulties "libxml2", "sdl", -- key definitions "spidermonkey", "wxwidgets", "zlib", } if not os.is("windows") and not os.is("macosx") then -- X11 should only be linked on *nix table.insert(atlas_extern_libs, "x11") end setup_atlas_project("AtlasUI", "SharedLib", atlas_src, { -- include "..", "CustomControls", "Misc" }, atlas_extern_libs, { -- extra_params pch_dir = rootdir.."/source/tools/atlas/AtlasUI/Misc/", no_pch = (has_broken_pch), extra_links = atlas_extra_links, extra_files = { "Misc/atlas.rc" } }) end -- Atlas 'frontend' tool-launching projects function setup_atlas_frontend_project (project_name) local target_type = get_main_project_target_type() project_create(project_name, target_type) project_add_extern_libs({ "spidermonkey", }, target_type) project_add_x11_dirs() local source_root = rootdir.."/source/tools/atlas/AtlasFrontends/" files { source_root..project_name..".cpp" } if os.is("windows") then files { source_root..project_name..".rc" } end includedirs { source_root .. ".." } -- Platform Specifics if os.is("windows") then defines { "_UNICODE" } -- required to use WinMain() on Windows, otherwise will default to main() flags { "WinMain" } -- see manifest.cpp project_add_manifest() else -- Non-Windows, = Unix links { "AtlasObject" } end links { "AtlasUI" } end function setup_atlas_frontends() setup_atlas_frontend_project("ActorEditor") end -------------------------------------------------------------------------------- -- collada -------------------------------------------------------------------------------- function setup_collada_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params) project_create(project_name, target_type) local source_root = source_root.."collada/" extra_params["pch_dir"] = source_root project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params) project_add_extern_libs(extern_libs, target_type) project_add_x11_dirs() -- Platform Specifics if os.is("windows") then -- required to use WinMain() on Windows, otherwise will default to main() flags { "WinMain" } elseif os.is("linux") then defines { "LINUX" } links { "dl", } -- FCollada is not aliasing-safe, so disallow dangerous optimisations -- (TODO: It'd be nice to fix FCollada, but that looks hard) buildoptions { "-fno-strict-aliasing" } buildoptions { "-rdynamic" } linkoptions { "-rdynamic" } elseif os.is("bsd") then if os.getversion().description == "OpenBSD" then links { "c", } end if os.getversion().description == "GNU/kFreeBSD" then links { "dl", } end buildoptions { "-fno-strict-aliasing" } buildoptions { "-rdynamic" } linkoptions { "-rdynamic" } elseif os.is("macosx") then -- define MACOS-something? -- install_name settings aren't really supported yet by premake, but there are plans for the future. -- we currently use this hack to work around some bugs with wrong install_names. if target_type == "SharedLib" then if _OPTIONS["macosx-bundle"] then -- If we're building a bundle, it will be in ../Frameworks linkoptions { "-install_name @executable_path/../Frameworks/lib"..project_name..".dylib" } else linkoptions { "-install_name @executable_path/lib"..project_name..".dylib" } end end buildoptions { "-fno-strict-aliasing" } -- On OSX, fcollada uses a few utility functions from coreservices linkoptions { "-framework CoreServices" } end end -- build all Collada component projects function setup_collada_projects() setup_collada_project("Collada", "SharedLib", { -- src "." },{ -- include },{ -- extern_libs "fcollada", "libxml2" },{ -- extra_params }) end -------------------------------------------------------------------------------- -- tests -------------------------------------------------------------------------------- -- Cxxtestgen needs to create .cpp files from the .h files before they can be compiled. -- By default we are using prebuildcommands, but we are also using customizations of premake -- for makefiles. The reason is that premake currently has a bug with makefiles and parallel -- builds (e.g. -j5). It's not guaranteed that prebuildcommands always run before building. -- All the *.cpp and *.h files need to be added to files no matter if prebuildcommands -- or customizations are used. -- If no customizations are implemented for a specific action (e.g. vs2010), passing the -- parameters won't have any effects. function configure_cxxtestgen() local lcxxtestrootfile = source_root.."test_root.cpp" files { lcxxtestrootfile } -- Define the options used for cxxtestgen local lcxxtestoptions = "--have-std" local lcxxtestrootoptions = "--have-std" if os.is("windows") then lcxxtestrootoptions = lcxxtestrootoptions .. " --gui=PsTestWrapper --runner=Win32ODSPrinter" else lcxxtestrootoptions = lcxxtestrootoptions .. " --gui=PsTestWrapper --runner=ErrorPrinter" end -- Precompiled headers - the header is added to all generated .cpp files -- note that the header isn't actually precompiled here, only #included -- so that the build stage can use it as a precompiled header. local include = " --include=precompiled.h" lcxxtestrootoptions = lcxxtestrootoptions .. include lcxxtestoptions = lcxxtestoptions .. include -- Set all the parameters used in our cxxtestgen customization in premake. cxxtestrootfile(lcxxtestrootfile) cxxtestpath(lcxxtestpath) cxxtestrootoptions(lcxxtestrootoptions) cxxtestoptions(lcxxtestoptions) -- The file paths needs to be made relative to the project directory for the prebuildcommands. -- premake's paths are relative to premake4.lua by default. lcxxtestrootfile = path.rebase(lcxxtestrootfile, path.getabsolute("."), _OPTIONS["outpath"]) lcxxtestpath = path.rebase(lcxxtestpath, path.getabsolute("."), _OPTIONS["outpath"]) -- On windows we have to use backlashes in our paths. We don't have to take care -- of that for the parameters passed to our cxxtestgen customizations. if os.is("windows") then lcxxtestrootfile = path.translate(lcxxtestrootfile, "\\") lcxxtestpath = path.translate(lcxxtestpath, "\\") end if _ACTION ~= "gmake" and _ACTION ~= "vs2010" and _ACTION ~= "vs2012" and _ACTION ~= "vs2013" then prebuildcommands { lcxxtestpath.." --root "..lcxxtestrootoptions.." -o "..lcxxtestrootfile } end -- Find header files in 'test' subdirectories local all_files = os.matchfiles(source_root .. "**/tests/*.h") for i,v in pairs(all_files) do -- Don't include sysdep tests on the wrong sys -- Don't include Atlas tests unless Atlas is being built if not (string.find(v, "/sysdep/os/win/") and not os.is("windows")) and not (string.find(v, "/tools/atlas/") and not _OPTIONS["atlas"]) and not (string.find(v, "/sysdep/arch/x86_x64/") and ((arch ~= "amd64") or (arch ~= "x86"))) then local src_file = string.sub(v, 1, -3) .. ".cpp" cxxtestsrcfiles { src_file } files { src_file } cxxtesthdrfiles { v } if _ACTION ~= "gmake" and _ACTION ~= "vs2010" and _ACTION ~= "vs2012" and _ACTION ~= "vs2013" then -- see detailed comment above. src_file = path.rebase(src_file, path.getabsolute("."), _OPTIONS["outpath"]) v = path.rebase(v, path.getabsolute("."), _OPTIONS["outpath"]) if os.is("windows") then src_file = path.translate(src_file, "\\") v = path.translate(v, "\\") end prebuildcommands { lcxxtestpath.." --part "..lcxxtestoptions.." -o "..src_file.." "..v } end end end end function setup_tests() local target_type = get_main_project_target_type() project_create("test", target_type) configure_cxxtestgen() links { static_lib_names } configuration "Debug" links { static_lib_names_debug } configuration "Release" links { static_lib_names_release } configuration { } links { "mocks_test" } if _OPTIONS["atlas"] then links { "AtlasObject" } project_add_extern_libs({"wxwidgets"}, target_type) end extra_params = { extra_files = { "test_setup.cpp" }, } project_add_contents(source_root, {}, {}, extra_params) project_add_extern_libs(used_extern_libs, target_type) project_add_x11_dirs() -- TODO: should fix the duplication between this OS-specific linking -- code, and the similar version in setup_main_exe if os.is("windows") then -- from "lowlevel" static lib; must be added here to be linked in files { source_root.."lib/sysdep/os/win/error_dialog.rc" } flags { "NoRTTI" } -- see wstartup.h linkoptions { "/INCLUDE:_wstartup_InitAndRegisterShutdown" } -- Enables console for the TEST project on Windows linkoptions { "/SUBSYSTEM:CONSOLE" } project_add_manifest() elseif os.is("linux") or os.is("bsd") then if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then links { "rt" } end if _OPTIONS["android"] then -- NDK's STANDALONE-TOOLCHAIN.html says this is required linkoptions { "-Wl,--fix-cortex-a8" } end if os.is("linux") or os.getversion().description == "GNU/kFreeBSD" then links { -- Dynamic libraries (needed for linking for gold) "dl", } elseif os.is("bsd") then links { -- Needed for backtrace* on BSDs "execinfo", } end -- Threading support buildoptions { "-pthread" } if not _OPTIONS["android"] then linkoptions { "-pthread" } end -- For debug_resolve_symbol configuration "Debug" linkoptions { "-rdynamic" } configuration { } includedirs { source_root .. "pch/test/" } end end -- must come first, so that VC sets it as the default project and therefore -- allows running via F5 without the "where is the EXE" dialog. setup_main_exe() setup_all_libs() -- add the static libs to the main EXE project. only now (after -- setup_all_libs has run) are the lib names known. cannot move -- setup_main_exe to run after setup_all_libs (see comment above). -- we also don't want to hardcode the names - that would require more -- work when changing the static lib breakdown. project("pyrogenesis") -- Set the main project active links { static_lib_names } configuration "Debug" links { static_lib_names_debug } configuration "Release" links { static_lib_names_release } configuration { } if _OPTIONS["atlas"] then setup_atlas_projects() setup_atlas_frontends() end if _OPTIONS["collada"] then setup_collada_projects() end if not _OPTIONS["without-tests"] then setup_tests() end Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 14331) +++ ps/trunk/source/network/NetServer.cpp (revision 14332) @@ -1,959 +1,1023 @@ /* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "NetServer.h" #include "NetClient.h" #include "NetMessage.h" #include "NetSession.h" #include "NetStats.h" #include "NetTurnManager.h" #include "lib/external_libraries/enet.h" #include "ps/CLogger.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" +// Next four files are for UPnP port forwarding. +#include +#include +#include +#include + #define DEFAULT_SERVER_NAME L"Unnamed Server" #define DEFAULT_WELCOME_MESSAGE L"Welcome" #define MAX_CLIENTS 8 static const int CHANNEL_COUNT = 1; /** * enet_host_service timeout (msecs). * Smaller numbers may hurt performance; larger numbers will * hurt latency responding to messages from game thread. */ static const int HOST_SERVICE_TIMEOUT = 50; CNetServer* g_NetServer = NULL; static CStr DebugName(CNetServerSession* session) { if (session == NULL) return "[unknown host]"; if (session->GetGUID().empty()) return "[unauthed host]"; return "[" + session->GetGUID().substr(0, 8) + "...]"; } /** * Async task for receiving the initial game state to be forwarded to another * client that is rejoining an in-progress network game. */ class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ServerRejoin); public: CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID) : m_Server(server), m_RejoinerHostID(hostID) { } virtual void OnComplete() { // We've received the game state from an existing player - now // we need to send it onwards to the newly rejoining player // Find the session corresponding to the rejoining host (if any) CNetServerSession* session = NULL; for (size_t i = 0; i < m_Server.m_Sessions.size(); ++i) { if (m_Server.m_Sessions[i]->GetHostID() == m_RejoinerHostID) { session = m_Server.m_Sessions[i]; break; } } if (!session) { LOGMESSAGE(L"Net server: rejoining client disconnected before we sent to it"); return; } // Store the received state file, and tell the client to start downloading it from us // TODO: this will get kind of confused if there's multiple clients downloading in parallel; // they'll race and get whichever happens to be the latest received by the server, // which should still work but isn't great m_Server.m_JoinSyncFile = m_Buffer; CJoinSyncStartMessage message; session->SendMessage(&message); } private: CNetServerWorker& m_Server; u32 m_RejoinerHostID; }; /* * XXX: We use some non-threadsafe functions from the worker thread. * See http://trac.wildfiregames.com/ticket/654 */ CNetServerWorker::CNetServerWorker(int autostartPlayers) : m_AutostartPlayers(autostartPlayers), m_Shutdown(false), m_ScriptInterface(NULL), m_NextHostID(1), m_Host(NULL), m_Stats(NULL) { m_State = SERVER_STATE_UNCONNECTED; m_ServerTurnManager = NULL; m_ServerName = DEFAULT_SERVER_NAME; m_WelcomeMessage = DEFAULT_WELCOME_MESSAGE; } CNetServerWorker::~CNetServerWorker() { if (m_State != SERVER_STATE_UNCONNECTED) { // Tell the thread to shut down { CScopeLock lock(m_WorkerMutex); m_Shutdown = true; } // Wait for it to shut down cleanly pthread_join(m_WorkerThread, NULL); } // Clean up resources delete m_Stats; for (size_t i = 0; i < m_Sessions.size(); ++i) { m_Sessions[i]->DisconnectNow(NDR_UNEXPECTED_SHUTDOWN); delete m_Sessions[i]; } if (m_Host) { enet_host_destroy(m_Host); } delete m_ServerTurnManager; } bool CNetServerWorker::SetupConnection() { ENSURE(m_State == SERVER_STATE_UNCONNECTED); ENSURE(!m_Host); // Bind to default host ENetAddress addr; addr.host = ENET_HOST_ANY; addr.port = PS_DEFAULT_PORT; // Create ENet server m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0); if (!m_Host) { LOGERROR(L"Net server: enet_host_create failed"); return false; } m_Stats = new CNetStatsTable(); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); m_State = SERVER_STATE_PREGAME; // Launch the worker thread int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); + // Start UPnP Setup. + + // Values we want to set. + const char * internalPort = "20595"; + const char * externalPort = "20595"; + const char * leaseDuration = "86400"; // 24 Hours. + const char * description = "0AD Multiplayer"; + const char * protocall = "UDP"; + char internalIPAddress[64]; + char externalIPAddress[40]; + // Variables to hold the values that actually get set. + char intClient[40]; + char intPort[6]; + char duration[16]; + int r; + // Intermediate variables. + struct UPNPUrls urls; + struct IGDdatas data; + struct UPNPDev * devlist = 0; + + // Try getting the UPnP device for 7 seconds. TODO: Make this asynchronous? + devlist = upnpDiscover(7000, 0, 0, 0, 0, 0); + + // Get our internal IP address. + UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress)); + + // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance. + UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress); + + // See if we actually got our address. + if(externalIPAddress[0]) + LOGMESSAGE(L"Net server: ExternalIPAddress = %s\n", externalIPAddress); + else + LOGMESSAGE(L"Net server: GetExternalIPAddress failed.\n"); + + // Try to setup port forwarding. + r = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, + externalPort, internalPort, internalIPAddress, description, + protocall, 0, leaseDuration); + + // Check the port actually got forwarded. + if (r != UPNPCOMMAND_SUCCESS) + LOGMESSAGE(L"Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)\n", + externalPort, internalPort, internalIPAddress, r, strupnperror(r)); + r = UPNP_GetSpecificPortMappingEntry(urls.controlURL, + data.first.servicetype, + externalPort, protocall, + intClient, intPort, NULL/*desc*/, + NULL/*enabled*/, duration); + if(r!=UPNPCOMMAND_SUCCESS) + LOGMESSAGE(L"Net server: GetSpecificPortMappingEntry() failed with code %d (%s)\n", r, strupnperror(r)); + + // If we succeed, log it. + if(intClient[0]) + LOGMESSAGE(L"Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)\n", + externalIPAddress, externalPort, protocall, intClient, intPort, duration); + // End UPnP setup. + return true; } bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message) { ENSURE(m_Host); CNetServerSession* session = static_cast(peer->data); return CNetHost::SendMessage(message, peer, DebugName(session).c_str()); } bool CNetServerWorker::Broadcast(const CNetMessage* message) { ENSURE(m_Host); bool ok = true; // Send to all sessions that are active and has finished authentication for (size_t i = 0; i < m_Sessions.size(); ++i) { if (m_Sessions[i]->GetCurrState() == NSS_PREGAME || m_Sessions[i]->GetCurrState() == NSS_INGAME) { if (!m_Sessions[i]->SendMessage(message)) ok = false; // TODO: this does lots of repeated message serialisation if we have lots // of remote peers; could do it more efficiently if that's a real problem } } return ok; } void* CNetServerWorker::RunThread(void* data) { debug_SetThreadName("NetServer"); static_cast(data)->Run(); return NULL; } void CNetServerWorker::Run() { // To avoid the need for JS_SetContextThread, we create and use and destroy // the script interface entirely within this network thread m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime()); while (true) { if (!RunStep()) break; // Implement autostart mode if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers) StartGame(); // Update profiler stats m_Stats->LatchHostState(m_Host); } // Clear roots before deleting their context m_GameAttributes = CScriptValRooted(); m_SavedCommands.clear(); SAFE_DELETE(m_ScriptInterface); } bool CNetServerWorker::RunStep() { // Check for messages from the game thread. // (Do as little work as possible while the mutex is held open, // to avoid performance problems and deadlocks.) std::vector > newAssignPlayer; std::vector newStartGame; std::vector newGameAttributes; std::vector newTurnLength; { CScopeLock lock(m_WorkerMutex); if (m_Shutdown) return false; newStartGame.swap(m_StartGameQueue); newAssignPlayer.swap(m_AssignPlayerQueue); newGameAttributes.swap(m_GameAttributesQueue); newTurnLength.swap(m_TurnLengthQueue); } for (size_t i = 0; i < newAssignPlayer.size(); ++i) AssignPlayer(newAssignPlayer[i].first, newAssignPlayer[i].second); if (!newGameAttributes.empty()) UpdateGameAttributes(GetScriptInterface().ParseJSON(newGameAttributes.back())); if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); // Do StartGame last, so we have the most up-to-date game attributes when we start if (!newStartGame.empty()) StartGame(); // Perform file transfers for (size_t i = 0; i < m_Sessions.size(); ++i) m_Sessions[i]->GetFileTransferer().Poll(); // Process network events: ENetEvent event; int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT); if (status < 0) { LOGERROR(L"CNetServerWorker: enet_host_service failed (%d)", status); // TODO: notify game that the server has shut down return false; } if (status == 0) { // Reached timeout with no events - try again return true; } // Process the event: switch (event.type) { case ENET_EVENT_TYPE_CONNECT: { // Report the client address char hostname[256] = "(error)"; enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname)); LOGMESSAGE(L"Net server: Received connection from %hs:%u", hostname, (unsigned int)event.peer->address.port); // Set up a session object for this peer CNetServerSession* session = new CNetServerSession(*this, event.peer); m_Sessions.push_back(session); SetupSession(session); ENSURE(event.peer->data == NULL); event.peer->data = session; HandleConnect(session); break; } case ENET_EVENT_TYPE_DISCONNECT: { // If there is an active session with this peer, then reset and delete it CNetServerSession* session = static_cast(event.peer->data); if (session) { LOGMESSAGE(L"Net server: Disconnected %hs", DebugName(session).c_str()); // Remove the session first, so we won't send player-update messages to it // when updating the FSM m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end()); session->Update((uint)NMT_CONNECTION_LOST, NULL); delete session; event.peer->data = NULL; } break; } case ENET_EVENT_TYPE_RECEIVE: { // If there is an active session with this peer, then process the message CNetServerSession* session = static_cast(event.peer->data); if (session) { // Create message from raw data CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface()); if (msg) { LOGMESSAGE(L"Net server: Received message %hs of size %lu from %hs", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str()); HandleMessageReceive(msg, session); delete msg; } } // Done using the packet enet_packet_destroy(event.packet); break; } case ENET_EVENT_TYPE_NONE: break; } return true; } void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session) { // Handle non-FSM messages first Status status = session->GetFileTransferer().HandleMessageReceive(message); if (status != INFO::SKIPPED) return; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; // Rejoining client got our JoinSyncStart after we received the state from // another client, and has now requested that we forward it to them ENSURE(!m_JoinSyncFile.empty()); session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile); return; } // Update FSM bool ok = session->Update(message->GetType(), (void*)message); if (!ok) LOGERROR(L"Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState()); } void CNetServerWorker::SetupSession(CNetServerSession* session) { void* context = session; // Set up transitions for session session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context); session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context); session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, context); session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context); session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, context); // Set first state session->SetFirstState(NSS_HANDSHAKE); } bool CNetServerWorker::HandleConnect(CNetServerSession* session) { CSrvHandshakeMessage handshake; handshake.m_Magic = PS_PROTOCOL_MAGIC; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; return session->SendMessage(&handshake); } void CNetServerWorker::OnUserJoin(CNetServerSession* session) { AddPlayer(session->GetGUID(), session->GetUserName()); CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; session->SendMessage(&gameSetupMessage); CPlayerAssignmentMessage assignMessage; ConstructPlayerAssignmentMessage(assignMessage); session->SendMessage(&assignMessage); } void CNetServerWorker::OnUserLeave(CNetServerSession* session) { RemovePlayer(session->GetGUID()); if (m_ServerTurnManager) m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers // TODO: ought to switch the player controlled by that client // back to AI control, or something? } void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name) { // Find all player IDs in active use; we mustn't give them to a second player std::set usedIDs; for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) if (it->second.m_Enabled) usedIDs.insert(it->second.m_PlayerID); // If the player is rejoining after disconnecting, try to give them // back their old player ID i32 playerID = -1; bool foundPlayerID = false; // Try to match GUID first for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; foundPlayerID = true; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now break; } } // Try to match username next if (!foundPlayerID) { for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; foundPlayerID = true; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now break; } } } // Otherwise pick the first free player ID if (!foundPlayerID) { for (playerID = 1; usedIDs.find(playerID) != usedIDs.end(); ++playerID) { // (do nothing) } } PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = name; assignment.m_PlayerID = playerID; m_PlayerAssignments[guid] = assignment; // Send the new assignments to all currently active players // (which does not include the one that's just joining) SendPlayerAssignments(); } void CNetServerWorker::RemovePlayer(const CStr& guid) { m_PlayerAssignments[guid].m_Enabled = false; SendPlayerAssignments(); } void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) { // Remove anyone who's already assigned to this player for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (it->second.m_PlayerID == playerID) it->second.m_PlayerID = -1; } // Update this host's assignment if it exists if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end()) m_PlayerAssignments[guid].m_PlayerID = playerID; SendPlayerAssignments(); } void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message) { for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled) continue; CPlayerAssignmentMessage::S_m_Hosts h; h.m_GUID = it->first; h.m_Name = it->second.m_Name; h.m_PlayerID = it->second.m_PlayerID; message.m_Hosts.push_back(h); } } void CNetServerWorker::SendPlayerAssignments() { CPlayerAssignmentMessage message; ConstructPlayerAssignmentMessage(message); Broadcast(&message); } ScriptInterface& CNetServerWorker::GetScriptInterface() { return *m_ScriptInterface; } void CNetServerWorker::SetTurnLength(u32 msecs) { if (m_ServerTurnManager) m_ServerTurnManager->SetTurnLength(msecs); } bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef(); if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION) { session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION); return false; } CSrvHandshakeResponseMessage handshakeResponse; handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION; handshakeResponse.m_Message = server.m_WelcomeMessage; handshakeResponse.m_Flags = 0; session->SendMessage(&handshakeResponse); return true; } bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef(); CStrW username = server.DeduplicatePlayerName(SanitisePlayerName(message->m_Name)); bool isRejoining = false; if (server.m_State != SERVER_STATE_PREGAME) { // isRejoining = true; // uncomment this to test rejoining even if the player wasn't connected previously // Search for an old disconnected player of the same name // (TODO: if GUIDs were stable, we should use them instead) for (PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.begin(); it != server.m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->second.m_Name == username) { isRejoining = true; break; } } // Players who weren't already in the game are not allowed to join now that it's started if (!isRejoining) { LOGMESSAGE(L"Refused connection after game start from not-previously-known user \"%ls\"", username.c_str()); session->Disconnect(NDR_SERVER_ALREADY_IN_GAME); return true; } } // TODO: check server password etc? u32 newHostID = server.m_NextHostID++; session->SetUserName(username); session->SetGUID(message->m_GUID); session->SetHostID(newHostID); CAuthenticateResultMessage authenticateResult; authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK; authenticateResult.m_HostID = newHostID; authenticateResult.m_Message = L"Logged in"; session->SendMessage(&authenticateResult); server.OnUserJoin(session); if (isRejoining) { // Request a copy of the current game state from an existing player, // so we can send it on to the new player // Assume session 0 is most likely the local player, so they're // the most efficient client to request a copy from CNetServerSession* sourceSession = server.m_Sessions.at(0); sourceSession->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID)) ); session->SetNextState(NSS_JOIN_SYNCING); } return true; } bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event) { // TODO: should split each of these cases into a separate method CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CNetMessage* message = (CNetMessage*)event->GetParamRef(); if (message->GetType() == (uint)NMT_SIMULATION_COMMAND) { CSimulationMessage* simMessage = static_cast (message); // Send it back to all clients immediately server.Broadcast(simMessage); // Save all the received commands if (server.m_SavedCommands.size() < simMessage->m_Turn + 1) server.m_SavedCommands.resize(simMessage->m_Turn + 1); server.m_SavedCommands[simMessage->m_Turn].push_back(*simMessage); // TODO: we should do some validation of ownership (clients can't send commands on behalf of opposing players) // TODO: we shouldn't send the message back to the client that first sent it } else if (message->GetType() == (uint)NMT_SYNC_CHECK) { CSyncCheckMessage* syncMessage = static_cast (message); server.m_ServerTurnManager->NotifyFinishedClientUpdate(session->GetHostID(), syncMessage->m_Turn, syncMessage->m_Hash); } else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH) { CEndCommandBatchMessage* endMessage = static_cast (message); server.m_ServerTurnManager->NotifyFinishedClientCommands(session->GetHostID(), endMessage->m_Turn); } return true; } bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CChatMessage* message = (CChatMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message); return true; } bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // We're in the loading state, so wait until every player has loaded before // starting the game ENSURE(server.m_State == SERVER_STATE_LOADING); server.CheckGameLoadStatus(session); return true; } bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event) { // A client rejoining an in-progress game has now finished loading the // map and deserialized the initial state. // The simulation may have progressed since then, so send any subsequent // commands to them and set them as an active player so they can participate // in all future turns. // // (TODO: if it takes a long time for them to receive and execute all these // commands, the other players will get frozen for that time and may be unhappy; // we could try repeating this process a few times until the client converges // on the up-to-date state, before setting them as active.) ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef(); u32 turn = message->m_CurrentTurn; u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn(); // Send them all commands received since their saved state, // and turn-ended messages for any turns that have already been processed for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i) { if (i < server.m_SavedCommands.size()) for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j) session->SendMessage(&server.m_SavedCommands[i][j]); if (i <= readyTurn) { CEndCommandBatchMessage endMessage; endMessage.m_Turn = i; endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i); session->SendMessage(&endMessage); } } // Tell the turn manager to expect commands from this new client server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn); // Tell the client that everything has finished loading and it should start now CLoadedGameMessage loaded; loaded.m_CurrentTurn = readyTurn; session->SendMessage(&loaded); return true; } bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); server.OnUserLeave(session); return true; } void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession) { for (size_t i = 0; i < m_Sessions.size(); ++i) { if (m_Sessions[i] != changedSession && m_Sessions[i]->GetCurrState() != NSS_INGAME) return; } CLoadedGameMessage loaded; loaded.m_CurrentTurn = 0; Broadcast(&loaded); m_State = SERVER_STATE_INGAME; } void CNetServerWorker::StartGame() { m_ServerTurnManager = new CNetServerTurnManager(*this); for (size_t i = 0; i < m_Sessions.size(); ++i) m_ServerTurnManager->InitialiseClient(m_Sessions[i]->GetHostID(), 0); // TODO: only for non-observers m_State = SERVER_STATE_LOADING; // Send the final setup state to all clients UpdateGameAttributes(m_GameAttributes); SendPlayerAssignments(); CGameStartMessage gameStart; Broadcast(&gameStart); } void CNetServerWorker::UpdateGameAttributes(const CScriptValRooted& attrs) { m_GameAttributes = attrs; if (!m_Host) return; CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; Broadcast(&gameSetupMessage); } CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; CStrW name = original; name.Replace(L"[", L"{"); // remove GUI tags name.Replace(L"]", L"}"); // remove for symmetry // Restrict the length if (name.length() > MAX_LENGTH) name = name.Left(MAX_LENGTH); // Don't allow surrounding whitespace name.Trim(PS_TRIM_BOTH); // Don't allow empty name if (name.empty()) name = L"Anonymous"; return name; } CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original) { CStrW name = original; // Try names "Foo", "Foo (2)", "Foo (3)", etc size_t id = 2; while (true) { bool unique = true; for (size_t i = 0; i < m_Sessions.size(); ++i) { if (m_Sessions[i]->GetUserName() == name) { unique = false; break; } } if (unique) return name; name = original + L" (" + CStrW::FromUInt(id++) + L")"; } } CNetServer::CNetServer(int autostartPlayers) : m_Worker(new CNetServerWorker(autostartPlayers)) { } CNetServer::~CNetServer() { delete m_Worker; } bool CNetServer::SetupConnection() { return m_Worker->SetupConnection(); } void CNetServer::AssignPlayer(int playerID, const CStr& guid) { CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_AssignPlayerQueue.push_back(std::make_pair(playerID, guid)); } void CNetServer::StartGame() { CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_StartGameQueue.push_back(true); } void CNetServer::UpdateGameAttributes(const CScriptVal& attrs, ScriptInterface& scriptInterface) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = scriptInterface.StringifyJSON(attrs.get(), false); CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_GameAttributesQueue.push_back(attrsJSON); } void CNetServer::SetTurnLength(u32 msecs) { CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_TurnLengthQueue.push_back(msecs); }