diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 471bf268..37bb6332 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -27,7 +27,8 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y groff + # for Sphinx + sudo apt-get install -y python3-pip ./build-linux-64.sh - name: macOS Build diff --git a/CMakeLists.txt b/CMakeLists.txt index eb6580e4..20431f28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_policy(SET CMP0028 NEW) project (ericw-tools) +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) + # Grab the git describe output and store it in GIT_DESCRIBE # Thanks to http://xit0.org/2013/04/cmake-use-git-branch-and-commit-details-in-project/ execute_process( @@ -92,6 +94,7 @@ endif () add_subdirectory(qbsp) add_subdirectory(vis) +add_subdirectory(docs) install(FILES README.md DESTINATION bin) install(FILES changelog.md DESTINATION bin) diff --git a/README.md b/README.md index e33b8b68..04ab8077 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,15 @@ source code. ## Compiling -Dependencies: Embree 3.0+, TBB (TODO: version?), groff (for building manuals) +Dependencies: Embree 3.0+, TBB (TODO: version?), Sphinx (for building manuals) ### Ubuntu ``` -sudo apt install libembree-dev libtbb-dev groff cmake build-essential g++ +sudo apt install libembree-dev libtbb-dev cmake build-essential g++ +sudo apt install python3-pip +python3 -m pip install sphinx_rtd_theme +export PATH="~/.local/bin/:$PATH" git clone --recursive https://github.com/ericwa/ericw-tools cd ericw-tools mkdir build @@ -58,6 +61,8 @@ cmake .. Example using vcpkg (32-bit build): ``` +# TODO: sphinx installation + git clone --recursive https://github.com/ericwa/ericw-tools cd ericw-tools git clone https://github.com/microsoft/vcpkg @@ -78,6 +83,7 @@ cmake .. -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../vcpkg/scripts/buildsystems/vcpkg.cmak ``` brew install embree tbb +python3 -m pip install sphinx_rtd_theme git clone --recursive https://github.com/ericwa/ericw-tools cd ericw-tools mkdir build diff --git a/bsputil/CMakeLists.txt b/bsputil/CMakeLists.txt index 84fcd61c..e45d1301 100644 --- a/bsputil/CMakeLists.txt +++ b/bsputil/CMakeLists.txt @@ -5,4 +5,9 @@ set(BSPUTIL_SOURCES add_executable(bsputil ${BSPUTIL_SOURCES}) target_link_libraries(bsputil common TBB::tbb fmt::fmt) + +# HACK: copy .dll dependencies +add_custom_command(TARGET bsputil POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$") + install(TARGETS bsputil RUNTIME DESTINATION bin) diff --git a/build-linux-64.sh b/build-linux-64.sh index 737c0662..eaa793ee 100755 --- a/build-linux-64.sh +++ b/build-linux-64.sh @@ -1,5 +1,8 @@ #!/bin/bash +python3 -m pip install sphinx_rtd_theme +export PATH="~/.local/bin/:$PATH" + BUILD_DIR=build-linux if [ -d "$BUILD_DIR" ]; then diff --git a/build-osx.sh b/build-osx.sh index 887a4bea..90d5f0bf 100755 --- a/build-osx.sh +++ b/build-osx.sh @@ -3,6 +3,8 @@ # for sha256sum, used by the tests brew install coreutils +python3 -m pip install sphinx_rtd_theme + BUILD_DIR=build-osx EMBREE_ZIP="https://github.com/embree/embree/releases/download/v3.13.0/embree-3.13.0.x86_64.macosx.zip" diff --git a/cmake/FindSphinx.cmake b/cmake/FindSphinx.cmake new file mode 100644 index 00000000..9af7396e --- /dev/null +++ b/cmake/FindSphinx.cmake @@ -0,0 +1,14 @@ +# From: +# https://devblogs.microsoft.com/cppblog/clear-functional-c-documentation-with-sphinx-breathe-doxygen-cmake/ + +#Look for an executable called sphinx-build +find_program(SPHINX_EXECUTABLE + NAMES sphinx-build + DOC "Path to sphinx-build executable") + +include(FindPackageHandleStandardArgs) + +#Handle standard arguments to find_package like REQUIRED and QUIET +find_package_handle_standard_args(Sphinx + "Failed to find sphinx-build executable" + SPHINX_EXECUTABLE) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index ff161807..90d1a120 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -25,4 +25,4 @@ add_library(common STATIC ${CMAKE_SOURCE_DIR}/include/common/fs.hh ${CMAKE_SOURCE_DIR}/include/common/imglib.hh) -target_link_libraries(common ${CMAKE_THREAD_LIBS_INIT} fmt::fmt nlohmann_json::nlohmann_json) +target_link_libraries(common ${CMAKE_THREAD_LIBS_INIT} TBB::tbb fmt::fmt nlohmann_json::nlohmann_json) diff --git a/common/threads.cc b/common/threads.cc index e8d9a8bb..33df3ba3 100644 --- a/common/threads.cc +++ b/common/threads.cc @@ -169,3 +169,20 @@ void RunThreadsOn(int start, int workcnt, void *(func)(void *), void *arg) } #endif /* HAVE_THREADS */ + +/* + * ======================================================================= + * TBB + * ======================================================================= + */ + +std::unique_ptr ConfigureTBB(int maxthreads) +{ + auto tbbOptions = std::unique_ptr(); + + if (maxthreads > 0) { + tbbOptions = std::make_unique(tbb::global_control::max_allowed_parallelism, maxthreads); + } + + return tbbOptions; +} diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt new file mode 100644 index 00000000..a2556a66 --- /dev/null +++ b/docs/CMakeLists.txt @@ -0,0 +1,18 @@ +# From: +# https://devblogs.microsoft.com/cppblog/clear-functional-c-documentation-with-sphinx-breathe-doxygen-cmake/ + +find_package(Sphinx) + +if (Sphinx_FOUND) + set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}) + set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/docs/sphinx) + + add_custom_target(Sphinx ALL + COMMAND + ${SPHINX_EXECUTABLE} -b html + ${SPHINX_SOURCE} ${SPHINX_BUILD} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating documentation with Sphinx") + + install(DIRECTORY ${SPHINX_BUILD}/ DESTINATION doc) +endif() diff --git a/include/common/threads.hh b/include/common/threads.hh index bf94236d..5ef6ffc4 100644 --- a/include/common/threads.hh +++ b/include/common/threads.hh @@ -2,6 +2,10 @@ #pragma once +#include + +#include "tbb/global_control.h" + extern int numthreads; void LowerProcessPriority(void); @@ -15,3 +19,10 @@ void ThreadUnlock(void); /* Call if needing to print to stdout - should be called with lock held */ void InterruptThreadProgress__(void); + +/** + * Configures TBB to have the given max threads (specify 0 for unlimited). + * + * Call this from main() and keep the returned object until main() finishes. + */ +std::unique_ptr ConfigureTBB(int maxthreads); diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index edfeb647..d1d00d81 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -117,7 +117,7 @@ public: bool fLeakTest = false; bool fContentHack = false; vec_t worldExtent = 0.f; - bool fNoThreads = false; + int threads = 0; // 0 = let TBB auto select ideal number of threads bool includeSkip = false; bool fNoTJunc = false; }; diff --git a/qbsp/CMakeLists.txt b/qbsp/CMakeLists.txt index 6525c62c..01156143 100644 --- a/qbsp/CMakeLists.txt +++ b/qbsp/CMakeLists.txt @@ -38,6 +38,10 @@ target_link_libraries(qbsp libqbsp) install(TARGETS qbsp RUNTIME DESTINATION bin) +# HACK: copy .dll dependencies +add_custom_command(TARGET qbsp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$") + # test (copied from light/CMakeLists.txt) set(QBSP_TEST_SOURCE diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 50d1a804..9262bdf1 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -603,10 +604,10 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) // build all the portals in the bsp tree // some portals are solid polygons, and some are paths to other leafs - if (entity == pWorldEnt() && !options.fNofill) { + if (entity == pWorldEnt()) { // assume non-world bmodels are simple PortalizeWorld(entity, nodes, hullnum); - if (FillOutside(nodes, hullnum)) { + if (!options.fNofill && FillOutside(nodes, hullnum)) { FreeAllPortals(nodes); // get the remaining faces together into surfaces again @@ -1059,7 +1060,7 @@ PrintOptions " -expand Write hull 1 expanded brushes to expanded.map for debugging\n" " -leaktest Make compilation fail if the map leaks\n" " -contenthack Hack to fix leaks through solids. Causes missing faces in some cases so disabled by default.\n" - " -nothreads Disable multithreading\n" + " -threads n Set the number of threads (1 to disable multithreading)\n" " sourcefile .MAP file to process\n" " destfile .BSP file to output\n"); @@ -1295,8 +1296,11 @@ static void ParseOptions(char *szOptions) options.fLeakTest = true; } else if (!Q_strcasecmp(szTok, "contenthack")) { options.fContentHack = true; - } else if (!Q_strcasecmp(szTok, "nothreads")) { - options.fNoThreads = true; + } else if (!Q_strcasecmp(szTok, "threads")) { + szTok2 = GetTok(szTok + strlen(szTok) + 1, szEnd); + if (!szTok2) + FError("Invalid argument to option {}", szTok); + options.threads = atoi(szTok2); } else if (!Q_strcasecmp(szTok, "?") || !Q_strcasecmp(szTok, "help")) { PrintOptions(); } else { @@ -1421,11 +1425,7 @@ int qbsp_main(int argc, const char **argv) InitQBSP(argc, argv); - // disable TBB if requested - auto tbbOptions = std::unique_ptr(); - if (options.fNoThreads) { - tbbOptions = std::make_unique(tbb::global_control::max_allowed_parallelism, 1); - } + auto tbbOptions = ConfigureTBB(options.threads); // do it! auto start = I_FloatTime(); diff --git a/testmaps/automatated_tests.sh b/testmaps/automatated_tests.sh index 6578a83f..29f4d8d7 100755 --- a/testmaps/automatated_tests.sh +++ b/testmaps/automatated_tests.sh @@ -1,7 +1,7 @@ #!/bin/bash # usage: -# ./automated_tests.sh [--update-hashes] +# ./automated_tests.sh [--update-hashes|--continue-on-failure] # # If --update-hashes is given, updates the expected hash files. # Otherwise tests the generated .bsp's match the expected hashes. @@ -14,8 +14,11 @@ set -x UPDATE_HASHES=0 +CONTINUE_ON_FAILURE=0 if [[ "$1" == "--update-hashes" ]]; then UPDATE_HASHES=1 +elif [[ "$1" == "--continue-on-failure" ]]; then + CONTINUE_ON_FAILURE=1 elif [[ "$1" != "" ]]; then echo "usage: ./automated_tests.sh [--update-hashes]" exit 1 @@ -93,7 +96,10 @@ for bsp in ${COMMIT_JSON_MAPS}; do echo "Diff returned $diffreturn" file reference_bsp_json/${bsp}.json file ${bsp}.json - exit 1 + + if [[ $CONTINUE_ON_FAILURE -ne 1]]; then + exit 1 + fi fi fi done @@ -122,7 +128,12 @@ qbsp -noverbose qbspfeatures.map || if [[ $UPDATE_HASHES -ne 0 ]]; then sha256sum ${HASH_CHECK_BSPS} ${HASH_CHECK_PRTS} > qbsp.sha256sum || exit 1 else - sha256sum --strict --check qbsp.sha256sum || exit 1 + sha256sum --strict --check qbsp.sha256sum + + hash_check_return=$? + if [[ $hash_check_return -ne 0 ]] && [[ $CONTINUE_ON_FAILURE -ne 1]]; then + exit 1 + fi fi # now run vis @@ -139,7 +150,12 @@ wait if [[ $UPDATE_HASHES -ne 0 ]]; then sha256sum ${HASH_CHECK_BSPS} > qbsp-vis.sha256sum || exit 1 else - sha256sum --strict --check qbsp-vis.sha256sum || exit 1 + sha256sum --strict --check qbsp-vis.sha256sum + + hash_check_return=$? + if [[ $hash_check_return -ne 0 ]] && [[ $CONTINUE_ON_FAILURE -ne 1]]; then + exit 1 + fi fi # FIXME: light output is nondeterministic so we can't check the hashes currently diff --git a/testmaps/qbsp_leaf_contents_bug.map b/testmaps/qbsp_leaf_contents_bug.map new file mode 100644 index 00000000..e3f3cffb --- /dev/null +++ b/testmaps/qbsp_leaf_contents_bug.map @@ -0,0 +1,143 @@ +// Game: Quake +// Format: Valve + +// Noclip forward from the start position, and turn around +// Fire the rocket launcher. +// +// Expected: it explodes immediately +// Bug: you're in an empty leaf, so it flies into the wall +// (also, the fact that there's a face facing the void) +// +// Explanation: +// There's a "wedge" shaped brush sealing the map, +// which forms a T-junction with another brush sealing the map. +// This second brush isn't split by the wedge, and +// the code in MarkFacesTouchingOccupiedLeafs prefers to keep +// this non-split face (which breaks leaf content assignment +// and causes some empty faces in the void) rather than delete it +// (which would create HOMs). + +// entity 0 +{ +"classname" "worldspawn" +"wad" "deprecated/free_wad.wad" +"_tb_def" "builtin:Quake.fgd" +// brush 0 +{ +( 1952 -960 2432 ) ( 1952 -960 2400 ) ( 1952 -832 2400 ) bolt18 [ -1.1102230246251565e-16 -1 0 169.92432 ] [ 0 0 -1.0000000000000002 0 ] 0 1 1 +( 1760 -960 2464 ) ( 1920 -960 2592 ) ( 1920 -960 2464 ) bolt18 [ 1 -1.1102230246251565e-16 0 -54.075684 ] [ 0 0 -1.0000000000000002 0 ] 0 1 1 +( 1984 -928 2304 ) ( 1824 -768 2304 ) ( 1664 -928 2304 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1984 -928 2400 ) ( 1664 -928 2400 ) ( 1824 -768 2400 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1856 -928 2464 ) ( 1920 -928 2464 ) ( 1920 -928 2592 ) bolt18 [ -1 1.1102230246251565e-16 0 105.92456 ] [ 0 0 -1.0000000000000002 0 ] 0 1 1 +( 1984 -928 2304 ) ( 1984 -1216 2240 ) ( 1984 -1216 2304 ) bolt18 [ 1.1102230246251565e-16 1 0 -182.07556 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 1 +{ +( 2112 -928 1984 ) ( 2112 -928 2464 ) ( 1984 -960 2464 ) bolt18 [ -0.9701425001453319 -0.24253562503633297 0 0 ] [ -0 0 -1 0 ] 0 1 1 +( 2112 -960 1984 ) ( 1984 -960 1984 ) ( 1984 -960 2464 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2112 -960 1984 ) ( 2112 -928 1984 ) ( 1984 -960 1984 ) bolt18 [ -1 0 0 0 ] [ 1.3877787807814457e-17 -1 0 0 ] 357.6386 1 1 +( 1984 -960 2464 ) ( 2112 -928 2464 ) ( 2112 -960 2464 ) bolt18 [ 1 0 0 0 ] [ 1.3877787807814457e-17 -1 0 0 ] 2.3613892 1 1 +( 2112 -960 2464 ) ( 2112 -928 2464 ) ( 2112 -928 1984 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 2 +{ +( 1920 -960 2368 ) ( 1920 -960 2336 ) ( 1920 -832 2336 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1760 -992 2464 ) ( 1920 -992 2592 ) ( 1920 -992 2464 ) bolt18 [ 1 -1.1102230246251565e-16 0 -54.075684 ] [ 0 0 -1.0000000000000002 0 ] 0 1 1 +( 1984 -928 2304 ) ( 1824 -768 2304 ) ( 1664 -928 2304 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1984 -928 2400 ) ( 1664 -928 2400 ) ( 1824 -768 2400 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1760 -960 2464 ) ( 1920 -960 2464 ) ( 1920 -960 2592 ) bolt18 [ 1 -1.1102230246251565e-16 0 -54.075684 ] [ 0 0 -1.0000000000000002 0 ] 0 1 1 +( 1984 -928 2304 ) ( 1984 -1216 2240 ) ( 1984 -1216 2304 ) bolt18 [ 1.1102230246251565e-16 1 0 -182.07556 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 3 +{ +( 1664 -960 2304 ) ( 1664 -959 2304 ) ( 1664 -960 2305 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1888 -960 2304 ) ( 1888 -960 2305 ) ( 1889 -960 2304 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1888 -960 2304 ) ( 1889 -960 2304 ) ( 1888 -959 2304 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1920 -944 2400 ) ( 1920 -943 2400 ) ( 1921 -944 2400 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1920 -944 2320 ) ( 1921 -944 2320 ) ( 1920 -944 2321 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1920 -944 2320 ) ( 1920 -944 2321 ) ( 1920 -943 2320 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 4 +{ +( 1632 -960 2448 ) ( 1632 -959 2448 ) ( 1632 -960 2449 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1632 -960 2448 ) ( 1632 -960 2449 ) ( 1633 -960 2448 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -672 1984 ) ( 1664 -960 1984 ) ( 1984 -960 1984 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1664 -928 2464 ) ( 1664 -927 2464 ) ( 1665 -928 2464 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1664 -384 2464 ) ( 1665 -384 2464 ) ( 1664 -384 2465 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1664 -928 2464 ) ( 1664 -928 2465 ) ( 1664 -927 2464 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 5 +{ +( 1632 -960 1968 ) ( 1632 -959 1968 ) ( 1632 -960 1969 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1632 -960 1968 ) ( 1632 -960 1969 ) ( 1633 -960 1968 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1632 -960 1968 ) ( 1633 -960 1968 ) ( 1632 -959 1968 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2256 -320 1984 ) ( 2256 -319 1984 ) ( 2257 -320 1984 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2256 -384 1984 ) ( 2257 -384 1984 ) ( 2256 -384 1985 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2256 -320 1984 ) ( 2256 -320 1985 ) ( 2256 -319 1984 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 6 +{ +( 1632 -960 2464 ) ( 1632 -959 2464 ) ( 1632 -960 2465 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +( 1632 -960 2464 ) ( 1632 -960 2465 ) ( 1633 -960 2464 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +( 1632 -960 2464 ) ( 1633 -960 2464 ) ( 1632 -959 2464 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2256 -320 2480 ) ( 2256 -319 2480 ) ( 2257 -320 2480 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2256 -384 2480 ) ( 2257 -384 2480 ) ( 2256 -384 2481 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +( 2256 -320 2480 ) ( 2256 -320 2481 ) ( 2256 -319 2480 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +} +// brush 7 +{ +( 1664 -400 1984 ) ( 1664 -399 1984 ) ( 1664 -400 1985 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1696 -400 1984 ) ( 1696 -400 1985 ) ( 1697 -400 1984 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1696 -400 1984 ) ( 1697 -400 1984 ) ( 1696 -399 1984 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2256 -384 2464 ) ( 2256 -383 2464 ) ( 2257 -384 2464 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2256 -384 2000 ) ( 2257 -384 2000 ) ( 2256 -384 2001 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2256 -384 2000 ) ( 2256 -384 2001 ) ( 2256 -383 2000 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 8 +{ +( 2112 -944 1984 ) ( 2112 -943 1984 ) ( 2112 -944 1985 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2112 -944 1984 ) ( 2112 -944 1985 ) ( 2113 -944 1984 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2112 -944 1984 ) ( 2113 -944 1984 ) ( 2112 -943 1984 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2128 -384 2464 ) ( 2128 -383 2464 ) ( 2129 -384 2464 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2128 -400 2000 ) ( 2129 -400 2000 ) ( 2128 -400 2001 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2128 -384 2000 ) ( 2128 -384 2001 ) ( 2128 -383 2000 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 9 +{ +( 1664 -960 2304 ) ( 1664 -672 2240 ) ( 1664 -672 2304 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -960 2304 ) ( 1664 -960 2240 ) ( 1664 -960 2304 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -672 1984 ) ( 1664 -960 1984 ) ( 1984 -960 1984 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1984 -672 2304 ) ( 1664 -672 2304 ) ( 1824 -512 2304 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1984 -928 2400 ) ( 2112 -928 2416 ) ( 1984 -928 2416 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -672 2304 ) ( 1984 -960 2240 ) ( 1984 -960 2304 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 10 +{ +( 1664 -960 2464 ) ( 1664 -672 2400 ) ( 1664 -672 2464 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -960 2464 ) ( 1664 -960 2400 ) ( 1664 -960 2464 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -672 2400 ) ( 1664 -960 2400 ) ( 1984 -960 2400 ) bolt18 [ -1.0000000000000002 0 0 0 ] [ 0 -1.0000000000000002 0 -96 ] 0 1 1 +( 1984 -672 2464 ) ( 1664 -672 2464 ) ( 1824 -512 2464 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 1984 -928 2400 ) ( 2112 -928 2416 ) ( 1984 -928 2416 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1984 -672 2464 ) ( 1984 -960 2400 ) ( 1984 -960 2464 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 11 +{ +( 2000 -928 2288 ) ( 2000 -927 2288 ) ( 2000 -928 2289 ) bolt18 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1968 -912 2288 ) ( 1968 -912 2289 ) ( 1969 -912 2288 ) bolt18 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 1968 -928 2288 ) ( 1969 -928 2288 ) ( 1968 -927 2288 ) bolt18 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2080 -816 2304 ) ( 2080 -815 2304 ) ( 2081 -816 2304 ) bolt18 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 2080 -816 2304 ) ( 2081 -816 2304 ) ( 2080 -816 2305 ) bolt18 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 2080 -816 2304 ) ( 2080 -816 2305 ) ( 2080 -815 2304 ) bolt18 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "2048 -864 2360" +"angle" "270" +} +// entity 2 +{ +"classname" "light" +"origin" "1992 -520 2312" +}