diff --git a/BUILD b/BUILD
index 3c683b19..dd59d8ac 100644
--- a/BUILD
+++ b/BUILD
@@ -2,7 +2,9 @@
# DeepMind Lab, a machine-learning research environment
# forked from ioquake/ioq3.
-licenses(["restricted"]) # GPLv2
+licenses(["restricted"]) # GPLv2, code/tools/lcc is separate
+
+exports_files(["LICENSE"])
CODE_DIR = "engine/code"
@@ -545,280 +547,188 @@ cc_binary(
deps = ["@zlib_archive//:zlib"],
)
-# To link with the SDL frontend, depend on :game_lib_sdl
-# and add "-lGL" to the linker flags.
+cc_library(
+ name = "level_cache_types",
+ hdrs = ["public/level_cache_types.h"],
+ visibility = ["//visibility:public"],
+)
+
+IOQ3_COMMON_SRCS = [
+ CODE_DIR + "/asm/ftola.c",
+ CODE_DIR + "/asm/qasm-inline.h",
+ CODE_DIR + "/asm/snapvector.c",
+ CODE_DIR + "/cgame/cg_public.h",
+ CODE_DIR + "/client/libmumblelink.c",
+ CODE_DIR + "/deepmind/context.h",
+ CODE_DIR + "/deepmind/dm_public.h",
+ CODE_DIR + "/deepmind/dmlab_load_model.c",
+ CODE_DIR + "/deepmind/dmlab_load_model.h",
+ CODE_DIR + "/deepmind/dmlab_recording.c",
+ CODE_DIR + "/deepmind/dmlab_recording.h",
+ CODE_DIR + "/deepmind/dmlab_save_model.c",
+ CODE_DIR + "/deepmind/dmlab_save_model.h",
+ CODE_DIR + "/game/bg_public.h",
+ CODE_DIR + "/game/g_public.h",
+ CODE_DIR + "/renderergl2/tr_image_dds.c",
+ CODE_DIR + "/sys/con_log.c",
+ CODE_DIR + "/sys/con_passive.c",
+ CODE_DIR + "/sys/sys_main.c",
+ CODE_DIR + "/sys/sys_unix.c",
+ CODE_DIR + "/ui/ui_public.h",
+] + glob(
+ include = [
+ CODE_DIR + "/botlib/*.c",
+ CODE_DIR + "/botlib/*.h",
+ CODE_DIR + "/client/*.h",
+ CODE_DIR + "/client/cl_*.c",
+ CODE_DIR + "/client/snd_*.c",
+ CODE_DIR + "/qcommon/*.c",
+ CODE_DIR + "/qcommon/*.h",
+ CODE_DIR + "/renderercommon/*.c",
+ CODE_DIR + "/renderercommon/*.h",
+ CODE_DIR + "/renderergl1/*.c",
+ CODE_DIR + "/renderergl1/*.h",
+ CODE_DIR + "/server/*.c",
+ CODE_DIR + "/server/*.h",
+ CODE_DIR + "/sys/*.h",
+ ],
+ exclude = [
+ CODE_DIR + "/client/fx_*.h",
+ CODE_DIR + "/renderercommon/tr_types.h",
+ CODE_DIR + "/renderercommon/tr_public.h",
+ CODE_DIR + "/renderergl1/tr_local.h",
+ CODE_DIR + "/renderergl1/tr_subs.c",
+ CODE_DIR + "/server/sv_rankings.c",
+ CODE_DIR + "/qcommon/vm_armv7l.c",
+ CODE_DIR + "/qcommon/vm_none.c",
+ CODE_DIR + "/qcommon/vm_powerpc*.c",
+ CODE_DIR + "/qcommon/vm_powerpc_asm.h",
+ CODE_DIR + "/qcommon/vm_sparc.c",
+ CODE_DIR + "/qcommon/vm_sparc.h",
+ ],
+)
+
+IOQ3_COMMON_DEPS = [
+ ":level_cache_types",
+ ":qcommon_hdrs",
+ "//deepmind/include:context_hdrs",
+ "//third_party/rl_api:env_c_api",
+ "@jpeg_archive//:jpeg",
+ "@sdl_system//:sdl2",
+ "@zlib_archive//:zlib",
+]
+
+IOQ3_COMMON_TEXTUAL_HDRS = [
+ CODE_DIR + "/renderercommon/tr_types.h",
+ CODE_DIR + "/renderercommon/tr_public.h",
+ CODE_DIR + "/renderergl1/tr_local.h",
+]
+
+IOQ3_COMMON_COPTS = [
+ "-std=c99",
+ "-fno-strict-aliasing",
+ ARCH_VAR,
+ STANDALONE_VAR,
+]
+
+IOQ3_COMMON_DEFINES = [
+ "BOTLIB",
+ "_GNU_SOURCE",
+]
+
cc_library(
name = "game_lib_sdl",
- srcs = [
- CODE_DIR + "/asm/ftola.c",
- CODE_DIR + "/asm/qasm-inline.h",
- CODE_DIR + "/asm/snapvector.c",
- CODE_DIR + "/cgame/cg_public.h",
- CODE_DIR + "/client/libmumblelink.c",
- CODE_DIR + "/deepmind/dm_public.h",
+ srcs = IOQ3_COMMON_SRCS + [
CODE_DIR + "/deepmind/dmlab_connect.c",
- CODE_DIR + "/game/bg_public.h",
- CODE_DIR + "/game/g_public.h",
CODE_DIR + "/sdl/sdl_icon.h",
CODE_DIR + "/sdl/sdl_input.c",
CODE_DIR + "/sdl/sdl_snd.c",
- CODE_DIR + "/sys/con_log.c",
- CODE_DIR + "/sys/con_passive.c",
- CODE_DIR + "/sys/sys_main.c",
- CODE_DIR + "/sys/sys_unix.c",
- CODE_DIR + "/ui/ui_public.h",
## OpenGL rendering
CODE_DIR + "/sdl/sdl_gamma.c",
CODE_DIR + "/sdl/sdl_glimp.c",
- ] + glob(
- [
- CODE_DIR + "/botlib/*.c",
- CODE_DIR + "/client/cl_*.c",
- CODE_DIR + "/client/snd_*.c",
- CODE_DIR + "/qcommon/*.c",
- CODE_DIR + "/renderercommon/*.c",
- CODE_DIR + "/renderergl1/*.c",
- CODE_DIR + "/server/*.c",
- ],
- exclude = [
- CODE_DIR + "/renderergl1/tr_subs.c",
- CODE_DIR + "/server/sv_rankings.c",
- CODE_DIR + "/qcommon/vm_none.c",
- CODE_DIR + "/qcommon/vm_powerpc*.c",
- CODE_DIR + "/qcommon/vm_sparc.c",
- ],
- ),
- hdrs = [
- "public/dmlab.h",
- CODE_DIR + "/deepmind/context.h",
- ] + glob(
- [
- CODE_DIR + "/botlib/*.h",
- CODE_DIR + "/client/*.h",
- CODE_DIR + "/qcommon/*.h",
- CODE_DIR + "/sys/*.h",
- CODE_DIR + "/server/*.h",
- CODE_DIR + "/renderercommon/*.h",
- CODE_DIR + "/renderergl1/*.h",
- ],
- exclude = [
- CODE_DIR + "/client/fx_*.h",
- CODE_DIR + "/qcommon/vm_powerpc_asm.h",
- CODE_DIR + "/qcommon/vm_sparc.h",
- CODE_DIR + "/renderercommon/tr_types.h",
- CODE_DIR + "/renderercommon/tr_public.h",
- CODE_DIR + "/renderergl1/tr_local.h",
- ],
- ),
- copts = [
- "-std=c99",
- "-fno-strict-aliasing",
- ARCH_VAR,
- STANDALONE_VAR,
- ],
- defines = [
- "BOTLIB",
- "_GNU_SOURCE",
- ],
- textual_hdrs = [
- CODE_DIR + "/renderercommon/tr_types.h",
- CODE_DIR + "/renderercommon/tr_public.h",
- CODE_DIR + "/renderergl1/tr_local.h",
- ],
- deps = [
- ":qcommon_hdrs",
- "//deepmind/include:context_headers",
- "//third_party/rl_api:env_c_api",
- "@jpeg_archive//:jpeg",
- "@sdl_system//:sdl2",
],
+ hdrs = ["public/dmlab.h"],
+ copts = IOQ3_COMMON_COPTS,
+ defines = IOQ3_COMMON_DEFINES,
+ linkopts = ["-lGL"],
+ textual_hdrs = IOQ3_COMMON_TEXTUAL_HDRS,
+ deps = IOQ3_COMMON_DEPS,
)
-# To link with the headless OSMesa frontend, depend on :game_lib_headless_osmesa
-# and add "-lGL -lOSMesa" to the linker flags.
cc_library(
name = "game_lib_headless_osmesa",
- srcs = [
- CODE_DIR + "/asm/ftola.c",
- CODE_DIR + "/asm/qasm-inline.h",
- CODE_DIR + "/asm/snapvector.c",
- CODE_DIR + "/cgame/cg_public.h",
- CODE_DIR + "/client/libmumblelink.c",
- CODE_DIR + "/deepmind/dm_public.h",
+ srcs = IOQ3_COMMON_SRCS + [
CODE_DIR + "/deepmind/dmlab_connect.c",
- CODE_DIR + "/game/bg_public.h",
- CODE_DIR + "/game/g_public.h",
CODE_DIR + "/null/null_input.c",
CODE_DIR + "/null/null_snddma.c",
- CODE_DIR + "/sys/con_log.c",
- CODE_DIR + "/sys/con_passive.c",
- CODE_DIR + "/sys/sys_main.c",
- CODE_DIR + "/sys/sys_unix.c",
- CODE_DIR + "/ui/ui_public.h",
## OpenGL rendering
CODE_DIR + "/deepmind/headless_osmesa_glimp.c",
CODE_DIR + "/deepmind/glimp_common.h",
CODE_DIR + "/deepmind/glimp_common.c",
- ] + glob(
- [
- CODE_DIR + "/botlib/*.c",
- CODE_DIR + "/client/cl_*.c",
- CODE_DIR + "/client/snd_*.c",
- CODE_DIR + "/qcommon/*.c",
- CODE_DIR + "/renderercommon/*.c",
- CODE_DIR + "/renderergl1/*.c",
- CODE_DIR + "/server/*.c",
- ],
- exclude = [
- CODE_DIR + "/renderergl1/tr_subs.c",
- CODE_DIR + "/server/sv_rankings.c",
- CODE_DIR + "/qcommon/vm_none.c",
- CODE_DIR + "/qcommon/vm_powerpc*.c",
- CODE_DIR + "/qcommon/vm_sparc.c",
- ],
- ),
- hdrs = [
- "public/dmlab.h",
- CODE_DIR + "/deepmind/context.h",
- ] + glob(
- [
- CODE_DIR + "/botlib/*.h",
- CODE_DIR + "/client/*.h",
- CODE_DIR + "/qcommon/*.h",
- CODE_DIR + "/sys/*.h",
- CODE_DIR + "/server/*.h",
- CODE_DIR + "/renderercommon/*.h",
- CODE_DIR + "/renderergl1/*.h",
- ],
- exclude = [
- CODE_DIR + "/client/fx_*.h",
- CODE_DIR + "/qcommon/vm_powerpc_asm.h",
- CODE_DIR + "/qcommon/vm_sparc.h",
- CODE_DIR + "/renderercommon/tr_types.h",
- CODE_DIR + "/renderercommon/tr_public.h",
- CODE_DIR + "/renderergl1/tr_local.h",
- ],
- ),
- copts = [
- "-std=c99",
- "-fno-strict-aliasing",
- ARCH_VAR,
- STANDALONE_VAR,
- ],
- defines = [
- "BOTLIB",
- "_GNU_SOURCE",
- ],
- textual_hdrs = [
- CODE_DIR + "/renderercommon/tr_types.h",
- CODE_DIR + "/renderercommon/tr_public.h",
- CODE_DIR + "/renderergl1/tr_local.h",
- ],
- deps = [
- ":qcommon_hdrs",
- "//deepmind/include:context_headers",
- "//third_party/rl_api:env_c_api",
- "@jpeg_archive//:jpeg",
- "@sdl_system//:sdl2",
],
+ hdrs = ["public/dmlab.h"],
+ copts = IOQ3_COMMON_COPTS,
+ defines = IOQ3_COMMON_DEFINES,
+ linkopts = ["-lOSMesa"],
+ textual_hdrs = IOQ3_COMMON_TEXTUAL_HDRS,
+ deps = IOQ3_COMMON_DEPS,
)
-# To link with the headless GLX frontend, depend on :game_lib_headless_glx
-# and add "-lGL -lX11" to the linker flags.
cc_library(
name = "game_lib_headless_glx",
- srcs = [
- CODE_DIR + "/asm/ftola.c",
- CODE_DIR + "/asm/qasm-inline.h",
- CODE_DIR + "/asm/snapvector.c",
- CODE_DIR + "/cgame/cg_public.h",
- CODE_DIR + "/client/libmumblelink.c",
- CODE_DIR + "/deepmind/dm_public.h",
+ srcs = IOQ3_COMMON_SRCS + [
CODE_DIR + "/deepmind/dmlab_connect.c",
- CODE_DIR + "/game/bg_public.h",
- CODE_DIR + "/game/g_public.h",
CODE_DIR + "/null/null_input.c",
CODE_DIR + "/null/null_snddma.c",
- CODE_DIR + "/sys/con_log.c",
- CODE_DIR + "/sys/con_passive.c",
- CODE_DIR + "/sys/sys_main.c",
- CODE_DIR + "/sys/sys_unix.c",
- CODE_DIR + "/ui/ui_public.h",
## OpenGL rendering
CODE_DIR + "/deepmind/headless_native_glimp.c",
CODE_DIR + "/deepmind/glimp_common.h",
CODE_DIR + "/deepmind/glimp_common.c",
- ] + glob(
- [
- CODE_DIR + "/botlib/*.c",
- CODE_DIR + "/client/cl_*.c",
- CODE_DIR + "/client/snd_*.c",
- CODE_DIR + "/qcommon/*.c",
- CODE_DIR + "/renderercommon/*.c",
- CODE_DIR + "/renderergl1/*.c",
- CODE_DIR + "/server/*.c",
- ],
- exclude = [
- CODE_DIR + "/renderergl1/tr_subs.c",
- CODE_DIR + "/server/sv_rankings.c",
- CODE_DIR + "/qcommon/vm_none.c",
- CODE_DIR + "/qcommon/vm_powerpc*.c",
- CODE_DIR + "/qcommon/vm_sparc.c",
- ],
- ),
- hdrs = [
- "public/dmlab.h",
- CODE_DIR + "/deepmind/context.h",
- ] + glob(
- [
- CODE_DIR + "/botlib/*.h",
- CODE_DIR + "/client/*.h",
- CODE_DIR + "/qcommon/*.h",
- CODE_DIR + "/sys/*.h",
- CODE_DIR + "/server/*.h",
- CODE_DIR + "/renderercommon/*.h",
- CODE_DIR + "/renderergl1/*.h",
- ],
- exclude = [
- CODE_DIR + "/client/fx_*.h",
- CODE_DIR + "/qcommon/vm_powerpc_asm.h",
- CODE_DIR + "/qcommon/vm_sparc.h",
- CODE_DIR + "/renderercommon/tr_types.h",
- CODE_DIR + "/renderercommon/tr_public.h",
- CODE_DIR + "/renderergl1/tr_local.h",
- ],
- ),
- copts = [
- "-std=c99",
- "-fno-strict-aliasing",
- ARCH_VAR,
- STANDALONE_VAR,
],
- defines = [
- "BOTLIB",
- "_GNU_SOURCE",
+ hdrs = ["public/dmlab.h"],
+ copts = IOQ3_COMMON_COPTS,
+ defines = IOQ3_COMMON_DEFINES,
+ linkopts = [
+ "-lGL",
+ "-lX11",
],
- textual_hdrs = [
- CODE_DIR + "/renderercommon/tr_types.h",
- CODE_DIR + "/renderercommon/tr_public.h",
- CODE_DIR + "/renderergl1/tr_local.h",
+ textual_hdrs = IOQ3_COMMON_TEXTUAL_HDRS,
+ deps = IOQ3_COMMON_DEPS,
+)
+
+cc_library(
+ name = "game_lib_headless_egl",
+ srcs = IOQ3_COMMON_SRCS + [
+ CODE_DIR + "/deepmind/dmlab_connect.c",
+ CODE_DIR + "/null/null_input.c",
+ CODE_DIR + "/null/null_snddma.c",
+
+ ## OpenGL rendering
+ CODE_DIR + "/deepmind/headless_egl_glimp.c",
+ CODE_DIR + "/deepmind/glimp_common.h",
+ CODE_DIR + "/deepmind/glimp_common.c",
],
- deps = [
- ":qcommon_hdrs",
- "//deepmind/include:context_headers",
- "//third_party/rl_api:env_c_api",
- "@jpeg_archive//:jpeg",
- "@sdl_system//:sdl2",
+ hdrs = ["public/dmlab.h"],
+ copts = IOQ3_COMMON_COPTS,
+ defines = IOQ3_COMMON_DEFINES,
+ linkopts = [
+ "-lEGL",
+ "-lGL",
],
+ textual_hdrs = IOQ3_COMMON_TEXTUAL_HDRS,
+ deps = IOQ3_COMMON_DEPS + ["//third_party/GL/util:egl_util"],
)
ASSETS = [
"assets/default.cfg",
"assets/q3config.cfg",
-] + glob(["assets/game_scripts/**/*.lua"])
+] + glob([
+ "assets/game_scripts/**/*.lua",
+ "assets/game_scripts/**/*.png",
+])
MAPS = glob(["assets/maps/*.map"])
@@ -829,13 +739,14 @@ genrule(
cmd = "cp -t $(@D)/baselab $(SRCS); " +
"for s in $(SRCS); do " +
" BM=$$(basename $${s}); M=$${BM/.map/}; " +
- " $(location //deepmind/level_generation:compile_map_sh).runfiles/org_deepmind_lab/deepmind/level_generation/compile_map_sh $(@D)/baselab/$${M}; " +
+ " $(location //deepmind/level_generation:compile_map_sh).runfiles/org_deepmind_lab/deepmind/level_generation/compile_map_sh -a $(@D)/baselab/$${M}; " +
"done",
tools = [
"//:bspc",
"//deepmind/level_generation:compile_map_sh",
"//q3map2",
],
+ visibility = ["//testing:__subpackages__"],
)
genrule(
@@ -897,6 +808,7 @@ genrule(
],
outs = ["baselab/vm.pk3"],
cmd = "A=$$(pwd); mkdir $(@D)/vm; ln -s -r -t $(@D)/vm -- $(SRCS); (cd $(@D); zip -r $${A}/$(OUTS) -- vm)",
+ visibility = ["//testing:__subpackages__"],
)
cc_binary(
@@ -911,10 +823,7 @@ cc_binary(
":non_pk3_assets",
":vm_pk3",
],
- linkopts = [
- "-lGL",
- "-lm",
- ],
+ linkopts = ["-lm"],
deps = [
":game_lib_sdl",
"//deepmind/engine:callbacks",
@@ -924,45 +833,48 @@ cc_binary(
)
config_setting(
- name = "dmlab_lib_sdl",
- values = {"define": "headless=false"},
+ name = "dmlab_glmode_egl",
+ values = {"define": "glmode=egl"},
)
config_setting(
- name = "dmlab_headless_hw",
- values = {"define": "headless=glx"},
+ name = "dmlab_glmode_glx",
+ values = {"define": "glmode=glx"},
)
-config_setting(
- name = "dmlab_headless_sw",
- values = {"define": "headless=osmesa"},
+cc_binary(
+ name = "libdmlab_headless_hw.so",
+ linkopts = [
+ "-Wl,--version-script",
+ ":dmlab.lds",
+ ],
+ linkshared = 1,
+ linkstatic = 1,
+ visibility = ["//testing:__subpackages__"],
+ deps = [
+ ":dmlab.lds",
+ "//deepmind/engine:callbacks",
+ "//deepmind/engine:context",
+ "@zlib_archive//:zlib",
+ ] + select({
+ "dmlab_glmode_egl": [":game_lib_headless_egl"],
+ "dmlab_glmode_glx": [":game_lib_headless_glx"],
+ "//conditions:default": [":game_lib_headless_egl"],
+ }),
)
cc_binary(
- name = "libdmlab.so",
- linkopts = select({
- "//conditions:default": ["-lOSMesa"],
- ":dmlab_headless_hw": [
- "-lGL",
- "-lX11",
- ],
- ":dmlab_lib_sdl": [
- "-lGL",
- "-lX11",
- ],
- }) + [
+ name = "libdmlab_headless_sw.so",
+ linkopts = [
"-Wl,--version-script",
":dmlab.lds",
],
linkshared = 1,
linkstatic = 1,
- deps = select({
- "//conditions:default": [":game_lib_headless_osmesa"],
- ":dmlab_lib_sdl": [":game_lib_sdl"],
- ":dmlab_headless_hw": [":game_lib_headless_glx"],
- ":dmlab_headless_sw": [":game_lib_headless_osmesa"],
- }) + [
+ visibility = ["//testing:__subpackages__"],
+ deps = [
":dmlab.lds",
+ ":game_lib_headless_osmesa",
"//deepmind/engine:callbacks",
"//deepmind/engine:context",
"@zlib_archive//:zlib",
@@ -973,14 +885,18 @@ cc_library(
name = "dmlablib",
srcs = ["public/dmlab_so_loader.cc"],
hdrs = ["public/dmlab.h"],
- copts = ["-DDMLAB_SO_LOCATION=\\\"libdmlab.so\\\""],
data = [
":assets",
- ":libdmlab.so",
+ ":libdmlab_headless_hw.so",
+ ":libdmlab_headless_sw.so",
"//deepmind/level_generation:compile_map_sh",
],
linkopts = ["-ldl"],
- deps = ["//third_party/rl_api:env_c_api"],
+ visibility = ["//testing:__subpackages__"],
+ deps = [
+ ":level_cache_types",
+ "//third_party/rl_api:env_c_api",
+ ],
)
cc_binary(
@@ -1004,37 +920,46 @@ cc_binary(
],
linkshared = 1,
linkstatic = 1,
+ visibility = [
+ "//python/pip_package:__subpackages__",
+ "//python/tests:__subpackages__",
+ ],
deps = [
":dmlablib",
"@python_system//:python",
],
)
-py_test(
- name = "python_module_test",
- srcs = ["python/dmlab_module_test.py"],
- data = [":deepmind_lab.so"],
- imports = ["python"],
- main = "python/dmlab_module_test.py",
-)
-
-py_binary(
- name = "python_benchmark",
- srcs = ["python/benchmark.py"],
- data = [":deepmind_lab.so"],
- main = "python/benchmark.py",
-)
-
py_binary(
- name = "random_agent",
+ name = "python_random_agent",
srcs = ["python/random_agent.py"],
data = [":deepmind_lab.so"],
main = "python/random_agent.py",
+ visibility = ["//python/tests:__subpackages__"],
)
-py_test(
- name = "random_agent_test",
- srcs = ["python/random_agent_test.py"],
- main = "python/random_agent_test.py",
- deps = [":random_agent"],
+LOAD_TEST_SCRIPTS = [
+ level_script[len("assets/game_scripts/"):-len(".lua")]
+ for level_script in glob(["assets/game_scripts/*.lua"])
+]
+
+SKIPPED_TESTS = [
+ "concept_task_01_test",
+ "concept_task_03_test",
+]
+
+test_suite(
+ name = "load_level_test",
+ tests = ["load_level_test_" + level_name for level_name in LOAD_TEST_SCRIPTS if level_name not in SKIPPED_TESTS],
)
+
+[
+ cc_test(
+ name = "load_level_test_" + level_name,
+ size = "large",
+ args = [level_name],
+ deps = ["//testing:load_level_test_lib"],
+ )
+ for level_name in LOAD_TEST_SCRIPTS
+ if level_name not in SKIPPED_TESTS
+]
diff --git a/README.md b/README.md
index ddb07c1e..c1d340af 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-#
+#
*DeepMind Lab* is a 3D learning environment based on id Software's
[Quake III Arena](https://github.com/id-Software/Quake-III-Arena) via
@@ -43,7 +43,7 @@ You can reach us at [lab@deepmind.com](mailto:lab@deepmind.com).
## Getting started on Linux
-* Get [Bazel from bazel.io](http://bazel.io/docs/install.html).
+* Get [Bazel from bazel.io](https://docs.bazel.build/versions/master/install.html).
* Clone DeepMind Lab, e.g. by running
@@ -52,14 +52,7 @@ $ git clone https://github.com/deepmind/lab
$ cd lab
```
-* For a live example of a random agent, run
-
-```shell
-lab$ bazel run :random_agent --define headless=false -- \
- --length=10000 --width=640 --height=480
-```
-
-Here is some [more detailed build documentation](docs/build.md),
+Here is some [more detailed build documentation](/docs/users/build.md),
including how to install dependencies if you don't have them.
### Play as a human
@@ -67,9 +60,13 @@ including how to install dependencies if you don't have them.
To test the game using human input controls, run
```shell
-lab$ bazel run :game -- --level_script tests/demo_map
+lab$ bazel run :game -- --level_script=test_levels/empty_room_test --level_setting=logToStdErr=true
+# or:
+lab$ bazel run :game -- -l test_levels/empty_room_test -s logToStdErr=true
```
+Leave the `logToStdErr` setting off to disable most log output.
+
### Train an agent
*DeepMind Lab* ships with an example random agent in
@@ -78,15 +75,15 @@ which can be used as a starting point for implementing a learning agent. To let
this agent interact with DeepMind Lab for training, run
```shell
-lab$ bazel run :random_agent
+lab$ bazel run :python_random_agent
```
-The Python API for the agent-environment interaction is described
-in [docs/python_api.md](docs/python_api.md).
+The [Python API](/docs/users/python_api.md)
+is used for agent-environment interactions.
*DeepMind Lab* ships with different levels implementing different tasks. These
-tasks can be configured using Lua scripts,
-as described in [docs/lua_api.md](docs/lua_api.md).
+tasks can be configured using Lua scripts, as described in the
+[Lua API](/docs/developers/reference/lua_api.md).
-----------------
@@ -104,14 +101,14 @@ with those projects are best fixed upstream and then merged into *DeepMind Lab*.
* *q3map2* is taken from
[github.com/TTimo/GtkRadiant](https://github.com/TTimo/GtkRadiant),
- revision 8557f1820f8e0c7cef9d52a78b2847fa401a4a95. A few minor local
+ revision 69972f94582f0723c9ceaabf6751a911bf1fdcc3. A few minor local
modifications add synchronization and use C99 constructs to replace
formerly non-portable or undefined behaviour. We also expect this code to be
stable.
* *ioquake3* is taken from
[github.com/ioquake/ioq3](https://github.com/ioquake/ioq3),
- revision 0672905ef1b8f6ca219341e7252044dd727753dd. The code contains extensive
+ revision 690c5a4dac3c3d0d59ee271aadd8f19a29a9f338. The code contains extensive
modifications and additions. We aim to merge upstream changes occasionally.
We are very grateful to the maintainers of these repositories for all their hard
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
new file mode 100644
index 00000000..c89b9cd1
--- /dev/null
+++ b/RELEASE_NOTES.md
@@ -0,0 +1,32 @@
+# DeepMind Lab Release Notes
+
+## Current Release
+
+### New Features:
+
+1. Lua customizable themes.
+2. String observations.
+3. Add ability for script to generate events visible to the EnvCApi.
+4. Updated EnvCApi to propagate error messages.
+5. Added customization point to enable reading files from external sources.
+
+### Minor Improvements:
+
+1. Fixed ramp jump velocity in level lt_space_bounce_hard.
+2. Fixed Lua function 'addScore' from module 'dmlab.system.game' to allow
+ negative scores added to a player.
+3. Minor removal of undefined behaviour in the engine.
+4. Change LuaSnippetEmitter methods to use table call conventions.
+5. Added config variable for monochromatic lightmaps ('r_monolightmaps').
+ Enabled by default.
+6. Python module 'observation_spec' now returns current observation spec.
+7. Added config variable to limit texture size ('r_textureMaxSize').
+8. Increased score range to include all 32 bit integers.
+9. api:modifyTexture must now return whether the texture was modified.
+10. Internal engine events are propagated to the script via gameEvent.
+11. Added ability to adjust rewards.
+12. Added ability to raycast between different points on the map.
+13. Reduced inaccuracies related to angle conversion and normalization.
+14. Added flag "sv_rateLimit" to disable rate limiting for networked games.
+
+## release-2016-12-06 Initial release
diff --git a/WORKSPACE b/WORKSPACE
index a15bffdb..5c0362ff 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,10 +1,15 @@
workspace(name = "org_deepmind_lab")
-new_git_repository(
- name = "googletest",
- build_file = "googletest.BUILD",
- commit = "d8fe70f477d8a99745b69f7650f75eacf96866f9",
- remote = "https://github.com/google/googletest.git",
+http_archive(
+ name = "com_google_googletest",
+ strip_prefix = "googletest-master",
+ urls = ["https://github.com/google/googletest/archive/master.zip"],
+)
+
+http_archive(
+ name = "com_google_absl",
+ strip_prefix = "abseil-cpp-master",
+ urls = ["https://github.com/abseil/abseil-cpp/archive/master.zip"],
)
new_http_archive(
@@ -18,8 +23,8 @@ new_http_archive(
new_http_archive(
name = "glib_archive",
build_file = "glib.BUILD",
- strip_prefix = "glib-2.38.2",
sha256 = "056a9854c0966a0945e16146b3345b7a82562a5ba4d5516fd10398732aea5734",
+ strip_prefix = "glib-2.38.2",
url = "http://ftp.gnome.org/pub/gnome/sources/glib/2.38/glib-2.38.2.tar.xz",
)
diff --git a/assets/game_scripts/common/color_bots.lua b/assets/game_scripts/common/color_bots.lua
new file mode 100644
index 00000000..8daf3f6c
--- /dev/null
+++ b/assets/game_scripts/common/color_bots.lua
@@ -0,0 +1,95 @@
+local colors = require 'common.colors'
+local game = require 'dmlab.system.game'
+local image = require 'dmlab.system.image'
+
+local SINGLE_BOT_SKIN_TEXTURE = 'models/players/crash_color/skin_base.tga'
+local SATURATION = 1.0
+local VALUE = 1.0
+
+local color_bots = {}
+
+color_bots.BOT_NAMES = {
+ 'Cygni',
+ 'Leonis',
+ 'Epsilon',
+ 'Cephei',
+ 'Centauri',
+ 'Draconis',
+}
+
+color_bots.BOT_NAMES_COLOR = {
+ 'CygniColor',
+ 'LeonisColor',
+ 'EpsilonColor',
+ 'CepheiColor',
+ 'CentauriColor',
+ 'DraconisColor',
+}
+
+local characterSkinData = nil
+
+local function characterSkins()
+ if not characterSkinData then
+ local playerDir = game:runFiles() .. '/baselab/game_scripts/player/'
+ characterSkinData = {
+ image.load(playerDir .. 'dm_character_skin_mask_a.png'),
+ image.load(playerDir .. 'dm_character_skin_mask_b.png'),
+ image.load(playerDir .. 'dm_character_skin_mask_c.png'),
+ }
+ end
+ return characterSkinData
+end
+
+--[[ Returns a list of bots compatible with the addBot API.
+
+Keyword arguments:
+
+ * count - Number [0, #color_bots.BOT_NAMES] - Number of bots to create.
+ * color - Boolean (false) - Whether to create the bots with custom colors.
+ * skill - Number - Skill level of bot. In range [1, 5]
+]]
+function color_bots:makeBots(kwargs)
+ assert(kwargs.count, 'Missing count')
+ local bots = {}
+ local botNames = kwargs.color and self.BOT_NAMES_COLOR or self.BOT_NAMES
+ for i, name in ipairs(botNames) do
+ if i == kwargs.count + 1 then
+ break
+ end
+ bots[#bots + 1] = {name = name, skill = kwargs.skill or 4.0}
+ end
+ return bots
+end
+
+local playerSkin = nil
+
+-- Required to inform bot api of the skin texture used by the bots. Call
+-- from modifyTexture API if color bots are desirded.
+function color_bots:findSkin(name, texture)
+ if name == SINGLE_BOT_SKIN_TEXTURE then
+ playerSkin = texture:clone()
+ end
+end
+
+-- Required to set the color of the bots. Call from call mapLoaded API.
+function color_bots:colorizeBots(hue)
+ assert(playerSkin, 'findSkin must be called during modifyTexture.')
+ local skin = playerSkin:clone()
+ local skins = characterSkins()
+ local hueAngle = 360 / #skins
+ local shape = skin:shape()
+ for i, charachterSkin in ipairs(skins) do
+ local r, g, b = colors.hsvToRgb(hue, SATURATION, VALUE)
+ local skinC = charachterSkin:clone()
+ skinC:select(3, 1):mul(r / 255.0)
+ skinC:select(3, 2):mul(g / 255.0)
+ skinC:select(3, 3):mul(b / 255.0)
+ skin:cadd(skinC)
+ hue = (hue + hueAngle) % 360
+ end
+ game:updateTexture(SINGLE_BOT_SKIN_TEXTURE, skin)
+ collectgarbage()
+ collectgarbage()
+end
+
+return color_bots
diff --git a/assets/game_scripts/common/colors.lua b/assets/game_scripts/common/colors.lua
new file mode 100644
index 00000000..5b243662
--- /dev/null
+++ b/assets/game_scripts/common/colors.lua
@@ -0,0 +1,61 @@
+-- Utilities for color conversion.
+
+local colors = {}
+
+--[[ Converts an HSV color value to RGB.
+
+Conversion formula adapted from
+http://en.wikipedia.org/wiki/HSV_color_space. Assumes h in [0, 360), s and v are
+contained in the set [0, 1].
+
+Returns r, g, b each in the set [0, 255].
+]]
+function colors.hsvToRgb(h, s, v)
+ local i = math.floor(h / 60)
+ local f = h / 60 - i
+ local p = v * (1 - s)
+ local q = v * (1 - f * s)
+ local t = v * (1 - (1 - f) * s)
+
+ i = i % 6
+ local r, g, b
+ if i == 0 then r, g, b = v, t, p
+ elseif i == 1 then r, g, b = q, v, p
+ elseif i == 2 then r, g, b = p, v, t
+ elseif i == 3 then r, g, b = p, q, v
+ elseif i == 4 then r, g, b = t, p, v
+ elseif i == 5 then r, g, b = v, p, q
+ end
+
+ return r * 255, g * 255, b * 255
+end
+
+
+--[[ Converts an HSL color value to RGB.
+
+Based on formula at https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL.
+Assumes h ∈ [0°, 360°), s ∈ [0, 1] and v ∈ [0, 1].
+
+Returns r, g, b each in the set [0, 255].
+]]
+function colors.hslToRgb(h, s, l)
+ local c = (1 - math.abs(2 * l - 1)) * s
+ local hprime = h / 60
+ local x = c * (1 - math.abs(hprime % 2 - 1))
+
+ local m = l - 0.5 * c
+ c = c + m
+ x = x + m
+ local r, g, b
+ if hprime <= 1 then r, g, b = c, x, m
+ elseif hprime <= 2 then r, g, b = x, c, m
+ elseif hprime <= 3 then r, g, b = m, c, x
+ elseif hprime <= 4 then r, g, b = m, x, c
+ elseif hprime <= 5 then r, g, b = x, m, c
+ elseif hprime < 6 then r, g, b = c, m, x
+ end
+
+ return r * 255, g * 255, b * 255
+end
+
+return colors
diff --git a/assets/game_scripts/common/combinatorics.lua b/assets/game_scripts/common/combinatorics.lua
new file mode 100644
index 00000000..099731ba
--- /dev/null
+++ b/assets/game_scripts/common/combinatorics.lua
@@ -0,0 +1,109 @@
+-- Utility functions for combinatorics operations.
+-- See https://en.wikipedia.org/wiki/Combinatorics
+
+local combinatorics = {}
+
+--[[ Returns "n choose k" = n! / (k!(n - k)!).
+
+This is the number of unique ways to sample without replacement k elements from
+a bucket of n elements.
+
+See https://en.wikipedia.org/wiki/Combination.
+--]]
+function combinatorics.choose(k, n)
+ assert(0 <= k and k <= n)
+
+ --[[ We can repeatedly use the identity,
+ C(n, k) = n/k C(n - 1, k - 1)
+ to calculate C(n, k) while avoiding large numbers (and corresponding
+ floating point error).
+
+ C(n, k) = product[i=0..k-1] (n-i)/(k-i) * C(n - k, 0)
+ = product[i=0..k-1] (n-i)/(k-i)
+
+ Moreover, since C(n, k) = C(n, n - k), we can ensure we loop at most n/2
+ times by setting k' = min(k, n-k). Then,
+ C(n, k) = C(n, k')
+ --]]
+ local kPrime = math.min(k, n - k)
+ local accumulator = 1
+ for i = 0, kPrime - 1 do
+ accumulator = accumulator * ((n - i) / (kPrime - i))
+ end
+ return accumulator
+end
+
+--[[ Return the `idx`th selection from an enumeration of all possible
+selections {a,b} with 1 <= a < b <= n.
+
+`idx` is a number between 1 and C(n,2) = n*(n-1)/2.
+
+Given n items, there are C(n,2) = n*(n-1)/2 ways to select two of those
+items. We enumerate all those ways and assign each an index from 1 to C(n,2).
+We return the `idx`th pair from that enumeration.
+--]]
+function combinatorics.twoItemSelection(idx, n)
+ assert(1 <= idx and idx <= combinatorics.choose(2, n))
+
+ --[[
+ First, enumerate possible combinatorics of {x,y} with 0 <= x <= y <= n-2 by
+ drawing them in the triangular configuration below.
+
+ Then, use triangular numbers to map from i = idx-1 to {x,y} coordinates
+ (see https://en.wikipedia.org/wiki/Triangular_number).
+
+ 0 1 2 3 . . n-2
+ 0 *
+ 1 * *
+ 2 * * *
+ . * * * *
+ . * * i * * --- x <== The `i`th triangular point.
+ . * * * * * * We map it to coordinates {x,y}.
+ n-2 * * * * * * *
+ |
+ y
+
+ If R is the number of points in first r rows, then
+ R = 1 + 2 + 3 + ... + r
+ R = r * (r + 1) / 2
+ 0 = r^2 + r - 2R
+ r = [-1 + sqrt(1 - 4*1*(-2R))] / 2, by quadratic formula,
+ and since r must be positive
+ r = [-1 + sqrt(1 + 8R)] / 2
+ r = -0.5 + sqrt(0.25 + 2R)
+ This is the "triangular root" of x, which is monotonically increasing,
+ so the floor of it will give us the closest row.
+
+ x = floor( -0.5 + sqrt(2*i + 0.25) )
+
+ The column is given by subtracting the number of points in the first
+ x rows from i.
+
+ y = i - [x * (x + 1) / 2]
+ --]]
+ local i = idx - 1
+ local x = math.floor(-0.5 + math.sqrt(0.25 + 2 * i))
+ local y = i - x * (x + 1) / 2
+
+ -- We want {a, b} where 1 <= a < b <= n, so we need to adjust the {x,y}
+ -- indices to match the coordinate system below.
+ --
+ -- Note that `a` runs from 1..n-1, and `b` runs from 2..n because a < b.
+ --
+ -- n . . . 4 3 2
+ -- n-1 *
+ -- . * *
+ -- . * * *
+ -- . * * * *
+ -- 3 * *idx* * --- a <== The `idx`th triangular point.
+ -- 2 * * * * * * We map it to coordinates {a,b}.
+ -- 1 * * * * * * *
+ -- |
+ -- b
+ --
+ local a = n - x - 1
+ local b = n - y
+ return {a, b}
+end
+
+return combinatorics
diff --git a/assets/game_scripts/common/game_rewards.lua b/assets/game_scripts/common/game_rewards.lua
new file mode 100644
index 00000000..f2b1ecc7
--- /dev/null
+++ b/assets/game_scripts/common/game_rewards.lua
@@ -0,0 +1,82 @@
+local events = require 'dmlab.system.events'
+local tensor = require 'dmlab.system.tensor'
+
+local game_rewards = {}
+
+game_rewards.REASONS = {
+ -- `playerId` touched reward pickup.
+ 'PICKUP_REWARD',
+
+ -- `playerId` touched goal pickup.
+ 'PICKUP_GOAL',
+
+ -- Level triggered a reward at `playerId`.
+ 'TARGET_SCORE',
+
+ -- `playerId` tagged self.
+ 'TAG_SELF',
+
+ -- `playerId` tagged `otherPlayerId`
+ 'TAG_PLAYER',
+
+ -- `playerId` picked up enemy flag.
+ 'CTF_FLAG_BONUS',
+
+ -- `playerId` captured the opposing team's flag.
+ 'CTF_CAPTURE_BONUS',
+
+ -- `playerId` is part of a team that has captured the opposing team's flag.
+ 'CTF_TEAM_BONUS',
+
+ -- `playerId` tagged opponent(`otherPlayerId`) flag carrier.
+ 'CTF_FRAG_CARRIER_BONUS',
+
+ -- `playerId` returned their team flag to the team's base.
+ 'CTF_RECOVERY_BONUS',
+
+ -- `playerId` is on the same team as the flag carrier and tagged opponent
+ -- (`otherPlayerId`) who damaged our flag carrier.
+ 'CTF_CARRIER_DANGER_PROTECT_BONUS',
+
+ -- `playerId` tagged opponent(`otherPlayerId`) while `playerId` or
+ -- `otherPlayerId` is near `playerId`'s flag.
+ 'CTF_FLAG_DEFENSE_BONUS',
+
+ -- `playerId` tagged opponent(`otherPlayerId`) while `playerId` or
+ -- `otherPlayerId` is near `playerId`'s flag carrier.
+ 'CTF_CARRIER_PROTECT_BONUS',
+
+ -- `playerId` returned the team flag just before payerId's team captured
+ -- the opposing team's flag.
+ 'CTF_RETURN_FLAG_ASSIST_BONUS',
+
+ -- `playerId` tagged opponent team's flag carrier just before a capturing
+ -- event occurred.
+ 'CTF_FRAG_CARRIER_ASSIST_BONUS',
+}
+
+function game_rewards:initScoreOveride(kwargs)
+ for _, reason in ipairs(self.REASONS) do
+ kwargs['reward.' .. reason] = false
+ kwargs['reward.red.' .. reason] = false
+ kwargs['reward.blue.' .. reason] = false
+ end
+end
+
+function game_rewards:overrideScore(kwargs, rewardInfo)
+ local playerId = rewardInfo.playerId
+ local reason = rewardInfo.reason
+ local team = rewardInfo.team
+ local location = rewardInfo.location or {0, 0, 0}
+ local otherPlayerId = rewardInfo.otherPlayerId or -1
+ local score = kwargs['reward.' .. team .. '.' .. reason] or
+ kwargs['reward.' .. reason] or rewardInfo.score
+ score = tonumber(score)
+ local d = tensor.DoubleTensor
+ events:add('reward', reason, team, d{score}, d{playerId + 1}, d(location),
+ d{otherPlayerId + 1})
+ return score
+end
+
+
+return game_rewards
diff --git a/assets/game_scripts/common/game_types.lua b/assets/game_scripts/common/game_types.lua
new file mode 100644
index 00000000..a8eb38e9
--- /dev/null
+++ b/assets/game_scripts/common/game_types.lua
@@ -0,0 +1,9 @@
+-- These must match gametype_t found in 'engine/code/game/bg_public.h'
+return {
+ FREE_FOR_ALL = 0,
+ TOURNAMENT = 1,
+ SINGLE_PLAYER_FREE_FOR_ALL = 2,
+ TEAM_DEATHMATCH = 3,
+ CAPTURE_THE_FLAG = 4,
+ ONE_FLAG_CAPTURE_THE_FLAG = 5,
+}
diff --git a/assets/game_scripts/common/geometric_pickups.lua b/assets/game_scripts/common/geometric_pickups.lua
new file mode 100644
index 00000000..7b9892f8
--- /dev/null
+++ b/assets/game_scripts/common/geometric_pickups.lua
@@ -0,0 +1,103 @@
+local random = require 'common.random'
+local pickups = require 'common.pickups'
+
+local geometric_pickups = {}
+
+-- Matches the 'fut_obj_*' models available in assets/models.
+local OBJECTS = {
+ 'fut_obj_barbell_',
+ 'fut_obj_coil_',
+ 'fut_obj_cone_',
+ 'fut_obj_crossbar_',
+ 'fut_obj_cube_',
+ 'fut_obj_cylinder_',
+ 'fut_obj_doubleprism_',
+ 'fut_obj_glowball_',
+ 'fut_obj_potcone_',
+ 'fut_obj_prismjack_',
+ 'fut_obj_sphere_',
+ 'fut_obj_toroid_',
+}
+
+local OBJECT_MODS = 3
+local EAT_REWARD_MIN = -1
+local EAT_REWARD_MAX = 1
+
+-- Returns a table with the names of all of the available pickups.
+function geometric_pickups.createPickupNames()
+ local pickupNames = {}
+
+ for i = 1, #OBJECTS do
+ for j = 1, OBJECT_MODS do
+ pickupNames[#pickupNames + 1] = OBJECTS[i] .. string.format('%02d', j)
+ end
+ end
+
+ return pickupNames
+end
+
+-- Returns a table with all available objects as pickups (see common.pickups).
+function geometric_pickups.createPickups()
+ local geoPickups = {}
+
+ local pickupNames = geometric_pickups.createPickupNames()
+ for k, v in ipairs(pickupNames) do
+ geoPickups[v] = {
+ name = v,
+ classname = v,
+ model = 'models/' .. v .. '.md3',
+ quantity = 0,
+ type = pickups.type.REWARD
+ }
+ end
+
+ return geoPickups
+end
+
+-- Assigns random rewards (within supplied inclusive range) to pickups table.
+function geometric_pickups.randomisePickupRewards(
+ pickupTable, minReward, maxReward)
+ minReward = minReward or EAT_REWARD_MIN
+ maxReward = maxReward or EAT_REWARD_MAX
+ assert(minReward <= maxReward)
+
+ for k, v in pairs(pickupTable) do
+ v.quantity = random:uniformInt(0, maxReward - minReward) + minReward
+ end
+end
+
+--[[ Returns a table with the requested number of pickup chosen from the list of
+possible pickups. Returned table contains unique elements unless the number of
+elements requested is greater than the size of the available elements, then
+elements will be repeated.
+]]
+function geometric_pickups.randomUniquePickupNames(numPickups)
+ local pickupNames = geometric_pickups.createPickupNames()
+ assert(0 <= numPickups, "Invalid numPickups!")
+ random:shuffleInPlace(pickupNames, numPickups)
+ local randElements = {}
+ for i = 1, numPickups do
+ randElements[i] = pickupNames[(i - 1) % #pickupNames + 1]
+ end
+ return randElements
+end
+
+--[[ Returns a shuffled list of pickups. List contains 'setSize' copies of
+'numTypes' types of object, selected at random from 'allEdibles'.
+]]
+function geometric_pickups.randomPickups(allEdibles, numTypes, setSize)
+ local pickupKeys = geometric_pickups.randomUniquePickupNames(numTypes)
+ local pickupsList = {}
+
+ for j = 1, #pickupKeys do
+ local obj_type = pickupKeys[j]
+ for i = 1, setSize do
+ pickupsList[#pickupsList + 1] = obj_type
+ end
+ end
+ random:shuffleInPlace(pickupsList)
+
+ return pickupsList
+end
+
+return geometric_pickups
diff --git a/assets/game_scripts/common/helpers.lua b/assets/game_scripts/common/helpers.lua
index 197088d5..d34ac3c2 100644
--- a/assets/game_scripts/common/helpers.lua
+++ b/assets/game_scripts/common/helpers.lua
@@ -1,34 +1,180 @@
-local random = require 'common.random'
-- Common utilities.
+local io = require 'io'
+
+local SECONDS_IN_MINUTE = 60
+local SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60
local helpers = {}
--- Shuffles an array in place. (Uses the 'common.random'.)
-function helpers.shuffleInPlace(array)
- for i = 1, #array - 1 do
- local j = random.uniformInt(i, #array)
- array[j], array[i] = array[i], array[j]
+
+-- Returns an array of strings split according to single character separator.
+-- Skips empty fields.
+function helpers.split(str, sep)
+ local words = {}
+ for word in string.gmatch(str, '([^' .. sep .. ']+)') do
+ words[#words + 1] = word
end
+ return words
+end
- return array
+-- Converts a space-delimited string of numerical values to a table of numbers.
+-- e.g. "1.000 2.00 3.00" becomes {1, 2, 3}
+function helpers.spawnVarToNumberTable(spaceSeperatedString)
+ local result = {}
+ for v in string.gmatch(spaceSeperatedString, "%S+") do
+ local index = #result + 1
+ result[index] = tonumber(v)
+ end
+ return result
end
--- Returns a shuffled copy of an array. (Uses the 'common.random'.)
-function helpers.shuffle(array)
- local ret = {}
- for i, obj in ipairs(array) do
- ret[i] = obj
+--[[ Joins a path with a base directory.
+
+* pathJoin('base', 'path') => 'base/path'
+* pathJoin('base/', 'path') => 'base/path'
+* pathJoin('base', '/path') => '/path'
+* pathJoin('base/', '/path') => '/path'
+]]
+function helpers.pathJoin(base, path)
+ if path:sub(1, 1) == '/' then
+ return path
+ elseif base:sub(#base) == '/' then
+ return base .. path
+ else
+ return base .. '/' .. path
end
- return helpers.shuffleInPlace(ret)
end
--- Returns an array of strings split according to single character separator.
--- Skips empty fields.
-function helpers.split(str, sep)
- words = {}
- for word in string.gmatch(str, '([^' .. sep .. ']+)') do
- words[#words + 1] = word
+function helpers.dirname(str)
+ return str:match(".-/.-") and str:gsub("(.*/)(.*)", "%1") or ''
+end
+
+-- Returns whether a file exists by opening it for read.
+function helpers.fileExists(file)
+ local f = io.open(file, "r")
+ if f ~= nil then
+ io.close(f)
+ return true
+ else
+ return false
end
- return words
+end
+
+-- Given the name of a Lua module from game_scripts, e.g. 'common.helpers',
+-- returns the full path to the source Lua file on disk, or nil.
+function helpers.findFileInLuaPath(moduleName)
+ local moduleName = moduleName:gsub('[.]', '/')
+ for element in package.path:gmatch('[^;]+') do
+ if element:find('/game_scripts/') then
+ local path = element:gsub('[?]', moduleName)
+ if helpers.fileExists(path) then
+ return path
+ end
+ end
+ end
+ return nil
+end
+
+--[[ Returns a shallow copy of a table.
+
+That is, copies the first level of key value pairs, but does not recurse down
+into the table.
+]]
+function helpers.shallowCopy(input)
+ if input == nil then return nil end
+ assert(type(input) == 'table')
+ local output = {}
+ for k, v in pairs(input) do
+ output[k] = v
+ end
+ setmetatable(output, getmetatable(input))
+ return output
+end
+
+--[[ Returns a deep copy of a table.
+
+Performs a deep copy of the key-value pairs of a table, recursing for each
+value, depth-first, up to the specified depth, or indefinitely if that parameter
+is nil.
+]]
+function helpers.deepCopy(input, depth)
+ if type(input) == 'table' then
+ if depth == nil or depth > 0 then
+ local output = {}
+ local below = depth ~= nil and depth - 1 or nil
+ for k, v in pairs(input) do
+ output[k] = helpers.deepCopy(v, below)
+ end
+ setmetatable(output, helpers.deepCopy(getmetatable(input), below))
+ return output
+ end
+ end
+ return input
+end
+
+-- Naive recursive pretty-printer.
+-- Prints the table hierarchically. Assumes all the keys are simple values.
+function helpers.tostring(input, spacing, limit)
+ limit = limit or 5
+ if limit < 0 then
+ return ''
+ end
+ spacing = spacing or ''
+ if type(input) == 'table' then
+ local res = '{\n'
+ for k, v in pairs(input) do
+ if type(k) ~= 'string' or string.sub(k, 1, 2) ~= '__' then
+ res = res .. spacing .. ' [\'' .. tostring(k) .. '\'] = ' ..
+ helpers.tostring(v, spacing .. ' ', limit - 1)
+ end
+ end
+ return res .. spacing .. '}\n'
+ else
+ return tostring(input) .. '\n'
+ end
+end
+
+--[[ Convert a number of seconds into a string of the format hhh:mm:ss, where
+
+* 'hhh' represents the hours,
+* 'mm' the minutes, and
+* 'ss' the seconds.
+
+Omits 'hhh:' if hours is 0, and also 'mm:' if hours and minutes are zero.
+Rounds-up timeSeconds to the closest second.
+--]]
+function helpers.secondsToTimeString(timeSeconds)
+ local s = math.ceil(timeSeconds)
+ if s < SECONDS_IN_MINUTE then
+ return string.format('%d', s)
+ elseif s < SECONDS_IN_HOUR then
+ return string.format('%d:%.2d',
+ s / SECONDS_IN_MINUTE, s % SECONDS_IN_MINUTE)
+ else
+ return string.format('%d:%.2d:%.2d',
+ s / SECONDS_IN_HOUR,
+ s / SECONDS_IN_MINUTE % SECONDS_IN_MINUTE,
+ s % SECONDS_IN_MINUTE)
+ end
+end
+
+--[[ Converts from a string to its most likely type.
+
+Numbers are returned as numbers.
+"true" and "false" are returned as booleans.
+Everything else is unchanged.
+]]
+function helpers.fromString(input)
+ local number = tonumber(input)
+ if number ~= nil then
+ return number
+ end
+ if input == "true" then
+ return true
+ end
+ if input == "false" then
+ return false
+ end
+ return input
end
return helpers
diff --git a/assets/game_scripts/common/human_recognisable_pickups.lua b/assets/game_scripts/common/human_recognisable_pickups.lua
new file mode 100644
index 00000000..8b85dc0e
--- /dev/null
+++ b/assets/game_scripts/common/human_recognisable_pickups.lua
@@ -0,0 +1,376 @@
+-- Objects created by this code will only have their correct appearance if the
+-- level has decorators/human_recognisable_pickups applied.
+
+local combinatorics = require 'common.combinatorics'
+local helpers = require 'common.helpers'
+local image = require 'dmlab.system.image'
+local pickups = require 'common.pickups'
+local random = require 'common.random'
+local set = require 'common.set'
+local tensor = require 'dmlab.system.tensor'
+local game = require 'dmlab.system.game'
+
+local PATTERN_DIR = game:runFiles() .. '/baselab/game_scripts/patterns/'
+
+local hrp = {}
+
+local PREFIX = 'HRP_'
+
+local SHAPES = {
+ 'apple2',
+ 'ball',
+ 'balloon',
+ 'banana',
+ 'bottle',
+ 'cake',
+ 'can',
+ 'car',
+ 'cassette',
+ 'chair',
+ 'cherries',
+ 'cow',
+ 'flower',
+ 'fork',
+ 'fridge',
+ 'guitar',
+ 'hair_brush',
+ 'hammer',
+ 'hat',
+ 'ice_lolly',
+ 'jug',
+ 'key',
+ 'knife',
+ 'ladder',
+ 'mug',
+ 'pencil',
+ 'pig',
+ 'pincer',
+ 'plant',
+ 'saxophone',
+ 'shoe',
+ 'spoon',
+ 'suitcase',
+ 'tennis_racket',
+ 'tomato',
+ 'toothbrush',
+ 'tree',
+ 'tv',
+ 'wine_glass',
+ 'zebra',
+}
+
+local SHAPES_SET = set.Set(SHAPES)
+
+local TWO_COLOR_PATTERNS = {
+ 'chequered',
+ 'crosses',
+ 'diagonal_stripe',
+ 'discs',
+ 'hex',
+ 'pinstripe',
+ 'spots',
+ 'swirls',
+}
+
+local PATTERNS = helpers.shallowCopy(TWO_COLOR_PATTERNS)
+PATTERNS[#PATTERNS + 1] = 'solid'
+
+local PATTERNS_SET = set.Set(PATTERNS)
+
+local SCALES = {
+ small = 0.62,
+ medium = 1.0,
+ large = 1.62,
+}
+
+-- Per object scaling for objects where the default large size exceeds the
+-- bounds of a maze cell.
+local CUSTOM_SCALES = {
+ banana = {large = 1.58},
+ car = {large = 1.02, medium = 0.8},
+ cassette = {large = 1.45, medium = 0.95},
+ chair = {large = 1.15, medium = 0.84},
+ cow = {large = 1.06, medium = 0.81},
+ hat = {large = 1.55},
+ pig = {large = 1.52},
+}
+
+local COLORS = {
+ {0, 0, 0},
+ {0, 0, 170},
+ {0, 170, 0},
+ {0, 170, 170},
+ {170, 0, 0},
+ {170, 0, 170},
+ {170, 85, 0},
+ {170, 170, 170},
+ {85, 85, 85},
+ {85, 85, 255},
+ {85, 255, 85},
+ {85, 255, 255},
+ {255, 85, 85},
+ {255, 85, 255},
+ {255, 255, 85},
+ {255, 255, 255},
+}
+
+-- Store loaded textures.
+hrp._patternTextures = {}
+
+-- Convert a scale name to a number, accounting for per-shape overrides.
+local function scaleNameToNumber(scale, shape)
+ return CUSTOM_SCALES[shape] and CUSTOM_SCALES[shape][scale] or SCALES[scale]
+end
+
+-- Returns normalised float texture, loading once from disk if need be.
+function hrp.getPatternTexture(patternName, width, height)
+ if not PATTERNS_SET[patternName] then
+ error("Unknown pattern: " .. tostring(patternName))
+ end
+
+ if not hrp._patternTextures[patternName] then
+ local floatImage
+ if patternName == 'solid' then
+ floatImage = tensor.FloatTensor(width, height, 4):fill(1.0)
+ else
+ local path = PATTERN_DIR .. patternName .. '_d.png'
+ local byteImage = image.load(path)
+ local shape = byteImage:shape()
+ if shape[1] ~= width or shape[2] ~= height then
+ byteImage = image.scale(byteImage, width, height)
+ end
+ floatImage = byteImage:float():mul(1 / 255)
+ end
+ assert(floatImage:shape()[3] == 4,
+ 'Pattern textures must be in RGBA format.')
+ hrp._patternTextures[patternName] = floatImage
+ end
+ return hrp._patternTextures[patternName]
+end
+
+function hrp.create(kwargs)
+ local shape = kwargs.shape or error("Missing shape")
+ if not SHAPES_SET[shape] then error("Unknown shape: " .. tostring(shape)) end
+
+ local id = hrp._nextId
+ hrp._nextId = hrp._nextId + 1
+
+ local scale = 1.0
+ if kwargs.scale then
+ if type(kwargs.scale) == 'number' then
+ scale = kwargs.scale
+ else
+ assert(type(kwargs.scale) == 'string')
+ scale = scaleNameToNumber(kwargs.scale, shape)
+ assert(scale, '"' .. tostring(kwargs.scale) .. '" is not a valid scale.')
+ end
+ end
+
+ local transformSuffix = scale ~= 1 and '%scale{' .. scale .. '}' or ''
+
+ local obj = {
+ name = shape,
+ classname = shape,
+ model = string.format('%s%d:models/hr_%s.md3%s',
+ PREFIX, id, shape, transformSuffix),
+ quantity = kwargs.quantity or 0,
+ type = pickups.type.REWARD,
+ tag = kwargs.moveType or pickups.moveType.BOB,
+ }
+ -- Register the requested configuration against the ID for later lookup by
+ -- modifyTexture.
+ hrp._objectConfigs[id] = {
+ pattern = kwargs.pattern or error("Missing pattern"),
+ color1 = kwargs.color1 or error("Missing color1"),
+ color2 = kwargs.color2 or error("Missing color2"),
+ }
+ return obj
+end
+
+function hrp.replaceModelName(modelName)
+ if modelName:sub(1, #PREFIX) == PREFIX then
+ local prefixTexture, newModelName = modelName:match('(.*:)(.*)')
+ return newModelName, prefixTexture
+ end
+end
+
+function hrp.replaceTextureName(textureName)
+ if textureName:sub(1, #PREFIX) == PREFIX then
+ -- Strip prefix and id.
+ return textureName:match('.*:(.*)')
+ end
+end
+
+local function pattern(patternChannel, color1, color2)
+ local invChannel = patternChannel:clone():mul(-1):add(1)
+ return patternChannel:clone():mul(color1):cadd(invChannel:mul(color2))
+end
+
+function hrp.applyPatternAndColors(objTexture, patternTexture, color1, color2)
+ local floatObjTexture = objTexture:float()
+ local r = floatObjTexture:select(3, 1)
+ local g = floatObjTexture:select(3, 2)
+ local b = floatObjTexture:select(3, 3)
+ local a = floatObjTexture:select(3, 4):clone():mul(1 / 255)
+ local invA = a:clone():mul(-1):add(1)
+
+ -- Generate colorised copies of the pattern for each channel.
+ local patternR = pattern(patternTexture:select(3, 1), color1[1], color2[1])
+ local patternG = pattern(patternTexture:select(3, 2), color1[2], color2[2])
+ local patternB = pattern(patternTexture:select(3, 3), color1[3], color2[3])
+
+ -- Combine original texture with pattern according to original alpha.
+ r:cmul(invA):cadd(patternR:cmul(a))
+ g:cmul(invA):cadd(patternG:cmul(a))
+ b:cmul(invA):cadd(patternB:cmul(a))
+
+ objTexture:copy(floatObjTexture:byte())
+
+ collectgarbage()
+ collectgarbage()
+end
+
+function hrp.modifyTexture(textureName, texture)
+ if textureName:sub(1, #PREFIX) == PREFIX then
+ local id = tonumber(textureName:match('(%d+):'))
+ local config = hrp._objectConfigs[id]
+ local shape = texture:shape()
+ local patternTexture = hrp.getPatternTexture(config.pattern, shape[1],
+ shape[2])
+ hrp.applyPatternAndColors(texture, patternTexture, config.color1,
+ config.color2)
+ return true
+ end
+ return false
+end
+
+function hrp.shapes()
+ return SHAPES
+end
+
+function hrp.patterns()
+ return PATTERNS
+end
+
+function hrp.twoColorPatterns()
+ return TWO_COLOR_PATTERNS
+end
+
+function hrp.colors()
+ return COLORS
+end
+
+
+-- Return an array of n object specifications, all of which have a unique set
+-- of colors, shapes, and patterns.
+function hrp.uniquePickups(n)
+ local shapes = hrp.shapes()
+ local patterns = hrp.twoColorPatterns()
+ local colors = hrp.colors()
+
+ return hrp.specifyUniquePickups(n, shapes, patterns, colors)
+end
+
+
+-- Return an array of n object specifications, all of which have a unique set
+-- of colors, shapes, and patterns.
+function hrp.specifyUniquePickups(n, shapes, patterns, colors, colorNames)
+ -- There are `count` unique pickups: #shapes * #patterns * choose(2, #colors)
+ -- where choose(2, #colors) is the number of ways to select two different
+ -- colors, ignoring order.
+ local count = #shapes * #patterns * combinatorics.choose(2, #colors)
+ assert(n <= count, "Requesting more unique pickups than can be generated.")
+ assert(colorNames == nil or #colorNames == #colors,
+ "Must have as many color names as colors.")
+
+ local result = {}
+ local idGenerator = random:shuffledIndexGenerator(count)
+ for i = 1, n do
+ -- id represents one of the unique pickups. 0 <= id < count.
+ -- We will convert id to its shape, pattern, and two colors.
+ local id = idGenerator() - 1
+
+ -- Modulo-out the shape and pattern dimensions from id.
+ local shape = (id % #shapes) + 1
+ id = math.floor(id / #shapes)
+
+ local pattern = (id % #patterns) + 1
+ id = math.floor(id / #patterns)
+
+ -- Remaining id value is between 0 and C(#colors,2) - 1.
+ -- colors[0] < colors[1], so randomly swap them for more variability.
+ local colorPair = combinatorics.twoItemSelection(id + 1, #colors)
+ local swap = random:uniformInt(0, 1) == 0
+ local color1 = swap and colorPair[2] or colorPair[1]
+ local color2 = swap and colorPair[1] or colorPair[2]
+ result[i] = {
+ shape = shapes[shape],
+ pattern = patterns[pattern],
+ color1 = colors[color1],
+ color2 = colors[color2]
+ }
+ if colorNames then
+ result[i].colorName1 = colorNames[color1]
+ result[i].colorName2 = colorNames[color2]
+ end
+ end
+ return result
+end
+
+-- Return an array of n object specifications, all of which have a unique
+-- shape, and which also have patterns of colors applied.
+function hrp.uniquelyShapedPickups(n)
+ local shapes = hrp.shapes()
+ local patterns = hrp.twoColorPatterns()
+ local colors = hrp.colors()
+
+ return hrp.specifyUniquelyShapedPickups(n, shapes, patterns, colors)
+end
+
+-- Return an array of n object specifications, all of which have a unique
+-- shape, and which also have patterns of colors applied.
+function hrp.specifyUniquelyShapedPickups(n, shapes, patterns, colors,
+ colorNames)
+ assert(n <= #shapes, "Requesting more unique pickups than can be generated.")
+ assert(colorNames == nil or #colorNames == #colors,
+ "Must have as many color names as colors.")
+
+ local result = {}
+ local idGenerator = random:shuffledIndexGenerator(#shapes)
+ for i = 1, n do
+ -- id represents one of the unique pickups. 0 <= id < count.
+ -- We will convert id to its shape, pattern, and two colors.
+ local shape = idGenerator()
+
+ local pattern = random:uniformInt(1, #patterns)
+ local colorPairIndex =
+ random:uniformInt(1, combinatorics.choose(2, #colors) - 1)
+
+ -- Remaining id value is between 0 and C(#colors,2) - 1.
+ -- colors[0] < colors[1], so randomly swap them for more variability.
+ local colorPair = combinatorics.twoItemSelection(colorPairIndex, #colors)
+ local swap = random:uniformInt(0, 1) == 0
+ local color1 = swap and colorPair[2] or colorPair[1]
+ local color2 = swap and colorPair[1] or colorPair[2]
+ result[i] = {
+ shape = shapes[shape],
+ pattern = patterns[pattern],
+ color1 = colors[color1],
+ color2 = colors[color2]
+ }
+ if colorNames then
+ result[i].colorName1 = colorNames[color1]
+ result[i].colorName2 = colorNames[color2]
+ end
+ end
+ return result
+end
+
+function hrp.reset()
+ hrp._nextId = 1
+ hrp._objectConfigs = {}
+end
+
+hrp.reset()
+
+return hrp
diff --git a/assets/game_scripts/common/inventory.lua b/assets/game_scripts/common/inventory.lua
new file mode 100644
index 00000000..098f5a09
--- /dev/null
+++ b/assets/game_scripts/common/inventory.lua
@@ -0,0 +1,166 @@
+-- Enums extracted from "engine/code/qcommon/q_shared.h"
+
+local inventory = {}
+
+inventory.GADGETS = {
+ IMPULSE = 2, -- Contact gadget.
+ RAPID = 3, -- Rapid fire gadget.
+ ORB = 6, -- Area damage gadget. (Knocks players)
+ BEAM = 7, -- Accurate and very rapid fire beam.
+ DISC = 8, -- Powerful but long period between firing.
+}
+
+inventory.UNLIMITED = -1
+
+inventory.POWERUPS = {
+ RED_FLAG = 8,
+ BLUE_FLAG = 9,
+}
+
+local function gadgetsToStat(gadgets)
+ local result = 0
+ for i, gadget in ipairs(gadgets) do
+ result = result + 2 ^ (gadget - 1)
+ end
+ return result
+end
+
+local function statToGadgets(stat)
+ local result = {}
+ local counter = 1
+ while stat > 0 do
+ local old = stat / 2
+ local new = math.floor(old)
+ if old ~= new then
+ result[#result + 1] = counter
+ end
+ counter = counter + 1
+ stat = new
+ end
+ return result
+end
+
+local STATS = {
+ HEALTH = 1, -- Health of player
+ GADGETS = 3, -- Mask of current gadgets held.
+ ARMOR = 4, -- Quantity of armour.
+ MAX_HEALTH = 7, -- If health is greater than this value it decreases to it
+ -- over time.
+}
+
+local View = {}
+local ViewMT = {__index = View}
+
+function View:gadgetAmount(gadget)
+ return self._loadOut.amounts[gadget]
+end
+
+function View:setGadgetAmount(gadget, amount)
+ self._loadOut.amounts[gadget] = amount
+end
+
+-- Returns list of gadgets.
+function View:gadgets()
+ return statToGadgets(self._loadOut.stats[STATS.GADGETS])
+end
+
+-- Sets list of gadgets.
+function View:setGadgets(gadgets)
+ self._loadOut.stats[STATS.GADGETS] = gadgetsToStat(gadgets)
+end
+
+-- Adds gadget with optional amount.
+function View:addGadget(gadget, amount)
+ if amount then
+ self:setGadgetAmount(gadget, amount)
+ end
+ local gadgets = self:gadgets()
+ for i, v in gadgets do
+ if v == gadget then
+ return
+ end
+ end
+ gadgets[#gadgets + 1] = gadget
+ self:setGadgets(gadgets)
+end
+
+-- Removes a gadget if present, sets the gadgets amount to 0.
+function View:removeGadget(gadget)
+ local gadgets = self:gadgets()
+ local newGadgets = {}
+ for i, v in gadgets do
+ if v ~= gadget then
+ newGadgets[#newGadgets + 1] = v
+ end
+ end
+ self:setGadgets(newGadgets)
+ self:setGadgetAmount(gadget, 0)
+end
+
+-- Returns whether powerup is active.
+function View:hasPowerUp(powerUp)
+ return self._loadOut.powerups[powerUp] ~= 0
+end
+
+-- Returns player's armor
+function View:armor()
+ return self._loadOut.stats[STATS.ARMOR]
+end
+
+-- Sets player's armor
+function View:setArmor(amount)
+ self._loadOut.stats[STATS.ARMOR] = amount
+end
+
+-- Gets player's health.
+function View:health()
+ return self._loadOut.stats[STATS.HEALTH]
+end
+
+-- Sets player's health.
+function View:setHealth(amount)
+ self._loadOut.stats[STATS.HEALTH] = amount
+end
+
+-- Gets player's max health. If health() > maxHealth() health will reduce until
+-- it matches maxHealth().
+function View:maxHealth()
+ return self._loadOut.stats[STATS.MAX_HEALTH]
+end
+
+-- Sets player's max health.
+function View:setMaxHealth(amount)
+ self._loadOut.stats[STATS.MAX_HEALTH] = amount
+end
+
+-- Returns player's eye position in world units.
+function View:eyePos()
+ local x, y, z = unpack(self._loadOut.position)
+ return {x, y, z + self._loadOut.height}
+end
+
+-- Returns players view direction in Euler angles degrees.
+function View:eyeAngles()
+ return self._loadOut.angles
+end
+
+-- Returns players id.
+function View:playerId()
+ return self._loadOut.playerId
+end
+
+-- Returns players gadget.
+function View:gadget()
+ return self._loadOut.gadget
+end
+
+function View:loadOut()
+ return self._loadOut
+end
+
+function inventory.View(loadOut)
+ return setmetatable({_loadOut = loadOut}, ViewMT)
+end
+
+
+return inventory
diff --git a/assets/game_scripts/common/make_map.lua b/assets/game_scripts/common/make_map.lua
index d46d56a8..e3dc0aff 100644
--- a/assets/game_scripts/common/make_map.lua
+++ b/assets/game_scripts/common/make_map.lua
@@ -1,36 +1,53 @@
local map_maker = require 'dmlab.system.map_maker'
-
-local LEVEL_DATA = '/tmp/dmlab_level_data'
+local random = require 'common.random'
+local themes = require 'themes.themes'
+local texture_sets = require 'themes.texture_sets'
local make_map = {}
-local pickups = {
+local PICKUPS = {
A = 'apple_reward',
G = 'goal',
}
-function make_map.makeMap(mapName, mapEntityLayer, mapVariationsLayer)
- os.execute('mkdir -p ' .. LEVEL_DATA .. '/baselab')
- assert(mapName)
+local SKYBOX_TEXTURE_NAME = 'map/lab_games/sky/lg_sky_03'
+
+function make_map.makeMap(kwargs)
+ assert(kwargs.mapName)
+ local skyboxTextureName = nil
+ if kwargs.useSkybox then
+ skyboxTextureName = SKYBOX_TEXTURE_NAME
+ end
map_maker:mapFromTextLevel{
- entityLayer = mapEntityLayer,
- variationsLayer = mapVariationsLayer,
- outputDir = LEVEL_DATA .. '/baselab',
- mapName = mapName,
- callback = function(i, j, c, maker)
- if pickups[c] then
- return maker:makeEntity(i, j, pickups[c])
+ entityLayer = kwargs.mapEntityLayer,
+ variationsLayer = kwargs.mapVariationsLayer,
+ mapName = kwargs.mapName,
+ allowBots = kwargs.allowBots,
+ skyboxTextureName = skyboxTextureName,
+ theme = kwargs.theme or themes.fromTextureSet{
+ textureSet = kwargs.textureSet or texture_sets.MISHMASH,
+ decalFrequency = kwargs.decalFrequency,
+ floorModelFrequency = kwargs.floorModelFrequency,
+ },
+ callback = kwargs.callback or function(i, j, c, maker)
+ local pickup = kwargs.pickups and kwargs.pickups[c] or PICKUPS[c]
+ if pickup then
+ return maker:makeEntity{
+ i = i,
+ j = j,
+ classname = pickup,
+ }
end
end
}
- return mapName
-end
-
-function make_map.commandLine(old_command_line)
- return old_command_line .. '+set sv_pure 0 +set fs_steampath ' .. LEVEL_DATA
+ return kwargs.mapName
end
function make_map.seedRng(value)
map_maker:randomGen():seed(value)
end
+function make_map.random()
+ return random(map_maker:randomGen())
+end
+
return make_map
diff --git a/assets/game_scripts/common/pickups.lua b/assets/game_scripts/common/pickups.lua
index 80de5789..62420217 100644
--- a/assets/game_scripts/common/pickups.lua
+++ b/assets/game_scripts/common/pickups.lua
@@ -1,68 +1,75 @@
local pickups = {}
+-- Must match itemType_t in engine/code/game/bg_public.h.
pickups.type = {
- kInvalid = 0,
- kWeapon = 1,
- kAmmo = 2,
- kArmor = 3,
- kHealth = 4,
- kPowerUp = 5,
- kHoldable = 6,
- kPersistant_PowerUp = 7,
- kTeam = 8,
- kReward = 9,
- kGoal = 10
+ INVALID = 0,
+ WEAPON = 1,
+ AMMO = 2,
+ ARMOR = 3,
+ HEALTH = 4,
+ POWER_UP = 5,
+ HOLDABLE = 6,
+ PERSISTANT_POWERUP = 7,
+ TEAM = 8,
+ REWARD = 9,
+ GOAL = 10
+}
+
+-- Must match reward_mv_t in engine/code/game/bg_public.h.
+pickups.moveType = {
+ BOB = 0,
+ STATIC = 1
}
pickups.defaults = {
apple_reward = {
name = 'Apple',
- class_name = 'apple_reward',
- model_name = 'models/apple.md3',
+ classname = 'apple_reward',
+ model = 'models/apple.md3',
quantity = 1,
- type = pickups.type.kReward
+ type = pickups.type.REWARD
},
lemon_reward = {
name = 'Lemon',
- class_name = 'lemon_reward',
- model_name = 'models/lemon.md3',
+ classname = 'lemon_reward',
+ model = 'models/lemon.md3',
quantity = -1,
- type = pickups.type.kReward
+ type = pickups.type.REWARD
},
strawberry_reward = {
name = 'Strawberry',
- class_name = 'strawberry_reward',
- model_name = 'models/strawberry.md3',
+ classname = 'strawberry_reward',
+ model = 'models/strawberry.md3',
quantity = 2,
- type = pickups.type.kReward
+ type = pickups.type.REWARD
},
fungi_reward = {
name = 'Fungi',
- class_name = 'fungi_reward',
- model_name = 'models/toadstool.md3',
+ classname = 'fungi_reward',
+ model = 'models/toadstool.md3',
quantity = -10,
- type = pickups.type.kReward
+ type = pickups.type.REWARD
},
watermelon_goal = {
name = 'Watermelon',
- class_name = 'watermelon_goal',
- model_name = 'models/watermelon.md3',
+ classname = 'watermelon_goal',
+ model = 'models/watermelon.md3',
quantity = 20,
- type = pickups.type.kGoal
+ type = pickups.type.GOAL
},
goal = {
name = 'Goal',
- class_name = 'goal',
- model_name = 'models/goal_object_02.md3',
+ classname = 'goal',
+ model = 'models/goal_object_02.md3',
quantity = 10,
- type = pickups.type.kGoal
+ type = pickups.type.GOAL
},
- goal_mango = {
+ mango_goal = {
name = 'Mango',
- class_name = 'goal',
- model_name = 'models/mango.md3',
+ classname = 'mango_goal',
+ model = 'models/mango.md3',
quantity = 100,
- type = pickups.type.kGoal
+ type = pickups.type.GOAL
}
}
diff --git a/assets/game_scripts/common/position_trigger.lua b/assets/game_scripts/common/position_trigger.lua
new file mode 100644
index 00000000..2b3302d1
--- /dev/null
+++ b/assets/game_scripts/common/position_trigger.lua
@@ -0,0 +1,156 @@
+local maze_generation = require 'dmlab.system.maze_generation'
+
+local PositionTrigger = {}
+local PositionTriggerMT = {__index = PositionTrigger}
+
+--[[ Create a position trigger class that starts at currentTime, if specified.
+
+You must call `update(currentPosition)` whenever the `currentPosition` changes.
+This call will make any callbacks on any triggers that have fired with the new
+position.
+--]]
+function PositionTrigger.new()
+ return setmetatable({_triggers = {}}, PositionTriggerMT)
+end
+
+--[[ Start trigger `kwargs.name` that will fire when currentPosition overlaps
+a square in `kwargs.maze` that matches `kwargs.triggerWhenEnter` or
+`kwargs.triggerWhenExit`.
+
+Once the trigger fires, `kwargs.callback` will be called and the trigger will be
+removed unless `true` is returned.
+
+Keyword arguments:
+
+* `name` (string) The name of the trigger.
+* `maze` (string) A text layout of the maze.
+* `triggerWhenEnter` (character) The character in `maze` that sets off the
+ trigger, the first time the position is on that character in `maze`.
+* `triggerWhenExit` (character) The character in `maze` that sets off the
+ trigger, when position stops being that character in `maze`.
+* `callback` (function) Callback that's called when the trigger goes off.
+ If the callback returns `true` then the trigger is re-engaged to go off
+ under the same enter/exit conditions. By default, however, the trigger is
+ removed.
+
+Example: Print a happy message in 2 seconds.
+
+```Lua
+local PositionTrigger = require 'common.position_trigger'
+
+local HAPPINESS_LAYER = [ [
+..LL.LL..
+.L..L..L.
+..L...L..
+...L.L...
+....L....
+] ]
+
+myTriggers = PositionTrigger.new()
+myTriggers:start{
+ name = 'my happiness trigger',
+ maze = HAPPINESS_LAYER,
+ triggerWhenEnter = 'L',
+ callback = function() print('HAPPINESS!') io.flush() end,
+}
+
+function api:myFunctionCalledEveryFrame(currentPosition)
+ myPositionTrigger:update(currentPosition)
+end
+
+```
+--]]
+function PositionTrigger:start(kwargs)
+ assert(kwargs.name)
+ assert(kwargs.maze)
+ assert(kwargs.triggerWhenExit or kwargs.triggerWhenEnter)
+ assert(kwargs.callback)
+ local sizeI, sizeJ = self:mazeSize()
+ self._triggers[kwargs.name] = {
+ maze = maze_generation:mazeGeneration{entity = kwargs.maze},
+ triggerWhenExit = kwargs.triggerWhenExit,
+ triggerWhenEnter = kwargs.triggerWhenEnter,
+ callback = kwargs.callback,
+ }
+
+ -- All mazes must have the same size, since `update(currentPosition)` assumes
+ -- the same maze for all position triggers.
+ assert(sizeI == nil or
+ self._triggers[kwargs.name].maze:size() == sizeI, sizeJ)
+end
+
+-- Remove `name` from the active triggers.
+function PositionTrigger:remove(name)
+ self._triggers[name] = nil
+end
+
+-- Remove all triggers, but maintain the current position.
+function PositionTrigger:removeAll()
+ self._triggers = {}
+end
+
+-- Returns true if there exists an active trigger called `name`.
+function PositionTrigger:exists(name)
+ return self._triggers[name] ~= nil
+end
+
+-- All mazes must have the same size. Return the size of the first trigger's
+-- maze, or nil if no triggers exist.
+function PositionTrigger:mazeSize()
+ for _, v in pairs(self._triggers) do
+ return v.maze:size()
+ end
+ return nil, nil
+end
+
+-- Return the size of the first text maze, which has to be consistent for all
+-- triggers.
+function PositionTrigger:mazeCoordinatesFromWorldPosition(worldPosition)
+ for _, v in pairs(self._triggers) do
+ return v.maze:fromWorldPos(worldPosition[1], worldPosition[2])
+ end
+ return nil, nil
+end
+
+--[[ Provide the new current position in world space.
+Check if any triggers have fired.
+For triggers that have fired, call their callbacks and remove them unless
+the callback returns `true`. Must be called manually by the owner of this class.
+--]]
+function PositionTrigger:update(currentPosition)
+ -- If we have no triggers, or if the maze coordinates haven't changed, then
+ -- there is nothing to do.
+ local nextI, nextJ = self:mazeCoordinatesFromWorldPosition(currentPosition)
+ if nextI == nil then return end
+ if self._currentI == nextI and self._currentJ == nextJ then return end
+
+ -- Check if we've transitioned cell types. If so, see if we've triggered
+ -- the enter or exit criteria. If so, make callback.
+ local triggersToRemove = {}
+ for name, v in pairs(self._triggers) do
+ local currentCell = self._currentI and
+ v.maze:getEntityCell(self._currentI, self._currentJ)
+ local nextCell = v.maze:getEntityCell(nextI, nextJ)
+ if currentCell ~= nextCell then
+ if (v.triggerWhenExit and v.triggerWhenExit == currentCell) or
+ (v.triggerWhenEnter and v.triggerWhenEnter == nextCell) then
+ local persist = v.callback()
+ if not persist then
+ triggersToRemove[#triggersToRemove + 1] = name
+ end
+ end
+ end
+ end
+
+ -- Remove any triggers that have fired.
+ for _, name in pairs(triggersToRemove) do
+ self:remove(name)
+ end
+
+ -- Update our current maze coordinates.
+ self._currentI = nextI
+ self._currentJ = nextJ
+end
+
+return PositionTrigger
+
diff --git a/assets/game_scripts/common/random.lua b/assets/game_scripts/common/random.lua
index 14d86f9f..b81153ce 100644
--- a/assets/game_scripts/common/random.lua
+++ b/assets/game_scripts/common/random.lua
@@ -3,28 +3,203 @@
local sys_random = require 'dmlab.system.random'
+local MAP_COUNT = 100000
+local THEME_COUNT = 1000
+
local random = {}
--- Set the seed of the underlying pseudo-random-bit generator. The argument
--- may be a number or a string representation of a number. Beware of precision
--- loss when using very large numeric values.
---
--- It is probably useful to call this function with the per-episode seed in
--- the "init" callback so that episodes play out reproducibly.
-function random.seed(value)
- sys_random:seed(value)
+--[[ Set the seed of the underlying pseudo-random-bit generator. The argument
+may be a number or a string representation of a number. Beware of precision loss
+when using very large numeric values.
+
+It is probably useful to call this function with the per-episode seed in the
+"start" callback so that episodes play out reproducibly. ]]
+function random:seed(value)
+ return self._rng:seed(value)
+end
+
+-- Returns an integer sampled uniformly at random from the closed range
+-- [1, MAP_COUNT].
+function random:mapGenerationSeed()
+ return self._rng:uniformInt(1, MAP_COUNT)
+end
+
+
+-- Returns an integer sampled uniformly at random from the closed range
+-- [1, THEME_COUNT].
+function random:themeGenerationSeed()
+ return self._rng:uniformInt(1, THEME_COUNT)
end
-- Returns an integer sampled uniformly at random from the closed range
-- [lower, upper].
-function random.uniformInt(lower, upper)
- return sys_random:uniformInt(lower, upper)
+function random:uniformInt(lower, upper)
+ return self._rng:uniformInt(lower, upper)
end
-- Returns a real number sampled uniformly at random from the half-open range
-- [lower, upper).
-function random.uniformReal(lower, upper)
- return sys_random:uniformReal(lower, upper)
+function random:uniformReal(lower, upper)
+ return self._rng:uniformReal(lower, upper)
+end
+
+-- Returns an integer in the range [1, n] where the probability of each
+-- integer i is the ith weight divided by the sum of the n weights.
+function random:discreteDistribution(weights)
+ return self._rng:discreteDistribution(weights)
+end
+
+-- Returns a real number sampled from the random distrubution centered around
+-- mean with standard distribution stddev.
+function random:normal(mean, stddev)
+ return self._rng:normal(mean, stddev)
+end
+
+-- Returns an 8-bit triplet representing a random RGB color:
+-- {0~255, 0~255, 0~255}.
+function random:color()
+ local function uniform255() return self._rng:uniformInt(0, 255) end
+ return {uniform255(), uniform255(), uniform255()}
+end
+
+-- Returns an element sampled uniformly at random from a table. You can specify
+-- a consecutive part of the table to sample by specifying startIndex and
+-- endIndex. By default the entire table will be used.
+function random:choice(t, startIndex, endIndex)
+ t = t or {}
+ if not startIndex or startIndex < 1 then
+ startIndex = 1
+ end
+ if not endIndex or endIndex > #t then
+ endIndex = #t
+ end
+ if startIndex > endIndex then
+ return nil
+ end
+ return t[self._rng:uniformInt(startIndex, endIndex)]
+end
+
+-- Shuffles a Lua array in place. If n is given, the shuffle stops after the
+-- first n elements have been placed. 'n' is clamped to the size of the array.
+function random:shuffleInPlace(array, n)
+ local c = (n and n < #array) and n or #array - 1
+ for i = 1, c do
+ local j = self._rng:uniformInt(i, #array)
+ array[j], array[i] = array[i], array[j]
+ end
+ return array
+end
+
+-- Returns a shuffled copy of a Lua array.
+function random:shuffle(array)
+ local ret = {}
+ for i, obj in ipairs(array) do
+ ret[i] = obj
+ end
+ return self:shuffleInPlace(ret)
+end
+
+function random:generator()
+ return self._rng
+end
+
+--[[ Returns a 'sampling without replacement' number generator with the integer
+range range [1, count].
+
+Generator's memory grows linearly. Initialization and calling costs are both
+O(1).
+
+The generator returns nil when no new samples are available and resets to its
+initial state.
+--]]
+function random:shuffledIndexGenerator(count)
+ -- The number of values that have been generated so far.
+ local current = 0
+
+ -- A sparse array of shuffled values.
+ local elements = {}
+
+ --[[
+ All values <= current that are *not* in
+ the completed shuffling.
+ completed These values are at indices that *are* in
+ shuffling the completed shuffling.
+
+ elements [+++++++++++|--+---------+--++----------+----------+--]
+ . . .
+ . . .
+ 1 current count
+
+ Invariant A: All values <= current will be in `elements`.
+ Invariant B: Every value in the completed shuffling has
+ elements[value] ~= nil.
+
+ The algorithm starts with current = 0 and elements empty, so the
+ invariants start true.
+
+ The algorithm increments as follows,
+ 1. Increment current
+ current = current + 1
+
+ 2. Generate `random`, a random value in the range [current, count]
+ random = randomNumberInRange(current, count)
+
+ 3. Basic operation:
+ (a) output `random` as the next completed shuffling value; that is,
+ insert `random` at elements[current].
+ elements[random] is nil ==> elements[current] = random
+
+ (b) insert `current` at elements[random].
+ elements[current] is nil ==> elements[random] = current
+
+ Exception operations:
+ - If elements[current] already has a value, then (by invariant B),
+ current is already in the completed shuffling. In this case we
+ should avoid (b) and instead push the existing elements[current]
+ back into elements[random].
+ elements[current] has a value ==>
+ elements[random] = elements[current]
+
+ - If elements[random] already has a value, then (by invariant B),
+ random is already in the completed shuffling. In this case we
+ should avoid (a) and instead output the deferred value
+ elements[random] in the completed shuffling.
+ elements[random] has a value ==>
+ elements[current] = elements[random]
+
+ Note that both the basic and exception operations maintain the invariants,
+ so by induction the invariants are always true.
+ --]]
+ return function()
+ -- If we've shuffled all the elements, return nil for one call and reset
+ -- to initial conditions. The caller can start again if a new shuffling is
+ -- desired.
+ if count == current then
+ elements = {}
+ current = 0
+ return nil
+ end
+
+ -- Step 1.
+ current = current + 1
+
+ -- Step 2.
+ local random = self._rng:uniformInt(current, count)
+
+ -- Step 3.
+ local currentStartValue = elements[current]
+ elements[current] = elements[random] or random
+ elements[random] = currentStartValue or current
+
+ -- Return the tail of the completed shuffling that we just generated.
+ return elements[current]
+ end
+end
+
+local randomMT = {__index = random}
+
+function randomMT:__call(rng)
+ return setmetatable({_rng = rng}, randomMT)
end
-return random
+return setmetatable({_rng = sys_random}, randomMT)
diff --git a/assets/game_scripts/common/screen_message.lua b/assets/game_scripts/common/screen_message.lua
index 79dfb587..3c4d2bc0 100644
--- a/assets/game_scripts/common/screen_message.lua
+++ b/assets/game_scripts/common/screen_message.lua
@@ -1,11 +1,11 @@
local screen_message = {
-- Alignment for screen_message alignment parameter.
- kAlignLeft = 0,
- kAlignRight = 1,
- kAlignCenter = 2,
+ ALIGN_LEFT = 0,
+ ALIGN_RIGHT = 1,
+ ALIGN_CENTER = 2,
-- Default border to stop text from being to close to the edge.
- kBorderSize = 5,
+ BORDER_SIZE = 5,
}
return screen_message
diff --git a/assets/game_scripts/common/set.lua b/assets/game_scripts/common/set.lua
new file mode 100644
index 00000000..59c44fc3
--- /dev/null
+++ b/assets/game_scripts/common/set.lua
@@ -0,0 +1,65 @@
+-- Provides common set-like operations.
+local set = {}
+
+-- Insert the elements of list into existingSet.
+function set.insert(existingSet, list)
+ for _, v in ipairs(list) do
+ existingSet[v] = true
+ end
+ return existingSet
+end
+
+-- Turn a list L into a set S. For any V in L, S[V] = true.
+function set.Set(list)
+ return set.insert({}, list)
+end
+
+function set.toList(lhs)
+ local list = {}
+ for key, _ in pairs(lhs) do
+ list[#list + 1] = key
+ end
+ return list
+end
+
+function set.isSame(lhs, rhs)
+ for k, _ in pairs(lhs) do
+ if not rhs[k] then return false end
+ end
+ for k, _ in pairs(rhs) do
+ if not lhs[k] then return false end
+ end
+ return true
+end
+
+function set.intersect(lhs, rhs)
+ local intersection = {}
+ for key, _ in pairs(lhs) do
+ intersection[key] = rhs[key]
+ end
+ return intersection
+end
+
+-- Returns lhs - rhs
+function set.difference(lhs, rhs)
+ local out = {}
+ for key, _ in pairs(lhs) do
+ if not rhs[key] then
+ out[key] = true
+ end
+ end
+ return out
+end
+
+function set.union(lhs, rhs)
+ local out = {}
+ for key, _ in pairs(lhs) do
+ out[key] = true
+ end
+ for key, _ in pairs(rhs) do
+ out[key] = true
+ end
+ return out
+end
+
+return set
diff --git a/assets/game_scripts/common/timer.lua b/assets/game_scripts/common/timer.lua
new file mode 100644
index 00000000..77ded73f
--- /dev/null
+++ b/assets/game_scripts/common/timer.lua
@@ -0,0 +1,105 @@
+local Timer = {}
+local TimerMT = {__index = Timer}
+
+--[[ Create a timer class that starts at currentTime, if specified, or 0 if not.
+
+The returned `t` must have `t:update(currentTime)` called on it whenever
+`currentTime` changes. This call will trigger any callbacks on timers that have
+expired.
+--]]
+function Timer.new(currentTime)
+ return setmetatable({_currentTime = currentTime or 0, _timers = {}}, TimerMT)
+end
+
+--[[ Start timer `kwargs.name` that will fire in `kwargs.time` seconds.
+
+Once the timer fires, `kwargs.callback` will be called and the timer will be
+removed, unless the callback returns `true`.
+
+Keyword arguments:
+
+* `name` (string) The name of the timer.
+* `time` (number) The number of seconds from now until this timer goes off.
+* `callback` (function) Callback that's called when the timer goes off.
+ If the callback returns `true` then the timer is re-engaged to go off again
+ after the same amount of time. By default, however, the timer is removed.
+
+Example: Print a lovely message in 2 seconds.
+
+```Lua
+local Timer = require 'common.timer'
+
+myTimer = Timer.new()
+myTimer:start{
+ name = 'my love timer',
+ time = 2,
+ callback = function() print('LOVE!') end,
+}
+
+function api:myFunctionCalledEveryFrame(timeSeconds)
+ myTimer:update(timeSeconds)
+end
+
+```
+--]]
+function Timer:start(kwargs)
+ assert(kwargs.name)
+ assert(kwargs.time)
+ assert(kwargs.callback)
+ self._timers[kwargs.name] = {
+ triggerTime = self._currentTime + kwargs.time,
+ time = kwargs.time,
+ callback = kwargs.callback,
+ }
+end
+
+-- Remove `name` from the active timers.
+function Timer:remove(name)
+ self._timers[name] = nil
+end
+
+-- Remove all timers, but maintain the current time.
+function Timer:removeAll()
+ self._timers = {}
+end
+
+-- Returns true if there exists an active timer called `name`.
+function Timer:exists(name)
+ return self._timers[name] ~= nil
+end
+
+-- Returns the time remaining for the timer called `name`.
+-- If no such timer exists (perhaps because the timer has already fired and been
+-- removed), returns 0.
+function Timer:timeRemaining(name)
+ if not self:exists(name) then
+ return 0
+ end
+ return self._timers[name].triggerTime - self._currentTime
+end
+
+-- Provide the new current time. Check if any timers have fired. For timers that
+-- have fired, call their callbacks and remove them unless the callback returns
+-- `true`. Must be called manually by the owner of this class.
+function Timer:update(currentTime)
+ assert(self._currentTime <= currentTime)
+ self._currentTime = currentTime
+
+ local timersToRemove = {}
+ for name, v in pairs(self._timers) do
+ if currentTime >= v.triggerTime then
+ local persist = v.callback()
+ if persist then
+ -- Re-engage the timer. Ensure time is not in the past.
+ v.triggerTime = math.max(currentTime, v.triggerTime + v.time)
+ else
+ timersToRemove[#timersToRemove + 1] = name
+ end
+ end
+ end
+ for _, name in pairs(timersToRemove) do
+ self:remove(name)
+ end
+end
+
+return Timer
diff --git a/assets/game_scripts/common/transform.lua b/assets/game_scripts/common/transform.lua
new file mode 100644
index 00000000..d534dc5c
--- /dev/null
+++ b/assets/game_scripts/common/transform.lua
@@ -0,0 +1,44 @@
+-- Utility functions to construct transformation matrices as tensors.
+
+local sys_transform = require 'dmlab.system.transform'
+
+local transform = {}
+
+-- Returns a 4x4 tensor with the coefficients of a column-major transformation
+-- matrix which applies a translation by vector 'ofs'.
+function transform.translate(ofs)
+ return sys_transform.translate(ofs)
+end
+
+-- Returns a 4x4 tensor with the coefficients of a column-major transformation
+-- matrix which applies a rotation of 'angle' degrees around vector 'axis'.
+function transform.rotate(angle, axis)
+ return sys_transform.rotate(angle, axis)
+end
+
+-- Returns a 4x4 tensor with the coefficients of a column-major transformation
+-- matrix which applies a rotation of 'angle' degrees around the X axis.
+function transform.rotateX(angle)
+ return sys_transform.rotate(angle, {1, 0, 0})
+end
+
+-- Returns a 4x4 tensor with the coefficients of a column-major transformation
+-- matrix which applies a rotation of 'angle' degrees around the Y axis.
+function transform.rotateY(angle)
+ return sys_transform.rotate(angle, {0, 1, 0})
+end
+
+-- Returns a 4x4 tensor with the coefficients of a column-major transformation
+-- matrix which applies a rotation of 'angle' degrees around the Z axis.
+function transform.rotateZ(angle)
+ return sys_transform.rotate(angle, {0, 0, 1})
+end
+
+-- Returns a 4x4 tensor with the coefficients of a column-major transformation
+-- matrix which applies scale factors in vector 'scl' to their corresponding
+-- coordinates.
+function transform.scale(scl)
+ return sys_transform.scale(scl)
+end
+
+return transform
diff --git a/assets/game_scripts/decorators/custom_decals_decoration.lua b/assets/game_scripts/decorators/custom_decals_decoration.lua
new file mode 100644
index 00000000..440673fa
--- /dev/null
+++ b/assets/game_scripts/decorators/custom_decals_decoration.lua
@@ -0,0 +1,45 @@
+local datasets_selector = require 'datasets.selector'
+local texture_sets = require 'themes.texture_sets'
+local tensor = require 'dmlab.system.tensor'
+local decals = require 'themes.decals'
+
+local decorator = {}
+
+local SUBSTITUTION = {}
+
+function decorator.randomize(name, rng)
+ local dataset = decorator._dataset
+ if name ~= decorator._datasetName then
+ dataset = datasets_selector.loadDataset(name)
+ end
+ assert(dataset)
+ decorator._dataset = dataset
+ decorator._datasetName = name
+ local gen = rng:shuffledIndexGenerator(dataset:getSize())
+ for _, k in ipairs(decals.images) do
+ SUBSTITUTION[k] = gen()
+ end
+end
+
+function decorator.decorate(api)
+ local loadTexture = api.loadTexture
+ function api:loadTexture(textureName)
+ local index = SUBSTITUTION[textureName]
+ if index then
+ local result = decorator._dataset:getImage(index)
+ local shape = result:shape()
+ local outTensor = tensor.ByteTensor(shape[1], shape[2], 4)
+ -- Copying by 3 select calls is faster than via a one narrow.
+ outTensor:select(3, 1):copy(result:select(3, 1))
+ outTensor:select(3, 2):copy(result:select(3, 2))
+ outTensor:select(3, 3):copy(result:select(3, 3))
+ outTensor:select(3, 4):fill(255)
+ return outTensor
+ end
+ if loadTexture then
+ return loadTexture(self, textureName)
+ end
+ end
+end
+
+return decorator
diff --git a/assets/game_scripts/decorators/custom_floors.lua b/assets/game_scripts/decorators/custom_floors.lua
new file mode 100644
index 00000000..b94002c3
--- /dev/null
+++ b/assets/game_scripts/decorators/custom_floors.lua
@@ -0,0 +1,51 @@
+--[[ Provide simple re-coloring of floor textures.
+
+Use along with a theme using the CUSTOMIZABLE_FLOORS texture set:
+
+```
+local theme = themes.fromTextureSet{
+ textureSet = texture_sets.CUSTOMIZABLE_FLOORS,
+ randomizeFloorTextures = false,
+}
+```
+
+RGB values floor variations can be specified via setVariationColor() and will
+be automatically applied. Variation 0 represents the default corridor.
+]]
+local decorator = {}
+
+local colorMappings = {}
+
+function decorator.setVariationColor(variation, color)
+ assert(#color == 3, 'Expected RGB color')
+ colorMappings[variation] = color
+end
+
+function decorator.setVariationColors(variationToColor)
+ colorMappings = {}
+ for variation, color in pairs(variationToColor) do
+ decorator.setVariationColor(variation, color)
+ end
+end
+
+function decorator.decorate(api)
+ local modifyTexture = api.modifyTexture
+ function api:modifyTexture(textureName, texture)
+ local res = false
+ if modifyTexture then
+ res = modifyTexture(self, textureName, texture)
+ end
+
+ local variation = textureName:match('lg_floor_placeholder_(.)_d%.tga')
+ local color = variation and colorMappings[variation]
+ if color then
+ texture:select(3, 1):mul(color[1] / 255)
+ texture:select(3, 2):mul(color[2] / 255)
+ texture:select(3, 3):mul(color[3] / 255)
+ return true
+ end
+ return res
+ end
+end
+
+return decorator
diff --git a/assets/game_scripts/decorators/custom_observations.lua b/assets/game_scripts/decorators/custom_observations.lua
index 8069d17d..7f207761 100644
--- a/assets/game_scripts/decorators/custom_observations.lua
+++ b/assets/game_scripts/decorators/custom_observations.lua
@@ -1,10 +1,19 @@
-local tensor = require 'dmlab.system.tensor'
+local debug_observations = require 'decorators.debug_observations'
local game = require 'dmlab.system.game'
-local custom_observations = {}
+local inventory = require 'common.inventory'
+local tensor = require 'dmlab.system.tensor'
+
local obs = {}
local obsSpec = {}
+local instructionObservation = ''
+
+local custom_observations = {}
-function custom_observations.add_spec(name, type, shape, callback)
+custom_observations.playerNames = {''}
+custom_observations.playerInventory = {}
+custom_observations.playerTeams = {}
+
+function custom_observations.addSpec(name, type, shape, callback)
obsSpec[#obsSpec + 1] = {name = name, type = type, shape = shape}
obs[name] = callback
end
@@ -23,14 +32,32 @@ local function angularVelocity()
return tensor.DoubleTensor(game:playerInfo().anglesVel)
end
--- Decorate the api with a player translation velocity and angular velocity
--- observation. These observations are relative to the player.
+local function languageChannel()
+ return instructionObservation or ''
+end
+
+local function teamScore()
+ local info = game:playerInfo()
+ return tensor.DoubleTensor{info.teamScore, info.otherTeamScore}
+end
+
+--[[ Decorate the api to support custom observations:
+
+1. Player translational velocity (VEL.TRANS).
+2. Player angular velocity (VEL.ROT).
+3. Language channel for, e.g. giving instructions to the agent (INSTR).
+4. See debug_observations.lua for those.
+]]
function custom_observations.decorate(api)
local init = api.init
function api:init(params)
- custom_observations.add_spec('VEL.TRANS', 'Doubles', {3}, velocity)
- custom_observations.add_spec('VEL.ROT', 'Doubles', {3}, angularVelocity)
- return init and init(params)
+ custom_observations.addSpec('VEL.TRANS', 'Doubles', {3}, velocity)
+ custom_observations.addSpec('VEL.ROT', 'Doubles', {3}, angularVelocity)
+ custom_observations.addSpec('INSTR', 'String', {0}, languageChannel)
+ custom_observations.addSpec('TEAM.SCORE', 'Doubles', {0}, teamScore)
+ api.setInstruction('')
+ debug_observations.extend(custom_observations)
+ return init and init(api, params)
end
local customObservationSpec = api.customObservationSpec
@@ -42,10 +69,38 @@ function custom_observations.decorate(api)
return specs
end
+ local team = api.team
+ function api:team(playerId, playerName)
+ custom_observations.playerNames[playerId] = playerName
+ local result = team and team(self, playerId, playerName) or 'p'
+ custom_observations.playerTeams[playerId] = result
+ return result
+ end
+
+ local spawnInventory = api.spawnInventory
+ function api:spawnInventory(loadOut)
+ local view = inventory.View(loadOut)
+ custom_observations.playerInventory[view:playerId()] = view
+ return spawnInventory and spawnInventory(self, loadOut)
+ end
+
+ local updateInventory = api.updateInventory
+ function api:updateInventory(loadOut)
+ local view = inventory.View(loadOut)
+ custom_observations.playerInventory[view:playerId()] = view
+ return updateInventory and updateInventory(self, loadOut)
+ end
+
local customObservation = api.customObservation
function api:customObservation(name)
return obs[name] and obs[name]() or customObservation(api, name)
end
+
+ -- Levels can call this to define the language channel observation string
+ -- returned to the agent.
+ function api.setInstruction(text)
+ instructionObservation = text
+ end
end
return custom_observations
diff --git a/assets/game_scripts/decorators/debug_observations.lua b/assets/game_scripts/decorators/debug_observations.lua
new file mode 100644
index 00000000..a16a0959
--- /dev/null
+++ b/assets/game_scripts/decorators/debug_observations.lua
@@ -0,0 +1,209 @@
+local game = require 'dmlab.system.game'
+local tensor = require 'dmlab.system.tensor'
+local game_entities = require 'dmlab.system.game_entities'
+local inventory = require 'common.inventory'
+
+local debug_observations = {}
+local names = {}
+local inventories = {}
+local teams = {}
+
+local function playerPosition()
+ return tensor.DoubleTensor(game:playerInfo().pos)
+end
+
+local function playerOrientation()
+ return tensor.DoubleTensor(game:playerInfo().angles)
+end
+
+local function playerId()
+ return tensor.DoubleTensor{game:playerInfo().playerId}
+end
+
+local function playersId()
+ local playerIds = {}
+ for playerId, name in pairs(names) do
+ playerIds[#playerIds + 1] = playerId
+ end
+ return tensor.DoubleTensor(playerIds)
+end
+
+
+local function playersName()
+ return table.concat(names, '\n')
+end
+
+local function playersHealth()
+ local playerHealth = {}
+ for playerId, inv in pairs(inventories) do
+ playerHealth[#playerHealth + 1] = inv:health()
+ end
+ return tensor.DoubleTensor(playerHealth)
+end
+
+local function playersArmor()
+ local playerArmor = {}
+ for playerId, inv in pairs(inventories) do
+ playerArmor[#playerArmor + 1] = inv:armor()
+ end
+ return tensor.DoubleTensor(playerArmor)
+end
+
+local function playersGadget()
+ local playerGadget = {}
+ for playerId, inv in pairs(inventories) do
+ playerGadget[#playerGadget + 1] = inv:gadget()
+ end
+ return tensor.DoubleTensor(playerGadget)
+end
+
+local function playersGadgetAmount()
+ local playerGadgetAmount = {}
+ for playerId, inv in pairs(inventories) do
+ playerGadgetAmount[#playerGadgetAmount + 1] =
+ inv:gadgetAmount(inv:gadget())
+ end
+ return tensor.DoubleTensor(playerGadgetAmount)
+end
+
+local function playersEyePos()
+ local eyePos = {}
+ for playerId, inv in pairs(inventories) do
+ eyePos[#eyePos + 1] = inv:eyePos()
+ end
+ return tensor.DoubleTensor(eyePos)
+end
+
+local function playersEyeRot()
+ local eyeRot = {}
+ for playerId, inv in pairs(inventories) do
+ eyeRot[#eyeRot + 1] = inv:eyeAngles()
+ end
+ return tensor.DoubleTensor(eyeRot)
+end
+
+local TEAM_LOOKUP = {r = 1, b = 2}
+local function playersTeam()
+ local playerTeam = {}
+ for playerId, team in pairs(teams) do
+ playerTeam[#playerTeam + 1] = TEAM_LOOKUP[team] or 0
+ end
+ return tensor.DoubleTensor(playerTeam)
+end
+
+local function playersHoldingFlag()
+ local playerHoldingFlag = {}
+ for playerId, inv in pairs(inventories) do
+ local holding = 0
+ if inv:hasPowerUp(inventory.POWERUPS.RED_FLAG) then
+ holding = holding + 1
+ end
+ if inv:hasPowerUp(inventory.POWERUPS.BLUE_FLAG) then
+ holding = holding + 2
+ end
+ playerHoldingFlag[#playerHoldingFlag + 1] = holding
+ end
+ return tensor.DoubleTensor(playerHoldingFlag)
+end
+
+local FLAG_STATE = {
+ NONE = 0,
+ HOME = 1,
+ CARRIED = 2,
+ DROPPED = 3,
+}
+
+local function redFlag()
+ for i, ent in ipairs(game_entities:entities{'team_CTF_redflag'}) do
+ if ent.visible then
+ local x, y, z = unpack(ent.position)
+ local player_id = 0
+ local flagState = i == 1 and FLAG_STATE.HOME or FLAG_STATE.DROPPED
+ return tensor.DoubleTensor{x, y, z, player_id, flagState}
+ end
+ end
+ for playerId, inv in pairs(inventories) do
+ if inv:hasPowerUp(inventory.POWERUPS.RED_FLAG) then
+ local x, y, z = unpack(inv:eyePos())
+ return tensor.DoubleTensor{x, y, z, inv:playerId(), FLAG_STATE.CARRIED}
+ end
+ end
+ return tensor.DoubleTensor{0, 0, 0, 0, FLAG_STATE.NONE}
+end
+
+local function blueFlag()
+ for i, ent in ipairs(game_entities:entities{'team_CTF_blueflag'}) do
+ if ent.visible then
+ local x, y, z = unpack(ent.position)
+ local player_id = 0
+ local flagState = i == 1 and FLAG_STATE.HOME or FLAG_STATE.DROPPED
+ return tensor.DoubleTensor{x, y, z, player_id, flagState}
+ end
+ end
+
+ for _, inv in pairs(inventories) do
+ if inv:hasPowerUp(inventory.POWERUPS.BLUE_FLAG) then
+ local x, y, z = unpack(inv:eyePos())
+ return tensor.DoubleTensor{x, y, z, inv:playerId(), FLAG_STATE.CARRIED}
+ end
+ end
+ return tensor.DoubleTensor{0, 0, 0, 0, FLAG_STATE.NONE}
+end
+
+local HOME_FLAG_STATE = {
+ NONE = 0,
+ HOME = 1,
+ AWAY = 2,
+}
+
+local function redFlagHome()
+ for i, ent in ipairs(game_entities:entities{'team_CTF_redflag'}) do
+ local x, y, z = unpack(ent.position)
+ local state = ent.visible and HOME_FLAG_STATE.HOME or HOME_FLAG_STATE.AWAY
+ return tensor.DoubleTensor{x, y, z, state}
+ end
+ return tensor.DoubleTensor{0, 0, 0, FLAG_STATE.NONE}
+end
+
+local function blueFlagHome()
+ for i, ent in ipairs(game_entities:entities{'team_CTF_blueflag'}) do
+ local x, y, z = unpack(ent.position)
+ local state = ent.visible and HOME_FLAG_STATE.HOME or HOME_FLAG_STATE.AWAY
+ return tensor.DoubleTensor{x, y, z, state}
+ end
+ return tensor.DoubleTensor{0, 0, 0, HOME_FLAG_STATE.NONE}
+end
+
+--[[ Extend custom_observations to contain debug observations. ]]
+function debug_observations.extend(custom_observations)
+ local co = custom_observations
+ names = co.playerNames
+ inventories = co.playerInventory
+ teams = co.playerTeams
+
+ co.addSpec('DEBUG.POS.TRANS', 'Doubles', {3}, playerPosition)
+ co.addSpec('DEBUG.POS.ROT', 'Doubles', {3}, playerOrientation)
+ co.addSpec('DEBUG.PLAYER_ID', 'Doubles', {1}, playerId)
+ co.addSpec('DEBUG.PLAYERS.ARMOR', 'Doubles', {0}, playersArmor)
+ co.addSpec('DEBUG.PLAYERS.GADGET', 'Doubles', {0}, playersGadget)
+ co.addSpec('DEBUG.PLAYERS.GADGET_AMOUNT', 'Doubles', {0}, playersGadgetAmount)
+ co.addSpec('DEBUG.PLAYERS.HEALTH', 'Doubles', {0}, playersHealth)
+ co.addSpec('DEBUG.PLAYERS.HOLDING_FLAG', 'Doubles', {0}, playersHoldingFlag)
+ co.addSpec('DEBUG.PLAYERS.ID', 'Doubles', {0}, playersId)
+ co.addSpec('DEBUG.PLAYERS.EYE.POS', 'Doubles', {0, 3}, playersEyePos)
+ co.addSpec('DEBUG.PLAYERS.EYE.ROT', 'Doubles', {0, 3}, playersEyeRot)
+ -- New line separated string.
+ co.addSpec('DEBUG.PLAYERS.NAME', 'String', {1}, playersName)
+ co.addSpec('DEBUG.PLAYERS.TEAM', 'Doubles', {0}, playersTeam)
+
+ -- Flag information (x, y, z, playerId,
+ -- {NONE = 0, HOME = 1, CARRIED = 2, DROPPED = 3})
+ co.addSpec('DEBUG.FLAGS.RED', 'Doubles', {5}, redFlag)
+ co.addSpec('DEBUG.FLAGS.BLUE', 'Doubles', {5}, blueFlag)
+
+ -- Flag information (x, y, z, {NONE = 0, HOME = 1, AWAY = 2})
+ co.addSpec('DEBUG.FLAGS.RED_HOME', 'Doubles', {4}, redFlagHome)
+ co.addSpec('DEBUG.FLAGS.BLUE_HOME', 'Doubles', {4}, blueFlagHome)
+end
+
+return debug_observations
diff --git a/assets/game_scripts/decorators/human_recognisable_pickups.lua b/assets/game_scripts/decorators/human_recognisable_pickups.lua
new file mode 100644
index 00000000..9e3eb839
--- /dev/null
+++ b/assets/game_scripts/decorators/human_recognisable_pickups.lua
@@ -0,0 +1,66 @@
+-- Carries out transformations specified by common.human_recognisable_pickups.
+
+local hrp = require 'common.human_recognisable_pickups'
+local model = require 'dmlab.system.model'
+local transform = require 'common.transform'
+local decorator = {}
+
+function decorator.decorate(api)
+ local replaceModelName = api.replaceModelName
+ function api:replaceModelName(modelName)
+ local replace = replaceModelName and {replaceModelName(self, modelName)}
+ or {nil}
+ if replace[1] == nil then
+ replace = {hrp.replaceModelName(modelName)}
+ end
+ return unpack(replace)
+ end
+
+ local function buildTransform(transformSuffix)
+ -- Matches name{amount}.
+ local name, amount = string.match(transformSuffix, '(.*){(.*)}')
+ assert(name == 'scale', 'Only scale operation supported')
+ amount = tonumber(amount)
+ assert(name ~= nil, 'Amount must be a number')
+ return transform.scale({amount, amount, amount})
+ end
+
+ local createModel = api.createModel
+ function api:createModel(modelName)
+ -- Matches prefix%transformSuffix.
+ local prefix, transformSuffix = string.match(modelName, '(.+)%%(.+)')
+ if transformSuffix ~= nil then
+ local modelNameActual, prefix = self:replaceModelName(prefix)
+ if modelNameActual ~= nil then
+ local modelRaw = model:loadMD3(modelNameActual)
+ -- Custom loaded models must have their textures updated with the
+ -- correct prefix.
+ for _, v in pairs(modelRaw.surfaces) do
+ v.shaderName = prefix .. v.shaderName
+ end
+ return model:hierarchy{
+ transform = buildTransform(transformSuffix),
+ model = modelRaw
+ }
+ end
+ end
+ return createModel and createModel(self, modelName)
+ end
+
+ local replaceTextureName = api.replaceTextureName
+ function api:replaceTextureName(textureName)
+ return replaceTextureName and replaceTextureName(self, textureName) or
+ hrp.replaceTextureName(textureName)
+ end
+
+ local modifyTexture = api.modifyTexture
+ function api:modifyTexture(textureName, texture)
+ local res = false
+ if modifyTexture then
+ res = modifyTexture(self, textureName, texture)
+ end
+ return hrp.modifyTexture(textureName, texture) or res
+ end
+end
+
+return decorator
diff --git a/assets/game_scripts/decorators/setting_overrides.lua b/assets/game_scripts/decorators/setting_overrides.lua
new file mode 100644
index 00000000..9958b500
--- /dev/null
+++ b/assets/game_scripts/decorators/setting_overrides.lua
@@ -0,0 +1,76 @@
+local helpers = require 'common.helpers'
+local timeout = require 'decorators.timeout'
+
+-- These parameters may or may not be specified in the init `params`, depending
+-- on the execution environment. We should ignore them without raising an
+-- 'Invalid setting' error.
+local PARAMS_WHITELIST = {
+ episodeLengthSeconds = true,
+ invocationMode = true,
+ levelGenerator = true,
+ playerId = true,
+ players = true,
+ randomSeed = true,
+ datasetPath = true,
+}
+
+-- Some scripts still pass these in, though they are not used.
+-- Output a warning but not an error.
+local PARAMS_DEPRECATED = {
+ __platform__ = true,
+ textureRandomization = true,
+}
+
+local setting_overrides = {}
+
+--[[ Decorate the api with an init(params) function that overrides values in
+apiParams with the corresponding values in params. This lets command-line and
+Python environments override the apiParams settings.
+
+Keyword arguments:
+
+* `api` the class to decorate. Gets an `init(params)` function added to it.
+* `apiParams` the parameters that hold values overridden by the `params`
+ passed into `init(params)`
+* `decorateWithTimeout` (boolean, default nil) if true, decorate `api` with
+ the timeout decorator, and use the (potentially updated) version of
+ `apiParams.episodeLengthSeconds`.
+]]
+function setting_overrides.decorate(kwargs)
+ local api = kwargs.api
+ local apiParams = kwargs.apiParams
+ local decorateWithTimeout = kwargs.decorateWithTimeout
+ assert(api ~= nil and apiParams ~= nil)
+
+ -- Preserve the existing init call.
+ local apiInit = api.init
+
+ -- Override the init() function to parse known settings.
+ function api:init(params)
+ -- Override known settings.
+ for k, v in pairs(params) do
+ if apiParams[k] ~= nil then
+ apiParams[k] = helpers.fromString(v)
+ elseif PARAMS_DEPRECATED[k] then
+ io.stderr:write('WARNING: "' .. k .. '" (here set to "' .. v .. '")' ..
+ ' has been deprecated.\n')
+ elseif not PARAMS_WHITELIST[k] then
+ error('Invalid setting: "' .. k .. '" = "' .. v .. '"')
+ end
+ end
+
+ -- Decorate with timeout. We do this at the end because episodeLengthSeconds
+ -- may have been overridden.
+ if decorateWithTimeout and apiParams.episodeLengthSeconds then
+ timeout.decorate(api, apiParams.episodeLengthSeconds)
+ end
+
+ -- Call version of `init` that existed before decoration.
+ if apiInit then
+ return apiInit(api, params)
+ end
+ end
+end
+
+return setting_overrides
+
diff --git a/assets/game_scripts/decorators/test_only.lua b/assets/game_scripts/decorators/test_only.lua
new file mode 100644
index 00000000..1f9014dd
--- /dev/null
+++ b/assets/game_scripts/decorators/test_only.lua
@@ -0,0 +1,20 @@
+local test_only = {}
+
+--[[ Decorate the api so that an error is raised if the environment is not
+invoked in an evaluation context (e.g. from the Testbed or a human agent).
+
+This may be used to prevent researchers from accidentally training agents
+against evaluation levels that they are not meant to train against.
+]]
+function test_only.decorate(api)
+ local init = api.init
+ function api:init(params)
+ assert(params.invocationMode == "testbed",
+ "This level must only be used during evaluation. " ..
+ "For training, use the *_train.lua version of the level.")
+ return init and init(api, params)
+ end
+ return api
+end
+
+return test_only
diff --git a/assets/game_scripts/decorators/timeout.lua b/assets/game_scripts/decorators/timeout.lua
index 7ee66483..a4dab19e 100644
--- a/assets/game_scripts/decorators/timeout.lua
+++ b/assets/game_scripts/decorators/timeout.lua
@@ -1,28 +1,27 @@
+local helpers = require 'common.helpers'
local screen_message = require 'common.screen_message'
local timeout = {}
--- Function for displaying a timer in the top right of the screen.
--- 'args' is a table which can be passed through from api:screenMessages(args).
--- 'width' (number) Screen width.
--- 'time_seconds' (number). Rounded up and if greater than 60, then minutes are
--- displayed, too.
--- Returns
--- A table, suitable for use as an entry in the array returned by
--- api:screenMessages
-local function timeDisplay(args, time_seconds)
- local s = math.ceil(time_seconds)
- local time_remaining
- if s < 60 then
- time_remaining = string.format('%.2d', s % 60)
- else
- time_remaining = string.format('%.2d:%.2d', s / 60 % 60, s % 60)
- end
+--[[ Function for displaying a timer in the top right of the screen.
+
+Arguments:
+
+* 'args' (table) Passed through from api:screenMessages(args).
+* 'timeSeconds' (number). Rounded up and if greater than 60, then minutes are
+ displayed, too.
+
+Returns:
+
+* A table, suitable for use as an entry in the array returned by
+ api:screenMessages.
+]]
+local function timeDisplay(args, timeSeconds)
return {
- message = time_remaining,
- x = args.width - screen_message.kBorderSize,
+ message = helpers.secondsToTimeString(timeSeconds),
+ x = args.width - screen_message.BORDER_SIZE,
y = 0,
- alignment = screen_message.kAlignRight
+ alignment = screen_message.ALIGN_RIGHT
}
end
@@ -45,9 +44,9 @@ function timeout.decorate(api, episodeLength)
end
local hasEpisodeFinished = api.hasEpisodeFinished
- function api:hasEpisodeFinished(time_seconds)
- timeRemaining = episodeLength - time_seconds
- return hasEpisodeFinished and hasEpisodeFinished(api, time_seconds) or
+ function api:hasEpisodeFinished(timeSeconds)
+ timeRemaining = episodeLength - timeSeconds
+ return hasEpisodeFinished and hasEpisodeFinished(api, timeSeconds) or
timeRemaining <= 0
end
end
diff --git a/assets/game_scripts/demo_levels/extra_entities.lua b/assets/game_scripts/demo_levels/extra_entities.lua
new file mode 100644
index 00000000..12213b2e
--- /dev/null
+++ b/assets/game_scripts/demo_levels/extra_entities.lua
@@ -0,0 +1,88 @@
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local custom_observations = require 'decorators.custom_observations'
+local game = require 'dmlab.system.game'
+local timeout = require 'decorators.timeout'
+local api = {}
+
+local MAP_ENTITIES = [[
+*********
+* *
+* *
+* *
+* P *
+* *
+* *
+* *
+*********
+]]
+
+function api:init(params)
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = 'empty_room',
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ theme = 'TETRIS'
+ }
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ -- Spawn facing East.
+ spawnVars.angle = '0'
+ spawnVars.randomAngleRange = '0'
+ end
+ -- This will also print the origins of the extra entities.
+ print(spawnVars.origin)
+ io.flush()
+ return spawnVars
+end
+
+function api:extraEntities()
+ -- List of entities to create.
+ local vars = {
+ {
+ classname = 'apple_reward',
+ origin = '550 450 30',
+ },
+ {
+ classname = 'apple_reward',
+ origin = '600 450 30',
+ },
+ {
+ classname = 'apple_reward',
+ origin = '650 450 30',
+ },
+ {
+ classname = 'apple_reward',
+ origin = '700 450 30',
+ }
+ }
+ -- `updateSpawnVars` is not called by default; so call it anyway.
+ local varsResult = {}
+ for i, v in ipairs(vars) do
+ varsResult[#varsResult + 1] = self:updateSpawnVars(v)
+ end
+ return varsResult
+end
+
+function api:createPickup(classname)
+ if classname == 'apple_reward' then
+ return {
+ name = 'Apple',
+ classname = 'apple_reward',
+ model = 'models/apple.md3',
+ quantity = 1,
+ type = pickups.type.REWARD,
+ }
+ end
+end
+
+timeout.decorate(api, 60 * 60)
+custom_observations.decorate(api)
+return api
diff --git a/assets/game_scripts/demo_levels/hrp_demo.lua b/assets/game_scripts/demo_levels/hrp_demo.lua
new file mode 100644
index 00000000..369e77c2
--- /dev/null
+++ b/assets/game_scripts/demo_levels/hrp_demo.lua
@@ -0,0 +1,58 @@
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local random = require 'common.random'
+local hrp = require 'common.human_recognisable_pickups'
+
+local custom_observations = require 'decorators.custom_observations'
+local pickup_decorator = require 'decorators.human_recognisable_pickups'
+
+local api = {}
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ spawnVars.angle = '0'
+ spawnVars.randomAngleRange = '0'
+ elseif spawnVars.classname == 'recognisable_object' then
+ -- If there's no id set, canPickup won't be called.
+ spawnVars.id = '1'
+ end
+ return spawnVars
+end
+
+-- Intercept object creation to override by using code from
+-- common.human_recognizable_pickups.
+function api:createPickup(className)
+ if className == 'recognisable_object' then
+ return hrp.create{
+ shape = "cherries",
+ color1 = {255, 0, 0},
+ color2 = {0, 255, 0},
+ pattern = "chequered",
+ }
+ end
+ return pickups.defaults[className]
+end
+
+function api:canPickup(id)
+ -- Turn off pickups so you can get up close and personal.
+ return false
+end
+
+-- Sets all objects in the map to have the class name 'recognisable_object',
+-- which we'll customise in api:createPickup().
+function api:nextMap()
+ hrp.reset()
+
+ local map = 'PO'
+ return make_map.makeMap{
+ mapName = 'hrpdemo_map',
+ mapEntityLayer = map,
+ useSkybox = true,
+ pickups = {O = 'recognisable_object'}
+ }
+end
+
+custom_observations.decorate(api)
+pickup_decorator.decorate(api)
+
+return api
diff --git a/assets/game_scripts/demo_levels/hrp_gallery.lua b/assets/game_scripts/demo_levels/hrp_gallery.lua
new file mode 100644
index 00000000..854e15b5
--- /dev/null
+++ b/assets/game_scripts/demo_levels/hrp_gallery.lua
@@ -0,0 +1,103 @@
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local random = require 'common.random'
+local hrp = require 'common.human_recognisable_pickups'
+
+local custom_observations = require 'decorators.custom_observations'
+local pickup_decorator = require 'decorators.human_recognisable_pickups'
+
+local timeout = require 'decorators.timeout'
+
+local SCALES = {"small", "medium", "large"}
+local MOVE_TYPE = {pickups.moveType.STATIC, pickups.moveType.BOB}
+
+local api = {}
+
+local function nameToPickupId(name)
+ return tonumber(name:match('^pickup:(%d+)$'))
+end
+
+function api:init(settings)
+ make_map.seedRng(1) -- Use a fixed seed since this is a simple demo level.
+ random:seed(1)
+ self._shapes = hrp.shapes()
+ table.sort(self._shapes)
+ self._patterns = hrp.patterns()
+ table.sort(self._patterns)
+ self._map = self:_makeMap()
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ -- Spawn looking along the row of objects
+ spawnVars.angle = '0'
+ spawnVars.randomAngleRange = '0'
+ else
+ local id = nameToPickupId(spawnVars.classname)
+ spawnVars.id = id and tostring(id) or nil
+ end
+ return spawnVars
+end
+
+function api:createPickup(classname)
+ -- If classname is 'pickup:x', return self._pickups[x]
+ local id = nameToPickupId(classname)
+ return id and self._pickups[id] or pickups.defaults[classname]
+end
+
+function api:canPickup(id)
+ -- Turn off pickups so you can get up close and personal.
+ return false
+end
+
+function api:_makePickup(c)
+ if c == 'O' then
+ -- Make new pickup and classname to reference it.
+ local id = #self._pickups + 1
+ self._pickups[id] = hrp.create{
+ shape = self._shapes[id],
+ color1 = random:color(),
+ color2 = random:color(),
+ pattern = self._patterns[(id - 1) % #self._patterns + 1],
+ scale = SCALES[(id - 1) % #SCALES + 1],
+ moveType = MOVE_TYPE[(id - 1) % #MOVE_TYPE + 1],
+ }
+ return 'pickup:' .. id
+ end
+end
+
+function api:_makeMap()
+ hrp.reset()
+ self._pickups = {}
+
+ -- Repeat 'O' enough time to show each shape once, add spawn on next row.
+ local map = ' ' .. string.rep(' ', #self._shapes) .. ' \n' ..
+ ' ' .. string.rep('O', #self._shapes) .. ' \n' ..
+ 'P' .. string.rep(' ', #self._shapes) .. ' '
+ local var = ' ' .. string.rep(' ', #self._shapes) .. ' \n' ..
+ ' ' .. string.rep('AB', #self._shapes / 2) .. ' \n' ..
+ ' ' .. string.rep(' ', #self._shapes) .. ' '
+
+ return make_map.makeMap{
+ mapName = 'hrpgallery_map',
+ mapEntityLayer = map,
+ mapVariationsLayer = var,
+ useSkybox = true,
+ callback = function (i, j, c, maker)
+ local pickup = self:_makePickup(c)
+ if pickup then
+ return maker:makeEntity{i = i, j = j, classname = pickup}
+ end
+ end,
+ }
+end
+
+custom_observations.decorate(api)
+pickup_decorator.decorate(api)
+timeout.decorate(api, 60 * 60) -- 60 minutes.
+
+return api
diff --git a/assets/game_scripts/demo_levels/map_generation/make_map_from_text.lua b/assets/game_scripts/demo_levels/map_generation/make_map_from_text.lua
new file mode 100644
index 00000000..e055bd60
--- /dev/null
+++ b/assets/game_scripts/demo_levels/map_generation/make_map_from_text.lua
@@ -0,0 +1,62 @@
+-- Demonstration of creating a fixed level described using text.
+
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local texture_sets = require 'themes.texture_sets'
+local api = {}
+
+--[[ Text map contents:
+
+'P' - Player spawn point. Player is spawned with random orientation.
+'A' - Apple pickup. 1 reward point when picked up.
+'G' - Goal object. 10 reward points and the level restarts.
+'I' - Door. Open and closes West-East corridors.
+'H' - Door. Open and closes North-South corridors.
+'*' - Walls.
+
+Lights are placed randomly through out and decals are randomly placed on the
+walls according to the theme.
+]]
+local TEXT_MAP = [[
+**************
+*G * A ***** *
+** * * *
+***** I *
+* * * *
+* ** ***** *
+* * * *
+******H*******
+* I P *
+**************
+]]
+
+-- Called only once at start up. Settings not recognised by DM Lab internal
+-- are forwarded through the params dictionary.
+function api:init(params)
+ -- Seed the map so only one map is created with lights and decals placed in
+ -- the same place each run.
+ make_map.random():seed(1)
+ api._map = make_map.makeMap{
+ mapName = "demo_map_settings",
+ mapEntityLayer = TEXT_MAP,
+ useSkybox = true,
+ textureSet = texture_sets.TETRIS
+ }
+end
+
+-- `make_map` has default pickup types A = apple_reward and G = goal.
+-- This callback is used to create pickups with those names.
+function api:createPickup(classname)
+ return pickups.defaults[classname]
+end
+
+-- On first call we return the name of the map. On subsequent calls we return
+-- an empty string. This informs the engine to only perform a quik map restart
+-- instead.
+function api:nextMap()
+ local mapName = api._map
+ api._map = ''
+ return mapName
+end
+
+return api
diff --git a/assets/game_scripts/demo_levels/map_generation/random_length.lua b/assets/game_scripts/demo_levels/map_generation/random_length.lua
new file mode 100644
index 00000000..1acecdaf
--- /dev/null
+++ b/assets/game_scripts/demo_levels/map_generation/random_length.lua
@@ -0,0 +1,46 @@
+-- Demonstration of using text to create a different level each time the player
+-- reaches a goal object.
+
+local random = require 'common.random'
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local api = {}
+
+-- `make_map` has default pickup types A = apple_reward and G = goal.
+-- This callback is used to create pickups with those names.
+function api:createPickup(className)
+ return pickups.defaults[className]
+end
+
+-- Called at the begining of each episode.
+function api:start(episode, seed)
+ random:seed(seed)
+ -- When converting from text to map the theme variations, wall decorations and
+ -- light positions are chosen randomly.
+ make_map.random():seed(random:mapGenerationSeed())
+end
+
+-- Called each time a map is needed to be generated.
+-- The first map is generated after start is called and the following each time
+-- the goal object is picked up.
+function api:nextMap()
+
+ -- All maps must have at least one spawn point.
+ local map = 'P'
+
+ -- Add between [0, 5] apples.
+ for i = 0, make_map.random():uniformInt(0, 5) do
+ map = map .. ' A'
+ end
+
+ -- Add a door and a goal object.
+ -- When reaching the goal object the map is finished and a new map is
+ -- requested.
+ map = map .. ' I G'
+ return make_map.makeMap{
+ mapName = 'random_length',
+ mapEntityLayer = map
+ }
+end
+
+return api
diff --git a/assets/game_scripts/demo_levels/replace_model.lua b/assets/game_scripts/demo_levels/replace_model.lua
new file mode 100644
index 00000000..4f90f682
--- /dev/null
+++ b/assets/game_scripts/demo_levels/replace_model.lua
@@ -0,0 +1,82 @@
+-- Demonstration of modifying a texture of a model multiple times.
+-- In the level there should apples and lemons of varying colors.
+local tensor = require 'dmlab.system.tensor'
+local pickups = require 'common.pickups'
+local api = {}
+
+function api:start(episode, seed)
+ api._count = 0
+end
+
+function api:nextMap()
+ return 'seekavoid_arena_01'
+end
+
+local COLORS = {
+ {255, 168, 0},
+ {0, 255, 28},
+ {255, 98, 0},
+ {250, 0, 255},
+ {255, 34, 0},
+ {0, 255, 160},
+ {0, 223, 255},
+}
+
+local PREFIX = 'PICKUP_'
+
+function api:createPickup(classname)
+ local obj_orig = pickups.defaults[classname]
+ if obj_orig ~= nil then
+ local obj = {}
+ for k, v in pairs(obj_orig) do
+ obj[k] = v
+ end
+ local color = api._count % #COLORS + 1
+ api._count = api._count + 1
+ obj.classname = obj.classname .. '_' .. color
+ obj.model = PREFIX .. color .. ':' .. obj.model
+ return obj
+ end
+ return obj_orig
+end
+
+function api:replaceModelName(modelName)
+ if modelName:sub(1, #PREFIX) == PREFIX then
+ local prefixTexture, newModelName = modelName:match('(.*:)(.*)')
+ return newModelName, prefixTexture
+ end
+end
+
+function api:replaceTextureName(textureName)
+ -- Remove the texture's prefix. This will load a new copy of the same texture
+ -- ready for modifyTexture.
+ if textureName:sub(1, #PREFIX) == PREFIX then
+ -- Strip prefix and color id.
+ return textureName:match('.*:(.*)')
+ end
+end
+
+function api:modifyTexture(textureName, texture)
+ if textureName:sub(1, #PREFIX) ~= PREFIX then
+ return false
+ end
+ local color = tonumber(textureName:match('(%d+):'))
+ local r, g, b = unpack(COLORS[color])
+ -- Make texture black and white.
+ texture:mul(1 / 3)
+ local red = texture:select(3, 1)
+ local green = texture:select(3, 2)
+ local blue = texture:select(3, 3)
+ red:cadd(green):cadd(blue)
+ green:copy(red)
+ blue:copy(red)
+
+ -- Set texture color.
+ red:mul(r / 255)
+ green:mul(g / 255)
+ blue:mul(b / 255)
+ return true
+end
+
+return api
+
diff --git a/assets/game_scripts/demo_levels/replace_texture.lua b/assets/game_scripts/demo_levels/replace_texture.lua
new file mode 100644
index 00000000..7b62ad35
--- /dev/null
+++ b/assets/game_scripts/demo_levels/replace_texture.lua
@@ -0,0 +1,74 @@
+-- Demonstration of modifying a texture in multiple ways.
+-- In the level there should be a blue picture on the wall.
+
+local game = require 'dmlab.system.game'
+local make_map = require 'common.make_map'
+local tensor = require 'dmlab.system.tensor'
+local api = {}
+
+function api:start(episode, seed)
+ make_map.seedRng(1)
+ api._count = 0
+end
+
+function api:nextMap()
+ if api._map then
+ return api._map
+ end
+ api._map = make_map.makeMap{
+ mapName = 'demo_rectangle',
+ mapEntityLayer = 'P ',
+ useSkybox = true,
+ }
+ return api._map
+end
+
+local TEXTURE_NAME = 'textures/decal/lab_games/dec_img_style02_013'
+function api:replaceTextureName(textureName)
+ if textureName == TEXTURE_NAME then
+ textureName = textureName .. '%01'
+ end
+ return textureName
+end
+
+function api:loadTexture(textureName)
+ if textureName == TEXTURE_NAME .. '%01' then
+ return tensor.ByteTensor(8, 8, 4):fill(255)
+ end
+end
+
+local colourIndex = 0
+local columnIndex = 0
+
+local function changeTexture(textureData)
+ for i = 0, 2 do
+ local c = (math.floor(colourIndex) == i) and 255 or 0
+ textureData:select(3, i + 1):fill(c)
+ end
+ textureData:select(2, math.floor(columnIndex) + 1):fill(255)
+ colourIndex = (colourIndex + 0.01) % 3
+ columnIndex = (columnIndex + 0.01) % 8
+end
+
+function api:modifyTexture(textureName, tensorData)
+ if textureName == TEXTURE_NAME then
+ assert(tensorData == tensor.ByteTensor(8, 8, 4):fill(255))
+ -- Make texture red.
+ changeTexture(tensorData)
+ return true
+ end
+ return false
+end
+
+local textureData = tensor.ByteTensor(8, 8, 4):fill(255)
+
+function api:modifyControl(actions)
+ if actions.crouchJump > 0 then
+ actions.crouchJump = 0
+ changeTexture(textureData)
+ game:updateTexture(TEXTURE_NAME, textureData)
+ end
+ return actions
+end
+
+return api
diff --git a/assets/game_scripts/demo_levels/screen_decoration/rectangles.lua b/assets/game_scripts/demo_levels/screen_decoration/rectangles.lua
new file mode 100644
index 00000000..0a6a08d8
--- /dev/null
+++ b/assets/game_scripts/demo_levels/screen_decoration/rectangles.lua
@@ -0,0 +1,52 @@
+-- Demonstration of rendering rectangles in screen space.
+local make_map = require 'common.make_map'
+
+local api = {}
+
+function api:nextMap()
+ return make_map.makeMap{mapName = 'rectangles', mapEntityLayer = " P "}
+end
+
+--[[ Renders rectangles to the screen.
+
+Coordinate system is 0,0 Top Left. Width and height is always 640, 480.
+
+Keyword Args
+
+ * width(640) - Ideal screen width textures are streched to match this.
+ * height(480) - Ideal screen height textures are streched to match this.
+
+Returns list of rectangles to be rendered.
+]]
+
+function api:filledRectangles(args)
+ local rectangles = {
+ -- Green rectangle in top right 8th of the screen.
+ {
+ x = args.width * 0.75,
+ y = 0,
+ width = args.width * 0.25,
+ height = args.height * 0.25,
+ rgba = {0.0, 1.0, 0.0, 1.0}
+ },
+ -- Blue semi-transparent top left 8th of the screen.
+ {
+ x = 0,
+ y = 0,
+ width = args.width * 0.25,
+ height = args.height * 0.25,
+ rgba = {0.0, 0.0, 1.0, 0.5}
+ },
+ -- Red semi-transparent center 8th of the screen.
+ {
+ x = args.width * 0.375,
+ y = args.height * 0.375,
+ width = args.width * 0.25,
+ height = args.height * 0.25,
+ rgba = {1.0, 0.0, 0.0, 0.5}
+ },
+ }
+ return rectangles
+end
+
+return api
diff --git a/assets/game_scripts/demo_levels/screen_decoration/text.lua b/assets/game_scripts/demo_levels/screen_decoration/text.lua
new file mode 100644
index 00000000..b1800fc6
--- /dev/null
+++ b/assets/game_scripts/demo_levels/screen_decoration/text.lua
@@ -0,0 +1,79 @@
+-- Demonstration of rendering rectangles in screen space.
+local make_map = require 'common.make_map'
+local screen_message = require 'common.screen_message'
+
+local api = {}
+
+function api:nextMap()
+ return make_map.makeMap{mapName = 'text', mapEntityLayer = " P "}
+end
+
+--[[ Renders fixed width strings to the screen.
+
+Coordinate system is 0,0 Top Left. Width and height is always 640, 480.
+
+Keyword Args
+
+ * max_string_length(79) Strings will be truncated to this length.
+ * line_height(20) - Multiple are best separated by this distance.
+ * width(640) - Ideal screen width textures are streched to match this.
+ * height(480) - Ideal screen height textures are streched to match this.
+
+Returns text to be rendered.
+]]
+function api:screenMessages(args)
+ return {
+ -- Right text with shadow.
+ {
+ x = screen_message.BORDER_SIZE,
+ y = args.height * 0.5 - args.line_height * 0.5,
+ message = 'Right Text',
+ alignment = screen_message.ALIGN_RIGHT,
+ rgba = {1, 1, 1, 1},
+ },
+ -- Center text custom drop shadow.
+ {
+ x = args.width * 0.5 + 1,
+ y = args.height * 0.5 - args.line_height * 0.5 + 1,
+ message = 'Center Text',
+ alignment = screen_message.ALIGN_CENTER,
+ rgba = {0.5, 0, 0, 1},
+ shadow = false,
+ },
+ -- Center text white
+ {
+ x = args.width * 0.5 - 1,
+ y = args.height * 0.5 - args.line_height * 0.5 - 1,
+ message = 'Center Text',
+ alignment = screen_message.ALIGN_CENTER,
+ rgba = {1, 1, 1, 1},
+ shadow = false,
+ },
+ -- Right text with shadow.
+ {
+ x = args.width - screen_message.BORDER_SIZE,
+ y = args.height * 0.5 - args.line_height * 0.5,
+ message = 'Right Text',
+ alignment = screen_message.ALIGN_RIGHT,
+ rgba = {1, 1, 1, 1},
+ },
+ -- Multiline text
+ {
+ x = args.width * 0.5 - 1,
+ y = args.BORDER_SIZE,
+ message = 'Multiline Line 1',
+ alignment = screen_message.ALIGN_LEFT,
+ rgba = {0.5, 1, 0.5, 1},
+ },
+ {
+ x = args.width * 0.5 - 1,
+ y = screen_message.BORDER_SIZE + args.line_height,
+ message = 'Multiline Line 2',
+ alignment = screen_message.ALIGN_LEFT,
+ rgba = {0.5, 1, 0.5, 1},
+ },
+ }
+end
+
+return api
+
diff --git a/assets/game_scripts/demo_levels/set_instruction.lua b/assets/game_scripts/demo_levels/set_instruction.lua
new file mode 100644
index 00000000..bf4a5a82
--- /dev/null
+++ b/assets/game_scripts/demo_levels/set_instruction.lua
@@ -0,0 +1,42 @@
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local custom_observations = require 'decorators.custom_observations'
+
+local api = {}
+
+function api:start(episode, seed)
+ make_map.seedRng(0) -- Use a fixed seed since this is a simple demo level.
+ api._count = 0
+end
+
+function api:createPickup(className)
+ return pickups.defaults[className]
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ -- Spawn facing the apples.
+ spawnVars.angle = '0'
+ spawnVars.randomAngleRange = '0'
+ end
+ return spawnVars
+end
+
+function api:nextMap()
+ api._count = api._count + 1
+ local map = 'P'
+ for i = 1, api._count do
+ map = map .. 'A'
+ end
+ map = map .. 'G'
+ local str = api._count == 1 and ' apple' or ' apples'
+ api.setInstruction(api._count .. str)
+ return make_map.makeMap{
+ mapName = 'instruction_map',
+ mapEntityLayer = map
+ }
+end
+
+custom_observations.decorate(api)
+
+return api
diff --git a/assets/game_scripts/factories/lt_factory.lua b/assets/game_scripts/factories/lt_factory.lua
index 034c04de..6a924000 100644
--- a/assets/game_scripts/factories/lt_factory.lua
+++ b/assets/game_scripts/factories/lt_factory.lua
@@ -1,126 +1,68 @@
-local random = require 'common.random'
+local color_bots = require 'common.color_bots'
+local colors = require 'common.colors'
local custom_observations = require 'decorators.custom_observations'
-local timeout = require 'decorators.timeout'
-
-local BOT_NAMES_COLOR = {
- 'CygniColor',
- 'LeonisColor',
- 'EpsilonColor',
- 'CepheiColor',
- 'CentauriColor',
- 'DraconisColor'
-}
-
-local BOT_NAMES = {
- 'Cygni',
- 'Leonis',
- 'Epsilon',
- 'Cephei',
- 'Centauri',
- 'Draconis'
-}
-
---[[ Converts an HSV color value to RGB. Conversion formula adapted from
-http://en.wikipedia.org/wiki/HSV_color_space. Assumes h, s and v are contained
-in the set [0, 1]. Returns r, g, b each in the set [0, 255].
-]]
-local function hsvToRgb(h, s, v)
- local r, g, b
-
- local i = math.floor(h * 6);
- local f = h * 6 - i;
- local p = v * (1 - s);
- local q = v * (1 - f * s);
- local t = v * (1 - (1 - f) * s);
-
- i = i % 6
-
- if i == 0 then r, g, b = v, t, p
- elseif i == 1 then r, g, b = q, v, p
- elseif i == 2 then r, g, b = p, v, t
- elseif i == 3 then r, g, b = p, q, v
- elseif i == 4 then r, g, b = t, p, v
- elseif i == 5 then r, g, b = v, p, q
- end
-
- return r * 255, g * 255, b * 255
-end
-
-local SATURATION = 1.0
-local VALUE = 1.0
+local random = require 'common.random'
+local setting_overrides = require 'decorators.setting_overrides'
local factory = {}
--[[ Creates a Laser tag API.
+
Keyword arguments:
* `mapName` (string) - Name of map to load.
-* `botCount` (number, [-1, 6], default 4) - Number of bots. (-1 for all).
+* `botCount` (number, [-1, 6]) - Number of bots. (-1 for all).
* `skill` (number, [1.0, 5.0], default 4.0) - Skill level of bot.
* `episodeLengthSeconds` (number, default 600) - Episode length in seconds.
* `color` (boolean, default false) - Change color of bots each episode.
]]
function factory.createLevelApi(kwargs)
- assert(kwargs.mapName)
- kwargs.botCount = kwargs.botCount or 4
+ assert(kwargs.botCount, "must supply botCount")
kwargs.skill = kwargs.skill or 4.0
kwargs.episodeLengthSeconds = kwargs.episodeLengthSeconds or 600
kwargs.color = kwargs.color or false
- assert(kwargs.botCount <= (kwargs.color and #BOT_NAMES_COLOR or #BOT_NAMES))
+ assert(kwargs.botCount <= #color_bots.BOT_NAMES)
local api = {}
function api:nextMap()
- return kwargs.mapName
+ local map = kwargs.mapName
+ kwargs.mapName = ''
+ return map
end
function api:start(episode, seed, params)
- random.seed(seed)
+ random:seed(seed)
if kwargs.color then
-- Pick a random angle.
- api.bot_hue_degrees_ = random.uniformInt(0, 359)
+ api._botHueDegrees = random:uniformInt(0, 359)
end
end
if kwargs.color then
- function api:modifyTexture(name, tensor)
- if string.match(name, 'players/crash_color/dm_character_skin_mask') then
-
- local hue = api.bot_hue_degrees_
-
- -- Based on the mask name, determine the hue using a triad distribution.
- if string.sub(name, -string.len('mask_a.tga')) == 'mask_a.tga' then
- hue = hue / 360.0
- elseif string.sub(name, -string.len('mask_b.tga')) == 'mask_b.tga' then
- hue = ((hue + 120) % 360) / 360.0
- elseif string.sub(name, -string.len('mask_c.tga')) == 'mask_c.tga' then
- hue = ((hue + 240) % 360) / 360.0
- else
- logging.raiseError('Unrecognised mask: ' .. name)
- return
- end
+ function api:modifyTexture(name, skin)
+ color_bots:findSkin(name, skin)
+ return false
+ end
- local r, g, b = hsvToRgb(hue, SATURATION, VALUE)
- tensor:select(3, 1):fill(r)
- tensor:select(3, 2):fill(g)
- tensor:select(3, 3):fill(b)
- end
+ function api:mapLoaded()
+ color_bots:colorizeBots(api._botHueDegrees)
end
end
-
function api:addBots()
- local bots = {}
- for i, name in ipairs(kwargs.color and BOT_NAMES_COLOR or BOT_NAMES) do
- if i == kwargs.botCount + 1 then
- break
- end
- bots[#bots + 1] = {name = name, skill = kwargs.skill}
- end
- return bots
+ return color_bots:makeBots{
+ count = kwargs.botCount,
+ color = kwargs.color,
+ skill = kwargs.skill,
+ }
end
custom_observations.decorate(api)
- timeout.decorate(api, kwargs.episodeLengthSeconds)
+ setting_overrides.decorate{
+ api = api,
+ apiParams = kwargs,
+ decorateWithTimeout = true
+ }
return api
end
diff --git a/assets/game_scripts/factories/random_goal_factory.lua b/assets/game_scripts/factories/random_goal_factory.lua
index 67890480..4e9c417a 100644
--- a/assets/game_scripts/factories/random_goal_factory.lua
+++ b/assets/game_scripts/factories/random_goal_factory.lua
@@ -1,10 +1,13 @@
-local maze_gen = require 'dmlab.system.maze_generation'
local game = require 'dmlab.system.game'
-local random = require 'common.random'
-local pickups = require 'common.pickups'
+local map_maker = require 'dmlab.system.map_maker'
+local maze_generation = require 'dmlab.system.maze_generation'
local helpers = require 'common.helpers'
+local pickups = require 'common.pickups'
local custom_observations = require 'decorators.custom_observations'
local timeout = require 'decorators.timeout'
+local random = require 'common.random'
+local map_maker = require 'dmlab.system.map_maker'
+local randomMap = random(map_maker:randomGen())
local factory = {}
@@ -20,27 +23,27 @@ Keyword arguments:
function factory.createLevelApi(kwargs)
kwargs.scatteredRewardDensity = kwargs.scatteredRewardDensity or 0.1
kwargs.episodeLengthSeconds = kwargs.episodeLengthSeconds or 600
- local maze = maze_gen.MazeGeneration{entity = kwargs.entityLayer}
+ local maze = maze_generation.mazeGeneration{entity = kwargs.entityLayer}
local api = {}
- function api:createPickup(class_name)
- return pickups.defaults[class_name]
+ function api:createPickup(class)
+ return pickups.defaults[class]
end
function api:start(episode, seed, params)
- api._time_remaining = kwargs.episodeLengthSeconds
- random.seed(seed)
+ random:seed(seed)
+ randomMap:seed(seed)
+ api._timeRemaining = kwargs.episodeLengthSeconds
local height, width = maze:size()
height = (height - 1) / 2
width = (width - 1) / 2
- api._goal = {random.uniformInt(1, height) * 2,
- random.uniformInt(1, width) * 2}
+ api._goal = {random:uniformInt(1, height) * 2,
+ random:uniformInt(1, width) * 2}
- local goal_location
- local all_spawn_locations = {}
- local fruit_locations = {}
- local fruit_locations_reverse = {}
+ local goalLocation
+ local allSpawnLocations = {}
+ local fruitLocations = {}
maze:visitFill{cell = api._goal, func = function(row, col, distance)
if row % 2 == 1 or col % 2 == 1 then
return
@@ -49,29 +52,22 @@ function factory.createLevelApi(kwargs)
col = col / 2 - 1
-- Axis is flipped in DeepMind Lab.
row = height - row - 1
- local key = ''.. (col * 100 + 50) .. ' ' .. (row * 100 + 50) .. ' '
+ local key = '' .. (col * 100 + 50) .. ' ' .. (row * 100 + 50) .. ' '
if distance == 0 then
- goal_location = key .. '20'
+ goalLocation = key .. '20'
end
if distance > 0 then
- fruit_locations[#fruit_locations + 1] = key .. '20'
+ fruitLocations[#fruitLocations + 1] = key .. '20'
end
if distance > 8 then
- all_spawn_locations[#all_spawn_locations + 1] = key .. '30'
+ allSpawnLocations[#allSpawnLocations + 1] = key .. '30'
end
end}
- helpers.shuffleInPlace(fruit_locations)
- api._goal_location = goal_location
- api._fruit_locations = fruit_locations
- api._all_spawn_locations = all_spawn_locations
- end
-
- function api:pickup(spawn_id)
- api._count = api._count + 1
- if api._count == api._finish_count then
- game:finishMap()
- end
+ random:shuffleInPlace(fruitLocations)
+ api._goalLocation = goalLocation
+ api._fruitLocations = fruitLocations
+ api._allSpawnLocations = allSpawnLocations
end
function api:updateSpawnVars(spawnVars)
@@ -84,39 +80,41 @@ function factory.createLevelApi(kwargs)
return spawnVars
end
- function api:hasEpisodeFinished(time_seconds)
- api._time_remaining = kwargs.episodeLengthSeconds - time_seconds
- return api._time_remaining <= 0
+ function api:hasEpisodeFinished(timeSeconds)
+ api._timeRemaining = kwargs.episodeLengthSeconds - timeSeconds
+ return api._timeRemaining <= 0
end
function api:nextMap()
api._newSpawnVars = {}
local maxFruit = math.floor(kwargs.scatteredRewardDensity *
- #api._fruit_locations + 0.5)
- for i, fruit_location in ipairs(api._fruit_locations) do
+ #api._fruitLocations + 0.5)
+ for i, fruitLocation in ipairs(api._fruitLocations) do
if i > maxFruit then
break
end
- api._newSpawnVars[fruit_location] = {
+ api._newSpawnVars[fruitLocation] = {
classname = 'apple_reward',
- origin = fruit_location
+ origin = fruitLocation
}
end
- local spawn_location = api._all_spawn_locations[
- random.uniformInt(1, #api._all_spawn_locations)]
+ local spawnLocation = api._allSpawnLocations[
+ random:uniformInt(1, #api._allSpawnLocations)]
api._newSpawnVarsPlayerStart = {
classname = 'info_player_start',
- origin = spawn_location
+ origin = spawnLocation
}
- api._newSpawnVars[api._goal_location] = {
+ api._newSpawnVars[api._goalLocation] = {
classname = 'goal',
- origin = api._goal_location
+ origin = api._goalLocation
}
-
- return kwargs.mapName
+ -- Fast map restarts.
+ local map = kwargs.mapName
+ kwargs.mapName = ''
+ return map
end
custom_observations.decorate(api)
diff --git a/assets/game_scripts/factories/seek_avoid_factory.lua b/assets/game_scripts/factories/seek_avoid_factory.lua
index bcb83775..3287aa41 100644
--- a/assets/game_scripts/factories/seek_avoid_factory.lua
+++ b/assets/game_scripts/factories/seek_avoid_factory.lua
@@ -1,9 +1,11 @@
local game = require 'dmlab.system.game'
-local random = require 'common.random'
local pickups = require 'common.pickups'
local helpers = require 'common.helpers'
local custom_observations = require 'decorators.custom_observations'
local timeout = require 'decorators.timeout'
+local random = require 'common.random'
+local map_maker = require 'dmlab.system.map_maker'
+local randomMap = random(map_maker:randomGen())
local factory = {}
@@ -18,12 +20,13 @@ function factory.createLevelApi(kwargs)
local api = {}
- function api:createPickup(class_name)
- return pickups.defaults[class_name]
+ function api:createPickup(classname)
+ return pickups.defaults[classname]
end
function api:start(episode, seed, params)
- random.seed(seed)
+ random:seed(seed)
+ randomMap:seed(random:mapGenerationSeed())
api._has_goal = false
api._count = 0
api._finish_count = 0
@@ -42,16 +45,16 @@ function factory.createLevelApi(kwargs)
local possibleClassNames = helpers.split(spawnVars.random_items, ',')
if #possibleClassNames > 0 then
classname = possibleClassNames[
- random.uniformInt(1, #possibleClassNames)]
+ random:uniformInt(1, #possibleClassNames)]
end
end
local pickup = pickups.defaults[spawnVars.classname]
if pickup then
- if pickup.type == pickups.type.kReward and pickup.quantity > 0 then
+ if pickup.type == pickups.type.REWARD and pickup.quantity > 0 then
api._finish_count = api._finish_count + 1
spawnVars.id = tostring(api._finish_count)
end
- if pickup.type == pickups.type.kGoal then
+ if pickup.type == pickups.type.GOAL then
api._has_goal = true
end
end
@@ -60,7 +63,10 @@ function factory.createLevelApi(kwargs)
end
function api:nextMap()
- return kwargs.mapName
+ -- Fast map restarts.
+ local map = kwargs.mapName
+ kwargs.mapName = ''
+ return map
end
custom_observations.decorate(api)
diff --git a/assets/game_scripts/lt_chasm.lua b/assets/game_scripts/lt_chasm.lua
index 9b74511a..92cb605a 100644
--- a/assets/game_scripts/lt_chasm.lua
+++ b/assets/game_scripts/lt_chasm.lua
@@ -1,3 +1,6 @@
local factory = require 'factories.lt_factory'
-return factory.createLevelApi{mapName = 'lt_chasm'}
+return factory.createLevelApi{
+ mapName = 'lt_chasm',
+ botCount = 4,
+}
diff --git a/assets/game_scripts/lt_hallway_slope.lua b/assets/game_scripts/lt_hallway_slope.lua
index 0f9b0cc1..186a4524 100644
--- a/assets/game_scripts/lt_hallway_slope.lua
+++ b/assets/game_scripts/lt_hallway_slope.lua
@@ -1,3 +1,6 @@
local factory = require 'factories.lt_factory'
-return factory.createLevelApi{mapName = 'lt_hallway_slope'}
+return factory.createLevelApi{
+ mapName = 'lt_hallway_slope',
+ botCount = 4,
+}
diff --git a/assets/game_scripts/lt_horseshoe_color.lua b/assets/game_scripts/lt_horseshoe_color.lua
index 04166d06..1c541940 100644
--- a/assets/game_scripts/lt_horseshoe_color.lua
+++ b/assets/game_scripts/lt_horseshoe_color.lua
@@ -1,3 +1,7 @@
local factory = require 'factories.lt_factory'
-return factory.createLevelApi{mapName = 'lt_horseshoe_color', color = true}
+return factory.createLevelApi{
+ mapName = 'lt_horseshoe_color',
+ color = true,
+ botCount = 4,
+}
diff --git a/assets/game_scripts/lt_space_bounce_hard.lua b/assets/game_scripts/lt_space_bounce_hard.lua
index 00302c92..9fa7d3a6 100644
--- a/assets/game_scripts/lt_space_bounce_hard.lua
+++ b/assets/game_scripts/lt_space_bounce_hard.lua
@@ -3,5 +3,6 @@ local factory = require 'factories.lt_factory'
return factory.createLevelApi{
mapName = 'lt_space_bounce_01',
skill = 5,
- color = true
+ color = true,
+ botCount = 4,
}
diff --git a/assets/game_scripts/patterns/chequered_d.png b/assets/game_scripts/patterns/chequered_d.png
new file mode 100644
index 00000000..50c6fec2
Binary files /dev/null and b/assets/game_scripts/patterns/chequered_d.png differ
diff --git a/assets/game_scripts/patterns/crosses_d.png b/assets/game_scripts/patterns/crosses_d.png
new file mode 100644
index 00000000..9990a865
Binary files /dev/null and b/assets/game_scripts/patterns/crosses_d.png differ
diff --git a/assets/game_scripts/patterns/diagonal_stripe_d.png b/assets/game_scripts/patterns/diagonal_stripe_d.png
new file mode 100644
index 00000000..f86b4282
Binary files /dev/null and b/assets/game_scripts/patterns/diagonal_stripe_d.png differ
diff --git a/assets/game_scripts/patterns/discs_d.png b/assets/game_scripts/patterns/discs_d.png
new file mode 100644
index 00000000..da26fad2
Binary files /dev/null and b/assets/game_scripts/patterns/discs_d.png differ
diff --git a/assets/game_scripts/patterns/hex_d.png b/assets/game_scripts/patterns/hex_d.png
new file mode 100644
index 00000000..d5162fd7
Binary files /dev/null and b/assets/game_scripts/patterns/hex_d.png differ
diff --git a/assets/game_scripts/patterns/pinstripe_d.png b/assets/game_scripts/patterns/pinstripe_d.png
new file mode 100644
index 00000000..d74630d8
Binary files /dev/null and b/assets/game_scripts/patterns/pinstripe_d.png differ
diff --git a/assets/game_scripts/patterns/spots_d.png b/assets/game_scripts/patterns/spots_d.png
new file mode 100644
index 00000000..df3d77d8
Binary files /dev/null and b/assets/game_scripts/patterns/spots_d.png differ
diff --git a/assets/game_scripts/patterns/swirls_d.png b/assets/game_scripts/patterns/swirls_d.png
new file mode 100644
index 00000000..ba30f8a0
Binary files /dev/null and b/assets/game_scripts/patterns/swirls_d.png differ
diff --git a/assets/game_scripts/player/dm_character_skin_mask_a.png b/assets/game_scripts/player/dm_character_skin_mask_a.png
new file mode 100644
index 00000000..2c21b88a
Binary files /dev/null and b/assets/game_scripts/player/dm_character_skin_mask_a.png differ
diff --git a/assets/game_scripts/player/dm_character_skin_mask_b.png b/assets/game_scripts/player/dm_character_skin_mask_b.png
new file mode 100644
index 00000000..1193a2ce
Binary files /dev/null and b/assets/game_scripts/player/dm_character_skin_mask_b.png differ
diff --git a/assets/game_scripts/player/dm_character_skin_mask_c.png b/assets/game_scripts/player/dm_character_skin_mask_c.png
new file mode 100644
index 00000000..ec8541c2
Binary files /dev/null and b/assets/game_scripts/player/dm_character_skin_mask_c.png differ
diff --git a/assets/game_scripts/random_maze.lua b/assets/game_scripts/random_maze.lua
index e464c787..8b4a9f5b 100644
--- a/assets/game_scripts/random_maze.lua
+++ b/assets/game_scripts/random_maze.lua
@@ -1,4 +1,4 @@
-local maze_gen = require 'dmlab.system.maze_generation'
+local maze_generation = require 'dmlab.system.maze_generation'
local tensor = require 'dmlab.system.tensor'
local random = require 'common.random'
local make_map = require 'common.make_map'
@@ -13,11 +13,11 @@ local api = {}
local function getRandomEvenCoodinate(rows, cols)
-- Shape must be bigger than 3 and odd.
- assert (rows > 2 and rows % 2 == 1)
- assert (cols > 2 and cols % 2 == 1)
+ assert(rows > 2 and rows % 2 == 1)
+ assert(cols > 2 and cols % 2 == 1)
return {
- random.uniformInt(1, math.floor(rows / 2)) * 2,
- random.uniformInt(1, math.floor(cols / 2)) * 2
+ random:uniformInt(1, math.floor(rows / 2)) * 2,
+ random:uniformInt(1, math.floor(cols / 2)) * 2
}
end
@@ -49,7 +49,7 @@ local function generateTensorMaze(rows, cols)
maze(r, c):val(1)
local vistableCells = findVisitableCells(r, c, maze)
if #vistableCells > 0 then
- local choice = vistableCells[random.uniformInt(1, #vistableCells)]
+ local choice = vistableCells[random:uniformInt(1, #vistableCells)]
maze(unpack(choice[2])):val(1)
stack[#stack + 1] = choice[1]
else
@@ -59,24 +59,20 @@ local function generateTensorMaze(rows, cols)
return maze
end
-function api:commandLine(oldCommandLine)
- return make_map.commandLine(oldCommandLine)
-end
-
-function api:createPickup(className)
- return pickups.defaults[className]
+function api:createPickup(classname)
+ return pickups.defaults[classname]
end
function api:start(episode, seed, params)
- random.seed(seed)
+ random:seed(seed)
local rows, cols = 15, 15
local mazeT = generateTensorMaze(rows, cols)
- local maze = maze_gen.MazeGeneration{height = rows, width = cols}
+ local maze = maze_generation.mazeGeneration{height = rows, width = cols}
local variations = {'.', 'A', 'B', 'C'}
mazeT:applyIndexed(function(val, index)
local row, col = unpack(index)
- if 1 < row and row < rows and 1 < col and row < rows and
- random.uniformReal(0, 1) < 0.15 then
+ if 1 < row and row < rows and 1 < col and col < cols and
+ random:uniformReal(0, 1) < 0.15 then
maze:setEntityCell(row, col, ' ')
else
maze:setEntityCell(row, col, val == 0 and '*' or ' ')
@@ -109,8 +105,11 @@ function api:start(episode, seed, params)
print(maze:entityLayer())
io.flush()
- api._maze_name = make_map.makeMap('map_' .. episode .. '_' .. seed,
- maze:entityLayer(), maze:variationsLayer())
+ api._maze_name = make_map.makeMap{
+ mapName = 'map_random_maze',
+ mapEntityLayer = maze:entityLayer(),
+ mapVariationsLayer = maze:variationsLayer()
+ }
end
function api:nextMap()
diff --git a/assets/game_scripts/test_levels/callbacks_test.lua b/assets/game_scripts/test_levels/callbacks_test.lua
new file mode 100644
index 00000000..b36db60c
--- /dev/null
+++ b/assets/game_scripts/test_levels/callbacks_test.lua
@@ -0,0 +1,98 @@
+-- Tested in deepmind/engine/callbacks_test.cc.
+local tensor = require 'dmlab.system.tensor'
+local api = {}
+
+api._count = 0
+
+api._observations = {
+ LOCATION = tensor.Tensor{10, 20, 30},
+ ORDER = tensor.ByteTensor(),
+ EPISODE = tensor.Tensor{0},
+}
+
+local models = {
+ cube = {
+ surfaces = {
+ cube_surface = {
+ vertices = tensor.FloatTensor{
+ { -0.5, -0.5, -0.5, 0.0, 0.0, -1.0, 0.0, 0.0 },
+ { 0.5, -0.5, -0.5, 0.0, 0.0, -1.0, 1.0, 0.0 },
+ { 0.5, 0.5, -0.5, 0.0, 0.0, -1.0, 1.0, 1.0 },
+ { -0.5, 0.5, -0.5, 0.0, 0.0, -1.0, 0.0, 1.0 },
+ { 0.5, -0.5, -0.5, 1.0, 0.0, 0.0, 0.0, 0.0 },
+ { 0.5, -0.5, 0.5, 1.0, 0.0, 0.0, 1.0, 0.0 },
+ { 0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0, 1.0 },
+ { 0.5, 0.5, -0.5, 1.0, 0.0, 0.0, 0.0, 1.0 },
+ { 0.5, -0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.0 },
+ { -0.5, -0.5, 0.5, 0.0, 0.0, 1.0, 1.0, 0.0 },
+ { -0.5, 0.5, 0.5, 0.0, 0.0, 1.0, 1.0, 1.0 },
+ { 0.5, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 1.0 },
+ { -0.5, -0.5, 0.5, -1.0, 0.0, 0.0, 0.0, 0.0 },
+ { -0.5, -0.5, -0.5, -1.0, 0.0, 0.0, 1.0, 0.0 },
+ { -0.5, 0.5, -0.5, -1.0, 0.0, 0.0, 1.0, 1.0 },
+ { -0.5, 0.5, 0.5, -1.0, 0.0, 0.0, 0.0, 1.0 },
+ { -0.5, 0.5, -0.5, 0.0, 1.0, 0.0, 0.0, 0.0 },
+ { 0.5, 0.5, -0.5, 0.0, 1.0, 0.0, 1.0, 0.0 },
+ { 0.5, 0.5, 0.5, 0.0, 1.0, 0.0, 1.0, 1.0 },
+ { -0.5, 0.5, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0 },
+ { 0.5, -0.5, -0.5, 0.0, -1.0, 0.0, 0.0, 0.0 },
+ { -0.5, -0.5, -0.5, 0.0, -1.0, 0.0, 1.0, 0.0 },
+ { -0.5, -0.5, 0.5, 0.0, -1.0, 0.0, 1.0, 1.0 },
+ { 0.5, -0.5, 0.5, 0.0, -1.0, 0.0, 0.0, 1.0 }
+ },
+ indices = tensor.Int32Tensor{
+ { 1, 2, 3 },
+ { 1, 3, 4 },
+ { 5, 6, 7 },
+ { 5, 7, 8 },
+ { 9, 10, 11 },
+ { 9, 11, 12 },
+ { 13, 14, 15 },
+ { 13, 15, 16 },
+ { 17, 18, 19 },
+ { 17, 19, 20 },
+ { 21, 22, 23 },
+ { 21, 23, 24 }
+ },
+ shaderName = 'textures/model/beam'
+ }
+ }
+ }
+}
+
+function api:customObservationSpec()
+ return {
+ {name = 'LOCATION', type = 'Doubles', shape = {3}},
+ {name = 'ORDER', type = 'Bytes', shape = {0}},
+ {name = 'EPISODE', type = 'Doubles', shape = {1}},
+ }
+end
+
+function api:init(settings)
+ api._settings = settings
+ local order = settings.order or ''
+ api._observations.ORDER = tensor.ByteTensor{order:byte(1, -1)}
+end
+
+function api:customObservation(name)
+ return api._observations[name]
+end
+
+function api:start(episode, seed)
+ api._observations.EPISODE:val(episode)
+end
+
+function api:commandLine(oldCommandLine)
+ return oldCommandLine .. ' ' .. api._settings.command
+end
+
+function api:nextMap()
+ api._count = api._count + 1
+ return 'lt_chasm_' .. api._count
+end
+
+function api:createModel(modelName)
+ return models[modelName]
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/debug_observation_test.lua b/assets/game_scripts/test_levels/debug_observation_test.lua
new file mode 100644
index 00000000..13f2224a
--- /dev/null
+++ b/assets/game_scripts/test_levels/debug_observation_test.lua
@@ -0,0 +1,39 @@
+local factory = require 'factories.lt_factory'
+local make_map = require 'common.make_map'
+local tensor = require 'dmlab.system.tensor'
+
+local MAP = [[
+********
+* *P *
+*P *
+********
+]]
+
+local api = factory.createLevelApi{
+ episodeLengthSeconds = 60 * 5,
+ botCount = 1
+}
+
+local spawnCount = 1
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ -- Spawn facing East.
+ spawnVars.angle = '0'
+ spawnVars.randomAngleRange = '0'
+ -- Make Bot spawn on first 'P' and player on second 'P'.
+ spawnVars.nohumans = spawnCount == 1 and '1' or '0'
+ spawnVars.nobots = spawnCount == 2 and '1' or '0'
+ spawnCount = spawnCount + 1
+ end
+ return spawnVars
+end
+
+function api:nextMap()
+ return make_map.makeMap{
+ mapName = 'empty_room',
+ mapEntityLayer = MAP,
+ allowBots = true,
+ }
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/empty_room_test.lua b/assets/game_scripts/test_levels/empty_room_test.lua
new file mode 100644
index 00000000..a66f8460
--- /dev/null
+++ b/assets/game_scripts/test_levels/empty_room_test.lua
@@ -0,0 +1,46 @@
+-- Tested externally.
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local custom_observations = require 'decorators.custom_observations'
+local game = require 'dmlab.system.game'
+local timeout = require 'decorators.timeout'
+local api = {}
+
+local MAP_ENTITIES = [[
+*********
+* *
+* *
+* *
+* P *
+* *
+* *
+* *
+*********
+]]
+
+function api:init(params)
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = "empty_room",
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ theme = "TETRIS"
+ }
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == "info_player_start" then
+ -- Spawn facing East.
+ spawnVars.angle = "0"
+ spawnVars.randomAngleRange = "0"
+ end
+ return spawnVars
+end
+
+timeout.decorate(api, 60 * 60)
+custom_observations.decorate(api)
+return api
diff --git a/assets/game_scripts/test_levels/entity_info_test.lua b/assets/game_scripts/test_levels/entity_info_test.lua
new file mode 100644
index 00000000..47529932
--- /dev/null
+++ b/assets/game_scripts/test_levels/entity_info_test.lua
@@ -0,0 +1,58 @@
+-- Tested in python/episode_time_test.py.
+local custom_observations = require 'decorators.custom_observations'
+local game_entities = require 'dmlab.system.game_entities'
+local helpers = require 'common.helpers'
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local tensor = require 'dmlab.system.tensor'
+local api = {}
+
+function api:createPickup(classname)
+ return pickups.defaults[classname]
+end
+
+local MAP_ENTITIES = [[PAAALLL]]
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == "info_player_start" then
+ -- Spawn facing East.
+ spawnVars.angle = "0"
+ spawnVars.randomAngleRange = "0"
+ end
+ return spawnVars
+end
+
+function api:nextMap()
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = "empty_room",
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ pickups = {L = 'lemon_reward', A = 'apple_reward'},
+ }
+ return api._map
+end
+
+local PICKUP_CLASSES = {'lemon_reward', 'apple_reward'}
+
+local function observePickups()
+ local entities = game_entities:entities(PICKUP_CLASSES)
+ local result = tensor.DoubleTensor(#entities, 5)
+ for i, entity in pairs(entities) do
+ local x, y, z = unpack(entity.position)
+ local v = entity.visible and 1 or 0
+ local row = result(i)
+ row(1):val(x)
+ row(2):val(y)
+ row(3):val(z)
+ row(4):val(v)
+ row(5):val(pickups.defaults[entity.classname].quantity)
+ end
+ return result
+end
+
+custom_observations.decorate(api)
+custom_observations.addSpec('DEBUG.PICKUPS', 'Doubles', {0, 5}, observePickups)
+
+return api
+
diff --git a/assets/game_scripts/test_levels/episode_time_test.lua b/assets/game_scripts/test_levels/episode_time_test.lua
new file mode 100644
index 00000000..316e8875
--- /dev/null
+++ b/assets/game_scripts/test_levels/episode_time_test.lua
@@ -0,0 +1,23 @@
+-- Tested in python/episode_time_test.py.
+local game = require 'dmlab.system.game'
+local tensor = require 'dmlab.system.tensor'
+
+local api = {}
+
+function api:customObservationSpec()
+ return {
+ {name = 'EPISODE_TIME_SECONDS', type = 'Doubles', shape = {1}},
+ }
+end
+
+function api:customObservation(name)
+ if name == 'EPISODE_TIME_SECONDS' then
+ return tensor.Tensor{game:episodeTimeSeconds()}
+ end
+end
+
+function api:nextMap()
+ return 'lookat_test'
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/event_test.lua b/assets/game_scripts/test_levels/event_test.lua
new file mode 100644
index 00000000..d7a4a19b
--- /dev/null
+++ b/assets/game_scripts/test_levels/event_test.lua
@@ -0,0 +1,32 @@
+-- Tested in python/dmlab_module_test.py.
+local events = require 'dmlab.system.events'
+local tensor = require 'dmlab.system.tensor'
+
+local api = {}
+
+local map = 'seekavoid_arena_01'
+
+function api:nextMap()
+ local result = map
+ map = ''
+ return result
+end
+
+function api:start(episode, seed)
+ events:add('TEXT', 'EPISODE ' .. episode)
+ events:add('DOUBLE', tensor.DoubleTensor{{1, 0}, {0, 1}})
+ events:add('BYTE', tensor.ByteTensor{2, 2})
+ events:add('ALL', 'Text', tensor.ByteTensor{3}, tensor.DoubleTensor{7})
+end
+
+
+function api:hasEpisodeFinished(time)
+ if time >= 1.0 then
+ events:add('LOG', 'Episode ended')
+ return true
+ else
+ return false
+ end
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/extra_entities_test.lua b/assets/game_scripts/test_levels/extra_entities_test.lua
new file mode 100644
index 00000000..bbcd7482
--- /dev/null
+++ b/assets/game_scripts/test_levels/extra_entities_test.lua
@@ -0,0 +1,82 @@
+-- Tested externally.
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local custom_observations = require 'decorators.custom_observations'
+local game = require 'dmlab.system.game'
+local timeout = require 'decorators.timeout'
+local api = {}
+
+local MAP_ENTITIES = [[
+*********
+* *
+* *
+* *
+* P *
+* *
+* *
+* *
+*********
+]]
+
+function api:init(params)
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = 'empty_room',
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ theme = 'TETRIS'
+ }
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ -- Spawn facing East.
+ spawnVars.angle = '0'
+ spawnVars.randomAngleRange = '0'
+ end
+ return spawnVars
+end
+
+function api:extraEntities()
+ return {
+ {
+ classname = 'apple_reward',
+ origin = '550 450 30',
+ count = '1',
+ },
+ {
+ classname = 'apple_reward',
+ origin = '600 450 30',
+ count = '2', -- Override reward for testing purposes.
+ },
+ {
+ classname = 'apple_reward',
+ origin = '650 450 30',
+ count = '3',
+ },
+ {
+ classname = 'apple_reward',
+ origin = '700 450 30',
+ count = '4',
+ },
+ {
+ classname = 'apple_reward',
+ origin = '750 450 30',
+ count = '5',
+ }
+ }
+end
+
+-- Create apple explicitly
+function api:createPickup(classname)
+ return pickups.defaults[classname]
+end
+
+timeout.decorate(api, 60 * 60)
+custom_observations.decorate(api)
+return api
+
diff --git a/assets/game_scripts/test_levels/extra_entities_with_bots_test.lua b/assets/game_scripts/test_levels/extra_entities_with_bots_test.lua
new file mode 100644
index 00000000..dd10b262
--- /dev/null
+++ b/assets/game_scripts/test_levels/extra_entities_with_bots_test.lua
@@ -0,0 +1,76 @@
+-- Tested externally.
+local helpers = require 'common.helpers'
+local make_map = require 'common.make_map'
+local custom_observations = require 'decorators.custom_observations'
+local events = require 'dmlab.system.events'
+local game = require 'dmlab.system.game'
+local timeout = require 'decorators.timeout'
+local api = {}
+
+local MAP_NO_WEAPON = [[
+*********
+* P*
+* ***** *
+* ***** *
+* ***** *
+* ***** *
+* ***** *
+*P *
+*********
+]]
+
+function api:init(params)
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = 'empty_room',
+ mapEntityLayer = MAP_NO_WEAPON,
+ useSkybox = true,
+ theme = 'TETRIS',
+ allowBots = true
+ }
+ self._spawnWeapons = params.spawnWeapons
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:addBots()
+ local bots = {
+ {name = 'Cygni', skill = 5.0}
+ }
+ return bots
+end
+
+function api:extraEntities()
+ if helpers.fromString(self._spawnWeapons) then
+ -- List of entities to create.
+ local vars = {
+ {
+ classname = 'weapon_rocketlauncher',
+ origin = '750 150 30',
+ spawnflags = "1"
+ },
+ {
+ classname = 'weapon_lightning',
+ origin = '150 750 30',
+ spawnflags = "1"
+ }
+ }
+ return vars
+ else
+ return nil
+ end
+end
+
+function api:rewardOverride(args)
+ if args.reason == "TAG_PLAYER" and args.playerId == 1 and
+ args.otherPlayerId == 0 then
+ events:add('PLAYER_TAGGED', 'Player tagged by bot')
+ end
+end
+
+timeout.decorate(api, 60 * 60)
+custom_observations.decorate(api)
+return api
+
diff --git a/assets/game_scripts/test_levels/lookat_test.lua b/assets/game_scripts/test_levels/lookat_test.lua
new file mode 100644
index 00000000..b44d4b5f
--- /dev/null
+++ b/assets/game_scripts/test_levels/lookat_test.lua
@@ -0,0 +1,44 @@
+-- Tested in python/lookat_test.py.
+local game = require 'dmlab.system.game'
+local tensor = require 'dmlab.system.tensor'
+local random = require 'common.random'
+local pickups = require 'common.pickups'
+local custom_observations = require 'decorators.custom_observations'
+
+local api = {}
+local lookAt = tensor.Tensor(4)
+
+function api:createPickup(classname)
+ return pickups.defaults[classname]
+end
+
+function api:start(episode, seed)
+ random:seed(seed)
+end
+
+function api:nextMap()
+ return 'lookat_test'
+end
+
+function api:customObservationSpec()
+ return {
+ {name = 'LOOK_AT', type = 'Doubles', shape = lookAt:shape()},
+ }
+end
+
+function api:customObservation(name)
+ if name == 'LOOK_AT' then
+ return lookAt
+ end
+end
+
+function api:lookat(entity, lookedAt, position)
+ lookAt(1):val(position[1])
+ lookAt(2):val(position[2])
+ lookAt(3):val(position[3])
+ lookAt(4):val(lookedAt and 1.0 or 0.0)
+end
+
+custom_observations.decorate(api)
+
+return api
diff --git a/assets/game_scripts/test_levels/model_test.lua b/assets/game_scripts/test_levels/model_test.lua
new file mode 100644
index 00000000..d8592ad8
--- /dev/null
+++ b/assets/game_scripts/test_levels/model_test.lua
@@ -0,0 +1,70 @@
+-- Tested in deepmind/model_generation/lua_model_test.cc.
+local model = require 'dmlab.system.model'
+local transform = require 'common.transform'
+
+local api = {}
+
+function api:createModel(modelName)
+ local models = {
+ cone = model:cone{
+ phiSegments = 4,
+ radiusSegments = 4,
+ heightSegments = 4,
+ shaderName = 'textures/model/beam'
+ },
+ cube = model:cube{
+ segments = 4,
+ shaderName = 'textures/model/beam'
+ },
+ cylinder = model:cylinder{
+ phiSegments = 4,
+ radiusSegments = 4,
+ heightSegments = 4,
+ shaderName = 'textures/model/beam'
+ },
+ sphere = model:sphere{
+ phiSegments = 4,
+ thetaSegments = 4,
+ shaderName = 'textures/model/beam'
+ },
+ hierarchy = model:hierarchy{
+ transform = transform.rotateX(180),
+ model = model:cone{
+ radius = 10,
+ height = 20,
+ shaderName = 'textures/model/apple_d'
+ },
+ children = {
+ centre_bottom_centre_p = {
+ model = model:sphere{
+ radius = 8,
+ shaderName = 'textures/model/pig_d'
+ },
+ children = {
+ centre_top_left_p = {
+ locator = 'centre_bottom_centre_s',
+ model = model:cylinder{
+ radius = 0.5,
+ height = 5,
+ shaderName = 'textures/model/cherry_d'
+ },
+ children = {
+ centre_top_centre_p = {
+ locator = 'centre_bottom_centre_s',
+ model = model:cone{
+ radius = 3,
+ height = 2,
+ shaderName = 'textures/model/apple_d'
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return models[modelName]
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/raycast_test.lua b/assets/game_scripts/test_levels/raycast_test.lua
new file mode 100644
index 00000000..81c47265
--- /dev/null
+++ b/assets/game_scripts/test_levels/raycast_test.lua
@@ -0,0 +1,64 @@
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local custom_observations = require 'decorators.custom_observations'
+local game = require 'dmlab.system.game'
+local tensor = require 'dmlab.system.tensor'
+local helpers = require 'common.helpers'
+local api = {}
+
+local MAP_ENTITIES = [[
+*****
+*A A*
+* * *
+*P*A*
+*****
+]]
+
+
+function api:init(params)
+ make_map.seedRng(1)
+ self._map = make_map.makeMap{
+ mapName = "empty_room",
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ theme = "MISHMASH"
+ }
+ self.rewards = {}
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:createPickup(classname)
+ return pickups.defaults[classname]
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == "info_player_start" then
+ -- Spawn facing north.
+ spawnVars.angle = "90"
+ spawnVars.randomAngleRange = "0"
+ else
+ self.rewards[#self.rewards + 1] =
+ helpers.spawnVarToNumberTable(spawnVars.origin)
+ end
+ return spawnVars
+end
+
+function api:customObservationSpec()
+ return {{name = 'RAYCASTS', type = 'Doubles', shape = {0}}}
+end
+
+function api:customObservation(name)
+ assert(name == 'RAYCASTS', 'Bad observation name')
+ local playerInfo = game:playerInfo()
+ local result = tensor.DoubleTensor(#self.rewards)
+ for i, pos in ipairs(self.rewards) do
+ result(i):val(game:raycast(playerInfo.pos, pos))
+ end
+ return result
+end
+
+custom_observations.decorate(api)
+return api
diff --git a/assets/game_scripts/test_levels/recording_test.lua b/assets/game_scripts/test_levels/recording_test.lua
new file mode 100644
index 00000000..4b7eb207
--- /dev/null
+++ b/assets/game_scripts/test_levels/recording_test.lua
@@ -0,0 +1,43 @@
+-- Tested in testing/recording_test.cc.
+local game = require 'dmlab.system.game'
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+
+local api = {}
+
+function api:start(episode, seed)
+ make_map.seedRng(seed)
+ api._count = 0
+end
+
+function api:createPickup(classname)
+ return pickups.defaults[classname]
+end
+
+function api:nextMap()
+ api._count = api._count + 1
+
+ return make_map.makeMap{
+ mapName = 'recording_test',
+ mapEntityLayer = string.rep('A', api._count) .. 'P'
+ }
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == 'info_player_start' then
+ -- Spawn facing south.
+ spawnVars.angle = '180'
+ spawnVars.randomAngleRange = '0'
+ end
+ return spawnVars
+end
+
+function api:hasEpisodeFinished(time_seconds)
+ -- Each map lasts one second.
+ if time_seconds >= api._count then
+ game:finishMap()
+ end
+ return false
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/seed_test.lua b/assets/game_scripts/test_levels/seed_test.lua
new file mode 100644
index 00000000..1c8593cf
--- /dev/null
+++ b/assets/game_scripts/test_levels/seed_test.lua
@@ -0,0 +1,38 @@
+-- Tested in testing/load_level_test.cc.
+local asserts = require 'testing.asserts'
+local test_runner = require 'testing.test_runner'
+local random = require 'common.random'
+
+local tests = {}
+local testLevelScript = false
+
+function tests.testSeed()
+ assert(testLevelScript)
+
+ local seed = 2
+
+ local api = require(testLevelScript)
+ api:init{invocationMode = "testbed"}
+ api:start(0, seed)
+ local rand1 = random:uniformReal(0, 1)
+ local rand2 = random:uniformReal(0, 1)
+
+ -- If we generate the exact same random numbers again then it's very likely
+ -- that the random number generated was seeded.
+ api:start(0, seed)
+ asserts.EQ(rand1, random:uniformReal(0, 1))
+ asserts.EQ(rand2, random:uniformReal(0, 1))
+end
+
+local run_tests = test_runner.run(tests)
+
+-- Override the test runner initialiser to set the name of the level to test.
+local init = run_tests.init
+function run_tests:init(params)
+ assert(params.testLevelScript)
+ testLevelScript = params.testLevelScript
+
+ return init(run_tests, params)
+end
+
+return run_tests
diff --git a/assets/game_scripts/test_levels/spawn_inventory_test.lua b/assets/game_scripts/test_levels/spawn_inventory_test.lua
new file mode 100644
index 00000000..cf9ed256
--- /dev/null
+++ b/assets/game_scripts/test_levels/spawn_inventory_test.lua
@@ -0,0 +1,57 @@
+local api = {}
+
+-- Tested externally.
+local game = require 'dmlab.system.game'
+local make_map = require 'common.make_map'
+local pickups = require 'common.pickups'
+local inventory = require 'common.inventory'
+local custom_observations = require 'decorators.custom_observations'
+local timeout = require 'decorators.timeout'
+local api = {}
+
+local MAP_ENTITIES = [[
+*********
+* *
+* *
+* *
+* P *
+* *
+* *
+* *
+*********
+]]
+
+function api:init(params)
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = "empty_room",
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ theme = "TETRIS"
+ }
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == "info_player_start" then
+ -- Spawn facing East.
+ spawnVars.angle = "0"
+ spawnVars.randomAngleRange = "0"
+ end
+ return spawnVars
+end
+
+function api:spawnInventory(loadOut)
+ local view = inventory.View(loadOut)
+ view:setGadgetAmount(inventory.GADGETS.ORB, inventory.UNLIMITED)
+ view:setGadgets{inventory.GADGETS.ORB}
+ return view:loadOut()
+end
+
+timeout.decorate(api, 60 * 60)
+custom_observations.decorate(api)
+return api
+
diff --git a/assets/game_scripts/test_levels/text_observation_test.lua b/assets/game_scripts/test_levels/text_observation_test.lua
new file mode 100644
index 00000000..45c53e79
--- /dev/null
+++ b/assets/game_scripts/test_levels/text_observation_test.lua
@@ -0,0 +1,17 @@
+-- Tested in python/dmlab_module_test.py.
+local api = {}
+
+function api:nextMap()
+ return 'seekavoid_arena_01'
+end
+
+function api:customObservationSpec()
+ return {{name = 'CUSTOM_TEXT', type = 'String', shape = {0}}}
+end
+
+function api:customObservation(name)
+ assert(name == 'CUSTOM_TEXT')
+ return 'Example Output'
+end
+
+return api
diff --git a/assets/game_scripts/test_levels/update_inventory_test.lua b/assets/game_scripts/test_levels/update_inventory_test.lua
new file mode 100644
index 00000000..c6257e17
--- /dev/null
+++ b/assets/game_scripts/test_levels/update_inventory_test.lua
@@ -0,0 +1,81 @@
+local api = {}
+
+-- Tested externally.
+local game = require 'dmlab.system.game'
+local make_map = require 'common.make_map'
+local inventory = require 'common.inventory'
+local custom_observations = require 'decorators.custom_observations'
+local timeout = require 'decorators.timeout'
+local tensor = require 'dmlab.system.tensor'
+local api = {}
+
+local MAP_ENTITIES = [[
+*********
+* *
+* *
+* *
+* P *
+* *
+* *
+* *
+*********
+]]
+
+function api:init(params)
+ self._playerView = {}
+ make_map.seedRng(1)
+ api._map = make_map.makeMap{
+ mapName = "empty_room",
+ mapEntityLayer = MAP_ENTITIES,
+ useSkybox = true,
+ theme = "TETRIS"
+ }
+end
+
+function api:nextMap()
+ return self._map
+end
+
+function api:customObservationSpec()
+ return {
+ {name = 'DEBUG.AMOUNT', type = 'Doubles', shape = {1}},
+ {name = 'DEBUG.GADGET', type = 'Doubles', shape = {1}},
+ }
+end
+
+function api:customObservation(name)
+ local view = self._playerView[1]
+ if name == 'DEBUG.AMOUNT' then
+ return tensor.Tensor{view:gadgetAmount(view:gadget())}
+ elseif name == 'DEBUG.GADGET' then
+ return tensor.Tensor{view:gadget()}
+ end
+end
+
+function api:updateSpawnVars(spawnVars)
+ if spawnVars.classname == "info_player_start" then
+ -- Spawn facing East.
+ spawnVars.angle = "0"
+ spawnVars.randomAngleRange = "0"
+ end
+ return spawnVars
+end
+
+function api:spawnInventory(loadOut)
+ local view = inventory.View(loadOut)
+ view:setGadgets{inventory.GADGETS.ORB, inventory.GADGETS.RAPID}
+ view:setGadgetAmount(inventory.GADGETS.ORB, 2)
+ view:setGadgetAmount(inventory.GADGETS.RAPID, 10)
+ self._playerView[view:playerId()] = view
+ return view:loadOut()
+end
+
+function api:updateInventory(loadOut)
+ local view = inventory.View(loadOut)
+ self._playerView[view:playerId()] = view
+end
+
+timeout.decorate(api, 60 * 60)
+custom_observations.decorate(api)
+return api
+
diff --git a/assets/game_scripts/testing/asserts.lua b/assets/game_scripts/testing/asserts.lua
new file mode 100644
index 00000000..4a6876e0
--- /dev/null
+++ b/assets/game_scripts/testing/asserts.lua
@@ -0,0 +1,95 @@
+local asserts = {}
+
+local function fail(message, optional)
+ optional = optional and (' ' .. optional) or ''
+ error(message .. optional, 3)
+end
+
+local function areTablesEqual(target, expect)
+ local targetSize = 0
+ for k, v in pairs(target) do
+ targetSize = targetSize + 1
+ end
+ for k, v in pairs(expect) do
+ if type(v) == 'table' then
+ if not areTablesEqual(target[k], v) then
+ return false
+ end
+ elseif target[k] ~= v then
+ return false
+ end
+ targetSize = targetSize - 1
+ end
+ if targetSize ~= 0 then
+ -- There were values in target not present in expect
+ return false
+ end
+ return true
+end
+
+function asserts.EQ(target, expect, msg)
+ if target ~= expect then
+ fail('Expected: ' .. tostring(expect) .. '. ' ..
+ 'Actual: ' .. tostring(target) .. '.', msg)
+ end
+end
+
+function asserts.NE(target, expect, msg)
+ if target == expect then
+ fail('Expected values to differ: ' .. tostring(expect), msg)
+ end
+end
+
+-- Assert target > expect
+function asserts.GT(target, expect, msg)
+ if target <= expect then
+ fail('Expected: ' .. tostring(target) .. ' > ' .. tostring(expect), msg)
+ end
+end
+
+-- Assert target >= expect
+function asserts.GE(target, expect, msg)
+ if target < expect then
+ fail('Expected: ' .. tostring(target) .. ' >= ' .. tostring(expect), msg)
+ end
+end
+
+-- Assert target < expect
+function asserts.LT(target, expect, msg)
+ if target >= expect then
+ fail('Expected: ' .. tostring(target) .. ' < ' .. tostring(expect), msg)
+ end
+end
+
+-- Assert target <= expect
+function asserts.LE(target, expect, msg)
+ if target > expect then
+ fail('Expected: ' .. tostring(target) .. ' <= ' .. tostring(expect), msg)
+ end
+end
+
+
+function asserts.tablesEQ(target, expect, msg)
+ if type(target) ~= 'table' then fail('1st argument should be a table') end
+ if type(expect) ~= 'table' then fail('2nd argument should be a table') end
+ if not areTablesEqual(target, expect) then
+ fail("Expected equal table values.", msg)
+ end
+end
+
+function asserts.tablesNE(target, expect, msg)
+ if type(target) ~= 'table' then fail('1st argument should be a table') end
+ if type(expect) ~= 'table' then fail('2nd argument should be a table') end
+ if areTablesEqual(target, expect) then
+ fail("Expected tables with different values.", msg)
+ end
+end
+
+function asserts.shouldFail(fn)
+ local status, out = pcall(fn)
+ if status then
+ error("Expected an error but call succeeded.", 2)
+ end
+end
+
+return asserts
diff --git a/assets/game_scripts/testing/test_runner.lua b/assets/game_scripts/testing/test_runner.lua
new file mode 100644
index 00000000..5ffd92d9
--- /dev/null
+++ b/assets/game_scripts/testing/test_runner.lua
@@ -0,0 +1,49 @@
+--[[ Useful for running Lua tests in DM Lab.
+
+In the build file:
+
+```
+cc_test(
+ name = "test_script",
+ args = ["lua_tests/test_script.lua"],
+ data = [":test_script.lua"],
+ deps = ["/labyrinth/testing:lua_unit_test_lib"],
+)
+```
+
+In test_script.lua:
+
+```
+local tests = {}
+
+function tests.testAssert()
+ assert(true)
+end
+
+return test_runner.run(tests)
+```
+]]
+
+local test_runner = {}
+
+function test_runner.run(tests)
+ local function runAllTests()
+ local statusAll = true
+ local errors = ''
+ for name, test in pairs(tests) do
+ print('[ LuaTest ] - ' .. name)
+ local status, msg = xpcall(test, debug.traceback)
+ if not status then
+ errors = errors .. 'function tests.' .. name .. '\n' .. msg .. '\n'
+ print('[ Failed ] - ' .. name)
+ statusAll = false
+ else
+ print('[ Success ] - ' .. name)
+ end
+ end
+ return (statusAll and 0 or 1), errors
+ end
+ return {init = runAllTests}
+end
+
+return test_runner
diff --git a/assets/game_scripts/tests/callbacks_test.lua b/assets/game_scripts/tests/callbacks_test.lua
deleted file mode 100644
index 858f5306..00000000
--- a/assets/game_scripts/tests/callbacks_test.lua
+++ /dev/null
@@ -1,43 +0,0 @@
-local tensor = require 'dmlab.system.tensor'
-local api = {}
-
-api._count = 0
-
-api._observations = {
- LOCATION = tensor.Tensor{10, 20, 30},
- ORDER = tensor.ByteTensor(),
- EPISODE = tensor.Tensor{0},
-}
-
-function api:customObservationSpec()
- return {
- {name = 'LOCATION', type = 'Doubles', shape = {3}},
- {name = 'ORDER', type = 'Bytes', shape = {0}},
- {name = 'EPISODE', type = 'Doubles', shape = {1}},
- }
-end
-
-function api:init(settings)
- api._settings = settings
- local order = settings.order or ''
- api._observations.ORDER = tensor.ByteTensor{order:byte(1,-1)}
-end
-
-function api:customObservation(name)
- return api._observations[name]
-end
-
-function api:start(episode, seed)
- api._observations.EPISODE:val(episode)
-end
-
-function api:commandLine(oldCommandLine)
- return oldCommandLine .. ' ' .. api._settings.command
-end
-
-function api:nextMap()
- api._count = api._count + 1
- return 'lt_chasm_' .. api._count
-end
-
-return api
diff --git a/assets/game_scripts/tests/demo_map.lua b/assets/game_scripts/tests/demo_map.lua
deleted file mode 100644
index 6da418fb..00000000
--- a/assets/game_scripts/tests/demo_map.lua
+++ /dev/null
@@ -1,27 +0,0 @@
-local make_map = require 'common.make_map'
-local pickups = require 'common.pickups'
-local api = {}
-
-function api:start(episode, seed)
- make_map.seedRng(seed)
- api._count = 0
-end
-
-function api:commandLine(oldCommandLine)
- return make_map.commandLine(oldCommandLine)
-end
-
-function api:createPickup(className)
- return pickups.defaults[className]
-end
-
-function api:nextMap()
- map = "G I A P"
- api._count = api._count + 1
- for i = 0, api._count do
- map = map.." A"
- end
- return make_map.makeMap("demo_map_" .. api._count, map)
-end
-
-return api
diff --git a/assets/game_scripts/themes/decals.lua b/assets/game_scripts/themes/decals.lua
new file mode 100644
index 00000000..44f372c0
--- /dev/null
+++ b/assets/game_scripts/themes/decals.lua
@@ -0,0 +1,18 @@
+local NUMBER_OF_STYLES = 4
+local NUMBER_OF_IMAGES_PER_STYLE = 20
+local DEFAULT_WALL_DECALS = {}
+local DEFAULT_WALL_IMAGES = {}
+for i = 1, NUMBER_OF_STYLES do
+ for j = 1, NUMBER_OF_IMAGES_PER_STYLE do
+ local img = string.format('decal/lab_games/dec_img_style%02d_%03d', i, j)
+ DEFAULT_WALL_DECALS[#DEFAULT_WALL_DECALS + 1] = {
+ tex = img .. '_nonsolid'
+ }
+ DEFAULT_WALL_IMAGES[#DEFAULT_WALL_IMAGES + 1] = 'textures/' .. img
+ end
+end
+
+return {
+ decals = DEFAULT_WALL_DECALS,
+ images = DEFAULT_WALL_IMAGES,
+}
diff --git a/assets/game_scripts/themes/texture_sets.lua b/assets/game_scripts/themes/texture_sets.lua
new file mode 100644
index 00000000..ff9c28a0
--- /dev/null
+++ b/assets/game_scripts/themes/texture_sets.lua
@@ -0,0 +1,223 @@
+local decals = require 'themes.decals'
+
+local texture_sets = {}
+
+texture_sets.MISHMASH = {
+ floor = {
+ {tex = 'map/lab_games/lg_style_01_floor_orange'},
+ {tex = 'map/lab_games/lg_style_01_floor_orange_bright'},
+ {tex = 'map/lab_games/lg_style_01_floor_blue'},
+ {tex = 'map/lab_games/lg_style_01_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_02_floor_blue'},
+ {tex = 'map/lab_games/lg_style_02_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_02_floor_green'},
+ {tex = 'map/lab_games/lg_style_02_floor_green_bright'},
+ {tex = 'map/lab_games/lg_style_03_floor_green'},
+ {tex = 'map/lab_games/lg_style_03_floor_green_bright'},
+ {tex = 'map/lab_games/lg_style_03_floor_blue'},
+ {tex = 'map/lab_games/lg_style_03_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_04_floor_blue'},
+ {tex = 'map/lab_games/lg_style_04_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_04_floor_orange'},
+ {tex = 'map/lab_games/lg_style_04_floor_orange_bright'},
+ {tex = 'map/lab_games/lg_style_05_floor_blue'},
+ {tex = 'map/lab_games/lg_style_05_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_05_floor_orange'},
+ {tex = 'map/lab_games/lg_style_05_floor_orange_bright'},
+ },
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_01_wall_green'},
+ {tex = 'map/lab_games/lg_style_01_wall_green_bright'},
+ {tex = 'map/lab_games/lg_style_01_wall_red'},
+ {tex = 'map/lab_games/lg_style_01_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_02_wall_yellow'},
+ {tex = 'map/lab_games/lg_style_02_wall_yellow_bright'},
+ {tex = 'map/lab_games/lg_style_02_wall_blue'},
+ {tex = 'map/lab_games/lg_style_02_wall_blue_bright'},
+ {tex = 'map/lab_games/lg_style_03_wall_orange'},
+ {tex = 'map/lab_games/lg_style_03_wall_orange_bright'},
+ {tex = 'map/lab_games/lg_style_03_wall_gray'},
+ {tex = 'map/lab_games/lg_style_03_wall_gray_bright'},
+ {tex = 'map/lab_games/lg_style_04_wall_green'},
+ {tex = 'map/lab_games/lg_style_04_wall_green_bright'},
+ {tex = 'map/lab_games/lg_style_04_wall_red'},
+ {tex = 'map/lab_games/lg_style_04_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_05_wall_red'},
+ {tex = 'map/lab_games/lg_style_05_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_05_wall_yellow'},
+ {tex = 'map/lab_games/lg_style_05_wall_yellow_bright'}
+ },
+ wallDecals = decals.decals,
+}
+
+texture_sets.TRON = {
+ floor = {
+ {tex = 'map/lab_games/lg_style_01_floor_orange'},
+ {tex = 'map/lab_games/lg_style_01_floor_orange_bright'},
+ {tex = 'map/lab_games/lg_style_01_floor_blue'},
+ {tex = 'map/lab_games/lg_style_01_floor_blue_bright'},
+ },
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_01_wall_green'},
+ {tex = 'map/lab_games/lg_style_01_wall_green_bright'},
+ {tex = 'map/lab_games/lg_style_01_wall_red'},
+ {tex = 'map/lab_games/lg_style_01_wall_red_bright'},
+ },
+ wallDecals = decals.decals,
+}
+
+texture_sets.MINESWEEPER = {
+ floor = {
+ {tex = 'map/lab_games/lg_style_04_floor_blue'},
+ {tex = 'map/lab_games/lg_style_04_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_04_floor_orange'},
+ {tex = 'map/lab_games/lg_style_04_floor_orange_bright'},
+ },
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_04_wall_green'},
+ {tex = 'map/lab_games/lg_style_04_wall_green_bright'},
+ {tex = 'map/lab_games/lg_style_04_wall_red'},
+ {tex = 'map/lab_games/lg_style_04_wall_red_bright'},
+ },
+ wallDecals = decals.decals,
+ floorModels = {
+ {mod = 'models/fut_obj_barbell_01.md3'},
+ {mod = 'models/fut_obj_cylinder_01.md3'},
+ },
+}
+
+
+texture_sets.TETRIS = {
+ floor = {
+ {tex = 'map/lab_games/lg_style_02_floor_blue'},
+ {tex = 'map/lab_games/lg_style_02_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_02_floor_green'},
+ {tex = 'map/lab_games/lg_style_02_floor_green_bright'},
+ },
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_02_wall_yellow'},
+ {tex = 'map/lab_games/lg_style_02_wall_yellow_bright'},
+ {tex = 'map/lab_games/lg_style_02_wall_blue'},
+ {tex = 'map/lab_games/lg_style_02_wall_blue_bright'},
+ },
+ wallDecals = decals.decals,
+}
+
+texture_sets.GO = {
+ floor = {
+ {tex = 'map/lab_games/lg_style_03_floor_green'},
+ {tex = 'map/lab_games/lg_style_03_floor_green_bright'},
+ {tex = 'map/lab_games/lg_style_03_floor_blue'},
+ {tex = 'map/lab_games/lg_style_03_floor_blue_bright'},
+ },
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_03_wall_orange'},
+ {tex = 'map/lab_games/lg_style_03_wall_orange_bright'},
+ {tex = 'map/lab_games/lg_style_03_wall_gray'},
+ {tex = 'map/lab_games/lg_style_03_wall_gray_bright'},
+ },
+ wallDecals = decals.decals,
+ floorModels = {
+ {mod = 'models/fut_obj_barbell_01.md3'},
+ {mod = 'models/fut_obj_coil_01.md3'},
+ {mod = 'models/fut_obj_cone_01.md3'},
+ {mod = 'models/fut_obj_crossbar_01.md3'},
+ {mod = 'models/fut_obj_cube_01.md3'},
+ {mod = 'models/fut_obj_cylinder_01.md3'},
+ {mod = 'models/fut_obj_doubleprism_01.md3'},
+ {mod = 'models/fut_obj_glowball_01.md3'}
+ }
+}
+
+texture_sets.PACMAN = {
+ floor = {
+ {tex = 'map/lab_games/lg_style_05_floor_blue'},
+ {tex = 'map/lab_games/lg_style_05_floor_blue_bright'},
+ {tex = 'map/lab_games/lg_style_05_floor_orange'},
+ {tex = 'map/lab_games/lg_style_05_floor_orange_bright'},
+ },
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_05_wall_red'},
+ {tex = 'map/lab_games/lg_style_05_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_05_wall_yellow'},
+ {tex = 'map/lab_games/lg_style_05_wall_yellow_bright'},
+ },
+ wallDecals = decals.decals,
+ floorModels = {
+ {mod = 'models/fut_obj_toroid_01.md3'},
+ {mod = 'models/fut_obj_cylinder_01.md3'},
+ {mod = 'models/fut_obj_crossbar_01.md3'},
+ {mod = 'models/fut_obj_cube_01.md3'},
+ },
+}
+
+texture_sets.INVISIBLE_WALLS = {
+ floor = {{tex = 'map/lab_games/lg_style_01_floor_orange'}},
+ ceiling = {{tex = "map/lab_games/fake_sky"}},
+ wall = {{tex = 'map/poltergeist'}},
+}
+
+texture_sets.CUSTOMIZABLE_FLOORS = {
+ variations = {
+ A = {floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_A'}}},
+ B = {floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_B'}}},
+ C = {floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_C'}}},
+ D = {floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_D'}}},
+ E = {floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_E'}}},
+ F = {floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_F'}}},
+ },
+ floor = {{tex = 'map/lab_games/lg_style_01_floor_placeholder_0'}},
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {
+ {tex = 'map/lab_games/lg_style_01_wall_green'},
+ {tex = 'map/lab_games/lg_style_01_wall_green_bright'},
+ {tex = 'map/lab_games/lg_style_01_wall_red'},
+ {tex = 'map/lab_games/lg_style_01_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_02_wall_yellow'},
+ {tex = 'map/lab_games/lg_style_02_wall_yellow_bright'},
+ {tex = 'map/lab_games/lg_style_02_wall_blue'},
+ {tex = 'map/lab_games/lg_style_02_wall_blue_bright'},
+ {tex = 'map/lab_games/lg_style_03_wall_orange'},
+ {tex = 'map/lab_games/lg_style_03_wall_orange_bright'},
+ {tex = 'map/lab_games/lg_style_03_wall_gray'},
+ {tex = 'map/lab_games/lg_style_03_wall_gray_bright'},
+ {tex = 'map/lab_games/lg_style_04_wall_green'},
+ {tex = 'map/lab_games/lg_style_04_wall_green_bright'},
+ {tex = 'map/lab_games/lg_style_04_wall_red'},
+ {tex = 'map/lab_games/lg_style_04_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_05_wall_red'},
+ {tex = 'map/lab_games/lg_style_05_wall_red_bright'},
+ {tex = 'map/lab_games/lg_style_05_wall_yellow'},
+ {tex = 'map/lab_games/lg_style_05_wall_yellow_bright'}
+ },
+ wallDecals = decals.decals,
+}
+
+texture_sets.CAPTURE_THE_FLAG = {
+ variations = {
+ A = {
+ floor = {{tex = 'map/lab_games/lg_style_01_floor_red_team_d'}},
+ wall = {{tex = 'map/lab_games/lg_style_01_wall_red'}},
+ },
+ B = {
+ floor = {{tex = 'map/lab_games/lg_style_01_floor_blue_team_d'}},
+ wall = {{tex = 'map/lab_games/lg_style_02_wall_blue'}},
+ },
+ E = {
+ floor = {{tex = 'map/lab_games/lg_style_04_floor_orange'}},
+ wall = {{tex = 'map/lab_games/lg_style_03_wall_orange'}},
+ },
+ },
+ floor = {{tex = 'map/lab_games/lg_style_03_floor_green'}},
+ ceiling = {{tex = 'map/lab_games/fake_sky'}},
+ wall = {{tex = 'map/lab_games/lg_style_01_wall_green'}},
+ wallDecals = decals.decals,
+}
+
+return texture_sets
diff --git a/assets/game_scripts/themes/themes.lua b/assets/game_scripts/themes/themes.lua
new file mode 100644
index 00000000..7b04ca3c
--- /dev/null
+++ b/assets/game_scripts/themes/themes.lua
@@ -0,0 +1,98 @@
+local map_maker = require 'dmlab.system.map_maker'
+local random = require 'common.random'
+local randomMap = random(map_maker:randomGen())
+
+local themes = {}
+
+function themes.fromTextureSet(opts)
+ assert(opts.textureSet, "Must supply a textureSet")
+ local ts = opts.textureSet
+ local decalFrequency = opts.decalFrequency or 0.1
+ local floorModelFrequency = opts.floorModelFrequency or 0.05
+ local theme = {}
+ local themeVariation = {}
+ local riser = ((ts.riser and ts.riser[1])
+ or {tex = 'map/lab_games/lg_style_02_wall_blue'})
+ local tread = ((ts.tread and ts.tread[1])
+ or {tex = 'map/black_d', width = 64, height = 64})
+ themeVariation.default = {
+ floor = ts.floor[1],
+ ceiling = ts.ceiling[1],
+ wallN = ts.wall[1],
+ wallE = ts.wall[1],
+ wallS = ts.wall[1],
+ wallW = ts.wall[1],
+ riser = riser,
+ tread = tread,
+ }
+
+ local wallDecorations = {}
+ if ts.wallDecals then
+ for i = 1, #ts.wallDecals do
+ wallDecorations[#wallDecorations + 1] = ts.wallDecals[i]
+ end
+ end
+
+ local function variationSet(ts, variation, property)
+ return ts.variations and ts.variations[variation] and
+ ts.variations[variation][property] or ts[property]
+ end
+
+ function theme:mazeVariation(variation)
+ if not themeVariation[variation] then
+ local wall = randomMap:choice(variationSet(ts, variation, 'wall'))
+ themeVariation[variation] = {
+ floor = randomMap:choice(variationSet(ts, variation, 'floor')),
+ ceiling = randomMap:choice(variationSet(ts, variation, 'ceiling')),
+ wallN = wall,
+ wallE = wall,
+ wallS = wall,
+ wallW = wall,
+ }
+ end
+ return themeVariation[variation]
+ end
+
+ -- Only create function if it will return anything.
+ if decalFrequency > 0 and ts.wallDecals and #ts.wallDecals > 0 then
+ function theme:placeWallDecals(allWallLocations)
+ local decorationCount = math.floor(math.min(0.5 +
+ decalFrequency * #allWallLocations, #ts.wallDecals))
+ local wallDecals = {}
+ if decorationCount > 0 then
+ randomMap:shuffleInPlace(allWallLocations)
+ local wallDecalsShuffled = randomMap:shuffle(ts.wallDecals)
+ for i = 1, decorationCount do
+ wallDecals[i] = {
+ index = allWallLocations[i].index,
+ decal = wallDecalsShuffled[i],
+ }
+ end
+ end
+ return wallDecals
+ end
+ end
+
+ -- Only create function if it will return anything.
+ if floorModelFrequency > 0 and ts.floorModels and #ts.floorModels > 0 then
+ function theme:placeFloorModels(allFloorLocations)
+ local decorationCount = math.floor(floorModelFrequency *
+ #allFloorLocations)
+ local floorModels = {}
+ if decorationCount > 0 and #ts.floorModels > 0 then
+ randomMap:shuffleInPlace(allFloorLocations)
+ for i = 1, decorationCount do
+ floorModels[i] = {
+ index = allFloorLocations[i].index,
+ model = randomMap:choice(ts.floorModels)
+ }
+ end
+ end
+ return floorModels
+ end
+ end
+
+ return theme
+end
+
+return themes
diff --git a/assets/icons/iconf_blu1.tga b/assets/icons/iconf_blu1.tga
new file mode 100644
index 00000000..d4434643
Binary files /dev/null and b/assets/icons/iconf_blu1.tga differ
diff --git a/assets/icons/iconf_blu2.tga b/assets/icons/iconf_blu2.tga
new file mode 100644
index 00000000..33455cf2
Binary files /dev/null and b/assets/icons/iconf_blu2.tga differ
diff --git a/assets/icons/iconf_blu3.tga b/assets/icons/iconf_blu3.tga
new file mode 100644
index 00000000..63ce1c68
Binary files /dev/null and b/assets/icons/iconf_blu3.tga differ
diff --git a/assets/icons/iconf_red1.tga b/assets/icons/iconf_red1.tga
new file mode 100644
index 00000000..8b495e55
Binary files /dev/null and b/assets/icons/iconf_red1.tga differ
diff --git a/assets/icons/iconf_red2.tga b/assets/icons/iconf_red2.tga
new file mode 100644
index 00000000..61043978
Binary files /dev/null and b/assets/icons/iconf_red2.tga differ
diff --git a/assets/icons/iconf_red3.tga b/assets/icons/iconf_red3.tga
new file mode 100644
index 00000000..6be2591b
Binary files /dev/null and b/assets/icons/iconf_red3.tga differ
diff --git a/assets/maps/big_screen.map b/assets/maps/big_screen.map
new file mode 100644
index 00000000..a268275c
--- /dev/null
+++ b/assets/maps/big_screen.map
@@ -0,0 +1,217 @@
+// entity 0
+{
+"classname" "worldspawn"
+
+// skybox brush 0
+{
+( 4232 3896 -136 ) ( 4752 3896 -136 ) ( 4232 3424 -136 ) map/lg_sky_01_dn 140 257 0 -0.507812 0.460938 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lg_sky_01_dn 257 0 0 -0.460938 0.007812 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lg_sky_01_dn 140 0 0 -0.507812 0.007812 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lg_sky_01_dn 257 0 0 -0.460938 0.007812 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lg_sky_01_dn 140 0 0 -0.507812 0.007812 0 0 0
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lg_sky_01_dn 140 257 0 -0.507812 0.460938 0 0 0
+}
+// skybox brush 1
+{
+( 4232 3896 224 ) ( 4232 3424 224 ) ( 4752 3896 224 ) map/lg_sky_01_up 140 257 0 -0.507812 0.460938 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lg_sky_01_up 257 0 0 -0.460938 0.007812 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lg_sky_01_up 140 0 0 -0.507812 0.007812 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lg_sky_01_up 257 0 0 -0.460938 0.007812 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lg_sky_01_up 140 0 0 -0.507812 0.007812 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lg_sky_01_up 140 257 0 -0.507812 0.460938 0 0 0
+}
+// skybox brush 2
+{
+( 4752 3432 232 ) ( 4232 3432 232 ) ( 4752 3432 216 ) map/lg_sky_01_bk 140 631 0 -0.507812 0.367188 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lg_sky_01_bk 29 631 0 -0.007812 0.367188 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lg_sky_01_bk 29 631 0 -0.007812 0.367188 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lg_sky_01_bk 140 631 0 -0.507812 0.367188 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lg_sky_01_bk 140 29 0 -0.507812 0.007812 0 0 0
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lg_sky_01_bk 140 29 0 -0.507812 0.007812 0 0 0
+}
+// skybox brush 3
+{
+( 4744 3896 232 ) ( 4744 3424 232 ) ( 4744 3896 216 ) map/lg_sky_01_rt 257 631 0 -0.460938 0.367188 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lg_sky_01_rt 27 631 0 -0.007812 0.367188 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lg_sky_01_rt 257 631 0 -0.460938 0.367188 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lg_sky_01_rt 27 631 0 -0.007812 0.367188 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lg_sky_01_rt 27 257 0 -0.007812 0.460938 0 0 0
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lg_sky_01_rt 27 257 0 -0.007812 0.460938 0 0 0
+}
+// skybox brush 4
+{
+( 4232 3888 232 ) ( 4752 3888 232 ) ( 4232 3888 216 ) map/lg_sky_01_ft 140 631 0 -0.507812 0.367188 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lg_sky_01_ft 29 631 0 -0.007812 0.367188 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lg_sky_01_ft 140 631 0 -0.507812 0.367188 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lg_sky_01_ft 29 631 0 -0.007812 0.367188 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lg_sky_01_ft 140 29 0 -0.507812 0.007812 0 0 0
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lg_sky_01_ft 140 29 0 -0.507812 0.007812 0 0 0
+}
+// skybox brush 5
+{
+( 4240 3424 232 ) ( 4240 3896 232 ) ( 4240 3424 216 ) map/lg_sky_01_lf 257 631 0 -0.460938 0.367188 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lg_sky_01_lf 257 631 0 -0.460938 0.367188 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lg_sky_01_lf 27 631 0 -0.007812 0.367188 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lg_sky_01_lf 27 631 0 -0.007812 0.367188 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lg_sky_01_lf 27 257 0 -0.007812 0.460938 0 0 0
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lg_sky_01_lf 27 257 0 -0.007812 0.460938 0 0 0
+}
+
+// brush 0
+{
+( 256 -520 -1024 ) ( -256 -520 -1024 ) ( -256 -1032 -1024 ) map/lg_sky_02 272 0 0 -4.000000 0.015625 0 0 0
+( -256 -1032 1024 ) ( -256 -520 1024 ) ( 256 -520 1024 ) map/lg_sky_02 272 0 0 -4.000000 0.015625 0 0 0
+( -128 -1032 8 ) ( 384 -1032 8 ) ( 384 -1032 0 ) map/lg_sky_02 272 256 0 -4.000000 4.000000 0 0 0
+( 1088 -1032 -184 ) ( 1088 -520 -184 ) ( 1088 -520 -192 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( -960 -520 -176 ) ( -960 -1032 -176 ) ( -960 -1032 -184 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( 256 -1024 8 ) ( -256 -1024 8 ) ( 256 -1024 0 ) map/lg_sky_02 272 256 0 -4.000000 4.000000 0 0 0
+}
+// brush 1
+{
+( 1088 248 -184 ) ( 1088 -264 -184 ) ( 1088 248 -192 ) map/lg_sky_02 256 256 0 -4.031250 4.000000 0 0 0
+( 1096 1032 8 ) ( 584 1032 8 ) ( 584 1032 0 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( 1096 -248 8 ) ( 1096 264 8 ) ( 1096 264 0 ) map/lg_sky_02 256 256 0 -4.031250 4.000000 0 0 0
+( 584 -1032 8 ) ( 1096 -1032 8 ) ( 1096 -1032 0 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( 584 -264 1024 ) ( 584 248 1024 ) ( 1096 248 1024 ) map/lg_sky_02 0 256 0 -0.015625 4.031250 0 0 0
+( 1096 248 -1024 ) ( 584 248 -1024 ) ( 584 -264 -1024 ) map/lg_sky_02 0 256 0 -0.015625 4.031250 0 0 0
+}
+// brush 2
+{
+( 32 0 64 ) ( -32 0 64 ) ( -32 -64 64 ) map/lab_games/lg_style_01_4tile_d.tga 0 0 0 0.031200 0.031200 0 0 0
+( -32 -64 72 ) ( -32 0 72 ) ( 32 0 72 ) map/lab_games/lg_style_01_4tile_d.tga 0 0 0 0.031200 0.031200 0 0 0
+( -32 -64 8 ) ( 32 -64 8 ) ( 32 -64 0 ) map/lab_games/lg_style_01_4tile_d.tga 0 0 0 0.031200 0.031200 0 0 0
+( 32 -64 8 ) ( 32 0 8 ) ( 32 0 0 ) map/lab_games/lg_style_01_4tile_d.tga 0 0 0 0.031200 0.031200 0 0 0
+( 32 0 8 ) ( -32 0 8 ) ( -32 0 0 ) map/lab_games/lg_style_01_4tile_d.tga 0 0 0 0.031200 0.031200 0 0 0
+( -32 0 8 ) ( -32 -64 8 ) ( -32 -64 0 ) map/lab_games/lg_style_01_4tile_d.tga 0 0 0 0.031200 0.031200 0 0 0
+}
+// brush 3
+{
+( -832 1032 8 ) ( -1344 1032 8 ) ( -832 1032 0 ) map/lg_sky_02 272 256 0 -4.000000 4.000000 0 0 0
+( 1088 1024 -176 ) ( 1088 1536 -176 ) ( 1088 1536 -184 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( -128 1024 8 ) ( 384 1024 8 ) ( 384 1024 0 ) map/lg_sky_02 272 256 0 -4.000000 4.000000 0 0 0
+( -960 1536 -184 ) ( -960 1024 -184 ) ( -960 1024 -192 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( 384 1536 1024 ) ( 384 1024 1024 ) ( -128 1024 1024 ) map/lg_sky_02 272 0 0 -4.000000 0.015625 0 0 0
+( -128 1024 -1024 ) ( 384 1024 -1024 ) ( 384 1536 -1024 ) map/lg_sky_02 272 0 0 -4.000000 0.015625 0 0 0
+}
+// brush 4
+{
+( 384 256 1032 ) ( 384 -256 1032 ) ( -128 -256 1032 ) map/lg_sky_02 271 256 0 -4.031250 4.031250 0 0 0
+( 376 1032 848 ) ( -136 1032 848 ) ( -136 1032 840 ) map/lg_sky_02 271 0 0 -4.031250 0.015625 0 0 0
+( -968 200 848 ) ( -968 -312 848 ) ( -968 -312 840 ) map/lg_sky_02 256 0 0 -4.031250 0.015625 0 0 0
+( -120 -1032 848 ) ( 392 -1032 848 ) ( 392 -1032 840 ) map/lg_sky_02 271 0 0 -4.031250 0.015625 0 0 0
+( 1096 -184 848 ) ( 1096 328 848 ) ( 1096 328 840 ) map/lg_sky_02 256 0 0 -4.031250 0.015625 0 0 0
+( 384 -256 1024 ) ( 384 256 1024 ) ( -128 -256 1024 ) map/lg_sky_02 271 256 0 -4.031250 4.031250 0 0 0
+}
+// brush 5
+{
+( -200 -200 -1024 ) ( -200 328 -1024 ) ( 328 328 -1024 ) map/lg_sky_02 271 256 0 -4.031250 4.031250 0 0 0
+( 328 328 -1032 ) ( -200 328 -1032 ) ( -200 -200 -1032 ) map/lg_sky_02 271 256 0 -4.031250 4.031250 0 0 0
+( 1096 328 -1032 ) ( 1096 -200 -1032 ) ( 1096 -200 -1024 ) map/lg_sky_02 256 0 0 -4.031250 0.015625 0 0 0
+( 264 -1032 -1032 ) ( -264 -1032 -1032 ) ( -264 -1032 -1024 ) map/lg_sky_02 271 0 0 -4.031250 0.015625 0 0 0
+( -968 -200 -1032 ) ( -968 328 -1032 ) ( -968 328 -1024 ) map/lg_sky_02 256 0 0 -4.031250 0.015625 0 0 0
+( -120 1032 -1032 ) ( 408 1032 -1032 ) ( 408 1032 -1024 ) map/lg_sky_02 271 0 0 -4.031250 0.015625 0 0 0
+}
+// brush 6
+{
+( -1472 -256 -1024 ) ( -960 -256 -1024 ) ( -960 256 -1024 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( -960 256 1024 ) ( -960 -256 1024 ) ( -1472 -256 1024 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( -960 1024 8 ) ( -1472 1024 8 ) ( -1472 1024 0 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( -1472 -1024 8 ) ( -960 -1024 8 ) ( -960 -1024 0 ) map/lg_sky_02 0 256 0 -0.015625 4.000000 0 0 0
+( -960 -1032 8 ) ( -960 -520 8 ) ( -960 -520 0 ) map/lg_sky_02 256 256 0 -4.000000 4.000000 0 0 0
+( -968 1024 -176 ) ( -968 512 -176 ) ( -968 1024 -184 ) map/lg_sky_02 256 256 0 -4.000000 4.000000 0 0 0
+}
+
+// brush 0
+{
+patchDef2
+{
+map/slot1
+( 3 3 0 0 0 )
+(
+( ( -56 58.5 67 0 1 ) ( -56 58.5 123 0 0.5 ) ( -56 58.5 179 0 0 ) )
+( ( 0 58.5 67 0.5 1 ) ( 0 58.5 123 0.5 0.5 ) ( 0 58.5 179 0.5 0 ) )
+( ( 56 58.5 67 1 1 ) ( 56 58.5 123 1 0.5 ) ( 56 58.5 179 1 0 ) )
+)
+}
+}
+}
+
+//skybox
+{
+"origin" "4464 3656 -96"
+"classname" "_skybox"
+}
+
+// skybox entity 1
+{
+"modelscale" "1.5"
+"angle" "90"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"classname" "misc_model"
+"origin" "4464 3656 -34"
+"model" "models/stadium.md3"
+}
+
+// entity 1
+{
+"classname" "trigger_lookat"
+"targetname" "big_screen"
+"target" "lua_callback"
+"origin" "0 -216 16"
+// brush 0
+{
+patchDef2
+{
+common/caulk
+( 3 3 0 0 0 )
+(
+( ( -56 58 67 0 1 ) ( -56 58 123 0 0.5 ) ( -56 58 179 0 0 ) )
+( ( 0 58 67 0.5 1 ) ( 0 58 123 0.5 0.5 ) ( 0 58 179 0.5 0 ) )
+( ( 56 58 67 1 1 ) ( 56 58 123 1 0.5 ) ( 56 58 179 1 0 ) )
+)
+}
+}
+}
+// entity 2
+{
+"classname" "target_callback"
+"targetname" "lua_callback"
+"origin" "-16 120 120"
+}
+// entity 3
+{
+"classname" "func_button"
+"wait" "-1"
+"target" "kill"
+"angle" "-2"
+"lip" "4"
+// brush 0
+{
+( 256 256 40 ) ( -256 256 40 ) ( -256 -256 40 ) map/ghost 0 0 0 0.031200 0.031200 0 12 0
+( -248 -256 48 ) ( 264 -256 48 ) ( 264 -256 40 ) map/ghost 0 3 0 0.031200 0.031200 0 12 0
+( 256 -280 48 ) ( 256 232 48 ) ( 256 232 40 ) map/ghost 0 3 0 0.031200 0.031200 0 12 0
+( 248 256 48 ) ( -264 256 48 ) ( -264 256 40 ) map/ghost 0 3 0 0.031200 0.031200 0 12 0
+( -256 264 48 ) ( -256 -248 48 ) ( -256 -248 40 ) map/ghost 0 3 0 0.031200 0.031200 0 12 0
+( -256 256 48 ) ( 256 256 48 ) ( -256 -256 48 ) map/ghost 0 0 0 0.031200 0.031200 0 12 0
+}
+}
+// entity 4
+{
+"classname" "target_kill"
+"origin" "-136 128 128"
+"targetname" "kill"
+}
+// entity 5
+{
+"classname" "info_player_start"
+"angle" "90"
+"origin" "0 -32 96"
+"spawn_orientation_segment" "40"
+}
+// entity 6
+{
+"classname" "misc_model"
+"modelscale" "1.5"
+"model" "models/fut_view_screen.md3"
+"origin" "0 64 24"
+}
diff --git a/assets/maps/concept_task_01.map b/assets/maps/concept_task_01.map
new file mode 100644
index 00000000..72f55259
--- /dev/null
+++ b/assets/maps/concept_task_01.map
@@ -0,0 +1,230 @@
+// entity 0
+{
+"classname" "worldspawn"
+"wait" "-1"
+"target" "frag_1"
+"targetShaderName" "scripts/apple6"
+"targetShaderNewName" "scripts/strawberry_invis"
+// brush 0
+{
+( -576 568 128 ) ( -576 -768 128 ) ( -576 -768 120 ) map/lab_games/fake_sky -256 0 0 0.050000 0.050000 0 0 0
+( 1024 576 128 ) ( -576 576 128 ) ( -576 576 120 ) map/lab_games/fake_sky 0 0 0 0.050000 0.050000 0 0 0
+( 1024 -768 128 ) ( 1024 568 128 ) ( 1024 568 120 ) map/lab_games/fake_sky -256 0 0 0.050000 0.050000 0 0 0
+( -576 -768 128 ) ( 1024 -768 128 ) ( 1024 -768 120 ) map/lab_games/fake_sky 0 0 0 0.050000 0.050000 0 0 0
+( -576 -768 192 ) ( -576 568 192 ) ( 1024 568 192 ) map/lab_games/fake_sky 0 256 0 0.050000 0.050000 0 0 0
+( -576 568 184 ) ( -576 -768 184 ) ( 1024 568 184 ) map/lab_games/fake_sky 0 256 0 0.050000 0.050000 0 0 0
+}
+// brush 1
+{
+( -576 568 128 ) ( -576 -768 128 ) ( -576 -768 120 ) map/lab_games/lg_style_01_floor_orange -256 0 0 0.050000 0.050000 0 0 0
+( 1024 576 128 ) ( -576 576 128 ) ( -576 576 120 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.050000 0.050000 0 0 0
+( 1024 -768 128 ) ( 1024 568 128 ) ( 1024 568 120 ) map/lab_games/lg_style_01_floor_orange -256 0 0 0.050000 0.050000 0 0 0
+( -576 -768 128 ) ( 1024 -768 128 ) ( 1024 -768 120 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.050000 0.050000 0 0 0
+( 1024 568 0 ) ( -576 568 0 ) ( -576 -768 0 ) map/lab_games/lg_style_01_floor_orange 0 256 0 0.050000 0.050000 0 0 0
+( -576 568 8 ) ( 1024 568 8 ) ( -576 -768 8 ) map/lab_games/lg_style_01_floor_orange 0 256 0 0.050000 0.050000 0 0 0
+}
+// brush 2
+{
+( -576 568 128 ) ( -576 -768 128 ) ( -576 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 576 128 ) ( -576 576 128 ) ( -576 576 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 128 ) ( 1024 -768 128 ) ( 1024 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 192 ) ( -576 568 192 ) ( 1024 568 192 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 568 0 ) ( -576 568 0 ) ( -576 -768 0 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -568 -768 128 ) ( -568 568 128 ) ( -568 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+}
+// brush 3
+{
+( -576 568 128 ) ( 1024 568 128 ) ( -576 568 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 568 0 ) ( -576 568 0 ) ( -576 -768 0 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 192 ) ( -576 568 192 ) ( 1024 568 192 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 -768 128 ) ( 1024 568 128 ) ( 1024 568 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 576 128 ) ( -576 576 128 ) ( -576 576 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 568 128 ) ( -576 -768 128 ) ( -576 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+}
+// brush 4
+{
+( 1016 568 128 ) ( 1016 -768 128 ) ( 1016 568 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 568 0 ) ( -576 568 0 ) ( -576 -768 0 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 192 ) ( -576 568 192 ) ( 1024 568 192 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 128 ) ( 1024 -768 128 ) ( 1024 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 -768 128 ) ( 1024 568 128 ) ( 1024 568 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 576 128 ) ( -576 576 128 ) ( -576 576 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+}
+// brush 5
+{
+( -576 568 128 ) ( -576 -768 128 ) ( -576 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 -768 128 ) ( 1024 568 128 ) ( 1024 568 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 128 ) ( 1024 -768 128 ) ( 1024 -768 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( -576 -768 192 ) ( -576 568 192 ) ( 1024 568 192 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 568 0 ) ( -576 568 0 ) ( -576 -768 0 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+( 1024 -760 128 ) ( -576 -760 128 ) ( 1024 -760 120 ) map/lab_games/lg_style_01_wall_green 0 47 0 0.171500 0.171500 0 0 0
+}
+}
+// entity 1
+{
+"script_id" "21"
+"origin" "192 -128 32"
+"classname" "info_player_start"
+}
+// entity 2
+{
+"script_id" "1"
+"origin" "288 -128 24"
+"classname" "reward_object"
+"angle" "225"
+}
+// entity 3
+{
+"light" "1200"
+"origin" "192 -128 152"
+"classname" "light"
+}
+// entity 4
+{
+"light" "1200"
+"origin" "-159 191 152"
+"classname" "light"
+}
+// entity 5
+{
+"light" "1200"
+"origin" "576 191 152"
+"classname" "light"
+}
+// entity 6
+{
+"light" "1200"
+"origin" "-159 -419 152"
+"classname" "light"
+}
+// entity 7
+{
+"light" "1200"
+"origin" "576 -419 152"
+"classname" "light"
+}
+// entity 8
+{
+"script_id" "2"
+"classname" "reward_object"
+"origin" "384 -240 24"
+"angle" "135"
+}
+// entity 9
+{
+"script_id" "3"
+"classname" "reward_object"
+"origin" "64 -160 24"
+}
+// entity 10
+{
+"script_id" "4"
+"classname" "reward_object"
+"origin" "48 56 24"
+}
+// entity 11
+{
+"script_id" "5"
+"classname" "reward_object"
+"origin" "368 80 24"
+"angle" "270"
+}
+// entity 12
+{
+"script_id" "6"
+"classname" "reward_object"
+"origin" "352 -24 24"
+}
+// entity 13
+{
+"script_id" "7"
+"origin" "272 64 24"
+"classname" "reward_object"
+"angle" "180"
+}
+// entity 14
+{
+"script_id" "8"
+"origin" "72 -288 24"
+"classname" "reward_object"
+"angle" "135"
+}
+// entity 15
+{
+"script_id" "9"
+"origin" "-56 -48 24"
+"classname" "reward_object"
+"angle" "180"
+}
+// entity 16
+{
+"script_id" "10"
+"origin" "288 -312 24"
+"classname" "reward_object"
+}
+// entity 17
+{
+"script_id" "11"
+"classname" "reward_object"
+"origin" "128 144 24"
+"angle" "45"
+}
+// entity 18
+{
+"script_id" "12"
+"classname" "reward_object"
+"origin" "480 -216 24"
+"angle" "315"
+}
+// entity 19
+{
+"script_id" "13"
+"classname" "reward_object"
+"origin" "-120 -224 24"
+"angle" "45"
+}
+// entity 20
+{
+"script_id" "14"
+"origin" "-64 -440 24"
+"classname" "reward_object"
+"angle" "315"
+}
+// entity 21
+{
+"script_id" "15"
+"origin" "432 104 24"
+"classname" "reward_object"
+}
+// entity 22
+{
+"script_id" "16"
+"origin" "8 -360 24"
+"classname" "reward_object"
+}
+// entity 23
+{
+"script_id" "17"
+"origin" "296 -208 24"
+"classname" "reward_object"
+}
+// entity 24
+{
+"script_id" "18"
+"origin" "392 -88 24"
+"classname" "reward_object"
+"angle" "90"
+}
+// entity 25
+{
+"script_id" "19"
+"origin" "-48 -136 24"
+"classname" "reward_object"
+"angle" "225"
+}
+// entity 26
+{
+"script_id" "20"
+"origin" "144 24 24"
+"classname" "reward_object"
+}
diff --git a/assets/maps/concept_task_03_one_room.map b/assets/maps/concept_task_03_one_room.map
new file mode 100644
index 00000000..f37863ec
--- /dev/null
+++ b/assets/maps/concept_task_03_one_room.map
@@ -0,0 +1,203 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( 192 -16 0 ) ( -128 -16 0 ) ( -128 -272 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( -128 -272 80 ) ( -128 -16 80 ) ( 192 -16 80 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( -120 -280 64 ) ( 200 -280 64 ) ( 200 -280 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+( 336 -280 88 ) ( 336 -24 88 ) ( 336 -24 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( -272 -8 88 ) ( -272 -264 88 ) ( -272 -264 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 264 -272 64 ) ( -56 -272 64 ) ( 264 -272 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+}
+// brush 1
+{
+( 336 200 0 ) ( 16 200 0 ) ( 16 -56 0 ) map/fut_flat_wall_yellow_blank_d 0 512 0 -0.000977 0.531250 0 0 0
+( 16 -56 80 ) ( 16 200 80 ) ( 336 200 80 ) map/fut_flat_wall_yellow_blank_d 0 512 0 -0.000977 0.531250 0 0 0
+( 16 -280 64 ) ( 336 -280 64 ) ( 336 -280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000977 0.078125 0 0 0
+( 344 -64 64 ) ( 344 192 64 ) ( 344 192 0 ) map/fut_flat_wall_yellow_blank_d 4096 0 0 -0.066406 0.078125 0 0 0
+( 336 280 64 ) ( 16 280 64 ) ( 16 280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000977 0.078125 0 0 0
+( 336 200 88 ) ( 336 -56 88 ) ( 336 200 24 ) map/fut_flat_wall_yellow_blank_d 4096 0 0 -0.066406 0.078125 0 0 0
+}
+// brush 2
+{
+( 64 272 0 ) ( -256 272 0 ) ( -256 16 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( -256 16 80 ) ( -256 272 80 ) ( 64 272 80 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( 336 8 88 ) ( 336 264 88 ) ( 336 264 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 96 280 64 ) ( -224 280 64 ) ( -224 280 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+( -272 280 88 ) ( -272 24 88 ) ( -272 24 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( -176 272 64 ) ( 144 272 64 ) ( -176 272 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+}
+// brush 3
+{
+( -16 128 0 ) ( -336 128 0 ) ( -336 -128 0 ) map/fut_flat_wall_yellow_blank_d 0 512 0 -0.000977 0.531250 0 0 0
+( -336 -128 80 ) ( -336 128 80 ) ( -16 128 80 ) map/fut_flat_wall_yellow_blank_d 0 512 0 -0.000977 0.531250 0 0 0
+( -328 -280 64 ) ( -8 -280 64 ) ( -8 -280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000977 0.078125 0 0 0
+( -24 280 64 ) ( -344 280 64 ) ( -344 280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000977 0.078125 0 0 0
+( -280 136 64 ) ( -280 -120 64 ) ( -280 -120 0 ) map/fut_flat_wall_yellow_blank_d 4096 0 0 -0.066406 0.078125 0 0 0
+( -272 -128 88 ) ( -272 128 88 ) ( -272 -128 24 ) map/fut_flat_wall_yellow_blank_d 4096 0 0 -0.066406 0.078125 0 0 0
+}
+// brush 4
+{
+( -192 128 0 ) ( 128 128 0 ) ( -192 -128 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( -280 264 64 ) ( -280 8 64 ) ( -280 8 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 0 280 64 ) ( -320 280 64 ) ( -320 280 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 344 -264 64 ) ( 344 -8 64 ) ( 344 -8 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( -64 -280 64 ) ( 256 -280 64 ) ( 256 -280 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 128 128 -8 ) ( -192 128 -8 ) ( -192 -128 -8 ) map/script_highlight 0 0 0 0 0 0 0 0
+}
+// brush 5
+{
+( -192 128 80 ) ( -192 -128 80 ) ( 128 128 80 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( -280 264 80 ) ( -280 8 80 ) ( -280 8 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( 0 280 80 ) ( -320 280 80 ) ( -320 280 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( 344 -264 80 ) ( 344 -8 80 ) ( 344 -8 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( -64 -280 80 ) ( 256 -280 80 ) ( 256 -280 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( -192 -128 88 ) ( -192 128 88 ) ( 128 128 88 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+}
+}
+// entity 1
+{
+"angle" "0"
+"spawn_orientation_segment" "40"
+"origin" "-232 0 24"
+"classname" "info_player_start"
+}
+// entity 2
+{
+"classname" "light"
+"origin" "-176 184 64"
+"light" "100"
+}
+// entity 3
+{
+"origin" "-240 240 16"
+"classname" "apple_reward"
+"script_id" "1"
+}
+// entity 4
+{
+"script_id" "2"
+"classname" "apple_reward"
+"origin" "-112 128 16"
+}
+// entity 5
+{
+"origin" "-112 240 16"
+"classname" "apple_reward"
+"script_id" "3"
+}
+// entity 6
+{
+"script_id" "4"
+"classname" "apple_reward"
+"origin" "-240 -128 16"
+}
+// entity 7
+{
+"script_id" "5"
+"classname" "apple_reward"
+"origin" "32 240 16"
+}
+// entity 8
+{
+"origin" "168 240 16"
+"classname" "apple_reward"
+"script_id" "6"
+}
+// entity 9
+{
+"script_id" "7"
+"classname" "apple_reward"
+"origin" "304 128 16"
+}
+// entity 10
+{
+"origin" "168 128 16"
+"classname" "apple_reward"
+"script_id" "8"
+}
+// entity 11
+{
+"script_id" "9"
+"classname" "apple_reward"
+"origin" "56 0 16"
+}
+// entity 12
+{
+"origin" "-112 0 16"
+"classname" "apple_reward"
+"script_id" "10"
+}
+// entity 13
+{
+"script_id" "11"
+"classname" "box"
+"origin" "32 -128 16"
+}
+// entity 24
+{
+"light" "100"
+"origin" "-32 184 64"
+"classname" "light"
+}
+// entity 25
+{
+"classname" "light"
+"origin" "96 184 64"
+"light" "100"
+}
+// entity 26
+{
+"light" "100"
+"origin" "248 184 64"
+"classname" "light"
+}
+// entity 27
+{
+"classname" "light"
+"origin" "248 -184 64"
+"light" "100"
+}
+// entity 28
+{
+"light" "100"
+"origin" "96 -184 64"
+"classname" "light"
+}
+// entity 29
+{
+"classname" "light"
+"origin" "-32 -184 64"
+"light" "100"
+}
+// entity 30
+{
+"light" "100"
+"origin" "-176 -184 64"
+"classname" "light"
+}
+// entity 31
+{
+"classname" "light"
+"origin" "-176 0 64"
+"light" "100"
+}
+// entity 32
+{
+"light" "100"
+"origin" "-32 0 64"
+"classname" "light"
+}
+// entity 33
+{
+"classname" "light"
+"origin" "96 0 64"
+"light" "100"
+}
+// entity 34
+{
+"light" "100"
+"origin" "248 0 64"
+"classname" "light"
+}
diff --git a/assets/maps/concept_task_03_two_rooms.map b/assets/maps/concept_task_03_two_rooms.map
new file mode 100644
index 00000000..18eb4613
--- /dev/null
+++ b/assets/maps/concept_task_03_two_rooms.map
@@ -0,0 +1,455 @@
+// entity 0
+{
+"target" "reward_door"
+"classname" "worldspawn"
+// brush 0
+{
+( 192 -16 0 ) ( -128 -16 0 ) ( -128 -272 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( -128 -272 80 ) ( -128 -16 80 ) ( 192 -16 80 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( -120 -280 64 ) ( 200 -280 64 ) ( 200 -280 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+( 336 -280 88 ) ( 336 -24 88 ) ( 336 -24 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( -272 -8 88 ) ( -272 -264 88 ) ( -272 -264 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 264 -272 64 ) ( -56 -272 64 ) ( 264 -272 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+}
+// brush 1
+{
+( 336 200 0 ) ( 16 200 0 ) ( 16 -56 0 ) map/fut_flat_wall_yellow_blank_d 0 315 0 -0.002604 0.203125 0 0 0
+( 16 -56 80 ) ( 16 200 80 ) ( 336 200 80 ) map/fut_flat_wall_yellow_blank_d 0 315 0 -0.002604 0.203125 0 0 0
+( 32 64 64 ) ( 352 64 64 ) ( 352 64 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.002604 0.078125 0 0 0
+( 344 -72 64 ) ( 344 184 64 ) ( 344 184 0 ) map/fut_flat_wall_yellow_blank_d 945 0 0 -0.067708 0.078125 0 0 0
+( 336 280 64 ) ( 16 280 64 ) ( 16 280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.002604 0.078125 0 0 0
+( 336 200 88 ) ( 336 -56 88 ) ( 336 200 24 ) map/fut_flat_wall_yellow_blank_d 945 0 0 -0.067708 0.078125 0 0 0
+}
+// brush 2
+{
+( 64 272 0 ) ( -256 272 0 ) ( -256 16 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( -256 16 80 ) ( -256 272 80 ) ( 64 272 80 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.007812 0 0 0
+( 336 8 88 ) ( 336 264 88 ) ( 336 264 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 96 280 64 ) ( -224 280 64 ) ( -224 280 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+( -272 280 88 ) ( -272 24 88 ) ( -272 24 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( -176 272 64 ) ( 144 272 64 ) ( -176 272 0 ) map/fut_flat_wall_yellow_blank_d 5093 0 0 -0.065972 0.078125 0 0 0
+}
+// brush 3
+{
+( -16 128 0 ) ( -336 128 0 ) ( -336 -128 0 ) map/fut_flat_wall_yellow_blank_d 0 512 0 -0.000977 0.531250 0 0 0
+( -336 -128 80 ) ( -336 128 80 ) ( -16 128 80 ) map/fut_flat_wall_yellow_blank_d 0 512 0 -0.000977 0.531250 0 0 0
+( -328 -280 64 ) ( -8 -280 64 ) ( -8 -280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000977 0.078125 0 0 0
+( -24 280 64 ) ( -344 280 64 ) ( -344 280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000977 0.078125 0 0 0
+( -280 136 64 ) ( -280 -120 64 ) ( -280 -120 0 ) map/fut_flat_wall_yellow_blank_d 4096 0 0 -0.066406 0.078125 0 0 0
+( -272 -128 88 ) ( -272 128 88 ) ( -272 -128 24 ) map/fut_flat_wall_yellow_blank_d 4096 0 0 -0.066406 0.078125 0 0 0
+}
+// brush 4
+{
+( -192 128 0 ) ( 128 128 0 ) ( -192 -128 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( -280 264 64 ) ( -280 8 64 ) ( -280 8 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 0 280 64 ) ( -320 280 64 ) ( -320 280 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 344 -264 64 ) ( 344 -8 64 ) ( 344 -8 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( -64 -280 64 ) ( 256 -280 64 ) ( 256 -280 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 128 128 -8 ) ( -192 128 -8 ) ( -192 -128 -8 ) map/script_highlight 0 0 0 0 0 0 0 0
+}
+// brush 5
+{
+( -192 128 80 ) ( -192 -128 80 ) ( 128 128 80 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( -280 264 80 ) ( -280 8 80 ) ( -280 8 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( 0 280 80 ) ( -320 280 80 ) ( -320 280 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( 344 -264 80 ) ( 344 -8 80 ) ( 344 -8 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( -64 -280 80 ) ( 256 -280 80 ) ( 256 -280 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( -192 -128 88 ) ( -192 128 88 ) ( 128 128 88 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+}
+// brush 6
+{
+( 336 -136 88 ) ( 336 -392 88 ) ( 336 -136 24 ) map/fut_flat_wall_yellow_blank_d 2126 0 0 -0.067708 0.078125 0 0 0
+( 336 -64 64 ) ( 16 -64 64 ) ( 16 -64 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.002604 0.078125 0 0 0
+( 344 -408 64 ) ( 344 -152 64 ) ( 344 -152 0 ) map/fut_flat_wall_yellow_blank_d 2126 0 0 -0.067708 0.078125 0 0 0
+( 32 -280 64 ) ( 352 -280 64 ) ( 352 -280 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.002604 0.078125 0 0 0
+( 16 -400 80 ) ( 16 -144 80 ) ( 336 -144 80 ) map/fut_flat_wall_yellow_blank_d 0 708 0 -0.002604 0.203125 0 0 0
+( 336 -144 0 ) ( 16 -144 0 ) ( 16 -400 0 ) map/fut_flat_wall_yellow_blank_d 0 708 0 -0.002604 0.203125 0 0 0
+}
+// brush 7
+{
+( 352 32 0 ) ( 896 32 0 ) ( 352 -64 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 344 32 1792 ) ( 344 -64 1792 ) ( 344 -64 64 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 64 64 1792 ) ( -480 64 1792 ) ( -480 64 64 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 448 -64 1792 ) ( 448 32 1792 ) ( 448 32 64 ) map/script_highlight 0 0 0 0 0 0 0 0
+( -480 -64 1792 ) ( 64 -64 1792 ) ( 64 -64 64 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 896 32 -8 ) ( 352 32 -8 ) ( 352 -64 -8 ) map/script_highlight 0 0 0 0 0 0 0 0
+}
+// brush 8
+{
+( 352 32 80 ) ( 352 -64 80 ) ( 896 32 80 ) map/fut_ceiling_tile_02_d 0 0 0 0.031200 0.031200 0 0 0
+( 344 32 1792 ) ( 344 -64 1792 ) ( 344 -64 64 ) map/fut_ceiling_tile_02_d 0 0 0 0.031200 0.031200 0 0 0
+( 64 64 1792 ) ( -480 64 1792 ) ( -480 64 64 ) map/fut_ceiling_tile_02_d 0 0 0 0.031200 0.031200 0 0 0
+( 448 -64 1792 ) ( 448 32 1792 ) ( 448 32 64 ) map/fut_ceiling_tile_02_d 0 0 0 0.031200 0.031200 0 0 0
+( -480 -64 1792 ) ( 64 -64 1792 ) ( 64 -64 64 ) map/fut_ceiling_tile_02_d 0 0 0 0.031200 0.031200 0 0 0
+( 352 -64 88 ) ( 352 32 88 ) ( 896 32 88 ) map/fut_ceiling_tile_02_d 0 0 0 0.031200 0.031200 0 0 0
+}
+// brush 9
+{
+( -488 64 1792 ) ( 56 64 1792 ) ( -488 64 64 ) map/fut_flat_wall_yellow_blank_d 662 0 0 -0.058105 0.078125 0 0 0
+( 344 40 1792 ) ( 344 -56 1792 ) ( 344 -56 64 ) map/fut_flat_wall_yellow_blank_d 134 0 0 -0.055176 0.078125 0 0 0
+( 976 72 1792 ) ( 432 72 1792 ) ( 432 72 64 ) map/fut_flat_wall_yellow_blank_d 662 0 0 -0.058105 0.078125 0 0 0
+( 448 -56 1792 ) ( 448 40 1792 ) ( 448 40 64 ) map/fut_flat_wall_yellow_blank_d 134 0 0 -0.055176 0.078125 0 0 0
+( 352 -56 80 ) ( 352 40 80 ) ( 896 40 80 ) map/fut_flat_wall_yellow_blank_d 662 72 0 -0.058105 0.882812 0 0 0
+( 896 40 0 ) ( 352 40 0 ) ( 352 -56 0 ) map/fut_flat_wall_yellow_blank_d 662 72 0 -0.058105 0.882812 0 0 0
+}
+// brush 10
+{
+( 896 -96 0 ) ( 352 -96 0 ) ( 352 -192 0 ) map/fut_flat_wall_yellow_blank_d 662 -81 0 -0.058105 0.882812 0 0 0
+( 352 -192 80 ) ( 352 -96 80 ) ( 896 -96 80 ) map/fut_flat_wall_yellow_blank_d 662 -81 0 -0.058105 0.882812 0 0 0
+( 448 -192 1792 ) ( 448 -96 1792 ) ( 448 -96 64 ) map/fut_flat_wall_yellow_blank_d -280 0 0 -0.055176 0.078125 0 0 0
+( 128 -64 1792 ) ( -416 -64 1792 ) ( -416 -64 64 ) map/fut_flat_wall_yellow_blank_d 662 0 0 -0.058105 0.078125 0 0 0
+( 344 -96 1792 ) ( 344 -192 1792 ) ( 344 -192 64 ) map/fut_flat_wall_yellow_blank_d -280 0 0 -0.055176 0.078125 0 0 0
+( 352 -72 1792 ) ( 896 -72 1792 ) ( 352 -72 64 ) map/fut_flat_wall_yellow_blank_d 662 0 0 -0.058105 0.078125 0 0 0
+}
+// brush 11
+{
+( 712 -216 0 ) ( 392 -216 0 ) ( 392 -472 0 ) map/fut_flat_wall_yellow_blank_d 1003 708 0 -0.002604 0.203125 0 0 0
+( 392 -472 80 ) ( 392 -216 80 ) ( 712 -216 80 ) map/fut_flat_wall_yellow_blank_d 1003 708 0 -0.002604 0.203125 0 0 0
+( 400 -280 64 ) ( 720 -280 64 ) ( 720 -280 0 ) map/fut_flat_wall_yellow_blank_d 1003 0 0 -0.002604 0.078125 0 0 0
+( 704 -64 64 ) ( 384 -64 64 ) ( 384 -64 0 ) map/fut_flat_wall_yellow_blank_d 1003 0 0 -0.002604 0.078125 0 0 0
+( 448 -216 64 ) ( 448 -472 64 ) ( 448 -472 0 ) map/fut_flat_wall_yellow_blank_d 77 0 0 -0.067708 0.078125 0 0 0
+( 456 -456 88 ) ( 456 -200 88 ) ( 456 -456 24 ) map/fut_flat_wall_yellow_blank_d 77 0 0 -0.067708 0.078125 0 0 0
+}
+// brush 12
+{
+( 536 -128 88 ) ( 536 128 88 ) ( 856 128 88 ) map/fut_ceiling_tile_02_d -768 0 0 0.031250 0.031250 0 0 0
+( 664 -280 80 ) ( 984 -280 80 ) ( 984 -280 16 ) map/fut_ceiling_tile_02_d -768 0 0 0.031250 0.031250 0 0 0
+( 1072 -264 80 ) ( 1072 -8 80 ) ( 1072 -8 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( 728 280 80 ) ( 408 280 80 ) ( 408 280 16 ) map/fut_ceiling_tile_02_d -768 0 0 0.031250 0.031250 0 0 0
+( 448 264 80 ) ( 448 8 80 ) ( 448 8 16 ) map/fut_ceiling_tile_02_d 0 0 0 0.031250 0.031250 0 0 0
+( 536 128 80 ) ( 536 -128 80 ) ( 856 128 80 ) map/fut_ceiling_tile_02_d -768 0 0 0.031250 0.031250 0 0 0
+}
+// brush 13
+{
+( 456 -120 88 ) ( 456 136 88 ) ( 456 -120 24 ) map/fut_flat_wall_yellow_blank_d -79 0 0 -0.067708 0.078125 0 0 0
+( 448 136 64 ) ( 448 -120 64 ) ( 448 -120 0 ) map/fut_flat_wall_yellow_blank_d -79 0 0 -0.067708 0.078125 0 0 0
+( 704 280 64 ) ( 384 280 64 ) ( 384 280 0 ) map/fut_flat_wall_yellow_blank_d 1003 0 0 -0.002604 0.078125 0 0 0
+( 400 64 64 ) ( 720 64 64 ) ( 720 64 0 ) map/fut_flat_wall_yellow_blank_d 1003 0 0 -0.002604 0.078125 0 0 0
+( 392 -128 80 ) ( 392 128 80 ) ( 712 128 80 ) map/fut_flat_wall_yellow_blank_d 1003 315 0 -0.002604 0.203125 0 0 0
+( 712 128 0 ) ( 392 128 0 ) ( 392 -128 0 ) map/fut_flat_wall_yellow_blank_d 1003 315 0 -0.002604 0.203125 0 0 0
+}
+// brush 14
+{
+( 856 128 -8 ) ( 536 128 -8 ) ( 536 -128 -8 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 664 -280 64 ) ( 984 -280 64 ) ( 984 -280 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 1072 -264 64 ) ( 1072 -8 64 ) ( 1072 -8 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 728 280 64 ) ( 408 280 64 ) ( 408 280 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 448 264 64 ) ( 448 8 64 ) ( 448 8 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+( 536 128 0 ) ( 856 128 0 ) ( 536 -128 0 ) map/script_highlight 0 0 0 0 0 0 0 0
+}
+// brush 15
+{
+( 552 272 64 ) ( 872 272 64 ) ( 552 272 0 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.078125 0 0 0
+( 456 280 88 ) ( 456 24 88 ) ( 456 24 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 824 280 64 ) ( 504 280 64 ) ( 504 280 0 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.078125 0 0 0
+( 1064 8 88 ) ( 1064 264 88 ) ( 1064 264 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 472 16 80 ) ( 472 272 80 ) ( 792 272 80 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.007812 0 0 0
+( 792 272 0 ) ( 472 272 0 ) ( 472 16 0 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.007812 0 0 0
+}
+// brush 16
+{
+( 992 -272 64 ) ( 672 -272 64 ) ( 992 -272 0 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.078125 0 0 0
+( 456 -8 88 ) ( 456 -264 88 ) ( 456 -264 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 1064 -280 88 ) ( 1064 -24 88 ) ( 1064 -24 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.000868 0.078125 0 0 0
+( 608 -280 64 ) ( 928 -280 64 ) ( 928 -280 0 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.078125 0 0 0
+( 600 -272 80 ) ( 600 -16 80 ) ( 920 -16 80 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.007812 0 0 0
+( 920 -16 0 ) ( 600 -16 0 ) ( 600 -272 0 ) map/fut_flat_wall_yellow_blank_d -256 0 0 -0.065972 0.007812 0 0 0
+}
+// brush 17
+{
+( 1064 200 88 ) ( 1064 -56 88 ) ( 1064 200 24 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.066406 0.078125 0 0 0
+( 1064 280 64 ) ( 744 280 64 ) ( 744 280 0 ) map/fut_flat_wall_yellow_blank_d 689 0 0 -0.000977 0.078125 0 0 0
+( 1072 -64 64 ) ( 1072 192 64 ) ( 1072 192 0 ) map/fut_flat_wall_yellow_blank_d 0 0 0 -0.066406 0.078125 0 0 0
+( 744 -280 64 ) ( 1064 -280 64 ) ( 1064 -280 0 ) map/fut_flat_wall_yellow_blank_d 689 0 0 -0.000977 0.078125 0 0 0
+( 744 -56 80 ) ( 744 200 80 ) ( 1064 200 80 ) map/fut_flat_wall_yellow_blank_d 689 512 0 -0.000977 0.531250 0 0 0
+( 1064 200 0 ) ( 744 200 0 ) ( 744 -56 0 ) map/fut_flat_wall_yellow_blank_d 689 512 0 -0.000977 0.531250 0 0 0
+}
+}
+// entity 1
+{
+"angle" "0"
+"spawn_orientation_segment" "40"
+"origin" "-232 0 24"
+"classname" "info_player_start"
+}
+// entity 2
+{
+"classname" "light"
+"origin" "-176 184 64"
+"light" "100"
+}
+// entity 3
+{
+"origin" "-240 240 16"
+"classname" "apple_reward"
+"script_id" "1"
+}
+// entity 4
+{
+"script_id" "2"
+"classname" "apple_reward"
+"origin" "-112 128 16"
+}
+// entity 5
+{
+"origin" "-112 240 16"
+"classname" "apple_reward"
+"script_id" "3"
+}
+// entity 6
+{
+"script_id" "4"
+"classname" "apple_reward"
+"origin" "-240 -128 16"
+}
+// entity 7
+{
+"script_id" "5"
+"classname" "apple_reward"
+"origin" "32 240 16"
+}
+// entity 8
+{
+"origin" "168 240 16"
+"classname" "apple_reward"
+"script_id" "6"
+}
+// entity 9
+{
+"script_id" "7"
+"classname" "apple_reward"
+"origin" "304 128 16"
+}
+// entity 10
+{
+"origin" "168 128 16"
+"classname" "apple_reward"
+"script_id" "8"
+}
+// entity 11
+{
+"script_id" "9"
+"classname" "apple_reward"
+"origin" "56 0 16"
+}
+// entity 12
+{
+"origin" "-112 0 16"
+"classname" "apple_reward"
+"script_id" "10"
+}
+// entity 13
+{
+"script_id" "1"
+"classname" "cake"
+"origin" "32 -128 16"
+}
+// entity 14
+{
+"origin" "168 -128 16"
+"classname" "cake"
+"script_id" "2"
+}
+// entity 15
+{
+"script_id" "3"
+"classname" "cake"
+"origin" "-112 -128 16"
+}
+// entity 16
+{
+"origin" "-240 -240 16"
+"classname" "cake"
+"script_id" "4"
+}
+// entity 17
+{
+"script_id" "5"
+"classname" "cake"
+"origin" "32 -240 15"
+}
+// entity 18
+{
+"origin" "-112 -240 16"
+"classname" "cake"
+"script_id" "6"
+}
+// entity 19
+{
+"script_id" "7"
+"classname" "cake"
+"origin" "168 -240 16"
+}
+// entity 20
+{
+"origin" "304 -128 16"
+"classname" "cake"
+"script_id" "8"
+}
+// entity 21
+{
+"script_id" "9"
+"classname" "cake"
+"origin" "-240 128 16"
+}
+// entity 22
+{
+"script_id" "10"
+"classname" "cake"
+"origin" "32 128 16"
+}
+// entity 23
+{
+"light" "100"
+"origin" "-32 184 64"
+"classname" "light"
+}
+// entity 24
+{
+"classname" "light"
+"origin" "96 184 64"
+"light" "100"
+}
+// entity 25
+{
+"light" "100"
+"origin" "248 184 64"
+"classname" "light"
+}
+// entity 26
+{
+"classname" "light"
+"origin" "248 -184 64"
+"light" "100"
+}
+// entity 27
+{
+"light" "100"
+"origin" "96 -184 64"
+"classname" "light"
+}
+// entity 28
+{
+"classname" "light"
+"origin" "-32 -184 64"
+"light" "100"
+}
+// entity 29
+{
+"light" "100"
+"origin" "-176 -184 64"
+"classname" "light"
+}
+// entity 30
+{
+"classname" "light"
+"origin" "-176 0 64"
+"light" "100"
+}
+// entity 31
+{
+"light" "100"
+"origin" "-32 0 64"
+"classname" "light"
+}
+// entity 32
+{
+"classname" "light"
+"origin" "96 0 64"
+"light" "100"
+}
+// entity 33
+{
+"light" "100"
+"origin" "248 0 64"
+"classname" "light"
+}
+// entity 46
+{
+"origin" "760 128 16"
+"classname" "box"
+"script_id" "11"
+"target" "box_door"
+}
+// entity 47
+{
+"light" "100"
+"origin" "824 184 64"
+"classname" "light"
+}
+// entity 48
+{
+"classname" "light"
+"origin" "976 184 64"
+"light" "100"
+}
+// entity 49
+{
+"light" "100"
+"origin" "824 0 64"
+"classname" "light"
+}
+// entity 50
+{
+"classname" "light"
+"origin" "976 0 64"
+"light" "100"
+}
+// entity 56
+{
+"light" "100"
+"origin" "440 0 64"
+"classname" "light"
+}
+// entity 57
+{
+"angle" "-1"
+"classname" "func_door"
+"wait" "3600"
+"targetname" "box_door"
+// brush 0
+{
+( 344 64 48 ) ( 336 64 48 ) ( 336 64 16 ) map/fut_utility_panel_01_d -515 0 0 0.031189 0.031197 0 0 0
+( 344 -64 48 ) ( 344 64 48 ) ( 344 64 16 ) map/fut_utility_panel_01_d 1002 0 -180 0.031250 -0.031197 0 0 0
+( 336 -64 48 ) ( 344 -64 48 ) ( 344 -64 16 ) map/fut_utility_panel_01_d -515 0 0 0.031189 0.031197 0 0 0
+( 336 64 48 ) ( 336 -64 48 ) ( 336 -64 16 ) map/fut_utility_panel_01_d 1002 0 -180 0.031250 -0.031197 0 0 0
+( 336 64 80 ) ( 344 64 80 ) ( 344 -64 80 ) map/fut_utility_panel_01_d 1002 516 -90 0.031250 0.031219 0 0 0
+( 344 -64 -8 ) ( 344 64 -8 ) ( 336 64 -8 ) map/fut_utility_panel_01_d 1002 516 -90 0.031250 0.031219 0 0 0
+}
+}
+// entity 79
+{
+"light" "100"
+"origin" "696 0 64"
+"classname" "light"
+}
+// entity 82
+{
+"light" "100"
+"origin" "552 -184 64"
+"classname" "light"
+}
+// entity 83
+{
+"classname" "light"
+"origin" "696 -184 64"
+"light" "100"
+}
+// entity 87
+{
+"classname" "light"
+"origin" "552 0 64"
+"light" "100"
+}
+// entity 88
+{
+"light" "100"
+"origin" "696 184 64"
+"classname" "light"
+}
+// entity 92
+{
+"classname" "light"
+"origin" "552 184 64"
+"light" "100"
+}
diff --git a/assets/maps/ctf_bounce.map b/assets/maps/ctf_bounce.map
new file mode 100644
index 00000000..b7e67835
--- /dev/null
+++ b/assets/maps/ctf_bounce.map
@@ -0,0 +1,1197 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lab_games/sky/lg_sky_02_dn 140 257 0 -0.507812 0.460938 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lab_games/sky/lg_sky_02_dn 140 0 0 -0.507812 0.007812 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lab_games/sky/lg_sky_02_dn 257 0 0 -0.460938 0.007812 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lab_games/sky/lg_sky_02_dn 140 0 0 -0.507812 0.007812 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lab_games/sky/lg_sky_02_dn 257 0 0 -0.460938 0.007812 0 0 0
+( 4232 3896 -136 ) ( 4752 3896 -136 ) ( 4232 3424 -136 ) map/lab_games/sky/lg_sky_02_dn 140 257 0 -0.507812 0.460938 0 0 0
+}
+// brush 1
+{
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lab_games/sky/lg_sky_02_up 140 257 0 -0.507812 0.460938 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lab_games/sky/lg_sky_02_up 140 0 0 -0.507812 0.007812 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lab_games/sky/lg_sky_02_up 257 0 0 -0.460938 0.007812 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lab_games/sky/lg_sky_02_up 140 0 0 -0.507812 0.007812 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lab_games/sky/lg_sky_02_up 257 0 0 -0.460938 0.007812 0 0 0
+( 4232 3896 224 ) ( 4232 3424 224 ) ( 4752 3896 224 ) map/lab_games/sky/lg_sky_02_up 140 257 0 -0.507812 0.460938 0 0 0
+}
+// brush 2
+{
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lab_games/sky/lg_sky_02_bk 140 29 0 -0.507812 0.007812 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lab_games/sky/lg_sky_02_bk 140 29 0 -0.507812 0.007812 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lab_games/sky/lg_sky_02_bk 140 631 0 -0.507812 0.367188 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lab_games/sky/lg_sky_02_bk 29 631 0 -0.007812 0.367188 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lab_games/sky/lg_sky_02_bk 29 631 0 -0.007812 0.367188 0 0 0
+( 4752 3432 232 ) ( 4232 3432 232 ) ( 4752 3432 216 ) map/lab_games/sky/lg_sky_02_bk 140 631 0 -0.507812 0.367188 0 0 0
+}
+// brush 3
+{
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lab_games/sky/lg_sky_02_rt 27 257 0 -0.007812 0.460938 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lab_games/sky/lg_sky_02_rt 27 257 0 -0.007812 0.460938 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lab_games/sky/lg_sky_02_rt 27 631 0 -0.007812 0.367188 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lab_games/sky/lg_sky_02_rt 257 631 0 -0.460938 0.367188 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lab_games/sky/lg_sky_02_rt 27 631 0 -0.007812 0.367188 0 0 0
+( 4744 3896 232 ) ( 4744 3424 232 ) ( 4744 3896 216 ) map/lab_games/sky/lg_sky_02_rt 257 631 0 -0.460938 0.367188 0 0 0
+}
+// brush 4
+{
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lab_games/sky/lg_sky_02_ft 140 29 0 -0.507812 0.007812 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lab_games/sky/lg_sky_02_ft 140 29 0 -0.507812 0.007812 0 0 0
+( 4752 3424 232 ) ( 4752 3896 232 ) ( 4752 3896 216 ) map/lab_games/sky/lg_sky_02_ft 29 631 0 -0.007812 0.367188 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lab_games/sky/lg_sky_02_ft 140 631 0 -0.507812 0.367188 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lab_games/sky/lg_sky_02_ft 29 631 0 -0.007812 0.367188 0 0 0
+( 4232 3888 232 ) ( 4752 3888 232 ) ( 4232 3888 216 ) map/lab_games/sky/lg_sky_02_ft 140 631 0 -0.507812 0.367188 0 0 0
+}
+// brush 5
+{
+( 4752 3896 -144 ) ( 4232 3896 -144 ) ( 4232 3424 -144 ) map/lab_games/sky/lg_sky_02_lf 27 257 0 -0.007812 0.460938 0 0 0
+( 4232 3424 232 ) ( 4232 3896 232 ) ( 4752 3896 232 ) map/lab_games/sky/lg_sky_02_lf 27 257 0 -0.007812 0.460938 0 0 0
+( 4232 3424 232 ) ( 4752 3424 232 ) ( 4752 3424 216 ) map/lab_games/sky/lg_sky_02_lf 27 631 0 -0.007812 0.367188 0 0 0
+( 4752 3896 232 ) ( 4232 3896 232 ) ( 4232 3896 216 ) map/lab_games/sky/lg_sky_02_lf 27 631 0 -0.007812 0.367188 0 0 0
+( 4232 3896 232 ) ( 4232 3424 232 ) ( 4232 3424 216 ) map/lab_games/sky/lg_sky_02_lf 257 631 0 -0.460938 0.367188 0 0 0
+( 4240 3424 232 ) ( 4240 3896 232 ) ( 4240 3424 216 ) map/lab_games/sky/lg_sky_02_lf 257 631 0 -0.460938 0.367188 0 0 0
+}
+// brush 6
+{
+( 3328 3072 -1536 ) ( -2816 3072 -1536 ) ( -2816 -1792 -1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -3072 -3072 256 ) ( 3072 -3072 256 ) ( 3072 -3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 -1792 256 ) ( 3328 3072 256 ) ( 3328 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 3072 256 ) ( -2816 3072 256 ) ( -2816 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3072 256 ) ( -2816 -1792 256 ) ( -2816 -1792 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3072 -1528 ) ( 3328 3072 -1528 ) ( -2816 -1792 -1528 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 7
+{
+( 3328 3072 -1536 ) ( -2816 3072 -1536 ) ( -2816 -1792 -1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 -1792 1536 ) ( -2816 3072 1536 ) ( 3328 3072 1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -3072 -3072 256 ) ( 3072 -3072 256 ) ( 3072 -3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 -1792 256 ) ( 3328 3072 256 ) ( 3328 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3072 256 ) ( -2816 -1792 256 ) ( -2816 -1792 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3072 -3064 256 ) ( -3072 -3064 256 ) ( 3072 -3064 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 8
+{
+( 3328 3072 -1536 ) ( -2816 3072 -1536 ) ( -2816 -1792 -1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 -1792 1536 ) ( -2816 3072 1536 ) ( 3328 3072 1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -3072 -3072 256 ) ( 3072 -3072 256 ) ( 3072 -3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 -1792 256 ) ( 3328 3072 256 ) ( 3328 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 3072 256 ) ( -2816 3072 256 ) ( -2816 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3320 3072 256 ) ( 3320 -1792 256 ) ( 3320 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 9
+{
+( 3328 3072 -1536 ) ( -2816 3072 -1536 ) ( -2816 -1792 -1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 -1792 1536 ) ( -2816 3072 1536 ) ( 3328 3072 1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 -1792 256 ) ( 3328 3072 256 ) ( 3328 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 3072 256 ) ( -2816 3072 256 ) ( -2816 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3072 256 ) ( -2816 -1792 256 ) ( -2816 -1792 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3064 256 ) ( 3328 3064 256 ) ( -2816 3064 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 10
+{
+( 3328 3072 -1536 ) ( -2816 3072 -1536 ) ( -2816 -1792 -1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 -1792 1536 ) ( -2816 3072 1536 ) ( 3328 3072 1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -3072 -3072 256 ) ( 3072 -3072 256 ) ( 3072 -3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 3072 256 ) ( -2816 3072 256 ) ( -2816 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3072 256 ) ( -2816 -1792 256 ) ( -2816 -1792 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2808 -1792 256 ) ( -2808 3072 256 ) ( -2808 -1792 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 11
+{
+( 1280 -512 224 ) ( -1152 -512 224 ) ( -1152 -1536 224 ) map/lab_games/lg_style_01_floor_blue_team_d 0 -80 0 0.190000 0.190000 0 0 0
+( -1152 -1536 256 ) ( -1152 -512 256 ) ( 1280 -512 256 ) map/lab_games/lg_style_01_floor_blue_team_d 0 -80 0 0.190000 0.190000 0 0 0
+( -1168 -1344 1536 ) ( 1264 -1344 1536 ) ( 1264 -1344 -1536 ) map/lab_games/lg_style_01_floor_blue_team_d 0 0 0 0.190000 0.190000 0 0 0
+( 448 -1552 1536 ) ( 448 -528 1536 ) ( 448 -528 -1536 ) map/lab_games/lg_style_01_floor_blue_team_d 80 0 0 0.190000 0.190000 0 0 0
+( 1280 -896 1536 ) ( -1152 -896 1536 ) ( -1152 -896 -1536 ) map/lab_games/lg_style_01_floor_blue_team_d 0 0 0 0.190000 0.190000 0 0 0
+( -448 -512 1536 ) ( -448 -1536 1536 ) ( -448 -1536 -1536 ) map/lab_games/lg_style_01_floor_blue_team_d 80 0 0 0.190000 0.190000 0 0 0
+}
+// brush 12
+{
+( -448 1728 1536 ) ( -448 704 1536 ) ( -448 704 -1536 ) map/lab_games/lg_style_01_floor_red_team_d -445 0 0 0.190000 0.190000 0 0 0
+( 1280 1344 1536 ) ( -1152 1344 1536 ) ( -1152 1344 -1536 ) map/lab_games/lg_style_01_floor_red_team_d 0 0 0 0.190000 0.190000 0 0 0
+( 448 688 1536 ) ( 448 1712 1536 ) ( 448 1712 -1536 ) map/lab_games/lg_style_01_floor_red_team_d -445 0 0 0.190000 0.190000 0 0 0
+( -1168 896 1536 ) ( 1264 896 1536 ) ( 1264 896 -1536 ) map/lab_games/lg_style_01_floor_red_team_d 0 0 0 0.190000 0.190000 0 0 0
+( -1152 704 256 ) ( -1152 1728 256 ) ( 1280 1728 256 ) map/lab_games/lg_style_01_floor_red_team_d 0 445 0 0.190000 0.190000 0 0 0
+( 1280 1728 224 ) ( -1152 1728 224 ) ( -1152 704 224 ) map/lab_games/lg_style_01_floor_red_team_d 0 445 0 0.190000 0.190000 0 0 0
+}
+// brush 13
+{
+( 1280 1152 -480 ) ( -1152 1152 -480 ) ( -1152 128 -480 ) map/lab_games/lg_style_01_floor_orange 0 485 0 0.190000 0.190000 0 0 0
+( -1152 128 -448 ) ( -1152 1152 -448 ) ( 1280 1152 -448 ) map/lab_games/lg_style_01_floor_orange 0 485 0 0.190000 0.190000 0 0 0
+( -1168 -256 832 ) ( 1264 -256 832 ) ( 1264 -256 -2240 ) map/lab_games/lg_style_01_floor_orange 0 -633 0 0.190000 0.190000 0 0 0
+( 256 80 832 ) ( 256 1104 832 ) ( 256 1104 -2240 ) map/lab_games/lg_style_01_floor_orange -485 -633 0 0.190000 0.190000 0 0 0
+( 1216 256 832 ) ( -1216 256 832 ) ( -1216 256 -2240 ) map/lab_games/lg_style_01_floor_orange 0 -633 0 0.190000 0.190000 0 0 0
+( -256 1136 832 ) ( -256 112 832 ) ( -256 112 -2240 ) map/lab_games/lg_style_01_floor_orange -485 -633 0 0.190000 0.190000 0 0 0
+}
+// brush 14
+{
+( 640 512 -64 ) ( 640 -576 -64 ) ( 640 -576 -96 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 1152 576 -64 ) ( 640 576 -64 ) ( 640 576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 1152 -576 -64 ) ( 1152 512 -64 ) ( 1152 512 -96 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 640 -576 -64 ) ( 1152 -576 -64 ) ( 1152 -576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 640 -576 -64 ) ( 640 512 -64 ) ( 1152 512 -64 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 1152 512 -96 ) ( 640 512 -96 ) ( 640 -576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+}
+// brush 15
+{
+( -128 1136 2304 ) ( -128 112 2304 ) ( -128 112 -768 ) map/lab_games/lg_style_01_floor_orange -485 970 0 0.190000 0.190000 0 0 0
+( 1216 128 2304 ) ( -1216 128 2304 ) ( -1216 128 -768 ) map/lab_games/lg_style_01_floor_orange 0 970 0 0.190000 0.190000 0 0 0
+( 128 80 2304 ) ( 128 1104 2304 ) ( 128 1104 -768 ) map/lab_games/lg_style_01_floor_orange -485 970 0 0.190000 0.190000 0 0 0
+( -1152 -128 2304 ) ( 1280 -128 2304 ) ( 1280 -128 -768 ) map/lab_games/lg_style_01_floor_orange 0 970 0 0.190000 0.190000 0 0 0
+( -1152 128 1024 ) ( -1152 1152 1024 ) ( 1280 1152 1024 ) map/lab_games/lg_style_01_floor_orange 0 485 0 0.190000 0.190000 0 0 0
+( 1280 1152 992 ) ( -1152 1152 992 ) ( -1152 128 992 ) map/lab_games/lg_style_01_floor_orange 0 485 0 0.190000 0.190000 0 0 0
+}
+// brush 16
+{
+( 1304 1688 -2032 ) ( -1712 1688 -2032 ) ( -1712 -1136 -2032 ) map/nodrop -49 16 0 0.031200 0.031200 0 0 0
+( -1712 -1136 -512 ) ( -1712 1688 -512 ) ( 1304 1688 -512 ) map/nodrop -49 16 0 0.031200 0.031200 0 0 0
+( 304 -3064 -616 ) ( 3320 -3064 -616 ) ( 3320 -3064 -624 ) map/nodrop -49 38 0 0.031200 0.031200 0 0 0
+( 3320 -3064 -616 ) ( 3320 -240 -616 ) ( 3320 -240 -624 ) map/nodrop -16 38 0 0.031200 0.031200 0 0 0
+( 216 3064 -616 ) ( -2800 3064 -616 ) ( -2800 3064 -624 ) map/nodrop -49 38 0 0.031200 0.031200 0 0 0
+( -2808 3064 -616 ) ( -2808 240 -616 ) ( -2808 240 -624 ) map/nodrop -16 38 0 0.031200 0.031200 0 0 0
+}
+// brush 17
+{
+( 424 1344 256 ) ( -448 1344 256 ) ( -448 1336 256 ) map/floor_fill_d 0 45 0 0.190000 0.190000 0 0 0
+( -448 1336 280 ) ( -448 1344 280 ) ( 424 1344 280 ) map/floor_fill_d 0 45 0 0.190000 0.190000 0 0 0
+( -448 1336 272 ) ( 424 1336 272 ) ( 424 1336 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( 448 1336 272 ) ( 448 1344 272 ) ( 448 1344 256 ) map/floor_fill_d -45 -23 0 0.190000 0.190000 0 0 0
+( 424 1344 272 ) ( -448 1344 272 ) ( -448 1344 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( -448 1344 272 ) ( -448 1336 272 ) ( -448 1336 256 ) map/floor_fill_d -45 -23 0 0.190000 0.190000 0 0 0
+}
+// brush 18
+{
+( 1152 568 -48 ) ( 1144 568 -48 ) ( 1144 568 -64 ) map/floor_fill_d 4 20 0 0.190002 0.189999 0 0 0
+( 1152 728 -48 ) ( 1152 1600 -48 ) ( 1152 1600 -64 ) map/floor_fill_d -25 19 -180 0.190002 -0.189999 0 0 0
+( 1128 -568 -48 ) ( 1136 -568 -48 ) ( 1136 -568 -64 ) map/floor_fill_d 4 20 0 0.190002 0.189999 0 0 0
+( 1144 200 -48 ) ( 1144 -672 -48 ) ( 1144 -672 -64 ) map/floor_fill_d -25 19 -180 0.190002 -0.189999 0 0 0
+( 1144 200 -40 ) ( 1152 200 -40 ) ( 1152 -672 -40 ) map/floor_fill_d -25 -4 -90 0.190002 0.190002 0 0 0
+( 1152 -672 -64 ) ( 1152 200 -64 ) ( 1144 200 -64 ) map/floor_fill_d -25 -4 -90 0.190002 0.190002 0 0 0
+}
+// brush 19
+{
+( -1144 -672 -64 ) ( -1144 200 -64 ) ( -1152 200 -64 ) map/floor_fill_d 38 -56 -90 0.190002 0.190002 0 0 0
+( -1152 200 -40 ) ( -1144 200 -40 ) ( -1144 -672 -40 ) map/floor_fill_d 38 -56 -90 0.190002 0.190002 0 0 0
+( -1152 200 -48 ) ( -1152 -672 -48 ) ( -1152 -672 -64 ) map/floor_fill_d 39 19 -180 0.190002 -0.189999 0 0 0
+( -1168 -576 -48 ) ( -1160 -576 -48 ) ( -1160 -576 -64 ) map/floor_fill_d 56 20 0 0.190002 0.189999 0 0 0
+( -1144 728 -48 ) ( -1144 1600 -48 ) ( -1144 1600 -64 ) map/floor_fill_d 39 19 -180 0.190002 -0.189999 0 0 0
+( -1144 568 -48 ) ( -1152 568 -48 ) ( -1152 568 -64 ) map/floor_fill_d 56 20 0 0.190002 0.189999 0 0 0
+}
+// brush 20
+{
+( 424 -1336 256 ) ( -448 -1336 256 ) ( -448 -1344 256 ) map/floor_fill_d 0 20 0 0.190000 0.190000 0 0 0
+( -448 -1344 280 ) ( -448 -1336 280 ) ( 424 -1336 280 ) map/floor_fill_d 0 20 0 0.190000 0.190000 0 0 0
+( -448 -1344 272 ) ( 424 -1344 272 ) ( 424 -1344 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( 448 -1344 272 ) ( 448 -1336 272 ) ( 448 -1336 256 ) map/floor_fill_d -20 -23 0 0.190000 0.190000 0 0 0
+( 424 -1336 272 ) ( -448 -1336 272 ) ( -448 -1336 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( -448 -1336 272 ) ( -448 -1344 272 ) ( -448 -1344 256 ) map/floor_fill_d -20 -23 0 0.190000 0.190000 0 0 0
+}
+// brush 21
+{
+( -448 -1328 272 ) ( -448 -1336 272 ) ( -448 -1336 256 ) map/floor_fill_d -62 -23 0 0.190000 0.190000 0 0 0
+( 440 -896 272 ) ( -432 -896 272 ) ( -432 -896 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( -440 -1352 272 ) ( -440 -1344 272 ) ( -440 -1344 256 ) map/floor_fill_d -62 -23 0 0.190000 0.190000 0 0 0
+( -448 -1336 272 ) ( 424 -1336 272 ) ( 424 -1336 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( -448 -1336 280 ) ( -448 -1328 280 ) ( 424 -1328 280 ) map/floor_fill_d 0 62 0 0.190000 0.190000 0 0 0
+( 424 -1328 256 ) ( -448 -1328 256 ) ( -448 -1336 256 ) map/floor_fill_d 0 62 0 0.190000 0.190000 0 0 0
+}
+// brush 22
+{
+( 1312 -1328 256 ) ( 440 -1328 256 ) ( 440 -1336 256 ) map/floor_fill_d -65 62 0 0.190000 0.190000 0 0 0
+( 440 -1336 280 ) ( 440 -1328 280 ) ( 1312 -1328 280 ) map/floor_fill_d -65 62 0 0.190000 0.190000 0 0 0
+( 440 -1336 272 ) ( 1312 -1336 272 ) ( 1312 -1336 256 ) map/floor_fill_d -65 -23 0 0.190000 0.190000 0 0 0
+( 448 -1352 272 ) ( 448 -1344 272 ) ( 448 -1344 256 ) map/floor_fill_d -62 -23 0 0.190000 0.190000 0 0 0
+( 1328 -896 272 ) ( 456 -896 272 ) ( 456 -896 256 ) map/floor_fill_d -65 -23 0 0.190000 0.190000 0 0 0
+( 440 -1328 272 ) ( 440 -1336 272 ) ( 440 -1336 256 ) map/floor_fill_d -62 -23 0 0.190000 0.190000 0 0 0
+}
+// brush 23
+{
+( 440 904 272 ) ( 440 896 272 ) ( 440 896 256 ) map/floor_fill_d -33 -23 0 0.190000 0.190000 0 0 0
+( 1328 1336 272 ) ( 456 1336 272 ) ( 456 1336 256 ) map/floor_fill_d -65 -23 0 0.190000 0.190000 0 0 0
+( 448 880 272 ) ( 448 888 272 ) ( 448 888 256 ) map/floor_fill_d -33 -23 0 0.190000 0.190000 0 0 0
+( 440 896 272 ) ( 1312 896 272 ) ( 1312 896 256 ) map/floor_fill_d -65 -23 0 0.190000 0.190000 0 0 0
+( 440 896 280 ) ( 440 904 280 ) ( 1312 904 280 ) map/floor_fill_d -65 33 0 0.190000 0.190000 0 0 0
+( 1312 904 256 ) ( 440 904 256 ) ( 440 896 256 ) map/floor_fill_d -65 33 0 0.190000 0.190000 0 0 0
+}
+// brush 24
+{
+( 424 904 256 ) ( -448 904 256 ) ( -448 896 256 ) map/floor_fill_d 0 33 0 0.190000 0.190000 0 0 0
+( -448 896 280 ) ( -448 904 280 ) ( 424 904 280 ) map/floor_fill_d 0 33 0 0.190000 0.190000 0 0 0
+( -448 896 272 ) ( 424 896 272 ) ( 424 896 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( -440 880 272 ) ( -440 888 272 ) ( -440 888 256 ) map/floor_fill_d -33 -23 0 0.190000 0.190000 0 0 0
+( 440 1336 272 ) ( -432 1336 272 ) ( -432 1336 256 ) map/floor_fill_d 0 -23 0 0.190000 0.190000 0 0 0
+( -448 904 272 ) ( -448 896 272 ) ( -448 896 256 ) map/floor_fill_d -33 -23 0 0.190000 0.190000 0 0 0
+}
+// brush 25
+{
+( 640 576 -48 ) ( 632 576 -48 ) ( 632 576 -64 ) map/floor_fill_d -160 20 0 0.190002 0.189999 0 0 0
+( 1144 1872 -48 ) ( 1144 2744 -48 ) ( 1144 2744 -64 ) map/floor_fill_d 171 19 -180 0.190002 -0.189999 0 0 0
+( -232 568 -48 ) ( -224 568 -48 ) ( -224 568 -64 ) map/floor_fill_d -160 20 0 0.190002 0.189999 0 0 0
+( -1144 1344 -48 ) ( -1144 472 -48 ) ( -1144 472 -64 ) map/floor_fill_d 171 19 -180 0.190002 -0.189999 0 0 0
+( 640 1344 -40 ) ( 648 1344 -40 ) ( 648 472 -40 ) map/floor_fill_d 170 160 -90 0.190002 0.190002 0 0 0
+( 648 472 -64 ) ( 648 1344 -64 ) ( 640 1344 -64 ) map/floor_fill_d 170 160 -90 0.190002 0.190002 0 0 0
+}
+// brush 26
+{
+( 648 -672 -64 ) ( 648 200 -64 ) ( 640 200 -64 ) map/floor_fill_d -218 -95 -90 0.190002 0.190002 0 0 0
+( 640 200 -40 ) ( 648 200 -40 ) ( 648 -672 -40 ) map/floor_fill_d -218 -95 -90 0.190002 0.190002 0 0 0
+( -1144 200 -48 ) ( -1144 -672 -48 ) ( -1144 -672 -64 ) map/floor_fill_d -217 18 -180 0.190002 -0.189999 0 0 0
+( 1552 -576 -48 ) ( 1560 -576 -48 ) ( 1560 -576 -64 ) map/floor_fill_d 95 20 0 0.190002 0.189999 0 0 0
+( 1144 728 -48 ) ( 1144 1600 -48 ) ( 1144 1600 -64 ) map/floor_fill_d -217 18 -180 0.190002 -0.189999 0 0 0
+( 640 -568 -48 ) ( 632 -568 -48 ) ( 632 -568 -64 ) map/floor_fill_d 95 20 0 0.190002 0.189999 0 0 0
+}
+// brush 27
+{
+( -640 512 -64 ) ( -640 -576 -64 ) ( -640 -576 -96 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 256 -384 -64 ) ( -256 -384 -64 ) ( -256 -384 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 640 -616 -64 ) ( 640 472 -64 ) ( 640 472 -96 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 640 -576 -64 ) ( 1152 -576 -64 ) ( 1152 -576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 640 -576 -64 ) ( 640 512 -64 ) ( 1152 512 -64 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 1152 512 -96 ) ( 640 512 -96 ) ( 640 -576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+}
+// brush 28
+{
+( 1152 576 -96 ) ( 640 576 -96 ) ( 640 -512 -96 ) map/lab_games/lg_style_01_floor_orange 336 336 0 0.190000 0.190000 0 0 0
+( 640 -512 -64 ) ( 640 576 -64 ) ( 1152 576 -64 ) map/lab_games/lg_style_01_floor_orange 336 336 0 0.190000 0.190000 0 0 0
+( -216 384 -64 ) ( 296 384 -64 ) ( 296 384 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 640 -536 -64 ) ( 640 552 -64 ) ( 640 552 -96 ) map/lab_games/lg_style_01_floor_orange -336 0 0 0.190000 0.190000 0 0 0
+( 1152 576 -64 ) ( 640 576 -64 ) ( 640 576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( -640 560 -64 ) ( -640 -528 -64 ) ( -640 -528 -96 ) map/lab_games/lg_style_01_floor_orange -336 0 0 0.190000 0.190000 0 0 0
+}
+// brush 29
+{
+( 1152 512 -96 ) ( 640 512 -96 ) ( 640 -576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 640 -576 -64 ) ( 640 512 -64 ) ( 1152 512 -64 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( 640 -576 -64 ) ( 1152 -576 -64 ) ( 1152 -576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( -640 -664 -64 ) ( -640 424 -64 ) ( -640 424 -96 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 296 576 -64 ) ( -216 576 -64 ) ( -216 576 -96 ) map/lab_games/lg_style_01_floor_orange 336 0 0 0.190000 0.190000 0 0 0
+( -1152 496 -64 ) ( -1152 -592 -64 ) ( -1152 -592 -96 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 30
+{
+( 656 152 -448 ) ( 656 1024 -448 ) ( 648 1024 -448 ) map/floor_fill_d -233 202 -90 0.190002 0.190002 0 0 0
+( 648 1024 -424 ) ( 656 1024 -424 ) ( 656 152 -424 ) map/floor_fill_d -233 202 -90 0.190002 0.190002 0 0 0
+( -256 1024 -432 ) ( -256 152 -432 ) ( -256 152 -448 ) map/floor_fill_d -232 -210 -180 0.190002 -0.189999 0 0 0
+( -240 248 -432 ) ( -232 248 -432 ) ( -232 248 -448 ) map/floor_fill_d -202 -209 0 0.190002 0.189999 0 0 0
+( 256 1552 -432 ) ( 256 2424 -432 ) ( 256 2424 -448 ) map/floor_fill_d -232 -210 -180 0.190002 -0.189999 0 0 0
+( 648 256 -432 ) ( 640 256 -432 ) ( 640 256 -448 ) map/floor_fill_d -202 -209 0 0.190002 0.189999 0 0 0
+}
+// brush 31
+{
+( 648 -248 -432 ) ( 640 -248 -432 ) ( 640 -248 -448 ) map/floor_fill_d -202 -209 0 0.190002 0.189999 0 0 0
+( 256 1048 -432 ) ( 256 1920 -432 ) ( 256 1920 -448 ) map/floor_fill_d -68 -210 -180 0.190002 -0.189999 0 0 0
+( -240 -256 -432 ) ( -232 -256 -432 ) ( -232 -256 -448 ) map/floor_fill_d -202 -209 0 0.190002 0.189999 0 0 0
+( -256 520 -432 ) ( -256 -352 -432 ) ( -256 -352 -448 ) map/floor_fill_d -68 -210 -180 0.190002 -0.189999 0 0 0
+( 648 520 -424 ) ( 656 520 -424 ) ( 656 -352 -424 ) map/floor_fill_d -69 202 -90 0.190002 0.190002 0 0 0
+( 656 -352 -448 ) ( 656 520 -448 ) ( 648 520 -448 ) map/floor_fill_d -69 202 -90 0.190002 0.190002 0 0 0
+}
+// brush 32
+{
+( 656 -344 -448 ) ( 656 528 -448 ) ( 648 528 -448 ) map/floor_fill_d -26 202 -90 0.190002 0.190002 0 0 0
+( 648 528 -424 ) ( 656 528 -424 ) ( 656 -344 -424 ) map/floor_fill_d -26 202 -90 0.190002 0.190002 0 0 0
+( -256 528 -432 ) ( -256 -344 -432 ) ( -256 -344 -448 ) map/floor_fill_d -25 -210 -180 0.190002 -0.189999 0 0 0
+( -240 -248 -432 ) ( -232 -248 -432 ) ( -232 -248 -448 ) map/floor_fill_d -202 -209 0 0.190002 0.189999 0 0 0
+( -248 1072 -432 ) ( -248 1944 -432 ) ( -248 1944 -448 ) map/floor_fill_d -25 -210 -180 0.190002 -0.189999 0 0 0
+( 672 248 -432 ) ( 664 248 -432 ) ( 664 248 -448 ) map/floor_fill_d -202 -209 0 0.190002 0.189999 0 0 0
+}
+// brush 33
+{
+( 1176 248 -432 ) ( 1168 248 -432 ) ( 1168 248 -448 ) map/floor_fill_d -38 -209 0 0.190002 0.189999 0 0 0
+( 256 1072 -432 ) ( 256 1944 -432 ) ( 256 1944 -448 ) map/floor_fill_d -25 -210 -180 0.190002 -0.189999 0 0 0
+( 264 -248 -432 ) ( 272 -248 -432 ) ( 272 -248 -448 ) map/floor_fill_d -38 -209 0 0.190002 0.189999 0 0 0
+( 248 528 -432 ) ( 248 -344 -432 ) ( 248 -344 -448 ) map/floor_fill_d -25 -210 -180 0.190002 -0.189999 0 0 0
+( 1152 528 -424 ) ( 1160 528 -424 ) ( 1160 -344 -424 ) map/floor_fill_d -25 38 -90 0.190002 0.190002 0 0 0
+( 1160 -344 -448 ) ( 1160 528 -448 ) ( 1152 528 -448 ) map/floor_fill_d -25 38 -90 0.190002 0.190002 0 0 0
+}
+// brush 34
+{
+( 648 128 1040 ) ( 640 128 1040 ) ( 640 128 1024 ) map/floor_fill_d -202 -141 0 0.190002 0.189999 0 0 0
+( 128 1424 1040 ) ( 128 2296 1040 ) ( 128 2296 1024 ) map/floor_fill_d -137 -142 -180 0.190002 -0.189999 0 0 0
+( -240 120 1040 ) ( -232 120 1040 ) ( -232 120 1024 ) map/floor_fill_d -202 -141 0 0.190002 0.189999 0 0 0
+( -128 896 1040 ) ( -128 24 1040 ) ( -128 24 1024 ) map/floor_fill_d -137 -142 -180 0.190002 -0.189999 0 0 0
+( 648 896 1048 ) ( 656 896 1048 ) ( 656 24 1048 ) map/floor_fill_d -138 202 -90 0.190002 0.190002 0 0 0
+( 656 24 1024 ) ( 656 896 1024 ) ( 648 896 1024 ) map/floor_fill_d -138 202 -90 0.190002 0.190002 0 0 0
+}
+// brush 35
+{
+( 656 -224 1024 ) ( 656 648 1024 ) ( 648 648 1024 ) map/floor_fill_d -163 202 -90 0.190002 0.190002 0 0 0
+( 648 648 1048 ) ( 656 648 1048 ) ( 656 -224 1048 ) map/floor_fill_d -163 202 -90 0.190002 0.190002 0 0 0
+( -128 648 1040 ) ( -128 -224 1040 ) ( -128 -224 1024 ) map/floor_fill_d -162 -142 -180 0.190002 -0.189999 0 0 0
+( -240 -128 1040 ) ( -232 -128 1040 ) ( -232 -128 1024 ) map/floor_fill_d -202 -141 0 0.190002 0.189999 0 0 0
+( 128 1176 1040 ) ( 128 2048 1040 ) ( 128 2048 1024 ) map/floor_fill_d -162 -142 -180 0.190002 -0.189999 0 0 0
+( 648 -120 1040 ) ( 640 -120 1040 ) ( 640 -120 1024 ) map/floor_fill_d -202 -141 0 0.190002 0.189999 0 0 0
+}
+// brush 36
+{
+( 656 120 1040 ) ( 648 120 1040 ) ( 648 120 1024 ) map/floor_fill_d -202 -141 0 0.190002 0.189999 0 0 0
+( -120 1184 1040 ) ( -120 2056 1040 ) ( -120 2056 1024 ) map/floor_fill_d -119 -142 -180 0.190002 -0.189999 0 0 0
+( -240 -120 1040 ) ( -232 -120 1040 ) ( -232 -120 1024 ) map/floor_fill_d -202 -141 0 0.190002 0.189999 0 0 0
+( -128 656 1040 ) ( -128 -216 1040 ) ( -128 -216 1024 ) map/floor_fill_d -119 -142 -180 0.190002 -0.189999 0 0 0
+( 648 656 1048 ) ( 656 656 1048 ) ( 656 -216 1048 ) map/floor_fill_d -120 202 -90 0.190002 0.190002 0 0 0
+( 656 -216 1024 ) ( 656 656 1024 ) ( 648 656 1024 ) map/floor_fill_d -120 202 -90 0.190002 0.190002 0 0 0
+}
+// brush 37
+{
+( 904 -216 1024 ) ( 904 656 1024 ) ( 896 656 1024 ) map/floor_fill_d -120 227 -90 0.190002 0.190002 0 0 0
+( 896 656 1048 ) ( 904 656 1048 ) ( 904 -216 1048 ) map/floor_fill_d -120 227 -90 0.190002 0.190002 0 0 0
+( 120 656 1040 ) ( 120 -216 1040 ) ( 120 -216 1024 ) map/floor_fill_d -119 -142 -180 0.190002 -0.189999 0 0 0
+( 8 -120 1040 ) ( 16 -120 1040 ) ( 16 -120 1024 ) map/floor_fill_d -227 -141 0 0.190002 0.189999 0 0 0
+( 128 1184 1040 ) ( 128 2056 1040 ) ( 128 2056 1024 ) map/floor_fill_d -119 -142 -180 0.190002 -0.189999 0 0 0
+( 904 120 1040 ) ( 896 120 1040 ) ( 896 120 1024 ) map/floor_fill_d -227 -141 0 0.190002 0.189999 0 0 0
+}
+// brush 38
+{
+( -2816 3072 1528 ) ( -2816 -1792 1528 ) ( 3328 3072 1528 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 3072 256 ) ( -2816 -1792 256 ) ( -2816 -1792 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 3072 256 ) ( -2816 3072 256 ) ( -2816 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( 3328 -1792 256 ) ( 3328 3072 256 ) ( 3328 3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -3072 -3072 256 ) ( 3072 -3072 256 ) ( 3072 -3072 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+( -2816 -1792 1536 ) ( -2816 3072 1536 ) ( 3328 3072 1536 ) map/lab_games/sky/lg_sky_01 0 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 1
+{
+"classname" "_skybox"
+"origin" "4464 3656 -96"
+}
+// entity 2
+{
+"model" "models/stadium.md3"
+"origin" "4464 3656 -34"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+"modelscale" "1.5"
+}
+// entity 3
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "4488 3616 -48"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 4
+{
+"classname" "misc_model"
+"origin" "4536 3640 -120"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 5
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "4496 3648 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 6
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "4440 3664 -120"
+"classname" "misc_model"
+"angle" "159"
+}
+// entity 7
+{
+"classname" "trigger_push"
+"target" "landing1"
+// brush 0
+{
+( 384 -1024 312 ) ( 384 -912 312 ) ( 384 -912 256 ) common/caulk 44 9 -180 0.031250 -0.031189 0 0 0
+( 256 -1024 312 ) ( 432 -1024 312 ) ( 432 -1024 256 ) common/caulk 61 9 -180 0.031128 -0.031189 0 0 0
+( 320 -912 312 ) ( 320 -1024 312 ) ( 320 -1024 256 ) common/caulk 44 9 -180 0.031250 -0.031189 0 0 0
+( 432 -960 312 ) ( 256 -960 312 ) ( 256 -960 256 ) common/caulk 61 9 -180 0.031128 -0.031189 0 0 0
+( 432 -912 312 ) ( 432 -1024 312 ) ( 256 -1024 312 ) common/caulk 38 -61 -180 0.031250 0.031250 0 0 0
+( 256 -1024 272 ) ( 432 -1024 272 ) ( 432 -912 272 ) common/caulk 38 -61 -180 0.031250 0.031250 0 0 0
+}
+}
+// entity 8
+{
+"classname" "target_position"
+"origin" "632 -456 760"
+"targetname" "landing1"
+}
+// entity 9
+{
+"angle" "135"
+"model" "models/bounce_pad.md3"
+"origin" "352 -992 280"
+"classname" "misc_model"
+}
+// entity 10
+{
+"target" "t1"
+"classname" "trigger_push"
+// brush 0
+{
+( -256 1024 272 ) ( -432 1024 272 ) ( -432 912 272 ) common/caulk 38 3 0 0.031250 0.031250 0 0 0
+( -432 912 312 ) ( -432 1024 312 ) ( -256 1024 312 ) common/caulk 38 3 0 0.031250 0.031250 0 0 0
+( -432 960 312 ) ( -256 960 312 ) ( -256 960 256 ) common/caulk 61 9 0 0.031128 0.031189 0 0 0
+( -320 912 312 ) ( -320 1024 312 ) ( -320 1024 256 ) common/caulk -20 9 0 0.031250 0.031189 0 0 0
+( -256 1024 312 ) ( -432 1024 312 ) ( -432 1024 256 ) common/caulk 61 9 0 0.031128 0.031189 0 0 0
+( -384 1024 312 ) ( -384 912 312 ) ( -384 912 256 ) common/caulk -20 9 0 0.031250 0.031189 0 0 0
+}
+}
+// entity 11
+{
+"targetname" "t1"
+"origin" "-632 456 760"
+"classname" "target_position"
+}
+// entity 12
+{
+"classname" "misc_model"
+"origin" "-352 992 280"
+"model" "models/bounce_pad.md3"
+"angle" "315"
+}
+// entity 13
+{
+"target" "t3"
+"classname" "trigger_push"
+// brush 0
+{
+( -96 96 -432 ) ( -96 -80 -432 ) ( 16 -80 -432 ) common/caulk -25 2 90 0.031250 0.031250 0 0 0
+( 16 -80 -392 ) ( -96 -80 -392 ) ( -96 96 -392 ) common/caulk -25 2 90 0.031250 0.031250 0 0 0
+( -32 -80 -504 ) ( -32 96 -504 ) ( -32 96 -560 ) common/caulk -46 28 0 0.031128 0.031189 0 0 0
+( 16 32 -392 ) ( -96 32 -392 ) ( -96 32 -448 ) common/caulk -19 28 -180 0.031250 -0.031189 0 0 0
+( -96 96 -392 ) ( -96 -80 -392 ) ( -96 -80 -448 ) common/caulk -46 28 0 0.031128 0.031189 0 0 0
+( -96 -32 -392 ) ( 16 -32 -392 ) ( 16 -32 -448 ) common/caulk -19 28 -180 0.031250 -0.031189 0 0 0
+}
+}
+// entity 14
+{
+"targetname" "t3"
+"origin" "472 0 120"
+"classname" "target_position"
+}
+// entity 15
+{
+"classname" "misc_model"
+"origin" "-64 0 -424"
+"model" "models/bounce_pad.md3"
+"angle" "90"
+}
+// entity 16
+{
+"classname" "trigger_push"
+"target" "t4"
+// brush 0
+{
+( -32 96 -392 ) ( -32 -16 -392 ) ( -32 -16 -448 ) common/caulk -20 28 0 0.031250 0.031189 0 0 0
+( 96 96 -392 ) ( -80 96 -392 ) ( -80 96 -448 ) common/caulk 16 28 0 0.031128 0.031189 0 0 0
+( 32 -16 -392 ) ( 32 96 -392 ) ( 32 96 -448 ) common/caulk -20 28 0 0.031250 0.031189 0 0 0
+( -80 32 -504 ) ( 96 32 -504 ) ( 96 32 -560 ) common/caulk 16 28 0 0.031128 0.031189 0 0 0
+( -80 -16 -392 ) ( -80 96 -392 ) ( 96 96 -392 ) common/caulk 38 3 0 0.031250 0.031250 0 0 0
+( 96 96 -432 ) ( -80 96 -432 ) ( -80 -16 -432 ) common/caulk 38 3 0 0.031250 0.031250 0 0 0
+}
+}
+// entity 17
+{
+"classname" "target_position"
+"origin" "96 -680 376"
+"targetname" "t4"
+}
+// entity 18
+{
+"angle" "360"
+"model" "models/bounce_pad.md3"
+"origin" "0 64 -424"
+"classname" "misc_model"
+}
+// entity 19
+{
+"classname" "trigger_push"
+"target" "t6"
+// brush 0
+{
+( 96 32 -392 ) ( -16 32 -392 ) ( -16 32 -448 ) common/caulk -20 27 0 0.031250 0.031189 0 0 0
+( 96 -96 -392 ) ( 96 80 -392 ) ( 96 80 -448 ) common/caulk -47 27 -180 0.031128 -0.031189 0 0 0
+( -16 -32 -392 ) ( 96 -32 -392 ) ( 96 -32 -448 ) common/caulk -19 27 0 0.031250 0.031189 0 0 0
+( 32 80 -504 ) ( 32 -96 -504 ) ( 32 -96 -560 ) common/caulk -47 27 -180 0.031128 -0.031189 0 0 0
+( -16 80 -392 ) ( 96 80 -392 ) ( 96 -96 -392 ) common/caulk -26 3 -90 0.031250 0.031250 0 0 0
+( 96 -96 -432 ) ( 96 80 -432 ) ( -16 80 -432 ) common/caulk -26 3 -90 0.031250 0.031250 0 0 0
+}
+}
+// entity 20
+{
+"classname" "target_position"
+"origin" "-472 0 120"
+"targetname" "t6"
+}
+// entity 21
+{
+"angle" "270"
+"model" "models/bounce_pad.md3"
+"origin" "64 0 -424"
+"classname" "misc_model"
+}
+// entity 22
+{
+"classname" "trigger_push"
+"target" "t8"
+// brush 0
+{
+( 768 -384 -8 ) ( 656 -384 -8 ) ( 656 -384 -64 ) common/caulk 44 -11 0 0.031250 0.031189 0 0 0
+( 768 -512 -8 ) ( 768 -336 -8 ) ( 768 -336 -64 ) common/caulk 29 -10 -180 0.031250 -0.031189 0 0 0
+( 656 -448 -8 ) ( 768 -448 -8 ) ( 768 -448 -64 ) common/caulk 44 -11 0 0.031250 0.031189 0 0 0
+( 704 -336 -120 ) ( 704 -512 -120 ) ( 704 -512 -176 ) common/caulk 30 -10 -180 0.031250 -0.031189 0 0 0
+( 656 -336 -8 ) ( 768 -336 -8 ) ( 768 -512 -8 ) common/caulk 39 -62 -90 0.031250 0.031250 0 0 0
+( 768 -512 -48 ) ( 768 -336 -48 ) ( 656 -336 -48 ) common/caulk 39 -62 -90 0.031250 0.031250 0 0 0
+}
+}
+// entity 23
+{
+"classname" "target_position"
+"origin" "328 -760 504"
+"targetname" "t8"
+}
+// entity 24
+{
+"angle" "315"
+"model" "models/bounce_pad.md3"
+"origin" "736 -416 -40"
+"classname" "misc_model"
+}
+// entity 25
+{
+"target" "t10"
+"classname" "trigger_push"
+// brush 0
+{
+( -96 -96 -432 ) ( 80 -96 -432 ) ( 80 16 -432 ) common/caulk 37 3 -180 0.031250 0.031250 0 0 0
+( 80 16 -392 ) ( 80 -96 -392 ) ( -96 -96 -392 ) common/caulk 37 3 -180 0.031250 0.031250 0 0 0
+( 80 -32 -504 ) ( -96 -32 -504 ) ( -96 -32 -560 ) common/caulk 16 27 -180 0.031128 -0.031189 0 0 0
+( -32 16 -392 ) ( -32 -96 -392 ) ( -32 -96 -448 ) common/caulk -20 28 -180 0.031250 -0.031189 0 0 0
+( -96 -96 -392 ) ( 80 -96 -392 ) ( 80 -96 -448 ) common/caulk 16 27 -180 0.031128 -0.031189 0 0 0
+( 32 -96 -392 ) ( 32 16 -392 ) ( 32 16 -448 ) common/caulk -20 28 -180 0.031250 -0.031189 0 0 0
+}
+}
+// entity 26
+{
+"targetname" "t10"
+"origin" "-96 680 376"
+"classname" "target_position"
+}
+// entity 27
+{
+"classname" "misc_model"
+"origin" "0 -64 -424"
+"model" "models/bounce_pad.md3"
+"angle" "180"
+}
+// entity 28
+{
+"classname" "trigger_push"
+"target" "t11"
+// brush 0
+{
+( -768 64 -8 ) ( -656 64 -8 ) ( -656 64 -64 ) common/caulk -20 -11 -180 0.031250 -0.031189 0 0 0
+( -768 192 -8 ) ( -768 16 -8 ) ( -768 16 -64 ) common/caulk -34 -11 0 0.031250 0.031189 0 0 0
+( -656 128 -8 ) ( -768 128 -8 ) ( -768 128 -64 ) common/caulk -20 -11 -180 0.031250 -0.031189 0 0 0
+( -704 16 -120 ) ( -704 192 -120 ) ( -704 192 -176 ) common/caulk -34 -11 0 0.031250 0.031189 0 0 0
+( -656 16 -8 ) ( -768 16 -8 ) ( -768 192 -8 ) common/caulk -24 0 90 0.031250 0.031250 0 0 0
+( -768 192 -48 ) ( -768 16 -48 ) ( -656 16 -48 ) common/caulk -24 0 90 0.031250 0.031250 0 0 0
+}
+}
+// entity 29
+{
+"target" "t12"
+"classname" "trigger_push"
+// brush 0
+{
+( 768 -192 -48 ) ( 768 -16 -48 ) ( 656 -16 -48 ) common/caulk -25 -60 -90 0.031250 0.031250 0 0 0
+( 656 -16 -8 ) ( 768 -16 -8 ) ( 768 -192 -8 ) common/caulk -25 -60 -90 0.031250 0.031250 0 0 0
+( 704 -16 -120 ) ( 704 -192 -120 ) ( 704 -192 -176 ) common/caulk -56 -9 -180 0.031128 -0.031189 0 0 0
+( 656 -128 -8 ) ( 768 -128 -8 ) ( 768 -128 -64 ) common/caulk 44 -11 0 0.031250 0.031189 0 0 0
+( 768 -192 -8 ) ( 768 -16 -8 ) ( 768 -16 -64 ) common/caulk -56 -9 -180 0.031128 -0.031189 0 0 0
+( 768 -64 -8 ) ( 656 -64 -8 ) ( 656 -64 -64 ) common/caulk 44 -11 0 0.031250 0.031189 0 0 0
+}
+}
+// entity 30
+{
+"targetname" "t12"
+"origin" "-24 -96 360"
+"classname" "target_position"
+}
+// entity 31
+{
+"classname" "misc_model"
+"origin" "736 -96 -40"
+"model" "models/bounce_pad.md3"
+"angle" "270"
+}
+// entity 32
+{
+"classname" "team_CTF_blueplayer"
+"origin" "-352 -1248 288"
+}
+// entity 33
+{
+"origin" "0 -1248 288"
+"classname" "team_CTF_blueflag"
+}
+// entity 34
+{
+"origin" "-224 -1248 288"
+"classname" "team_CTF_blueplayer"
+}
+// entity 35
+{
+"classname" "team_CTF_blueplayer"
+"origin" "224 -1248 288"
+}
+// entity 36
+{
+"origin" "352 -1248 288"
+"classname" "team_CTF_blueplayer"
+}
+// entity 37
+{
+"classname" "team_CTF_redplayer"
+"origin" "352 1248 288"
+}
+// entity 38
+{
+"origin" "224 1248 288"
+"classname" "team_CTF_redplayer"
+}
+// entity 39
+{
+"classname" "team_CTF_redplayer"
+"origin" "-352 1248 288"
+}
+// entity 40
+{
+"origin" "-224 1248 288"
+"classname" "team_CTF_redplayer"
+}
+// entity 41
+{
+"classname" "team_CTF_redflag"
+"origin" "0 1248 288"
+}
+// entity 42
+{
+"classname" "trigger_push"
+"target" "t13"
+// brush 0
+{
+( 112 48 1080 ) ( 112 160 1080 ) ( 112 160 1024 ) common/caulk 43 57 -180 0.031250 -0.031189 0 0 0
+( -16 48 1080 ) ( 160 48 1080 ) ( 160 48 1024 ) common/caulk -37 57 -180 0.031128 -0.031189 0 0 0
+( 48 160 1080 ) ( 48 48 1080 ) ( 48 48 1024 ) common/caulk 43 57 -180 0.031250 -0.031189 0 0 0
+( 160 112 1080 ) ( -16 112 1080 ) ( -16 112 1024 ) common/caulk -37 57 -180 0.031128 -0.031189 0 0 0
+( 160 160 1080 ) ( 160 48 1080 ) ( -16 48 1080 ) common/caulk -26 -60 -180 0.031250 0.031250 0 0 0
+( -16 48 1040 ) ( 160 48 1040 ) ( 160 160 1040 ) common/caulk -26 -60 -180 0.031250 0.031250 0 0 0
+}
+}
+// entity 43
+{
+"classname" "target_position"
+"origin" "80 392 1224"
+"targetname" "t13"
+}
+// entity 44
+{
+"angle" "180"
+"model" "models/bounce_pad.md3"
+"origin" "80 80 1048"
+"classname" "misc_model"
+}
+// entity 45
+{
+"target" "t14"
+"classname" "trigger_push"
+// brush 0
+{
+( 16 -48 1040 ) ( -160 -48 1040 ) ( -160 -160 1040 ) common/caulk 38 -59 0 0.031250 0.031250 0 0 0
+( -160 -160 1080 ) ( -160 -48 1080 ) ( 16 -48 1080 ) common/caulk 38 -59 0 0.031250 0.031250 0 0 0
+( -160 -112 1080 ) ( 16 -112 1080 ) ( 16 -112 1024 ) common/caulk 27 57 0 0.031128 0.031128 0 0 0
+( -48 -160 1080 ) ( -48 -48 1080 ) ( -48 -48 1024 ) common/caulk 43 57 0 0.031250 0.031128 0 0 0
+( 16 -48 1080 ) ( -160 -48 1080 ) ( -160 -48 1024 ) common/caulk 27 57 0 0.031128 0.031128 0 0 0
+( -112 -48 1080 ) ( -112 -160 1080 ) ( -112 -160 1024 ) common/caulk 43 57 0 0.031250 0.031128 0 0 0
+}
+}
+// entity 46
+{
+"targetname" "t14"
+"origin" "-80 -392 1224"
+"classname" "target_position"
+}
+// entity 47
+{
+"classname" "misc_model"
+"origin" "-80 -80 1048"
+"model" "models/bounce_pad.md3"
+"angle" "360"
+}
+// entity 48
+{
+"classname" "target_kill"
+"origin" "-896 952 -688"
+"targetname" "kill1"
+}
+// entity 49
+{
+"wait" "0.00001"
+"target" "kill1"
+"classname" "trigger_multiple"
+// brush 0
+{
+( 960 904 -528 ) ( -832 904 -528 ) ( -832 -888 -528 ) map/poltergeist 0 0 0 0.031200 0.031200 0 0 0
+( -832 -888 -520 ) ( -832 904 -520 ) ( 960 904 -520 ) map/poltergeist 0 0 0 0.031200 0.031200 0 0 0
+( 1592 -3064 -520 ) ( 3384 -3064 -520 ) ( 3384 -3064 -536 ) map/poltergeist 0 107 0 0.031200 0.031200 0 0 0
+( 3320 -3080 -520 ) ( 3320 -1288 -520 ) ( 3320 -1288 -536 ) map/poltergeist 0 107 0 0.031200 0.031200 0 0 0
+( -1064 3064 -520 ) ( -2856 3064 -520 ) ( -2856 3064 -536 ) map/poltergeist 0 107 0 0.031200 0.031200 0 0 0
+( -2808 3304 -520 ) ( -2808 1512 -520 ) ( -2808 1512 -536 ) map/poltergeist 0 107 0 0.031200 0.031200 0 0 0
+}
+}
+// entity 50
+{
+"classname" "weapon_rocketlauncher"
+"origin" "0 0 1048"
+}
+// entity 51
+{
+"classname" "weapon_lightning"
+"origin" "-224 -992 280"
+}
+// entity 52
+{
+"origin" "224 -992 280"
+"classname" "weapon_lightning"
+}
+// entity 53
+{
+"origin" "-224 992 280"
+"classname" "weapon_lightning"
+}
+// entity 54
+{
+"classname" "weapon_lightning"
+"origin" "224 992 280"
+}
+// entity 55
+{
+"classname" "misc_model"
+"origin" "0 1248 256"
+"model" "models/pickup_platform.md3"
+}
+// entity 56
+{
+"model" "models/pickup_platform.md3"
+"origin" "0 -1248 256"
+"classname" "misc_model"
+}
+// entity 57
+{
+"target" "t15"
+"classname" "trigger_push"
+// brush 0
+{
+( -768 512 -48 ) ( -768 336 -48 ) ( -656 336 -48 ) common/caulk 39 2 90 0.031250 0.031250 0 0 0
+( -656 336 -8 ) ( -768 336 -8 ) ( -768 512 -8 ) common/caulk 39 2 90 0.031250 0.031250 0 0 0
+( -704 336 -120 ) ( -704 512 -120 ) ( -704 512 -176 ) common/caulk 30 -11 0 0.031250 0.031189 0 0 0
+( -656 448 -8 ) ( -768 448 -8 ) ( -768 448 -64 ) common/caulk -20 -11 -180 0.031250 -0.031189 0 0 0
+( -768 512 -8 ) ( -768 336 -8 ) ( -768 336 -64 ) common/caulk 29 -11 0 0.031250 0.031189 0 0 0
+( -768 384 -8 ) ( -656 384 -8 ) ( -656 384 -64 ) common/caulk -20 -11 -180 0.031250 -0.031189 0 0 0
+}
+}
+// entity 58
+{
+"targetname" "t15"
+"origin" "-328 760 504"
+"classname" "target_position"
+}
+// entity 59
+{
+"classname" "misc_model"
+"origin" "-736 416 -40"
+"model" "models/bounce_pad.md3"
+"angle" "135"
+}
+// entity 60
+{
+"origin" "-352 -1248 288"
+"classname" "team_CTF_bluespawn"
+}
+// entity 61
+{
+"classname" "team_CTF_bluespawn"
+"origin" "-224 -1248 288"
+}
+// entity 62
+{
+"origin" "224 -1248 288"
+"classname" "team_CTF_bluespawn"
+}
+// entity 63
+{
+"classname" "team_CTF_bluespawn"
+"origin" "352 -1248 288"
+}
+// entity 64
+{
+"origin" "352 1248 288"
+"classname" "team_CTF_redspawn"
+}
+// entity 65
+{
+"classname" "team_CTF_redspawn"
+"origin" "224 1248 288"
+}
+// entity 66
+{
+"origin" "-224 1248 288"
+"classname" "team_CTF_redspawn"
+}
+// entity 67
+{
+"classname" "team_CTF_redspawn"
+"origin" "-352 1248 288"
+}
+// entity 68
+{
+"model" "models/pickup_platform.md3"
+"origin" "-224 992 256"
+"classname" "misc_model"
+}
+// entity 69
+{
+"classname" "misc_model"
+"origin" "224 992 256"
+"model" "models/pickup_platform.md3"
+}
+// entity 70
+{
+"model" "models/pickup_platform.md3"
+"origin" "224 -992 256"
+"classname" "misc_model"
+}
+// entity 71
+{
+"classname" "misc_model"
+"origin" "-224 -992 256"
+"model" "models/pickup_platform.md3"
+}
+// entity 72
+{
+"model" "models/pickup_platform.md3"
+"origin" "0 0 1024"
+"classname" "misc_model"
+}
+// entity 73
+{
+"target" "t16"
+"classname" "trigger_push"
+// brush 0
+{
+( -1056 96 -48 ) ( -1056 -80 -48 ) ( -944 -80 -48 ) common/caulk 39 -63 90 0.031250 0.031250 0 0 0
+( -944 -80 -8 ) ( -1056 -80 -8 ) ( -1056 96 -8 ) common/caulk 39 -63 90 0.031250 0.031250 0 0 0
+( -992 -80 -120 ) ( -992 96 -120 ) ( -992 96 -176 ) common/caulk 30 -11 0 0.031250 0.031189 0 0 0
+( -944 32 -8 ) ( -1056 32 -8 ) ( -1056 32 -64 ) common/caulk 44 -11 -180 0.031250 -0.031189 0 0 0
+( -1056 96 -8 ) ( -1056 -80 -8 ) ( -1056 -80 -64 ) common/caulk 30 -11 0 0.031250 0.031189 0 0 0
+( -1056 -32 -8 ) ( -944 -32 -8 ) ( -944 -32 -64 ) common/caulk 44 -11 -180 0.031250 -0.031189 0 0 0
+}
+}
+// entity 74
+{
+"targetname" "t16"
+"origin" "-280 0 1288"
+"classname" "target_position"
+}
+// entity 75
+{
+"classname" "misc_model"
+"origin" "-1024 0 -40"
+"model" "models/bounce_pad.md3"
+"angle" "90"
+}
+// entity 76
+{
+"classname" "trigger_push"
+"target" "t17"
+// brush 0
+{
+( 1056 32 -8 ) ( 944 32 -8 ) ( 944 32 -64 ) common/caulk 44 -10 0 0.031250 0.031189 0 0 0
+( 1056 -96 -8 ) ( 1056 80 -8 ) ( 1056 80 -64 ) common/caulk 30 -10 -180 0.031250 -0.031189 0 0 0
+( 944 -32 -8 ) ( 1056 -32 -8 ) ( 1056 -32 -64 ) common/caulk 44 -10 0 0.031250 0.031189 0 0 0
+( 992 80 -120 ) ( 992 -96 -120 ) ( 992 -96 -176 ) common/caulk 30 -10 -180 0.031250 -0.031189 0 0 0
+( 944 80 -8 ) ( 1056 80 -8 ) ( 1056 -96 -8 ) common/caulk 37 -63 -90 0.031250 0.031250 0 0 0
+( 1056 -96 -48 ) ( 1056 80 -48 ) ( 944 80 -48 ) common/caulk 37 -63 -90 0.031250 0.031250 0 0 0
+}
+}
+// entity 77
+{
+"classname" "target_position"
+"origin" "280 0 1288"
+"targetname" "t17"
+}
+// entity 78
+{
+"angle" "270"
+"model" "models/bounce_pad.md3"
+"origin" "1024 0 -40"
+"classname" "misc_model"
+}
+// entity 79
+{
+"classname" "misc_model"
+"origin" "352 1248 256"
+"model" "models/teleport_platform.md3"
+}
+// entity 80
+{
+"model" "models/teleport_platform.md3"
+"origin" "224 1248 256"
+"classname" "misc_model"
+}
+// entity 81
+{
+"classname" "misc_model"
+"origin" "-224 1248 256"
+"model" "models/teleport_platform.md3"
+}
+// entity 82
+{
+"model" "models/teleport_platform.md3"
+"origin" "-352 1248 256"
+"classname" "misc_model"
+}
+// entity 83
+{
+"classname" "misc_model"
+"origin" "-352 -1248 256"
+"model" "models/teleport_platform.md3"
+}
+// entity 84
+{
+"classname" "misc_model"
+"origin" "-224 -1248 256"
+"model" "models/teleport_platform.md3"
+}
+// entity 85
+{
+"model" "models/teleport_platform.md3"
+"origin" "224 -1248 256"
+"classname" "misc_model"
+}
+// entity 86
+{
+"classname" "misc_model"
+"origin" "352 -1248 256"
+"model" "models/teleport_platform.md3"
+}
+// entity 87
+{
+"classname" "item_health_large"
+"origin" "-208 208 -432"
+}
+// entity 88
+{
+"origin" "208 -208 -432"
+"classname" "item_health_large"
+}
+// entity 89
+{
+"classname" "item_armor_combat"
+"origin" "208 208 -432"
+}
+// entity 90
+{
+"origin" "-208 -208 -432"
+"classname" "item_armor_combat"
+}
+// entity 91
+{
+"classname" "trigger_push"
+"target" "t18"
+// brush 0
+{
+( 320 1024 312 ) ( 320 912 312 ) ( 320 912 256 ) common/caulk -20 9 0 0.031250 0.031189 0 0 0
+( 448 1024 312 ) ( 272 1024 312 ) ( 272 1024 256 ) common/caulk -27 9 0 0.031128 0.031189 0 0 0
+( 384 912 312 ) ( 384 1024 312 ) ( 384 1024 256 ) common/caulk -20 9 0 0.031250 0.031189 0 0 0
+( 272 960 312 ) ( 448 960 312 ) ( 448 960 256 ) common/caulk -27 9 0 0.031128 0.031189 0 0 0
+( 272 912 312 ) ( 272 1024 312 ) ( 448 1024 312 ) common/caulk -26 3 0 0.031250 0.031250 0 0 0
+( 448 1024 272 ) ( 272 1024 272 ) ( 272 912 272 ) common/caulk -26 3 0 0.031250 0.031250 0 0 0
+}
+}
+// entity 92
+{
+"classname" "target_position"
+"origin" "632 456 760"
+"targetname" "t18"
+}
+// entity 93
+{
+"angle" "45"
+"model" "models/bounce_pad.md3"
+"origin" "352 992 280"
+"classname" "misc_model"
+}
+// entity 94
+{
+"target" "t19"
+"classname" "trigger_push"
+// brush 0
+{
+( -448 -1024 272 ) ( -272 -1024 272 ) ( -272 -912 272 ) common/caulk -26 -60 -180 0.031250 0.031250 0 0 0
+( -272 -912 312 ) ( -272 -1024 312 ) ( -448 -1024 312 ) common/caulk -26 -60 -180 0.031250 0.031250 0 0 0
+( -272 -960 312 ) ( -448 -960 312 ) ( -448 -960 256 ) common/caulk -27 8 -180 0.031128 -0.031189 0 0 0
+( -384 -912 312 ) ( -384 -1024 312 ) ( -384 -1024 256 ) common/caulk 44 9 -180 0.031250 -0.031189 0 0 0
+( -448 -1024 312 ) ( -272 -1024 312 ) ( -272 -1024 256 ) common/caulk -27 8 -180 0.031128 -0.031189 0 0 0
+( -320 -1024 312 ) ( -320 -912 312 ) ( -320 -912 256 ) common/caulk 44 9 -180 0.031250 -0.031189 0 0 0
+}
+}
+// entity 95
+{
+"targetname" "t19"
+"origin" "-632 -456 760"
+"classname" "target_position"
+}
+// entity 96
+{
+"classname" "misc_model"
+"origin" "-352 -992 280"
+"model" "models/bounce_pad.md3"
+"angle" "225"
+}
+// entity 97
+{
+"target" "t20"
+"classname" "trigger_push"
+// brush 0
+{
+( -704 -512 -48 ) ( -704 -336 -48 ) ( -816 -336 -48 ) common/caulk -26 0 -90 0.031250 0.031250 0 0 0
+( -816 -336 -8 ) ( -704 -336 -8 ) ( -704 -512 -8 ) common/caulk -26 0 -90 0.031250 0.031250 0 0 0
+( -768 -336 -120 ) ( -768 -512 -120 ) ( -768 -512 -176 ) common/caulk -34 54 -180 0.031250 -0.031189 0 0 0
+( -816 -448 -8 ) ( -704 -448 -8 ) ( -704 -448 -64 ) common/caulk -20 53 0 0.031250 0.031189 0 0 0
+( -704 -512 -8 ) ( -704 -336 -8 ) ( -704 -336 -64 ) common/caulk -35 54 -180 0.031250 -0.031189 0 0 0
+( -704 -384 -8 ) ( -816 -384 -8 ) ( -816 -384 -64 ) common/caulk -20 53 0 0.031250 0.031189 0 0 0
+}
+}
+// entity 98
+{
+"targetname" "t20"
+"origin" "-328 -760 504"
+"classname" "target_position"
+}
+// entity 99
+{
+"classname" "misc_model"
+"origin" "-736 -416 -40"
+"model" "models/bounce_pad.md3"
+"angle" "45"
+}
+// entity 100
+{
+"target" "t21"
+"classname" "trigger_push"
+// brush 0
+{
+( 768 448 -8 ) ( 656 448 -8 ) ( 656 448 -64 ) common/caulk -20 -11 0 0.031250 0.031189 0 0 0
+( 768 320 -8 ) ( 768 496 -8 ) ( 768 496 -64 ) common/caulk -35 -10 -180 0.031250 -0.031189 0 0 0
+( 656 384 -8 ) ( 768 384 -8 ) ( 768 384 -64 ) common/caulk -20 -11 0 0.031250 0.031189 0 0 0
+( 704 496 -120 ) ( 704 320 -120 ) ( 704 320 -176 ) common/caulk -34 -10 -180 0.031250 -0.031189 0 0 0
+( 656 496 -8 ) ( 768 496 -8 ) ( 768 320 -8 ) common/caulk -25 1 -90 0.031250 0.031250 0 0 0
+( 768 320 -48 ) ( 768 496 -48 ) ( 656 496 -48 ) common/caulk -25 1 -90 0.031250 0.031250 0 0 0
+}
+}
+// entity 101
+{
+"classname" "target_position"
+"origin" "328 760 504"
+"targetname" "t21"
+}
+// entity 102
+{
+"angle" "225"
+"model" "models/bounce_pad.md3"
+"origin" "736 416 -40"
+"classname" "misc_model"
+}
+// entity 103
+{
+"targetname" "t11"
+"origin" "24 96 360"
+"classname" "target_position"
+}
+// entity 104
+{
+"classname" "misc_model"
+"origin" "-736 96 -40"
+"model" "models/bounce_pad.md3"
+"angle" "90"
+}
+// entity 105
+{
+"classname" "item_armor_combat"
+"origin" "-208 0 -432"
+}
+// entity 106
+{
+"origin" "0 208 -432"
+"classname" "item_health_large"
+}
+// entity 107
+{
+"origin" "208 0 -432"
+"classname" "item_armor_combat"
+}
+// entity 108
+{
+"classname" "item_health_large"
+"origin" "0 -208 -432"
+}
+// entity 109
+{
+"classname" "item_armor_shard"
+"origin" "1112 32 -32"
+}
+// entity 110
+{
+"origin" "1112 -32 -32"
+"classname" "item_armor_shard"
+}
+// entity 111
+{
+"classname" "item_armor_shard"
+"origin" "1112 -96 -32"
+}
+// entity 112
+{
+"origin" "1112 96 -32"
+"classname" "item_armor_shard"
+}
+// entity 113
+{
+"classname" "item_armor_shard"
+"origin" "-1112 32 -32"
+}
+// entity 114
+{
+"origin" "-1112 96 -32"
+"classname" "item_armor_shard"
+}
+// entity 115
+{
+"classname" "item_armor_shard"
+"origin" "-1112 -32 -32"
+}
+// entity 116
+{
+"origin" "-1112 -96 -32"
+"classname" "item_armor_shard"
+}
diff --git a/assets/maps/em_eat.map b/assets/maps/em_eat.map
new file mode 100644
index 00000000..fb4be296
--- /dev/null
+++ b/assets/maps/em_eat.map
@@ -0,0 +1,369 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( 264 -272 64 ) ( -56 -272 64 ) ( 264 -272 0 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.078125 0 0 0
+( -272 -8 88 ) ( -272 -264 88 ) ( -272 -264 24 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000868 0.078125 0 0 0
+( 336 -280 88 ) ( 336 -24 88 ) ( 336 -24 24 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000868 0.078125 0 0 0
+( -120 -280 64 ) ( 200 -280 64 ) ( 200 -280 0 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.078125 0 0 0
+( -128 -272 80 ) ( -128 -16 80 ) ( 192 -16 80 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.007812 0 0 0
+( 192 -16 0 ) ( -128 -16 0 ) ( -128 -272 0 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.007812 0 0 0
+}
+// brush 1
+{
+( 336 200 88 ) ( 336 -56 88 ) ( 336 200 24 ) map/lab_games/lg_style_01_wall_green 4096 0 0 -0.066406 0.078125 0 0 0
+( 336 280 64 ) ( 16 280 64 ) ( 16 280 0 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000977 0.078125 0 0 0
+( 344 -64 64 ) ( 344 192 64 ) ( 344 192 0 ) map/lab_games/lg_style_01_wall_green 4096 0 0 -0.066406 0.078125 0 0 0
+( 16 -280 64 ) ( 336 -280 64 ) ( 336 -280 0 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000977 0.078125 0 0 0
+( 16 -56 80 ) ( 16 200 80 ) ( 336 200 80 ) map/lab_games/lg_style_01_wall_green 0 512 0 -0.000977 0.531250 0 0 0
+( 336 200 0 ) ( 16 200 0 ) ( 16 -56 0 ) map/lab_games/lg_style_01_wall_green 0 512 0 -0.000977 0.531250 0 0 0
+}
+// brush 2
+{
+( -176 272 64 ) ( 144 272 64 ) ( -176 272 0 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.078125 0 0 0
+( -272 280 88 ) ( -272 24 88 ) ( -272 24 24 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000868 0.078125 0 0 0
+( 96 280 64 ) ( -224 280 64 ) ( -224 280 0 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.078125 0 0 0
+( 336 8 88 ) ( 336 264 88 ) ( 336 264 24 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000868 0.078125 0 0 0
+( -256 16 80 ) ( -256 272 80 ) ( 64 272 80 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.007812 0 0 0
+( 64 272 0 ) ( -256 272 0 ) ( -256 16 0 ) map/lab_games/lg_style_01_wall_green 5093 0 0 -0.065972 0.007812 0 0 0
+}
+// brush 3
+{
+( -272 -128 88 ) ( -272 128 88 ) ( -272 -128 24 ) map/lab_games/lg_style_01_wall_green 4096 0 0 -0.066406 0.078125 0 0 0
+( -280 136 64 ) ( -280 -120 64 ) ( -280 -120 0 ) map/lab_games/lg_style_01_wall_green 4096 0 0 -0.066406 0.078125 0 0 0
+( -24 280 64 ) ( -344 280 64 ) ( -344 280 0 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000977 0.078125 0 0 0
+( -328 -280 64 ) ( -8 -280 64 ) ( -8 -280 0 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.000977 0.078125 0 0 0
+( -336 -128 80 ) ( -336 128 80 ) ( -16 128 80 ) map/lab_games/lg_style_01_wall_green 0 512 0 -0.000977 0.531250 0 0 0
+( -16 128 0 ) ( -336 128 0 ) ( -336 -128 0 ) map/lab_games/lg_style_01_wall_green 0 512 0 -0.000977 0.531250 0 0 0
+}
+// brush 4
+{
+( 128 128 -8 ) ( -192 128 -8 ) ( -192 -128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -280 64 ) ( 256 -280 64 ) ( 256 -280 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 344 -264 64 ) ( 344 -8 64 ) ( 344 -8 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 280 64 ) ( -320 280 64 ) ( -320 280 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -280 264 64 ) ( -280 8 64 ) ( -280 8 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 128 0 ) ( 128 128 0 ) ( -192 -128 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 5
+{
+( -192 -128 88 ) ( -192 128 88 ) ( 128 128 88 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 -280 80 ) ( 256 -280 80 ) ( 256 -280 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 344 -264 80 ) ( 344 -8 80 ) ( 344 -8 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 0 280 80 ) ( -320 280 80 ) ( -320 280 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -280 264 80 ) ( -280 8 80 ) ( -280 8 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -192 128 80 ) ( -192 -128 80 ) ( 128 128 80 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 6
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_dn 756 485 0 -0.507812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_dn 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_dn 485 0 0 -0.460938 0.007812 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_dn 756 0 0 -0.507812 0.007812 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_dn 485 0 0 -0.460938 0.007812 0 0 0
+( 904 224 -168 ) ( 1424 224 -168 ) ( 904 -248 -168 ) map/lab_games/sky/lg_sky_02_dn 756 485 0 -0.507812 0.460938 0 0 0
+}
+// brush 7
+{
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_up 756 485 0 -0.507812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_up 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_up 485 0 0 -0.460938 0.007812 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_up 756 0 0 -0.507812 0.007812 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_up 485 0 0 -0.460938 0.007812 0 0 0
+( 904 224 192 ) ( 904 -248 192 ) ( 1424 224 192 ) map/lab_games/sky/lg_sky_02_up 756 485 0 -0.507812 0.460938 0 0 0
+}
+// brush 8
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_bk 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_bk 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_bk 756 544 0 -0.507812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 -240 200 ) ( 904 -240 200 ) ( 1424 -240 184 ) map/lab_games/sky/lg_sky_02_bk 756 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 9
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_rt 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_rt 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_rt 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_rt 485 544 0 -0.460938 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_rt 0 544 0 -0.007812 0.367188 0 0 0
+( 1416 224 200 ) ( 1416 -248 200 ) ( 1416 224 184 ) map/lab_games/sky/lg_sky_02_rt 485 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 10
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_ft 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_ft 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_ft 756 544 0 -0.507812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 904 216 200 ) ( 1424 216 200 ) ( 904 216 184 ) map/lab_games/sky/lg_sky_02_ft 756 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 11
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_lf 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_lf 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_lf 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_lf 0 544 0 -0.007812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_lf 485 544 0 -0.460938 0.367188 0 0 0
+( 912 -248 200 ) ( 912 224 200 ) ( 912 -248 184 ) map/lab_games/sky/lg_sky_02_lf 485 544 0 -0.460938 0.367188 0 0 0
+}
+}
+// entity 1
+{
+"classname" "info_player_start"
+"origin" "-232 0 24"
+"spawn_orientation_segment" "40"
+"angle" "0"
+"randomAngleRange" "0"
+}
+// entity 2
+{
+"light" "100"
+"origin" "-176 184 64"
+"classname" "light"
+}
+// entity 3
+{
+"classname" "apple_reward"
+"origin" "-240 240 16"
+}
+// entity 4
+{
+"origin" "-112 128 16"
+"classname" "apple_reward"
+}
+// entity 5
+{
+"classname" "apple_reward"
+"origin" "-112 240 16"
+}
+// entity 6
+{
+"origin" "-240 -128 16"
+"classname" "apple_reward"
+}
+// entity 7
+{
+"origin" "32 240 16"
+"classname" "apple_reward"
+}
+// entity 8
+{
+"classname" "apple_reward"
+"origin" "168 240 16"
+}
+// entity 9
+{
+"origin" "304 128 16"
+"classname" "apple_reward"
+}
+// entity 10
+{
+"classname" "apple_reward"
+"origin" "168 128 16"
+}
+// entity 11
+{
+"origin" "56 0 16"
+"classname" "apple_reward"
+}
+// entity 12
+{
+"classname" "apple_reward"
+"origin" "-112 0 16"
+}
+// entity 13
+{
+"origin" "32 -128 16"
+"classname" "apple_reward"
+}
+// entity 14
+{
+"classname" "apple_reward"
+"origin" "168 -128 16"
+}
+// entity 15
+{
+"origin" "-112 -128 16"
+"classname" "apple_reward"
+}
+// entity 16
+{
+"classname" "apple_reward"
+"origin" "-240 -240 16"
+}
+// entity 17
+{
+"origin" "32 -240 16"
+"classname" "apple_reward"
+}
+// entity 18
+{
+"classname" "apple_reward"
+"origin" "-112 -240 16"
+}
+// entity 19
+{
+"origin" "168 -240 16"
+"classname" "apple_reward"
+}
+// entity 20
+{
+"classname" "apple_reward"
+"origin" "304 -128 16"
+}
+// entity 21
+{
+"origin" "-240 128 16"
+"classname" "apple_reward"
+}
+// entity 22
+{
+"model" "models/teleport_platform.md3"
+"origin" "280 0 0"
+"classname" "misc_model"
+}
+// entity 23
+{
+"classname" "misc_teleporter_dest"
+"targetname" "teleport_dest_start"
+"origin" "-232 0 24"
+"spawn_orientation_segment" "40"
+}
+// entity 24
+{
+"origin" "32 128 16"
+"classname" "apple_reward"
+}
+// entity 25
+{
+"classname" "light"
+"origin" "-32 184 64"
+"light" "100"
+}
+// entity 26
+{
+"light" "100"
+"origin" "96 184 64"
+"classname" "light"
+}
+// entity 27
+{
+"classname" "light"
+"origin" "248 184 64"
+"light" "100"
+}
+// entity 28
+{
+"light" "100"
+"origin" "248 -184 64"
+"classname" "light"
+}
+// entity 29
+{
+"classname" "light"
+"origin" "96 -184 64"
+"light" "100"
+}
+// entity 30
+{
+"light" "100"
+"origin" "-32 -184 64"
+"classname" "light"
+}
+// entity 31
+{
+"classname" "light"
+"origin" "-176 -184 64"
+"light" "100"
+}
+// entity 32
+{
+"light" "100"
+"origin" "-176 0 64"
+"classname" "light"
+}
+// entity 33
+{
+"classname" "light"
+"origin" "-32 0 64"
+"light" "100"
+}
+// entity 34
+{
+"light" "100"
+"origin" "96 0 64"
+"classname" "light"
+}
+// entity 35
+{
+"classname" "light"
+"origin" "248 0 64"
+"light" "100"
+}
+// entity 36
+{
+"classname" "trigger_multiple"
+"target" "teleport_dest_start"
+"id" "1"
+// brush 0
+{
+( 256 24 56 ) ( 256 -24 56 ) ( 256 -24 8 ) common/caulk 0 0 0 0.031250 0.031250 0 0 0
+( 304 24 56 ) ( 256 24 56 ) ( 256 24 8 ) common/caulk 0 0 0 0.031250 0.031250 0 0 0
+( 304 -24 56 ) ( 304 24 56 ) ( 304 24 8 ) common/caulk 0 0 0 0.031250 0.031250 0 0 0
+( 256 -24 56 ) ( 304 -24 56 ) ( 304 -24 8 ) common/caulk 0 0 0 0.031250 0.031250 0 0 0
+( 256 -24 56 ) ( 256 24 56 ) ( 304 24 56 ) common/caulk 0 0 0 0.031250 0.031250 0 0 0
+( 304 24 8 ) ( 256 24 8 ) ( 256 -24 8 ) common/caulk 0 0 0 0.031250 0.031250 0 0 0
+}
+}
+// entity 37
+{
+"classname" "_skybox"
+"origin" "1136 -16 -128"
+}
+// entity 38
+{
+"model" "models/stadium.md3"
+"origin" "1136 -16 -66"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 39
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "1160 -56 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 40
+{
+"classname" "misc_model"
+"origin" "1208 -32 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 41
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "1168 -24 -96"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 42
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "1112 -8 -152"
+"classname" "misc_model"
+"angle" "159"
+}
diff --git a/assets/maps/em_non_match.map b/assets/maps/em_non_match.map
new file mode 100644
index 00000000..76926463
--- /dev/null
+++ b/assets/maps/em_non_match.map
@@ -0,0 +1,479 @@
+// entity 0
+{
+"procedural_obj" "3"
+"classname" "worldspawn"
+// brush 0
+{
+( -256 256 0 ) ( -8 256 0 ) ( -256 -256 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -264 264 32 ) ( -264 -248 32 ) ( -264 -248 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -8 136 32 ) ( -256 136 32 ) ( -256 136 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 136 -256 32 ) ( 136 256 32 ) ( 136 256 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -240 -264 32 ) ( 8 -264 32 ) ( 8 -264 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -8 256 -8 ) ( -256 256 -8 ) ( -256 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 1
+{
+( 88 -32 0 ) ( 40 -32 0 ) ( 40 -88 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 40 -88 10 ) ( 40 -32 10 ) ( 88 -32 10 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 48 -96 32 ) ( 96 -96 32 ) ( 96 -96 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 96 -96 32 ) ( 96 -40 32 ) ( 96 -40 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 80 -32 32 ) ( 32 -32 32 ) ( 32 -32 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 32 -24 32 ) ( 32 -80 32 ) ( 32 -80 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+}
+// brush 2
+{
+( 384 384 128 ) ( 384 -128 128 ) ( 632 384 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.190000 0 0 0
+( 376 384 32 ) ( 376 -128 32 ) ( 376 -128 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.190000 0 0 0
+( 632 136 32 ) ( 384 136 32 ) ( 384 136 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.190000 0 0 0
+( 648 -128 32 ) ( 648 384 32 ) ( 648 384 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.190000 0 0 0
+( 384 -256 32 ) ( 632 -256 32 ) ( 632 -256 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.190000 0 0 0
+( 384 -128 136 ) ( 384 384 136 ) ( 632 384 136 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.190000 0 0 0
+}
+// brush 3
+{
+( 384 384 0 ) ( 632 384 0 ) ( 384 -128 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 376 384 32 ) ( 376 -128 32 ) ( 376 -128 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 632 136 32 ) ( 384 136 32 ) ( 384 136 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 648 -128 32 ) ( 648 384 32 ) ( 648 384 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 384 -256 32 ) ( 632 -256 32 ) ( 632 -256 16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 632 384 -8 ) ( 384 384 -8 ) ( 384 -128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 4
+{
+( 248 8 136 ) ( 248 -136 136 ) ( 248 -136 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.190000 0 0 0
+( 376 8 136 ) ( 312 8 136 ) ( 312 8 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.190000 0 0 0
+( 376 -136 136 ) ( 376 8 136 ) ( 376 8 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.190000 0 0 0
+( 312 -136 136 ) ( 376 -136 136 ) ( 376 -136 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.190000 0 0 0
+( 312 -136 136 ) ( 312 8 136 ) ( 376 8 136 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.190000 0 0 0
+( 376 8 128 ) ( 312 8 128 ) ( 312 -136 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.190000 0 0 0
+}
+// brush 5
+{
+( 376 8 -8 ) ( 248 8 -8 ) ( 248 -136 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 248 -136 0 ) ( 248 8 0 ) ( 376 8 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 248 -136 0 ) ( 376 -136 0 ) ( 376 -136 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 376 -136 0 ) ( 376 8 0 ) ( 376 8 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 376 8 0 ) ( 248 8 0 ) ( 248 8 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 248 8 0 ) ( 248 -136 0 ) ( 248 -136 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 6
+{
+( 8 -232 0 ) ( 8 -152 0 ) ( 0 -152 0 ) map/poltergeist -48 0 0 0.500000 0.500000 0 0 0
+( 8 -152 80 ) ( 8 -232 80 ) ( 0 -232 80 ) map/poltergeist -48 0 0 0.500000 0.500000 0 0 0
+( 8 -224 80 ) ( 8 -224 0 ) ( 0 -224 0 ) map/poltergeist -48 0 0 0.500000 0.500000 0 0 0
+( 8 -152 80 ) ( 8 -152 0 ) ( 8 -232 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 8 -160 0 ) ( 8 -160 80 ) ( 0 -160 80 ) map/poltergeist -48 0 0 0.500000 0.500000 0 0 0
+( 0 -152 0 ) ( 0 -152 80 ) ( 0 -232 80 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 7
+{
+( 64 -160 0 ) ( 64 -152 0 ) ( -8 -152 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( -8 -152 80 ) ( 64 -152 80 ) ( 64 -160 80 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 0 -152 48 ) ( 0 -160 48 ) ( 0 -160 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( -8 -160 48 ) ( 64 -160 48 ) ( 64 -160 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 80 -160 48 ) ( 80 -152 48 ) ( 80 -152 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 64 -152 48 ) ( -8 -152 48 ) ( -8 -152 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 8
+{
+( -256 256 128 ) ( -256 -256 128 ) ( -8 256 128 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -264 256 32 ) ( -264 -256 32 ) ( -264 -256 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 0 136 32 ) ( -248 136 32 ) ( -248 136 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 136 -256 32 ) ( 136 256 32 ) ( 136 256 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -232 -264 32 ) ( 16 -264 32 ) ( 16 -264 16 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -256 -256 136 ) ( -256 256 136 ) ( -8 256 136 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 9
+{
+( 128 256 0 ) ( -120 256 0 ) ( -120 -256 0 ) map/lab_games/lg_style_01_wall_blue 4 512 0 -0.001953 0.500000 0 0 0
+( -120 -256 128 ) ( -120 256 128 ) ( 128 256 128 ) map/lab_games/lg_style_01_wall_blue 4 512 0 -0.001953 0.500000 0 0 0
+( -128 -264 8 ) ( 120 -264 8 ) ( 120 -264 -8 ) map/lab_games/lg_style_01_wall_blue 4 0 0 -0.001953 0.125000 0 0 0
+( 136 16 8 ) ( 136 528 8 ) ( 136 528 -8 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 128 136 8 ) ( -120 136 8 ) ( -120 136 -8 ) map/lab_games/lg_style_01_wall_blue 4 0 0 -0.001953 0.125000 0 0 0
+( 128 592 8 ) ( 128 80 8 ) ( 128 592 -8 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 10
+{
+( -8 248 0 ) ( -256 248 0 ) ( -256 -264 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.007812 0 0 0
+( -256 -264 128 ) ( -256 248 128 ) ( -8 248 128 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.007812 0 0 0
+( -256 -264 32 ) ( -8 -264 32 ) ( -8 -264 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 128 -248 32 ) ( 128 264 32 ) ( 128 264 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.003906 0.125000 0 0 0
+( -256 248 32 ) ( -256 -264 32 ) ( -256 -264 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.003906 0.125000 0 0 0
+( -8 -256 32 ) ( -256 -256 32 ) ( -8 -256 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 11
+{
+( -8 136 0 ) ( -256 136 0 ) ( -256 -376 0 ) map/lab_games/lg_style_01_wall_blue 0 -1 0 -0.125000 0.007812 0 0 0
+( -256 -376 128 ) ( -256 136 128 ) ( -8 136 128 ) map/lab_games/lg_style_01_wall_blue 0 -1 0 -0.125000 0.007812 0 0 0
+( 136 -368 32 ) ( 136 144 32 ) ( 136 144 16 ) map/lab_games/lg_style_01_wall_blue -2 0 0 -0.003906 0.125000 0 0 0
+( 128 136 32 ) ( -120 136 32 ) ( -120 136 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( -256 136 32 ) ( -256 -376 32 ) ( -256 -376 16 ) map/lab_games/lg_style_01_wall_blue -2 0 0 -0.003906 0.125000 0 0 0
+( -256 128 32 ) ( -8 128 32 ) ( -256 128 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 12
+{
+( -16 256 0 ) ( -264 256 0 ) ( -264 -256 0 ) map/lab_games/lg_style_01_wall_blue 0 512 0 -0.001953 0.500000 0 0 0
+( -264 -256 128 ) ( -264 256 128 ) ( -16 256 128 ) map/lab_games/lg_style_01_wall_blue 0 512 0 -0.001953 0.500000 0 0 0
+( -264 -256 32 ) ( -16 -256 32 ) ( -16 -256 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.001953 0.125000 0 0 0
+( -16 136 32 ) ( -264 136 32 ) ( -264 136 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.001953 0.125000 0 0 0
+( -264 128 32 ) ( -264 -384 32 ) ( -264 -384 16 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.125000 0.125000 0 0 0
+( -256 -248 32 ) ( -256 264 32 ) ( -256 -248 16 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 13
+{
+( 376 8 0 ) ( 320 8 0 ) ( 320 0 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.007812 0 0 0
+( 320 0 128 ) ( 320 8 128 ) ( 376 8 128 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.007812 0 0 0
+( 328 0 32 ) ( 384 0 32 ) ( 384 0 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 376 0 32 ) ( 376 8 32 ) ( 376 8 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+( 376 8 32 ) ( 320 8 32 ) ( 320 8 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 256 8 32 ) ( 256 0 32 ) ( 256 0 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+}
+// brush 14
+{
+( 256 8 0 ) ( 248 8 0 ) ( 248 -128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+( 248 -128 128 ) ( 248 8 128 ) ( 256 8 128 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+( 248 -136 128 ) ( 256 -136 128 ) ( 256 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+( 256 -128 128 ) ( 256 8 128 ) ( 256 8 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 256 8 128 ) ( 248 8 128 ) ( 248 8 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+( 248 16 128 ) ( 248 -120 128 ) ( 248 -120 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 15
+{
+( 384 -512 32 ) ( 384 0 32 ) ( 384 -512 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.128906 0.125000 0 0 0
+( 376 232 32 ) ( 376 -280 32 ) ( 376 -280 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.128906 0.125000 0 0 0
+( 624 128 32 ) ( 376 128 32 ) ( 376 128 16 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.125000 0 0 0
+( 368 0 32 ) ( 616 0 32 ) ( 616 0 16 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.125000 0 0 0
+( 376 -256 128 ) ( 376 256 128 ) ( 624 256 128 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.257812 0 0 0
+( 624 256 0 ) ( 376 256 0 ) ( 376 -256 0 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.257812 0 0 0
+}
+// brush 16
+{
+( 632 136 0 ) ( 384 136 0 ) ( 384 -376 0 ) map/lab_games/lg_style_01_wall_blue 0 -1 0 -0.125000 0.007812 0 0 0
+( 384 -376 128 ) ( 384 136 128 ) ( 632 136 128 ) map/lab_games/lg_style_01_wall_blue 0 -1 0 -0.125000 0.007812 0 0 0
+( 640 -376 32 ) ( 640 136 32 ) ( 640 136 16 ) map/lab_games/lg_style_01_wall_blue -2 0 0 -0.003906 0.125000 0 0 0
+( 632 136 32 ) ( 384 136 32 ) ( 384 136 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 384 136 32 ) ( 384 -376 32 ) ( 384 -376 16 ) map/lab_games/lg_style_01_wall_blue -2 0 0 -0.003906 0.125000 0 0 0
+( 384 128 32 ) ( 632 128 32 ) ( 384 128 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 17
+{
+( 640 384 0 ) ( 392 384 0 ) ( 392 -128 0 ) map/lab_games/lg_style_01_wall_blue -1015 768 0 -0.001953 0.500000 0 0 0
+( 392 -128 128 ) ( 392 384 128 ) ( 640 384 128 ) map/lab_games/lg_style_01_wall_blue -1015 768 0 -0.001953 0.500000 0 0 0
+( 392 -256 32 ) ( 640 -256 32 ) ( 640 -256 16 ) map/lab_games/lg_style_01_wall_blue -1015 0 0 -0.001953 0.125000 0 0 0
+( 648 -128 32 ) ( 648 384 32 ) ( 648 384 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 640 136 32 ) ( 392 136 32 ) ( 392 136 16 ) map/lab_games/lg_style_01_wall_blue -1015 0 0 -0.001953 0.125000 0 0 0
+( 640 8 32 ) ( 640 -504 32 ) ( 640 8 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 18
+{
+( 632 -248 32 ) ( 384 -248 32 ) ( 632 -248 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 384 256 32 ) ( 384 -256 32 ) ( 384 -256 16 ) map/lab_games/lg_style_01_wall_blue 1 0 0 -0.003906 0.125000 0 0 0
+( 640 -256 32 ) ( 640 256 32 ) ( 640 256 16 ) map/lab_games/lg_style_01_wall_blue 1 0 0 -0.003906 0.125000 0 0 0
+( 384 -256 32 ) ( 632 -256 32 ) ( 632 -256 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 384 -256 128 ) ( 384 256 128 ) ( 632 256 128 ) map/lab_games/lg_style_01_wall_blue 0 1 0 -0.125000 0.007812 0 0 0
+( 632 256 0 ) ( 384 256 0 ) ( 384 -256 0 ) map/lab_games/lg_style_01_wall_blue 0 1 0 -0.125000 0.007812 0 0 0
+}
+// brush 19
+{
+( 624 0 0 ) ( 376 0 0 ) ( 376 -512 0 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.250000 0 0 0
+( 376 -512 128 ) ( 376 0 128 ) ( 624 0 128 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.250000 0 0 0
+( 368 -256 32 ) ( 616 -256 32 ) ( 616 -256 16 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.125000 0 0 0
+( 624 -128 32 ) ( 376 -128 32 ) ( 376 -128 16 ) map/lab_games/lg_style_01_wall_blue 98 0 0 -0.121094 0.125000 0 0 0
+( 376 -24 32 ) ( 376 -536 32 ) ( 376 -536 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 384 -632 32 ) ( 384 -120 32 ) ( 384 -632 16 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+}
+// brush 20
+{
+( 376 -128 0 ) ( 320 -128 0 ) ( 320 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.007812 0 0 0
+( 320 -136 128 ) ( 320 -128 128 ) ( 376 -128 128 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.007812 0 0 0
+( 344 -136 32 ) ( 400 -136 32 ) ( 400 -136 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 376 -136 32 ) ( 376 -128 32 ) ( 376 -128 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+( 320 -128 32 ) ( 264 -128 32 ) ( 264 -128 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.125000 0.125000 0 0 0
+( 256 -136 32 ) ( 256 -144 32 ) ( 256 -144 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.125000 0 0 0
+}
+// brush 21
+{
+( 904 224 -168 ) ( 1424 224 -168 ) ( 904 -248 -168 ) map/lab_games/sky/lg_sky_02_dn 756 485 0 -0.507812 0.460938 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_dn 485 0 0 -0.460938 0.007812 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_dn 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_dn 485 0 0 -0.460938 0.007812 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_dn 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_dn 756 485 0 -0.507812 0.460938 0 0 0
+}
+// brush 22
+{
+( 904 224 192 ) ( 904 -248 192 ) ( 1424 224 192 ) map/lab_games/sky/lg_sky_02_up 756 485 0 -0.507812 0.460938 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_up 485 0 0 -0.460938 0.007812 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_up 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_up 485 0 0 -0.460938 0.007812 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_up 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_up 756 485 0 -0.507812 0.460938 0 0 0
+}
+// brush 23
+{
+( 1424 -240 200 ) ( 904 -240 200 ) ( 1424 -240 184 ) map/lab_games/sky/lg_sky_02_bk 756 544 0 -0.507812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_bk 756 544 0 -0.507812 0.367188 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_bk 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_bk 756 0 0 -0.507812 0.007812 0 0 0
+}
+// brush 24
+{
+( 1416 224 200 ) ( 1416 -248 200 ) ( 1416 224 184 ) map/lab_games/sky/lg_sky_02_rt 485 544 0 -0.460938 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_rt 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_rt 485 544 0 -0.460938 0.367188 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_rt 0 544 0 -0.007812 0.367188 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_rt 0 485 0 -0.007812 0.460938 0 0 0
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_rt 0 485 0 -0.007812 0.460938 0 0 0
+}
+// brush 25
+{
+( 904 216 200 ) ( 1424 216 200 ) ( 904 216 184 ) map/lab_games/sky/lg_sky_02_ft 756 544 0 -0.507812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_ft 756 544 0 -0.507812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_ft 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_ft 756 0 0 -0.507812 0.007812 0 0 0
+}
+// brush 26
+{
+( 912 -248 200 ) ( 912 224 200 ) ( 912 -248 184 ) map/lab_games/sky/lg_sky_02_lf 485 544 0 -0.460938 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_lf 485 544 0 -0.460938 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_lf 0 544 0 -0.007812 0.367188 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_lf 0 544 0 -0.007812 0.367188 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_lf 0 485 0 -0.007812 0.460938 0 0 0
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_lf 0 485 0 -0.007812 0.460938 0 0 0
+}
+// brush 27
+{
+( 0 104 48 ) ( 0 32 48 ) ( 0 32 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 16 96 48 ) ( 8 96 48 ) ( 8 96 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 8 24 32 ) ( 8 96 32 ) ( 8 96 -16 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 8 32 48 ) ( 16 32 48 ) ( 16 32 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 8 24 80 ) ( 8 96 80 ) ( 16 96 80 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 16 96 0 ) ( 8 96 0 ) ( 8 24 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 28
+{
+( 80 96 0 ) ( 72 96 0 ) ( 72 24 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 24 80 ) ( 72 96 80 ) ( 80 96 80 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 32 48 ) ( 80 32 48 ) ( 80 32 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 80 24 48 ) ( 80 96 48 ) ( 80 96 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 80 96 48 ) ( 72 96 48 ) ( 72 96 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 96 48 ) ( 72 24 48 ) ( 72 24 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 29
+{
+( 72 104 48 ) ( 0 104 48 ) ( 0 104 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 80 96 48 ) ( 80 104 48 ) ( 80 104 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( -8 96 48 ) ( 64 96 48 ) ( 64 96 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 0 104 48 ) ( 0 96 48 ) ( 0 96 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 0 104 80 ) ( 72 104 80 ) ( 72 96 80 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 96 0 ) ( 72 104 0 ) ( 0 104 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+}
+// brush 30
+{
+( 72 24 0 ) ( 72 32 0 ) ( 0 32 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 0 32 80 ) ( 72 32 80 ) ( 72 24 80 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 0 32 48 ) ( 0 24 48 ) ( 0 24 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 8 24 48 ) ( 80 24 48 ) ( 80 24 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 80 24 48 ) ( 80 32 48 ) ( 80 32 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 64 32 48 ) ( -8 32 48 ) ( -8 32 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+}
+// brush 31
+{
+( 64 -224 48 ) ( -8 -224 48 ) ( -8 -224 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 80 -232 48 ) ( 80 -224 48 ) ( 80 -224 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 8 -232 48 ) ( 80 -232 48 ) ( 80 -232 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 0 -224 48 ) ( 0 -232 48 ) ( 0 -232 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 0 -224 80 ) ( 72 -224 80 ) ( 72 -232 80 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 -232 0 ) ( 72 -224 0 ) ( 0 -224 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+}
+// brush 32
+{
+( 80 -160 0 ) ( 72 -160 0 ) ( 72 -232 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 -232 80 ) ( 72 -160 80 ) ( 80 -160 80 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 -224 48 ) ( 80 -224 48 ) ( 80 -224 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 80 -232 48 ) ( 80 -160 48 ) ( 80 -160 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+( 80 -160 48 ) ( 72 -160 48 ) ( 72 -160 0 ) map/poltergeist -16 0 0 0.500000 0.500000 0 0 0
+( 72 -160 48 ) ( 72 -232 48 ) ( 72 -232 0 ) map/poltergeist 0 0 0 0.500000 0.500000 0 0 0
+}
+}
+// entity 1
+{
+"spawn_id" "1"
+"origin" "-232 -64 24"
+"classname" "info_player_start"
+"spawn_orientation_segment" "20"
+"randomAngleRange" "0"
+}
+// entity 2
+{
+"target" "teleport_dest_second_room"
+"classname" "trigger_teleport"
+"id" "1"
+// brush 0
+{
+( 80 -40 8 ) ( 48 -40 8 ) ( 48 -80 8 ) common/caulk -12 -16 0 0.062500 0.062500 0 4 0
+( 48 -80 48 ) ( 48 -40 48 ) ( 80 -40 48 ) common/caulk -12 -16 0 0.062500 0.062500 0 4 0
+( 48 -88 24 ) ( 80 -88 24 ) ( 80 -88 16 ) common/caulk -12 16 0 0.062500 0.062500 0 4 0
+( 88 -80 24 ) ( 88 -40 24 ) ( 88 -40 16 ) common/caulk 16 16 0 0.062500 0.062500 0 4 0
+( 80 -40 24 ) ( 48 -40 24 ) ( 48 -40 16 ) common/caulk -12 16 0 0.062500 0.062500 0 4 0
+( 40 -40 24 ) ( 40 -80 24 ) ( 40 -80 16 ) common/caulk 16 16 0 0.062500 0.062500 0 4 0
+}
+}
+// entity 3
+{
+"classname" "light"
+"origin" "-128 64 112"
+"light" "150"
+}
+// entity 4
+{
+"classname" "light"
+"origin" "-128 -192 112"
+"light" "150"
+}
+// entity 5
+{
+"classname" "light"
+"origin" "40 64 104"
+"light" "200"
+}
+// entity 6
+{
+"light" "150"
+"origin" "592 -64 112"
+"classname" "light"
+}
+// entity 7
+{
+"classname" "misc_teleporter_dest"
+"origin" "288 -64 48"
+"targetname" "teleport_dest_second_room"
+"spawn_orientation_segment" "20"
+}
+// entity 8
+{
+"classname" "light"
+"origin" "464 24 112"
+"light" "150"
+}
+// entity 9
+{
+"origin" "600 64 48"
+"classname" "pickup_choice_1"
+}
+// entity 10
+{
+"classname" "pickup_choice_2"
+"origin" "600 -192 48"
+}
+// entity 11
+{
+"light" "150"
+"origin" "464 -168 112"
+"classname" "light"
+}
+// entity 12
+{
+"light" "200"
+"origin" "40 -192 104"
+"classname" "light"
+}
+// entity 13
+{
+"classname" "pickup_example_2"
+"origin" "40 -192 40"
+}
+// entity 14
+{
+"origin" "40 64 40"
+"classname" "pickup_example_1"
+}
+// entity 15
+{
+"light" "150"
+"origin" "344 -64 112"
+"classname" "light"
+}
+// entity 16
+{
+"origin" "1136 -16 -128"
+"classname" "_skybox"
+}
+// entity 17
+{
+"angle" "90"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"classname" "misc_model"
+"origin" "1136 -16 -82"
+"model" "models/stadium.md3"
+}
+// entity 18
+{
+"angle" "113"
+"classname" "misc_model"
+"origin" "1160 -56 -80"
+"model" "models/signal_line.md3"
+"modelscale" "1"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+}
+// entity 19
+{
+"angle" "38"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"modelscale" "1.1"
+"model" "models/signal_line.md3"
+"origin" "1208 -32 -152"
+"classname" "misc_model"
+}
+// entity 20
+{
+"modelscale" "0.61"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"model" "models/signal_line.md3"
+"origin" "1168 -24 -88"
+"classname" "misc_model"
+"angle" "28"
+}
+// entity 21
+{
+"angle" "159"
+"classname" "misc_model"
+"origin" "1112 -8 -152"
+"model" "models/signal_line.md3"
+"modelscale" "0.6"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+}
+// entity 22
+{
+"model" "models/pickup_platform.md3"
+"origin" "600 64 0"
+"classname" "misc_model"
+}
+// entity 23
+{
+"classname" "misc_model"
+"origin" "600 -192 0"
+"model" "models/pickup_platform.md3"
+}
+// entity 24
+{
+"classname" "misc_model"
+"origin" "40 64 0"
+"model" "models/pickup_platform.md3"
+}
+// entity 25
+{
+"model" "models/pickup_platform.md3"
+"origin" "40 -192 0"
+"classname" "misc_model"
+}
+// entity 26
+{
+"model" "models/teleport_platform.md3"
+"origin" "64 -64 0"
+"classname" "misc_model"
+}
diff --git a/assets/maps/em_tmaze_a1.map b/assets/maps/em_tmaze_a1.map
new file mode 100644
index 00000000..967176cf
--- /dev/null
+++ b/assets/maps/em_tmaze_a1.map
@@ -0,0 +1,238 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( -200 208 8 ) ( -200 80 8 ) ( -200 80 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 56 128 8 ) ( -72 128 8 ) ( -72 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -72 64 8 ) ( 56 64 8 ) ( 56 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -72 64 96 ) ( -72 192 96 ) ( 56 192 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( 56 192 0 ) ( -72 192 0 ) ( -72 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( -192 16 -24 ) ( -192 144 -24 ) ( -192 16 -32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 1
+{
+( -200 152 8 ) ( -200 24 8 ) ( -200 24 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.001563 0.093750 0 0 0
+( 96 136 8 ) ( -32 136 8 ) ( -32 136 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( 136 8 8 ) ( 136 136 8 ) ( 136 136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.001563 0.093750 0 0 0
+( -64 8 96 ) ( -64 136 96 ) ( 64 136 96 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.007812 0 0 0
+( 64 136 0 ) ( -64 136 0 ) ( -64 8 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.007812 0 0 0
+( -136 128 8 ) ( -8 128 8 ) ( -136 128 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 2
+{
+( 24 128 8 ) ( -104 128 8 ) ( -104 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 136 64 8 ) ( 136 192 8 ) ( 136 192 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -104 64 8 ) ( 24 64 8 ) ( 24 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -104 64 96 ) ( -104 192 96 ) ( 24 192 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( 24 192 0 ) ( -104 192 0 ) ( -104 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( 128 128 8 ) ( 128 0 8 ) ( 128 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 3
+{
+( -200 208 -24 ) ( -200 80 -24 ) ( -200 80 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 56 136 -24 ) ( -72 136 -24 ) ( -72 136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 136 64 -24 ) ( 136 192 -24 ) ( 136 192 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 56 -24 ) ( 64 56 -24 ) ( 64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 64 104 ) ( -64 192 104 ) ( 64 192 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 192 96 ) ( -64 64 96 ) ( 64 192 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 4
+{
+( 64 64 -8 ) ( -64 64 -8 ) ( -64 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -64 8 ) ( 64 -64 8 ) ( 64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -64 8 ) ( 0 64 8 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 56 8 ) ( -64 56 8 ) ( -64 56 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 64 8 ) ( -72 -64 8 ) ( -72 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 64 0 ) ( 64 64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 5
+{
+( -64 -64 104 ) ( -64 64 104 ) ( 64 64 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 -64 -24 ) ( 64 -64 -24 ) ( 64 -64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 0 -64 -24 ) ( 0 64 -24 ) ( 0 64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 64 56 -24 ) ( -64 56 -24 ) ( -64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -72 64 -24 ) ( -72 -64 -24 ) ( -72 -64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 64 96 ) ( -64 -64 96 ) ( 64 64 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 6
+{
+( 64 64 0 ) ( -64 64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.007812 0 0 0
+( -64 -64 96 ) ( -64 64 96 ) ( 64 64 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.007812 0 0 0
+( -128 -64 8 ) ( 0 -64 8 ) ( 0 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 0 -64 40 ) ( 0 64 40 ) ( 0 64 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -64 64 40 ) ( -64 -64 40 ) ( -64 -64 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 64 -56 8 ) ( -64 -56 8 ) ( 64 -56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 7
+{
+( 8 64 0 ) ( -120 64 0 ) ( -120 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 546 0 -0.058594 0.117188 0 0 0
+( -120 -64 96 ) ( -120 64 96 ) ( 8 64 96 ) map/lab_games/lg_style_01_wall_blue 0 546 0 -0.058594 0.117188 0 0 0
+( -120 -64 8 ) ( 8 -64 8 ) ( 8 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.058594 0.093750 0 0 0
+( 8 -56 40 ) ( 8 72 40 ) ( 8 72 32 ) map/lab_games/lg_style_01_wall_blue 1092 0 0 -0.058594 0.093750 0 0 0
+( 8 64 8 ) ( -120 64 8 ) ( -120 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.058594 0.093750 0 0 0
+( 0 72 -24 ) ( 0 -56 -24 ) ( 0 72 -32 ) map/lab_games/lg_style_01_wall_blue 1092 0 0 -0.058594 0.093750 0 0 0
+}
+// brush 8
+{
+( 56 64 0 ) ( -72 64 0 ) ( -72 -64 0 ) map/lab_games/lg_style_01_wall_blue 955 546 0 -0.058594 0.117188 0 0 0
+( -72 -64 96 ) ( -72 64 96 ) ( 56 64 96 ) map/lab_games/lg_style_01_wall_blue 955 546 0 -0.058594 0.117188 0 0 0
+( -72 -56 8 ) ( 56 -56 8 ) ( 56 -56 0 ) map/lab_games/lg_style_01_wall_blue 955 0 0 -0.058594 0.093750 0 0 0
+( 56 64 8 ) ( -72 64 8 ) ( -72 64 0 ) map/lab_games/lg_style_01_wall_blue 955 0 0 -0.058594 0.093750 0 0 0
+( -72 72 8 ) ( -72 -56 8 ) ( -72 -56 0 ) map/lab_games/lg_style_01_wall_blue 1092 0 0 -0.058594 0.093750 0 0 0
+( -64 -56 40 ) ( -64 72 40 ) ( -64 -56 32 ) map/lab_games/lg_style_01_wall_blue 1092 0 0 -0.058594 0.093750 0 0 0
+}
+// brush 9
+{
+( -64 64 8 ) ( -192 64 8 ) ( -64 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 0 184 0 ) ( -128 184 0 ) ( -128 56 0 ) map/lab_games/lg_style_01_wall_blue 0 512 0 -0.062500 0.125000 0 0 0
+( -128 56 96 ) ( -128 184 96 ) ( 0 184 96 ) map/lab_games/lg_style_01_wall_blue 0 512 0 -0.062500 0.125000 0 0 0
+( 272 56 8 ) ( 400 56 8 ) ( 400 56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 136 56 8 ) ( 136 184 8 ) ( 136 184 0 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( 8 200 40 ) ( 8 72 40 ) ( 8 72 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 10
+{
+( -200 64 136 ) ( -200 56 136 ) ( -200 56 -8 ) map/lab_games/lg_style_01_wall_blue 1092 0 0 -0.058594 0.093750 0 0 0
+( -64 64 136 ) ( -192 64 136 ) ( -192 64 -8 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( -64 -56 168 ) ( -64 -48 168 ) ( -64 -48 24 ) map/lab_games/lg_style_01_wall_blue 1092 0 0 -0.058594 0.093750 0 0 0
+( -136 56 136 ) ( -8 56 136 ) ( -8 56 -8 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( -192 56 96 ) ( -192 64 96 ) ( -64 64 96 ) map/lab_games/lg_style_01_wall_blue 1024 546 0 -0.062500 0.117188 0 0 0
+( -64 64 0 ) ( -192 64 0 ) ( -192 56 0 ) map/lab_games/lg_style_01_wall_blue 1024 546 0 -0.062500 0.117188 0 0 0
+}
+// brush 11
+{
+( -64 192 0 ) ( 64 192 0 ) ( -64 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 192 -8 ) ( -64 192 -8 ) ( -64 64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 56 8 ) ( 64 56 8 ) ( 64 56 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 136 64 8 ) ( 136 192 8 ) ( 136 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 56 136 8 ) ( -72 136 8 ) ( -72 136 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -200 208 8 ) ( -200 80 8 ) ( -200 80 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 12
+{
+( 776 320 -176 ) ( 256 320 -176 ) ( 256 -152 -176 ) map/lab_games/sky/lg_sky_02_dn -520 693 0 -0.507812 0.460938 0 0 0
+( 256 -152 200 ) ( 776 -152 200 ) ( 776 -152 184 ) map/lab_games/sky/lg_sky_02_dn -520 0 0 -0.507812 0.007812 0 0 0
+( 776 -152 200 ) ( 776 320 200 ) ( 776 320 184 ) map/lab_games/sky/lg_sky_02_dn 693 0 0 -0.460938 0.007812 0 0 0
+( 776 320 200 ) ( 256 320 200 ) ( 256 320 184 ) map/lab_games/sky/lg_sky_02_dn -520 0 0 -0.507812 0.007812 0 0 0
+( 256 320 200 ) ( 256 -152 200 ) ( 256 -152 184 ) map/lab_games/sky/lg_sky_02_dn 693 0 0 -0.460938 0.007812 0 0 0
+( 256 320 -168 ) ( 776 320 -168 ) ( 256 -152 -168 ) map/lab_games/sky/lg_sky_02_dn -520 693 0 -0.507812 0.460938 0 0 0
+}
+// brush 13
+{
+( 256 -152 200 ) ( 256 320 200 ) ( 776 320 200 ) map/lab_games/sky/lg_sky_02_up -520 693 0 -0.507812 0.460938 0 0 0
+( 256 -152 200 ) ( 776 -152 200 ) ( 776 -152 184 ) map/lab_games/sky/lg_sky_02_up -520 0 0 -0.507812 0.007812 0 0 0
+( 776 -152 200 ) ( 776 320 200 ) ( 776 320 184 ) map/lab_games/sky/lg_sky_02_up 693 0 0 -0.460938 0.007812 0 0 0
+( 776 320 200 ) ( 256 320 200 ) ( 256 320 184 ) map/lab_games/sky/lg_sky_02_up -520 0 0 -0.507812 0.007812 0 0 0
+( 256 320 200 ) ( 256 -152 200 ) ( 256 -152 184 ) map/lab_games/sky/lg_sky_02_up 693 0 0 -0.460938 0.007812 0 0 0
+( 256 320 192 ) ( 256 -152 192 ) ( 776 320 192 ) map/lab_games/sky/lg_sky_02_up -520 693 0 -0.507812 0.460938 0 0 0
+}
+// brush 14
+{
+( 776 320 -176 ) ( 256 320 -176 ) ( 256 -152 -176 ) map/lab_games/sky/lg_sky_02_bk -520 0 0 -0.507812 0.007812 0 0 0
+( 256 -152 200 ) ( 256 320 200 ) ( 776 320 200 ) map/lab_games/sky/lg_sky_02_bk -520 0 0 -0.507812 0.007812 0 0 0
+( 256 -152 200 ) ( 776 -152 200 ) ( 776 -152 184 ) map/lab_games/sky/lg_sky_02_bk -520 544 0 -0.507812 0.367188 0 0 0
+( 776 -152 200 ) ( 776 320 200 ) ( 776 320 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 256 320 200 ) ( 256 -152 200 ) ( 256 -152 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 776 -144 200 ) ( 256 -144 200 ) ( 776 -144 184 ) map/lab_games/sky/lg_sky_02_bk -520 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 15
+{
+( 776 320 -176 ) ( 256 320 -176 ) ( 256 -152 -176 ) map/lab_games/sky/lg_sky_02_rt 1018 693 0 -0.007812 0.460938 0 0 0
+( 256 -152 200 ) ( 256 320 200 ) ( 776 320 200 ) map/lab_games/sky/lg_sky_02_rt 1018 693 0 -0.007812 0.460938 0 0 0
+( 256 -152 200 ) ( 776 -152 200 ) ( 776 -152 184 ) map/lab_games/sky/lg_sky_02_rt 1018 544 0 -0.007812 0.367188 0 0 0
+( 776 -152 200 ) ( 776 320 200 ) ( 776 320 184 ) map/lab_games/sky/lg_sky_02_rt 693 544 0 -0.460938 0.367188 0 0 0
+( 776 320 200 ) ( 256 320 200 ) ( 256 320 184 ) map/lab_games/sky/lg_sky_02_rt 1018 544 0 -0.007812 0.367188 0 0 0
+( 768 320 200 ) ( 768 -152 200 ) ( 768 320 184 ) map/lab_games/sky/lg_sky_02_rt 693 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 16
+{
+( 776 320 -176 ) ( 256 320 -176 ) ( 256 -152 -176 ) map/lab_games/sky/lg_sky_02_ft -520 0 0 -0.507812 0.007812 0 0 0
+( 256 -152 200 ) ( 256 320 200 ) ( 776 320 200 ) map/lab_games/sky/lg_sky_02_ft -520 0 0 -0.507812 0.007812 0 0 0
+( 776 -152 200 ) ( 776 320 200 ) ( 776 320 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 776 320 200 ) ( 256 320 200 ) ( 256 320 184 ) map/lab_games/sky/lg_sky_02_ft -520 544 0 -0.507812 0.367188 0 0 0
+( 256 320 200 ) ( 256 -152 200 ) ( 256 -152 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 256 312 200 ) ( 776 312 200 ) ( 256 312 184 ) map/lab_games/sky/lg_sky_02_ft -520 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 17
+{
+( 776 320 -176 ) ( 256 320 -176 ) ( 256 -152 -176 ) map/lab_games/sky/lg_sky_02_lf 1018 693 0 -0.007812 0.460938 0 0 0
+( 256 -152 200 ) ( 256 320 200 ) ( 776 320 200 ) map/lab_games/sky/lg_sky_02_lf 1018 693 0 -0.007812 0.460938 0 0 0
+( 256 -152 200 ) ( 776 -152 200 ) ( 776 -152 184 ) map/lab_games/sky/lg_sky_02_lf 1018 544 0 -0.007812 0.367188 0 0 0
+( 776 320 200 ) ( 256 320 200 ) ( 256 320 184 ) map/lab_games/sky/lg_sky_02_lf 1018 544 0 -0.007812 0.367188 0 0 0
+( 256 320 200 ) ( 256 -152 200 ) ( 256 -152 184 ) map/lab_games/sky/lg_sky_02_lf 693 544 0 -0.460938 0.367188 0 0 0
+( 264 -152 200 ) ( 264 320 200 ) ( 264 -152 184 ) map/lab_games/sky/lg_sky_02_lf 693 544 0 -0.460938 0.367188 0 0 0
+}
+}
+// entity 1
+{
+"spawn_orientation_segment" "20"
+"angle" "90"
+"origin" "-32 -32 24"
+"classname" "info_player_start"
+}
+// entity 2
+{
+"classname" "light"
+"origin" "-32 96 80"
+"light" "500"
+}
+// entity 3
+{
+"classname" "placeholder_1"
+"origin" "-176 96 16"
+}
+// entity 4
+{
+"classname" "placeholder_2"
+"origin" "112 96 16"
+}
+// entity 5
+{
+"classname" "_skybox"
+"origin" "488 80 -128"
+}
+// entity 6
+{
+"model" "models/stadium.md3"
+"origin" "488 80 -82"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 7
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "512 40 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 8
+{
+"classname" "misc_model"
+"origin" "560 64 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 9
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "520 72 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 10
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "464 88 -152"
+"classname" "misc_model"
+"angle" "159"
+}
diff --git a/assets/maps/em_tmaze_a2.map b/assets/maps/em_tmaze_a2.map
new file mode 100644
index 00000000..d1da46a0
--- /dev/null
+++ b/assets/maps/em_tmaze_a2.map
@@ -0,0 +1,256 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( -64 192 96 ) ( -64 64 96 ) ( 64 192 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 64 104 ) ( -64 192 104 ) ( 64 192 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 56 -24 ) ( 64 56 -24 ) ( 64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 200 64 -24 ) ( 200 192 -24 ) ( 200 192 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 56 136 -24 ) ( -72 136 -24 ) ( -72 136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -264 216 -24 ) ( -264 88 -24 ) ( -264 88 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 1
+{
+( -64 64 0 ) ( 64 64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 64 8 ) ( -72 -64 8 ) ( -72 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 56 8 ) ( -64 56 8 ) ( -64 56 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -64 8 ) ( 0 64 8 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 -136 8 ) ( 56 -136 8 ) ( 56 -136 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 64 -8 ) ( -64 64 -8 ) ( -64 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 2
+{
+( -64 64 96 ) ( -64 -64 96 ) ( 64 64 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -72 64 -24 ) ( -72 -64 -24 ) ( -72 -64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 64 56 -24 ) ( -64 56 -24 ) ( -64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 0 -64 -24 ) ( 0 64 -24 ) ( 0 64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 -136 -24 ) ( 64 -136 -24 ) ( 64 -136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 -64 104 ) ( -64 64 104 ) ( 64 64 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 3
+{
+( -264 216 8 ) ( -264 88 8 ) ( -264 88 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 120 136 8 ) ( -8 136 8 ) ( -8 136 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 200 64 8 ) ( 200 192 8 ) ( 200 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 56 8 ) ( 128 56 8 ) ( 128 56 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 128 192 -8 ) ( 0 192 -8 ) ( 0 64 -8 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 0 192 0 ) ( 128 192 0 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+}
+// brush 4
+{
+( -64 -136 40 ) ( -64 -8 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( -72 80 40 ) ( -72 -48 40 ) ( -72 -48 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( 56 64 8 ) ( -72 64 8 ) ( -72 64 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( -73 -136 8 ) ( 55 -136 8 ) ( 55 -136 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( -73 -64 96 ) ( -73 64 96 ) ( 55 64 96 ) map/lab_games/lg_style_01_wall_blue 2048 341 0 -0.062500 0.187500 0 0 0
+( 55 64 0 ) ( -73 64 0 ) ( -73 -64 0 ) map/lab_games/lg_style_01_wall_blue 2048 341 0 -0.062500 0.187500 0 0 0
+}
+// brush 5
+{
+( -135 128 8 ) ( -7 128 8 ) ( -135 128 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.093750 0 0 0
+( 64 136 0 ) ( -64 136 0 ) ( -64 8 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.007812 0 0 0
+( -64 8 96 ) ( -64 136 96 ) ( 64 136 96 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.007812 0 0 0
+( 192 8 8 ) ( 192 136 8 ) ( 192 136 0 ) map/lab_games/lg_style_01_wall_blue 7167 0 0 -0.001116 0.093750 0 0 0
+( 105 136 8 ) ( -23 136 8 ) ( -23 136 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.093750 0 0 0
+( -256 152 8 ) ( -256 24 8 ) ( -256 24 0 ) map/lab_games/lg_style_01_wall_blue 7167 0 0 -0.001116 0.093750 0 0 0
+}
+// brush 6
+{
+( 0 0 -24 ) ( 0 -128 -24 ) ( 0 0 -32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( 8 64 8 ) ( -120 64 8 ) ( -120 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.059896 0.093750 0 0 0
+( 8 -48 40 ) ( 8 80 40 ) ( 8 80 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( -121 -136 8 ) ( 7 -136 8 ) ( 7 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.059896 0.093750 0 0 0
+( -121 -64 96 ) ( -121 64 96 ) ( 7 64 96 ) map/lab_games/lg_style_01_wall_blue 0 341 0 -0.059896 0.187500 0 0 0
+( 7 64 0 ) ( -121 64 0 ) ( -121 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 341 0 -0.059896 0.187500 0 0 0
+}
+// brush 7
+{
+( 65 -128 8 ) ( -63 -128 8 ) ( 65 -128 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.093750 0 0 0
+( -64 -8 40 ) ( -64 -136 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 0 -136 40 ) ( 0 -8 40 ) ( 0 -8 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -128 -136 8 ) ( 0 -136 8 ) ( 0 -136 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.093750 0 0 0
+( -64 -136 96 ) ( -64 -8 96 ) ( 64 -8 96 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.007812 0 0 0
+( 64 -8 0 ) ( -64 -8 0 ) ( -64 -136 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.007812 0 0 0
+}
+// brush 8
+{
+( -256 16 -24 ) ( -256 144 -24 ) ( -256 16 -32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -8 192 0 ) ( -136 192 0 ) ( -136 64 0 ) map/lab_games/lg_style_01_wall_blue 682 0 0 -0.008789 0.062500 0 0 0
+( -136 64 96 ) ( -136 192 96 ) ( -8 192 96 ) map/lab_games/lg_style_01_wall_blue 682 0 0 -0.008789 0.062500 0 0 0
+( -135 64 8 ) ( -7 64 8 ) ( -7 64 0 ) map/lab_games/lg_style_01_wall_blue 682 0 0 -0.008789 0.093750 0 0 0
+( -7 128 8 ) ( -135 128 8 ) ( -135 128 0 ) map/lab_games/lg_style_01_wall_blue 682 0 0 -0.008789 0.093750 0 0 0
+( -264 208 8 ) ( -264 80 8 ) ( -264 80 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 9
+{
+( -64 64 0 ) ( -192 64 0 ) ( -192 56 0 ) map/lab_games/lg_style_01_wall_blue 2088 327 0 -0.065104 0.195312 0 0 0
+( -192 56 96 ) ( -192 64 96 ) ( -64 64 96 ) map/lab_games/lg_style_01_wall_blue 2088 327 0 -0.065104 0.195312 0 0 0
+( -136 56 136 ) ( -8 56 136 ) ( -8 56 -8 ) map/lab_games/lg_style_01_wall_blue 2088 0 0 -0.065104 0.093750 0 0 0
+( -72 -56 168 ) ( -72 -48 168 ) ( -72 -48 24 ) map/lab_games/lg_style_01_wall_blue 983 0 0 -0.065104 0.093750 0 0 0
+( -127 64 136 ) ( -255 64 136 ) ( -255 64 -8 ) map/lab_games/lg_style_01_wall_blue 2088 0 0 -0.065104 0.093750 0 0 0
+( -264 56 136 ) ( -264 48 136 ) ( -264 48 -8 ) map/lab_games/lg_style_01_wall_blue 983 0 0 -0.065104 0.093750 0 0 0
+}
+// brush 10
+{
+( 192 128 8 ) ( 192 0 8 ) ( 192 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 88 192 0 ) ( -40 192 0 ) ( -40 64 0 ) map/lab_games/lg_style_01_wall_blue 585 0 0 -0.006836 0.062500 0 0 0
+( -40 64 96 ) ( -40 192 96 ) ( 88 192 96 ) map/lab_games/lg_style_01_wall_blue 585 0 0 -0.006836 0.062500 0 0 0
+( -39 64 8 ) ( 89 64 8 ) ( 89 64 0 ) map/lab_games/lg_style_01_wall_blue 585 0 0 -0.006836 0.093750 0 0 0
+( 200 64 8 ) ( 200 192 8 ) ( 200 192 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 89 128 8 ) ( -39 128 8 ) ( -39 128 0 ) map/lab_games/lg_style_01_wall_blue 585 0 0 -0.006836 0.093750 0 0 0
+}
+// brush 11
+{
+( -7 64 8 ) ( -135 64 8 ) ( -7 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -8 184 0 ) ( -136 184 0 ) ( -136 56 0 ) map/lab_games/lg_style_01_wall_blue 0 356 0 -0.062500 0.179688 0 0 0
+( -136 56 96 ) ( -136 184 96 ) ( -8 184 96 ) map/lab_games/lg_style_01_wall_blue 0 356 0 -0.062500 0.179688 0 0 0
+( 256 56 8 ) ( 384 56 8 ) ( 384 56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 200 56 8 ) ( 200 184 8 ) ( 200 184 0 ) map/lab_games/lg_style_01_wall_blue 1068 0 0 -0.059896 0.093750 0 0 0
+( 0 200 40 ) ( 0 72 40 ) ( 0 72 32 ) map/lab_games/lg_style_01_wall_blue 1068 0 0 -0.059896 0.093750 0 0 0
+}
+// brush 12
+{
+( 944 304 -176 ) ( 424 304 -176 ) ( 424 -168 -176 ) map/lab_games/sky/lg_sky_02_dn -189 658 0 -0.507812 0.460938 0 0 0
+( 424 -168 200 ) ( 944 -168 200 ) ( 944 -168 184 ) map/lab_games/sky/lg_sky_02_dn -189 0 0 -0.507812 0.007812 0 0 0
+( 944 -168 200 ) ( 944 304 200 ) ( 944 304 184 ) map/lab_games/sky/lg_sky_02_dn 658 0 0 -0.460938 0.007812 0 0 0
+( 944 304 200 ) ( 424 304 200 ) ( 424 304 184 ) map/lab_games/sky/lg_sky_02_dn -189 0 0 -0.507812 0.007812 0 0 0
+( 424 304 200 ) ( 424 -168 200 ) ( 424 -168 184 ) map/lab_games/sky/lg_sky_02_dn 658 0 0 -0.460938 0.007812 0 0 0
+( 424 304 -168 ) ( 944 304 -168 ) ( 424 -168 -168 ) map/lab_games/sky/lg_sky_02_dn -189 658 0 -0.507812 0.460938 0 0 0
+}
+// brush 13
+{
+( 424 -168 200 ) ( 424 304 200 ) ( 944 304 200 ) map/lab_games/sky/lg_sky_02_up -189 658 0 -0.507812 0.460938 0 0 0
+( 424 -168 200 ) ( 944 -168 200 ) ( 944 -168 184 ) map/lab_games/sky/lg_sky_02_up -189 0 0 -0.507812 0.007812 0 0 0
+( 944 -168 200 ) ( 944 304 200 ) ( 944 304 184 ) map/lab_games/sky/lg_sky_02_up 658 0 0 -0.460938 0.007812 0 0 0
+( 944 304 200 ) ( 424 304 200 ) ( 424 304 184 ) map/lab_games/sky/lg_sky_02_up -189 0 0 -0.507812 0.007812 0 0 0
+( 424 304 200 ) ( 424 -168 200 ) ( 424 -168 184 ) map/lab_games/sky/lg_sky_02_up 658 0 0 -0.460938 0.007812 0 0 0
+( 424 304 192 ) ( 424 -168 192 ) ( 944 304 192 ) map/lab_games/sky/lg_sky_02_up -189 658 0 -0.507812 0.460938 0 0 0
+}
+// brush 14
+{
+( 944 304 -176 ) ( 424 304 -176 ) ( 424 -168 -176 ) map/lab_games/sky/lg_sky_02_bk -189 0 0 -0.507812 0.007812 0 0 0
+( 424 -168 200 ) ( 424 304 200 ) ( 944 304 200 ) map/lab_games/sky/lg_sky_02_bk -189 0 0 -0.507812 0.007812 0 0 0
+( 424 -168 200 ) ( 944 -168 200 ) ( 944 -168 184 ) map/lab_games/sky/lg_sky_02_bk -189 544 0 -0.507812 0.367188 0 0 0
+( 944 -168 200 ) ( 944 304 200 ) ( 944 304 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 424 304 200 ) ( 424 -168 200 ) ( 424 -168 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 944 -160 200 ) ( 424 -160 200 ) ( 944 -160 184 ) map/lab_games/sky/lg_sky_02_bk -189 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 15
+{
+( 944 304 -176 ) ( 424 304 -176 ) ( 424 -168 -176 ) map/lab_games/sky/lg_sky_02_rt -3 658 0 -0.007812 0.460938 0 0 0
+( 424 -168 200 ) ( 424 304 200 ) ( 944 304 200 ) map/lab_games/sky/lg_sky_02_rt -3 658 0 -0.007812 0.460938 0 0 0
+( 424 -168 200 ) ( 944 -168 200 ) ( 944 -168 184 ) map/lab_games/sky/lg_sky_02_rt -3 544 0 -0.007812 0.367188 0 0 0
+( 944 -168 200 ) ( 944 304 200 ) ( 944 304 184 ) map/lab_games/sky/lg_sky_02_rt 658 544 0 -0.460938 0.367188 0 0 0
+( 944 304 200 ) ( 424 304 200 ) ( 424 304 184 ) map/lab_games/sky/lg_sky_02_rt -3 544 0 -0.007812 0.367188 0 0 0
+( 936 304 200 ) ( 936 -168 200 ) ( 936 304 184 ) map/lab_games/sky/lg_sky_02_rt 658 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 16
+{
+( 944 304 -176 ) ( 424 304 -176 ) ( 424 -168 -176 ) map/lab_games/sky/lg_sky_02_ft -189 0 0 -0.507812 0.007812 0 0 0
+( 424 -168 200 ) ( 424 304 200 ) ( 944 304 200 ) map/lab_games/sky/lg_sky_02_ft -189 0 0 -0.507812 0.007812 0 0 0
+( 944 -168 200 ) ( 944 304 200 ) ( 944 304 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 944 304 200 ) ( 424 304 200 ) ( 424 304 184 ) map/lab_games/sky/lg_sky_02_ft -189 544 0 -0.507812 0.367188 0 0 0
+( 424 304 200 ) ( 424 -168 200 ) ( 424 -168 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 424 296 200 ) ( 944 296 200 ) ( 424 296 184 ) map/lab_games/sky/lg_sky_02_ft -189 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 17
+{
+( 944 304 -176 ) ( 424 304 -176 ) ( 424 -168 -176 ) map/lab_games/sky/lg_sky_02_lf -3 658 0 -0.007812 0.460938 0 0 0
+( 424 -168 200 ) ( 424 304 200 ) ( 944 304 200 ) map/lab_games/sky/lg_sky_02_lf -3 658 0 -0.007812 0.460938 0 0 0
+( 424 -168 200 ) ( 944 -168 200 ) ( 944 -168 184 ) map/lab_games/sky/lg_sky_02_lf -3 544 0 -0.007812 0.367188 0 0 0
+( 944 304 200 ) ( 424 304 200 ) ( 424 304 184 ) map/lab_games/sky/lg_sky_02_lf -3 544 0 -0.007812 0.367188 0 0 0
+( 424 304 200 ) ( 424 -168 200 ) ( 424 -168 184 ) map/lab_games/sky/lg_sky_02_lf 658 544 0 -0.460938 0.367188 0 0 0
+( 432 -168 200 ) ( 432 304 200 ) ( 432 -168 184 ) map/lab_games/sky/lg_sky_02_lf 658 544 0 -0.460938 0.367188 0 0 0
+}
+}
+// entity 1
+{
+"classname" "info_player_start"
+"origin" "-32 -96 24"
+"angle" "90"
+"spawn_orientation_segment" "20"
+}
+// entity 2
+{
+"classname" "light"
+"origin" "-32 96 80"
+"light" "500"
+}
+// entity 3
+{
+"light" "500"
+"origin" "-208 96 80"
+"classname" "light"
+}
+// entity 4
+{
+"light" "500"
+"origin" "144 96 80"
+"classname" "light"
+}
+// entity 5
+{
+"light" "500"
+"origin" "-32 -32 80"
+"classname" "light"
+}
+// entity 6
+{
+"classname" "placeholder_1"
+"origin" "-232 96 16"
+}
+// entity 7
+{
+"classname" "placeholder_2"
+"origin" "168 96 16"
+}
+// entity 8
+{
+"classname" "_skybox"
+"origin" "656 64 -128"
+}
+// entity 9
+{
+"model" "models/stadium.md3"
+"origin" "656 64 -82"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 10
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "680 24 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 11
+{
+"classname" "misc_model"
+"origin" "728 48 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 12
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "688 56 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 13
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "632 72 -152"
+"classname" "misc_model"
+"angle" "159"
+}
diff --git a/assets/maps/em_tmaze_a3.map b/assets/maps/em_tmaze_a3.map
new file mode 100644
index 00000000..8d21f4e9
--- /dev/null
+++ b/assets/maps/em_tmaze_a3.map
@@ -0,0 +1,256 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( -328 216 -24 ) ( -328 88 -24 ) ( -328 88 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 56 136 -24 ) ( -72 136 -24 ) ( -72 136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 264 64 -24 ) ( 264 192 -24 ) ( 264 192 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 56 -24 ) ( 64 56 -24 ) ( 64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 64 104 ) ( -64 192 104 ) ( 64 192 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 192 96 ) ( -64 64 96 ) ( 64 192 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 1
+{
+( 64 64 -8 ) ( -64 64 -8 ) ( -64 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 -136 8 ) ( 56 -136 8 ) ( 56 -136 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -64 8 ) ( 0 64 8 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 56 8 ) ( -64 56 8 ) ( -64 56 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 64 8 ) ( -72 -64 8 ) ( -72 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 64 0 ) ( 64 64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 2
+{
+( -64 -64 104 ) ( -64 64 104 ) ( 64 64 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 -136 -24 ) ( 64 -136 -24 ) ( 64 -136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 0 -64 -24 ) ( 0 64 -24 ) ( 0 64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( 64 56 -24 ) ( -64 56 -24 ) ( -64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -72 64 -24 ) ( -72 -64 -24 ) ( -72 -64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+( -64 64 96 ) ( -64 -64 96 ) ( 64 64 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 3
+{
+( 0 192 0 ) ( 128 192 0 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 128 192 -8 ) ( 0 192 -8 ) ( 0 64 -8 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 0 56 8 ) ( 128 56 8 ) ( 128 56 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 264 64 8 ) ( 264 192 8 ) ( 264 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 120 136 8 ) ( -8 136 8 ) ( -8 136 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( -328 216 8 ) ( -328 88 8 ) ( -328 88 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 4
+{
+( 55 64 0 ) ( -73 64 0 ) ( -73 -64 0 ) map/lab_games/lg_style_01_wall_blue 2048 341 0 -0.062500 0.187500 0 0 0
+( -73 -64 96 ) ( -73 64 96 ) ( 55 64 96 ) map/lab_games/lg_style_01_wall_blue 2048 341 0 -0.062500 0.187500 0 0 0
+( -73 -136 8 ) ( 55 -136 8 ) ( 55 -136 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( 56 64 8 ) ( -72 64 8 ) ( -72 64 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( -72 80 40 ) ( -72 -48 40 ) ( -72 -48 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( -64 -136 40 ) ( -64 -8 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 5
+{
+( -328 152 8 ) ( -328 24 8 ) ( -328 24 0 ) map/lab_games/lg_style_01_wall_blue 7167 0 0 -0.001116 0.093750 0 0 0
+( 105 136 8 ) ( -23 136 8 ) ( -23 136 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.093750 0 0 0
+( 264 8 8 ) ( 264 136 8 ) ( 264 136 0 ) map/lab_games/lg_style_01_wall_blue 7167 0 0 -0.001116 0.093750 0 0 0
+( -64 8 96 ) ( -64 136 96 ) ( 64 136 96 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.007812 0 0 0
+( 64 136 0 ) ( -64 136 0 ) ( -64 8 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.007812 0 0 0
+( -135 128 8 ) ( -7 128 8 ) ( -135 128 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 6
+{
+( 7 64 0 ) ( -121 64 0 ) ( -121 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 341 0 -0.059896 0.187500 0 0 0
+( -121 -64 96 ) ( -121 64 96 ) ( 7 64 96 ) map/lab_games/lg_style_01_wall_blue 0 341 0 -0.059896 0.187500 0 0 0
+( -121 -136 8 ) ( 7 -136 8 ) ( 7 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.059896 0.093750 0 0 0
+( 8 -48 40 ) ( 8 80 40 ) ( 8 80 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( 8 64 8 ) ( -120 64 8 ) ( -120 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.059896 0.093750 0 0 0
+( 0 0 -24 ) ( 0 -128 -24 ) ( 0 0 -32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 7
+{
+( 64 -8 0 ) ( -64 -8 0 ) ( -64 -136 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.007812 0 0 0
+( -64 -136 96 ) ( -64 -8 96 ) ( 64 -8 96 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.007812 0 0 0
+( -128 -136 8 ) ( 0 -136 8 ) ( 0 -136 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.093750 0 0 0
+( 0 -136 40 ) ( 0 -8 40 ) ( 0 -8 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -64 -8 40 ) ( -64 -136 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 65 -128 8 ) ( -63 -128 8 ) ( 65 -128 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 8
+{
+( -328 208 8 ) ( -328 80 8 ) ( -328 80 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -71 128 8 ) ( -199 128 8 ) ( -199 128 0 ) map/lab_games/lg_style_01_wall_blue 568 0 0 -0.008789 0.093750 0 0 0
+( -199 64 8 ) ( -71 64 8 ) ( -71 64 0 ) map/lab_games/lg_style_01_wall_blue 568 0 0 -0.008789 0.093750 0 0 0
+( -200 64 96 ) ( -200 192 96 ) ( -72 192 96 ) map/lab_games/lg_style_01_wall_blue 568 0 0 -0.008789 0.062500 0 0 0
+( -72 192 0 ) ( -200 192 0 ) ( -200 64 0 ) map/lab_games/lg_style_01_wall_blue 568 0 0 -0.008789 0.062500 0 0 0
+( -320 16 -24 ) ( -320 144 -24 ) ( -320 16 -32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 9
+{
+( -328 56 136 ) ( -328 48 136 ) ( -328 48 -8 ) map/lab_games/lg_style_01_wall_blue 983 0 0 -0.065104 0.093750 0 0 0
+( -135 64 136 ) ( -263 64 136 ) ( -263 64 -8 ) map/lab_games/lg_style_01_wall_blue 2088 0 0 -0.065104 0.093750 0 0 0
+( -72 -56 168 ) ( -72 -48 168 ) ( -72 -48 24 ) map/lab_games/lg_style_01_wall_blue 983 0 0 -0.065104 0.093750 0 0 0
+( -136 56 136 ) ( -8 56 136 ) ( -8 56 -8 ) map/lab_games/lg_style_01_wall_blue 2088 0 0 -0.065104 0.093750 0 0 0
+( -192 56 96 ) ( -192 64 96 ) ( -64 64 96 ) map/lab_games/lg_style_01_wall_blue 2088 327 0 -0.065104 0.195312 0 0 0
+( -64 64 0 ) ( -192 64 0 ) ( -192 56 0 ) map/lab_games/lg_style_01_wall_blue 2088 327 0 -0.065104 0.195312 0 0 0
+}
+// brush 10
+{
+( 153 128 8 ) ( 25 128 8 ) ( 25 128 0 ) map/lab_games/lg_style_01_wall_blue 731 0 0 -0.006836 0.093750 0 0 0
+( 264 64 8 ) ( 264 192 8 ) ( 264 192 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 25 64 8 ) ( 153 64 8 ) ( 153 64 0 ) map/lab_games/lg_style_01_wall_blue 731 0 0 -0.006836 0.093750 0 0 0
+( 24 64 96 ) ( 24 192 96 ) ( 152 192 96 ) map/lab_games/lg_style_01_wall_blue 731 0 0 -0.006836 0.062500 0 0 0
+( 152 192 0 ) ( 24 192 0 ) ( 24 64 0 ) map/lab_games/lg_style_01_wall_blue 731 0 0 -0.006836 0.062500 0 0 0
+( 256 128 8 ) ( 256 0 8 ) ( 256 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 11
+{
+( 0 200 40 ) ( 0 72 40 ) ( 0 72 32 ) map/lab_games/lg_style_01_wall_blue 1068 0 0 -0.059896 0.093750 0 0 0
+( 264 56 8 ) ( 264 184 8 ) ( 264 184 0 ) map/lab_games/lg_style_01_wall_blue 1068 0 0 -0.059896 0.093750 0 0 0
+( 256 56 8 ) ( 384 56 8 ) ( 384 56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -136 56 96 ) ( -136 184 96 ) ( -8 184 96 ) map/lab_games/lg_style_01_wall_blue 0 356 0 -0.062500 0.179688 0 0 0
+( -8 184 0 ) ( -136 184 0 ) ( -136 56 0 ) map/lab_games/lg_style_01_wall_blue 0 356 0 -0.062500 0.179688 0 0 0
+( -7 64 8 ) ( -135 64 8 ) ( -7 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 12
+{
+( 968 264 -176 ) ( 448 264 -176 ) ( 448 -208 -176 ) map/lab_games/sky/lg_sky_02_dn -141 571 0 -0.507812 0.460938 0 0 0
+( 448 -208 200 ) ( 968 -208 200 ) ( 968 -208 184 ) map/lab_games/sky/lg_sky_02_dn -141 0 0 -0.507812 0.007812 0 0 0
+( 968 -208 200 ) ( 968 264 200 ) ( 968 264 184 ) map/lab_games/sky/lg_sky_02_dn 571 0 0 -0.460938 0.007812 0 0 0
+( 968 264 200 ) ( 448 264 200 ) ( 448 264 184 ) map/lab_games/sky/lg_sky_02_dn -141 0 0 -0.507812 0.007812 0 0 0
+( 448 264 200 ) ( 448 -208 200 ) ( 448 -208 184 ) map/lab_games/sky/lg_sky_02_dn 571 0 0 -0.460938 0.007812 0 0 0
+( 448 264 -168 ) ( 968 264 -168 ) ( 448 -208 -168 ) map/lab_games/sky/lg_sky_02_dn -141 571 0 -0.507812 0.460938 0 0 0
+}
+// brush 13
+{
+( 448 -208 200 ) ( 448 264 200 ) ( 968 264 200 ) map/lab_games/sky/lg_sky_02_up -141 571 0 -0.507812 0.460938 0 0 0
+( 448 -208 200 ) ( 968 -208 200 ) ( 968 -208 184 ) map/lab_games/sky/lg_sky_02_up -141 0 0 -0.507812 0.007812 0 0 0
+( 968 -208 200 ) ( 968 264 200 ) ( 968 264 184 ) map/lab_games/sky/lg_sky_02_up 571 0 0 -0.460938 0.007812 0 0 0
+( 968 264 200 ) ( 448 264 200 ) ( 448 264 184 ) map/lab_games/sky/lg_sky_02_up -141 0 0 -0.507812 0.007812 0 0 0
+( 448 264 200 ) ( 448 -208 200 ) ( 448 -208 184 ) map/lab_games/sky/lg_sky_02_up 571 0 0 -0.460938 0.007812 0 0 0
+( 448 264 192 ) ( 448 -208 192 ) ( 968 264 192 ) map/lab_games/sky/lg_sky_02_up -141 571 0 -0.507812 0.460938 0 0 0
+}
+// brush 14
+{
+( 968 264 -176 ) ( 448 264 -176 ) ( 448 -208 -176 ) map/lab_games/sky/lg_sky_02_bk -141 0 0 -0.507812 0.007812 0 0 0
+( 448 -208 200 ) ( 448 264 200 ) ( 968 264 200 ) map/lab_games/sky/lg_sky_02_bk -141 0 0 -0.507812 0.007812 0 0 0
+( 448 -208 200 ) ( 968 -208 200 ) ( 968 -208 184 ) map/lab_games/sky/lg_sky_02_bk -141 544 0 -0.507812 0.367188 0 0 0
+( 968 -208 200 ) ( 968 264 200 ) ( 968 264 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 448 264 200 ) ( 448 -208 200 ) ( 448 -208 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 968 -200 200 ) ( 448 -200 200 ) ( 968 -200 184 ) map/lab_games/sky/lg_sky_02_bk -141 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 15
+{
+( 968 264 -176 ) ( 448 264 -176 ) ( 448 -208 -176 ) map/lab_games/sky/lg_sky_02_rt -3 571 0 -0.007812 0.460938 0 0 0
+( 448 -208 200 ) ( 448 264 200 ) ( 968 264 200 ) map/lab_games/sky/lg_sky_02_rt -3 571 0 -0.007812 0.460938 0 0 0
+( 448 -208 200 ) ( 968 -208 200 ) ( 968 -208 184 ) map/lab_games/sky/lg_sky_02_rt -3 544 0 -0.007812 0.367188 0 0 0
+( 968 -208 200 ) ( 968 264 200 ) ( 968 264 184 ) map/lab_games/sky/lg_sky_02_rt 571 544 0 -0.460938 0.367188 0 0 0
+( 968 264 200 ) ( 448 264 200 ) ( 448 264 184 ) map/lab_games/sky/lg_sky_02_rt -3 544 0 -0.007812 0.367188 0 0 0
+( 960 264 200 ) ( 960 -208 200 ) ( 960 264 184 ) map/lab_games/sky/lg_sky_02_rt 571 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 16
+{
+( 968 264 -176 ) ( 448 264 -176 ) ( 448 -208 -176 ) map/lab_games/sky/lg_sky_02_ft -141 0 0 -0.507812 0.007812 0 0 0
+( 448 -208 200 ) ( 448 264 200 ) ( 968 264 200 ) map/lab_games/sky/lg_sky_02_ft -141 0 0 -0.507812 0.007812 0 0 0
+( 968 -208 200 ) ( 968 264 200 ) ( 968 264 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 968 264 200 ) ( 448 264 200 ) ( 448 264 184 ) map/lab_games/sky/lg_sky_02_ft -141 544 0 -0.507812 0.367188 0 0 0
+( 448 264 200 ) ( 448 -208 200 ) ( 448 -208 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 448 256 200 ) ( 968 256 200 ) ( 448 256 184 ) map/lab_games/sky/lg_sky_02_ft -141 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 17
+{
+( 968 264 -176 ) ( 448 264 -176 ) ( 448 -208 -176 ) map/lab_games/sky/lg_sky_02_lf -3 571 0 -0.007812 0.460938 0 0 0
+( 448 -208 200 ) ( 448 264 200 ) ( 968 264 200 ) map/lab_games/sky/lg_sky_02_lf -3 571 0 -0.007812 0.460938 0 0 0
+( 448 -208 200 ) ( 968 -208 200 ) ( 968 -208 184 ) map/lab_games/sky/lg_sky_02_lf -3 544 0 -0.007812 0.367188 0 0 0
+( 968 264 200 ) ( 448 264 200 ) ( 448 264 184 ) map/lab_games/sky/lg_sky_02_lf -3 544 0 -0.007812 0.367188 0 0 0
+( 448 264 200 ) ( 448 -208 200 ) ( 448 -208 184 ) map/lab_games/sky/lg_sky_02_lf 571 544 0 -0.460938 0.367188 0 0 0
+( 456 -208 200 ) ( 456 264 200 ) ( 456 -208 184 ) map/lab_games/sky/lg_sky_02_lf 571 544 0 -0.460938 0.367188 0 0 0
+}
+}
+// entity 1
+{
+"angle" "90"
+"origin" "-32 -96 24"
+"classname" "info_player_start"
+"spawn_orientation_segment" "20"
+}
+// entity 2
+{
+"light" "500"
+"origin" "-32 96 80"
+"classname" "light"
+}
+// entity 3
+{
+"classname" "light"
+"origin" "-208 96 80"
+"light" "500"
+}
+// entity 4
+{
+"classname" "light"
+"origin" "144 96 80"
+"light" "500"
+}
+// entity 5
+{
+"classname" "light"
+"origin" "-32 -32 80"
+"light" "500"
+}
+// entity 6
+{
+"classname" "placeholder_1"
+"origin" "-296 96 16"
+}
+// entity 7
+{
+"classname" "placeholder_2"
+"origin" "232 96 16"
+}
+// entity 8
+{
+"classname" "_skybox"
+"origin" "680 24 -128"
+}
+// entity 9
+{
+"model" "models/stadium.md3"
+"origin" "680 24 -82"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 10
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "704 -16 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 11
+{
+"classname" "misc_model"
+"origin" "752 8 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 12
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "712 16 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 13
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "656 32 -152"
+"classname" "misc_model"
+"angle" "159"
+}
diff --git a/assets/maps/em_tmaze_a4.map b/assets/maps/em_tmaze_a4.map
new file mode 100644
index 00000000..34e12348
--- /dev/null
+++ b/assets/maps/em_tmaze_a4.map
@@ -0,0 +1,268 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( -392 216 -24 ) ( -392 88 -24 ) ( -392 88 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 56 136 -24 ) ( -72 136 -24 ) ( -72 136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 328 64 -24 ) ( 328 192 -24 ) ( 328 192 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 56 -24 ) ( 64 56 -24 ) ( 64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 64 104 ) ( -64 192 104 ) ( 64 192 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 192 96 ) ( -64 64 96 ) ( 64 192 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+}
+// brush 1
+{
+( 64 64 -8 ) ( -64 64 -8 ) ( -64 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 -136 8 ) ( 56 -136 8 ) ( 56 -136 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -64 8 ) ( 0 64 8 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 56 8 ) ( -64 56 8 ) ( -64 56 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 64 8 ) ( -72 -64 8 ) ( -72 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 64 0 ) ( 64 64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 2
+{
+( -64 -64 104 ) ( -64 64 104 ) ( 64 64 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 -136 -24 ) ( 64 -136 -24 ) ( 64 -136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 0 -64 -24 ) ( 0 64 -24 ) ( 0 64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 64 56 -24 ) ( -64 56 -24 ) ( -64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -72 64 -24 ) ( -72 -64 -24 ) ( -72 -64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 64 96 ) ( -64 -64 96 ) ( 64 64 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+}
+// brush 3
+{
+( 0 192 0 ) ( 128 192 0 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 128 192 -8 ) ( 0 192 -8 ) ( 0 64 -8 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 0 56 8 ) ( 128 56 8 ) ( 128 56 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 328 64 8 ) ( 328 192 8 ) ( 328 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 120 136 8 ) ( -8 136 8 ) ( -8 136 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( -392 216 8 ) ( -392 88 8 ) ( -392 88 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 4
+{
+( 55 64 0 ) ( -73 64 0 ) ( -73 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 327 0 -0.003906 0.195312 0 0 0
+( -73 -64 96 ) ( -73 64 96 ) ( 55 64 96 ) map/lab_games/lg_style_01_wall_blue 0 327 0 -0.003906 0.195312 0 0 0
+( -73 -136 8 ) ( 55 -136 8 ) ( 55 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.003906 0.093750 0 0 0
+( 56 64 8 ) ( -72 64 8 ) ( -72 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.003906 0.093750 0 0 0
+( -72 80 40 ) ( -72 -48 40 ) ( -72 -48 32 ) map/lab_games/lg_style_01_wall_blue 655 0 0 -0.097656 0.093750 0 0 0
+( -64 -136 40 ) ( -64 -8 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 655 0 0 -0.097656 0.093750 0 0 0
+}
+// brush 5
+{
+( -392 152 8 ) ( -392 24 8 ) ( -392 24 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.001302 0.093750 0 0 0
+( 105 136 8 ) ( -23 136 8 ) ( -23 136 0 ) map/lab_games/lg_style_01_wall_blue 2798 0 0 -0.117188 0.093750 0 0 0
+( 328 8 8 ) ( 328 136 8 ) ( 328 136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.001302 0.093750 0 0 0
+( -64 8 96 ) ( -64 136 96 ) ( 64 136 96 ) map/lab_games/lg_style_01_wall_blue 2798 0 0 -0.117188 0.007812 0 0 0
+( 64 136 0 ) ( -64 136 0 ) ( -64 8 0 ) map/lab_games/lg_style_01_wall_blue 2798 0 0 -0.117188 0.007812 0 0 0
+( -199 128 8 ) ( -71 128 8 ) ( -199 128 0 ) map/lab_games/lg_style_01_wall_blue 2798 0 0 -0.117188 0.093750 0 0 0
+}
+// brush 6
+{
+( 7 64 0 ) ( -121 64 0 ) ( -121 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 327 0 -0.003906 0.195312 0 0 0
+( -121 -64 96 ) ( -121 64 96 ) ( 7 64 96 ) map/lab_games/lg_style_01_wall_blue 0 327 0 -0.003906 0.195312 0 0 0
+( -121 -136 8 ) ( 7 -136 8 ) ( 7 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.003906 0.093750 0 0 0
+( 8 -48 40 ) ( 8 80 40 ) ( 8 80 32 ) map/lab_games/lg_style_01_wall_blue 655 0 0 -0.097656 0.093750 0 0 0
+( 8 64 8 ) ( -120 64 8 ) ( -120 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.003906 0.093750 0 0 0
+( 0 0 -24 ) ( 0 -128 -24 ) ( 0 0 -32 ) map/lab_games/lg_style_01_wall_blue 655 0 0 -0.097656 0.093750 0 0 0
+}
+// brush 7
+{
+( 64 -8 0 ) ( -64 -8 0 ) ( -64 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.007812 0 0 0
+( -64 -136 96 ) ( -64 -8 96 ) ( 64 -8 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.007812 0 0 0
+( -128 -136 8 ) ( 0 -136 8 ) ( 0 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 0 -136 40 ) ( 0 -8 40 ) ( 0 -8 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -64 -8 40 ) ( -64 -136 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 65 -128 8 ) ( -63 -128 8 ) ( 65 -128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 8
+{
+( -392 208 8 ) ( -392 80 8 ) ( -392 80 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -135 128 8 ) ( -263 128 8 ) ( -263 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -263 64 8 ) ( -135 64 8 ) ( -135 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -264 64 96 ) ( -264 192 96 ) ( -136 192 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( -136 192 0 ) ( -264 192 0 ) ( -264 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( -384 16 -24 ) ( -384 144 -24 ) ( -384 16 -32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 9
+{
+( -392 56 136 ) ( -392 48 136 ) ( -392 48 -8 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.002604 0.093750 0 0 0
+( -199 64 136 ) ( -327 64 136 ) ( -327 64 -8 ) map/lab_games/lg_style_01_wall_blue 2380 0 0 -0.104167 0.093750 0 0 0
+( -72 -56 168 ) ( -72 -48 168 ) ( -72 -48 24 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.002604 0.093750 0 0 0
+( -136 56 136 ) ( -8 56 136 ) ( -8 56 -8 ) map/lab_games/lg_style_01_wall_blue 2380 0 0 -0.104167 0.093750 0 0 0
+( -192 56 96 ) ( -192 64 96 ) ( -64 64 96 ) map/lab_games/lg_style_01_wall_blue 2380 0 0 -0.104167 0.007812 0 0 0
+( -64 64 0 ) ( -192 64 0 ) ( -192 56 0 ) map/lab_games/lg_style_01_wall_blue 2380 0 0 -0.104167 0.007812 0 0 0
+}
+// brush 10
+{
+( 217 128 8 ) ( 89 128 8 ) ( 89 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 328 64 8 ) ( 328 192 8 ) ( 328 192 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 89 64 8 ) ( 217 64 8 ) ( 217 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 88 64 96 ) ( 88 192 96 ) ( 216 192 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( 216 192 0 ) ( 88 192 0 ) ( 88 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.062500 0 0 0
+( 320 128 8 ) ( 320 0 8 ) ( 320 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 11
+{
+( 0 200 40 ) ( 0 72 40 ) ( 0 72 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.002604 0.093750 0 0 0
+( 328 56 8 ) ( 328 184 8 ) ( 328 184 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.002604 0.093750 0 0 0
+( 256 56 8 ) ( 384 56 8 ) ( 384 56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.106771 0.093750 0 0 0
+( -136 56 96 ) ( -136 184 96 ) ( -8 184 96 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.106771 0.007812 0 0 0
+( -8 184 0 ) ( -136 184 0 ) ( -136 56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.106771 0.007812 0 0 0
+( 57 64 8 ) ( -71 64 8 ) ( 57 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.106771 0.093750 0 0 0
+}
+// brush 12
+{
+( 1048 240 -176 ) ( 528 240 -176 ) ( 528 -232 -176 ) map/lab_games/sky/lg_sky_02_dn 15 519 0 -0.507812 0.460938 0 0 0
+( 528 -232 200 ) ( 1048 -232 200 ) ( 1048 -232 184 ) map/lab_games/sky/lg_sky_02_dn 15 0 0 -0.507812 0.007812 0 0 0
+( 1048 -232 200 ) ( 1048 240 200 ) ( 1048 240 184 ) map/lab_games/sky/lg_sky_02_dn 519 0 0 -0.460938 0.007812 0 0 0
+( 1048 240 200 ) ( 528 240 200 ) ( 528 240 184 ) map/lab_games/sky/lg_sky_02_dn 15 0 0 -0.507812 0.007812 0 0 0
+( 528 240 200 ) ( 528 -232 200 ) ( 528 -232 184 ) map/lab_games/sky/lg_sky_02_dn 519 0 0 -0.460938 0.007812 0 0 0
+( 528 240 -168 ) ( 1048 240 -168 ) ( 528 -232 -168 ) map/lab_games/sky/lg_sky_02_dn 15 519 0 -0.507812 0.460938 0 0 0
+}
+// brush 13
+{
+( 528 -232 200 ) ( 528 240 200 ) ( 1048 240 200 ) map/lab_games/sky/lg_sky_02_up 15 519 0 -0.507812 0.460938 0 0 0
+( 528 -232 200 ) ( 1048 -232 200 ) ( 1048 -232 184 ) map/lab_games/sky/lg_sky_02_up 15 0 0 -0.507812 0.007812 0 0 0
+( 1048 -232 200 ) ( 1048 240 200 ) ( 1048 240 184 ) map/lab_games/sky/lg_sky_02_up 519 0 0 -0.460938 0.007812 0 0 0
+( 1048 240 200 ) ( 528 240 200 ) ( 528 240 184 ) map/lab_games/sky/lg_sky_02_up 15 0 0 -0.507812 0.007812 0 0 0
+( 528 240 200 ) ( 528 -232 200 ) ( 528 -232 184 ) map/lab_games/sky/lg_sky_02_up 519 0 0 -0.460938 0.007812 0 0 0
+( 528 240 192 ) ( 528 -232 192 ) ( 1048 240 192 ) map/lab_games/sky/lg_sky_02_up 15 519 0 -0.507812 0.460938 0 0 0
+}
+// brush 14
+{
+( 1048 240 -176 ) ( 528 240 -176 ) ( 528 -232 -176 ) map/lab_games/sky/lg_sky_02_bk 15 -1023 0 -0.507812 0.007812 0 0 0
+( 528 -232 200 ) ( 528 240 200 ) ( 1048 240 200 ) map/lab_games/sky/lg_sky_02_bk 15 -1023 0 -0.507812 0.007812 0 0 0
+( 528 -232 200 ) ( 1048 -232 200 ) ( 1048 -232 184 ) map/lab_games/sky/lg_sky_02_bk 15 544 0 -0.507812 0.367188 0 0 0
+( 1048 -232 200 ) ( 1048 240 200 ) ( 1048 240 184 ) map/lab_games/sky/lg_sky_02_bk -1023 544 0 -0.007812 0.367188 0 0 0
+( 528 240 200 ) ( 528 -232 200 ) ( 528 -232 184 ) map/lab_games/sky/lg_sky_02_bk -1023 544 0 -0.007812 0.367188 0 0 0
+( 1048 -224 200 ) ( 528 -224 200 ) ( 1048 -224 184 ) map/lab_games/sky/lg_sky_02_bk 15 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 15
+{
+( 1048 240 -176 ) ( 528 240 -176 ) ( 528 -232 -176 ) map/lab_games/sky/lg_sky_02_rt -3 519 0 -0.007812 0.460938 0 0 0
+( 528 -232 200 ) ( 528 240 200 ) ( 1048 240 200 ) map/lab_games/sky/lg_sky_02_rt -3 519 0 -0.007812 0.460938 0 0 0
+( 528 -232 200 ) ( 1048 -232 200 ) ( 1048 -232 184 ) map/lab_games/sky/lg_sky_02_rt -3 544 0 -0.007812 0.367188 0 0 0
+( 1048 -232 200 ) ( 1048 240 200 ) ( 1048 240 184 ) map/lab_games/sky/lg_sky_02_rt 519 544 0 -0.460938 0.367188 0 0 0
+( 1048 240 200 ) ( 528 240 200 ) ( 528 240 184 ) map/lab_games/sky/lg_sky_02_rt -3 544 0 -0.007812 0.367188 0 0 0
+( 1040 240 200 ) ( 1040 -232 200 ) ( 1040 240 184 ) map/lab_games/sky/lg_sky_02_rt 519 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 16
+{
+( 1048 240 -176 ) ( 528 240 -176 ) ( 528 -232 -176 ) map/lab_games/sky/lg_sky_02_ft 15 -1023 0 -0.507812 0.007812 0 0 0
+( 528 -232 200 ) ( 528 240 200 ) ( 1048 240 200 ) map/lab_games/sky/lg_sky_02_ft 15 -1023 0 -0.507812 0.007812 0 0 0
+( 1048 -232 200 ) ( 1048 240 200 ) ( 1048 240 184 ) map/lab_games/sky/lg_sky_02_ft -1023 544 0 -0.007812 0.367188 0 0 0
+( 1048 240 200 ) ( 528 240 200 ) ( 528 240 184 ) map/lab_games/sky/lg_sky_02_ft 15 544 0 -0.507812 0.367188 0 0 0
+( 528 240 200 ) ( 528 -232 200 ) ( 528 -232 184 ) map/lab_games/sky/lg_sky_02_ft -1023 544 0 -0.007812 0.367188 0 0 0
+( 528 232 200 ) ( 1048 232 200 ) ( 528 232 184 ) map/lab_games/sky/lg_sky_02_ft 15 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 17
+{
+( 1048 240 -176 ) ( 528 240 -176 ) ( 528 -232 -176 ) map/lab_games/sky/lg_sky_02_lf -3 519 0 -0.007812 0.460938 0 0 0
+( 528 -232 200 ) ( 528 240 200 ) ( 1048 240 200 ) map/lab_games/sky/lg_sky_02_lf -3 519 0 -0.007812 0.460938 0 0 0
+( 528 -232 200 ) ( 1048 -232 200 ) ( 1048 -232 184 ) map/lab_games/sky/lg_sky_02_lf -3 544 0 -0.007812 0.367188 0 0 0
+( 1048 240 200 ) ( 528 240 200 ) ( 528 240 184 ) map/lab_games/sky/lg_sky_02_lf -3 544 0 -0.007812 0.367188 0 0 0
+( 528 240 200 ) ( 528 -232 200 ) ( 528 -232 184 ) map/lab_games/sky/lg_sky_02_lf 519 544 0 -0.460938 0.367188 0 0 0
+( 536 -232 200 ) ( 536 240 200 ) ( 536 -232 184 ) map/lab_games/sky/lg_sky_02_lf 519 544 0 -0.460938 0.367188 0 0 0
+}
+}
+// entity 1
+{
+"angle" "90"
+"origin" "-32 -96 24"
+"classname" "info_player_start"
+"spawn_orientation_segment" "20"
+}
+// entity 2
+{
+"light" "500"
+"origin" "-32 96 80"
+"classname" "light"
+}
+// entity 3
+{
+"classname" "light"
+"origin" "-192 96 80"
+"light" "500"
+}
+// entity 4
+{
+"classname" "light"
+"origin" "128 96 80"
+"light" "500"
+}
+// entity 5
+{
+"classname" "light"
+"origin" "-32 -32 80"
+"light" "500"
+}
+// entity 6
+{
+"light" "500"
+"origin" "288 96 80"
+"classname" "light"
+}
+// entity 7
+{
+"light" "500"
+"origin" "-352 96 80"
+"classname" "light"
+}
+// entity 8
+{
+"classname" "placeholder_1"
+"origin" "-360 96 16"
+}
+// entity 9
+{
+"classname" "placeholder_2"
+"origin" "296 96 16"
+}
+// entity 10
+{
+"classname" "_skybox"
+"origin" "760 0 -128"
+}
+// entity 11
+{
+"model" "models/stadium.md3"
+"origin" "760 0 -82"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 12
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "784 -40 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 13
+{
+"classname" "misc_model"
+"origin" "832 -16 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 14
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "792 -8 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 15
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "736 8 -152"
+"classname" "misc_model"
+"angle" "159"
+}
diff --git a/assets/maps/em_tmaze_a5.map b/assets/maps/em_tmaze_a5.map
new file mode 100644
index 00000000..a341e7d4
--- /dev/null
+++ b/assets/maps/em_tmaze_a5.map
@@ -0,0 +1,268 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( -456 216 -24 ) ( -456 88 -24 ) ( -456 88 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 56 136 -24 ) ( -72 136 -24 ) ( -72 136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 392 56 -24 ) ( 392 184 -24 ) ( 392 184 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 56 -24 ) ( 64 56 -24 ) ( 64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 64 104 ) ( -64 192 104 ) ( 64 192 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 192 96 ) ( -64 64 96 ) ( 64 192 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+}
+// brush 1
+{
+( 64 64 -8 ) ( -64 64 -8 ) ( -64 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 -136 8 ) ( 56 -136 8 ) ( 56 -136 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -64 8 ) ( 0 64 8 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 56 8 ) ( -64 56 8 ) ( -64 56 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -72 64 8 ) ( -72 -64 8 ) ( -72 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 64 0 ) ( 64 64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 2
+{
+( -64 -64 104 ) ( -64 64 104 ) ( 64 64 104 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 -136 -24 ) ( 64 -136 -24 ) ( 64 -136 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 0 -64 -24 ) ( 0 64 -24 ) ( 0 64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( 64 56 -24 ) ( -64 56 -24 ) ( -64 56 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -72 64 -24 ) ( -72 -64 -24 ) ( -72 -64 -32 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+( -64 64 96 ) ( -64 -64 96 ) ( 64 64 96 ) map/lab_games/sky/lg_sky_01 0 0 0 0.031250 0.031250 0 0 0
+}
+// brush 3
+{
+( 0 192 0 ) ( 128 192 0 ) ( 0 64 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 128 192 -8 ) ( 0 192 -8 ) ( 0 64 -8 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 0 56 8 ) ( 128 56 8 ) ( 128 56 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( 392 56 8 ) ( 392 184 8 ) ( 392 184 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 120 136 8 ) ( -8 136 8 ) ( -8 136 0 ) map/lab_games/lg_style_01_floor_orange -3 0 0 0.190000 0.190000 0 0 0
+( -456 216 8 ) ( -456 88 8 ) ( -456 88 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 4
+{
+( 55 64 0 ) ( -73 64 0 ) ( -73 -64 0 ) map/lab_games/lg_style_01_wall_blue 2048 341 0 -0.062500 0.187500 0 0 0
+( -73 -64 96 ) ( -73 64 96 ) ( 55 64 96 ) map/lab_games/lg_style_01_wall_blue 2048 341 0 -0.062500 0.187500 0 0 0
+( -73 -136 8 ) ( 55 -136 8 ) ( 55 -136 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( 56 64 8 ) ( -72 64 8 ) ( -72 64 0 ) map/lab_games/lg_style_01_wall_blue 2048 0 0 -0.062500 0.093750 0 0 0
+( -72 80 40 ) ( -72 -48 40 ) ( -72 -48 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( -64 -136 40 ) ( -64 -8 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 5
+{
+( -456 152 8 ) ( -456 24 8 ) ( -456 24 0 ) map/lab_games/lg_style_01_wall_blue 7167 0 0 -0.001116 0.093750 0 0 0
+( 105 136 8 ) ( -23 136 8 ) ( -23 136 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.093750 0 0 0
+( 392 8 8 ) ( 392 136 8 ) ( 392 136 0 ) map/lab_games/lg_style_01_wall_blue 7167 0 0 -0.001116 0.093750 0 0 0
+( -64 8 96 ) ( -64 136 96 ) ( 64 136 96 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.007812 0 0 0
+( 64 136 0 ) ( -64 136 0 ) ( -64 8 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.007812 0 0 0
+( -199 128 8 ) ( -71 128 8 ) ( -199 128 0 ) map/lab_games/lg_style_01_wall_blue 3072 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 6
+{
+( 7 64 0 ) ( -121 64 0 ) ( -121 -64 0 ) map/lab_games/lg_style_01_wall_blue 0 341 0 -0.059896 0.187500 0 0 0
+( -121 -64 96 ) ( -121 64 96 ) ( 7 64 96 ) map/lab_games/lg_style_01_wall_blue 0 341 0 -0.059896 0.187500 0 0 0
+( -121 -136 8 ) ( 7 -136 8 ) ( 7 -136 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.059896 0.093750 0 0 0
+( 8 -48 40 ) ( 8 80 40 ) ( 8 80 32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+( 8 64 8 ) ( -120 64 8 ) ( -120 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.059896 0.093750 0 0 0
+( 0 0 -24 ) ( 0 -128 -24 ) ( 0 0 -32 ) map/lab_games/lg_style_01_wall_blue 1024 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 7
+{
+( 64 -8 0 ) ( -64 -8 0 ) ( -64 -136 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.007812 0 0 0
+( -64 -136 96 ) ( -64 -8 96 ) ( 64 -8 96 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.007812 0 0 0
+( -128 -136 8 ) ( 0 -136 8 ) ( 0 -136 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.093750 0 0 0
+( 0 -136 40 ) ( 0 -8 40 ) ( 0 -8 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( -64 -8 40 ) ( -64 -136 40 ) ( -64 -136 32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.007812 0.093750 0 0 0
+( 65 -128 8 ) ( -63 -128 8 ) ( 65 -128 0 ) map/lab_games/lg_style_01_wall_blue 16 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 8
+{
+( -456 208 8 ) ( -456 80 8 ) ( -456 80 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -199 128 8 ) ( -327 128 8 ) ( -327 128 0 ) map/lab_games/lg_style_01_wall_blue 340 0 0 -0.008789 0.093750 0 0 0
+( -327 64 8 ) ( -199 64 8 ) ( -199 64 0 ) map/lab_games/lg_style_01_wall_blue 340 0 0 -0.008789 0.093750 0 0 0
+( -328 64 96 ) ( -328 192 96 ) ( -200 192 96 ) map/lab_games/lg_style_01_wall_blue 340 0 0 -0.008789 0.062500 0 0 0
+( -200 192 0 ) ( -328 192 0 ) ( -328 64 0 ) map/lab_games/lg_style_01_wall_blue 340 0 0 -0.008789 0.062500 0 0 0
+( -448 16 -24 ) ( -448 144 -24 ) ( -448 16 -32 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 9
+{
+( -456 56 136 ) ( -456 48 136 ) ( -456 48 -8 ) map/lab_games/lg_style_01_wall_blue 983 0 0 -0.065104 0.093750 0 0 0
+( -263 64 136 ) ( -391 64 136 ) ( -391 64 -8 ) map/lab_games/lg_style_01_wall_blue 2088 0 0 -0.065104 0.093750 0 0 0
+( -72 -56 168 ) ( -72 -48 168 ) ( -72 -48 24 ) map/lab_games/lg_style_01_wall_blue 983 0 0 -0.065104 0.093750 0 0 0
+( -136 56 136 ) ( -8 56 136 ) ( -8 56 -8 ) map/lab_games/lg_style_01_wall_blue 2088 0 0 -0.065104 0.093750 0 0 0
+( -192 56 96 ) ( -192 64 96 ) ( -64 64 96 ) map/lab_games/lg_style_01_wall_blue 2088 327 0 -0.065104 0.195312 0 0 0
+( -64 64 0 ) ( -192 64 0 ) ( -192 56 0 ) map/lab_games/lg_style_01_wall_blue 2088 327 0 -0.065104 0.195312 0 0 0
+}
+// brush 10
+{
+( 281 128 8 ) ( 153 128 8 ) ( 153 128 0 ) map/lab_games/lg_style_01_wall_blue 1023 0 0 -0.006836 0.093750 0 0 0
+( 392 64 8 ) ( 392 192 8 ) ( 392 192 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( 153 64 8 ) ( 281 64 8 ) ( 281 64 0 ) map/lab_games/lg_style_01_wall_blue 1023 0 0 -0.006836 0.093750 0 0 0
+( 152 64 96 ) ( 152 192 96 ) ( 280 192 96 ) map/lab_games/lg_style_01_wall_blue 1023 0 0 -0.006836 0.062500 0 0 0
+( 280 192 0 ) ( 152 192 0 ) ( 152 64 0 ) map/lab_games/lg_style_01_wall_blue 1023 0 0 -0.006836 0.062500 0 0 0
+( 384 128 8 ) ( 384 0 8 ) ( 384 128 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 11
+{
+( 0 200 40 ) ( 0 72 40 ) ( 0 72 32 ) map/lab_games/lg_style_01_wall_blue 1068 0 0 -0.059896 0.093750 0 0 0
+( 392 56 8 ) ( 392 184 8 ) ( 392 184 0 ) map/lab_games/lg_style_01_wall_blue 1068 0 0 -0.059896 0.093750 0 0 0
+( 256 56 8 ) ( 384 56 8 ) ( 384 56 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+( -136 56 96 ) ( -136 184 96 ) ( -8 184 96 ) map/lab_games/lg_style_01_wall_blue 0 356 0 -0.062500 0.179688 0 0 0
+( -8 184 0 ) ( -136 184 0 ) ( -136 56 0 ) map/lab_games/lg_style_01_wall_blue 0 356 0 -0.062500 0.179688 0 0 0
+( 121 64 8 ) ( -7 64 8 ) ( 121 64 0 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.062500 0.093750 0 0 0
+}
+// brush 12
+{
+( 1064 232 -176 ) ( 544 232 -176 ) ( 544 -240 -176 ) map/lab_games/sky/lg_sky_02_dn 47 502 0 -0.507812 0.460938 0 0 0
+( 544 -240 200 ) ( 1064 -240 200 ) ( 1064 -240 184 ) map/lab_games/sky/lg_sky_02_dn 47 0 0 -0.507812 0.007812 0 0 0
+( 1064 -240 200 ) ( 1064 232 200 ) ( 1064 232 184 ) map/lab_games/sky/lg_sky_02_dn 502 0 0 -0.460938 0.007812 0 0 0
+( 1064 232 200 ) ( 544 232 200 ) ( 544 232 184 ) map/lab_games/sky/lg_sky_02_dn 47 0 0 -0.507812 0.007812 0 0 0
+( 544 232 200 ) ( 544 -240 200 ) ( 544 -240 184 ) map/lab_games/sky/lg_sky_02_dn 502 0 0 -0.460938 0.007812 0 0 0
+( 544 232 -168 ) ( 1064 232 -168 ) ( 544 -240 -168 ) map/lab_games/sky/lg_sky_02_dn 47 502 0 -0.507812 0.460938 0 0 0
+}
+// brush 13
+{
+( 544 -240 200 ) ( 544 232 200 ) ( 1064 232 200 ) map/lab_games/sky/lg_sky_02_up 47 502 0 -0.507812 0.460938 0 0 0
+( 544 -240 200 ) ( 1064 -240 200 ) ( 1064 -240 184 ) map/lab_games/sky/lg_sky_02_up 47 0 0 -0.507812 0.007812 0 0 0
+( 1064 -240 200 ) ( 1064 232 200 ) ( 1064 232 184 ) map/lab_games/sky/lg_sky_02_up 502 0 0 -0.460938 0.007812 0 0 0
+( 1064 232 200 ) ( 544 232 200 ) ( 544 232 184 ) map/lab_games/sky/lg_sky_02_up 47 0 0 -0.507812 0.007812 0 0 0
+( 544 232 200 ) ( 544 -240 200 ) ( 544 -240 184 ) map/lab_games/sky/lg_sky_02_up 502 0 0 -0.460938 0.007812 0 0 0
+( 544 232 192 ) ( 544 -240 192 ) ( 1064 232 192 ) map/lab_games/sky/lg_sky_02_up 47 502 0 -0.507812 0.460938 0 0 0
+}
+// brush 14
+{
+( 1064 232 -176 ) ( 544 232 -176 ) ( 544 -240 -176 ) map/lab_games/sky/lg_sky_02_bk 47 -1023 0 -0.507812 0.007812 0 0 0
+( 544 -240 200 ) ( 544 232 200 ) ( 1064 232 200 ) map/lab_games/sky/lg_sky_02_bk 47 -1023 0 -0.507812 0.007812 0 0 0
+( 544 -240 200 ) ( 1064 -240 200 ) ( 1064 -240 184 ) map/lab_games/sky/lg_sky_02_bk 47 544 0 -0.507812 0.367188 0 0 0
+( 1064 -240 200 ) ( 1064 232 200 ) ( 1064 232 184 ) map/lab_games/sky/lg_sky_02_bk -1023 544 0 -0.007812 0.367188 0 0 0
+( 544 232 200 ) ( 544 -240 200 ) ( 544 -240 184 ) map/lab_games/sky/lg_sky_02_bk -1023 544 0 -0.007812 0.367188 0 0 0
+( 1064 -232 200 ) ( 544 -232 200 ) ( 1064 -232 184 ) map/lab_games/sky/lg_sky_02_bk 47 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 15
+{
+( 1064 232 -176 ) ( 544 232 -176 ) ( 544 -240 -176 ) map/lab_games/sky/lg_sky_02_rt -2 502 0 -0.007812 0.460938 0 0 0
+( 544 -240 200 ) ( 544 232 200 ) ( 1064 232 200 ) map/lab_games/sky/lg_sky_02_rt -2 502 0 -0.007812 0.460938 0 0 0
+( 544 -240 200 ) ( 1064 -240 200 ) ( 1064 -240 184 ) map/lab_games/sky/lg_sky_02_rt -2 544 0 -0.007812 0.367188 0 0 0
+( 1064 -240 200 ) ( 1064 232 200 ) ( 1064 232 184 ) map/lab_games/sky/lg_sky_02_rt 502 544 0 -0.460938 0.367188 0 0 0
+( 1064 232 200 ) ( 544 232 200 ) ( 544 232 184 ) map/lab_games/sky/lg_sky_02_rt -2 544 0 -0.007812 0.367188 0 0 0
+( 1056 232 200 ) ( 1056 -240 200 ) ( 1056 232 184 ) map/lab_games/sky/lg_sky_02_rt 502 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 16
+{
+( 1064 232 -176 ) ( 544 232 -176 ) ( 544 -240 -176 ) map/lab_games/sky/lg_sky_02_ft 47 -1023 0 -0.507812 0.007812 0 0 0
+( 544 -240 200 ) ( 544 232 200 ) ( 1064 232 200 ) map/lab_games/sky/lg_sky_02_ft 47 -1023 0 -0.507812 0.007812 0 0 0
+( 1064 -240 200 ) ( 1064 232 200 ) ( 1064 232 184 ) map/lab_games/sky/lg_sky_02_ft -1023 544 0 -0.007812 0.367188 0 0 0
+( 1064 232 200 ) ( 544 232 200 ) ( 544 232 184 ) map/lab_games/sky/lg_sky_02_ft 47 544 0 -0.507812 0.367188 0 0 0
+( 544 232 200 ) ( 544 -240 200 ) ( 544 -240 184 ) map/lab_games/sky/lg_sky_02_ft -1023 544 0 -0.007812 0.367188 0 0 0
+( 544 224 200 ) ( 1064 224 200 ) ( 544 224 184 ) map/lab_games/sky/lg_sky_02_ft 47 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 17
+{
+( 1064 232 -176 ) ( 544 232 -176 ) ( 544 -240 -176 ) map/lab_games/sky/lg_sky_02_lf -2 502 0 -0.007812 0.460938 0 0 0
+( 544 -240 200 ) ( 544 232 200 ) ( 1064 232 200 ) map/lab_games/sky/lg_sky_02_lf -2 502 0 -0.007812 0.460938 0 0 0
+( 544 -240 200 ) ( 1064 -240 200 ) ( 1064 -240 184 ) map/lab_games/sky/lg_sky_02_lf -2 544 0 -0.007812 0.367188 0 0 0
+( 1064 232 200 ) ( 544 232 200 ) ( 544 232 184 ) map/lab_games/sky/lg_sky_02_lf -2 544 0 -0.007812 0.367188 0 0 0
+( 544 232 200 ) ( 544 -240 200 ) ( 544 -240 184 ) map/lab_games/sky/lg_sky_02_lf 502 544 0 -0.460938 0.367188 0 0 0
+( 552 -240 200 ) ( 552 232 200 ) ( 552 -240 184 ) map/lab_games/sky/lg_sky_02_lf 502 544 0 -0.460938 0.367188 0 0 0
+}
+}
+// entity 1
+{
+"angle" "90"
+"origin" "-32 -96 24"
+"classname" "info_player_start"
+"spawn_orientation_segment" "20"
+}
+// entity 2
+{
+"light" "500"
+"origin" "-32 96 80"
+"classname" "light"
+}
+// entity 3
+{
+"classname" "light"
+"origin" "-192 96 80"
+"light" "500"
+}
+// entity 4
+{
+"classname" "light"
+"origin" "128 96 80"
+"light" "500"
+}
+// entity 5
+{
+"classname" "light"
+"origin" "-32 -32 80"
+"light" "500"
+}
+// entity 6
+{
+"light" "500"
+"origin" "288 96 80"
+"classname" "light"
+}
+// entity 7
+{
+"light" "500"
+"origin" "-352 96 80"
+"classname" "light"
+}
+// entity 8
+{
+"classname" "placeholder_1"
+"origin" "-424 96 16"
+}
+// entity 9
+{
+"classname" "placeholder_2"
+"origin" "360 96 16"
+}
+// entity 10
+{
+"classname" "_skybox"
+"origin" "776 -8 -128"
+}
+// entity 11
+{
+"model" "models/stadium.md3"
+"origin" "776 -8 -82"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 12
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "800 -48 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 13
+{
+"classname" "misc_model"
+"origin" "848 -24 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 14
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "808 -16 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 15
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "752 0 -152"
+"classname" "misc_model"
+"angle" "159"
+}
diff --git a/assets/maps/em_watermaze.map b/assets/maps/em_watermaze.map
new file mode 100644
index 00000000..5cd71ba3
--- /dev/null
+++ b/assets/maps/em_watermaze.map
@@ -0,0 +1,463 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( 768 -768 0 ) ( -768 -768 0 ) ( 768 768 0 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 768 0 -8 ) ( 768 0 200 ) ( 768 768 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 710 294 -8 ) ( 710 294 200 ) ( 416 1004 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 543 543 -8 ) ( 543 543 200 ) ( 0 1086 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 294 710 -8 ) ( 294 710 200 ) ( -416 1004 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 0 768 -8 ) ( 0 768 200 ) ( -768 768 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -294 710 -8 ) ( -294 710 200 ) ( -1004 416 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -543 543 -8 ) ( -543 543 200 ) ( -1086 0 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -710 294 -8 ) ( -710 294 200 ) ( -1004 -416 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -768 0 -8 ) ( -768 0 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -710 -294 -8 ) ( -710 -294 200 ) ( -416 -1004 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -543 -543 -8 ) ( -543 -543 200 ) ( 0 -1086 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( -294 -710 -8 ) ( -294 -710 200 ) ( 416 -1004 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 0 -768 -8 ) ( 0 -768 200 ) ( 768 -768 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 294 -710 -8 ) ( 294 -710 200 ) ( 1004 -416 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 543 -543 -8 ) ( 543 -543 200 ) ( 1086 0 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+( 710 -294 -8 ) ( 710 -294 200 ) ( 1004 416 200 ) map/lab_games/lg_style_01_floor_blue 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 1
+{
+( 768 -768 192 ) ( 768 768 192 ) ( -768 -768 192 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 768 0 -8 ) ( 768 0 200 ) ( 768 768 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 710 294 -8 ) ( 710 294 200 ) ( 416 1004 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 543 543 -8 ) ( 543 543 200 ) ( 0 1086 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 294 710 -8 ) ( 294 710 200 ) ( -416 1004 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 0 768 -8 ) ( 0 768 200 ) ( -768 768 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -294 710 -8 ) ( -294 710 200 ) ( -1004 416 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -543 543 -8 ) ( -543 543 200 ) ( -1086 0 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -710 294 -8 ) ( -710 294 200 ) ( -1004 -416 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -768 0 -8 ) ( -768 0 200 ) ( -768 -768 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -710 -294 -8 ) ( -710 -294 200 ) ( -416 -1004 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -543 -543 -8 ) ( -543 -543 200 ) ( 0 -1086 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( -294 -710 -8 ) ( -294 -710 200 ) ( 416 -1004 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 0 -768 -8 ) ( 0 -768 200 ) ( 768 -768 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 294 -710 -8 ) ( 294 -710 200 ) ( 1004 -416 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 543 -543 -8 ) ( 543 -543 200 ) ( 1086 0 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+( 710 -294 -8 ) ( 710 -294 200 ) ( 1004 416 200 ) map/lab_games/fake_sky 0 0 0 0.031250 0.031200 0 0 0
+}
+// brush 2
+{
+( 703 -291 200 ) ( 703 -291 -8 ) ( 997 419 200 ) map/lab_games/lg_style_01_wall_blue 149 984 0 -0.153318 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -191 586 0 -0.063487 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -191 586 0 -0.063484 0.190000 0 0 0
+( 768 0 -8 ) ( 768 0 200 ) ( 768 768 200 ) map/lab_games/lg_style_01_wall_blue 547 984 0 -0.009720 0.190000 0 0 0
+( 543 -543 -8 ) ( 543 -543 200 ) ( 1086 0 200 ) map/lab_games/lg_style_01_wall_blue 306 984 0 -0.006868 0.190000 0 0 0
+( 710 -294 -8 ) ( 710 -294 200 ) ( 1004 416 200 ) map/lab_games/lg_style_01_wall_blue 922 984 0 -0.136728 0.190000 0 0 0
+}
+// brush 3
+{
+( 537 -537 200 ) ( 537 -537 -8 ) ( 1080 6 200 ) map/lab_games/lg_style_01_wall_blue 668 984 0 -0.120634 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 354 334 0 -0.120635 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 354 334 0 -0.120634 0.190000 0 0 0
+( 294 -710 -8 ) ( 294 -710 200 ) ( 1004 -416 200 ) map/lab_games/lg_style_01_wall_blue -644 984 0 -0.010001 0.190000 0 0 0
+( 543 -543 -8 ) ( 543 -543 200 ) ( 1086 0 200 ) map/lab_games/lg_style_01_wall_blue 21 984 0 -0.106492 0.190000 0 0 0
+( 710 -294 -8 ) ( 710 -294 200 ) ( 1004 416 200 ) map/lab_games/lg_style_01_wall_blue 631 984 0 -0.009999 0.190000 0 0 0
+}
+// brush 4
+{
+( 291 -703 200 ) ( 291 -703 -8 ) ( 1001 -409 200 ) map/lab_games/lg_style_01_wall_blue -150 984 0 -0.153316 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -150 95 0 -0.153315 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -150 95 0 -0.153318 0.190000 0 0 0
+( 0 -768 -8 ) ( 0 -768 200 ) ( 768 -768 200 ) map/lab_games/lg_style_01_wall_blue -549 984 0 -0.009720 0.190000 0 0 0
+( 294 -710 -8 ) ( 294 -710 200 ) ( 1004 -416 200 ) map/lab_games/lg_style_01_wall_blue 100 984 0 -0.136728 0.190000 0 0 0
+( 543 -543 -8 ) ( 543 -543 200 ) ( 1086 0 200 ) map/lab_games/lg_style_01_wall_blue 334 984 0 -0.006871 0.190000 0 0 0
+}
+// brush 5
+{
+( 0 -760 200 ) ( 0 -760 -8 ) ( 768 -760 200 ) map/lab_games/lg_style_01_wall_blue 0 984 0 -0.169191 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.169186 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.169193 0.190000 0 0 0
+( -294 -710 -8 ) ( -294 -710 200 ) ( 416 -1004 200 ) map/lab_games/lg_style_01_wall_blue -958 984 0 -0.009434 0.190000 0 0 0
+( 0 -768 -8 ) ( 0 -768 200 ) ( 768 -768 200 ) map/lab_games/lg_style_01_wall_blue 0 984 0 -0.150323 0.190000 0 0 0
+( 294 -710 -8 ) ( 294 -710 200 ) ( 1004 -416 200 ) map/lab_games/lg_style_01_wall_blue -66 984 0 -0.009433 0.190000 0 0 0
+}
+// brush 6
+{
+( -291 -703 200 ) ( -291 -703 -8 ) ( 419 -997 200 ) map/lab_games/lg_style_01_wall_blue 149 984 0 -0.153316 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 149 95 0 -0.153318 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 149 95 0 -0.153319 0.190000 0 0 0
+( -543 -543 -8 ) ( -543 -543 200 ) ( 0 -1086 200 ) map/lab_games/lg_style_01_wall_blue 301 984 0 -0.006869 0.190000 0 0 0
+( -294 -710 -8 ) ( -294 -710 200 ) ( 416 -1004 200 ) map/lab_games/lg_style_01_wall_blue -102 984 0 -0.136730 0.190000 0 0 0
+( 0 -768 -8 ) ( 0 -768 200 ) ( 768 -768 200 ) map/lab_games/lg_style_01_wall_blue -477 984 0 -0.009720 0.190000 0 0 0
+}
+// brush 7
+{
+( -537 -537 200 ) ( -537 -537 -8 ) ( 6 -1080 200 ) map/lab_games/lg_style_01_wall_blue 668 984 0 -0.120634 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -355 334 0 -0.120635 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -355 334 0 -0.120635 0.190000 0 0 0
+( -710 -294 -8 ) ( -710 -294 200 ) ( -416 -1004 200 ) map/lab_games/lg_style_01_wall_blue 638 984 0 -0.010000 0.190000 0 0 0
+( -543 -543 -8 ) ( -543 -543 200 ) ( 0 -1086 200 ) map/lab_games/lg_style_01_wall_blue 21 984 0 -0.106492 0.190000 0 0 0
+( -294 -710 -8 ) ( -294 -710 200 ) ( 416 -1004 200 ) map/lab_games/lg_style_01_wall_blue -383 984 0 -0.010001 0.190000 0 0 0
+}
+// brush 8
+{
+( -703 -291 200 ) ( -703 -291 -8 ) ( -409 -1001 200 ) map/lab_games/lg_style_01_wall_blue 149 984 0 -0.153315 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -834 586 0 -0.063486 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -834 586 0 -0.063486 0.190000 0 0 0
+( -768 0 -8 ) ( -768 0 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 548 984 0 -0.009720 0.190000 0 0 0
+( -710 -294 -8 ) ( -710 -294 200 ) ( -416 -1004 200 ) map/lab_games/lg_style_01_wall_blue 922 984 0 -0.136727 0.190000 0 0 0
+( -543 -543 -8 ) ( -543 -543 200 ) ( 0 -1086 200 ) map/lab_games/lg_style_01_wall_blue 306 984 0 -0.006868 0.190000 0 0 0
+}
+// brush 9
+{
+( -760 0 200 ) ( -760 0 -8 ) ( -760 -768 200 ) map/lab_games/lg_style_01_wall_blue 1023 984 0 -0.169191 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 0 511 0 -0.003906 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 0 512 0 -0.003906 0.190000 0 0 0
+( -710 294 -8 ) ( -710 294 200 ) ( -1004 -416 200 ) map/lab_games/lg_style_01_wall_blue 957 984 0 -0.009434 0.190000 0 0 0
+( -768 0 -8 ) ( -768 0 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 0 984 0 -0.150323 0.190000 0 0 0
+( -710 -294 -8 ) ( -710 -294 200 ) ( -416 -1004 200 ) map/lab_games/lg_style_01_wall_blue 67 984 0 -0.009434 0.190000 0 0 0
+}
+// brush 10
+{
+( -703 291 200 ) ( -703 291 -8 ) ( -997 -419 200 ) map/lab_games/lg_style_01_wall_blue 874 984 0 -0.153314 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -834 437 0 -0.063486 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -834 437 0 -0.063484 0.190000 0 0 0
+( -543 543 -8 ) ( -543 543 200 ) ( -1086 0 200 ) map/lab_games/lg_style_01_wall_blue 707 984 0 -0.006869 0.190000 0 0 0
+( -710 294 -8 ) ( -710 294 200 ) ( -1004 -416 200 ) map/lab_games/lg_style_01_wall_blue 101 984 0 -0.136731 0.190000 0 0 0
+( -768 0 -8 ) ( -768 0 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 476 984 0 -0.009720 0.190000 0 0 0
+}
+// brush 11
+{
+( -537 537 200 ) ( -537 537 -8 ) ( -1080 -6 200 ) map/lab_games/lg_style_01_wall_blue 355 984 0 -0.120634 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -356 689 0 -0.120632 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -356 689 0 -0.120632 0.190000 0 0 0
+( -294 710 -8 ) ( -294 710 200 ) ( -1004 416 200 ) map/lab_games/lg_style_01_wall_blue -386 984 0 -0.010000 0.190000 0 0 0
+( -543 543 -8 ) ( -543 543 200 ) ( -1086 0 200 ) map/lab_games/lg_style_01_wall_blue 1002 984 0 -0.106492 0.190000 0 0 0
+( -710 294 -8 ) ( -710 294 200 ) ( -1004 -416 200 ) map/lab_games/lg_style_01_wall_blue 385 984 0 -0.010000 0.190000 0 0 0
+}
+// brush 12
+{
+( -291 703 200 ) ( -291 703 -8 ) ( -1001 409 200 ) map/lab_games/lg_style_01_wall_blue 149 984 0 -0.153315 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 149 928 0 -0.153319 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 149 928 0 -0.153318 0.190000 0 0 0
+( 0 768 -8 ) ( 0 768 200 ) ( -768 768 200 ) map/lab_games/lg_style_01_wall_blue -476 984 0 -0.009720 0.190000 0 0 0
+( -294 710 -8 ) ( -294 710 200 ) ( -1004 416 200 ) map/lab_games/lg_style_01_wall_blue -102 984 0 -0.136731 0.190000 0 0 0
+( -543 543 -8 ) ( -543 543 200 ) ( -1086 0 200 ) map/lab_games/lg_style_01_wall_blue 726 984 0 -0.006868 0.190000 0 0 0
+}
+// brush 13
+{
+( 0 760 200 ) ( 0 760 -8 ) ( -768 760 200 ) map/lab_games/lg_style_01_wall_blue 0 984 0 -0.169191 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.169190 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 0 0 0 -0.169191 0.190000 0 0 0
+( 294 710 -8 ) ( 294 710 200 ) ( -416 1004 200 ) map/lab_games/lg_style_01_wall_blue -66 984 0 -0.009433 0.190000 0 0 0
+( 0 768 -8 ) ( 0 768 200 ) ( -768 768 200 ) map/lab_games/lg_style_01_wall_blue 0 984 0 -0.150323 0.190000 0 0 0
+( -294 710 -8 ) ( -294 710 200 ) ( -1004 416 200 ) map/lab_games/lg_style_01_wall_blue -957 984 0 -0.009434 0.190000 0 0 0
+}
+// brush 14
+{
+( 291 703 200 ) ( 291 703 -8 ) ( -419 997 200 ) map/lab_games/lg_style_01_wall_blue -150 984 0 -0.153315 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -150 928 0 -0.153318 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -150 928 0 -0.153317 0.190000 0 0 0
+( 543 543 -8 ) ( 543 543 200 ) ( 0 1086 200 ) map/lab_games/lg_style_01_wall_blue 712 984 0 -0.006869 0.190000 0 0 0
+( 294 710 -8 ) ( 294 710 200 ) ( -416 1004 200 ) map/lab_games/lg_style_01_wall_blue 100 984 0 -0.136727 0.190000 0 0 0
+( 0 768 -8 ) ( 0 768 200 ) ( -768 768 200 ) map/lab_games/lg_style_01_wall_blue -548 984 0 -0.009721 0.190000 0 0 0
+}
+// brush 15
+{
+( 537 537 200 ) ( 537 537 -8 ) ( -6 1080 200 ) map/lab_games/lg_style_01_wall_blue 355 984 0 -0.120635 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 354 689 0 -0.120635 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 355 689 0 -0.120630 0.190000 0 0 0
+( 710 294 -8 ) ( 710 294 200 ) ( 416 1004 200 ) map/lab_games/lg_style_01_wall_blue 382 984 0 -0.010001 0.190000 0 0 0
+( 543 543 -8 ) ( 543 543 200 ) ( 0 1086 200 ) map/lab_games/lg_style_01_wall_blue 1002 984 0 -0.106495 0.190000 0 0 0
+( 294 710 -8 ) ( 294 710 200 ) ( -416 1004 200 ) map/lab_games/lg_style_01_wall_blue -642 984 0 -0.010001 0.190000 0 0 0
+}
+// brush 16
+{
+( 703 291 200 ) ( 703 291 -8 ) ( 409 1001 200 ) map/lab_games/lg_style_01_wall_blue 874 984 0 -0.153318 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue -191 437 0 -0.063486 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue -191 437 0 -0.063486 0.190000 0 0 0
+( 768 0 -8 ) ( 768 0 200 ) ( 768 768 200 ) map/lab_games/lg_style_01_wall_blue 475 984 0 -0.009720 0.190000 0 0 0
+( 710 294 -8 ) ( 710 294 200 ) ( 416 1004 200 ) map/lab_games/lg_style_01_wall_blue 101 984 0 -0.136728 0.190000 0 0 0
+( 543 543 -8 ) ( 543 543 200 ) ( 0 1086 200 ) map/lab_games/lg_style_01_wall_blue 717 984 0 -0.006868 0.190000 0 0 0
+}
+// brush 17
+{
+( 760 0 200 ) ( 760 0 -8 ) ( 760 768 200 ) map/lab_games/lg_style_01_wall_blue 1023 984 0 -0.169191 0.190000 0 0 0
+( 768 768 200 ) ( 768 -768 200 ) ( -768 -768 200 ) map/lab_games/lg_style_01_wall_blue 0 511 0 -0.003906 0.190000 0 0 0
+( -768 -768 -8 ) ( 768 -768 -8 ) ( 768 768 -8 ) map/lab_games/lg_style_01_wall_blue 0 512 0 -0.003906 0.190000 0 0 0
+( 768 0 -8 ) ( 768 0 200 ) ( 768 768 200 ) map/lab_games/lg_style_01_wall_blue 0 984 0 -0.150323 0.190000 0 0 0
+( 710 294 -8 ) ( 710 294 200 ) ( 416 1004 200 ) map/lab_games/lg_style_01_wall_blue 955 984 0 -0.009434 0.190000 0 0 0
+( 710 -294 -8 ) ( 710 -294 200 ) ( 1004 416 200 ) map/lab_games/lg_style_01_wall_blue 68 984 0 -0.009435 0.190000 0 0 0
+}
+// brush 18
+{
+( 710 -294 0 ) ( 710 -294 208 ) ( 1004 416 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 543 -543 0 ) ( 543 -543 208 ) ( 1086 0 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 294 -710 0 ) ( 294 -710 208 ) ( 1004 -416 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 0 -768 0 ) ( 0 -768 208 ) ( 768 -768 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -294 -710 0 ) ( -294 -710 208 ) ( 416 -1004 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -543 -543 0 ) ( -543 -543 208 ) ( 0 -1086 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -710 -294 0 ) ( -710 -294 208 ) ( -416 -1004 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -768 0 0 ) ( -768 0 208 ) ( -768 -768 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -710 294 0 ) ( -710 294 208 ) ( -1004 -416 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -543 543 0 ) ( -543 543 208 ) ( -1086 0 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -294 710 0 ) ( -294 710 208 ) ( -1004 416 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 0 768 0 ) ( 0 768 208 ) ( -768 768 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 294 710 0 ) ( 294 710 208 ) ( -416 1004 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 543 543 0 ) ( 543 543 208 ) ( 0 1086 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 710 294 0 ) ( 710 294 208 ) ( 416 1004 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( 768 0 0 ) ( 768 0 208 ) ( 768 768 208 ) map/water_d 0 128 0 0.062500 0.062500 0 0 0
+( -768 -768 0 ) ( 768 -768 0 ) ( 768 768 0 ) map/water_d 0 0 0 0.062500 0.062500 0 0 0
+( 768 -768 16 ) ( -768 -768 16 ) ( 768 768 16 ) map/water_d 0 0 0 0.062500 0.062500 0 0 0
+}
+// brush 19
+{
+( 710 -294 0 ) ( 710 -294 208 ) ( 1004 416 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 543 -543 0 ) ( 543 -543 208 ) ( 1086 0 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 294 -710 0 ) ( 294 -710 208 ) ( 1004 -416 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 0 -768 0 ) ( 0 -768 208 ) ( 768 -768 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -294 -710 0 ) ( -294 -710 208 ) ( 416 -1004 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -543 -543 0 ) ( -543 -543 208 ) ( 0 -1086 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -710 -294 0 ) ( -710 -294 208 ) ( -416 -1004 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -768 0 0 ) ( -768 0 208 ) ( -768 -768 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -710 294 0 ) ( -710 294 208 ) ( -1004 -416 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -543 543 0 ) ( -543 543 208 ) ( -1086 0 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -294 710 0 ) ( -294 710 208 ) ( -1004 416 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 0 768 0 ) ( 0 768 208 ) ( -768 768 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 294 710 0 ) ( 294 710 208 ) ( -416 1004 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 543 543 0 ) ( 543 543 208 ) ( 0 1086 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 710 294 0 ) ( 710 294 208 ) ( 416 1004 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 768 0 0 ) ( 768 0 208 ) ( 768 768 208 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( -768 -768 0 ) ( 768 -768 0 ) ( 768 768 0 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+( 768 -768 16 ) ( -768 -768 16 ) ( 768 768 16 ) map/water_d 0 0 0 0.500000 0.500000 0 0 0
+}
+}
+// entity 1
+{
+"light" "500"
+"origin" "0 0 168"
+"classname" "light"
+}
+// entity 2
+{
+"classname" "light"
+"origin" "0 416 168"
+"light" "1500"
+}
+// entity 3
+{
+"classname" "light"
+"origin" "-416 0 168"
+"light" "1500"
+}
+// entity 4
+{
+"light" "1500"
+"origin" "416 0 168"
+"classname" "light"
+}
+// entity 5
+{
+"angle" "315"
+"origin" "-488 488 24"
+"classname" "info_player_start"
+}
+// entity 6
+{
+"origin" "-728 0 64"
+"classname" "info_player_start"
+}
+// entity 7
+{
+"angle" "45"
+"classname" "info_player_start"
+"origin" "-496 -496 24"
+}
+// entity 8
+{
+"angle" "90"
+"origin" "0 -712 16"
+"classname" "info_player_start"
+}
+// entity 9
+{
+"angle" "135"
+"origin" "504 -504 24"
+"classname" "info_player_start"
+}
+// entity 10
+{
+"angle" "180"
+"classname" "info_player_start"
+"origin" "712 0 24"
+}
+// entity 11
+{
+"angle" "225"
+"classname" "info_player_start"
+"origin" "504 504 24"
+}
+// entity 12
+{
+"angle" "270"
+"classname" "info_player_start"
+"origin" "0 720 24"
+}
+// entity 13
+{
+"classname" "light"
+"origin" "0 -416 168"
+"light" "1500"
+}
+// entity 14
+{
+"target" "reward"
+"targetname" "timer"
+"origin" "-40 136 80"
+"classname" "func_timer"
+"wait" "0"
+"random" "0"
+}
+// entity 15
+{
+"classname" "target_callback"
+"targetname" "reward"
+}
+// entity 16
+{
+"lip" "56"
+"wait" "0.001"
+"speed" "700"
+"random_platform" "1"
+"angle" "-1"
+"classname" "func_button"
+"target" "timer"
+// brush 0
+{
+( 64 64 192 ) ( 64 -64 192 ) ( -64 -64 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -64 -64 0 ) ( 64 -64 0 ) ( 64 64 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 64 0 128 ) ( 64 0 192 ) ( 64 64 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 45 45 128 ) ( 45 45 192 ) ( 0 90 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 0 64 128 ) ( 0 64 192 ) ( -64 64 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -45 45 128 ) ( -45 45 192 ) ( -90 0 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -64 0 128 ) ( -64 0 192 ) ( -64 -64 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -45 -45 128 ) ( -45 -45 192 ) ( 0 -90 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 0 -64 128 ) ( 0 -64 192 ) ( 64 -64 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 45 -45 128 ) ( 45 -45 192 ) ( 90 0 192 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+}
+}
+// entity 17
+{
+"random_platform" "1"
+"classname" "func_plat"
+"height" "96"
+"dmg" "0"
+"targetname" "platform"
+"speed" "700"
+// brush 0
+{
+( 63 64 94 ) ( 63 -64 94 ) ( -65 -64 94 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( -64 -64 0 ) ( 64 -64 0 ) ( 64 64 0 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( 64 0 0 ) ( 64 0 64 ) ( 64 64 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( 45 45 0 ) ( 45 45 64 ) ( 0 90 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( 0 64 0 ) ( 0 64 64 ) ( -64 64 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( -45 45 0 ) ( -45 45 64 ) ( -90 0 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( -64 0 0 ) ( -64 0 64 ) ( -64 -64 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( -45 -45 0 ) ( -45 -45 64 ) ( 0 -90 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( 0 -64 0 ) ( 0 -64 64 ) ( 64 -64 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+( 45 -45 0 ) ( 45 -45 64 ) ( 90 0 64 ) map/fut_utility_panel_01_d 0 0 0 0.500000 0.500000 0 0 0
+}
+}
+// entity 18
+{
+"random_platform" "1"
+"classname" "func_button"
+"target" "platform"
+"wait" "0.25"
+"angle" "-2"
+"speed" "700"
+// brush 0
+{
+( 45 -45 0 ) ( 45 -45 64 ) ( 90 0 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 0 -64 0 ) ( 0 -64 64 ) ( 64 -64 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -45 -45 0 ) ( -45 -45 64 ) ( 0 -90 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -64 0 0 ) ( -64 0 64 ) ( -64 -64 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -45 45 0 ) ( -45 45 64 ) ( -90 0 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 0 64 0 ) ( 0 64 64 ) ( -64 64 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 45 45 0 ) ( 45 45 64 ) ( 0 90 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 64 0 0 ) ( 64 0 64 ) ( 64 64 64 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( -64 -64 0 ) ( 64 -64 0 ) ( 64 64 0 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+( 64 64 24 ) ( 64 -64 24 ) ( -64 -64 24 ) map/ghost 0 0 0 0.500000 0.500000 0 12 0
+}
+}
+// entity 19
+{
+"angle" "270"
+"classname" "misc_model"
+"origin" "0 -760 96"
+"model" "models/fut_ldm_img_frame_arc.md3"
+"modelscale" "0.5"
+}
+// entity 20
+{
+"modelscale" "0.5"
+"model" "models/fut_ldm_img_frame_crc.md3"
+"origin" "760 0 96"
+"classname" "misc_model"
+"angle" "360"
+}
+// entity 21
+{
+"angle" "180"
+"classname" "misc_model"
+"origin" "-760 0 96"
+"model" "models/fut_ldm_img_frame_clc.md3"
+"modelscale" "0.5"
+}
+// entity 22
+{
+"modelscale" "0.5"
+"model" "models/fut_ldm_img_frame_sqc.md3"
+"origin" "0 760 96"
+"classname" "misc_model"
+"angle" "90"
+}
+// entity 23
+{
+"angle" "225"
+"classname" "misc_model"
+"origin" "-536 -536 96"
+"model" "models/fut_ldm_img_frame_hxc.md3"
+"modelscale" "0.5"
+}
+// entity 24
+{
+"modelscale" "0.5"
+"model" "models/fut_ldm_img_frame_trc.md3"
+"origin" "536 528 96"
+"classname" "misc_model"
+"angle" "45"
+}
+// entity 25
+{
+"angle" "135"
+"classname" "misc_model"
+"origin" "-536 536 96"
+"model" "models/fut_ldm_img_frame_dic.md3"
+"modelscale" "0.5"
+}
+// entity 26
+{
+"angle" "315"
+"classname" "misc_model"
+"origin" "536 -536 96"
+"model" "models/fut_ldm_img_frame_stc.md3"
+"modelscale" "0.5"
+}
+// entity 27
+{
+"radius" "200"
+"targetname" "platform"
+"origin" "0 0 120"
+"classname" "func_lua_mover"
+"random_platform" "1"
+"id" "1"
+}
diff --git a/assets/maps/lookat_test.map b/assets/maps/lookat_test.map
new file mode 100644
index 00000000..ef189672
--- /dev/null
+++ b/assets/maps/lookat_test.map
@@ -0,0 +1,105 @@
+// entity 0
+{
+ "light" "100"
+ "worldtype" "2"
+ "classname" "worldspawn"
+ // brush 0
+ {
+ ( 0 0 1 ) ( 0 1 1 ) ( 1 0 1 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 300 0 ) ( 1 300 0 ) ( 0 300 1 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.062500 0.062500 0 0 0
+ ( 300 0 0 ) ( 300 0 1 ) ( 300 1 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.062500 0.062500 0 0 0
+ }
+ // brush 1
+ {
+ ( 0 0 100 ) ( 0 1 100 ) ( 1 0 100 ) map/lab_games/fake_sky 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 0 99 ) ( 1 0 99 ) ( 0 1 99 ) map/lab_games/fake_sky 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 300 0 ) ( 1 300 0 ) ( 0 300 1 ) map/lab_games/fake_sky 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) map/lab_games/fake_sky 0 0 0 0.062500 0.062500 0 0 0
+ ( 300 0 0 ) ( 300 0 1 ) ( 300 1 0 ) map/lab_games/fake_sky 0 0 0 0.062500 0.062500 0 0 0
+ ( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) map/lab_games/fake_sky 0 0 0 0.062500 0.062500 0 0 0
+ }
+ // brush 2
+ {
+ ( 0 0 100 ) ( 0 1 100 ) ( 1 0 100 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 1 0 ) ( 1 1 0 ) ( 0 1 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 300 0 0 ) ( 300 0 1 ) ( 300 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ }
+ // brush 3
+ {
+ ( 0 0 100 ) ( 0 1 100 ) ( 1 0 100 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 300 0 ) ( 1 300 0 ) ( 0 300 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 299 0 ) ( 0 299 1 ) ( 1 299 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 300 0 0 ) ( 300 0 1 ) ( 300 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ }
+ // brush 4
+ {
+ ( 0 0 100 ) ( 0 1 100 ) ( 1 0 100 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 300 0 ) ( 1 300 0 ) ( 0 300 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 1 0 0 ) ( 1 0 1 ) ( 1 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ }
+ // brush 5
+ {
+ ( 0 0 100 ) ( 0 1 100 ) ( 1 0 100 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 300 0 ) ( 1 300 0 ) ( 0 300 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 300 0 0 ) ( 300 0 1 ) ( 300 1 0 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ ( 299 0 0 ) ( 299 1 0 ) ( 299 0 1 ) map/lab_games/lg_style_02_wall_yellow 0 0 0 0.250000 0.250000 0 0 0
+ }
+}
+// entity 1
+{
+ "spawnflags" "0"
+ "style" "0"
+ "light" "100.000000"
+ "origin" "150.000000 150.000000 70.000000"
+ "classname" "light"
+}
+// entity 2
+{
+ "classname" "info_player_deathmatch"
+ "angle" "0.0000"
+ "randomAngleRange" "0"
+ "origin" "150.000000 150.000000 40.000000"
+}
+// entity 3
+{
+ "targetname" "balloon"
+ "classname" "misc_model"
+ "origin" "250.000 150.000 40.000"
+ "model" "models/hr_balloon.md3"
+}
+// entity 4
+{
+ "count" "1"
+ "targetname" "reward"
+ "origin" "250 150 40"
+ "classname" "target_score"
+ "max_count" "2"
+}
+// entity 5
+{
+ "classname" "trigger_lookat"
+ "wait" "0.75"
+ "target" "reward"
+ // brush 0
+ {
+ ( 288 168 24 ) ( 264 168 24 ) ( 264 136 24 ) common/caulk 0 0 0 0.500000 0.500000 0 4 0
+ ( 264 136 64 ) ( 264 168 64 ) ( 288 168 64 ) common/caulk 0 0 0 0.500000 0.500000 0 4 0
+ ( 264 136 32 ) ( 288 136 32 ) ( 288 136 24 ) common/caulk 0 -16 0 0.500000 0.500000 0 4 0
+ ( 288 136 32 ) ( 288 168 32 ) ( 288 168 24 ) common/caulk 0 -16 0 0.500000 0.500000 0 4 0
+ ( 288 168 32 ) ( 264 168 32 ) ( 264 168 24 ) common/caulk 0 -16 0 0.500000 0.500000 0 4 0
+ ( 264 168 32 ) ( 264 136 32 ) ( 264 136 24 ) common/caulk 0 -16 0 0.500000 0.500000 0 4 0
+ }
+}
\ No newline at end of file
diff --git a/assets/maps/lt_space_bounce_01.map b/assets/maps/lt_space_bounce_01.map
index 90c446e8..bff473f1 100644
--- a/assets/maps/lt_space_bounce_01.map
+++ b/assets/maps/lt_space_bounce_01.map
@@ -300,24 +300,6 @@
}
// brush 33
{
-( 1592 320 -480 ) ( 1592 -1472 -480 ) ( 1592 320 -496 ) map/nebula 248 256 0 -5.593750 6.031250 0 0 0
-( 1072 848 -1544 ) ( -928 848 -1544 ) ( -928 -944 -1544 ) map/nebula 0 248 0 -0.015625 5.593750 0 0 0
-( -928 -944 1544 ) ( -928 848 1544 ) ( 1072 848 1544 ) map/nebula 0 248 0 -0.015625 5.593750 0 0 0
-( -400 -1472 128 ) ( 1600 -1472 128 ) ( 1600 -1472 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
-( 1600 -1472 128 ) ( 1600 320 128 ) ( 1600 320 112 ) map/nebula 248 256 0 -5.593750 6.031250 0 0 0
-( 552 1392 128 ) ( -1448 1392 128 ) ( -1448 1392 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
-}
-// brush 34
-{
-( 1600 -1464 128 ) ( -400 -1464 128 ) ( 1600 -1464 112 ) map/nebula 268 256 0 -5.953125 6.031250 0 0 0
-( 1072 848 -1544 ) ( -928 848 -1544 ) ( -928 -944 -1544 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
-( -928 -944 1544 ) ( -928 848 1544 ) ( 1072 848 1544 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
-( -400 -1472 128 ) ( 1600 -1472 128 ) ( 1600 -1472 112 ) map/nebula 268 256 0 -5.953125 6.031250 0 0 0
-( 1600 -1472 128 ) ( 1600 320 128 ) ( 1600 320 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
-( -1448 1392 128 ) ( -1448 -400 128 ) ( -1448 -400 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
-}
-// brush 35
-{
( -928 848 -1536 ) ( 1072 848 -1536 ) ( -928 -944 -1536 ) map/nebula 268 248 0 -5.953125 5.593750 0 0 0
( 1072 848 -1544 ) ( -928 848 -1544 ) ( -928 -944 -1544 ) map/nebula 268 248 0 -5.953125 5.593750 0 0 0
( -400 -1472 -1256 ) ( 1600 -1472 -1256 ) ( 1600 -1472 -1272 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
@@ -325,7 +307,7 @@
( 552 1392 -1256 ) ( -1448 1392 -1256 ) ( -1448 1392 -1272 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
( -1448 1392 -1256 ) ( -1448 -400 -1256 ) ( -1448 -400 -1272 ) map/nebula 248 0 0 -5.593750 0.015625 0 0 0
}
-// brush 36
+// brush 34
{
( 1816 -904 -168 ) ( 2336 -904 -168 ) ( 1816 -1376 -168 ) map/nebula_dn 251 42 0 -1.015625 0.921875 0 0 0
( 1816 -904 200 ) ( 1816 -1376 200 ) ( 1816 -1376 184 ) map/nebula_dn 42 0 0 -0.921875 0.015625 0 0 0
@@ -334,7 +316,7 @@
( 1816 -1376 200 ) ( 2336 -1376 200 ) ( 2336 -1376 184 ) map/nebula_dn 251 0 0 -1.015625 0.015625 0 0 0
( 2336 -904 -176 ) ( 1816 -904 -176 ) ( 1816 -1376 -176 ) map/nebula_dn 251 42 0 -1.015625 0.921875 0 0 0
}
-// brush 37
+// brush 35
{
( 1816 -904 192 ) ( 1816 -1376 192 ) ( 2336 -904 192 ) map/nebula_up 251 42 0 -1.015625 0.921875 0 0 0
( 1816 -904 200 ) ( 1816 -1376 200 ) ( 1816 -1376 184 ) map/nebula_up 42 0 0 -0.921875 0.015625 0 0 0
@@ -343,7 +325,7 @@
( 1816 -1376 200 ) ( 2336 -1376 200 ) ( 2336 -1376 184 ) map/nebula_up 251 0 0 -1.015625 0.015625 0 0 0
( 1816 -1376 200 ) ( 1816 -904 200 ) ( 2336 -904 200 ) map/nebula_up 251 42 0 -1.015625 0.921875 0 0 0
}
-// brush 38
+// brush 36
{
( 2336 -1368 200 ) ( 1816 -1368 200 ) ( 2336 -1368 184 ) map/nebula_bk 251 272 0 -1.015625 0.734375 0 0 0
( 1816 -904 200 ) ( 1816 -1376 200 ) ( 1816 -1376 184 ) map/nebula_bk 0 272 0 -0.015625 0.734375 0 0 0
@@ -352,7 +334,7 @@
( 1816 -1376 200 ) ( 1816 -904 200 ) ( 2336 -904 200 ) map/nebula_bk 251 0 0 -1.015625 0.015625 0 0 0
( 2336 -904 -176 ) ( 1816 -904 -176 ) ( 1816 -1376 -176 ) map/nebula_bk 251 0 0 -1.015625 0.015625 0 0 0
}
-// brush 39
+// brush 37
{
( 2328 -904 200 ) ( 2328 -1376 200 ) ( 2328 -904 184 ) map/nebula_rt 42 272 0 -0.921875 0.734375 0 0 0
( 2336 -904 200 ) ( 1816 -904 200 ) ( 1816 -904 184 ) map/nebula_rt 0 272 0 -0.015625 0.734375 0 0 0
@@ -361,7 +343,7 @@
( 1816 -1376 200 ) ( 1816 -904 200 ) ( 2336 -904 200 ) map/nebula_rt 0 42 0 -0.015625 0.921875 0 0 0
( 2336 -904 -176 ) ( 1816 -904 -176 ) ( 1816 -1376 -176 ) map/nebula_rt 0 42 0 -0.015625 0.921875 0 0 0
}
-// brush 40
+// brush 38
{
( 1816 -912 200 ) ( 2336 -912 200 ) ( 1816 -912 184 ) map/nebula_ft 251 272 0 -1.015625 0.734375 0 0 0
( 1816 -904 200 ) ( 1816 -1376 200 ) ( 1816 -1376 184 ) map/nebula_ft 0 272 0 -0.015625 0.734375 0 0 0
@@ -370,7 +352,7 @@
( 1816 -1376 200 ) ( 1816 -904 200 ) ( 2336 -904 200 ) map/nebula_ft 251 0 0 -1.015625 0.015625 0 0 0
( 2336 -904 -176 ) ( 1816 -904 -176 ) ( 1816 -1376 -176 ) map/nebula_ft 251 0 0 -1.015625 0.015625 0 0 0
}
-// brush 41
+// brush 39
{
( 1824 -1376 200 ) ( 1824 -904 200 ) ( 1824 -1376 184 ) map/nebula_lf 42 272 0 -0.921875 0.734375 0 0 0
( 1816 -904 200 ) ( 1816 -1376 200 ) ( 1816 -1376 184 ) map/nebula_lf 42 272 0 -0.921875 0.734375 0 0 0
@@ -379,7 +361,7 @@
( 1816 -1376 200 ) ( 1816 -904 200 ) ( 2336 -904 200 ) map/nebula_lf 0 42 0 -0.015625 0.921875 0 0 0
( 2336 -904 -176 ) ( 1816 -904 -176 ) ( 1816 -1376 -176 ) map/nebula_lf 0 42 0 -0.015625 0.921875 0 0 0
}
-// brush 42
+// brush 40
{
( -1048 992 1432 ) ( -1064 992 1432 ) ( -1064 984 1432 ) common/caulk 0 0 0 0.031200 0.031200 0 0 0
( -1064 984 1448 ) ( -1064 992 1448 ) ( -1048 992 1448 ) common/caulk 0 0 0 0.031200 0.031200 0 0 0
@@ -388,16 +370,7 @@
( -1048 992 1448 ) ( -1064 992 1448 ) ( -1064 992 1432 ) common/caulk 0 0 0 0.031200 0.031200 0 0 0
( -1064 992 1448 ) ( -1064 984 1448 ) ( -1064 984 1432 ) common/caulk 0 0 0 0.031200 0.031200 0 0 0
}
-// brush 43
-{
-( -1448 1392 984 ) ( -1448 -400 984 ) ( -1448 -400 968 ) map/nebula 248 0 0 -5.593750 0.015625 0 0 0
-( 552 1392 984 ) ( -1448 1392 984 ) ( -1448 1392 968 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
-( 1600 -1472 984 ) ( 1600 320 984 ) ( 1600 320 968 ) map/nebula 248 0 0 -5.593750 0.015625 0 0 0
-( -400 -1472 984 ) ( 1600 -1472 984 ) ( 1600 -1472 968 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
-( -928 -944 1544 ) ( -928 848 1544 ) ( 1072 848 1544 ) map/nebula 268 248 0 -5.953125 5.593750 0 0 0
-( -928 848 1536 ) ( -928 -944 1536 ) ( 1072 848 1536 ) map/nebula 268 248 0 -5.953125 5.593750 0 0 0
-}
-// brush 44
+// brush 41
{
( 320 256 88 ) ( 320 -256 88 ) ( 320 -256 56 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( 336 256 80 ) ( 320 256 80 ) ( 320 256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
@@ -406,7 +379,7 @@
( 320 -256 88 ) ( 320 256 88 ) ( 336 256 88 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( 336 256 48 ) ( 320 256 48 ) ( 320 -256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
}
-// brush 45
+// brush 42
{
( -272 272 64 ) ( -272 256 64 ) ( -272 256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( 336 272 64 ) ( -272 272 64 ) ( -272 272 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
@@ -415,7 +388,7 @@
( -272 256 88 ) ( -272 272 88 ) ( 336 272 88 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( 336 272 48 ) ( -272 272 48 ) ( -272 256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
}
-// brush 46
+// brush 43
{
( -272 -256 64 ) ( -272 -272 64 ) ( -272 -272 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( 336 -256 64 ) ( -272 -256 64 ) ( -272 -256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
@@ -424,7 +397,7 @@
( -272 -272 88 ) ( -272 -256 88 ) ( 336 -256 88 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( 336 -256 48 ) ( -272 -256 48 ) ( -272 -272 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
}
-// brush 47
+// brush 44
{
( -272 256 64 ) ( -272 -256 64 ) ( -272 -256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( -256 256 64 ) ( -272 256 64 ) ( -272 256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
@@ -433,7 +406,7 @@
( -272 -256 88 ) ( -272 256 88 ) ( -256 256 88 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
( -256 256 48 ) ( -272 256 48 ) ( -272 -256 48 ) map/green_glow 0 0 0 0.031200 0.031200 0 0 0
}
-// brush 48
+// brush 45
{
( -1440 1384 -120 ) ( -1440 -1440 -120 ) ( -1440 -1440 -128 ) map/nodrop 0 0 0 0.031200 0.031200 0 0 0
( 1576 1384 -120 ) ( -1440 1384 -120 ) ( -1440 1384 -128 ) map/nodrop 0 0 0 0.031200 0.031200 0 0 0
@@ -442,6 +415,33 @@
( -1424 -1456 -80 ) ( -1424 1368 -80 ) ( 1592 1368 -80 ) map/nodrop 0 0 0 0.031200 0.031200 0 0 0
( 1592 1368 -1536 ) ( -1424 1368 -1536 ) ( -1424 -1456 -1536 ) map/nodrop 0 0 0 0.031200 0.031200 0 0 0
}
+// brush 46
+{
+( -1448 1392 128 ) ( -1448 -400 128 ) ( -1448 -400 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
+( 1600 -1472 128 ) ( 1600 320 128 ) ( 1600 320 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
+( -400 -1472 128 ) ( 1600 -1472 128 ) ( 1600 -1472 112 ) map/nebula 268 256 0 -5.953125 6.031250 0 0 0
+( -928 -944 1544 ) ( -928 848 1544 ) ( 1072 848 1544 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
+( 1072 848 -1544 ) ( -928 848 -1544 ) ( -928 -944 -1544 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
+( 1600 -1464 128 ) ( -400 -1464 128 ) ( 1600 -1464 112 ) map/nebula 268 256 0 -5.953125 6.031250 0 0 0
+}
+// brush 47
+{
+( -928 848 1536 ) ( -928 -944 1536 ) ( 1072 848 1536 ) map/nebula 268 248 0 -5.953125 5.593750 0 0 0
+( -928 -944 1544 ) ( -928 848 1544 ) ( 1072 848 1544 ) map/nebula 268 248 0 -5.953125 5.593750 0 0 0
+( -400 -1472 984 ) ( 1600 -1472 984 ) ( 1600 -1472 968 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
+( 1600 -1472 984 ) ( 1600 320 984 ) ( 1600 320 968 ) map/nebula 248 0 0 -5.593750 0.015625 0 0 0
+( 552 1392 984 ) ( -1448 1392 984 ) ( -1448 1392 968 ) map/nebula 268 0 0 -5.953125 0.015625 0 0 0
+( -1448 1392 984 ) ( -1448 -400 984 ) ( -1448 -400 968 ) map/nebula 248 0 0 -5.593750 0.015625 0 0 0
+}
+// brush 48
+{
+( 1592 320 -480 ) ( 1592 -1472 -480 ) ( 1592 320 -496 ) map/nebula 248 256 0 -5.593750 6.031250 0 0 0
+( 1072 848 -1544 ) ( -928 848 -1544 ) ( -928 -944 -1544 ) map/nebula 0 248 0 -0.015625 5.593750 0 0 0
+( -928 -944 1544 ) ( -928 848 1544 ) ( 1072 848 1544 ) map/nebula 0 248 0 -0.015625 5.593750 0 0 0
+( -400 -1472 128 ) ( 1600 -1472 128 ) ( 1600 -1472 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
+( 1600 -1472 128 ) ( 1600 320 128 ) ( 1600 320 112 ) map/nebula 248 256 0 -5.593750 6.031250 0 0 0
+( 552 1392 128 ) ( -1448 1392 128 ) ( -1448 1392 112 ) map/nebula 0 256 0 -0.015625 6.031250 0 0 0
+}
}
// entity 1
{
@@ -471,12 +471,6 @@
}
// entity 4
{
-"classname" "target_position"
-"origin" "-192 -520 368"
-"targetname" "landing2"
-}
-// entity 5
-{
"target" "landing21"
"classname" "trigger_push"
// brush 0
@@ -489,13 +483,13 @@
( 216 -176 120 ) ( 216 -288 120 ) ( 216 -288 64 ) common/caulk -20 0 0 0.031200 0.031200 0 0 0
}
}
-// entity 6
+// entity 5
{
"targetname" "landing21"
-"origin" "336 -280 632"
+"origin" "432 -376 672"
"classname" "target_position"
}
-// entity 7
+// entity 6
{
"target" "landing3"
"classname" "trigger_push"
@@ -509,13 +503,7 @@
( -216 -728 416 ) ( -216 -840 416 ) ( -216 -840 360 ) common/caulk -56 15 0 0.031200 0.031200 0 0 0
}
}
-// entity 8
-{
-"targetname" "landing3"
-"origin" "208 -760 632"
-"classname" "target_position"
-}
-// entity 9
+// entity 7
{
"target" "landing5"
"classname" "trigger_push"
@@ -529,13 +517,13 @@
( -320 -480 360 ) ( -496 -480 360 ) ( -496 -592 360 ) common/caulk -14 -59 0 0.031200 0.031200 0 0 0
}
}
-// entity 10
+// entity 8
{
"classname" "target_position"
"origin" "-280 -320 576"
"targetname" "landing5"
}
-// entity 11
+// entity 9
{
"target" "landing7"
"classname" "trigger_push"
@@ -549,13 +537,13 @@
( 520 672 488 ) ( 520 560 488 ) ( 520 560 432 ) common/caulk 0 -45 0 0.031200 0.031200 0 0 0
}
}
-// entity 12
+// entity 10
{
"targetname" "landing7"
-"origin" "416 640 624"
+"origin" "368 640 576"
"classname" "target_position"
}
-// entity 13
+// entity 11
{
"classname" "trigger_push"
"target" "landing8"
@@ -569,13 +557,13 @@
( -464 528 712 ) ( -640 528 712 ) ( -640 416 712 ) common/caulk 56 -6 0 0.031200 0.031200 0 0 0
}
}
-// entity 14
+// entity 12
{
"classname" "target_position"
"origin" "-560 368 784"
"targetname" "landing8"
}
-// entity 15
+// entity 13
{
"target" "landing10"
"classname" "trigger_push"
@@ -589,13 +577,7 @@
( 752 -368 600 ) ( 752 -480 600 ) ( 752 -480 544 ) common/caulk 52 24 0 0.031200 0.031200 0 0 0
}
}
-// entity 16
-{
-"targetname" "landing10"
-"origin" "784 -232 640"
-"classname" "target_position"
-}
-// entity 17
+// entity 14
{
"classname" "trigger_push"
"target" "landing14"
@@ -609,13 +591,13 @@
( 320 160 96 ) ( 144 160 96 ) ( 144 48 96 ) common/caulk 17 36 0 0.031200 0.031200 0 0 0
}
}
-// entity 18
+// entity 15
{
"classname" "target_position"
-"origin" "480 128 456"
+"origin" "584 128 544"
"targetname" "landing14"
}
-// entity 19
+// entity 16
{
"target" "landing15"
"classname" "trigger_push"
@@ -629,13 +611,7 @@
( -240 56 128 ) ( -240 -56 128 ) ( -240 -56 72 ) common/caulk 33 0 0 0.031200 0.031200 0 0 0
}
}
-// entity 20
-{
-"targetname" "landing15"
-"origin" "-320 24 312"
-"classname" "target_position"
-}
-// entity 21
+// entity 17
{
"classname" "trigger_push"
"target" "landing13"
@@ -649,13 +625,13 @@
( 184 608 504 ) ( 8 608 504 ) ( 8 496 504 ) common/caulk -41 60 0 0.031200 0.031200 0 0 0
}
}
-// entity 22
+// entity 18
{
"classname" "target_position"
-"origin" "-104 576 704"
+"origin" "-152 576 784"
"targetname" "landing13"
}
-// entity 23
+// entity 19
{
"classname" "trigger_push"
"target" "landing12"
@@ -669,13 +645,7 @@
( -544 136 288 ) ( -720 136 288 ) ( -720 24 288 ) common/caulk 61 35 0 0.031200 0.031200 0 0 0
}
}
-// entity 24
-{
-"classname" "target_position"
-"origin" "-672 416 792"
-"targetname" "landing12"
-}
-// entity 25
+// entity 20
{
"target" "landing22"
"classname" "trigger_push"
@@ -689,38 +659,32 @@
( -576 -160 352 ) ( -576 -272 352 ) ( -576 -272 296 ) common/caulk -22 -51 0 0.031200 0.031200 0 0 0
}
}
-// entity 26
-{
-"classname" "target_position"
-"origin" "-544 -352 424"
-"targetname" "landing22"
-}
-// entity 27
+// entity 21
{
"classname" "info_player_start"
"origin" "712 -608 544"
}
-// entity 28
+// entity 22
{
"origin" "712 240 432"
"classname" "info_player_start"
}
-// entity 29
+// entity 23
{
"classname" "info_player_start"
"origin" "136 448 512"
}
-// entity 30
+// entity 24
{
"origin" "-432 688 712"
"classname" "info_player_start"
}
-// entity 31
+// entity 25
{
"classname" "info_player_start"
"origin" "-552 -48 296"
}
-// entity 32
+// entity 26
{
"target" "landing16"
"classname" "trigger_push"
@@ -734,145 +698,145 @@
( 576 168 480 ) ( 576 280 480 ) ( 576 280 424 ) common/caulk 39 -46 -180 0.031219 -0.031204 0 0 0
}
}
-// entity 33
+// entity 27
{
"targetname" "landing16"
"origin" "408 200 536"
"classname" "target_position"
}
-// entity 34
+// entity 28
{
"classname" "light"
"origin" "88 0 1152"
"light" "5000"
}
-// entity 35
+// entity 29
{
"origin" "192 -24 88"
"classname" "weapon_lightning"
}
-// entity 36
+// entity 30
{
"classname" "misc_model"
"origin" "544 200 432"
"model" "models/bounce_pad.md3"
"angle" "270"
}
-// entity 37
+// entity 31
{
"angle" "270"
"model" "models/bounce_pad.md3"
"origin" "552 640 432"
"classname" "misc_model"
}
-// entity 38
+// entity 32
{
"angle" "270"
"model" "models/bounce_pad.md3"
"origin" "88 576 512"
"classname" "misc_model"
}
-// entity 39
+// entity 33
{
"angle" "180"
"model" "models/bounce_pad.md3"
"origin" "784 -400 544"
"classname" "misc_model"
}
-// entity 40
+// entity 34
{
"angle" "45"
"model" "models/bounce_pad.md3"
"origin" "248 -200 88"
"classname" "misc_model"
}
-// entity 41
+// entity 35
{
"angle" "360"
"model" "models/bounce_pad.md3"
"origin" "-192 -192 88"
"classname" "misc_model"
}
-// entity 42
+// entity 36
{
"angle" "90"
"model" "models/bounce_pad.md3"
"origin" "224 128 88"
"classname" "misc_model"
}
-// entity 43
+// entity 37
{
"angle" "270"
"model" "models/bounce_pad.md3"
"origin" "-208 24 88"
"classname" "misc_model"
}
-// entity 44
+// entity 38
{
"angle" "360"
"model" "models/bounce_pad.md3"
"origin" "-544 -192 296"
"classname" "misc_model"
}
-// entity 45
+// entity 39
{
"angle" "360"
"model" "models/bounce_pad.md3"
"origin" "-560 496 712"
"classname" "misc_model"
}
-// entity 46
+// entity 40
{
"angle" "90"
"model" "models/bounce_pad.md3"
"origin" "-184 -760 360"
"classname" "misc_model"
}
-// entity 47
+// entity 41
{
"angle" "135"
"model" "models/bounce_pad.md3"
"origin" "-416 -512 360"
"classname" "misc_model"
}
-// entity 48
+// entity 42
{
"angle" "180"
"model" "models/bounce_pad.md3"
"origin" "-640 104 296"
"classname" "misc_model"
}
-// entity 49
+// entity 43
{
"classname" "weapon_lightning"
"origin" "720 152 424"
}
-// entity 50
+// entity 44
{
"origin" "144 528 504"
"classname" "weapon_lightning"
}
-// entity 51
+// entity 45
{
"classname" "weapon_lightning"
"origin" "-592 648 704"
}
-// entity 52
+// entity 46
{
"origin" "-552 8 288"
"classname" "weapon_lightning"
}
-// entity 53
+// entity 47
{
"classname" "weapon_lightning"
"origin" "-416 -624 352"
}
-// entity 54
+// entity 48
{
"origin" "936 -736 536"
"classname" "weapon_lightning"
}
-// entity 55
+// entity 49
{
"classname" "trigger_push"
"target" "landing20"
@@ -886,81 +850,81 @@
( 480 -240 424 ) ( 656 -240 424 ) ( 656 -128 424 ) common/caulk 3 37 -180 0.031189 0.031219 0 0 0
}
}
-// entity 56
+// entity 50
{
"classname" "target_position"
-"origin" "576 -256 536"
+"origin" "576 -384 632"
"targetname" "landing20"
}
-// entity 57
+// entity 51
{
"angle" "360"
"model" "models/bounce_pad.md3"
"origin" "576 -208 432"
"classname" "misc_model"
}
-// entity 58
+// entity 52
{
"origin" "2056 -1136 24"
"classname" "_skybox"
}
-// entity 59
+// entity 53
{
"_color" "0.3 1 0.3"
"light" "2400"
"origin" "48 56 248"
"classname" "light"
}
-// entity 60
+// entity 54
{
"_color" "0 0 1"
"light" "800"
"origin" "128 568 608"
"classname" "light"
}
-// entity 61
+// entity 55
{
"classname" "light"
"origin" "720 440 576"
"light" "800"
"_color" "0.5 0 1"
}
-// entity 62
+// entity 56
{
"_color" "0.5 0 1"
"light" "800"
"origin" "704 0 576"
"classname" "light"
}
-// entity 63
+// entity 57
{
"classname" "light"
"origin" "720 -600 776"
"light" "800"
"_color" "1 0.3 0"
}
-// entity 64
+// entity 58
{
"_color" "0 1 1"
"light" "800"
"origin" "-400 -640 488"
"classname" "light"
}
-// entity 65
+// entity 59
{
"classname" "light"
"origin" "-544 -8 408"
"light" "800"
"_color" "1 0 0"
}
-// entity 66
+// entity 60
{
"classname" "light"
"origin" "-488 656 904"
"light" "800"
"_color" "1 1 0.4"
}
-// entity 67
+// entity 61
{
"speed" "55"
"targetname" "ufo"
@@ -975,7 +939,7 @@
( -80 1216 1520 ) ( -80 1016 1520 ) ( -80 1016 1480 ) map/ghost 0 0 0 0.031200 0.031200 0 0 0
}
}
-// entity 68
+// entity 62
{
"speed" "75"
"_remap" "*;textures/model/mothership_d"
@@ -985,7 +949,7 @@
"origin" "360 1136 1432"
"classname" "misc_model"
}
-// entity 69
+// entity 63
{
"classname" "misc_model"
"origin" "80 -520 1136"
@@ -994,7 +958,7 @@
"target" "ufo2"
"_remap" "*;textures/model/mothership_d"
}
-// entity 70
+// entity 64
{
"targetname" "ufo3"
"classname" "func_rotating"
@@ -1009,7 +973,7 @@
( -880 -272 -512 ) ( -880 -472 -512 ) ( -880 -472 -552 ) map/ghost -52 -104 0 0.031200 0.031200 0 0 0
}
}
-// entity 71
+// entity 65
{
"target" "ufo3"
"_remap" "*;textures/model/mothership_d"
@@ -1018,7 +982,7 @@
"origin" "-440 -352 -600"
"classname" "misc_model"
}
-// entity 72
+// entity 66
{
"target" "ufo4"
"speed" "45"
@@ -1034,7 +998,7 @@
( 328 456 -1272 ) ( 144 456 -1272 ) ( 144 256 -1272 ) map/ghost 117 -39 0 0.031200 0.031200 0 0 0
}
}
-// entity 73
+// entity 67
{
"origin" "584 376 -1192"
"targetname" "ufo4"
@@ -1043,12 +1007,12 @@
"modelscale" "3"
"_remap" "*;textures/model/mothership_d"
}
-// entity 74
+// entity 68
{
"origin" "104 112 136"
"classname" "misc_model"
}
-// entity 75
+// entity 69
{
"classname" "func_rotating"
"targetname" "ufo2"
@@ -1063,48 +1027,43 @@
( 464 -416 1072 ) ( 280 -416 1072 ) ( 280 -616 1072 ) map/ghost 109 -83 0 0.031200 0.031200 0 0 0
}
}
-// entity 76
+// entity 70
{
"classname" "info_player_start"
"origin" "-488 -672 364"
}
-// entity 77
+// entity 71
{
"origin" "-224 -624 364"
"classname" "info_player_start"
}
-// entity 78
-{
-"origin" "984 -480 544"
-"classname" "info_player_start"
-}
-// entity 79
+// entity 72
{
"classname" "info_player_start"
"origin" "840 632 432"
}
-// entity 80
+// entity 73
{
"classname" "info_player_start"
"origin" "-800 640 712"
}
-// entity 81
+// entity 74
{
"origin" "-648 -200 296"
"classname" "info_player_start"
}
-// entity 82
+// entity 75
{
"classname" "info_player_start"
"origin" "0 -216 88"
"angle" "90"
}
-// entity 83
+// entity 76
{
"origin" "280 576 512"
"classname" "info_player_start"
}
-// entity 84
+// entity 77
{
"wait" "0.00001"
"classname" "trigger_multiple"
@@ -1119,3 +1078,44 @@
( -1440 1576 -120 ) ( -1440 -216 -120 ) ( -1440 -216 -136 ) map/poltergeist 0 0 0 0.031200 0.031200 0 0 0
}
}
+// entity 78
+{
+"origin" "984 -480 544"
+"classname" "info_player_start"
+}
+// entity 79
+{
+"targetname" "landing15"
+"origin" "-448 24 336"
+"classname" "target_position"
+}
+// entity 80
+{
+"targetname" "landing22"
+"origin" "-544 -424 464"
+"classname" "target_position"
+}
+// entity 81
+{
+"targetname" "landing2"
+"origin" "-192 -480 424"
+"classname" "target_position"
+}
+// entity 82
+{
+"classname" "target_position"
+"origin" "-640 544 840"
+"targetname" "landing12"
+}
+// entity 83
+{
+"classname" "target_position"
+"origin" "320 -760 680"
+"targetname" "landing3"
+}
+// entity 84
+{
+"classname" "target_position"
+"origin" "784 -280 648"
+"targetname" "landing10"
+}
diff --git a/assets/maps/probabilistic_fruit_01.map b/assets/maps/probabilistic_fruit_01.map
new file mode 100644
index 00000000..361eea4a
--- /dev/null
+++ b/assets/maps/probabilistic_fruit_01.map
@@ -0,0 +1,852 @@
+// entity 0
+{
+"classname" "worldspawn"
+// brush 0
+{
+( 344 384 192 ) ( -384 384 192 ) ( -384 -144 192 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.062500 0 0 0
+( -384 -144 200 ) ( -384 384 200 ) ( 344 384 200 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.062500 0 0 0
+( -408 -256 320 ) ( 320 -256 320 ) ( 320 -256 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.062500 0 0 0
+( 128 -176 320 ) ( 128 352 320 ) ( 128 352 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.062500 0 0 0
+( 328 320 320 ) ( -400 320 320 ) ( -400 320 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.062500 0 0 0
+( -320 376 320 ) ( -320 -152 320 ) ( -320 -152 0 ) map/lab_games/sky/lg_sky_01 0 0 0 0.062500 0.062500 0 0 0
+}
+// brush 1
+{
+( -320 -88 0 ) ( -336 -88 0 ) ( -336 -384 0 ) map/lab_games/lg_style_01_wall_green 0 568 0 -0.005208 0.187500 0 0 0
+( -336 -384 192 ) ( -336 -88 192 ) ( -320 -88 192 ) map/lab_games/lg_style_01_wall_green 0 568 0 -0.005208 0.187500 0 0 0
+( -328 -256 200 ) ( -312 -256 200 ) ( -312 -256 192 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.005208 0.187500 0 0 0
+( -320 -368 200 ) ( -320 -72 200 ) ( -320 -72 192 ) map/lab_games/lg_style_01_wall_green 1706 0 0 -0.187500 0.187500 0 0 0
+( -320 320 200 ) ( -336 320 200 ) ( -336 320 192 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.005208 0.187500 0 0 0
+( -336 -104 200 ) ( -336 -400 200 ) ( -336 -400 192 ) map/lab_games/lg_style_01_wall_green 1706 0 0 -0.187500 0.187500 0 0 0
+}
+// brush 2
+{
+( 128 -88 200 ) ( 128 -384 200 ) ( 128 -384 192 ) map/lab_games/lg_style_01_wall_green 1706 0 0 -0.187500 0.187500 0 0 0
+( 144 320 200 ) ( 128 320 200 ) ( 128 320 192 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.005208 0.187500 0 0 0
+( 144 -448 200 ) ( 144 -152 200 ) ( 144 -152 192 ) map/lab_games/lg_style_01_wall_green 1706 0 0 -0.187500 0.187500 0 0 0
+( 144 -256 200 ) ( 160 -256 200 ) ( 160 -256 192 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.005208 0.187500 0 0 0
+( 128 -384 192 ) ( 128 -88 192 ) ( 144 -88 192 ) map/lab_games/lg_style_01_wall_green 0 568 0 -0.005208 0.187500 0 0 0
+( 144 -88 0 ) ( 128 -88 0 ) ( 128 -384 0 ) map/lab_games/lg_style_01_wall_green 0 568 0 -0.005208 0.187500 0 0 0
+}
+// brush 3
+{
+( 256 336 0 ) ( -320 336 0 ) ( -320 320 0 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( -320 320 192 ) ( -320 336 192 ) ( 256 336 192 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( -448 320 192 ) ( 128 320 192 ) ( 128 320 0 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( 128 320 192 ) ( 128 336 192 ) ( 128 336 0 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.007812 0.187500 0 0 0
+( 256 336 192 ) ( -320 336 192 ) ( -320 336 0 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( -320 336 192 ) ( -320 320 192 ) ( -320 320 0 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.007812 0.187500 0 0 0
+}
+// brush 4
+{
+( 256 -256 0 ) ( -320 -256 0 ) ( -320 -272 0 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( -320 -272 192 ) ( -320 -256 192 ) ( 256 -256 192 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( -320 -272 192 ) ( 256 -272 192 ) ( 256 -272 0 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( 128 -272 184 ) ( 128 -256 184 ) ( 128 -256 -8 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.007812 0.187500 0 0 0
+( 256 -256 192 ) ( -320 -256 192 ) ( -320 -256 0 ) map/lab_games/lg_style_01_wall_green 585 0 0 -0.218750 0.187500 0 0 0
+( -320 -256 184 ) ( -320 -272 184 ) ( -320 -272 -8 ) map/lab_games/lg_style_01_wall_green 0 0 0 -0.007812 0.187500 0 0 0
+}
+// brush 5
+{
+( -256 320 -24 ) ( -320 320 -24 ) ( -320 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -320 -256 0 ) ( -320 320 0 ) ( -256 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -320 -256 0 ) ( -256 -256 0 ) ( -256 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -256 -16 ) ( -256 320 -16 ) ( -256 320 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 320 0 ) ( -320 320 0 ) ( -320 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -320 320 0 ) ( -320 -256 0 ) ( -320 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 6
+{
+( -192 320 -16 ) ( -192 -256 -16 ) ( -192 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 320 0 ) ( -192 320 0 ) ( -192 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -256 -16 ) ( -128 320 -16 ) ( -128 320 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -256 0 ) ( -128 -256 0 ) ( -128 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -256 0 ) ( -192 320 0 ) ( -128 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 320 -24 ) ( -192 320 -24 ) ( -192 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 7
+{
+( 0 320 -24 ) ( -64 320 -24 ) ( -64 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -256 0 ) ( -64 320 0 ) ( 0 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -256 0 ) ( 0 -256 0 ) ( 0 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -256 -16 ) ( 0 320 -16 ) ( 0 320 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 320 0 ) ( -64 320 0 ) ( -64 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 320 -16 ) ( -64 -256 -16 ) ( -64 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 8
+{
+( 64 320 -16 ) ( 64 -256 -16 ) ( 64 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 128 320 0 ) ( 64 320 0 ) ( 64 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 128 -256 0 ) ( 128 320 0 ) ( 128 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -256 0 ) ( 128 -256 0 ) ( 128 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -256 0 ) ( 64 320 0 ) ( 128 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 128 320 -24 ) ( 64 320 -24 ) ( 64 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 9
+{
+( -256 320 -16 ) ( -256 -256 -16 ) ( -256 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 320 0 ) ( -256 320 0 ) ( -256 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -256 -16 ) ( -192 320 -16 ) ( -192 320 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -216 256 0 ) ( -152 256 0 ) ( -152 256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -256 0 ) ( -256 320 0 ) ( -192 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 320 -24 ) ( -256 320 -24 ) ( -256 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 10
+{
+( -192 192 -24 ) ( -256 192 -24 ) ( -256 -384 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -384 0 ) ( -256 192 0 ) ( -192 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -216 128 0 ) ( -152 128 0 ) ( -152 128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -384 -16 ) ( -192 192 -16 ) ( -192 192 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 192 0 ) ( -256 192 0 ) ( -256 192 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 192 -16 ) ( -256 -384 -16 ) ( -256 -384 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 11
+{
+( -256 64 -16 ) ( -256 -512 -16 ) ( -256 -512 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 64 0 ) ( -256 64 0 ) ( -256 64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -512 -16 ) ( -192 64 -16 ) ( -192 64 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -216 0 0 ) ( -152 0 0 ) ( -152 0 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -512 0 ) ( -256 64 0 ) ( -192 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 64 -24 ) ( -256 64 -24 ) ( -256 -512 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 12
+{
+( -192 -64 -24 ) ( -256 -64 -24 ) ( -256 -640 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -640 0 ) ( -256 -64 0 ) ( -192 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -216 -128 0 ) ( -152 -128 0 ) ( -152 -128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -640 -16 ) ( -192 -64 -16 ) ( -192 -64 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -64 0 ) ( -256 -64 0 ) ( -256 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -64 -16 ) ( -256 -640 -16 ) ( -256 -640 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 13
+{
+( -256 -192 -16 ) ( -256 -768 -16 ) ( -256 -768 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -192 0 ) ( -256 -192 0 ) ( -256 -192 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -768 -16 ) ( -192 -192 -16 ) ( -192 -192 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -216 -256 0 ) ( -152 -256 0 ) ( -152 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -256 -768 0 ) ( -256 -192 0 ) ( -192 -192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -192 -192 -24 ) ( -256 -192 -24 ) ( -256 -768 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 14
+{
+( -64 320 -24 ) ( -128 320 -24 ) ( -128 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -256 0 ) ( -128 320 0 ) ( -64 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -88 256 0 ) ( -24 256 0 ) ( -24 256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -256 -16 ) ( -64 320 -16 ) ( -64 320 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 320 0 ) ( -128 320 0 ) ( -128 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 320 -16 ) ( -128 -256 -16 ) ( -128 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 15
+{
+( -128 192 -16 ) ( -128 -384 -16 ) ( -128 -384 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 192 0 ) ( -128 192 0 ) ( -128 192 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -384 -16 ) ( -64 192 -16 ) ( -64 192 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -88 128 0 ) ( -24 128 0 ) ( -24 128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -384 0 ) ( -128 192 0 ) ( -64 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 192 -24 ) ( -128 192 -24 ) ( -128 -384 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 16
+{
+( -64 64 -24 ) ( -128 64 -24 ) ( -128 -512 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -512 0 ) ( -128 64 0 ) ( -64 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -88 0 0 ) ( -24 0 0 ) ( -24 0 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -512 -16 ) ( -64 64 -16 ) ( -64 64 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 64 0 ) ( -128 64 0 ) ( -128 64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 64 -16 ) ( -128 -512 -16 ) ( -128 -512 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 17
+{
+( -128 -64 -16 ) ( -128 -640 -16 ) ( -128 -640 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -64 0 ) ( -128 -64 0 ) ( -128 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -640 -16 ) ( -64 -64 -16 ) ( -64 -64 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -88 -128 0 ) ( -24 -128 0 ) ( -24 -128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -640 0 ) ( -128 -64 0 ) ( -64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -64 -24 ) ( -128 -64 -24 ) ( -128 -640 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 18
+{
+( -64 -192 -24 ) ( -128 -192 -24 ) ( -128 -768 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -768 0 ) ( -128 -192 0 ) ( -64 -192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -88 -256 0 ) ( -24 -256 0 ) ( -24 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -768 -16 ) ( -64 -192 -16 ) ( -64 -192 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -64 -192 0 ) ( -128 -192 0 ) ( -128 -192 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( -128 -192 -16 ) ( -128 -768 -16 ) ( -128 -768 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 19
+{
+( 0 320 -16 ) ( 0 -256 -16 ) ( 0 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 320 0 ) ( 0 320 0 ) ( 0 320 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -256 -16 ) ( 64 320 -16 ) ( 64 320 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 40 256 0 ) ( 104 256 0 ) ( 104 256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -256 0 ) ( 0 320 0 ) ( 64 320 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 320 -24 ) ( 0 320 -24 ) ( 0 -256 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 20
+{
+( 64 192 -24 ) ( 0 192 -24 ) ( 0 -384 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -384 0 ) ( 0 192 0 ) ( 64 192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 40 128 0 ) ( 104 128 0 ) ( 104 128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -384 -16 ) ( 64 192 -16 ) ( 64 192 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 192 0 ) ( 0 192 0 ) ( 0 192 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 192 -16 ) ( 0 -384 -16 ) ( 0 -384 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 21
+{
+( 0 64 -16 ) ( 0 -512 -16 ) ( 0 -512 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 64 0 ) ( 0 64 0 ) ( 0 64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -512 -16 ) ( 64 64 -16 ) ( 64 64 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 40 0 0 ) ( 104 0 0 ) ( 104 0 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -512 0 ) ( 0 64 0 ) ( 64 64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 64 -24 ) ( 0 64 -24 ) ( 0 -512 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 22
+{
+( 64 -64 -24 ) ( 0 -64 -24 ) ( 0 -640 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -640 0 ) ( 0 -64 0 ) ( 64 -64 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 40 -128 0 ) ( 104 -128 0 ) ( 104 -128 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -640 -16 ) ( 64 -64 -16 ) ( 64 -64 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -64 0 ) ( 0 -64 0 ) ( 0 -64 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -64 -16 ) ( 0 -640 -16 ) ( 0 -640 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 23
+{
+( 0 -192 -16 ) ( 0 -768 -16 ) ( 0 -768 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -192 0 ) ( 0 -192 0 ) ( 0 -192 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -768 -16 ) ( 64 -192 -16 ) ( 64 -192 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 40 -256 0 ) ( 104 -256 0 ) ( 104 -256 -8 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 0 -768 0 ) ( 0 -192 0 ) ( 64 -192 0 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+( 64 -192 -24 ) ( 0 -192 -24 ) ( 0 -768 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.190000 0 0 0
+}
+// brush 24
+{
+( -336 336 -16 ) ( -336 -272 -16 ) ( -336 -272 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.187500 0 0 0
+( 136 336 -16 ) ( -336 336 -16 ) ( -336 336 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.187500 0 0 0
+( 144 -272 -16 ) ( 144 336 -16 ) ( 144 336 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.187500 0 0 0
+( -336 -272 -16 ) ( 136 -272 -16 ) ( 136 -272 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.187500 0 0 0
+( -336 -272 -16 ) ( -336 336 -16 ) ( 136 336 -16 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.187500 0 0 0
+( 136 336 -24 ) ( -336 336 -24 ) ( -336 -272 -24 ) map/lab_games/lg_style_01_floor_orange 0 0 0 0.190000 0.187500 0 0 0
+}
+// brush 25
+{
+( 144 -256 -24 ) ( 128 -256 -24 ) ( 128 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -272 200 ) ( 128 -256 200 ) ( 144 -256 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -272 -16 ) ( 144 -272 -16 ) ( 144 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 144 -272 -16 ) ( 144 -256 -16 ) ( 144 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 144 -256 -16 ) ( 128 -256 -16 ) ( 128 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -256 -16 ) ( 128 -272 -16 ) ( 128 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 26
+{
+( -320 -256 -24 ) ( -336 -256 -24 ) ( -336 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 -272 200 ) ( -336 -256 200 ) ( -320 -256 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 -272 200 ) ( -320 -272 200 ) ( -320 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 -272 200 ) ( -320 -256 200 ) ( -320 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 -256 200 ) ( -336 -256 200 ) ( -336 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 -256 200 ) ( -336 -272 200 ) ( -336 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 27
+{
+( -320 336 -24 ) ( -336 336 -24 ) ( -336 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 320 200 ) ( -336 336 200 ) ( -320 336 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 320 200 ) ( -320 320 200 ) ( -320 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 320 200 ) ( -320 336 200 ) ( -320 336 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 336 200 ) ( -336 336 200 ) ( -336 336 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 336 200 ) ( -336 320 200 ) ( -336 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 28
+{
+( 144 336 -24 ) ( 128 336 -24 ) ( 128 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 320 200 ) ( 128 336 200 ) ( 144 336 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 320 200 ) ( 144 320 200 ) ( 144 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 144 320 200 ) ( 144 336 200 ) ( 144 336 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 144 336 200 ) ( 128 336 200 ) ( 128 336 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 336 200 ) ( 128 320 200 ) ( 128 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 29
+{
+( 128 336 192 ) ( -320 336 192 ) ( -320 320 192 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 320 200 ) ( -320 336 200 ) ( 128 336 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 320 200 ) ( 128 320 200 ) ( 128 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 320 200 ) ( 128 336 200 ) ( 128 336 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 336 200 ) ( -320 336 200 ) ( -320 336 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 336 200 ) ( -320 320 200 ) ( -320 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 30
+{
+( 144 320 192 ) ( 128 320 192 ) ( 128 -256 192 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -256 200 ) ( 128 320 200 ) ( 144 320 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -256 200 ) ( 144 -256 200 ) ( 144 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 144 -256 200 ) ( 144 320 200 ) ( 144 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 144 320 200 ) ( 128 320 200 ) ( 128 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 320 200 ) ( 128 -256 200 ) ( 128 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 31
+{
+( 128 -256 192 ) ( -320 -256 192 ) ( -320 -272 192 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 -272 200 ) ( -320 -256 200 ) ( 128 -256 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 -272 200 ) ( 128 -272 200 ) ( 128 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -272 200 ) ( 128 -256 200 ) ( 128 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( 128 -256 200 ) ( -320 -256 200 ) ( -320 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 -256 200 ) ( -320 -272 200 ) ( -320 -272 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 32
+{
+( -328 320 192 ) ( -336 320 192 ) ( -336 -256 192 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 -256 200 ) ( -336 320 200 ) ( -328 320 200 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 -256 200 ) ( -328 -256 200 ) ( -328 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -320 -256 200 ) ( -320 320 200 ) ( -320 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -328 320 200 ) ( -336 320 200 ) ( -336 320 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+( -336 320 200 ) ( -336 -256 200 ) ( -336 -256 -24 ) common/caulk 0 0 0 0.500000 0.500000 0 0 0
+}
+// brush 33
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_dn 756 485 0 -0.507812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_dn 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_dn 485 0 0 -0.460938 0.007812 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_dn 756 0 0 -0.507812 0.007812 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_dn 485 0 0 -0.460938 0.007812 0 0 0
+( 904 224 -168 ) ( 1424 224 -168 ) ( 904 -248 -168 ) map/lab_games/sky/lg_sky_02_dn 756 485 0 -0.507812 0.460938 0 0 0
+}
+// brush 34
+{
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_up 756 485 0 -0.507812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_up 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_up 485 0 0 -0.460938 0.007812 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_up 756 0 0 -0.507812 0.007812 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_up 485 0 0 -0.460938 0.007812 0 0 0
+( 904 224 192 ) ( 904 -248 192 ) ( 1424 224 192 ) map/lab_games/sky/lg_sky_02_up 756 485 0 -0.507812 0.460938 0 0 0
+}
+// brush 35
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_bk 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_bk 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_bk 756 544 0 -0.507812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_bk 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 -240 200 ) ( 904 -240 200 ) ( 1424 -240 184 ) map/lab_games/sky/lg_sky_02_bk 756 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 36
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_rt 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_rt 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_rt 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_rt 485 544 0 -0.460938 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_rt 0 544 0 -0.007812 0.367188 0 0 0
+( 1416 224 200 ) ( 1416 -248 200 ) ( 1416 224 184 ) map/lab_games/sky/lg_sky_02_rt 485 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 37
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_ft 756 0 0 -0.507812 0.007812 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_ft 756 0 0 -0.507812 0.007812 0 0 0
+( 1424 -248 200 ) ( 1424 224 200 ) ( 1424 224 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_ft 756 544 0 -0.507812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_ft 0 544 0 -0.007812 0.367188 0 0 0
+( 904 216 200 ) ( 1424 216 200 ) ( 904 216 184 ) map/lab_games/sky/lg_sky_02_ft 756 544 0 -0.507812 0.367188 0 0 0
+}
+// brush 38
+{
+( 1424 224 -176 ) ( 904 224 -176 ) ( 904 -248 -176 ) map/lab_games/sky/lg_sky_02_lf 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 904 224 200 ) ( 1424 224 200 ) map/lab_games/sky/lg_sky_02_lf 0 485 0 -0.007812 0.460938 0 0 0
+( 904 -248 200 ) ( 1424 -248 200 ) ( 1424 -248 184 ) map/lab_games/sky/lg_sky_02_lf 0 544 0 -0.007812 0.367188 0 0 0
+( 1424 224 200 ) ( 904 224 200 ) ( 904 224 184 ) map/lab_games/sky/lg_sky_02_lf 0 544 0 -0.007812 0.367188 0 0 0
+( 904 224 200 ) ( 904 -248 200 ) ( 904 -248 184 ) map/lab_games/sky/lg_sky_02_lf 485 544 0 -0.460938 0.367188 0 0 0
+( 912 -248 200 ) ( 912 224 200 ) ( 912 -248 184 ) map/lab_games/sky/lg_sky_02_lf 485 544 0 -0.460938 0.367188 0 0 0
+}
+// brush 39
+{
+( -128 -128 0 ) ( -128 -192 0 ) ( -128 -192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 -128 0 ) ( -128 -128 0 ) ( -128 -128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 -192 0 ) ( -64 -128 0 ) ( -64 -128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -128 -192 0 ) ( -64 -192 0 ) ( -64 -192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -128 -192 0 ) ( -128 -128 0 ) ( -64 -128 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -64 -128 -4 ) ( -128 -128 -4 ) ( -128 -192 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+}
+// brush 40
+{
+( -192 -128 -4 ) ( -256 -128 -4 ) ( -256 -192 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -256 -192 0 ) ( -256 -128 0 ) ( -192 -128 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -256 -192 0 ) ( -192 -192 0 ) ( -192 -192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 -192 0 ) ( -192 -128 0 ) ( -192 -128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 -128 0 ) ( -256 -128 0 ) ( -256 -128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -256 -128 0 ) ( -256 -192 0 ) ( -256 -192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+}
+// brush 41
+{
+( 0 -128 0 ) ( 0 -192 0 ) ( 0 -192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 -128 0 ) ( 0 -128 0 ) ( 0 -128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 -192 0 ) ( 64 -128 0 ) ( 64 -128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 0 -192 0 ) ( 64 -192 0 ) ( 64 -192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 0 -192 0 ) ( 0 -128 0 ) ( 64 -128 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( 64 -128 -4 ) ( 0 -128 -4 ) ( 0 -192 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+}
+// brush 42
+{
+( -64 0 -4 ) ( -128 0 -4 ) ( -128 -64 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -128 -64 0 ) ( -128 0 0 ) ( -64 0 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -128 -64 0 ) ( -64 -64 0 ) ( -64 -64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 -64 0 ) ( -64 0 0 ) ( -64 0 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 0 0 ) ( -128 0 0 ) ( -128 0 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -128 0 0 ) ( -128 -64 0 ) ( -128 -64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+}
+// brush 43
+{
+( -256 0 0 ) ( -256 -64 0 ) ( -256 -64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 0 0 ) ( -256 0 0 ) ( -256 0 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 -64 0 ) ( -192 0 0 ) ( -192 0 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -256 -64 0 ) ( -192 -64 0 ) ( -192 -64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -256 -64 0 ) ( -256 0 0 ) ( -192 0 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -192 0 -4 ) ( -256 0 -4 ) ( -256 -64 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+}
+// brush 44
+{
+( 64 0 -4 ) ( 0 0 -4 ) ( 0 -64 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( 0 -64 0 ) ( 0 0 0 ) ( 64 0 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( 0 -64 0 ) ( 64 -64 0 ) ( 64 -64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 -64 0 ) ( 64 0 0 ) ( 64 0 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 0 0 ) ( 0 0 0 ) ( 0 0 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 0 0 0 ) ( 0 -64 0 ) ( 0 -64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+}
+// brush 45
+{
+( -128 128 0 ) ( -128 64 0 ) ( -128 64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 128 0 ) ( -128 128 0 ) ( -128 128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 64 0 ) ( -64 128 0 ) ( -64 128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -128 64 0 ) ( -64 64 0 ) ( -64 64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -128 64 0 ) ( -128 128 0 ) ( -64 128 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -64 128 -4 ) ( -128 128 -4 ) ( -128 64 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+}
+// brush 46
+{
+( -192 128 -4 ) ( -256 128 -4 ) ( -256 64 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -256 64 0 ) ( -256 128 0 ) ( -192 128 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -256 64 0 ) ( -192 64 0 ) ( -192 64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 64 0 ) ( -192 128 0 ) ( -192 128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 128 0 ) ( -256 128 0 ) ( -256 128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -256 128 0 ) ( -256 64 0 ) ( -256 64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+}
+// brush 47
+{
+( 0 128 0 ) ( 0 64 0 ) ( 0 64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 128 0 ) ( 0 128 0 ) ( 0 128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 64 0 ) ( 64 128 0 ) ( 64 128 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 0 64 0 ) ( 64 64 0 ) ( 64 64 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 0 64 0 ) ( 0 128 0 ) ( 64 128 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( 64 128 -4 ) ( 0 128 -4 ) ( 0 64 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+}
+// brush 48
+{
+( -64 256 -4 ) ( -128 256 -4 ) ( -128 192 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -128 192 0 ) ( -128 256 0 ) ( -64 256 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -128 192 0 ) ( -64 192 0 ) ( -64 192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 192 0 ) ( -64 256 0 ) ( -64 256 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -64 256 0 ) ( -128 256 0 ) ( -128 256 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -128 256 0 ) ( -128 192 0 ) ( -128 192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+}
+// brush 49
+{
+( -256 256 0 ) ( -256 192 0 ) ( -256 192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 256 0 ) ( -256 256 0 ) ( -256 256 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -192 192 0 ) ( -192 256 0 ) ( -192 256 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -256 192 0 ) ( -192 192 0 ) ( -192 192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( -256 192 0 ) ( -256 256 0 ) ( -192 256 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( -192 256 -4 ) ( -256 256 -4 ) ( -256 192 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+}
+// brush 50
+{
+( 64 256 -4 ) ( 0 256 -4 ) ( 0 192 -4 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( 0 192 0 ) ( 0 256 0 ) ( 64 256 0 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.062500 0 0 0
+( 0 192 0 ) ( 64 192 0 ) ( 64 192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 192 0 ) ( 64 256 0 ) ( 64 256 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 64 256 0 ) ( 0 256 0 ) ( 0 256 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+( 0 256 0 ) ( 0 192 0 ) ( 0 192 -16 ) map/ctp_tech_lava_d 0 0 0 -0.062500 0.003906 0 0 0
+}
+}
+// entity 1
+{
+"angle" "90"
+"origin" "-96 -224 24"
+"classname" "info_player_start"
+}
+// entity 2
+{
+"origin" "-296 224 24"
+"classname" "apple_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 3
+{
+"origin" "-160 96 24"
+"classname" "lemon_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 4
+{
+"origin" "-24 224 24"
+"classname" "fungi_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 5
+{
+"origin" "96 96 24"
+"classname" "strawberry_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 6
+{
+"classname" "strawberry_reward"
+"origin" "-160 -32 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 7
+{
+"origin" "-288 -160 24"
+"classname" "strawberry_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 8
+{
+"classname" "strawberry_reward"
+"origin" "-288 96 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 9
+{
+"classname" "apple_reward"
+"origin" "-160 -160 24"
+}
+// entity 10
+{
+"origin" "96 -32 24"
+"classname" "apple_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 11
+{
+"classname" "fungi_reward"
+"origin" "-288 -32 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 12
+{
+"origin" "96 224 24"
+"classname" "strawberry_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 13
+{
+"classname" "strawberry_reward"
+"origin" "96 -160 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 14
+{
+"classname" "apple_reward"
+"origin" "-32 96 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 15
+{
+"classname" "lemon_reward"
+"origin" "-32 -32 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 16
+{
+"classname" "lemon_reward"
+"origin" "-160 224 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 17
+{
+"classname" "lemon_reward"
+"origin" "32 -96 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 18
+{
+"classname" "lemon_reward"
+"origin" "-232 -96 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 19
+{
+"classname" "strawberry_reward"
+"origin" "-104 -96 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 20
+{
+"origin" "-224 160 24"
+"classname" "apple_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 21
+{
+"origin" "32 160 24"
+"classname" "fungi_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 22
+{
+"classname" "strawberry_reward"
+"origin" "-224 32 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 23
+{
+"origin" "32 32 24"
+"classname" "strawberry_reward"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 24
+{
+"classname" "apple_reward"
+"origin" "-96 32 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 25
+{
+"classname" "apple_reward"
+"origin" "-96 160 24"
+"random_items" "apple_reward,lemon_reward,strawberry_reward,fungi_reward"
+}
+// entity 26
+{
+"classname" "target_kill"
+"origin" "-294 304 154"
+"targetname" "kill"
+}
+// entity 27
+{
+"classname" "mango_goal"
+"origin" "-100 288 24"
+}
+// entity 28
+{
+"classname" "light"
+"origin" "-96 192 176"
+"light" "600"
+}
+// entity 29
+{
+"light" "600"
+"origin" "-96 -128 176"
+"classname" "light"
+}
+// entity 30
+{
+"classname" "_skybox"
+"origin" "1136 -16 -128"
+}
+// entity 31
+{
+"model" "models/stadium.md3"
+"origin" "1136 -16 -82"
+"classname" "misc_model"
+"_remap" "*;textures/map/lab_games/stadium_d"
+"angle" "90"
+}
+// entity 32
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "1"
+"model" "models/signal_line.md3"
+"origin" "1160 -56 -80"
+"classname" "misc_model"
+"angle" "113"
+}
+// entity 33
+{
+"classname" "misc_model"
+"origin" "1208 -32 -152"
+"model" "models/signal_line.md3"
+"modelscale" "1.1"
+"_remap" "*;textures/map/lab_games/signal_pulse_blue"
+"angle" "38"
+}
+// entity 34
+{
+"angle" "28"
+"classname" "misc_model"
+"origin" "1168 -24 -88"
+"model" "models/signal_line.md3"
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.61"
+}
+// entity 35
+{
+"_remap" "*;textures/map/lab_games/signal_pulse_red"
+"modelscale" "0.6"
+"model" "models/signal_line.md3"
+"origin" "1112 -8 -152"
+"classname" "misc_model"
+"angle" "159"
+}
+// entity 36
+{
+"target" "kill"
+"classname" "trigger_multiple"
+// brush 0
+{
+( -128 -136 8 ) ( -128 -192 8 ) ( -128 -192 0 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -64 -128 -8 ) ( -120 -128 -8 ) ( -120 -128 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -64 -192 -8 ) ( -64 -136 -8 ) ( -64 -136 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -120 -192 -8 ) ( -64 -192 -8 ) ( -64 -192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -120 -192 16 ) ( -120 -136 16 ) ( -64 -136 16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -63 -136 -17 ) ( -119 -136 -17 ) ( -119 -192 -17 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 37
+{
+"target" "kill"
+"classname" "trigger_multiple"
+// brush 0
+{
+( 0 -136 15 ) ( 0 -192 15 ) ( 0 -192 7 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 64 -128 -8 ) ( 8 -128 -8 ) ( 8 -128 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 64 -192 -8 ) ( 64 -136 -8 ) ( 64 -136 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 8 -192 -8 ) ( 64 -192 -8 ) ( 64 -192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 8 -192 16 ) ( 8 -136 16 ) ( 64 -136 16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 65 -136 -1 ) ( 9 -136 -1 ) ( 9 -192 -1 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 38
+{
+"target" "kill"
+"classname" "trigger_multiple"
+// brush 0
+{
+( -256 -136 -8 ) ( -256 -192 -8 ) ( -256 -192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -192 -128 -8 ) ( -248 -128 -8 ) ( -248 -128 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -192 -192 15 ) ( -192 -136 15 ) ( -192 -136 7 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -248 -192 -8 ) ( -192 -192 -8 ) ( -192 -192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -248 -192 16 ) ( -248 -136 16 ) ( -192 -136 16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -192 -136 -1 ) ( -248 -136 -1 ) ( -248 -192 -1 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 39
+{
+"classname" "trigger_multiple"
+"target" "kill"
+// brush 0
+{
+( -63 -8 -17 ) ( -119 -8 -17 ) ( -119 -64 -17 ) common/caulk 0 33 0 0.190000 0.190000 0 0 0
+( -120 -64 16 ) ( -120 -8 16 ) ( -64 -8 16 ) common/caulk 0 33 0 0.190000 0.190000 0 0 0
+( -120 -64 -8 ) ( -64 -64 -8 ) ( -64 -64 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -64 -64 -8 ) ( -64 -8 -8 ) ( -64 -8 -16 ) common/caulk -33 0 0 0.190000 0.190000 0 0 0
+( -64 0 -8 ) ( -120 0 -8 ) ( -120 0 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -128 -8 8 ) ( -128 -64 8 ) ( -128 -64 0 ) common/caulk -33 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 40
+{
+"classname" "trigger_multiple"
+"target" "kill"
+// brush 0
+{
+( 65 -8 -1 ) ( 9 -8 -1 ) ( 9 -64 -1 ) common/caulk 0 33 0 0.190000 0.190000 0 0 0
+( 8 -64 16 ) ( 8 -8 16 ) ( 64 -8 16 ) common/caulk 0 33 0 0.190000 0.190000 0 0 0
+( 8 -64 -8 ) ( 64 -64 -8 ) ( 64 -64 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 64 -64 -8 ) ( 64 -8 -8 ) ( 64 -8 -16 ) common/caulk -33 0 0 0.190000 0.190000 0 0 0
+( 64 0 -8 ) ( 8 0 -8 ) ( 8 0 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 0 -8 15 ) ( 0 -64 15 ) ( 0 -64 7 ) common/caulk -33 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 41
+{
+"classname" "trigger_multiple"
+"target" "kill"
+// brush 0
+{
+( -192 -8 -1 ) ( -248 -8 -1 ) ( -248 -64 -1 ) common/caulk 0 33 0 0.190000 0.190000 0 0 0
+( -248 -64 16 ) ( -248 -8 16 ) ( -192 -8 16 ) common/caulk 0 33 0 0.190000 0.190000 0 0 0
+( -248 -64 -8 ) ( -192 -64 -8 ) ( -192 -64 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -192 -64 15 ) ( -192 -8 15 ) ( -192 -8 7 ) common/caulk -33 0 0 0.190000 0.190000 0 0 0
+( -192 0 -8 ) ( -248 0 -8 ) ( -248 0 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -256 -8 -8 ) ( -256 -64 -8 ) ( -256 -64 -16 ) common/caulk -33 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 42
+{
+"target" "kill"
+"classname" "trigger_multiple"
+// brush 0
+{
+( -128 120 8 ) ( -128 64 8 ) ( -128 64 0 ) common/caulk -2 0 0 0.190000 0.190000 0 0 0
+( -64 128 -8 ) ( -120 128 -8 ) ( -120 128 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -64 64 -8 ) ( -64 120 -8 ) ( -64 120 -16 ) common/caulk -2 0 0 0.190000 0.190000 0 0 0
+( -120 64 -8 ) ( -64 64 -8 ) ( -64 64 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -120 64 16 ) ( -120 120 16 ) ( -64 120 16 ) common/caulk 0 2 0 0.190000 0.190000 0 0 0
+( -63 120 -17 ) ( -119 120 -17 ) ( -119 64 -17 ) common/caulk 0 2 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 43
+{
+"target" "kill"
+"classname" "trigger_multiple"
+// brush 0
+{
+( 0 120 15 ) ( 0 64 15 ) ( 0 64 7 ) common/caulk -2 0 0 0.190000 0.190000 0 0 0
+( 64 128 -8 ) ( 8 128 -8 ) ( 8 128 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 64 64 -8 ) ( 64 120 -8 ) ( 64 120 -16 ) common/caulk -2 0 0 0.190000 0.190000 0 0 0
+( 8 64 -8 ) ( 64 64 -8 ) ( 64 64 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 8 64 16 ) ( 8 120 16 ) ( 64 120 16 ) common/caulk 0 2 0 0.190000 0.190000 0 0 0
+( 65 120 -1 ) ( 9 120 -1 ) ( 9 64 -1 ) common/caulk 0 2 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 44
+{
+"target" "kill"
+"classname" "trigger_multiple"
+// brush 0
+{
+( -256 120 -8 ) ( -256 64 -8 ) ( -256 64 -16 ) common/caulk -2 0 0 0.190000 0.190000 0 0 0
+( -192 128 -8 ) ( -248 128 -8 ) ( -248 128 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -192 64 15 ) ( -192 120 15 ) ( -192 120 7 ) common/caulk -2 0 0 0.190000 0.190000 0 0 0
+( -248 64 -8 ) ( -192 64 -8 ) ( -192 64 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -248 64 16 ) ( -248 120 16 ) ( -192 120 16 ) common/caulk 0 2 0 0.190000 0.190000 0 0 0
+( -192 120 -1 ) ( -248 120 -1 ) ( -248 64 -1 ) common/caulk 0 2 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 45
+{
+"classname" "trigger_multiple"
+"target" "kill"
+// brush 0
+{
+( -63 248 -17 ) ( -119 248 -17 ) ( -119 192 -17 ) common/caulk 0 35 0 0.190000 0.190000 0 0 0
+( -120 192 16 ) ( -120 248 16 ) ( -64 248 16 ) common/caulk 0 35 0 0.190000 0.190000 0 0 0
+( -120 192 -8 ) ( -64 192 -8 ) ( -64 192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -64 192 -8 ) ( -64 248 -8 ) ( -64 248 -16 ) common/caulk -35 0 0 0.190000 0.190000 0 0 0
+( -64 256 -8 ) ( -120 256 -8 ) ( -120 256 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -128 248 8 ) ( -128 192 8 ) ( -128 192 0 ) common/caulk -35 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 46
+{
+"classname" "trigger_multiple"
+"target" "kill"
+// brush 0
+{
+( 65 248 -1 ) ( 9 248 -1 ) ( 9 192 -1 ) common/caulk 0 35 0 0.190000 0.190000 0 0 0
+( 8 192 16 ) ( 8 248 16 ) ( 64 248 16 ) common/caulk 0 35 0 0.190000 0.190000 0 0 0
+( 8 192 -8 ) ( 64 192 -8 ) ( 64 192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 64 192 -8 ) ( 64 248 -8 ) ( 64 248 -16 ) common/caulk -35 0 0 0.190000 0.190000 0 0 0
+( 64 256 -8 ) ( 8 256 -8 ) ( 8 256 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( 0 248 15 ) ( 0 192 15 ) ( 0 192 7 ) common/caulk -35 0 0 0.190000 0.190000 0 0 0
+}
+}
+// entity 47
+{
+"classname" "trigger_multiple"
+"target" "kill"
+// brush 0
+{
+( -192 248 -1 ) ( -248 248 -1 ) ( -248 192 -1 ) common/caulk 0 35 0 0.190000 0.190000 0 0 0
+( -248 192 16 ) ( -248 248 16 ) ( -192 248 16 ) common/caulk 0 35 0 0.190000 0.190000 0 0 0
+( -248 192 -8 ) ( -192 192 -8 ) ( -192 192 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -192 192 15 ) ( -192 248 15 ) ( -192 248 7 ) common/caulk -35 0 0 0.190000 0.190000 0 0 0
+( -192 256 -8 ) ( -248 256 -8 ) ( -248 256 -16 ) common/caulk 0 0 0 0.190000 0.190000 0 0 0
+( -256 248 -8 ) ( -256 192 -8 ) ( -256 192 -16 ) common/caulk -35 0 0 0.190000 0.190000 0 0 0
+}
+}
diff --git a/assets/models/bush_desert_01.md3 b/assets/models/bush_desert_01.md3
new file mode 100644
index 00000000..cd38e1ad
Binary files /dev/null and b/assets/models/bush_desert_01.md3 differ
diff --git a/assets/models/cactus_desert_01.md3 b/assets/models/cactus_desert_01.md3
new file mode 100644
index 00000000..bbfd54ce
Binary files /dev/null and b/assets/models/cactus_desert_01.md3 differ
diff --git a/assets/models/flags/b_flag.md3 b/assets/models/flags/b_flag.md3
new file mode 100644
index 00000000..82324a30
Binary files /dev/null and b/assets/models/flags/b_flag.md3 differ
diff --git a/assets/models/flags/b_flag.tga b/assets/models/flags/b_flag.tga
new file mode 100644
index 00000000..f8054130
Binary files /dev/null and b/assets/models/flags/b_flag.tga differ
diff --git a/assets/models/flags/n_flag.md3 b/assets/models/flags/n_flag.md3
new file mode 100644
index 00000000..87d7a55d
Binary files /dev/null and b/assets/models/flags/n_flag.md3 differ
diff --git a/assets/models/flags/n_flag.tga b/assets/models/flags/n_flag.tga
new file mode 100644
index 00000000..5c566fa9
Binary files /dev/null and b/assets/models/flags/n_flag.tga differ
diff --git a/assets/models/flags/r_flag.md3 b/assets/models/flags/r_flag.md3
new file mode 100644
index 00000000..1c644bb3
Binary files /dev/null and b/assets/models/flags/r_flag.md3 differ
diff --git a/assets/models/flags/r_flag.tga b/assets/models/flags/r_flag.tga
new file mode 100644
index 00000000..b0c9a3e7
Binary files /dev/null and b/assets/models/flags/r_flag.tga differ
diff --git a/assets/models/fut_teleport.md3 b/assets/models/fut_teleport.md3
deleted file mode 100644
index 6068c35d..00000000
Binary files a/assets/models/fut_teleport.md3 and /dev/null differ
diff --git a/assets/models/hr_apple2.md3 b/assets/models/hr_apple2.md3
new file mode 100644
index 00000000..68f313ee
Binary files /dev/null and b/assets/models/hr_apple2.md3 differ
diff --git a/assets/models/hr_ball.md3 b/assets/models/hr_ball.md3
new file mode 100644
index 00000000..5844368f
Binary files /dev/null and b/assets/models/hr_ball.md3 differ
diff --git a/assets/models/hr_balloon.md3 b/assets/models/hr_balloon.md3
new file mode 100644
index 00000000..53761ec2
Binary files /dev/null and b/assets/models/hr_balloon.md3 differ
diff --git a/assets/models/hr_banana.md3 b/assets/models/hr_banana.md3
new file mode 100644
index 00000000..e994b152
Binary files /dev/null and b/assets/models/hr_banana.md3 differ
diff --git a/assets/models/hr_bee.md3 b/assets/models/hr_bee.md3
new file mode 100644
index 00000000..b310d0b1
Binary files /dev/null and b/assets/models/hr_bee.md3 differ
diff --git a/assets/models/hr_bottle.md3 b/assets/models/hr_bottle.md3
new file mode 100644
index 00000000..059be1df
Binary files /dev/null and b/assets/models/hr_bottle.md3 differ
diff --git a/assets/models/hr_cake.md3 b/assets/models/hr_cake.md3
new file mode 100644
index 00000000..fd04bc65
Binary files /dev/null and b/assets/models/hr_cake.md3 differ
diff --git a/assets/models/hr_can.md3 b/assets/models/hr_can.md3
new file mode 100644
index 00000000..7e6ee30d
Binary files /dev/null and b/assets/models/hr_can.md3 differ
diff --git a/assets/models/hr_car.md3 b/assets/models/hr_car.md3
new file mode 100644
index 00000000..138b9526
Binary files /dev/null and b/assets/models/hr_car.md3 differ
diff --git a/assets/models/hr_cassette.md3 b/assets/models/hr_cassette.md3
new file mode 100644
index 00000000..f10ca115
Binary files /dev/null and b/assets/models/hr_cassette.md3 differ
diff --git a/assets/models/hr_chair.md3 b/assets/models/hr_chair.md3
new file mode 100644
index 00000000..5ec746a3
Binary files /dev/null and b/assets/models/hr_chair.md3 differ
diff --git a/assets/models/hr_cherries.md3 b/assets/models/hr_cherries.md3
new file mode 100644
index 00000000..8aa83625
Binary files /dev/null and b/assets/models/hr_cherries.md3 differ
diff --git a/assets/models/hr_cow.md3 b/assets/models/hr_cow.md3
new file mode 100644
index 00000000..364c8b9e
Binary files /dev/null and b/assets/models/hr_cow.md3 differ
diff --git a/assets/models/hr_flower.md3 b/assets/models/hr_flower.md3
new file mode 100644
index 00000000..c14d8aa9
Binary files /dev/null and b/assets/models/hr_flower.md3 differ
diff --git a/assets/models/hr_fork.md3 b/assets/models/hr_fork.md3
new file mode 100644
index 00000000..aaa54385
Binary files /dev/null and b/assets/models/hr_fork.md3 differ
diff --git a/assets/models/hr_fridge.md3 b/assets/models/hr_fridge.md3
new file mode 100644
index 00000000..7e079d1c
Binary files /dev/null and b/assets/models/hr_fridge.md3 differ
diff --git a/assets/models/hr_guitar.md3 b/assets/models/hr_guitar.md3
new file mode 100644
index 00000000..2fb10e65
Binary files /dev/null and b/assets/models/hr_guitar.md3 differ
diff --git a/assets/models/hr_hair_brush.md3 b/assets/models/hr_hair_brush.md3
new file mode 100644
index 00000000..a18513dd
Binary files /dev/null and b/assets/models/hr_hair_brush.md3 differ
diff --git a/assets/models/hr_hammer.md3 b/assets/models/hr_hammer.md3
new file mode 100644
index 00000000..291d128c
Binary files /dev/null and b/assets/models/hr_hammer.md3 differ
diff --git a/assets/models/hr_hat.md3 b/assets/models/hr_hat.md3
new file mode 100644
index 00000000..083d12ba
Binary files /dev/null and b/assets/models/hr_hat.md3 differ
diff --git a/assets/models/hr_ice_lolly.md3 b/assets/models/hr_ice_lolly.md3
new file mode 100644
index 00000000..0921b718
Binary files /dev/null and b/assets/models/hr_ice_lolly.md3 differ
diff --git a/assets/models/hr_ice_lolly_lrg.md3 b/assets/models/hr_ice_lolly_lrg.md3
new file mode 100644
index 00000000..7aa8a07e
Binary files /dev/null and b/assets/models/hr_ice_lolly_lrg.md3 differ
diff --git a/assets/models/hr_jug.md3 b/assets/models/hr_jug.md3
new file mode 100644
index 00000000..82b3bf55
Binary files /dev/null and b/assets/models/hr_jug.md3 differ
diff --git a/assets/models/hr_key.md3 b/assets/models/hr_key.md3
new file mode 100644
index 00000000..5a7380b0
Binary files /dev/null and b/assets/models/hr_key.md3 differ
diff --git a/assets/models/hr_key_lrg.md3 b/assets/models/hr_key_lrg.md3
new file mode 100644
index 00000000..970ca212
Binary files /dev/null and b/assets/models/hr_key_lrg.md3 differ
diff --git a/assets/models/hr_knife.md3 b/assets/models/hr_knife.md3
new file mode 100644
index 00000000..e137299b
Binary files /dev/null and b/assets/models/hr_knife.md3 differ
diff --git a/assets/models/hr_ladder.md3 b/assets/models/hr_ladder.md3
new file mode 100644
index 00000000..00e95850
Binary files /dev/null and b/assets/models/hr_ladder.md3 differ
diff --git a/assets/models/hr_mug.md3 b/assets/models/hr_mug.md3
new file mode 100644
index 00000000..4c5d5f32
Binary files /dev/null and b/assets/models/hr_mug.md3 differ
diff --git a/assets/models/hr_pencil.md3 b/assets/models/hr_pencil.md3
new file mode 100644
index 00000000..28eea9e7
Binary files /dev/null and b/assets/models/hr_pencil.md3 differ
diff --git a/assets/models/hr_pig.md3 b/assets/models/hr_pig.md3
new file mode 100644
index 00000000..b75b2512
Binary files /dev/null and b/assets/models/hr_pig.md3 differ
diff --git a/assets/models/hr_pincer.md3 b/assets/models/hr_pincer.md3
new file mode 100644
index 00000000..f44c85cc
Binary files /dev/null and b/assets/models/hr_pincer.md3 differ
diff --git a/assets/models/hr_plant.md3 b/assets/models/hr_plant.md3
new file mode 100644
index 00000000..5c720ee4
Binary files /dev/null and b/assets/models/hr_plant.md3 differ
diff --git a/assets/models/hr_saxophone.md3 b/assets/models/hr_saxophone.md3
new file mode 100644
index 00000000..891eb66b
Binary files /dev/null and b/assets/models/hr_saxophone.md3 differ
diff --git a/assets/models/hr_shoe.md3 b/assets/models/hr_shoe.md3
new file mode 100644
index 00000000..b22001fc
Binary files /dev/null and b/assets/models/hr_shoe.md3 differ
diff --git a/assets/models/hr_spoon.md3 b/assets/models/hr_spoon.md3
new file mode 100644
index 00000000..f9e4c2b8
Binary files /dev/null and b/assets/models/hr_spoon.md3 differ
diff --git a/assets/models/hr_suitcase.md3 b/assets/models/hr_suitcase.md3
new file mode 100644
index 00000000..b2659bfc
Binary files /dev/null and b/assets/models/hr_suitcase.md3 differ
diff --git a/assets/models/hr_tennis_racket.md3 b/assets/models/hr_tennis_racket.md3
new file mode 100644
index 00000000..c98bfe5c
Binary files /dev/null and b/assets/models/hr_tennis_racket.md3 differ
diff --git a/assets/models/hr_tomato.md3 b/assets/models/hr_tomato.md3
new file mode 100644
index 00000000..bf935722
Binary files /dev/null and b/assets/models/hr_tomato.md3 differ
diff --git a/assets/models/hr_toothbrush.md3 b/assets/models/hr_toothbrush.md3
new file mode 100644
index 00000000..0ae99158
Binary files /dev/null and b/assets/models/hr_toothbrush.md3 differ
diff --git a/assets/models/hr_tree.md3 b/assets/models/hr_tree.md3
new file mode 100644
index 00000000..5d6a0631
Binary files /dev/null and b/assets/models/hr_tree.md3 differ
diff --git a/assets/models/hr_tv.md3 b/assets/models/hr_tv.md3
new file mode 100644
index 00000000..ec73887b
Binary files /dev/null and b/assets/models/hr_tv.md3 differ
diff --git a/assets/models/hr_wine_glass.md3 b/assets/models/hr_wine_glass.md3
new file mode 100644
index 00000000..e31ee23b
Binary files /dev/null and b/assets/models/hr_wine_glass.md3 differ
diff --git a/assets/models/hr_zebra.md3 b/assets/models/hr_zebra.md3
new file mode 100644
index 00000000..1f0891a8
Binary files /dev/null and b/assets/models/hr_zebra.md3 differ
diff --git a/assets/models/mushroom_desert_01a.md3 b/assets/models/mushroom_desert_01a.md3
new file mode 100644
index 00000000..b3aa91cb
Binary files /dev/null and b/assets/models/mushroom_desert_01a.md3 differ
diff --git a/assets/models/players/crash_color/dm_character_skin_mask_a.tga b/assets/models/players/crash_color/dm_character_skin_mask_a.tga
deleted file mode 100644
index 1d484ac9..00000000
Binary files a/assets/models/players/crash_color/dm_character_skin_mask_a.tga and /dev/null differ
diff --git a/assets/models/players/crash_color/dm_character_skin_mask_c.tga b/assets/models/players/crash_color/dm_character_skin_mask_c.tga
deleted file mode 100644
index 316483e6..00000000
Binary files a/assets/models/players/crash_color/dm_character_skin_mask_c.tga and /dev/null differ
diff --git a/assets/models/players/crash_color/head_skin2.skin b/assets/models/players/crash_color/head_skin2.skin
new file mode 100644
index 00000000..7636979a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin2.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base2.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin2_blue.skin b/assets/models/players/crash_color/head_skin2_blue.skin
new file mode 100644
index 00000000..7636979a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin2_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base2.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin2_red.skin b/assets/models/players/crash_color/head_skin2_red.skin
new file mode 100644
index 00000000..7636979a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin2_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base2.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin3.skin b/assets/models/players/crash_color/head_skin3.skin
new file mode 100644
index 00000000..eab9c3fd
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin3.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base3.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin3_blue.skin b/assets/models/players/crash_color/head_skin3_blue.skin
new file mode 100644
index 00000000..eab9c3fd
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin3_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base3.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin3_red.skin b/assets/models/players/crash_color/head_skin3_red.skin
new file mode 100644
index 00000000..eab9c3fd
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin3_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base3.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin4.skin b/assets/models/players/crash_color/head_skin4.skin
new file mode 100644
index 00000000..e46f4110
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin4.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base4.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin4_blue.skin b/assets/models/players/crash_color/head_skin4_blue.skin
new file mode 100644
index 00000000..e46f4110
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin4_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base4.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin4_red.skin b/assets/models/players/crash_color/head_skin4_red.skin
new file mode 100644
index 00000000..e46f4110
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin4_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base4.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin5.skin b/assets/models/players/crash_color/head_skin5.skin
new file mode 100644
index 00000000..75dffdef
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin5.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base5.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin5_blue.skin b/assets/models/players/crash_color/head_skin5_blue.skin
new file mode 100644
index 00000000..75dffdef
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin5_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base5.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin5_red.skin b/assets/models/players/crash_color/head_skin5_red.skin
new file mode 100644
index 00000000..75dffdef
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin5_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base5.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin6.skin b/assets/models/players/crash_color/head_skin6.skin
new file mode 100644
index 00000000..2fc69a3a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin6.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base6.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin6_blue.skin b/assets/models/players/crash_color/head_skin6_blue.skin
new file mode 100644
index 00000000..2fc69a3a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin6_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base6.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin6_red.skin b/assets/models/players/crash_color/head_skin6_red.skin
new file mode 100644
index 00000000..2fc69a3a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin6_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base6.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin7.skin b/assets/models/players/crash_color/head_skin7.skin
new file mode 100644
index 00000000..940a551a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin7.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base7.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin7_blue.skin b/assets/models/players/crash_color/head_skin7_blue.skin
new file mode 100644
index 00000000..940a551a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin7_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base7.tga
+tag_head
diff --git a/assets/models/players/crash_color/head_skin7_red.skin b/assets/models/players/crash_color/head_skin7_red.skin
new file mode 100644
index 00000000..940a551a
--- /dev/null
+++ b/assets/models/players/crash_color/head_skin7_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base7.tga
+tag_head
diff --git a/assets/models/players/crash_color/icon_skin1.tga b/assets/models/players/crash_color/icon_skin1.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin1.tga differ
diff --git a/assets/models/players/crash_color/icon_skin2.tga b/assets/models/players/crash_color/icon_skin2.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin2.tga differ
diff --git a/assets/models/players/crash_color/icon_skin3.tga b/assets/models/players/crash_color/icon_skin3.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin3.tga differ
diff --git a/assets/models/players/crash_color/icon_skin4.tga b/assets/models/players/crash_color/icon_skin4.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin4.tga differ
diff --git a/assets/models/players/crash_color/icon_skin5.tga b/assets/models/players/crash_color/icon_skin5.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin5.tga differ
diff --git a/assets/models/players/crash_color/icon_skin6.tga b/assets/models/players/crash_color/icon_skin6.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin6.tga differ
diff --git a/assets/models/players/crash_color/icon_skin7.tga b/assets/models/players/crash_color/icon_skin7.tga
new file mode 100644
index 00000000..4f900194
Binary files /dev/null and b/assets/models/players/crash_color/icon_skin7.tga differ
diff --git a/assets/models/players/crash_color/lower_skin1.skin b/assets/models/players/crash_color/lower_skin1.skin
new file mode 100644
index 00000000..504f7a48
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin1.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base1.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin1_blue.skin b/assets/models/players/crash_color/lower_skin1_blue.skin
new file mode 100644
index 00000000..504f7a48
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin1_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base1.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin1_red.skin b/assets/models/players/crash_color/lower_skin1_red.skin
new file mode 100644
index 00000000..504f7a48
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin1_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base1.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin2.skin b/assets/models/players/crash_color/lower_skin2.skin
new file mode 100644
index 00000000..76b82ea4
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin2.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base2.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin2_blue.skin b/assets/models/players/crash_color/lower_skin2_blue.skin
new file mode 100644
index 00000000..76b82ea4
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin2_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base2.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin2_red.skin b/assets/models/players/crash_color/lower_skin2_red.skin
new file mode 100644
index 00000000..76b82ea4
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin2_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base2.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin3.skin b/assets/models/players/crash_color/lower_skin3.skin
new file mode 100644
index 00000000..e22bd830
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin3.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base3.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin3_blue.skin b/assets/models/players/crash_color/lower_skin3_blue.skin
new file mode 100644
index 00000000..e22bd830
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin3_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base3.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin3_red.skin b/assets/models/players/crash_color/lower_skin3_red.skin
new file mode 100644
index 00000000..e22bd830
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin3_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base3.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin4.skin b/assets/models/players/crash_color/lower_skin4.skin
new file mode 100644
index 00000000..72c9d01f
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin4.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base4.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin4_blue.skin b/assets/models/players/crash_color/lower_skin4_blue.skin
new file mode 100644
index 00000000..72c9d01f
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin4_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base4.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin4_red.skin b/assets/models/players/crash_color/lower_skin4_red.skin
new file mode 100644
index 00000000..72c9d01f
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin4_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base4.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin5.skin b/assets/models/players/crash_color/lower_skin5.skin
new file mode 100644
index 00000000..82e29dfc
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin5.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base5.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin5_blue.skin b/assets/models/players/crash_color/lower_skin5_blue.skin
new file mode 100644
index 00000000..82e29dfc
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin5_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base5.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin5_red.skin b/assets/models/players/crash_color/lower_skin5_red.skin
new file mode 100644
index 00000000..82e29dfc
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin5_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base5.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin6.skin b/assets/models/players/crash_color/lower_skin6.skin
new file mode 100644
index 00000000..2e64e349
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin6.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base6.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin6_blue.skin b/assets/models/players/crash_color/lower_skin6_blue.skin
new file mode 100644
index 00000000..2e64e349
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin6_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base6.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin6_red.skin b/assets/models/players/crash_color/lower_skin6_red.skin
new file mode 100644
index 00000000..2e64e349
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin6_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base6.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin7.skin b/assets/models/players/crash_color/lower_skin7.skin
new file mode 100644
index 00000000..3884915b
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin7.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base7.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin7_blue.skin b/assets/models/players/crash_color/lower_skin7_blue.skin
new file mode 100644
index 00000000..3884915b
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin7_blue.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base7.tga
+tag_torso
diff --git a/assets/models/players/crash_color/lower_skin7_red.skin b/assets/models/players/crash_color/lower_skin7_red.skin
new file mode 100644
index 00000000..3884915b
--- /dev/null
+++ b/assets/models/players/crash_color/lower_skin7_red.skin
@@ -0,0 +1,2 @@
+l_lower,models/players/crash_color/skin_base7.tga
+tag_torso
diff --git a/assets/models/players/crash_color/skin1/head_blue.skin b/assets/models/players/crash_color/skin1/head_blue.skin
new file mode 100644
index 00000000..5a371e68
--- /dev/null
+++ b/assets/models/players/crash_color/skin1/head_blue.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base1.tga
+tag_head
diff --git a/assets/models/players/crash_color/skin1/head_default.skin b/assets/models/players/crash_color/skin1/head_default.skin
new file mode 100644
index 00000000..5a371e68
--- /dev/null
+++ b/assets/models/players/crash_color/skin1/head_default.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base1.tga
+tag_head
diff --git a/assets/models/players/crash_color/skin1/head_red.skin b/assets/models/players/crash_color/skin1/head_red.skin
new file mode 100644
index 00000000..5a371e68
--- /dev/null
+++ b/assets/models/players/crash_color/skin1/head_red.skin
@@ -0,0 +1,2 @@
+h_face,models/players/crash_color/skin_base1.tga
+tag_head
diff --git a/assets/models/players/crash_color/skin_base.tga b/assets/models/players/crash_color/skin_base.tga
index 30978cc3..4b3e87a3 100644
Binary files a/assets/models/players/crash_color/skin_base.tga and b/assets/models/players/crash_color/skin_base.tga differ
diff --git a/assets/models/players/crash_color/skin_base1.tga b/assets/models/players/crash_color/skin_base1.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base1.tga differ
diff --git a/assets/models/players/crash_color/skin_base2.tga b/assets/models/players/crash_color/skin_base2.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base2.tga differ
diff --git a/assets/models/players/crash_color/skin_base3.tga b/assets/models/players/crash_color/skin_base3.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base3.tga differ
diff --git a/assets/models/players/crash_color/skin_base4.tga b/assets/models/players/crash_color/skin_base4.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base4.tga differ
diff --git a/assets/models/players/crash_color/skin_base5.tga b/assets/models/players/crash_color/skin_base5.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base5.tga differ
diff --git a/assets/models/players/crash_color/skin_base6.tga b/assets/models/players/crash_color/skin_base6.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base6.tga differ
diff --git a/assets/models/players/crash_color/skin_base7.tga b/assets/models/players/crash_color/skin_base7.tga
new file mode 100644
index 00000000..4b3e87a3
Binary files /dev/null and b/assets/models/players/crash_color/skin_base7.tga differ
diff --git a/assets/models/players/crash_color/upper_skin1.skin b/assets/models/players/crash_color/upper_skin1.skin
new file mode 100644
index 00000000..a87c97fa
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin1.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base1..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin1_blue.skin b/assets/models/players/crash_color/upper_skin1_blue.skin
new file mode 100644
index 00000000..a87c97fa
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin1_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base1..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin1_red.skin b/assets/models/players/crash_color/upper_skin1_red.skin
new file mode 100644
index 00000000..a87c97fa
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin1_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base1..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin2.skin b/assets/models/players/crash_color/upper_skin2.skin
new file mode 100644
index 00000000..5bdbbb87
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin2.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base2..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin2_blue.skin b/assets/models/players/crash_color/upper_skin2_blue.skin
new file mode 100644
index 00000000..5bdbbb87
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin2_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base2..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin2_red.skin b/assets/models/players/crash_color/upper_skin2_red.skin
new file mode 100644
index 00000000..5bdbbb87
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin2_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base2..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin3.skin b/assets/models/players/crash_color/upper_skin3.skin
new file mode 100644
index 00000000..92733a87
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin3.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base3..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin3_blue.skin b/assets/models/players/crash_color/upper_skin3_blue.skin
new file mode 100644
index 00000000..92733a87
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin3_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base3..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin3_red.skin b/assets/models/players/crash_color/upper_skin3_red.skin
new file mode 100644
index 00000000..92733a87
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin3_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base3..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin4.skin b/assets/models/players/crash_color/upper_skin4.skin
new file mode 100644
index 00000000..9b9ce4e8
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin4.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base4..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin4_blue.skin b/assets/models/players/crash_color/upper_skin4_blue.skin
new file mode 100644
index 00000000..9b9ce4e8
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin4_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base4..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin4_red.skin b/assets/models/players/crash_color/upper_skin4_red.skin
new file mode 100644
index 00000000..9b9ce4e8
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin4_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base4..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin5.skin b/assets/models/players/crash_color/upper_skin5.skin
new file mode 100644
index 00000000..2ac63fcf
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin5.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base5..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin5_blue.skin b/assets/models/players/crash_color/upper_skin5_blue.skin
new file mode 100644
index 00000000..2ac63fcf
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin5_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base5..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin5_red.skin b/assets/models/players/crash_color/upper_skin5_red.skin
new file mode 100644
index 00000000..2ac63fcf
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin5_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base5..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin6.skin b/assets/models/players/crash_color/upper_skin6.skin
new file mode 100644
index 00000000..a6329a64
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin6.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base6..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin6_blue.skin b/assets/models/players/crash_color/upper_skin6_blue.skin
new file mode 100644
index 00000000..a6329a64
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin6_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base6..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin6_red.skin b/assets/models/players/crash_color/upper_skin6_red.skin
new file mode 100644
index 00000000..a6329a64
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin6_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base6..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin7.skin b/assets/models/players/crash_color/upper_skin7.skin
new file mode 100644
index 00000000..5d504b02
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin7.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base7..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin7_blue.skin b/assets/models/players/crash_color/upper_skin7_blue.skin
new file mode 100644
index 00000000..5d504b02
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin7_blue.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base7..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/players/crash_color/upper_skin7_red.skin b/assets/models/players/crash_color/upper_skin7_red.skin
new file mode 100644
index 00000000..5d504b02
--- /dev/null
+++ b/assets/models/players/crash_color/upper_skin7_red.skin
@@ -0,0 +1,4 @@
+u_body,models/players/crash_color/skin_base7..tga
+tag_head,
+tag_weapon,
+tag_torso,
diff --git a/assets/models/rock_desert_01.md3 b/assets/models/rock_desert_01.md3
new file mode 100644
index 00000000..53e805c8
Binary files /dev/null and b/assets/models/rock_desert_01.md3 differ
diff --git a/assets/q3config.cfg b/assets/q3config.cfg
index 7db6cd09..30855325 100644
--- a/assets/q3config.cfg
+++ b/assets/q3config.cfg
@@ -158,7 +158,7 @@ seta cg_voipTeamOnly "1"
seta cg_weaponBarStyle "0"
seta cg_weaponOrder "/1/2/4/3/6/7/8/9/5/"
seta cg_zoomfov "22.5"
-seta cl_allowDownload "0"
+seta cl_allowDownload "1"
seta cl_anonymous "0"
seta cl_autoRecordDemo "0"
seta cl_aviFrameRate "25"
@@ -199,7 +199,7 @@ seta color2 "5"
seta com_altivec "0"
seta com_ansiColor "0"
seta com_blood "0"
-seta com_busyWait "0"
+seta com_busyWait "1"
seta com_hunkMegs "128"
seta com_introplayed "1"
seta com_logToStdErr "0"
@@ -231,7 +231,7 @@ seta elimination_startHealth "200"
seta elimination_warmup "7"
seta fraglimit "0"
seta g_admin "admin.dat"
-seta g_adminLog "admin.log"
+seta g_adminLog ""
seta g_adminMaxBan "2w"
seta g_adminNameProtect "1"
seta g_adminParseSay "1"
@@ -250,7 +250,7 @@ seta g_floodMinTime "2000"
seta g_friendlyfire "0"
seta g_lagLightning "1"
seta g_lms_mode "0"
-seta g_log "games.log"
+seta g_log ""
seta g_logsync "0"
seta g_mappools "0\maps_dm.cfg\1\maps_tourney.cfg\3\maps_tdm.cfg\4\maps_ctf.cfg\5\maps_oneflag.cfg\6\maps_obelisk.cfg\7\maps_harvester.cfg\8\maps_elimination.cfg\9\maps_ctf.cfg\10\maps_lms.cfg\11\maps_dd.cfg\12\maps_dom.cfg\"
seta g_maxGameClients "0"
@@ -375,6 +375,7 @@ seta r_flaresDlightShrink "1"
seta r_flareSun "0"
seta r_fullscreen "0"
seta r_gamma "1"
+seta r_gpuDeviceIndex "0"
seta r_greyscale "0"
seta r_iconmip "1"
seta r_ignoreFastPath "1"
@@ -391,7 +392,7 @@ seta r_lodCurveError "250"
seta r_marksOnTriangleMeshes "0"
seta r_mockvr "0"
seta r_mode "-1"
-seta r_monolightmaps "0"
+seta r_monolightmaps "1"
seta r_motionblur "0"
seta r_noborder "0"
seta r_ntsc "0"
@@ -418,6 +419,7 @@ seta r_subdivisions "1"
seta r_suggestiveThemes "1"
seta r_swapInterval "0"
seta r_texturebits "0"
+seta r_textureMaxSize "256"
seta r_textureMode "GL_NEAREST_MIPMAP_NEAREST"
seta r_tvConsoleMode "0"
seta r_tvMode "0"
@@ -457,21 +459,26 @@ seta server8 ""
seta server9 ""
seta sex "male"
seta snaps "20"
+seta sv_allowDownload "1"
seta sv_banFile "serverbans.dat"
-seta sv_dlRate "100"
+seta sv_dlRate "8192"
seta sv_dlURL ""
-seta sv_floodProtect "1"
+seta sv_floodProtect "0"
seta sv_fps "20"
seta sv_hostname "noname"
seta sv_lanForceRate "1"
seta sv_master3 ""
seta sv_master4 ""
seta sv_master5 ""
-seta sv_maxclients "8"
+seta sv_maxclients "64"
seta sv_maxPing "0"
+seta sv_rateLimit "0"
seta sv_maxRate "0"
seta sv_minPing "0"
seta sv_minRate "0"
+seta sv_pure "0"
+seta sv_reconnectlimit "0"
+seta sv_timeout "3600"
seta team_headmodel "crash"
seta team_model "crash"
seta timelimit "0"
diff --git a/assets/scripts/decal.shader b/assets/scripts/decal.shader
new file mode 100644
index 00000000..afe611b4
--- /dev/null
+++ b/assets/scripts/decal.shader
@@ -0,0 +1,720 @@
+textures/decal/lab_games/dec_img_style01_001_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_001
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_001
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_002_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_002
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_002
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_003_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_003
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_003
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_004_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_004
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_004
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_005_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_005
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_005
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_006_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_006
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_006
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_007_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_007
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_007
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_008_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_008
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_008
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_009_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_009
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_009
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_010_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_010
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_010
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_011_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_011
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_011
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_012_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_012
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_012
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_013_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_013
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_013
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_014_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_014
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_014
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_015_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_015
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_015
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_016_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_016
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_016
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_017_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_017
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_017
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_018_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_018
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_018
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_019_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_019
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_019
+ }
+}
+
+textures/decal/lab_games/dec_img_style01_020_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style01_020
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style01_020
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_001_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_001
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_001
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_002_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_002
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_002
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_003_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_003
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_003
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_004_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_004
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_004
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_005_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_005
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_005
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_006_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_006
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_006
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_007_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_007
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_007
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_008_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_008
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_008
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_009_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_009
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_009
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_010_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_010
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_010
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_011_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_011
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_011
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_012_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_012
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_012
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_013_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_013
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_013
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_014_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_014
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_014
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_015_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_015
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_015
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_016_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_016
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_016
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_017_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_017
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_017
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_018_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_018
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_018
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_019_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_019
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_019
+ }
+}
+
+textures/decal/lab_games/dec_img_style02_020_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style02_020
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style02_020
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_001_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_001
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_001
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_002_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_002
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_002
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_003_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_003
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_003
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_004_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_004
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_004
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_005_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_005
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_005
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_006_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_006
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_006
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_007_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_007
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_007
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_008_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_008
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_008
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_009_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_009
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_009
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_010_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_010
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_010
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_011_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_011
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_011
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_012_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_012
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_012
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_013_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_013
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_013
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_014_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_014
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_014
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_015_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_015
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_015
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_016_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_016
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_016
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_017_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_017
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_017
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_018_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_018
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_018
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_019_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_019
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_019
+ }
+}
+
+textures/decal/lab_games/dec_img_style03_020_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style03_020
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style03_020
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_001_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_001
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_001
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_002_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_002
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_002
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_003_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_003
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_003
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_004_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_004
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_004
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_005_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_005
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_005
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_006_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_006
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_006
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_007_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_007
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_007
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_008_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_008
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_008
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_009_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_009
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_009
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_010_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_010
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_010
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_011_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_011
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_011
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_012_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_012
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_012
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_013_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_013
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_013
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_014_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_014
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_014
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_015_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_015
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_015
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_016_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_016
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_016
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_017_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_017
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_017
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_018_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_018
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_018
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_019_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_019
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_019
+ }
+}
+
+textures/decal/lab_games/dec_img_style04_020_nonsolid
+{
+ qer_editorimage textures/decal/lab_games/dec_img_style04_020
+ surfaceparm nonsolid
+ {
+ map textures/decal/lab_games/dec_img_style04_020
+ }
+}
+
diff --git a/assets/scripts/fut_teleport_d.shader b/assets/scripts/fut_teleport_d.shader
deleted file mode 100644
index 448451cb..00000000
--- a/assets/scripts/fut_teleport_d.shader
+++ /dev/null
@@ -1,30 +0,0 @@
-textures/model/fut_teleport_d
-{
- qer_editorimage textures/model/fut_teleport_d.tga
- surfaceparm metalsteps
- {
- map textures/map/water_env.tga
- blendfunc add
- tcGen environment
- }
- {
- map textures/model/fut_teleport_d.tga
- blendfunc GL_ZERO GL_SRC_ALPHA
- }
-
- {
- map textures/model/fut_teleport_d.tga
- }
- {
- map $lightmap
- blendfunc filter
- tcGen lightmap
- }
- {
- map textures/model/fut_teleport_e.tga
- rgbGen wave sin 0.3 0.3 0.0 0.5
- blendfunc add
- }
-}
-
-
diff --git a/assets/scripts/lab_floors.shader b/assets/scripts/lab_floors.shader
new file mode 100644
index 00000000..6acf92d2
--- /dev/null
+++ b/assets/scripts/lab_floors.shader
@@ -0,0 +1,183 @@
+// FLOORS FOR DYNAMIC COLOURING
+
+textures/map/lab_floors/lg_style_06_floor_1
+{
+ qer_editorimage textures/map/lab_games/lg_style_01_floor_orange_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_01_floor_orange_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.00 0.58 1.00 )
+ }
+}
+
+textures/map/lab_floors/lg_style_06_floor_2
+{
+ qer_editorimage textures/map/lab_games/lg_style_01_floor_orange_bright_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_01_floor_orange_bright_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.00 0.58 1.00 )
+ }
+}
+
+textures/map/lab_floors/lg_style_06_floor_3
+{
+ qer_editorimage textures/map/lab_games/lg_style_01_floor_blue_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_01_floor_blue_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m
+ blendfunc add
+ rgbgen const ( 0.00 0.58 1.00 )
+ }
+}
+
+textures/map/lab_floors/lg_style_06_floor_4
+{
+ qer_editorimage textures/map/lab_games/lg_style_01_floor_blue_bright_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_01_floor_blue_bright_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m
+ blendfunc add
+ rgbgen const ( 0.00 0.58 1.00 )
+ }
+}
+
+textures/map/lab_floors/lg_style_06_floor_5
+{
+ qer_editorimage textures/map/lab_games/lg_style_02_floor_blue_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_02_floor_blue_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_02_floor_light_m.tga
+ blendfunc add
+ rgbGen wave sin 1.0 1.0 0.0 0.15
+ }
+}
+
+textures/map/lab_floors/lg_style_06_floor_6
+{
+ qer_editorimage textures/map/lab_games/lg_style_02_floor_blue_bright_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_02_floor_blue_bright_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_02_floor_light_m.tga
+ blendfunc add
+ rgbGen wave sin 1.0 1.0 0.0 0.15
+ }
+}
+
+textures/map/lab_floors/lg_style_06_floor_7
+{
+ qer_editorimage textures/map/lab_games/lg_style_02_floor_green_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_style_02_floor_green_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_02_floor_light_m.tga
+ blendfunc add
+ rgbGen wave sin 1.0 1.0 0.0 0.15
+ }
+}
diff --git a/assets/scripts/lab_floors_placeholders.shader b/assets/scripts/lab_floors_placeholders.shader
new file mode 100644
index 00000000..96de62ed
--- /dev/null
+++ b/assets/scripts/lab_floors_placeholders.shader
@@ -0,0 +1,181 @@
+textures/map/lab_games/lg_style_01_floor_placeholder_0
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_0_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_0_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
+
+textures/map/lab_games/lg_style_01_floor_placeholder_A
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_A_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_A_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
+
+textures/map/lab_games/lg_style_01_floor_placeholder_B
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_B_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_B_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
+
+textures/map/lab_games/lg_style_01_floor_placeholder_C
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_C_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_C_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
+
+textures/map/lab_games/lg_style_01_floor_placeholder_D
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_D_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_D_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
+
+textures/map/lab_games/lg_style_01_floor_placeholder_E
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_E_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_E_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
+
+textures/map/lab_games/lg_style_01_floor_placeholder_F
+{
+ qer_editorimage textures/map/lab_games/lg_floor_placeholder_F_d.tga
+ nopicmip
+ q3map_lightmapSampleSize 8
+ {
+ map textures/map/lab_games/lg_floor_placeholder_F_d.tga
+ }
+ {
+ map textures/map/lab_games/lg_circuit_r.tga
+ blendfunc add
+ rgbgen const ( 0.06 0.06 0.06 )
+ tcgen environment
+ }
+ {
+ map $lightmap
+ blendFunc GL_DST_COLOR GL_ZERO
+ rgbGen identity
+ }
+ {
+ map textures/map/lab_games/lg_style_01_floor_light_m.tga
+ blendfunc add
+ rgbgen const ( 0.50 0.50 0.50 )
+ }
+}
diff --git a/assets/scripts/lab_games.shader b/assets/scripts/lab_games.shader
index fcb437c0..9caccc52 100644
--- a/assets/scripts/lab_games.shader
+++ b/assets/scripts/lab_games.shader
@@ -2020,6 +2020,35 @@ textures/map/ghost
}
}
+textures/map/water_d
+{
+ qer_editorimage textures/map/glass_d.tga
+ surfaceparm trans
+ {
+ map textures/map/glass_d.tga
+ blendfunc blend
+ }
+ {
+ map textures/map/caustics_d.tga
+ blendfunc add
+ tcMod scale 0.7 0.7
+ tcMod stretch sin 1.0 0.02 0 0.1
+ tcMod turb 0 0.04 0 0.12
+ }
+ {
+ map textures/map/caustics_d.tga
+ blendfunc add
+ tcMod scale 0.5 0.5
+ tcMod stretch sin 1.0 0.04 0 0.2
+ tcMod turb 0 0.06 0.5 0.1
+ }
+ {
+ map textures/map/water_env.tga
+ blendfunc add
+ tcGen environment
+ }
+}
+
// SPECIFIC OBJECTS
textures/model/mothership_d
diff --git a/assets/scripts/psychlab.shader b/assets/scripts/psychlab.shader
new file mode 100644
index 00000000..6da34d42
--- /dev/null
+++ b/assets/scripts/psychlab.shader
@@ -0,0 +1,8 @@
+textures/map/slot1
+{
+ nomipmaps
+ qer_editorimage textures/map/slot1
+ {
+ map textures/map/slot1
+ }
+}
diff --git a/assets/scripts/shaderlist.txt b/assets/scripts/shaderlist.txt
index 33e07345..42e255f5 100644
--- a/assets/scripts/shaderlist.txt
+++ b/assets/scripts/shaderlist.txt
@@ -1,8 +1,12 @@
ctp_tech_lava_d
+decal
fut_objects
ghost
lab_games
+lab_floors
+lab_floors_placeholders
dm_lab
poltergeist
+psychlab
skies
skies_nat_lab
diff --git a/assets/textures/map/desert_day_sky_bk.tga b/assets/textures/map/desert_day_sky_bk.tga
new file mode 100644
index 00000000..2b04237a
Binary files /dev/null and b/assets/textures/map/desert_day_sky_bk.tga differ
diff --git a/assets/textures/map/desert_day_sky_dn.tga b/assets/textures/map/desert_day_sky_dn.tga
new file mode 100644
index 00000000..9f1f5824
Binary files /dev/null and b/assets/textures/map/desert_day_sky_dn.tga differ
diff --git a/assets/textures/map/desert_day_sky_ft.tga b/assets/textures/map/desert_day_sky_ft.tga
new file mode 100644
index 00000000..2b04237a
Binary files /dev/null and b/assets/textures/map/desert_day_sky_ft.tga differ
diff --git a/assets/textures/map/desert_day_sky_lf.tga b/assets/textures/map/desert_day_sky_lf.tga
new file mode 100644
index 00000000..2b04237a
Binary files /dev/null and b/assets/textures/map/desert_day_sky_lf.tga differ
diff --git a/assets/textures/map/desert_day_sky_rt.tga b/assets/textures/map/desert_day_sky_rt.tga
new file mode 100644
index 00000000..2b04237a
Binary files /dev/null and b/assets/textures/map/desert_day_sky_rt.tga differ
diff --git a/assets/textures/map/desert_day_sky_up.tga b/assets/textures/map/desert_day_sky_up.tga
new file mode 100644
index 00000000..720c7f99
Binary files /dev/null and b/assets/textures/map/desert_day_sky_up.tga differ
diff --git a/assets/textures/map/desert_night_sky_bk.tga b/assets/textures/map/desert_night_sky_bk.tga
new file mode 100644
index 00000000..c32359c0
Binary files /dev/null and b/assets/textures/map/desert_night_sky_bk.tga differ
diff --git a/assets/textures/map/desert_night_sky_dn.tga b/assets/textures/map/desert_night_sky_dn.tga
new file mode 100644
index 00000000..f5ea4817
Binary files /dev/null and b/assets/textures/map/desert_night_sky_dn.tga differ
diff --git a/assets/textures/map/desert_night_sky_ft.tga b/assets/textures/map/desert_night_sky_ft.tga
new file mode 100644
index 00000000..c32359c0
Binary files /dev/null and b/assets/textures/map/desert_night_sky_ft.tga differ
diff --git a/assets/textures/map/desert_night_sky_lf.tga b/assets/textures/map/desert_night_sky_lf.tga
new file mode 100644
index 00000000..c32359c0
Binary files /dev/null and b/assets/textures/map/desert_night_sky_lf.tga differ
diff --git a/assets/textures/map/desert_night_sky_rt.tga b/assets/textures/map/desert_night_sky_rt.tga
new file mode 100644
index 00000000..c32359c0
Binary files /dev/null and b/assets/textures/map/desert_night_sky_rt.tga differ
diff --git a/assets/textures/map/desert_night_sky_up.tga b/assets/textures/map/desert_night_sky_up.tga
new file mode 100644
index 00000000..56b49e89
Binary files /dev/null and b/assets/textures/map/desert_night_sky_up.tga differ
diff --git a/assets/textures/map/desert_rockface_01_d.tga b/assets/textures/map/desert_rockface_01_d.tga
new file mode 100644
index 00000000..b169bf5b
Binary files /dev/null and b/assets/textures/map/desert_rockface_01_d.tga differ
diff --git a/assets/textures/map/desert_sandground_01_d.tga b/assets/textures/map/desert_sandground_01_d.tga
new file mode 100644
index 00000000..ad688e42
Binary files /dev/null and b/assets/textures/map/desert_sandground_01_d.tga differ
diff --git a/assets/textures/map/floor_fill_d.tga b/assets/textures/map/floor_fill_d.tga
new file mode 100644
index 00000000..6a7943b0
Binary files /dev/null and b/assets/textures/map/floor_fill_d.tga differ
diff --git a/assets/textures/map/fut_ceiling_tile_02_d.tga b/assets/textures/map/fut_ceiling_tile_02_d.tga
new file mode 100644
index 00000000..f8ca9d82
Binary files /dev/null and b/assets/textures/map/fut_ceiling_tile_02_d.tga differ
diff --git a/assets/textures/map/fut_flat_wall_yellow_blank_d.tga b/assets/textures/map/fut_flat_wall_yellow_blank_d.tga
new file mode 100644
index 00000000..bb712db6
Binary files /dev/null and b/assets/textures/map/fut_flat_wall_yellow_blank_d.tga differ
diff --git a/assets/textures/map/glass_d.tga b/assets/textures/map/glass_d.tga
new file mode 100644
index 00000000..cfbc1c75
Binary files /dev/null and b/assets/textures/map/glass_d.tga differ
diff --git a/assets/textures/map/grey_clang.tga b/assets/textures/map/grey_clang.tga
new file mode 100644
index 00000000..80a8f48c
Binary files /dev/null and b/assets/textures/map/grey_clang.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_0_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_0_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_0_d.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_A_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_A_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_A_d.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_B_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_B_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_B_d.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_C_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_C_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_C_d.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_D_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_D_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_D_d.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_E_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_E_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_E_d.tga differ
diff --git a/assets/textures/map/lab_games/lg_floor_placeholder_F_d.tga b/assets/textures/map/lab_games/lg_floor_placeholder_F_d.tga
new file mode 100644
index 00000000..50e35535
Binary files /dev/null and b/assets/textures/map/lab_games/lg_floor_placeholder_F_d.tga differ
diff --git a/assets/textures/map/lg_sky_01_bk.tga b/assets/textures/map/lg_sky_01_bk.tga
new file mode 100644
index 00000000..e95023f3
Binary files /dev/null and b/assets/textures/map/lg_sky_01_bk.tga differ
diff --git a/assets/textures/map/lg_sky_01_dn.tga b/assets/textures/map/lg_sky_01_dn.tga
new file mode 100644
index 00000000..bc0a13fe
Binary files /dev/null and b/assets/textures/map/lg_sky_01_dn.tga differ
diff --git a/assets/textures/map/lg_sky_01_ft.tga b/assets/textures/map/lg_sky_01_ft.tga
new file mode 100644
index 00000000..17cd7c7b
Binary files /dev/null and b/assets/textures/map/lg_sky_01_ft.tga differ
diff --git a/assets/textures/map/lg_sky_01_lf.tga b/assets/textures/map/lg_sky_01_lf.tga
new file mode 100644
index 00000000..7aaa2036
Binary files /dev/null and b/assets/textures/map/lg_sky_01_lf.tga differ
diff --git a/assets/textures/map/lg_sky_01_rt.tga b/assets/textures/map/lg_sky_01_rt.tga
new file mode 100644
index 00000000..6e9f7a51
Binary files /dev/null and b/assets/textures/map/lg_sky_01_rt.tga differ
diff --git a/assets/textures/map/lg_sky_01_up.tga b/assets/textures/map/lg_sky_01_up.tga
new file mode 100644
index 00000000..45baecba
Binary files /dev/null and b/assets/textures/map/lg_sky_01_up.tga differ
diff --git a/assets/textures/map/lg_sky_02_bk.tga b/assets/textures/map/lg_sky_02_bk.tga
new file mode 100644
index 00000000..4fdc3dbb
Binary files /dev/null and b/assets/textures/map/lg_sky_02_bk.tga differ
diff --git a/assets/textures/map/lg_sky_02_dn.tga b/assets/textures/map/lg_sky_02_dn.tga
new file mode 100644
index 00000000..bc0a13fe
Binary files /dev/null and b/assets/textures/map/lg_sky_02_dn.tga differ
diff --git a/assets/textures/map/lg_sky_02_ft.tga b/assets/textures/map/lg_sky_02_ft.tga
new file mode 100644
index 00000000..ee2ea579
Binary files /dev/null and b/assets/textures/map/lg_sky_02_ft.tga differ
diff --git a/assets/textures/map/lg_sky_02_lf.tga b/assets/textures/map/lg_sky_02_lf.tga
new file mode 100644
index 00000000..66443eaf
Binary files /dev/null and b/assets/textures/map/lg_sky_02_lf.tga differ
diff --git a/assets/textures/map/lg_sky_02_rt.tga b/assets/textures/map/lg_sky_02_rt.tga
new file mode 100644
index 00000000..5b861f25
Binary files /dev/null and b/assets/textures/map/lg_sky_02_rt.tga differ
diff --git a/assets/textures/map/lg_sky_02_up.tga b/assets/textures/map/lg_sky_02_up.tga
new file mode 100644
index 00000000..de49a04f
Binary files /dev/null and b/assets/textures/map/lg_sky_02_up.tga differ
diff --git a/assets/textures/map/water_env.tga b/assets/textures/map/water_env.tga
new file mode 100644
index 00000000..1e51be7c
Binary files /dev/null and b/assets/textures/map/water_env.tga differ
diff --git a/assets/textures/model/apple2_d.tga b/assets/textures/model/apple2_d.tga
new file mode 100644
index 00000000..cb715f4c
Binary files /dev/null and b/assets/textures/model/apple2_d.tga differ
diff --git a/assets/textures/model/banana_d.tga b/assets/textures/model/banana_d.tga
new file mode 100644
index 00000000..3fe8335e
Binary files /dev/null and b/assets/textures/model/banana_d.tga differ
diff --git a/assets/models/players/crash_color/dm_character_skin_mask_b.tga b/assets/textures/model/bee_d.tga
similarity index 58%
rename from assets/models/players/crash_color/dm_character_skin_mask_b.tga
rename to assets/textures/model/bee_d.tga
index 8ea525b3..c9ae4bf5 100644
Binary files a/assets/models/players/crash_color/dm_character_skin_mask_b.tga and b/assets/textures/model/bee_d.tga differ
diff --git a/assets/textures/model/bottle_d.tga b/assets/textures/model/bottle_d.tga
new file mode 100644
index 00000000..b854ae0e
Binary files /dev/null and b/assets/textures/model/bottle_d.tga differ
diff --git a/assets/textures/model/bush_desert_01_d.tga b/assets/textures/model/bush_desert_01_d.tga
new file mode 100644
index 00000000..a58f5c9d
Binary files /dev/null and b/assets/textures/model/bush_desert_01_d.tga differ
diff --git a/assets/textures/model/cactus_d.tga b/assets/textures/model/cactus_d.tga
new file mode 100644
index 00000000..f084a6cf
Binary files /dev/null and b/assets/textures/model/cactus_d.tga differ
diff --git a/assets/textures/model/car_d.tga b/assets/textures/model/car_d.tga
new file mode 100644
index 00000000..0263b8e8
Binary files /dev/null and b/assets/textures/model/car_d.tga differ
diff --git a/assets/textures/model/cherry_d.tga b/assets/textures/model/cherry_d.tga
new file mode 100644
index 00000000..e580f6a6
Binary files /dev/null and b/assets/textures/model/cherry_d.tga differ
diff --git a/assets/textures/model/cow_d.tga b/assets/textures/model/cow_d.tga
new file mode 100644
index 00000000..1041cbaf
Binary files /dev/null and b/assets/textures/model/cow_d.tga differ
diff --git a/assets/textures/model/flower_d.tga b/assets/textures/model/flower_d.tga
new file mode 100644
index 00000000..dc1536a4
Binary files /dev/null and b/assets/textures/model/flower_d.tga differ
diff --git a/assets/textures/model/fork_d.tga b/assets/textures/model/fork_d.tga
new file mode 100644
index 00000000..e6168860
Binary files /dev/null and b/assets/textures/model/fork_d.tga differ
diff --git a/assets/textures/model/fridge_d.tga b/assets/textures/model/fridge_d.tga
new file mode 100644
index 00000000..f1347ade
Binary files /dev/null and b/assets/textures/model/fridge_d.tga differ
diff --git a/assets/textures/model/fut_teleport_d.tga b/assets/textures/model/fut_teleport_d.tga
deleted file mode 100644
index 4aa80746..00000000
Binary files a/assets/textures/model/fut_teleport_d.tga and /dev/null differ
diff --git a/assets/textures/model/fut_teleport_e.tga b/assets/textures/model/fut_teleport_e.tga
deleted file mode 100644
index 0a7efe43..00000000
Binary files a/assets/textures/model/fut_teleport_e.tga and /dev/null differ
diff --git a/assets/textures/model/hammer_d.tga b/assets/textures/model/hammer_d.tga
new file mode 100644
index 00000000..fcd07c0d
Binary files /dev/null and b/assets/textures/model/hammer_d.tga differ
diff --git a/assets/textures/model/jug_d.tga b/assets/textures/model/jug_d.tga
new file mode 100644
index 00000000..02f09d4f
Binary files /dev/null and b/assets/textures/model/jug_d.tga differ
diff --git a/assets/textures/model/key_d.tga b/assets/textures/model/key_d.tga
new file mode 100644
index 00000000..0c50916c
Binary files /dev/null and b/assets/textures/model/key_d.tga differ
diff --git a/assets/textures/model/knife_d.tga b/assets/textures/model/knife_d.tga
new file mode 100644
index 00000000..e6168860
Binary files /dev/null and b/assets/textures/model/knife_d.tga differ
diff --git a/assets/textures/model/mushroom_desert_white_d.tga b/assets/textures/model/mushroom_desert_white_d.tga
new file mode 100644
index 00000000..e33b147e
Binary files /dev/null and b/assets/textures/model/mushroom_desert_white_d.tga differ
diff --git a/assets/textures/model/pig_d.tga b/assets/textures/model/pig_d.tga
new file mode 100644
index 00000000..6f1a04ce
Binary files /dev/null and b/assets/textures/model/pig_d.tga differ
diff --git a/assets/textures/model/pincer_d.tga b/assets/textures/model/pincer_d.tga
new file mode 100644
index 00000000..9384a2a3
Binary files /dev/null and b/assets/textures/model/pincer_d.tga differ
diff --git a/assets/textures/model/plant_d.tga b/assets/textures/model/plant_d.tga
new file mode 100644
index 00000000..9de3c8d2
Binary files /dev/null and b/assets/textures/model/plant_d.tga differ
diff --git a/assets/textures/model/rock_desert_d.tga b/assets/textures/model/rock_desert_d.tga
new file mode 100644
index 00000000..f18695e4
Binary files /dev/null and b/assets/textures/model/rock_desert_d.tga differ
diff --git a/assets/textures/model/saxophone_d.tga b/assets/textures/model/saxophone_d.tga
new file mode 100644
index 00000000..3d0e84b2
Binary files /dev/null and b/assets/textures/model/saxophone_d.tga differ
diff --git a/assets/textures/model/shoe_d.tga b/assets/textures/model/shoe_d.tga
new file mode 100644
index 00000000..d4db90a5
Binary files /dev/null and b/assets/textures/model/shoe_d.tga differ
diff --git a/assets/textures/model/spoon_d.tga b/assets/textures/model/spoon_d.tga
new file mode 100644
index 00000000..e6168860
Binary files /dev/null and b/assets/textures/model/spoon_d.tga differ
diff --git a/assets/textures/model/strawberry_d.tga b/assets/textures/model/strawberry_d.tga
old mode 100755
new mode 100644
diff --git a/assets/textures/model/tennis_racket_d.tga b/assets/textures/model/tennis_racket_d.tga
new file mode 100644
index 00000000..455d63b6
Binary files /dev/null and b/assets/textures/model/tennis_racket_d.tga differ
diff --git a/assets/textures/model/tomato_d.tga b/assets/textures/model/tomato_d.tga
new file mode 100644
index 00000000..3d6a6952
Binary files /dev/null and b/assets/textures/model/tomato_d.tga differ
diff --git a/assets/textures/model/tree_d.tga b/assets/textures/model/tree_d.tga
new file mode 100644
index 00000000..4290b7e0
Binary files /dev/null and b/assets/textures/model/tree_d.tga differ
diff --git a/assets/textures/model/wine_glass_d.tga b/assets/textures/model/wine_glass_d.tga
new file mode 100644
index 00000000..e6168860
Binary files /dev/null and b/assets/textures/model/wine_glass_d.tga differ
diff --git a/assets/textures/model/zebra_d.tga b/assets/textures/model/zebra_d.tga
new file mode 100644
index 00000000..07ba53bc
Binary files /dev/null and b/assets/textures/model/zebra_d.tga differ
diff --git a/assets_oa/scripts/bots.txt b/assets_oa/scripts/bots.txt
index 156cfba5..689b9629 100644
--- a/assets_oa/scripts/bots.txt
+++ b/assets_oa/scripts/bots.txt
@@ -1,71 +1,121 @@
{
-name Tauri
+name Cygni
model crash
-aifile bots/crash_c.c
+aifile bots/major_c.c
}
{
-name Centauri
+name Leonis
model crash
-aifile bots/dark_c.c
+aifile bots/gargoyle_c.c
}
{
-name Draconis
+name Epsilon
model crash
-aifile bots/ayumi_c.c
+aifile bots/beret_c.c
}
{
-name Epsilon
+name Cephei
model crash
-aifile bots/beret_c.c
+aifile bots/liz_c.c
}
{
-name Leonis
+name Centauri
model crash
-aifile bots/gargoyle_c.c
+aifile bots/dark_c.c
}
{
-name Cephei
+name Draconis
model crash
-aifile bots/liz_c.c
+aifile bots/ayumi_c.c
}
{
-name Cygni
+name Tauri
model crash
-aifile bots/major_c.c
+aifile bots/crash_c.c
}
{
-name TauriColor
+name CygniColor
+funname Cygni
model crash_color
-aifile bots/crash_c.c
+aifile bots/major_c.c
+}
+{
+name LeonisColor
+funname Leonis
+model crash_color
+aifile bots/gargoyle_c.c
+}
+{
+name EpsilonColor
+funname Epsilon
+model crash_color
+aifile bots/beret_c.c
+}
+{
+name CepheiColor
+funname Cephei
+model crash_color
+aifile bots/liz_c.c
}
{
name CentauriColor
+funname Centauri
model crash_color
aifile bots/dark_c.c
}
{
name DraconisColor
+funname Draconis
model crash_color
aifile bots/ayumi_c.c
}
{
-name EpsilonColor
+name TauriColor
+funname Tauri
model crash_color
-aifile bots/beret_c.c
+aifile bots/crash_c.c
}
{
-name LeonisColor
-model crash_color
+name CygniColorAlt
+funname Cygni
+model crash_color/skin1
+aifile bots/major_c.c
+}
+
+{
+name LeonisColorAlt
+funname Leonis
+model crash_color/skin2
aifile bots/gargoyle_c.c
}
{
-name CepheiColor
-model crash_color
+name EpsilonColorAlt
+funname Epsilon
+model crash_color/skin3
+aifile bots/beret_c.c
+}
+{
+name CepheiColorAlt
+funname Cephei
+model crash_color/skin4
aifile bots/liz_c.c
}
{
-name CygniColor
-model crash_color
-aifile bots/major_c.c
+name CentauriColorAlt
+funname Centauri
+model crash_color/skin5
+aifile bots/dark_c.c
+}
+{
+name DraconisColorAlt
+funname Draconis
+model crash_color/skin6
+aifile bots/ayumi_c.c
+}
+{
+name TauriColorAlt
+funname Tauri
+model crash_color/skin7
+aifile bots/crash_c.c
}
diff --git a/assets_oa/scripts/character.shader b/assets_oa/scripts/character.shader
index 17aa4250..d3b54394 100644
--- a/assets_oa/scripts/character.shader
+++ b/assets_oa/scripts/character.shader
@@ -4,7 +4,7 @@ models/players/crash/redskin
nopicmip
{
map models/players/crash/redskin.tga
- blendfunc gl_src_alpha gl_one_minus_src_alpha
+ blendfunc blend
alphaFunc GE128
depthwrite
}
@@ -15,7 +15,7 @@ models/players/crash/redskin
}
{
map models/players/crash/thruster_glow.tga
- blendfunc gl_one gl_one
+ blendfunc add
}
}
@@ -25,7 +25,7 @@ models/players/crash/blueskin
nopicmip
{
map models/players/crash/blueskin.tga
- blendfunc gl_src_alpha gl_one_minus_src_alpha
+ blendfunc blend
alphaFunc GE128
depthwrite
}
@@ -36,7 +36,7 @@ models/players/crash/blueskin
}
{
map models/players/crash/thruster_glow.tga
- blendfunc gl_one gl_one
+ blendfunc add
}
}
@@ -46,18 +46,18 @@ models/players/crash/skin1
nopicmip
{
map models/players/crash/skin1.tga
- blendfunc gl_src_alpha gl_one_minus_src_alpha
+ blendfunc blend
alphaFunc GE128
depthwrite
}
{
map models/players/crash/skin1_scroll.tga
- blendfunc gl_one gl_one
+ blendfunc add
tcMod scroll -1.6 0
}
{
map models/players/crash/thruster_glow.tga
- blendfunc gl_one gl_one
+ blendfunc add
}
}
@@ -67,35 +67,17 @@ models/players/crash_color/skin_base //crash color shader
nomipmaps
{
map models/players/crash_color/skin_base.tga
- blendfunc gl_src_alpha gl_one_minus_src_alpha
+ blendfunc blend
alphaFunc GE128
depthwrite
}
-
- {
- map models/players/crash_color/dm_character_skin_mask_a.tga
- blendfunc blend
- rgbGen lightingDiffuse
- }
-
- {
- map models/players/crash_color/dm_character_skin_mask_b.tga
- blendfunc blend
- rgbGen lightingDiffuse
- }
-
- {
- map models/players/crash_color/dm_character_skin_mask_c.tga
- blendfunc blend
- rgbGen lightingDiffuse
- }
{
map models/players/crash/skin1_scroll.tga
- blendfunc gl_one gl_one
+ blendfunc add
tcMod scroll -1.6 0
}
{
map models/players/crash/thruster_glow.tga
- blendfunc gl_one gl_one
+ blendfunc add
}
}
diff --git a/assets_oa/scripts/common.shader b/assets_oa/scripts/common.shader
new file mode 100644
index 00000000..ca17ca9e
--- /dev/null
+++ b/assets_oa/scripts/common.shader
@@ -0,0 +1,6 @@
+textures/common/caulk
+{
+ surfaceparm nodraw
+ surfaceparm nomarks
+ surfaceparm nolightmap
+}
diff --git a/assets_oa/scripts/ctf.shader b/assets_oa/scripts/ctf.shader
new file mode 100644
index 00000000..97c30af1
--- /dev/null
+++ b/assets_oa/scripts/ctf.shader
@@ -0,0 +1,50 @@
+sprites/friend
+{
+ nomipmaps
+ {
+ map sprites/friend1.tga
+ blendfunc blend
+ }
+}
+
+sprites/foe
+{
+ nomipmaps
+ {
+ map sprites/foe2.tga
+ blendfunc blend
+ }
+}
+
+models/flags/b_flag
+{
+ cull none
+ nopicmip
+ {
+ map models/flags/b_flag.tga
+ }
+}
+
+models/flags/pole
+{
+ {
+ map textures/base_wall/chrome_env.tga
+ rgbGen lightingDiffuse
+ tcMod scale 0.5 0.5
+ tcGen environment
+ }
+ {
+ map models/flags/pole.tga
+ blendfunc filter
+ rgbGen identity
+ }
+}
+
+models/flags/r_flag
+{
+ cull none
+ nopicmip
+ {
+ map models/flags/r_flag.tga
+ }
+}
diff --git a/assets_oa/scripts/decals.shader b/assets_oa/scripts/decals.shader
index b1ed5ba8..acb18bf6 100644
--- a/assets_oa/scripts/decals.shader
+++ b/assets_oa/scripts/decals.shader
@@ -30,6 +30,16 @@ gfx/damage/hole_lg_mrk //beam
}
}
+gfx/damage/plasma_mrk //disc
+{
+ polygonOffset
+ {
+ map textures/model/circuit_decal.tga
+ blendfunc add
+ rgbgen const ( 0.00 0.58 1.00 ) //blue
+ }
+}
+
//OTHER
gfx/misc/tracer
diff --git a/assets_oa/scripts/shaderlist.txt b/assets_oa/scripts/shaderlist.txt
index 97956c0c..efc48f6d 100644
--- a/assets_oa/scripts/shaderlist.txt
+++ b/assets_oa/scripts/shaderlist.txt
@@ -1,6 +1,7 @@
// this file lists all the separate shader files
ammo
character
+common
decals
iconsprites
weaponhits
diff --git a/deepmind/engine/BUILD b/deepmind/engine/BUILD
index d4128020..8a798cf3 100644
--- a/deepmind/engine/BUILD
+++ b/deepmind/engine/BUILD
@@ -7,22 +7,136 @@ cc_library(
name = "context",
srcs = ["context.cc"],
hdrs = ["context.h"],
- visibility = ["//:__pkg__"],
+ visibility = ["//visibility:public"],
deps = [
+ ":context_entities",
+ ":context_events",
+ ":context_game",
+ ":context_observations",
+ ":context_pickups",
+ ":lua_image",
":lua_maze_generation",
":lua_random",
":lua_text_level_maker",
- "//deepmind/include:context_headers",
+ "//:level_cache_types",
+ "//deepmind/include:context_hdrs",
+ "//deepmind/level_generation:compile_map",
"//deepmind/lua",
"//deepmind/lua:bind",
"//deepmind/lua:call",
"//deepmind/lua:class",
"//deepmind/lua:n_results_or",
+ "//deepmind/lua:push",
"//deepmind/lua:push_script",
"//deepmind/lua:read",
"//deepmind/lua:table_ref",
"//deepmind/lua:vm",
+ "//deepmind/model_generation:lua_model",
+ "//deepmind/model_generation:lua_transform",
+ "//deepmind/model_generation:model",
+ "//deepmind/model_generation:model_getters",
+ "//deepmind/model_generation:model_lua",
+ "//deepmind/support:logging",
"//deepmind/tensor:lua_tensor",
+ "//deepmind/tensor:tensor_view",
+ "//third_party/rl_api:env_c_api",
+ "@com_google_absl//absl/strings",
+ ],
+)
+
+cc_library(
+ name = "context_events",
+ srcs = ["context_events.cc"],
+ hdrs = ["context_events.h"],
+ deps = [
+ "//deepmind/lua",
+ "//deepmind/lua:class",
+ "//deepmind/lua:n_results_or",
+ "//deepmind/lua:read",
+ "//deepmind/tensor:lua_tensor",
+ "//deepmind/tensor:tensor_view",
+ "//third_party/rl_api:env_c_api",
+ ],
+)
+
+cc_library(
+ name = "context_game",
+ srcs = ["context_game.cc"],
+ hdrs = ["context_game.h"],
+ deps = [
+ "//deepmind/include:context_hdrs",
+ "//deepmind/lua",
+ "//deepmind/lua:class",
+ "//deepmind/lua:n_results_or",
+ "//deepmind/lua:push",
+ "//deepmind/lua:read",
+ "//deepmind/lua:table_ref",
+ "//deepmind/tensor:lua_tensor",
+ "//deepmind/tensor:tensor_view",
+ "//deepmind/util:files",
+ ],
+)
+
+cc_library(
+ name = "context_entities",
+ srcs = ["context_entities.cc"],
+ hdrs = ["context_entities.h"],
+ deps = [
+ "//deepmind/lua",
+ "//deepmind/lua:class",
+ "//deepmind/lua:n_results_or",
+ "//deepmind/lua:push",
+ "//deepmind/lua:read",
+ "//deepmind/lua:table_ref",
+ ],
+)
+
+cc_test(
+ name = "context_entities_test",
+ srcs = ["context_entities_test.cc"],
+ deps = [
+ ":context_entities",
+ "//deepmind/lua:bind",
+ "//deepmind/lua:call",
+ "//deepmind/lua:n_results_or_test_util",
+ "//deepmind/lua:push",
+ "//deepmind/lua:push_script",
+ "//deepmind/lua:read",
+ "//deepmind/lua:table_ref",
+ "//deepmind/lua:vm_test_util",
+ "@com_google_absl//absl/types:span",
+ "@com_google_googletest//:gtest_main",
+ ],
+)
+
+cc_library(
+ name = "context_observations",
+ srcs = ["context_observations.cc"],
+ hdrs = ["context_observations.h"],
+ deps = [
+ "//deepmind/lua",
+ "//deepmind/lua:call",
+ "//deepmind/lua:n_results_or",
+ "//deepmind/lua:push",
+ "//deepmind/lua:read",
+ "//deepmind/lua:table_ref",
+ "//deepmind/support:logging",
+ "//deepmind/tensor:lua_tensor",
+ "//deepmind/tensor:tensor_view",
+ "//third_party/rl_api:env_c_api",
+ ],
+)
+
+cc_library(
+ name = "context_pickups",
+ srcs = ["context_pickups.cc"],
+ hdrs = ["context_pickups.h"],
+ deps = [
+ "//deepmind/lua:call",
+ "//deepmind/lua:push",
+ "//deepmind/lua:read",
+ "//deepmind/lua:table_ref",
+ "//deepmind/support:logging",
],
)
@@ -30,14 +144,16 @@ cc_library(
name = "callbacks",
srcs = ["callbacks.cc"],
data = ["//:non_pk3_assets"],
- visibility = ["//:__pkg__"],
+ visibility = ["//visibility:public"],
deps = [
":context",
":lua_maze_generation",
":lua_random",
":lua_text_level_maker",
+ "//deepmind/include:context_hdrs",
"//deepmind/level_generation/text_level:lua_bindings",
"//deepmind/lua:vm",
+ "//deepmind/model_generation:lua_model",
"//deepmind/tensor:lua_tensor",
],
)
@@ -48,9 +164,10 @@ cc_test(
srcs = ["callbacks_test.cc"],
deps = [
":callbacks",
- "//deepmind/include:context_headers",
+ "//deepmind/include:context_hdrs",
+ "//deepmind/support:logging",
"//deepmind/support:test_srcdir",
- "@googletest//:gtest_main",
+ "@com_google_googletest//:gtest_main",
],
)
@@ -59,8 +176,10 @@ cc_library(
srcs = ["lua_maze_generation.cc"],
hdrs = ["lua_maze_generation.h"],
deps = [
+ ":lua_random",
"//deepmind/level_generation/text_level:char_grid",
"//deepmind/level_generation/text_maze_generation:algorithm",
+ "//deepmind/level_generation/text_maze_generation:flood_fill",
"//deepmind/level_generation/text_maze_generation:text_maze",
"//deepmind/lua",
"//deepmind/lua:bind",
@@ -79,11 +198,13 @@ cc_test(
srcs = ["lua_maze_generation_test.cc"],
deps = [
":lua_maze_generation",
+ ":lua_random",
+ "//deepmind/lua:bind",
"//deepmind/lua:call",
"//deepmind/lua:n_results_or_test_util",
"//deepmind/lua:push_script",
"//deepmind/lua:vm_test_util",
- "@googletest//:gtest_main",
+ "@com_google_googletest//:gtest_main",
],
)
@@ -91,6 +212,10 @@ cc_library(
name = "lua_random",
srcs = ["lua_random.cc"],
hdrs = ["lua_random.h"],
+ visibility = [
+ "//deepmind/include:__pkg__",
+ "//deepmind/tensor:__pkg__",
+ ],
deps = [
"//deepmind/lua",
"//deepmind/lua:class",
@@ -106,11 +231,12 @@ cc_test(
srcs = ["lua_random_test.cc"],
deps = [
":lua_random",
+ "//deepmind/lua:bind",
"//deepmind/lua:call",
"//deepmind/lua:n_results_or_test_util",
"//deepmind/lua:push_script",
"//deepmind/lua:vm_test_util",
- "@googletest//:gtest_main",
+ "@com_google_googletest//:gtest_main",
],
)
@@ -121,13 +247,49 @@ cc_library(
visibility = ["//deepmind:__subpackages__"],
deps = [
":lua_random",
+ "//:level_cache_types",
"//deepmind/level_generation:compile_map",
"//deepmind/level_generation/text_level:lua_bindings",
"//deepmind/lua",
+ "//deepmind/lua:call",
"//deepmind/lua:class",
"//deepmind/lua:n_results_or",
"//deepmind/lua:push",
"//deepmind/lua:read",
"//deepmind/lua:table_ref",
+ "//deepmind/util:files",
+ "@com_google_absl//absl/strings",
+ ],
+)
+
+cc_library(
+ name = "lua_image",
+ srcs = ["lua_image.cc"],
+ hdrs = ["lua_image.h"],
+ deps = [
+ "//deepmind/lua",
+ "//deepmind/lua:bind",
+ "//deepmind/lua:push",
+ "//deepmind/lua:read",
+ "//deepmind/lua:table_ref",
+ "//deepmind/tensor:lua_tensor",
+ "//deepmind/util:files",
+ "@com_google_absl//absl/strings",
+ "@png_archive//:png",
+ ],
+)
+
+cc_test(
+ name = "lua_image_test",
+ size = "small",
+ srcs = ["lua_image_test.cc"],
+ deps = [
+ ":lua_image",
+ "//deepmind/lua:call",
+ "//deepmind/lua:n_results_or_test_util",
+ "//deepmind/lua:push_script",
+ "//deepmind/lua:vm_test_util",
+ "//deepmind/tensor:lua_tensor",
+ "@com_google_googletest//:gtest_main",
],
)
diff --git a/deepmind/engine/callbacks.cc b/deepmind/engine/callbacks.cc
index 814505ca..4ecd5d4f 100644
--- a/deepmind/engine/callbacks.cc
+++ b/deepmind/engine/callbacks.cc
@@ -16,6 +16,8 @@
//
////////////////////////////////////////////////////////////////////////////////
+#include
+
#include
#include
#include
@@ -28,6 +30,7 @@
#include "deepmind/include/deepmind_context.h"
#include "deepmind/level_generation/text_level/lua_bindings.h"
#include "deepmind/lua/vm.h"
+#include "deepmind/model_generation/lua_model.h"
#include "deepmind/tensor/lua_tensor.h"
namespace lua = deepmind::lab::lua;
@@ -37,10 +40,15 @@ using deepmind::lab::LuaMazeGeneration;
using deepmind::lab::LuaRandom;
using deepmind::lab::LuaSnippetEmitter;
using deepmind::lab::LuaTextLevelMaker;
+using deepmind::lab::LuaModel;
extern "C" {
-int dmlab_create_context(const char* runfiles_path, DeepmindContext* ctx) {
+int dmlab_create_context(const char* runfiles_path, DeepmindContext* ctx,
+ bool (*file_reader_override)(const char* file_name,
+ char** buff,
+ size_t* size),
+ const char* temp_folder) {
lua::Vm lua_vm = lua::CreateVm();
lua_State* L = lua_vm.get();
tensor::LuaTensorRegister(L);
@@ -48,9 +56,10 @@ int dmlab_create_context(const char* runfiles_path, DeepmindContext* ctx) {
LuaRandom::Register(L);
LuaTextLevelMaker::Register(L);
LuaSnippetEmitter::Register(L);
+ LuaModel::Register(L);
- ctx->userdata =
- new Context(std::move(lua_vm), runfiles_path, &ctx->calls, &ctx->hooks);
+ ctx->userdata = new Context(std::move(lua_vm), runfiles_path, &ctx->calls,
+ &ctx->hooks, file_reader_override, temp_folder);
return 0;
}
diff --git a/deepmind/engine/callbacks_test.cc b/deepmind/engine/callbacks_test.cc
index 38ee0abd..f92514a0 100644
--- a/deepmind/engine/callbacks_test.cc
+++ b/deepmind/engine/callbacks_test.cc
@@ -30,8 +30,10 @@ using ::testing::ElementsAreArray;
TEST(DeepmindCallbackTest, CreateAndDestroyContext) {
DeepmindContext ctx{};
const char arg0[] = "dmlab";
- ASSERT_EQ(0, dmlab_create_context(TestSrcDir().c_str(), &ctx));
- ASSERT_EQ(0, ctx.hooks.set_script_name(ctx.userdata, "tests/callbacks_test"));
+ ASSERT_EQ(0,
+ dmlab_create_context(TestSrcDir().c_str(), &ctx, nullptr, nullptr));
+ ASSERT_EQ(
+ 0, ctx.hooks.set_script_name(ctx.userdata, "test_levels/callbacks_test"));
ctx.hooks.add_setting(ctx.userdata, "command", "hello");
ASSERT_EQ(0, ctx.hooks.init(ctx.userdata));
@@ -48,9 +50,10 @@ TEST(DeepmindCallbackTest, CreateAndDestroyContext) {
TEST(DeepmindCallbackTest, CustomObservations) {
DeepmindContext ctx{};
- const char callbacks_test[] = "tests/callbacks_test";
+ const char callbacks_test[] = "test_levels/callbacks_test";
const char order[] = "Find Apples!";
- ASSERT_EQ(0, dmlab_create_context(TestSrcDir().c_str(), &ctx));
+ ASSERT_EQ(0,
+ dmlab_create_context(TestSrcDir().c_str(), &ctx, nullptr, nullptr));
ctx.hooks.add_setting(ctx.userdata, "order", order);
ASSERT_EQ(0, ctx.hooks.set_script_name(ctx.userdata, callbacks_test));
ASSERT_EQ(0, ctx.hooks.init(ctx.userdata));
@@ -100,4 +103,36 @@ TEST(DeepmindCallbackTest, CustomObservations) {
dmlab_release_context(&ctx);
}
+TEST(DeepmindCallbackTest, CreateModel) {
+ DeepmindContext ctx{};
+ ASSERT_EQ(0,
+ dmlab_create_context(TestSrcDir().c_str(), &ctx, nullptr, nullptr));
+ ASSERT_EQ(
+ 0, ctx.hooks.set_script_name(ctx.userdata, "test_levels/callbacks_test"));
+ ASSERT_EQ(0, ctx.hooks.init(ctx.userdata));
+
+ ASSERT_TRUE(ctx.hooks.find_model(ctx.userdata, "cube"));
+ DeepmindModelGetters model;
+ void* model_data;
+ ctx.hooks.model_getters(ctx.userdata, &model, &model_data);
+
+ ASSERT_EQ(model.get_surface_count(model_data), 1);
+
+ ASSERT_EQ(model.get_surface_vertex_count(model_data, 0), 24);
+ ASSERT_EQ(model.get_surface_face_count(model_data, 0), 12);
+
+ float vertex_normal[3];
+ model.get_surface_vertex_normal(model_data, 0, 12, vertex_normal);
+ EXPECT_THAT(vertex_normal, ElementsAre(-1.0, 0.0, 0.0));
+ model.get_surface_vertex_normal(model_data, 0, 23, vertex_normal);
+ EXPECT_THAT(vertex_normal, ElementsAre(0.0, -1.0, 0.0));
+
+ int face_indices[3];
+ model.get_surface_face(model_data, 0, 11, face_indices);
+ EXPECT_THAT(face_indices, ElementsAre(20, 22, 23));
+
+ ctx.hooks.clear_model(ctx.userdata);
+ dmlab_release_context(&ctx);
+}
+
} // namespace
diff --git a/deepmind/engine/context.cc b/deepmind/engine/context.cc
index 97e9ee8b..d2792161 100644
--- a/deepmind/engine/context.cc
+++ b/deepmind/engine/context.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 Google Inc.
+// Copyright (C) 2016-2017 Google Inc.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -20,21 +20,40 @@
#include
#include
+#include
+#include
+#include
#include
+#include
#include
#include
#include
+#include
#include "deepmind/support/logging.h"
-#include "deepmind/engine/lua_random.h"
+#include "absl/strings/str_cat.h"
+#include "deepmind/engine/lua_image.h"
#include "deepmind/engine/lua_maze_generation.h"
+#include "deepmind/engine/lua_random.h"
#include "deepmind/engine/lua_text_level_maker.h"
+#include "deepmind/include/deepmind_model_getters.h"
+#include "deepmind/level_generation/compile_map.h"
#include "deepmind/lua/bind.h"
#include "deepmind/lua/call.h"
#include "deepmind/lua/class.h"
+#include "deepmind/lua/lua.h"
+#include "deepmind/lua/push.h"
#include "deepmind/lua/push_script.h"
+#include "deepmind/lua/read.h"
#include "deepmind/lua/table_ref.h"
+#include "deepmind/model_generation/lua_model.h"
+#include "deepmind/model_generation/lua_transform.h"
+#include "deepmind/model_generation/model_getters.h"
+#include "deepmind/model_generation/model_lua.h"
#include "deepmind/tensor/lua_tensor.h"
+#include "deepmind/tensor/tensor_view.h"
+#include "public/level_cache_types.h"
+#include "third_party/rl_api/env_c_api.h"
using deepmind::lab::Context;
@@ -43,6 +62,13 @@ static void add_setting(void* userdata, const char* key, const char* value) {
return static_cast(userdata)->AddSetting(key, value);
}
+static void set_level_cache_settings(
+ void* userdata, bool local, bool global,
+ DeepMindLabLevelCacheParams level_cache_params) {
+ return static_cast(userdata)->SetLevelCacheSetting(
+ local, global, level_cache_params);
+}
+
static int set_script_name(void* userdata, const char* script_name) {
return static_cast(userdata)->SetScriptName(script_name);
}
@@ -55,26 +81,70 @@ static int start(void* userdata, int episode, int seed) {
return static_cast(userdata)->Start(episode, seed);
}
+static const char* error_message(void* userdata) {
+ return static_cast(userdata)->ErrorMessage();
+}
+
+static void set_error_message(void* userdata, const char* error_message) {
+ static_cast(userdata)->SetErrorMessage(error_message);
+}
+
+static int map_loaded(void* userdata) {
+ return static_cast(userdata)->MapLoaded();
+}
+
static const char* replace_command_line(void* userdata,
const char* old_commandline) {
return static_cast(userdata)->GetCommandLine(old_commandline);
}
+static const char* get_temporary_folder(void* userdata) {
+ return static_cast(userdata)->TempDirectory().c_str();
+}
+
static const char* next_map(void* userdata) {
return static_cast(userdata)->NextMap();
}
+void update_inventory(void* userdata, bool is_spawning, int player_id,
+ int gadget_count, int gadget_inventory[], int stat_count,
+ int stat_inventory[], int powerup_count,
+ int powerup_time[], int gadget_held, float height,
+ float position[3], float view_angles[3]) {
+ static_cast(userdata)->UpdateInventory(
+ is_spawning, player_id, gadget_count, gadget_inventory, stat_count,
+ stat_inventory, powerup_count, powerup_time, gadget_held, height,
+ position, view_angles);
+}
+
+static int game_type(void* userdata) {
+ return static_cast(userdata)->GameType();
+}
+
+static char team_select(void* userdata, int player_id,
+ const char* player_name) {
+ return static_cast(userdata)->TeamSelect(player_id, player_name);
+}
+
+static bool has_alt_cameras(void* userdata) {
+ return static_cast(userdata)->HasAltCameras();
+}
+
+static void set_has_alt_cameras(void* userdata, bool has_alt_camera) {
+ static_cast(userdata)->SetHasAltCameras(has_alt_camera);
+}
+
static int run_lua_snippet(void* userdata, const char* command) {
std::size_t command_len = std::strlen(command);
return static_cast(userdata)->RunLuaSnippet(command, command_len);
}
-static void set_use_internal_controls(void* userdata, bool v) {
- return static_cast(userdata)->SetUseInternalControls(v);
+static void set_native_app(void* userdata, bool v) {
+ return static_cast(userdata)->SetNativeApp(v);
}
-static bool get_use_internal_controls(void* userdata) {
- return static_cast(userdata)->UseInternalControls();
+static bool get_native_app(void* userdata) {
+ return static_cast(userdata)->NativeApp();
}
static void set_actions(void* userdata, double look_down_up,
@@ -98,16 +168,29 @@ static void get_actions(void* userdata, double* look_down_up,
static int update_spawn_vars(void* userdata, char* spawn_var_chars,
int* num_spawn_var_chars,
int spawn_var_offsets[][2], int* num_spawn_vars) {
- return static_cast(userdata)->UpdateSpawnVars(
+ return static_cast(userdata)->MutablePickups()->UpdateSpawnVars(
spawn_var_chars, num_spawn_var_chars, spawn_var_offsets, num_spawn_vars);
}
+static int make_extra_entities(void* userdata) {
+ return static_cast(userdata)->MutablePickups()->MakeExtraEntities();
+}
+
+static void read_extra_entity(void* userdata, int entity_index,
+ char* spawn_var_chars, int* num_spawn_var_chars,
+ int spawn_var_offsets[][2], int* num_spawn_vars) {
+ return static_cast(userdata)->MutablePickups()->ReadExtraEntity(
+ entity_index, spawn_var_chars, num_spawn_var_chars, spawn_var_offsets,
+ num_spawn_vars);
+}
+
static bool find_item(void* userdata, const char* class_name, int* index) {
- return static_cast(userdata)->FindItem(class_name, index);
+ return static_cast(userdata)->MutablePickups()->FindItem(class_name,
+ index);
}
static int item_count(void* userdata) {
- return static_cast(userdata)->ItemCount();
+ return static_cast(userdata)->Pickups().ItemCount();
}
static bool item(void* userdata, int index,
@@ -115,33 +198,69 @@ static bool item(void* userdata, int index,
char* class_name, int max_class_name,
char* model_name, int max_model_name,
int* quantity, int* type, int* tag) {
- return static_cast(userdata)->GetItem(
+ return static_cast(userdata)->Pickups().GetItem(
index, item_name, max_item_name, class_name, max_class_name, model_name,
max_model_name, quantity, type, tag);
}
static void clear_items(void* userdata) {
- static_cast(userdata)->ClearItems();
+ static_cast(userdata)->MutablePickups()->ClearItems();
+}
+
+static bool find_model(void* userdata, const char* model_name) {
+ return static_cast(userdata)->FindModel(model_name);
+}
+
+static void model_getters(void* userdata, DeepmindModelGetters* model_getters,
+ void** model_data) {
+ static_cast(userdata)->GetModelGetters(model_getters, model_data);
+}
+
+static void clear_model(void* userdata) {
+ static_cast(userdata)->ClearModel();
}
static bool map_finished(void* userdata) {
- return static_cast(userdata)->MapFinished();
+ return static_cast(userdata)->Game().MapFinished();
}
static void set_map_finished(void* userdata, bool map_finished) {
- static_cast(userdata)->SetMapFinished(map_finished);
+ static_cast(userdata)->MutableGame()->SetMapFinished(map_finished);
}
static bool can_pickup(void* userdata, int entity_id) {
- return static_cast(userdata)->CanPickup(entity_id);
+ return static_cast(userdata)->MutablePickups()->CanPickup(
+ entity_id);
}
static bool override_pickup(void* userdata, int entity_id, int* respawn) {
- return static_cast(userdata)->OverridePickup(entity_id, respawn);
+ return static_cast(userdata)->MutablePickups()->OverridePickup(
+ entity_id, respawn);
}
-static int external_reward(void* userdata, int player_id) {
- return static_cast(userdata)->ExternalReward(player_id);
+static bool can_trigger(void* userdata, int entity_id,
+ const char* target_name) {
+ return static_cast(userdata)->CanTrigger(entity_id, target_name);
+}
+
+static bool override_trigger(void* userdata, int entity_id,
+ const char* target_name) {
+ return static_cast(userdata)->OverrideTrigger(entity_id,
+ target_name);
+}
+
+static void trigger_lookat(void* userdata, int entity_id, bool looked_at,
+ const float position[3]) {
+ static_cast(userdata)->TriggerLookat(entity_id, looked_at,
+ position);
+}
+
+static int reward_override(void* userdata, const char* reason_opt,
+ int player_id, int team,
+ const int* other_player_id_opt,
+ const float* origin_opt, int score) {
+ return static_cast(userdata)->RewardOverride(
+ reason_opt, player_id, team, other_player_id_opt, origin_opt, score);
}
static void add_score(void* userdata, int player_id, double reward) {
@@ -162,211 +281,260 @@ static void add_bots(void* userdata) {
return static_cast(userdata)->AddBots();
}
-static void modify_rgba_texture(void* userdata, const char* name,
+static bool modify_rgba_texture(void* userdata, const char* name,
unsigned char* data, int width, int height) {
- static_cast(userdata)->ModifyRgbaTexture(name, data, width, height);
+ return static_cast(userdata)->ModifyRgbaTexture(name, data, width,
+ height);
+}
+
+static bool replace_model_name(void* userdata, const char* name, char* new_name,
+ int new_name_size, char* texture_prefix,
+ int texture_prefix_size) {
+ return static_cast(userdata)->ReplaceModelName(
+ name, new_name, new_name_size, texture_prefix, texture_prefix_size);
+}
+
+static bool replace_texture_name(void* userdata, const char* name,
+ char* new_name, int max_size) {
+ return static_cast(userdata)->ReplaceTextureName(name, new_name,
+ max_size);
+}
+
+static bool load_texture(void* userdata, const char* name,
+ unsigned char** pixels, int* width, int* height,
+ void* (*allocator)(int size)) {
+ return static_cast(userdata)->LoadTexture(name, pixels, width,
+ height, allocator);
}
static int custom_observation_count(void* userdata) {
- return static_cast(userdata)->CustomObservationCount();
+ return static_cast(userdata)->Observations().Count();
}
static const char* custom_observation_name(void* userdata, int idx) {
- return static_cast(userdata)->CustomObservationName(idx);
+ return static_cast(userdata)->Observations().Name(idx);
}
static void custom_observation_spec(void* userdata, int idx,
EnvCApi_ObservationSpec* spec) {
- static_cast(userdata)->CustomObservationSpec(idx, spec);
+ static_cast(userdata)->Observations().Spec(idx, spec);
}
static void custom_observation(void* userdata, int idx,
EnvCApi_Observation* observation) {
- return static_cast(userdata)->CustomObservation(idx, observation);
+ return static_cast(userdata)->MutableObservations()->Observation(
+ idx, observation);
}
-void predicted_player_state(void* userdata, const float origin[3],
- const float velocity[3], const float viewangles[3],
- float height, int timestamp_msec) {
- return static_cast(userdata)->SetPredictPlayerState(
- origin, velocity, viewangles, height, timestamp_msec);
+static void player_state(void* userdata, const float origin[3],
+ const float velocity[3], const float viewangles[3],
+ float height, int team_score, int other_team_score,
+ int player_id, int timestamp_msec) {
+ return static_cast(userdata)->MutableGame()->SetPlayerState(
+ origin, velocity, viewangles, height, team_score, other_team_score,
+ player_id, timestamp_msec);
}
-int make_screen_messages(void* userdata, int screen_width, int screen_height,
- int line_height, int string_buffer_size) {
- return static_cast(userdata)->MakeScreenMesages(
+static int make_screen_messages(void* userdata, int screen_width,
+ int screen_height, int line_height,
+ int string_buffer_size) {
+ return static_cast(userdata)->MakeScreenMessages(
screen_width, screen_height, line_height, string_buffer_size);
}
-void get_screen_message(void* userdata, int message_id, char* buffer, int* x,
- int* y, int* align_l0_r1_c2) {
- static_cast(userdata)->GetScreenMessage(message_id, buffer, x, y,
- align_l0_r1_c2);
+static void get_screen_message(void* userdata, int message_id, char* buffer,
+ int* x, int* y, int* align_l0_r1_c2,
+ int* shadow, float rgba[4]) {
+ static_cast(userdata)->GetScreenMessage(
+ message_id, buffer, x, y, align_l0_r1_c2, shadow, rgba);
}
-} // extern "C"
+static int make_filled_rectangles(void* userdata, int screen_width,
+ int screen_height) {
+ return static_cast(userdata)->MakeFilledRectangles(screen_width,
+ screen_height);
+}
-namespace deepmind {
-namespace lab {
-namespace {
+static void get_filled_rectangle(void* userdata, int rectangle_id, int* x,
+ int* y, int* width, int* height,
+ float rgba[4]) {
+ static_cast(userdata)->GetFilledRectangle(rectangle_id, x, y, width,
+ height, rgba);
+}
-class LuaGameModule : public lua::Class {
- friend class Class;
- static const char* ClassName() { return "deepmind.lab.Game"; }
-
- public:
- // '*ctx' owned by the caller and should out-live this object.
- explicit LuaGameModule(Context* ctx) : ctx_(ctx) {}
-
- static void Register(lua_State* L) {
- const Class::Reg methods[] = {
- {"addScore", Member<&LuaGameModule::AddScore>},
- {"finishMap", Member<&LuaGameModule::FinishMap>},
- {"playerInfo", Member<&LuaGameModule::PlayerInfo>},
- };
- Class::Register(L, methods);
- }
+static void lua_mover(void* userdata, int entity_id,
+ const float entity_position[3],
+ const float player_position[3],
+ const float player_velocity[3],
+ float player_position_delta[3],
+ float player_velocity_delta[3]) {
+ static_cast(userdata)->CustomPlayerMovement(
+ entity_id, entity_position, player_position, player_velocity,
+ player_position_delta, player_velocity_delta);
+}
- private:
- lua::NResultsOr AddScore(lua_State* L) {
- int player_id = 0;
- double score = 0;
- if (lua::Read(L, 2, &player_id) && lua::Read(L, 3, &score) &&
- 0 <= player_id && player_id < 64) {
- ctx_->Calls()->add_score(player_id, score);
- return 0;
- }
- std::string error = "Invalid arguments player_id: ";
- error += lua::ToString(L, 2);
- error += " or reward: ";
- error += lua::ToString(L, 3);
- return std::move(error);
- }
+static void game_event(void* userdata, const char* event_name, int count,
+ const float* data) {
+ static_cast(userdata)->GameEvent(event_name, count, data);
+}
- lua::NResultsOr FinishMap(lua_State* L) {
- ctx_->SetMapFinished(true);
- return 0;
- }
+static void make_pk3_from_map(void* userdata, const char* map_path,
+ const char* map_name, bool gen_aas) {
+ static_cast(userdata)->MakePk3FromMap(map_path, map_name, gen_aas);
+}
- lua::NResultsOr PlayerInfo(lua_State* L) {
- const auto& pv = ctx_->GetPredictPlayerView();
- auto table = lua::TableRef::Create(L);
- table.Insert("pos", pv.pos);
- table.Insert("vel", pv.vel);
- table.Insert("angles", pv.angles);
- table.Insert("anglesVel", pv.anglesVel);
- table.Insert("height", pv.height);
- lua::Push(L, table);
- return 1;
- }
+static void events_clear(void* userdata) {
+ static_cast(userdata)->MutableEvents()->Clear();
+}
- Context* ctx_;
-};
+static int events_type_count(void* userdata) {
+ return static_cast(userdata)->Events().TypeCount();
+}
-constexpr char kGameScriptPath[] = "/baselab/game_scripts";
-constexpr int kMaxSpawnVars = 64;
-constexpr int kMaxSpawnVarChars = 4096;
-constexpr double kDefaultEpisodeLengthSeconds = 5 * 30.0;
+static const char* events_type_name(void* userdata, int event_type_idx) {
+ return static_cast(userdata)->Events().TypeName(
+ event_type_idx);
+}
-// If the string "arg" fits into the array pointed to by "dest" (including the
-// null terminator), copies the string into the array and returns true;
-// otherwise returns false.
-bool StringCopy(const std::string& arg, char* dest, std::size_t max_size) {
- auto len = arg.length() + 1;
- if (len <= max_size) {
- std::copy_n(arg.c_str(), len, dest);
- return true;
- } else {
- return false;
- }
+static int events_count(void* userdata) {
+ return static_cast(userdata)->Events().Count();
}
-lua::NResultsOr MapMakerModule(lua_State* L) {
- if (auto* ctx =
- static_cast(lua_touserdata(L, lua_upvalueindex(1)))) {
- LuaTextLevelMaker::CreateObject(L, ctx->ExecutableRunfiles());
- return 1;
- } else {
- return "Missing context!";
- }
+static void events_export(void* userdata, int event_idx, EnvCApi_Event* event) {
+ static_cast(userdata)->MutableEvents()->Export(event_idx, event);
}
-lua::NResultsOr GameModule(lua_State* L) {
+static void entities_clear(void* userdata) {
+ static_cast(userdata)->MutableGameEntities()->Clear();
+}
+
+static void entities_add(void* userdata, int entity_id, int user_id,
+ int type, int flags, float position[3],
+ const char* classname) {
+ static_cast(userdata)->MutableGameEntities()->Add(
+ entity_id, user_id, type, flags, position, classname);
+}
+
+} // extern "C"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+constexpr char kGameScriptPath[] = "/baselab/game_scripts";
+constexpr double kDefaultEpisodeLengthSeconds = 5 * 30.0;
+
+constexpr const char* const kTeamNames[] = {
+ "free",
+ "red",
+ "blue",
+ "spectator"
+};
+
+lua::NResultsOr MapMakerModule(lua_State* L) {
if (auto* ctx =
static_cast(lua_touserdata(L, lua_upvalueindex(1)))) {
- LuaGameModule::Register(L);
- LuaGameModule::CreateObject(L, ctx);
+ LuaTextLevelMaker::CreateObject(
+ L, ctx->ExecutableRunfiles(), ctx->TempDirectory(),
+ ctx->UseLocalLevelCache(), ctx->UseGlobalLevelCache(),
+ ctx->LevelCacheParams());
return 1;
} else {
return "Missing context!";
}
}
-lua::NResultsOr RandomModule(lua_State* L) {
- if (auto* ctx =
- static_cast(lua_touserdata(L, lua_upvalueindex(1)))) {
- LuaRandom::CreateObject(L, ctx->UserPrbg());
+lua::NResultsOr ModelModule(lua_State* L) {
+ if (const auto* calls = static_cast(
+ lua_touserdata(L, lua_upvalueindex(1)))) {
+ LuaModel::CreateObject(L, calls);
return 1;
} else {
return "Missing context!";
}
}
-// Returns the unique value in the range [-180, 180) that is equivalent to
-// 'angle', where two values x and y are considered equivalent whenever x - y
-// is an integral multiple of 360. (Note: the result may be meaningless if the
-// magnitude of 'angle' is very large.)
-double CanonicalAngle360(double angle) {
- const double n = std::floor((angle + 180.0) / 360.0);
- return angle - n * 360.0;
-}
-
} // namespace
Context::Context(lua::Vm lua_vm, const char* executable_runfiles,
- const DeepmindCalls* calls, DeepmindHooks* hooks)
+ const DeepmindCalls* calls, DeepmindHooks* hooks,
+ bool (*file_reader_override)(const char* file_name,
+ char** buff, size_t* size),
+ const char* temp_folder)
: lua_vm_(std::move(lua_vm)),
- executable_runfiles_(executable_runfiles),
- deepmind_calls_(calls),
- use_internal_controls_(false),
+ native_app_(false),
actions_{},
- map_finished_(false),
- random_seed_(0),
- predicted_player_view_{} {
+ level_cache_params_{},
+ game_(executable_runfiles, calls, file_reader_override,
+ temp_folder != nullptr ? temp_folder : ""),
+ has_alt_cameras_(false) {
CHECK(lua_vm_ != nullptr);
hooks->add_setting = add_setting;
+ hooks->set_level_cache_settings = set_level_cache_settings;
hooks->set_script_name = set_script_name;
hooks->start = start;
+ hooks->map_loaded = map_loaded;
hooks->init = init;
+ hooks->error_message = error_message;
+ hooks->set_error_message = set_error_message;
hooks->replace_command_line = replace_command_line;
hooks->next_map = next_map;
+ hooks->game_type = game_type;
+ hooks->team_select = team_select;
hooks->run_lua_snippet = run_lua_snippet;
- hooks->set_use_internal_controls = set_use_internal_controls;
- hooks->get_use_internal_controls = get_use_internal_controls;
+ hooks->set_native_app = set_native_app;
+ hooks->get_native_app = get_native_app;
hooks->set_actions = set_actions;
hooks->get_actions = get_actions;
hooks->update_spawn_vars = update_spawn_vars;
+ hooks->make_extra_entities = make_extra_entities;
+ hooks->read_extra_entity = read_extra_entity;
hooks->find_item = find_item;
hooks->item_count = item_count;
hooks->item = item;
hooks->clear_items = clear_items;
+ hooks->find_model = find_model;
+ hooks->model_getters = model_getters;
+ hooks->clear_model = clear_model;
hooks->map_finished = map_finished;
hooks->set_map_finished = set_map_finished;
hooks->can_pickup = can_pickup;
hooks->override_pickup = override_pickup;
- hooks->external_reward = external_reward;
+ hooks->can_trigger = can_trigger;
+ hooks->override_trigger = override_trigger;
+ hooks->trigger_lookat = trigger_lookat;
+ hooks->reward_override = reward_override;
hooks->add_score = add_score;
hooks->make_random_seed = make_random_seed;
hooks->has_episode_finished = has_episode_finished;
hooks->add_bots = add_bots;
+ hooks->replace_model_name = replace_model_name;
+ hooks->replace_texture_name = replace_texture_name;
+ hooks->load_texture = load_texture;
hooks->modify_rgba_texture = modify_rgba_texture;
hooks->custom_observation_count = custom_observation_count;
hooks->custom_observation_name = custom_observation_name;
hooks->custom_observation_spec = custom_observation_spec;
hooks->custom_observation = custom_observation;
- hooks->predicted_player_state = predicted_player_state;
+ hooks->player_state = player_state;
hooks->make_screen_messages = make_screen_messages;
hooks->get_screen_message = get_screen_message;
+ hooks->make_filled_rectangles = make_filled_rectangles;
+ hooks->get_filled_rectangle = get_filled_rectangle;
+ hooks->get_temporary_folder = get_temporary_folder;
+ hooks->make_pk3_from_map = make_pk3_from_map;
+ hooks->lua_mover = lua_mover;
+ hooks->game_event = game_event;
+ hooks->events.clear = events_clear;
+ hooks->events.type_count = events_type_count;
+ hooks->events.type_name = events_type_name;
+ hooks->events.count = events_count;
+ hooks->events.export_event = events_export;
+ hooks->entities.clear = entities_clear;
+ hooks->entities.add = entities_add;
+ hooks->update_inventory = update_inventory;
+ hooks->set_has_alt_cameras = set_has_alt_cameras;
+ hooks->has_alt_cameras = has_alt_cameras;
}
void Context::AddSetting(const char* key, const char* value) {
@@ -375,49 +543,60 @@ void Context::AddSetting(const char* key, const char* value) {
lua::NResultsOr Context::PushScript() {
lua_State* L = lua_vm_.get();
- if (script_name_.empty()) {
+ if (script_path_.empty()) {
return "Must have level script!";
}
- if (script_name_.length() > 4 &&
- // size_t pos, size_t len, const char* s, size_t n
- script_name_.compare(script_name_.length() - 4, 4, ".lua", 4) == 0) {
- return lua::PushScriptFile(L, script_name_);
- } else {
- std::string script_path = executable_runfiles_;
- script_path += kGameScriptPath;
- script_path += "/";
- script_path += script_name_;
- script_path += ".lua";
- return lua::PushScriptFile(L, script_path);
- }
+ return lua::PushScriptFile(L, script_path_);
}
-int Context::SetScriptName(const char* script_name) {
- script_name_ = script_name;
+int Context::SetScriptName(std::string script_name) {
+ if (script_name.length() > 4 &&
+ // size_t pos, size_t len, const char* s, size_t n
+ script_name.compare(script_name.length() - 4, 4, ".lua", 4) == 0) {
+ script_path_ = std::move(script_name);
+ } else if (!script_name.empty()) {
+ script_path_ = absl::StrCat(ExecutableRunfiles(), kGameScriptPath, "/",
+ script_name, ".lua");
+ }
auto result = PushScript();
lua_State* L = lua_vm_.get();
if (result.ok()) {
lua_pop(L, result.n_results());
return 0;
} else {
- std::cerr << "Level not found: " << result.error() << std::endl;
+ error_message_ = absl::StrCat("Level not found: ", result.error());
return 1;
}
}
int Context::Init() {
+ // Clear texture handles from previous levels and initialise temp folder.
+ int init_code = MutableGame()->Init();
+ if (init_code != 0) {
+ return init_code;
+ }
+
lua_State* L = lua_vm_.get();
auto result = PushScript();
if (!result.ok()) {
- std::cerr << result.error() << std::endl;
+ error_message_ = result.error();
return 1;
}
- std::string scripts_folder = executable_runfiles_;
- scripts_folder += kGameScriptPath;
+ std::string scripts_folder =
+ absl::StrCat(ExecutableRunfiles(), kGameScriptPath);
+
lua_vm_.AddPathToSearchers(scripts_folder);
+ // Add the current running script to the search path.
+ auto last_slash_pos = script_path_.find_last_of('/');
+ if (last_slash_pos != std::string::npos) {
+ auto running_script_folder = script_path_.substr(0, last_slash_pos);
+ lua_vm_.AddPathToSearchers(running_script_folder);
+ }
+
+ lua_vm_.AddCModuleToSearchers("dmlab.system.image", LuaImageRequire);
lua_vm_.AddCModuleToSearchers(
"dmlab.system.tensor", tensor::LuaTensorConstructors);
lua_vm_.AddCModuleToSearchers(
@@ -425,48 +604,76 @@ int Context::Init() {
lua_vm_.AddCModuleToSearchers(
"dmlab.system.map_maker", &lua::Bind, {this});
lua_vm_.AddCModuleToSearchers(
- "dmlab.system.game", &lua::Bind, {this});
+ "dmlab.system.game", &lua::Bind, {MutableGame()});
+ lua_vm_.AddCModuleToSearchers(
+ "dmlab.system.events", &lua::Bind,
+ {MutableEvents()});
+ lua_vm_.AddCModuleToSearchers("dmlab.system.game_entities",
+ &lua::Bind,
+ {MutableGameEntities()});
lua_vm_.AddCModuleToSearchers(
- "dmlab.system.random", &lua::Bind, {this});
+ "dmlab.system.random", &lua::Bind, {UserPrbg()});
+ lua_vm_.AddCModuleToSearchers(
+ "dmlab.system.model", &lua::Bind,
+ {const_cast(Game().Calls())});
+ lua_vm_.AddCModuleToSearchers(
+ "dmlab.system.transform", LuaTransform::Require);
- lua::Push(L, script_name_);
+ lua::Push(L, script_path_);
result = lua::Call(L, 1);
if (!result.ok()) {
- std::cerr << result.error() << std::endl;
+ error_message_ = result.error();
return 1;
}
if (result.n_results() != 1) {
- std::cerr
- << "Lua script must return only a table or userdata with metatable.";
+ error_message_ =
+ "Lua script must return only a table or userdata with metatable.";
lua_pop(L, result.n_results());
return 1;
}
if (!lua::Read(L, -1, &script_table_ref_)) {
- auto actual = lua::ToString(L, -1);
- std::cerr << "Lua script must return a table or userdata with metatable. "
- "Actually returned : '"
- << actual << "'" << std::endl;
+ error_message_ = absl::StrCat(
+ "Lua script must return a table or userdata with metatable. Actually "
+ "returned : '",
+ lua::ToString(L, -1), "'");
lua_pop(L, result.n_results());
return 1;
}
lua_pop(L, result.n_results());
int err = CallInit();
if (err != 0) return err;
- return CallObservationSpec();
+
+ MutablePickups()->SetScriptTableRef(script_table_ref_);
+ return MutableObservations()->ReadSpec(script_table_ref_);
}
int Context::Start(int episode, int seed) {
- predicted_player_view_.timestamp_msec = 0;
- random_seed_ = seed;
EnginePrbg()->seed(seed);
+ MutableGame()->NextMap();
lua_State* L = lua_vm_.get();
script_table_ref_.PushMemberFunction("start");
if (!lua_isnil(L, -2)) {
lua::Push(L, episode);
- lua::Push(L, static_cast(seed));
+ lua::Push(L, static_cast(MakeRandomSeed()));
auto result = lua::Call(L, 3);
if (!result.ok()) {
- std::cerr << result.error() << std::endl;
+ error_message_ = result.error();
+ return 1;
+ }
+ lua_pop(L, result.n_results());
+ } else {
+ lua_pop(L, 2);
+ }
+ return 0;
+}
+
+int Context::MapLoaded() {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("mapLoaded");
+ if (!lua_isnil(L, -2)) {
+ auto result = lua::Call(L, 1);
+ if (!result.ok()) {
+ error_message_ = result.error();
return 1;
}
lua_pop(L, result.n_results());
@@ -540,11 +747,109 @@ const char* Context::NextMap() {
CHECK_EQ(1, result.n_results()) << "'nextMap' must return one string.";
CHECK(lua::Read(L, -1, &map_name_))
<< "'nextMap' must return one string: Found " << lua::ToString(L, -1);
- predicted_player_view_.timestamp_msec = 0;
+ MutableGame()->NextMap();
lua_pop(L, result.n_results());
return map_name_.c_str();
}
+void Context::UpdateInventory(bool is_spawning, int player_id, int gadget_count,
+ int gadget_inventory[], int stat_count,
+ int stat_inventory[], int powerup_count,
+ int powerup_time[], int gadget_held, float height,
+ float position[3], float view_angles[3]) {
+ const char* update_type = is_spawning ? "spawnInventory" : "updateInventory";
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction(update_type);
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return;
+ }
+
+ auto table = lua::TableRef::Create(L);
+ table.Insert("playerId", player_id + 1);
+ table.Insert("amounts", absl::MakeConstSpan(gadget_inventory, gadget_count));
+ table.Insert("stats", absl::MakeConstSpan(stat_inventory, stat_count));
+ table.Insert("powerups", absl::MakeConstSpan(powerup_time, powerup_count));
+ table.Insert("position", absl::MakeConstSpan(position, 3));
+ table.Insert("angles", absl::MakeConstSpan(view_angles, 3));
+ table.Insert("height", height);
+ table.Insert("gadget", gadget_held + 1);
+ lua::Push(L, table);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[" << update_type << "] - " << result.error();
+ if (result.n_results() > 0) {
+ CHECK_EQ(1, result.n_results())
+ << "[" << update_type << "] - Must return table or nil!";
+ if (!lua_isnil(L, -1)) {
+ CHECK(lua::Read(L, -1, &table))
+ << "[" << update_type << "] - Must return table or nil!";
+ CHECK(table.LookUp("amounts",
+ absl::MakeSpan(gadget_inventory, gadget_count)))
+ << "[" << update_type << "] - Table missing 'amounts'!";
+ CHECK(table.LookUp("stats", absl::MakeSpan(stat_inventory, stat_count)))
+ << "[" << update_type << "] - Table missing 'stats'!";
+ }
+ lua_pop(L, result.n_results());
+ }
+}
+
+int Context::GameType() {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("gameType");
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return 0; // GT_FFA from bg_public.
+ } else {
+ auto result = lua::Call(L, 1);
+ CHECK(result.ok()) << "[gameType] - " << result.error();
+ int game_type = 0;
+ CHECK(lua::Read(L, -1, &game_type))
+ << "[gameType] - must return integer; actual \"" << lua::ToString(L, -1)
+ << "\"";
+ CHECK_LT(game_type, 8)
+ << "[gameType] - must return integer less than 8; actual \""
+ << game_type << "\"";
+ return game_type;
+ }
+}
+
+char Context::TeamSelect(int player_id, const char* player_name) {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("team");
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return '\0';
+ }
+
+ lua::Push(L, player_id + 1);
+ lua::Push(L, player_name);
+
+ auto result = lua::Call(L, 3);
+ CHECK(result.ok()) << result.error();
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return '\0';
+ }
+ CHECK_EQ(1, result.n_results()) << "[team] - must return one string.";
+ std::string team;
+ CHECK(lua::Read(L, -1, &team)) << "[team] - must return one string: Found \""
+ << lua::ToString(L, -1) << "\"";
+
+ lua_pop(L, result.n_results());
+ CHECK(!team.empty()) << "[team] - must return one character or nil: Found \""
+ << lua::ToString(L, -1) << "\"";
+
+ const char kTeam[] = "pbrs";
+ auto ret = std::find_if(std::begin(kTeam), std::end(kTeam),
+ [&team](char t) { return team[0] == t; });
+ if (ret != std::end(kTeam)) {
+ return *ret;
+ }
+
+ LOG(FATAL) << "[team] - must return one of 'r', 'b', 'p' and 's'; actual"
+ << lua::ToString(L, -1);
+}
+
void Context::SetActions(double look_down_up, double look_left_right,
signed char move_back_forward,
signed char strafe_left_right, signed char crouch_jump,
@@ -561,6 +866,36 @@ void Context::GetActions(double* look_down_up, double* look_left_right,
signed char* move_back_forward,
signed char* strafe_left_right,
signed char* crouch_jump, int* buttons_down) {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("modifyControl");
+ // Check function exists.
+ if (!lua_isnil(L, -2)) {
+ auto table = lua::TableRef::Create(L);
+ table.Insert("lookDownUp", actions_.look_down_up);
+ table.Insert("lookLeftRight", actions_.look_left_right);
+ table.Insert("moveBackForward", actions_.move_back_forward);
+ table.Insert("strafeLeftRight", actions_.strafe_left_right);
+ table.Insert("crouchJump", actions_.crouch_jump);
+ table.Insert("buttonsDown", actions_.buttons_down);
+ lua::Push(L, table);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << result.error();
+
+ if (result.n_results() >= 1) {
+ lua::Read(L, -1, &table);
+ CHECK(table.LookUp("lookDownUp", look_down_up));
+ CHECK(table.LookUp("lookLeftRight", look_left_right));
+ CHECK(table.LookUp("moveBackForward", move_back_forward));
+ CHECK(table.LookUp("strafeLeftRight", strafe_left_right));
+ CHECK(table.LookUp("crouchJump", crouch_jump));
+ CHECK(table.LookUp("buttonsDown", buttons_down));
+ lua_pop(L, result.n_results());
+ return;
+ }
+ } else {
+ lua_pop(L, 2);
+ }
+
*look_down_up = actions_.look_down_up;
*look_left_right = actions_.look_left_right;
*move_back_forward = actions_.move_back_forward;
@@ -574,182 +909,177 @@ int Context::MakeRandomSeed() {
1, std::numeric_limits::max())(*EnginePrbg());
}
-bool Context::UpdateSpawnVars(char* spawn_var_chars, int* num_spawn_var_chars,
- int spawn_var_offsets[][2], int* num_spawn_vars) {
+bool Context::FindModel(const char* model_name) {
lua_State* L = lua_vm_.get();
- script_table_ref_.PushMemberFunction("updateSpawnVars");
+ script_table_ref_.PushMemberFunction("createModel");
+
+ // Check function exists.
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
- return true;
+ return false;
}
- auto table = lua::TableRef::Create(L);
- for (int i = 0; i < *num_spawn_vars; ++i) {
- table.Insert(std::string(spawn_var_chars + spawn_var_offsets[i][0]),
- std::string(spawn_var_chars + spawn_var_offsets[i][1]));
- }
- lua::Push(L, table);
+ lua::Push(L, model_name);
+
auto result = lua::Call(L, 2);
- CHECK(result.ok()) << result.error();
+ CHECK(result.ok()) << "createModel: " << result.error();
- // Nil return so spawn is ignored.
- if (lua_isnil(L, -1)) {
+ // If no description is returned or the description is nil, don't create
+ // model.
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
lua_pop(L, result.n_results());
return false;
}
- std::unordered_map out_spawn_vars;
- lua::Read(L, -1, &out_spawn_vars);
-
- *num_spawn_vars = out_spawn_vars.size();
- CHECK_NE(0, *num_spawn_vars) << "Must have spawn vars or return nil. (Make "
- "sure all values are strings.)";
- CHECK_LT(*num_spawn_vars, kMaxSpawnVars) << "Too many spawn vars!";
- char* mem = spawn_var_chars;
-
- auto it = out_spawn_vars.begin();
- for (int i = 0; i < *num_spawn_vars; ++i) {
- const auto& key = it->first;
- const auto& value = it->second;
- ++it;
- std::size_t kl = key.length() + 1;
- std::size_t vl = value.length() + 1;
- *num_spawn_var_chars += kl + vl;
- CHECK_LT(*num_spawn_var_chars, kMaxSpawnVarChars) << "Too large spawn vars";
- std::copy(key.c_str(), key.c_str() + kl, mem);
- spawn_var_offsets[i][0] = std::distance(spawn_var_chars, mem);
- mem += kl;
- std::copy(value.c_str(), value.c_str() + vl, mem);
- spawn_var_offsets[i][1] = std::distance(spawn_var_chars, mem);
- mem += vl;
- }
+ // Read model
+ std::unique_ptr model(new Model());
+ CHECK(Read(L, -1, model.get()))
+ << "createModel: Failed to parse data for model " << model_name;
+ model_name_ = model_name;
+ model_ = std::move(model);
+
lua_pop(L, result.n_results());
return true;
}
-bool Context::FindItem(const char* class_name, int* index) {
+void Context::GetModelGetters(DeepmindModelGetters* model_getters,
+ void** model_data) {
+ CHECK(model_) << "No model was selected for this context!";
+
+ *model_getters = ModelGetters();
+ *model_data = model_.get();
+}
+
+
+bool Context::CanTrigger(int entity_id, const char* target_name) {
lua_State* L = lua_vm_.get();
- script_table_ref_.PushMemberFunction("createPickup");
+ script_table_ref_.PushMemberFunction("canTrigger");
// Check function exists.
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
- return false;
- }
-
- lua::Push(L, class_name);
-
- auto result = lua::Call(L, 2);
- CHECK(result.ok()) << result.error();
-
- // If nothing it returned or that it's nil, don't create item.
- if (result.n_results() == 0 || lua_isnil(L, -1)) {
- lua_pop(L, result.n_results());
- return false;
+ return true;
}
- lua::TableRef table;
- CHECK(Read(L, -1, &table)) << "Failed to read pickup table!";
+ lua::Push(L, entity_id);
+ lua::Push(L, target_name);
- PickupItem item = {};
- CHECK(table.LookUp("name", &item.name));
- CHECK(table.LookUp("class_name", &item.class_name));
- CHECK(table.LookUp("model_name", &item.model_name));
- CHECK(table.LookUp("quantity", &item.quantity));
- CHECK(table.LookUp("type", &item.type));
+ auto result = lua::Call(L, 3);
+ CHECK(result.ok()) << "[canTrigger] - " << result.error();
- // Optional tag field.
- table.LookUp("tag", &item.tag);
+ CHECK(result.n_results() != 0 && !lua_isnil(L, -1))
+ << "canTrigger: return value from lua canTrigger must be true or false.";
- items_.push_back(item);
- *index = ItemCount() - 1;
+ bool can_trigger;
+ CHECK(lua::Read(L, -1, &can_trigger))
+ << "canTrigger: Failed to read the return value as a boolean."
+ << "Return true or false.";
lua_pop(L, result.n_results());
- return true;
-}
-
-bool Context::GetItem(int index, char* item_name, int max_item_name, //
- char* class_name, int max_class_name, //
- char* model_name, int max_model_name, //
- int* quantity, int* type, int* tag) {
- CHECK_GE(index, 0) << "Index out of range!";
- CHECK_LT(index, ItemCount()) << "Index out of range!";
-
- const auto& item = items_[index];
- CHECK(StringCopy(item.name, item_name, max_item_name));
- CHECK(StringCopy(item.class_name, class_name, max_class_name));
- CHECK(StringCopy(item.model_name, model_name, max_model_name));
- *quantity = item.quantity;
- *type = static_cast(item.type);
- *tag = item.tag;
- return true;
+ return can_trigger;
}
-bool Context::CanPickup(int entity_id) {
+bool Context::OverrideTrigger(int entity_id, const char* target_name) {
lua_State* L = lua_vm_.get();
- script_table_ref_.PushMemberFunction("canPickup");
+ script_table_ref_.PushMemberFunction("trigger");
// Check function exists.
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
- return true;
+ return false;
}
lua::Push(L, entity_id);
+ lua::Push(L, target_name);
- auto result = lua::Call(L, 2);
- CHECK(result.ok()) << result.error();
+ auto result = lua::Call(L, 3);
+ CHECK(result.ok()) << "[trigger] - " << result.error();
- // If nothing returned or the return is nil, the default.
+ // If nothing was returned or if the first return value is nil, the trigger
+ // behaviour was not overridden
if (result.n_results() == 0 || lua_isnil(L, -1)) {
lua_pop(L, result.n_results());
- return true;
+ return false;
}
- bool can_pickup = true;
- CHECK(lua::Read(L, -1, &can_pickup))
- << "Failed to read canPickup return value";
+ bool has_override;
+ CHECK(lua::Read(L, -1, &has_override))
+ << "trigger: Failed to read the return value as a boolean."
+ << "Return true or false.";
lua_pop(L, result.n_results());
- return can_pickup;
+ return has_override;
}
-bool Context::OverridePickup(int entity_id, int* respawn) {
+void Context::TriggerLookat(int entity_id, bool looked_at,
+ const float position[3]) {
lua_State* L = lua_vm_.get();
- script_table_ref_.PushMemberFunction("pickup");
+ script_table_ref_.PushMemberFunction("lookat");
// Check function exists.
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
- return false;
+ return;
}
lua::Push(L, entity_id);
+ lua::Push(L, looked_at);
+ std::array float_array3;
+ std::copy_n(position, float_array3.size(), float_array3.data());
+ lua::Push(L, float_array3);
- auto result = lua::Call(L, 2);
- CHECK(result.ok()) << result.error();
-
- // If nothing returned or the return is nil, we're not overriding the
- // pickup behaviour.
- if (result.n_results() == 0 || lua_isnil(L, -1)) {
- lua_pop(L, result.n_results());
- return false;
- }
-
- CHECK(lua::Read(L, -1, &respawn)) << "Failed to read the respawn time";
+ auto result = lua::Call(L, 4);
+ CHECK(result.ok()) << "[lookat] - " << result.error();
lua_pop(L, result.n_results());
- return true;
+}
+
+int Context::RewardOverride(const char* optional_reason, int player_id,
+ int team, const int* optional_other_player_id,
+ const float* optional_origin, int score) {
+ if (optional_reason != nullptr) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("rewardOverride");
+ // Check function exists.
+ if (!lua_isnil(L, -2)) {
+ auto args = lua::TableRef::Create(L);
+ args.Insert("reason", optional_reason);
+ args.Insert("playerId", player_id);
+ if (team >= 0 && team
+ < std::distance(std::begin(kTeamNames), std::end(kTeamNames))) {
+ args.Insert("team", kTeamNames[team]);
+ }
+ if (optional_other_player_id != nullptr) {
+ args.Insert("otherPlayerId", *optional_other_player_id);
+ }
+ if (optional_origin != nullptr) {
+ std::array float_array3 = {
+ {optional_origin[0], optional_origin[1], optional_origin[2]}};
+ args.Insert("location", float_array3);
+ }
+ args.Insert("score", score);
+ lua::Push(L, args);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[scoreOverride] - " << result.error();
+ CHECK(result.n_results() <= 1)
+ << "[scoreOverride] - Must return new score or nil";
+ if (result.n_results() == 1 && !lua_isnil(L, -1)) {
+ CHECK(lua::Read(L, -1, &score))
+ << "[scoreOverride] - Score must be an integer!";
+ }
+ lua_pop(L, result.n_results());
+ } else {
+ lua_pop(L, 2);
+ }
+ }
+ return ExternalReward(player_id) + score;
}
int Context::ExternalReward(int player_id) {
CHECK_GE(player_id, 0) << "Invalid player Id!";
double reward = 0;
if (static_cast(player_id) < player_rewards_.size()) {
- if (player_rewards_[player_id] >= 1.0) {
- player_rewards_[player_id] =
- std::modf(player_rewards_[player_id], &reward);
- }
+ player_rewards_[player_id] = std::modf(player_rewards_[player_id], &reward);
}
return reward;
}
@@ -791,19 +1121,122 @@ void Context::AddBots() {
bot_table.LookUp("skill", &skill);
std::string team = "free";
bot_table.LookUp("team", &team);
- Calls()->add_bot(bot_name.c_str(), skill, team.c_str());
+ Game().Calls()->add_bot(bot_name.c_str(), skill, team.c_str());
}
lua_pop(L, result.n_results());
}
-void Context::ModifyRgbaTexture(const char* name, unsigned char* data,
+bool Context::ReplaceModelName(const char* name, char* new_name,
+ int new_name_size, char* texture_prefix,
+ int texture_prefix_size) {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("replaceModelName");
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return false;
+ }
+
+ lua::Push(L, name);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[replaceModelName] - " << result.error();
+
+ if (lua_isnoneornil(L, 1)) {
+ CHECK(lua_isnoneornil(L, 2))
+ << "[replaceModelName] - Return arg2 (texturePrefix) must be nil if "
+ "return arg1 (newModelName) is nil.";
+ lua_pop(L, result.n_results());
+ return false;
+ }
+
+ std::string replacement_name;
+ CHECK(lua::Read(L, 1, &replacement_name))
+ << "[replaceModelName] - Return arg1 (newModelName) must be a string.";
+ CHECK_LT(replacement_name.size(), new_name_size)
+ << "[replaceModelName] - Return arg1 (newModelName) is too long.";
+
+ std::string string_prefix;
+ if (result.n_results() == 2 && !lua_isnil(L, 2)) {
+ CHECK(lua::Read(L, 2, &string_prefix))
+ << "[replaceModelName] - Return arg2 (texturePrefix) must be a string.";
+ CHECK_LT(string_prefix.size(), texture_prefix_size)
+ << "[replaceModelName] - Return arg2 (texturePrefix) is too long.";
+ }
+
+ std::copy_n(replacement_name.c_str(), replacement_name.size() + 1, new_name);
+ std::copy_n(string_prefix.c_str(), string_prefix.size() + 1, texture_prefix);
+ lua_pop(L, result.n_results());
+ return true;
+}
+
+bool Context::ReplaceTextureName(const char* name, char* new_name,
+ int max_size) {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("replaceTextureName");
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return false;
+ }
+
+ lua::Push(L, name);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[replaceTextureName] - " << result.error();
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return false;
+ }
+ std::string replacement_name;
+ CHECK(lua::Read(L, 1, &replacement_name))
+ << "[replaceTextureName] - New name must be a string.";
+ CHECK_LT(replacement_name.size(), max_size)
+ << "[replaceTextureName] - New name is too long.";
+ std::copy_n(replacement_name.c_str(), replacement_name.size() + 1, new_name);
+ lua_pop(L, result.n_results());
+ return true;
+}
+
+bool Context::LoadTexture(const char* name, unsigned char** pixels, int* width,
+ int* height, void* (*allocator)(int size)) {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("loadTexture");
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return false;
+ }
+
+ lua::Push(L, name);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[loadTexture] - " << result.error();
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return false;
+ }
+ auto* image_tensor = tensor::LuaTensor::ReadObject(L, -1);
+ CHECK(image_tensor) << "[loadTexture] - Must return ByteTensor.";
+ const auto& view = image_tensor->tensor_view();
+ CHECK_EQ(3, view.shape().size())
+ << "[loadTexture] - Must return ByteTensor shaped HxWx4";
+ CHECK_EQ(4, view.shape()[2])
+ << "[loadTexture] - Must return ByteTensor shaped HxWx4";
+ *height = view.shape()[0];
+ *width = view.shape()[1];
+ *pixels = static_cast(allocator(view.num_elements()));
+ unsigned char* pixel_it = *pixels;
+ view.ForEach([&pixel_it](unsigned char value) { *pixel_it++ = value; });
+ lua_pop(L, result.n_results());
+ return true;
+}
+
+bool Context::ModifyRgbaTexture(const char* name, unsigned char* data,
int width, int height) {
lua_State* L = lua_vm_.get();
script_table_ref_.PushMemberFunction("modifyTexture");
// Check function exists.
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
- return;
+ return false;
}
lua::Push(L, name);
@@ -816,8 +1249,13 @@ void Context::ModifyRgbaTexture(const char* name, unsigned char* data,
storage_validity);
auto result = lua::Call(L, 3);
CHECK(result.ok()) << "[modifyTexture] - " << result.error();
+
+ bool modified_texture;
+ CHECK(lua::Read(L, -1, &modified_texture))
+ << "[modifyTexture] - must return true or false";
lua_pop(L, result.n_results());
storage_validity->Invalidate();
+ return modified_texture;
}
int Context::CallInit() {
@@ -829,151 +1267,32 @@ int Context::CallInit() {
}
lua::Push(L, settings_);
auto result = lua::Call(L, 2);
- lua_pop(L, result.n_results());
if (!result.ok()) {
- std::cerr << result.error() << '\n';
+ error_message_ = result.error();
return 1;
}
- return 0;
-}
-int Context::CallObservationSpec() {
- lua_State* L = lua_vm_.get();
- script_table_ref_.PushMemberFunction("customObservationSpec");
- if (lua_isnil(L, -2)) {
- lua_pop(L, 2);
- return 0;
- }
- auto result = lua::Call(L, 1);
- if (!result.ok()) {
- std::cerr << result.error() << '\n';
- return 1;
- }
- lua::TableRef observations;
- lua::Read(L, -1, &observations);
- observation_infos_.clear();
- observation_infos_.reserve(observations.ArraySize());
- for (std::size_t i = 0, c = observations.ArraySize(); i != c; ++i) {
- lua::TableRef observation_info;
- observations.LookUp(i + 1, &observation_info);
- ObservationSpecInfo info;
- if (!observation_info.LookUp("name", &info.name)) {
- std::cerr << "[customObservationSpec] - Missing 'name = '.\n";
- return 1;
- }
- std::string type = "Doubles";
- observation_info.LookUp("type", &type);
- if (type.compare("Bytes") == 0) {
- info.type = EnvCApi_ObservationBytes;
- } else if (type.compare("Doubles") == 0) {
- info.type = EnvCApi_ObservationDoubles;
+ int ret_val = 0;
+ bool correct_args = result.n_results() == 0 ||
+ (result.n_results() == 1 && lua_isnil(L, 1)) ||
+ (result.n_results() <= 2 && lua::Read(L, 1, &ret_val));
+ if (ret_val != 0) {
+ if (result.n_results() == 2) {
+ error_message_ = lua::ToString(L, 2);
} else {
- std::cerr
- << "[customObservationSpec] - Missing 'type = 'Bytes'|'Doubles''.\n";
- return 1;
- }
- if (!observation_info.LookUp("shape", &info.shape)) {
- std::cerr
- << "[customObservationSpec] - Missing 'shape = {, ...}'.\n";
- return 1;
+ error_message_ = "Error while calling 'init'.";
}
- observation_infos_.push_back(std::move(info));
}
lua_pop(L, result.n_results());
- return 0;
-}
-
-void Context::CustomObservation(int idx, EnvCApi_Observation* observation) {
- lua_State* L = lua_vm_.get();
- script_table_ref_.PushMemberFunction("customObservation");
- // Function must exist.
- CHECK(!lua_isnil(L, -2))
- << "Observations Spec set but no observation member function";
- const auto& info = observation_infos_[idx];
- lua::Push(L, info.name);
- auto result = lua::Call(L, 2);
- CHECK(result.ok()) << "[customObservation] - " << result.error();
-
- CHECK_EQ(1, result.n_results())
- << "[customObservation] - Must return a "
- << (info.type == EnvCApi_ObservationDoubles ? "DoubleTensor"
- : "ByteTensor");
-
- const tensor::Layout* layout = nullptr;
- if (info.type == EnvCApi_ObservationDoubles) {
- auto* double_tensor = tensor::LuaTensor::ReadObject(L, -1);
- if (double_tensor != nullptr) {
- const auto& view = double_tensor->tensor_view();
- CHECK(view.IsContiguous())
- << "[customObservation] - Must return a contiguous tensor!";
- layout = &view;
- observation->spec.type = EnvCApi_ObservationDoubles;
- observation->payload.doubles = view.storage() + view.start_offset();
- }
- } else {
- auto* byte_tensor = tensor::LuaTensor::ReadObject(L, -1);
- if (byte_tensor != nullptr) {
- const auto& view = byte_tensor->tensor_view();
- layout = &view;
- CHECK(view.IsContiguous())
- << "[customObservation] - Must return a contiguous tensor!";
- observation->spec.type = EnvCApi_ObservationBytes;
- observation->payload.bytes = view.storage() + view.start_offset();
- }
- }
- CHECK(layout != nullptr)
- << "[customObservation] - Must return a contiguous tensor!\n:"
- << "at idx" << idx << "\n"
- << lua::ToString(L, -1);
-
- observation_tensor_shape_.resize(layout->shape().size());
- std::copy(layout->shape().begin(), layout->shape().end(),
- observation_tensor_shape_.begin());
- observation->spec.dims = observation_tensor_shape_.size();
- observation->spec.shape = observation_tensor_shape_.data();
-
- // Prevent observation->payload from being destroyed during pop.
- lua::Read(L, -1, &observation_tensor_);
- lua_pop(L, result.n_results());
-}
-
-void Context::CustomObservationSpec(int idx,
- EnvCApi_ObservationSpec* spec) const {
- const auto& info = observation_infos_[idx];
- spec->type = info.type;
- spec->dims = info.shape.size();
- spec->shape = info.shape.data();
-}
-
-void Context::SetPredictPlayerState(const float pos[3], const float vel[3],
- const float angles[3], float height,
- int timestamp_msec) {
- PlayerView before = predicted_player_view_;
- std::copy_n(pos, 3, predicted_player_view_.pos.begin());
- std::copy_n(vel, 3, predicted_player_view_.vel.begin());
- std::copy_n(angles, 3, predicted_player_view_.angles.begin());
- predicted_player_view_.height = height;
- predicted_player_view_.timestamp_msec = timestamp_msec;
-
- int delta_time_msec =
- predicted_player_view_.timestamp_msec - before.timestamp_msec;
-
- // When delta_time_msec < 3 the velocities become inaccurate.
- if (before.timestamp_msec > 0 && delta_time_msec > 0) {
- double inv_delta_time = 1000.0 / delta_time_msec;
- for (int i : {0, 1, 2}) {
- predicted_player_view_.anglesVel[i] =
- CanonicalAngle360(predicted_player_view_.angles[i] -
- before.angles[i]) *
- inv_delta_time;
- }
- } else {
- predicted_player_view_.anglesVel.fill(0);
+ if (!correct_args) {
+ error_message_ = "[init] - Must return none, nil, or integer and message\n";
+ return 1;
}
+ return ret_val;
}
-int Context::MakeScreenMesages(int screen_width, int screen_height,
- int line_height, int string_buffer_size) {
+int Context::MakeScreenMessages(int screen_width, int screen_height,
+ int line_height, int string_buffer_size) {
screen_messages_.clear();
lua_State* L = lua_vm_.get();
script_table_ref_.PushMemberFunction("screenMessages");
@@ -996,7 +1315,7 @@ int Context::MakeScreenMesages(int screen_width, int screen_height,
<< "[screenMessages] - Must return an array of messages";
lua::TableRef messages_array;
lua::Read(L, -1, &messages_array);
- for (std::size_t i = 0, e = messages_array.ArraySize(); i != e; ++i) {
+ for (std::size_t i = 0, size = messages_array.ArraySize(); i != size; ++i) {
lua::TableRef message_table;
CHECK(messages_array.LookUp(i + 1, &message_table))
<< "[screenMessages] - Each message must be a table";
@@ -1009,6 +1328,10 @@ int Context::MakeScreenMesages(int screen_width, int screen_height,
message_table.LookUp("x", &message.x);
message_table.LookUp("y", &message.y);
message_table.LookUp("alignment", &message.align_l0_r1_c2);
+ std::fill(message.rgba.begin(), message.rgba.end(), 1.0);
+ message_table.LookUp("rgba", &message.rgba);
+ message.shadow = true;
+ message_table.LookUp("shadow", &message.shadow);
screen_messages_.push_back(std::move(message));
}
@@ -1017,15 +1340,153 @@ int Context::MakeScreenMesages(int screen_width, int screen_height,
}
void Context::GetScreenMessage(int message_id, char* buffer, int* x, int* y,
- int* align_l0_r1_c2) const {
+ int* align_l0_r1_c2, int* shadow,
+ float rgba[4]) const {
const auto& screen_message = screen_messages_[message_id];
const std::string& msg_text = screen_message.text;
- // MakeScreenMesages guarantees message is smaller than string_buffer_size.
+ // MakeScreenMessages guarantees message is smaller than string_buffer_size.
std::copy_n(msg_text.c_str(), msg_text.size() + 1, buffer);
*x = screen_message.x;
*y = screen_message.y;
*align_l0_r1_c2 = screen_message.align_l0_r1_c2;
+ *shadow = screen_message.shadow ? 1 : 0;
+ std::copy_n(screen_message.rgba.data(), screen_message.rgba.size(), rgba);
+}
+
+int Context::MakeFilledRectangles(int screen_width, int screen_height) {
+ filled_rectangles_.clear();
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("filledRectangles");
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return 0;
+ }
+
+ auto args = lua::TableRef::Create(L);
+ args.Insert("width", screen_width);
+ args.Insert("height", screen_height);
+ lua::Push(L, args);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[filledRectangles] - " << result.error();
+ CHECK_EQ(1, result.n_results())
+ << "[filledRectangles] - Must return an array of rectangles";
+ lua::TableRef rectangles_array;
+ lua::Read(L, -1, &rectangles_array);
+ for (std::size_t i = 0, size = rectangles_array.ArraySize(); i != size; ++i) {
+ lua::TableRef rectangle_table;
+ CHECK(rectangles_array.LookUp(i + 1, &rectangle_table))
+ << "[filledRectangles] - Each message must be a table";
+ FilledRectangle filled_rectangle = {};
+ CHECK(rectangle_table.LookUp("x", &filled_rectangle.x))
+ << "[filledRectangles] - Must supply x";
+ CHECK(rectangle_table.LookUp("y", &filled_rectangle.y))
+ << "[filledRectangles] - Must supply y";
+ CHECK(rectangle_table.LookUp("width", &filled_rectangle.width))
+ << "[filledRectangles] - Must supply width";
+ CHECK(rectangle_table.LookUp("height", &filled_rectangle.height))
+ << "[filledRectangles] - Must supply height";
+ CHECK(rectangle_table.LookUp("rgba", &filled_rectangle.rgba))
+ << "[filledRectangles] - Must supply rgba";
+ filled_rectangles_.push_back(filled_rectangle);
+ }
+
+ lua_pop(L, result.n_results());
+ return filled_rectangles_.size();
+}
+
+void Context::GetFilledRectangle(int rectangle_id, int* x, int* y, int* width,
+ int* height, float rgba[4]) const {
+ const auto& filled_rectangle = filled_rectangles_[rectangle_id];
+ *x = filled_rectangle.x;
+ *y = filled_rectangle.y;
+ *width = filled_rectangle.width;
+ *height = filled_rectangle.height;
+ std::copy_n(filled_rectangle.rgba.data(), filled_rectangle.rgba.size(), rgba);
+}
+
+void Context::MakePk3FromMap(const char* map_path, const char* map_name,
+ bool gen_aas) {
+ MapCompileSettings compile_settings;
+ compile_settings.generate_aas = gen_aas;
+ compile_settings.map_source_location =
+ absl::StrCat(ExecutableRunfiles(), "/", map_path);
+ compile_settings.use_local_level_cache = use_local_level_cache_;
+ compile_settings.use_global_level_cache = use_global_level_cache_;
+ compile_settings.level_cache_params = level_cache_params_;
+ std::string target = absl::StrCat(TempDirectory(), "/baselab/", map_name);
+ CHECK(RunMapCompileFor(ExecutableRunfiles(), target, compile_settings));
+}
+
+void Context::CustomPlayerMovement(int mover_id, const float mover_pos[3],
+ const float player_pos[3],
+ const float player_vel[3],
+ float player_pos_delta[3],
+ float player_vel_delta[3]) {
+ lua_State* L = lua_vm_.get();
+ script_table_ref_.PushMemberFunction("playerMover");
+
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return;
+ }
+
+ std::array float_array3;
+
+ auto args = lua::TableRef::Create(L);
+ args.Insert("moverId", mover_id);
+
+ std::copy_n(mover_pos, float_array3.size(), float_array3.data());
+ args.Insert("moverPos", float_array3);
+
+ std::copy_n(player_pos, float_array3.size(), float_array3.data());
+ args.Insert("playerPos", float_array3);
+
+ std::copy_n(player_vel, float_array3.size(), float_array3.data());
+ args.Insert("playerVel", float_array3);
+
+ lua::Push(L, args);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[playerMover] - " << result.error();
+
+ std::array pos_delta = {{0.0f, 0.0f, 0.0f}};
+ std::array vel_delta = {{0.0f, 0.0f, 0.0f}};
+
+ CHECK(lua_isnoneornil(L, 1) || lua::Read(L, 1, &pos_delta))
+ << "[playerMover] - First return value must be a table containing"
+ "player position delta values.";
+ CHECK(lua_isnoneornil(L, 2) || lua::Read(L, 2, &vel_delta))
+ << "[playerMover] - Second return value must be a table containing"
+ "player velocity delta values.";
+
+ std::copy_n(pos_delta.data(), pos_delta.size(), player_pos_delta);
+ std::copy_n(vel_delta.data(), vel_delta.size(), player_vel_delta);
+
+ lua_pop(L, result.n_results());
+}
+
+void Context::GameEvent(const char* event_name, int count,
+ const float* data) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("engineEvent");
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return;
+ }
+
+ lua::Push(L, event_name);
+ lua_createtable(L, count, 0);
+ for (std::size_t i = 0; i < count; ++i) {
+ lua::Push(L, i + 1);
+ lua::Push(L, data[i]);
+ lua_settable(L, -3);
+ }
+ auto result = lua::Call(L, 3);
+ CHECK(result.ok()) << result.error() << '\n';
+ lua_pop(L, result.n_results());
}
} // namespace lab
} // namespace deepmind
+
diff --git a/deepmind/engine/context.h b/deepmind/engine/context.h
index e7fa5bd5..afdcfb83 100644
--- a/deepmind/engine/context.h
+++ b/deepmind/engine/context.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 Google Inc.
+// Copyright (C) 2016-2017 Google Inc.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -21,45 +21,35 @@
#ifndef DML_DEEPMIND_ENGINE_CONTEXT_H_
#define DML_DEEPMIND_ENGINE_CONTEXT_H_
+#include
+
#include
#include
+#include
#include
#include
#include
#include
+#include "deepmind/engine/context_entities.h"
+#include "deepmind/engine/context_events.h"
+#include "deepmind/engine/context_game.h"
+#include "deepmind/engine/context_observations.h"
+#include "deepmind/engine/context_pickups.h"
#include "deepmind/include/deepmind_calls.h"
#include "deepmind/include/deepmind_hooks.h"
+#include "deepmind/include/deepmind_model_getters.h"
#include "deepmind/lua/n_results_or.h"
#include "deepmind/lua/table_ref.h"
#include "deepmind/lua/vm.h"
+#include "deepmind/model_generation/model.h"
+#include "deepmind/model_generation/model_getters.h"
+#include "public/level_cache_types.h"
+#include "third_party/rl_api/env_c_api.h"
namespace deepmind {
namespace lab {
-// Parameters for a custom pickup item.
-struct PickupItem {
- std::string name; // Name that will show when picking up item.
- std::string class_name; // Class name to spawn entity as. Must be unique.
- std::string model_name; // Model for pickup item.
- int quantity; // Amount to award on pickup.
- int type; // Type of pickup. E.g. health, ammo, frags etc.
- // Must match itemType_t in bg_public.h
- int tag; // Tag used in conjunction with type. E.g. determine
- // which weapon to award, or if a goal should bob and
- // rotate.
-};
-
-// Represents a player's state in world units.
-struct PlayerView {
- std::array pos; // Position (forward, left, up).
- std::array vel; // World velocity (forward, left, up).
- std::array angles; // Orientation degrees (pitch, yaw, roll).
- std::array anglesVel; // Angular velocity in degrees.
- int timestamp_msec; // Engine time in msec of the view.
- double height; // View height.
-};
-
// This is the userdata in DeepmindContext. It contains the Lua VM and
// methods for handling callbacks from DeepMind Lab.
class Context {
@@ -68,18 +58,27 @@ class Context {
// 'executable_runfiles' path to where DeepMind Lab assets are stored.
// 'calls' allow the context to call into the engine. (Owned by engine.)
// 'hooks' allow the engine to call into the context.
+ // 'file_reader_override' an optional function for reading from the file
+ // system. If set, a call returns whether the file 'file_name' was read
+ // successfully and if so 'buff' points to the content and 'size' contains the
+ // size of the file and after use 'buff' must be freed with 'free'. Otherwise
+ // returns false.
+ // 'temp_folder' optional folder to store temporary objects.
Context(lua::Vm lua_vm, const char* executable_runfiles,
- const DeepmindCalls* calls, DeepmindHooks* hooks);
+ const DeepmindCalls* calls, DeepmindHooks* hooks,
+ bool (*file_reader_override)(const char* file_name, char** buff,
+ size_t* size),
+ const char* temp_folder);
// Inserts 'key' 'value' into settings_.
// Must be called before Init.
void AddSetting(const char* key, const char* value);
- // 'script_name': name of a Lua file; this script is ran during first call to
+ // 'script_name': name of a Lua file; this script is run during first call to
// Init.
// Must be called before Init.
// Returns zero if successful and non-zero on error.
- int SetScriptName(const char* script_name);
+ int SetScriptName(std::string script_name);
// Runs the script named script_name_ and stores the result in
// script_table_ref_.
@@ -92,6 +91,10 @@ class Context {
// Returns zero if successful and non-zero on error.
int Start(int episode, int seed);
+ // Calls "mapLoaded" member function on the script_table_ref_ .
+ // Returns zero if successful and non-zero on error.
+ int MapLoaded();
+
// The return value is only valid until the next call to GetCommandLine().
// Must be called after Init.
const char* GetCommandLine(const char* old_commandline);
@@ -100,7 +103,19 @@ class Context {
// Must be called after Init.
const char* NextMap();
- // The script is called with the script_table_ref pushed on the the stack.
+ // Returns the game mode from script must match gametype_t. If not implemented
+ // in Lua it returns GT_FFA (free for all).
+ int GameType();
+
+ // Returns the team selection for a player.
+ // '\0' - No selection.
+ // 'p' - Free.
+ // 'r' - Red Team.
+ // 'b' - Blue Team.
+ // 's' - Spectator.
+ char TeamSelect(int player_id, const char* player_name);
+
+ // The script is called with the script_table_ref pushed on the stack.
// Runs the contents in the lua_vm_. If the script returns an integer this
// function will return it too, else it returns 0.
int RunLuaSnippet(const char* buf, std::size_t buf_len);
@@ -115,6 +130,10 @@ class Context {
int buttons_down);
// Retrieves the current actions applied by the controller.
+ // The values of actions can be overridden by the lua callback named as
+ // modifyControl. The callback takes in a table param and returns a table.
+ // Both tables have 6 keys (look_down_up, look_left_right, ...) representing
+ // the six actions.
void GetActions( //
double* look_down_up, //
double* look_left_right, //
@@ -123,155 +142,245 @@ class Context {
signed char* crouch_jump, //
int* buttons_down);
- // This returns whether the internal controller will call SetActions.
- bool UseInternalControls() { return use_internal_controls_; }
-
- // Sets whether the internal controller will call SetActions.
- void SetUseInternalControls(bool v) { use_internal_controls_ = v; }
+ // This returns whether we are running a native app and the internal
+ // controller will call SetActions.
+ bool NativeApp() { return native_app_; }
- // Allows Lua to replace contents of this c-style dictionary.
- // 'spawn_var_chars' is pointing at the memory holding the strings.
- // '*num_spawn_var_chars' is the total length of all strings and nulls.
- // 'spawn_var_offsets' is the key, value offsets in 'spawn_var_chars'.
- // '*num_spawn_vars' is the number of valid spawn_var_offsets.
- //
- // So the dictionary { key0=value0, key1=value1, key2=value2 } would be
- // represented as:
- // spawn_var_chars[4096] = "key0\0value0\0key1\0value1\0key2\0value2";
- // *num_spawn_var_chars = 36
- // spawn_var_offsets[64][2] = {{0,5}, {12,17}, {24,29}};
- // *num_spawn_vars = 3
- // The update will not increase *num_spawn_var_chars to greater than 4096.
- // and not increase *num_spawn_vars to greater than 64.
- bool UpdateSpawnVars( //
- char* spawn_var_chars, //
- int* num_spawn_var_chars, //
- int spawn_var_offsets[][2], //
- int* num_spawn_vars);
-
- // Finds a pickup item by class_name, and registers this item with the
- // Context's item array. This is so all the VMs can update their respective
- // item lists by iterating through that array during update.
- // Returns whether the item was found, and if so, writes the index at which
- // the item now resides to *index.
- bool FindItem(const char* class_name, int* index);
+ // Sets whether we are running a native app and the internal controller will
+ // call SetActions.
+ void SetNativeApp(bool v) { native_app_ = v; }
// Adds all the bots specified in the script. Called on each map load.
void AddBots();
- // Get the current number of registered items.
- int ItemCount() const { return items_.size(); }
-
- // Get an item at a particular index and fill in the various buffers
- // provided.
- // Returns whether the operation succeeded.
- bool GetItem( //
- int index, //
- char* item_name, //
- int max_item_name, //
- char* class_name, //
- int max_class_name, //
- char* model_name, //
- int max_model_name, //
- int* quantity, //
- int* type, //
- int* tag);
-
- // Clear the current list of registered items. Called just before loading a
- // new map.
- void ClearItems() { items_.clear(); }
+ // Finds a model by model_name, and registers this item with the Context's
+ // model array.
+ bool FindModel(const char* model_name);
- // Returns whether we should finish the current map.
- bool MapFinished() const { return map_finished_; }
+ // Return the accessor API for currently selected model.
+ void GetModelGetters( //
+ DeepmindModelGetters* model_accessors, //
+ void** model_data);
- // Sets whether the current map should finish.
- void SetMapFinished(bool map_finished) { map_finished_ = map_finished; }
+ // Clear the current list of registered models. Called just before loading a
+ // new map.
+ void ClearModel() { model_.reset(); }
// Returns whether we should finish the episode. Called at the end of every
// frame.
bool HasEpisodeFinished(double elapsed_episode_time_seconds);
- // Returns whether we can pickup the specified entity id. By default this
+ // Returns whether the specified entity id can trigger. By default this
// returns true.
- bool CanPickup(int entity_id);
+ bool CanTrigger(int entity_id, const char* target_name);
- // Customization point for overriding the entity's pickup behaviour. Also
- // allows for modifying the default respawn time for the entity.
- // Returns true if the pickup behaviour has been overridden by the user,
- // otherwise calls the default pickup behaviour based on the item type.
- bool OverridePickup(int entity_id, int* respawn);
+ // Customization point for overriding the entity's trigger behaviour.
+ // Returns whether the trigger behaviour has been overridden by the user.
+ // If the trigger behaviour is not overridden, calls the default trigger
+ // behaviour based on the item type.
+ bool OverrideTrigger(int entity_id, const char* target_name);
- // Subtracts the integral part from the stashed reward (see AddScore) and
- // returns that integral part. The remaining stashed reward is smaller than
- // one in magnitude. The returned (integral) value is suitable for the game
- // server, which only deals in integral reward increments.
- int ExternalReward(int player_id);
+ // Customization point for triggering a callback in response to a trigger
+ // lookat.
+ void TriggerLookat(int entity_id, bool looked_at, const float position[3]);
+
+ // Customization point for overriding the value of a reward.
+ //
+ // * 'optional_reason' - The reason is either a nullptr or a string containing
+ // the reason this reward is being awarded.
+ //
+ // * 'player_id' Is the player the reward applies to.
+ //
+ // * 'team' is the team id the player belongs to.
+ //
+ // * 'optional_other_player_id' is a nullptr or the other player involved in
+ // the reward.
+ //
+ // * 'optional_origin' is either a nullptr or 3 floats containing the
+ // location of the reward.
+ //
+ // Returns the modified reward combined with the reward provided by
+ // 'ExternalReward'.
+ int RewardOverride(const char* optional_reason, int player_id, int team,
+ const int* optional_other_player_id,
+ const float* optional_origin, int score);
// Adds the given reward for the specified player. The reward is accumulated
// temporarily until it is harvested by ExternalReward.
void AddScore(int player_id, double reward);
// Path to where DeepMind Lab assets are stored.
- const std::string& ExecutableRunfiles() const { return executable_runfiles_; }
+ const std::string& ExecutableRunfiles() const {
+ return Game().ExecutableRunfiles();
+ }
+
+ const std::string& TempDirectory() const { return Game().TempFolder(); }
// Returns a new random seed on each call. Internally uses 'engine_prbg_' to
// generate new positive integers.
int MakeRandomSeed();
- const DeepmindCalls* Calls() const { return deepmind_calls_; }
-
- // Gets the seed this episode was launched with.
- int GetEpisodeSeed() const { return random_seed_; }
-
std::mt19937_64* UserPrbg() { return &user_prbg_; }
std::mt19937_64* EnginePrbg() { return &engine_prbg_; }
+ // Returns whether to replace the name of model being loaded with an
+ // alternative name and add a prefix to all the model's textures loaded with
+ // it.
+ //
+ // `name` - Name of the model being loaded.
+ // `new_name` - Pointer to a buffer to store the alternative name in.
+ // `new_name_size` - The size of the `new_name` buffer.
+ // `texture_prefix` - Pointer to a buffer to store the prefix in.
+ // `texture_prefix_size` - The size of the `texture_prefix` buffer.
+ bool ReplaceModelName(const char* name, char* new_name, int new_name_size,
+ char* texture_prefix, int texture_prefix_size);
+
+ // Returns whether to replace the name of a texture being loaded with an
+ // alternative one.
+ //
+ // `name` - Name of the texture about to be loaded.
+ // `new_name` - A pointer to a buffer to store the alternative name in.
+ // `max_size` - The size of the `new_name` buffer.
+ bool ReplaceTextureName(const char* name, char* new_name, int max_size);
+
+ // External texture loader. Returns whether a texture was loaded. If true,
+ // `pixels`, `width` and `height` will store the data about the texture.
+ // Otherwise the arguments are left unchanged and the built-in texture loaders
+ // are used.
+ //
+ // `name` - Name of the texture being loaded (without extension).
+ // `pixels` - A buffer sized to hold an rgba byte texture.
+ // `width` - Used to store the width of the created texture.
+ // `height` - Used to store the height of the created texture.
+ // `allocator` - Used to allocate the memory for `pixels`. The amount of
+ // memory allocated shall be *width * *height * 4 bytes.
+ bool LoadTexture(const char* name, unsigned char** pixels, int* width,
+ int* height, void* (*allocator)(int size));
+
// Modify a texture after loading.
- void ModifyRgbaTexture(const char* name, unsigned char* data, int width,
+ bool ModifyRgbaTexture(const char* name, unsigned char* data, int width,
int height);
- // Script observation count.
- int CustomObservationCount() const { return observation_infos_.size(); }
-
- // Script observation name.
- const char* CustomObservationName(int idx) const {
- return observation_infos_[idx].name.c_str();
- }
-
- // Script observation spec.
- void CustomObservationSpec(int idx, EnvCApi_ObservationSpec* spec) const;
-
- // Script observation.
- void CustomObservation(int idx, EnvCApi_Observation* obs);
-
- // Set latest predicted player state.
- void SetPredictPlayerState(const float pos[3], const float vel[3],
- const float angles[3], float height,
- int timestamp_msec);
-
- // Get latest predicted player view. (This is where the game renders from.)
- const PlayerView GetPredictPlayerView() {
- return predicted_player_view_;
- }
-
// Calls script to retrieve a list of screen messages. The message returned
// from the script shall be strictly smaller than buffer_size, since the
// buffer needs space for the null padding. 'screen_width' and 'screen_height'
// are the size of the screen.
- int MakeScreenMesages(int screen_width, int screen_height, int line_height,
- int string_buffer_size);
+ int MakeScreenMessages(int screen_width, int screen_height, int line_height,
+ int string_buffer_size);
// Retrieve screen message. 'buffer' is filled with a null terminated string.
// The room in the buffer is 'string_buffer_size' from the MakeScreenMessage
// command. 'x' and 'y' are the screen coordinates in terms of the screen
- // 'height' and 'width' also in from the MakeScreenMesages.
- // 'message_id' shall be greater than or equal to zero and less then what
+ // 'height' and 'width' also from the MakeScreenMesages.
+ // 'message_id' shall be greater than or equal to zero and less than what
// was returned by the last call of MakeScreenMesages.
// 'align_l0_r1_c2' is how the text is horizontally aligned. '0' for left,
- // '1' for right and '2' for center.
+ // '1' for right and '2' for center. 'shadow' is whether to render a black
+ // offset drop shadow. 'rgba' is the color and alpha of the text.
void GetScreenMessage(int message_id, char* buffer, int* x, int* y,
- int* align_l0_r1_c2) const;
+ int* align_l0_r1_c2, int* shadow, float rgba[4]) const;
+
+ // Calls script to retrieve a list of filled rectangles. 'screen_width' and
+ // 'screen_height' are the size of the screen.
+ int MakeFilledRectangles(int screen_width, int screen_height);
+
+ // Retrieve filled rectangle.
+ // 'rectangle_id' shall be greater than or equal to zero and less than what
+ // was returned by the last call of MakeFilledRectangles.
+ // 'x', 'y' is the position and 'width' and 'height' is the size of the
+ // rectangle in screen-coordinates. They shall be all greater or equal to
+ // zero. (Off-screen rendering is allowed.)
+ // 'rgba' is the color and alpha of the rendered rectangle. 'rgba' values
+ // shall be in the range [0, 1].
+ // The parts of the rectangles that are out of bounds are not rendered.
+ void GetFilledRectangle(int rectangle_id, int* x, int* y, int* width,
+ int* height, float rgba[4]) const;
+
+ // Retrieves player position and velocity deltas.
+ // 'mover_id' is the ID of the triggering entity.
+ // 'mover_pos' is the position of the triggering entity.
+ // 'player_pos' is the current position of the player.
+ // 'player_vel' is the current velocity of the player.
+ // 'player_pos_delta' retrieves the position delta for the player.
+ // 'player_vel_delta' retrieves the velocity delta for the player.
+ // If the Lua function 'playerMover' is unimplemented, player_pos_delta and
+ // player_vel_delta will remain unchanged.
+ void CustomPlayerMovement(int mover_id, const float mover_pos[3],
+ const float player_pos[3],
+ const float player_vel[3],
+ float player_pos_delta[3],
+ float player_vel_delta[3]);
+
+ // Called on the spawning and updating of each player. Arrays are prefixed
+ // with their count:
+ //
+ // `is_spawning` whether the player player is spawning.
+ // `gadget_inventory` array and matches the contents of playerState_t::ammo,
+ // `stat_inventory` array and matches the contents of playerState_t::stats,
+ // `powerup_time` array and matches the contents of playerState_t::powerups.
+ // `gadget_held` player gadget held (See game_scripts/common/inventory.lua)
+ // `height` player eye height.
+ // `position` player location in world units.
+ // `view_angles` player look direction in Euler degrees.
+ void UpdateInventory(bool is_spawning, int player_id, int gadget_count,
+ int gadget_inventory[], int stat_count,
+ int stat_inventory[], int powerup_count,
+ int powerup_time[], int gadget_held, float height,
+ float position[3], float view_angles[3]);
+
+ // Calls `gameEvent` with the event name and an array of data.
+ void GameEvent(const char* event_name, int count, const float* data);
+
+ // Generates a pk3 from the map in `map_path` named `map_name`.
+ // `gen_aas` should be set if bots are used with level.
+ void MakePk3FromMap(const char* map_path, const char* map_name, bool gen_aas);
+
+ // Sets which level caches to use. See MapCompileSettings in compile_map.h.
+ void SetLevelCacheSetting(bool local, bool global,
+ DeepMindLabLevelCacheParams level_cache_params) {
+ use_local_level_cache_ = local;
+ use_global_level_cache_ = global;
+ level_cache_params_ = level_cache_params;
+ }
+
+ bool UseLocalLevelCache() const { return use_local_level_cache_; }
+ bool UseGlobalLevelCache() const { return use_global_level_cache_; }
+ DeepMindLabLevelCacheParams LevelCacheParams() const {
+ return level_cache_params_;
+ }
+
+ const char* ErrorMessage() const { return error_message_.c_str(); }
+
+ // Sets current error message. 'message' shall be a null terminated string.
+ void SetErrorMessage(const char* message) {
+ error_message_ = std::string(message);
+ }
+
+ // Sets whether there are alternative cameras. This will make the server send
+ // all entities, so they are visible to all cameras.
+ void SetHasAltCameras(bool has_alt_cameras) {
+ has_alt_cameras_ = has_alt_cameras;
+ }
+
+ // Returns whether the server sends all entities, so they are visible to all
+ // cameras.
+ bool HasAltCameras() const { return has_alt_cameras_; }
+
+ const ContextGame& Game() const { return game_; }
+ ContextGame* MutableGame() { return &game_; }
+
+ const ContextEvents& Events() const { return events_; }
+ ContextEvents* MutableEvents() { return &events_; }
+
+ const ContextObservations& Observations() const { return observations_; }
+ ContextObservations* MutableObservations() { return &observations_; }
+
+ const ContextPickups& Pickups() const { return pickups_; }
+ ContextPickups* MutablePickups() { return &pickups_; }
+
+ const ContextEntities& GameEntities() const { return game_entities_; }
+ ContextEntities* MutableGameEntities() { return &game_entities_; }
private:
// Message to be placed on screen.
@@ -280,6 +389,16 @@ class Context {
int x;
int y;
int align_l0_r1_c2;
+ std::array rgba;
+ bool shadow;
+ };
+
+ struct FilledRectangle {
+ int x;
+ int y;
+ int width;
+ int height;
+ std::array rgba;
};
// Current action state.
@@ -292,17 +411,14 @@ class Context {
int buttons_down;
};
- // Entry for a custom observation spec.
- struct ObservationSpecInfo {
- std::string name;
- EnvCApi_ObservationType type;
- std::vector shape;
- };
+ // Subtracts the integral part from the stashed reward (see AddScore) and
+ // returns that integral part. The remaining stashed reward is smaller than
+ // one in magnitude. The returned (integral) value is suitable for the game
+ // server, which only deals in integral reward increments.
+ int ExternalReward(int player_id);
int CallInit();
- int CallObservationSpec();
-
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
@@ -322,8 +438,8 @@ class Context {
// The settings to run the script with.
std::unordered_map settings_;
- // The name of the script ran on first Init.
- std::string script_name_;
+ // The name of the script to run on first Init.
+ std::string script_path_;
// The result of the script that was run when Init was first called.
lua::TableRef script_table_ref_;
@@ -337,47 +453,60 @@ class Context {
// Cached map name to enable returning a pointer to its contents.
std::string map_name_;
- // Stores whether the internal controller will call SetActions.
- bool use_internal_controls_;
+ // Stores whether we are running a native app and the internal controller will
+ // call SetActions.
+ bool native_app_;
// Current actions to apply when lab is advanced.
Actions actions_;
- // Array of current custom pickup items. Reset each episode.
- std::vector items_;
-
- // Flag that can be set from the game to finish the current map.
- bool map_finished_;
+ // Current custom pickup model. Reset each episode.
+ std::string model_name_;
+ std::unique_ptr model_;
// Transient reward stash for each player. Rewards are added with AddScore and
// removed by ExternalReward.
std::vector player_rewards_;
- // Random seed used for this episode.
- int random_seed_;
-
// A pseudo-random-bit generator for exclusive use by users.
std::mt19937_64 user_prbg_;
// A pseudo-random-bit generator for exclusive use of the engine. Seeded each
- // episode with 'random_seed_'.
+ // episode with the episode start seed.
std::mt19937_64 engine_prbg_;
- // Storage of supplementary observation types from script.
- std::vector observation_infos_;
+ // A list of screen messages to display this frame.
+ std::vector screen_messages_;
- // Used to hold the EnvCApi_ObservationSpec::shape values until the next call
- // of observation.
- std::vector observation_tensor_shape_;
+ // A list of filled rectangles to display this frame.
+ std::vector filled_rectangles_;
- // Used to hold a reference to the observation tensor until the next call of
- // observation.
- lua::TableRef observation_tensor_;
+ bool use_local_level_cache_;
+ bool use_global_level_cache_;
- PlayerView predicted_player_view_;
+ // Callbacks for fetching/writing levels to cache.
+ DeepMindLabLevelCacheParams level_cache_params_;
- // A list of screen messages to display this frame.
- std::vector screen_messages_;
+ // Last error message.
+ std::string error_message_;
+
+ // An object for storing and retrieving events.
+ ContextEvents events_;
+
+ // An object for calling into the engine.
+ ContextGame game_;
+
+ // An object for storing and retrieving custom observations.
+ ContextObservations observations_;
+
+ // An object for interacting with pickups.
+ ContextPickups pickups_;
+
+ // An object for retrieving information about in game entities.
+ ContextEntities game_entities_;
+
+ // When enabled all entities are forced to be rendered.
+ bool has_alt_cameras_;
};
} // namespace lab
diff --git a/deepmind/engine/context_entities.cc b/deepmind/engine/context_entities.cc
new file mode 100644
index 00000000..c81231cc
--- /dev/null
+++ b/deepmind/engine/context_entities.cc
@@ -0,0 +1,107 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "deepmind/engine/context_entities.h"
+
+#include
+#include
+
+#include "deepmind/lua/class.h"
+#include "deepmind/lua/push.h"
+#include "deepmind/lua/read.h"
+#include "deepmind/lua/table_ref.h"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+class LuaEntitiesModule : public lua::Class {
+ friend class Class;
+ static const char* ClassName() { return "deepmind.lab.Entities"; }
+
+ public:
+ // '*ctx' owned by the caller and should out-live this object.
+ explicit LuaEntitiesModule(ContextEntities* ctx) : ctx_(ctx) {}
+
+ static void Register(lua_State* L) {
+ const Class::Reg methods[] = {
+ {"entities", Member<&LuaEntitiesModule::Entities>},
+ };
+ Class::Register(L, methods);
+ }
+
+ private:
+ // Returns a list of entities to Lua.
+ // [0, 1, -]
+ lua::NResultsOr Entities(lua_State* L) {
+ constexpr int kEntityNotVisibilityFlag = 0x80;
+ lua::TableRef table = lua::TableRef::Create(L);
+ int row_idx = 0;
+ std::vector filter;
+ lua::Read(L, 2, &filter);
+ for (const auto& row : ctx_->Entities()) {
+ if (filter.empty() || std::find(filter.begin(), filter.end(),
+ row.class_name) != filter.end()) {
+ lua::TableRef entity = table.CreateSubTable(++row_idx);
+ entity.Insert("entityId", row.entity_id + 1);
+ entity.Insert("id", row.user_id);
+ entity.Insert("type", row.type);
+ entity.Insert("visible", (row.flags & kEntityNotVisibilityFlag) == 0);
+ entity.Insert("position", row.position);
+ entity.Insert("classname", row.class_name);
+ }
+ }
+ lua::Push(L, table);
+ return 1;
+ }
+
+ ContextEntities* ctx_;
+};
+
+} // namespace
+
+lua::NResultsOr ContextEntities::Module(lua_State* L) {
+ if (auto* ctx = static_cast(
+ lua_touserdata(L, lua_upvalueindex(1)))) {
+ LuaEntitiesModule::Register(L);
+ LuaEntitiesModule::CreateObject(L, ctx);
+ return 1;
+ } else {
+ return "Missing context!";
+ }
+}
+
+void ContextEntities::Clear() {
+ entities_.clear();
+}
+
+void ContextEntities::Add(int entity_id, int user_id, int type, int flags,
+ const float position[3], const char* classname) {
+ entities_.emplace_back();
+ Entity& entity = entities_.back();
+ entity.entity_id = entity_id;
+ entity.user_id = user_id;
+ entity.type = type;
+ entity.flags = flags;
+ std::copy_n(position, 3, entity.position.begin());
+ entity.class_name = classname;
+}
+
+} // namespace lab
+} // namespace deepmind
+
diff --git a/deepmind/engine/context_entities.h b/deepmind/engine/context_entities.h
new file mode 100644
index 00000000..fee47739
--- /dev/null
+++ b/deepmind/engine/context_entities.h
@@ -0,0 +1,63 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef DML_DEEPMIND_ENGINE_CONTEXT_ENTITIES_H_
+#define DML_DEEPMIND_ENGINE_CONTEXT_ENTITIES_H_
+
+#include
+#include
+
+#include "deepmind/lua/lua.h"
+#include "deepmind/lua/n_results_or.h"
+
+namespace deepmind {
+namespace lab {
+
+// Receive calls from lua_script.
+class ContextEntities {
+ public:
+ struct Entity {
+ int entity_id;
+ int user_id;
+ int type;
+ int flags;
+ std::array position;
+ std::string class_name;
+ };
+
+ // Returns an entity module. A pointer to ContextEntities must exist in the up
+ // value. [0, 1, e]
+ static lua::NResultsOr Module(lua_State* L);
+
+ // Clears all entities and players. (Called at start of entity update.)
+ void Clear();
+
+ // Called on each entity every frame.
+ void Add(int entity_id, int user_id, int type, int flags,
+ const float position[3], const char* classname);
+
+ const std::vector& Entities() const { return entities_; }
+
+ private:
+ std::vector entities_; // Entities active this frame.
+};
+
+} // namespace lab
+} // namespace deepmind
+
+#endif // DML_DEEPMIND_ENGINE_CONTEXT_ENTITIES_H_
diff --git a/deepmind/engine/context_entities_test.cc b/deepmind/engine/context_entities_test.cc
new file mode 100644
index 00000000..236364ed
--- /dev/null
+++ b/deepmind/engine/context_entities_test.cc
@@ -0,0 +1,68 @@
+#include "deepmind/engine/context_entities.h"
+
+#include "gtest/gtest.h"
+#include "absl/types/span.h"
+#include "deepmind/lua/bind.h"
+#include "deepmind/lua/call.h"
+#include "deepmind/lua/n_results_or_test_util.h"
+#include "deepmind/lua/push.h"
+#include "deepmind/lua/push_script.h"
+#include "deepmind/lua/read.h"
+#include "deepmind/lua/table_ref.h"
+#include "deepmind/lua/vm_test_util.h"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+using ::deepmind::lab::lua::testing::IsOkAndHolds;
+
+class ContextEntitiesTest : public lua::testing::TestWithVm {
+ protected:
+ ContextEntitiesTest() {
+ vm()->AddCModuleToSearchers("dmlab.system.game_entities",
+ lua::Bind, {&ctx_});
+ }
+
+ ContextEntities ctx_;
+};
+
+constexpr char kGetEntity[] = R"(
+local game_entities = require 'dmlab.system.game_entities'
+local index = ...
+return game_entities:entities()[index]
+)";
+
+TEST_F(ContextEntitiesTest, UpdateEntities) {
+ for (int i = 0; i < 10; ++i) {
+ int entity_id = i * 10;
+ int type = 10;
+ int flags = 0x80;
+ float position[3] = {i * 1.0f, i * 2.0f, i * 3.0f};
+ ctx_.Add(entity_id, 0, type, flags, position, "classname");
+ }
+
+ const int index = 4;
+
+ lua::PushScript(L, kGetEntity, sizeof(kGetEntity) - 1, "kGetEntity");
+ lua::Push(L, index + 1);
+ ASSERT_THAT(lua::Call(L, 1), IsOkAndHolds(1));
+ lua::TableRef table;
+ ASSERT_TRUE(lua::Read(L, 1, &table));
+
+ int val = 0;
+ EXPECT_TRUE(table.LookUp("entityId", &val));
+ EXPECT_EQ(val, index * 10 + 1);
+ bool visible = true;
+ EXPECT_TRUE(table.LookUp("visible", &visible));
+ EXPECT_FALSE(visible);
+ float new_position[3];
+ EXPECT_TRUE(table.LookUp("position", absl::MakeSpan(new_position)));
+ float old_position[3] = {index * 1.0f, index * 2.0f, index * 3.0f};
+ EXPECT_EQ(absl::MakeConstSpan(old_position),
+ absl::MakeConstSpan(new_position));
+}
+
+} // namespace
+} // namespace lab
+} // namespace deepmind
diff --git a/deepmind/engine/context_events.cc b/deepmind/engine/context_events.cc
new file mode 100644
index 00000000..02c62fd4
--- /dev/null
+++ b/deepmind/engine/context_events.cc
@@ -0,0 +1,184 @@
+#include "deepmind/engine/context_events.h"
+
+#include
+
+#include "deepmind/lua/class.h"
+#include "deepmind/lua/lua.h"
+#include "deepmind/lua/read.h"
+#include "deepmind/tensor/lua_tensor.h"
+#include "deepmind/tensor/tensor_view.h"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+class LuaEventsModule : public lua::Class {
+ friend class Class;
+ static const char* ClassName() { return "deepmind.lab.Events"; }
+
+ public:
+ // '*ctx' owned by the caller and should out-live this object.
+ explicit LuaEventsModule(ContextEvents* ctx) : ctx_(ctx) {}
+
+ // Registers classes metatable with Lua.
+ static void Register(lua_State* L) {
+ const Class::Reg methods[] = {{"add", Member<&LuaEventsModule::Add>}};
+ Class::Register(L, methods);
+ }
+
+ private:
+ template
+ void AddTensorObservation(int id, const tensor::TensorView& view) {
+ const auto& shape = view.shape();
+ std::vector out_shape(shape.begin(), shape.end());
+
+ std::vector out_values;
+ out_values.reserve(view.num_elements());
+ view.ForEach([&out_values](T v) { out_values.push_back(v); });
+ ctx_->AddObservation(id, std::move(out_shape), std::move(out_values));
+ }
+
+ // Signature events:add(eventName, [obs1, [obs2 ...] ...])
+ // Called with an event name and a list of observations. Each observation
+ // maybe one of string, ByteTensor or DoubleTensor.
+ // [-(2 + #observations), 0, e]
+ lua::NResultsOr Add(lua_State* L) {
+ int top = lua_gettop(L);
+ std::string name;
+ if (!lua::Read(L, 2, &name)) {
+ return "Event name must be a string";
+ }
+ int id = ctx_->Add(std::move(name));
+ for (int i = 3; i <= top; ++i) {
+ std::string string_arg;
+ if (lua::Read(L, i, &string_arg)) {
+ ctx_->AddObservation(id, std::move(string_arg));
+ } else if (auto* double_tensor =
+ tensor::LuaTensor::ReadObject(L, i)) {
+ AddTensorObservation(id, double_tensor->tensor_view());
+ } else if (auto* byte_tensor =
+ tensor::LuaTensor::ReadObject(L, i)) {
+ AddTensorObservation(id, byte_tensor->tensor_view());
+ } else {
+ return "[event] - Observation type not supported. Must be one of "
+ "string|ByteTensor|DoubleTensor.";
+ }
+ }
+ return 0;
+ }
+
+ ContextEvents* ctx_;
+};
+
+} // namespace
+
+lua::NResultsOr ContextEvents::Module(lua_State* L) {
+ if (auto* ctx =
+ static_cast(lua_touserdata(L, lua_upvalueindex(1)))) {
+ LuaEventsModule::Register(L);
+ LuaEventsModule::CreateObject(L, ctx);
+ return 1;
+ } else {
+ return "Missing event context!";
+ }
+}
+
+int ContextEvents::Add(std::string name) {
+ auto iter_inserted = name_to_id_.emplace(std::move(name), names_.size());
+ if (iter_inserted.second) {
+ names_.push_back(iter_inserted.first->first.c_str());
+ }
+
+ int id = events_.size();
+ events_.push_back(Event{iter_inserted.first->second});
+ return id;
+}
+
+void ContextEvents::AddObservation(int event_id, std::string string_value) {
+ Event& event = events_[event_id];
+ event.observations.emplace_back();
+ auto& observation = event.observations.back();
+ observation.type = EnvCApi_ObservationString;
+
+ observation.shape_id = shapes_.size();
+ std::vector shape(1);
+ shape[0] = string_value.size();
+ shapes_.emplace_back(std::move(shape));
+
+ observation.array_id = strings_.size();
+ strings_.push_back(std::move(string_value));
+}
+
+void ContextEvents::AddObservation(int event_id, std::vector shape,
+ std::vector double_tensor) {
+ Event& event = events_[event_id];
+ event.observations.emplace_back();
+ auto& observation = event.observations.back();
+ observation.type = EnvCApi_ObservationDoubles;
+
+ observation.shape_id = shapes_.size();
+ shapes_.push_back(std::move(shape));
+
+ observation.array_id = doubles_.size();
+ doubles_.push_back(std::move(double_tensor));
+}
+
+void ContextEvents::AddObservation(int event_id, std::vector shape,
+ std::vector byte_tensor) {
+ Event& event = events_[event_id];
+ event.observations.emplace_back();
+ auto& observation = event.observations.back();
+ observation.type = EnvCApi_ObservationBytes;
+
+ observation.shape_id = shapes_.size();
+ shapes_.push_back(std::move(shape));
+
+ observation.array_id = bytes_.size();
+ bytes_.push_back(std::move(byte_tensor));
+}
+
+void ContextEvents::Clear() {
+ events_.clear();
+ strings_.clear();
+ shapes_.clear();
+ doubles_.clear();
+ bytes_.clear();
+}
+
+void ContextEvents::Export(int event_idx, EnvCApi_Event* event) {
+ const auto& internal_event = events_[event_idx];
+ observations_.clear();
+ observations_.reserve(internal_event.observations.size());
+ for (const auto& observation : internal_event.observations) {
+ observations_.emplace_back();
+ auto& observation_out = observations_.back();
+ observation_out.spec.type = observation.type;
+
+ const auto& shape = shapes_[observation.shape_id];
+ observation_out.spec.dims = shape.size();
+ observation_out.spec.shape = shape.data();
+
+ switch (observation.type) {
+ case EnvCApi_ObservationBytes: {
+ const auto& tensor = bytes_[observation.array_id];
+ observation_out.payload.bytes = tensor.data();
+ break;
+ }
+ case EnvCApi_ObservationDoubles: {
+ const auto& tensor = doubles_[observation.array_id];
+ observation_out.payload.doubles = tensor.data();
+ break;
+ }
+ case EnvCApi_ObservationString:
+ const auto& string_value = strings_[observation.array_id];
+ observation_out.payload.string = string_value.c_str();
+ break;
+ }
+ }
+ event->id = internal_event.type_id;
+ event->observations = observations_.data();
+ event->observation_count = observations_.size();
+}
+
+} // namespace lab
+} // namespace deepmind
diff --git a/deepmind/engine/context_events.h b/deepmind/engine/context_events.h
new file mode 100644
index 00000000..960d145f
--- /dev/null
+++ b/deepmind/engine/context_events.h
@@ -0,0 +1,103 @@
+#ifndef DML_DEEPMIND_ENGINE_CONTEXT_EVENTS_H_
+#define DML_DEEPMIND_ENGINE_CONTEXT_EVENTS_H_
+
+#include
+#include
+#include
+
+#include "deepmind/lua/lua.h"
+#include "deepmind/lua/n_results_or.h"
+#include "third_party/rl_api/env_c_api.h"
+
+namespace deepmind {
+namespace lab {
+
+// Support class for storing events generated from Lua. These events can be read
+// out of DM Lab using the events part of the EnvCApi. (See: env_c_api.h.)
+//
+// Each event contains a list of observations. Each observation type is one of
+// EnvCApi_Observation{Doubles,Bytes,String}.
+class ContextEvents {
+ public:
+ // Returns an event module. A pointer to ContextEvents must exist in the up
+ // value. [0, 1, -]
+ static lua::NResultsOr Module(lua_State* L);
+
+ // Adds event returning its index.
+ int Add(std::string name);
+
+ // Adds string observation to event at index 'event_id'.
+ void AddObservation(int event_id, std::string string_value);
+
+ // Adds DoubleTensor observation to event at index 'event_id'.
+ void AddObservation(int event_id, std::vector shape,
+ std::vector double_tensor);
+
+ // Adds ByteTensor observation to event at index 'event_id'.
+ void AddObservation(int event_id, std::vector shape,
+ std::vector byte_tensor);
+
+ // Exports an event at 'event_idx', which must be in range [0, Count()), to an
+ // EnvCApi_Event structure. Observations within the `event` are invalidated by
+ // calls to non-const methods.
+ void Export(int event_idx, EnvCApi_Event* event);
+
+ // Returns the number of events created since last call to ClearEvents().
+ int Count() const { return events_.size(); }
+
+ // Returns the number of event types.
+ int TypeCount() const { return names_.size(); }
+
+ // Returns the name of the event associated with event_type_id, which must be
+ // in range [0, EventTypeCount()). New events types maybe added at any point
+ // but the event_type_ids remain stable.
+ const char* TypeName(int event_type_id) const {
+ return names_[event_type_id];
+ }
+
+ // Clears all the events and their observations.
+ void Clear();
+
+ private:
+ struct Event {
+ int type_id; // Event type id.
+
+ struct Observation {
+ EnvCApi_ObservationType_enum type;
+
+ int shape_id; // Index in shapes_ for shape of this observation.
+
+ // Index of observation data. The array depends on type.
+ // If type == EnvCApi_ObservationDoubles then index in doubles_.
+ // If type == EnvCApi_ObservationBytes then index in bytes_.
+ // If type == EnvCApi_ObservationString then index in string_.
+ int array_id;
+ };
+
+ // List of observations associated with event.
+ std::vector observations;
+ };
+
+ // Events generated since construction or last call to Clear().
+ std::vector events_;
+
+ // Lookup type_id to event_name.
+ std::vector names_;
+
+ // Reverse lookup of event_name to type_id.
+ std::unordered_map name_to_id_;
+
+ // Event observation storage.
+ std::vector> shapes_;
+ std::vector> bytes_;
+ std::vector> doubles_;
+ std::vector strings_;
+
+ // Temporary EnvCApi_Observation observation storage.
+ std::vector observations_;
+};
+
+} // namespace lab
+} // namespace deepmind
+
+#endif // DML_DEEPMIND_ENGINE_CONTEXT_EVENTS_H_
diff --git a/deepmind/engine/context_game.cc b/deepmind/engine/context_game.cc
new file mode 100644
index 00000000..6b218da5
--- /dev/null
+++ b/deepmind/engine/context_game.cc
@@ -0,0 +1,337 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "deepmind/engine/context_game.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "deepmind/lua/class.h"
+#include "deepmind/lua/push.h"
+#include "deepmind/lua/read.h"
+#include "deepmind/lua/table_ref.h"
+#include "deepmind/tensor/lua_tensor.h"
+#include "deepmind/tensor/tensor_view.h"
+#include "deepmind/util/files.h"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+class StorageFreeChar : public tensor::StorageValidity {
+ public:
+ StorageFreeChar(char* data) : StorageValidity(kOwnsStorage), data_(data) {}
+ ~StorageFreeChar() { free(data_); }
+ unsigned char* data() { return reinterpret_cast(data_); }
+
+ private:
+ char* data_;
+};
+
+class StorageString : public tensor::StorageValidity {
+ public:
+ StorageString(std::string data)
+ : StorageValidity(kOwnsStorage), data_(std::move(data)) {}
+ unsigned char* data() { return reinterpret_cast(&data_[0]); }
+
+ private:
+ std::string data_;
+};
+
+class LuaGameModule : public lua::Class {
+ friend class Class;
+ static const char* ClassName() { return "deepmind.lab.Game"; }
+
+ public:
+ // '*ctx' owned by the caller and should out-live this object.
+ explicit LuaGameModule(ContextGame* ctx) : ctx_(ctx) {}
+
+ static void Register(lua_State* L) {
+ const Class::Reg methods[] = {
+ {"addScore", Member<&LuaGameModule::AddScore>},
+ {"finishMap", Member<&LuaGameModule::FinishMap>},
+ {"playerInfo", Member<&LuaGameModule::PlayerInfo>},
+ {"updateTexture", Member<&LuaGameModule::UpdateTexture>},
+ {"episodeTimeSeconds", Member<&LuaGameModule::EpisodeTimeSeconds>},
+ {"tempFolder", Member<&LuaGameModule::TempFolder>},
+ {"runFiles", Member<&LuaGameModule::ExecutableRunfiles>},
+ {"raycast", Member<&LuaGameModule::Raycast>},
+ {"loadFileToByteTensor", Member<&LuaGameModule::LoadFileToByteTensor>},
+ {"loadFileToString", Member<&LuaGameModule::LoadFileToString>},
+ {"copyFileToLocation", Member<&LuaGameModule::CopyFileToLocation>},
+ };
+ Class::Register(L, methods);
+ }
+
+ private:
+ lua::NResultsOr AddScore(lua_State* L) {
+ int player_id = 0;
+ double score = 0;
+ if (lua::Read(L, 2, &player_id) && lua::Read(L, 3, &score) &&
+ 0 <= player_id && player_id < 64) {
+ ctx_->Calls()->add_score(player_id, score);
+ return 0;
+ }
+ std::string error = "Invalid arguments player_id: ";
+ error += lua::ToString(L, 2);
+ error += " or reward: ";
+ error += lua::ToString(L, 3);
+ return std::move(error);
+ }
+
+ lua::NResultsOr Raycast(lua_State* L) {
+ std::array start, end;
+ if (lua::Read(L, 2, &start) && lua::Read(L, 3, &end)) {
+ lua::Push(L, ctx_->Calls()->raycast(start.data(), end.data()));
+ return 1;
+ }
+ return "Must provide start and end coordinates";
+ }
+
+ lua::NResultsOr FinishMap(lua_State* L) {
+ ctx_->SetMapFinished(true);
+ return 0;
+ }
+
+ lua::NResultsOr PlayerInfo(lua_State* L) {
+ const auto& pv = ctx_->GetPlayerView();
+ auto table = lua::TableRef::Create(L);
+ table.Insert("pos", pv.pos);
+ table.Insert("vel", pv.vel);
+ table.Insert("angles", pv.angles);
+ table.Insert("anglesVel", pv.anglesVel);
+ table.Insert("height", pv.height);
+ table.Insert("playerId", pv.player_id + 1);
+ table.Insert("teamScore", pv.team_score);
+ table.Insert("otherTeamScore", pv.other_team_score);
+ lua::Push(L, table);
+ return 1;
+ }
+
+ lua::NResultsOr UpdateTexture(lua_State* L) {
+ std::string name;
+ if (!lua::Read(L, 2, &name)) {
+ std::string error = "Invalid argument name: ";
+ error += lua::ToString(L, 2);
+ return std::move(error);
+ }
+ auto* data = tensor::LuaTensor::ReadObject(L, 3);
+ if (data == nullptr) {
+ std::string error = "Invalid argument data: ";
+ error += lua::ToString(L, 3);
+ return std::move(error);
+ }
+ const auto& tensor_view = data->tensor_view();
+ const auto& shape = tensor_view.shape();
+ if (shape.size() != 3 || shape[2] != 4) {
+ return "Invalid dimensions for argument data";
+ }
+ bool success = ctx_->Calls()->update_rgba_texture(
+ name.c_str(), shape[1], shape[0], tensor_view.storage());
+ if (!success) {
+ std::string error = "The texture named: '";
+ error += name;
+ error += "' has not been updated";
+ return error;
+ }
+ return 0;
+ }
+
+ lua::NResultsOr EpisodeTimeSeconds(lua_State* L) {
+ lua::Push(L, ctx_->Calls()->total_time_seconds());
+ return 1;
+ }
+
+ lua::NResultsOr TempFolder(lua_State* L) {
+ lua::Push(L, ctx_->TempFolder());
+ return 1;
+ }
+
+ lua::NResultsOr ExecutableRunfiles(lua_State* L) {
+ lua::Push(L, ctx_->ExecutableRunfiles());
+ return 1;
+ }
+
+ lua::NResultsOr LoadFileToString(lua_State* L) {
+ std::string file_name;
+ if (!lua::Read(L, -1, &file_name)) {
+ return "Must supply file name.";
+ }
+ if (ctx_->FileReaderOverride()) {
+ size_t size = 0;
+ char* buff = nullptr;
+ if (!ctx_->FileReaderOverride()(file_name.c_str(), &buff, &size)) {
+ return "File not found!";
+ }
+ lua_pushlstring(L, buff, size);
+ free(buff);
+ } else {
+ std::string contents;
+ if (!util::GetContents(file_name, &contents)) {
+ return "File not found!";
+ }
+ lua::Push(L, contents);
+ }
+ return 1;
+ }
+
+ lua::NResultsOr LoadFileToByteTensor(lua_State* L) {
+ std::string file_name;
+ if (!lua::Read(L, 2, &file_name)) {
+ return "Must supply file name.";
+ }
+
+ if (ctx_->FileReaderOverride()) {
+ size_t size = 0;
+ char* buff = nullptr;
+ if (!ctx_->FileReaderOverride()(file_name.c_str(), &buff, &size)) {
+ return "File not found!";
+ }
+
+ auto storage = std::make_shared(buff);
+ tensor::TensorView tensor_view(tensor::Layout({size}),
+ storage->data());
+ tensor::LuaTensor::CreateObject(L, std::move(tensor_view),
+ std::move(storage));
+ } else {
+ std::string data;
+ if (!util::GetContents(file_name, &data)) {
+ return "File not found!";
+ }
+ auto size = data.size();
+ auto storage = std::make_shared(std::move(data));
+ tensor::TensorView tensor_view(tensor::Layout({size}),
+ storage->data());
+
+ tensor::LuaTensor::CreateObject(L, std::move(tensor_view),
+ std::move(storage));
+ }
+ return 1;
+ }
+
+ lua::NResultsOr CopyFileToLocation(lua_State* L) {
+ std::string file_name_from, file_name_to;
+ if (!lua::Read(L, 2, &file_name_from)) {
+ return "Must supply from file name.";
+ }
+ if (!lua::Read(L, 3, &file_name_to)) {
+ return "Must supply from file name to.";
+ }
+
+ if (ctx_->FileReaderOverride()) {
+ size_t size = 0;
+ char* buff = nullptr;
+ if (!ctx_->FileReaderOverride()(file_name_from.c_str(), &buff, &size)) {
+ return "File not found!";
+ }
+ bool success =
+ util::SetContents(file_name_to, absl::string_view(buff, size));
+ free(buff);
+ if (!success) {
+ return "Failed to write file";
+ }
+ } else {
+ std::string contents;
+ if (!util::GetContents(file_name_from, &contents)) {
+ return "File not found!";
+ }
+ bool success = util::SetContents(file_name_to, contents);
+ if (!success) {
+ return "Failed to write file";
+ }
+ }
+ return 1;
+ }
+
+ ContextGame* ctx_;
+};
+
+// Returns the unique value in the range [-180, 180) that is equivalent to
+// 'angle', where two values x and y are considered equivalent whenever x - y
+// is an integral multiple of 360. (Note: the result may be meaningless if the
+// magnitude of 'angle' is very large.)
+double CanonicalAngle360(double angle) {
+ const double n = std::floor((angle + 180.0) * (1.0 / 360.0));
+ return angle - n * 360.0;
+}
+
+} // namespace
+
+int ContextGame::Init() {
+ temp_folder_owned_ = temp_folder_.empty();
+ if (temp_folder_owned_) {
+ temp_folder_ = util::GetTempDirectory() + "/dmlab_temp_folder_XXXXXX";
+ char* temp_folder_result = mkdtemp(&temp_folder_[0]);
+ if (temp_folder_result == nullptr) {
+ std::cerr << "Failed to create temp folder\n";
+ return 1;
+ }
+ }
+ return 0;
+}
+
+ContextGame::~ContextGame() {
+ if (!temp_folder_.empty() && temp_folder_owned_) {
+ util::RemoveDirectory(temp_folder_);
+ }
+}
+
+lua::NResultsOr ContextGame::Module(lua_State* L) {
+ if (auto* ctx =
+ static_cast(lua_touserdata(L, lua_upvalueindex(1)))) {
+ LuaGameModule::Register(L);
+ LuaGameModule::CreateObject(L, ctx);
+ return 1;
+ } else {
+ return "Missing context!";
+ }
+}
+
+void ContextGame::SetPlayerState(const float pos[3], const float vel[3],
+ const float angles[3], float height,
+ int team_score, int other_team_score,
+ int player_id, int timestamp_msec) {
+ PlayerView before = player_view_;
+ std::copy_n(pos, 3, player_view_.pos.begin());
+ std::copy_n(vel, 3, player_view_.vel.begin());
+ std::copy_n(angles, 3, player_view_.angles.begin());
+ player_view_.height = height;
+ player_view_.timestamp_msec = timestamp_msec;
+ player_view_.player_id = player_id;
+ player_view_.team_score = team_score;
+ player_view_.other_team_score = other_team_score;
+ int delta_time_msec = player_view_.timestamp_msec - before.timestamp_msec;
+
+ // When delta_time_msec < 3 the velocities become inaccurate.
+ if (before.timestamp_msec > 0 && delta_time_msec > 0) {
+ double inv_delta_time = 1000.0 / delta_time_msec;
+ for (int i : {0, 1, 2}) {
+ player_view_.anglesVel[i] =
+ CanonicalAngle360(player_view_.angles[i] - before.angles[i]) *
+ inv_delta_time;
+ }
+ } else {
+ player_view_.anglesVel.fill(0);
+ }
+}
+
+} // namespace lab
+} // namespace deepmind
diff --git a/deepmind/engine/context_game.h b/deepmind/engine/context_game.h
new file mode 100644
index 00000000..31aac5b7
--- /dev/null
+++ b/deepmind/engine/context_game.h
@@ -0,0 +1,128 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef DML_DEEPMIND_ENGINE_CONTEXT_GAME_H_
+#define DML_DEEPMIND_ENGINE_CONTEXT_GAME_H_
+
+#include
+
+#include
+#include
+#include
+
+#include "deepmind/include/deepmind_calls.h"
+#include "deepmind/lua/lua.h"
+#include "deepmind/lua/n_results_or.h"
+
+namespace deepmind {
+namespace lab {
+
+// Represents a player's state in world units.
+struct PlayerView {
+ std::array pos; // Position (forward, left, up).
+ std::array vel; // World velocity (forward, left, up).
+ std::array angles; // Orientation degrees (pitch, yaw, roll).
+ std::array anglesVel; // Angular velocity in degrees.
+ int team_score; // Number of times we captured a flag.
+ int other_team_score; // Number of times others captured a flag.
+ int player_id;
+ int timestamp_msec; // Engine time in msec of the view.
+ double height; // View height.
+};
+
+// Receive calls from lua_script.
+class ContextGame {
+ public:
+ // Optional override for reading contents of a file.
+ using Reader = bool (*)(const char* file_name, char** buff, size_t* size);
+
+ ContextGame(const char* executable_runfiles,
+ const DeepmindCalls* deepmind_calls, Reader file_reader_override,
+ std::string temp_folder)
+ : deepmind_calls_(deepmind_calls),
+ map_finished_(false),
+ player_view_{},
+ executable_runfiles_(executable_runfiles),
+ file_reader_override_(file_reader_override),
+ temp_folder_(std::move(temp_folder)) {}
+
+ ~ContextGame();
+
+ // Returns an event module. A pointer to ContextGame must exist in the up
+ // value. [0, 1, -]
+ static lua::NResultsOr Module(lua_State* L);
+
+ int Init();
+ void NextMap() { player_view_.timestamp_msec = 0; }
+
+ // Returns whether we should finish the current map.
+ bool MapFinished() const { return map_finished_; }
+
+ // Sets whether the current map should finish.
+ void SetMapFinished(bool map_finished) { map_finished_ = map_finished; }
+
+ // The path level scripts should use for temporary files.
+ const std::string& TempFolder() const { return temp_folder_; }
+
+ const std::string& ExecutableRunfiles() const { return executable_runfiles_; }
+
+ const DeepmindCalls* Calls() const { return deepmind_calls_; }
+
+ void AddTextureHandle(std::string name, int handle);
+
+ // Retrieves the handle for a named texture marked for update.
+ // Returns whether a matching handle was found.
+ bool TextureHandle(const std::string& name, int* handle) const;
+
+ // Set latest player state.
+ void SetPlayerState(const float pos[3], const float vel[3],
+ const float angles[3], float height,
+ int team_score, int other_team_score,
+ int player_id, int timestamp_msec);
+
+ // Get latest predicted player view. (This is where the game renders from.)
+ const PlayerView& GetPlayerView() {
+ return player_view_;
+ }
+
+ Reader FileReaderOverride() { return file_reader_override_; }
+
+ private:
+ // Calls into the engine.
+ const DeepmindCalls* deepmind_calls_;
+
+ // Flag that can be set from the game to finish the current map.
+ bool map_finished_;
+
+ PlayerView player_view_;
+
+ // Path to executables runfiles.
+ std::string executable_runfiles_;
+
+ // Optional override for reading contents of a file.
+ Reader file_reader_override_;
+
+ // The path level scripts should use for temporary files.
+ std::string temp_folder_;
+ bool temp_folder_owned_;
+};
+
+} // namespace lab
+} // namespace deepmind
+
+#endif // DML_DEEPMIND_ENGINE_CONTEXT_GAME_H_
diff --git a/deepmind/engine/context_observations.cc b/deepmind/engine/context_observations.cc
new file mode 100644
index 00000000..2452fe2d
--- /dev/null
+++ b/deepmind/engine/context_observations.cc
@@ -0,0 +1,158 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "deepmind/engine/context_observations.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "deepmind/support/logging.h"
+#include "deepmind/lua/call.h"
+#include "deepmind/lua/push.h"
+#include "deepmind/lua/read.h"
+#include "deepmind/tensor/lua_tensor.h"
+#include "deepmind/tensor/tensor_view.h"
+
+namespace deepmind {
+namespace lab {
+
+int ContextObservations::ReadSpec(lua::TableRef script_table_ref) {
+ script_table_ref_ = std::move(script_table_ref);
+ script_table_ref_.PushMemberFunction("customObservationSpec");
+ lua_State* L = script_table_ref_.LuaState();
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return 0;
+ }
+ auto result = lua::Call(L, 1);
+ if (!result.ok()) {
+ std::cerr << result.error() << '\n';
+ return 1;
+ }
+ lua::TableRef observations;
+ lua::Read(L, -1, &observations);
+ auto spec_count = observations.ArraySize();
+ infos_.clear();
+ infos_.reserve(spec_count);
+ for (std::size_t i = 0, c = spec_count; i != c; ++i) {
+ lua::TableRef info;
+ observations.LookUp(i + 1, &info);
+ SpecInfo spec_info;
+ if (!info.LookUp("name", &spec_info.name)) {
+ std::cerr << "[customObservationSpec] - Missing 'name = '.\n";
+ return 1;
+ }
+ std::string type = "Doubles";
+ info.LookUp("type", &type);
+ if (type.compare("Bytes") == 0) {
+ spec_info.type = EnvCApi_ObservationBytes;
+ } else if (type.compare("Doubles") == 0) {
+ spec_info.type = EnvCApi_ObservationDoubles;
+ } else if (type.compare("String") == 0) {
+ spec_info.type = EnvCApi_ObservationString;
+ } else {
+ std::cerr << "[customObservationSpec] - Missing 'type = "
+ "'Bytes'|'Doubles'|'String''.\n";
+ return 1;
+ }
+ if (!info.LookUp("shape", &spec_info.shape)) {
+ std::cerr
+ << "[customObservationSpec] - Missing 'shape = {, ...}'.\n";
+ return 1;
+ }
+ infos_.push_back(std::move(spec_info));
+ }
+ lua_pop(L, result.n_results());
+ return 0;
+}
+
+void ContextObservations::Observation(int idx,
+ EnvCApi_Observation* observation) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("customObservation");
+ // Function must exist.
+ CHECK(!lua_isnil(L, -2))
+ << "Observations Spec set but no observation member function";
+ const auto& info = infos_[idx];
+ lua::Push(L, info.name);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << "[customObservation] - " << result.error();
+
+ const tensor::Layout* layout = nullptr;
+ observation->spec.type = info.type;
+ switch (info.type) {
+ case EnvCApi_ObservationDoubles: {
+ const char error_message[] =
+ "[customObservation] - Must return a contiguous DoubleTensor";
+ CHECK_EQ(1, result.n_results()) << error_message;
+ auto* double_tensor = tensor::LuaTensor::ReadObject(L, -1);
+ CHECK(double_tensor != nullptr) << error_message;
+ const auto& view = double_tensor->tensor_view();
+ CHECK(view.IsContiguous()) << error_message;
+ layout = &view;
+ observation->payload.doubles = view.storage() + view.start_offset();
+ break;
+ }
+ case EnvCApi_ObservationBytes: {
+ const char error_message[] =
+ "[customObservation] - Must return a contiguous ByteTensor";
+ CHECK_EQ(1, result.n_results()) << error_message;
+ auto* byte_tensor = tensor::LuaTensor::ReadObject(L, -1);
+ CHECK(byte_tensor != nullptr) << error_message;
+ const auto& view = byte_tensor->tensor_view();
+ layout = &view;
+ CHECK(view.IsContiguous()) << error_message;
+ observation->payload.bytes = view.storage() + view.start_offset();
+ break;
+ }
+ case EnvCApi_ObservationString: {
+ const char error_message[] = "[customObservation] - Must return a string";
+ CHECK_EQ(1, result.n_results()) << error_message;
+ CHECK(lua::Read(L, -1, &string_)) << error_message;
+ observation->payload.string = string_.data();
+ tensor_shape_.assign(1, string_.length());
+ observation->spec.dims = tensor_shape_.size();
+ observation->spec.shape = tensor_shape_.data();
+ break;
+ }
+ }
+
+ if (layout != nullptr) {
+ tensor_shape_.resize(layout->shape().size());
+ std::copy(layout->shape().begin(), layout->shape().end(),
+ tensor_shape_.begin());
+ observation->spec.dims = tensor_shape_.size();
+ observation->spec.shape = tensor_shape_.data();
+ // Prevent observation->payload from being destroyed during pop.
+ lua::Read(L, -1, &tensor_);
+ }
+ lua_pop(L, result.n_results());
+}
+
+void ContextObservations::Spec(int idx, EnvCApi_ObservationSpec* spec) const {
+ const auto& info = infos_[idx];
+ spec->type = info.type;
+ spec->dims = info.shape.size();
+ spec->shape = info.shape.data();
+}
+
+} // namespace lab
+} // namespace deepmind
diff --git a/deepmind/engine/context_observations.h b/deepmind/engine/context_observations.h
new file mode 100644
index 00000000..e7fc34fc
--- /dev/null
+++ b/deepmind/engine/context_observations.h
@@ -0,0 +1,83 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef DML_DEEPMIND_ENGINE_CONTEXT_OBSERVATIONS_H_
+#define DML_DEEPMIND_ENGINE_CONTEXT_OBSERVATIONS_H_
+
+#include
+#include
+#include
+
+#include "deepmind/lua/lua.h"
+#include "deepmind/lua/n_results_or.h"
+#include "deepmind/lua/table_ref.h"
+#include "third_party/rl_api/env_c_api.h"
+
+namespace deepmind {
+namespace lab {
+
+class ContextObservations {
+ public:
+ // Reads the custom observation spec from the table passed in.
+ // Keeps a reference to the table for further calls. Returns 0 on success
+ // and non-zero on failure.
+ int ReadSpec(lua::TableRef script_table_ref);
+
+ // Script observation count.
+ int Count() const { return infos_.size(); }
+
+ // Script observation name. `idx` shall be in [0, Count()).
+ const char* Name(int idx) const {
+ return infos_[idx].name.c_str();
+ }
+
+ // Script observation spec. `idx` shall be in [0, Count()).
+ void Spec(int idx, EnvCApi_ObservationSpec* spec) const;
+
+ // Script observation. `idx` shall be in [0, Count()).
+ void Observation(int idx, EnvCApi_Observation* observation);
+
+ private:
+ // Entry for a custom observation spec.
+ struct SpecInfo {
+ std::string name;
+ EnvCApi_ObservationType type;
+ std::vector shape;
+ };
+
+ lua::TableRef script_table_ref_;
+
+ // Storage of supplementary observation types from script.
+ std::vector infos_;
+
+ // Used to hold the EnvCApi_ObservationSpec::shape values until the next call
+ // of observation.
+ std::vector tensor_shape_;
+
+ // Used to hold a reference to the observation tensor until the next call of
+ // observation.
+ lua::TableRef tensor_;
+
+ // Used to store the observation string until the next call of observation.
+ std::string string_;
+};
+
+} // namespace lab
+} // namespace deepmind
+
+#endif // DML_DEEPMIND_ENGINE_CONTEXT_OBSERVATIONS_H_
diff --git a/deepmind/engine/context_pickups.cc b/deepmind/engine/context_pickups.cc
new file mode 100644
index 00000000..73937a22
--- /dev/null
+++ b/deepmind/engine/context_pickups.cc
@@ -0,0 +1,265 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "deepmind/engine/context_pickups.h"
+
+#include
+#include
+#include
+
+#include "deepmind/support/logging.h"
+#include "deepmind/lua/call.h"
+#include "deepmind/lua/push.h"
+#include "deepmind/lua/read.h"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+constexpr int kMaxSpawnVars = 64;
+constexpr int kMaxSpawnVarChars = 4096;
+
+// If the string "arg" fits into the array pointed to by "dest" (including the
+// null terminator), copies the string into the array and returns true;
+// otherwise returns false.
+bool StringCopy(const std::string& arg, char* dest, std::size_t max_size) {
+ auto len = arg.length() + 1;
+ if (len <= max_size) {
+ std::copy_n(arg.c_str(), len, dest);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void ReadSpawnVars(const ContextPickups::EntityInstance& spawn_vars,
+ char* spawn_var_chars, int* num_spawn_var_chars,
+ int spawn_var_offsets[][2], int* num_spawn_vars) {
+ *num_spawn_var_chars = 0;
+ *num_spawn_vars = spawn_vars.size();
+ CHECK_NE(0, *num_spawn_vars) << "Must have spawn vars or return nil. (Make "
+ "sure all values are strings.)";
+ CHECK_LT(*num_spawn_vars, kMaxSpawnVars) << "Too many spawn vars!";
+ char* mem = spawn_var_chars;
+ auto it = spawn_vars.begin();
+ for (int i = 0; i < *num_spawn_vars; ++i) {
+ const auto& key = it->first;
+ const auto& value = it->second;
+ ++it;
+ std::size_t kl = key.length() + 1;
+ std::size_t vl = value.length() + 1;
+ *num_spawn_var_chars += kl + vl;
+ CHECK_LT(*num_spawn_var_chars, kMaxSpawnVarChars) << "Too large spawn vars";
+ std::copy(key.c_str(), key.c_str() + kl, mem);
+ spawn_var_offsets[i][0] = std::distance(spawn_var_chars, mem);
+ mem += kl;
+ std::copy(value.c_str(), value.c_str() + vl, mem);
+ spawn_var_offsets[i][1] = std::distance(spawn_var_chars, mem);
+ mem += vl;
+ }
+}
+
+} // namespace
+
+bool ContextPickups::UpdateSpawnVars(char* spawn_var_chars,
+ int* num_spawn_var_chars,
+ int spawn_var_offsets[][2],
+ int* num_spawn_vars) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("updateSpawnVars");
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return true;
+ }
+
+ auto table = lua::TableRef::Create(L);
+ for (int i = 0; i < *num_spawn_vars; ++i) {
+ table.Insert(std::string(spawn_var_chars + spawn_var_offsets[i][0]),
+ std::string(spawn_var_chars + spawn_var_offsets[i][1]));
+ }
+ lua::Push(L, table);
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << result.error();
+
+ // Nil return so spawn is ignored.
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return false;
+ }
+
+ EntityInstance entity;
+ lua::Read(L, -1, &entity);
+ lua_pop(L, result.n_results());
+ ReadSpawnVars(entity, spawn_var_chars, num_spawn_var_chars,
+ spawn_var_offsets, num_spawn_vars);
+ return true;
+}
+
+// Clears extra_spawn_vars_ and reads new entities from Lua.
+// Returns number of entities created.
+int ContextPickups::MakeExtraEntities() {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("extraEntities");
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return 0;
+ }
+ auto result = lua::Call(L, 1);
+ CHECK(result.ok()) << result.error();
+ // Nil return so no spawns returned.
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return 0;
+ }
+ extra_entities_.clear();
+ CHECK(lua::Read(L, -1, &extra_entities_))
+ << "[extraEntities] - Invalid return value";
+ lua_pop(L, result.n_results());
+ return extra_entities_.size();
+}
+
+// Read specific spawn var from extra_spawn_vars_. Shall be called after
+// with spawn_var_index in range [0, MakeExtraSpawnVars()).
+void ContextPickups::ReadExtraEntity( //
+ int entity_index, //
+ char* spawn_var_chars, //
+ int* num_spawn_var_chars, //
+ int spawn_var_offsets[][2], //
+ int* num_spawn_vars) {
+ CHECK(0 <= entity_index && entity_index < extra_entities_.size());
+ ReadSpawnVars(extra_entities_[entity_index], spawn_var_chars,
+ num_spawn_var_chars, spawn_var_offsets, num_spawn_vars);
+}
+
+bool ContextPickups::FindItem(const char* class_name, int* index) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("createPickup");
+
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return false;
+ }
+
+ lua::Push(L, class_name);
+
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << result.error();
+
+ // If no description is returned or the description is nil, don't create item.
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return false;
+ }
+
+ lua::TableRef table;
+ CHECK(Read(L, -1, &table)) << "Failed to read pickup table!";
+
+ PickupItem item = {};
+ CHECK(table.LookUp("name", &item.name));
+ CHECK(table.LookUp("classname", &item.class_name));
+ CHECK(table.LookUp("model", &item.model_name));
+ CHECK(table.LookUp("quantity", &item.quantity));
+ CHECK(table.LookUp("type", &item.type));
+
+ // Optional tag field.
+ table.LookUp("tag", &item.tag);
+
+ items_.push_back(item);
+ *index = ItemCount() - 1;
+
+ lua_pop(L, result.n_results());
+ return true;
+}
+
+bool ContextPickups::GetItem(int index, char* item_name, int max_item_name, //
+ char* class_name, int max_class_name, //
+ char* model_name, int max_model_name, //
+ int* quantity, int* type, int* tag) const {
+ CHECK_GE(index, 0) << "Index out of range!";
+ CHECK_LT(index, ItemCount()) << "Index out of range!";
+
+ const auto& item = items_[index];
+ CHECK(StringCopy(item.name, item_name, max_item_name));
+ CHECK(StringCopy(item.class_name, class_name, max_class_name));
+ CHECK(StringCopy(item.model_name, model_name, max_model_name));
+ *quantity = item.quantity;
+ *type = static_cast(item.type);
+ *tag = item.tag;
+ return true;
+}
+
+bool ContextPickups::CanPickup(int entity_id) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("canPickup");
+
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return true;
+ }
+
+ lua::Push(L, entity_id);
+
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << result.error();
+
+ // If nothing returned or the return is nil, the default.
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return true;
+ }
+
+ bool can_pickup = true;
+ CHECK(lua::Read(L, -1, &can_pickup))
+ << "Failed to read canPickup return value";
+
+ lua_pop(L, result.n_results());
+ return can_pickup;
+}
+
+bool ContextPickups::OverridePickup(int entity_id, int* respawn) {
+ lua_State* L = script_table_ref_.LuaState();
+ script_table_ref_.PushMemberFunction("pickup");
+
+ // Check function exists.
+ if (lua_isnil(L, -2)) {
+ lua_pop(L, 2);
+ return false;
+ }
+
+ lua::Push(L, entity_id);
+
+ auto result = lua::Call(L, 2);
+ CHECK(result.ok()) << result.error();
+
+ // If nothing returned or the return is nil, we're not overriding the
+ // pickup behaviour.
+ if (result.n_results() == 0 || lua_isnil(L, -1)) {
+ lua_pop(L, result.n_results());
+ return false;
+ }
+
+ CHECK(lua::Read(L, -1, respawn)) << "Failed to read the respawn time";
+
+ lua_pop(L, result.n_results());
+ return true;
+}
+
+} // namespace lab
+} // namespace deepmind
diff --git a/deepmind/engine/context_pickups.h b/deepmind/engine/context_pickups.h
new file mode 100644
index 00000000..555eff8c
--- /dev/null
+++ b/deepmind/engine/context_pickups.h
@@ -0,0 +1,138 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef DML_DEEPMIND_ENGINE_CONTEXT_PICKUPS_H_
+#define DML_DEEPMIND_ENGINE_CONTEXT_PICKUPS_H_
+
+#include
+#include
+#include
+
+#include "deepmind/lua/table_ref.h"
+
+namespace deepmind {
+namespace lab {
+
+class ContextPickups {
+ public:
+ // Parameters for a custom pickup item.
+ using EntityInstance = std::unordered_map;
+
+ void SetScriptTableRef(lua::TableRef script_table_ref) {
+ script_table_ref_ = std::move(script_table_ref);
+ }
+
+ // Allows Lua to replace contents of this c-style dictionary.
+ // 'spawn_var_chars' is pointing at the memory holding the strings.
+ // '*num_spawn_var_chars' is the total length of all strings and nulls.
+ // 'spawn_var_offsets' is the key, value offsets in 'spawn_var_chars'.
+ // '*num_spawn_vars' is the number of valid spawn_var_offsets.
+ //
+ // So the dictionary { key0=value0, key1=value1, key2=value2 } would be
+ // represented as:
+ // spawn_var_chars[4096] = "key0\0value0\0key1\0value1\0key2\0value2";
+ // *num_spawn_var_chars = 36
+ // spawn_var_offsets[64][2] = {{0,5}, {12,17}, {24,29}};
+ // *num_spawn_vars = 3
+ // The update will not increase *num_spawn_var_chars to greater than 4096.
+ // and not increase *num_spawn_vars to greater than 64.
+ bool UpdateSpawnVars( //
+ char* spawn_var_chars, //
+ int* num_spawn_var_chars, //
+ int spawn_var_offsets[][2], //
+ int* num_spawn_vars);
+
+ // Clears extra_spawn_vars_ and reads new spawn vars from lua.
+ // Returns number of spawn vars created.
+ int MakeExtraEntities();
+
+ // Read specific spawn var from extra_spawn_vars_. Shall be called after
+ // with entity_index in range [0, MakeExtraEntities()).
+ void ReadExtraEntity( //
+ int entity_index, //
+ char* spawn_var_chars, //
+ int* num_spawn_var_chars, //
+ int spawn_var_offsets[][2], //
+ int* num_spawn_vars);
+
+ // Finds a pickup item by class_name, and registers this item with the
+ // Context's item array. This is so all the VMs can update their respective
+ // item lists by iterating through that array during update.
+ // Returns whether the item was found, and if so, writes the index at which
+ // the item now resides to *index.
+ bool FindItem(const char* class_name, int* index);
+
+ // Get the current number of registered items.
+ int ItemCount() const { return items_.size(); }
+
+ // Get an item at a particular index and fill in the various buffers
+ // provided.
+ // Returns whether the operation succeeded.
+ bool GetItem( //
+ int index, //
+ char* item_name, //
+ int max_item_name, //
+ char* class_name, //
+ int max_class_name, //
+ char* model_name, //
+ int max_model_name, //
+ int* quantity, //
+ int* type, //
+ int* tag) const;
+
+ // Clear the current list of registered items. Called just before loading a
+ // new map.
+ void ClearItems() { items_.clear(); }
+
+ // Returns whether we can pickup the specified entity id. By default this
+ // returns true.
+ bool CanPickup(int entity_id);
+
+ // Customization point for overriding the entity's pickup behaviour. Also
+ // allows for modifying the default respawn time for the entity.
+ // Returns true if the pickup behaviour has been overridden by the user,
+ // otherwise calls the default pickup behaviour based on the item type.
+ bool OverridePickup(int entity_id, int* respawn);
+
+ private:
+ // Parameters for a custom pickup item.
+ struct PickupItem {
+ std::string name; // Name that will show when picking up item.
+ std::string class_name; // Class name to spawn entity as. Must be unique.
+ std::string model_name; // Model for pickup item.
+ int quantity; // Amount to award on pickup.
+ int type; // Type of pickup. E.g. health, ammo, frags etc.
+ // Must match itemType_t in bg_public.h
+ int tag; // Tag used in conjunction with type. E.g.
+ // determine which weapon to award, or if a goal
+ // should bob and rotate.
+ };
+
+ lua::TableRef script_table_ref_;
+
+ // Array of current custom pickup items. Reset each episode.
+ std::vector items_;
+
+ // Array of extra spawn vars for this level.
+ std::vector extra_entities_;
+};
+
+} // namespace lab
+} // namespace deepmind
+
+#endif // DML_DEEPMIND_ENGINE_CONTEXT_PICKUPS_H_
diff --git a/deepmind/engine/lua_image.cc b/deepmind/engine/lua_image.cc
new file mode 100644
index 00000000..22547cb9
--- /dev/null
+++ b/deepmind/engine/lua_image.cc
@@ -0,0 +1,481 @@
+// Copyright (C) 2017 Google Inc.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "deepmind/engine/lua_image.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "absl/strings/str_cat.h"
+#include "deepmind/lua/bind.h"
+#include "deepmind/lua/push.h"
+#include "deepmind/lua/read.h"
+#include "deepmind/lua/table_ref.h"
+#include "deepmind/tensor/lua_tensor.h"
+#include "deepmind/util/files.h"
+#include "png.h"
+
+namespace deepmind {
+namespace lab {
+namespace {
+
+std::vector PngParsePixels(png_structp png_ptr,
+ png_infop info_ptr,
+ const tensor::ShapeVector& shape) {
+ std::vector bytes;
+ bytes.reserve(shape[0] * shape[1] * shape[2]);
+ const png_uint_32 bytesPerRow = png_get_rowbytes(png_ptr, info_ptr);
+ std::unique_ptr row_data(new unsigned char[bytesPerRow]);
+ auto bytes_inserter = std::back_inserter(bytes);
+ for (std::size_t rowIdx = 0; rowIdx < shape[0]; ++rowIdx) {
+ png_read_row(png_ptr, row_data.get(), nullptr);
+ std::copy_n(row_data.get(), shape[1] * shape[2], bytes_inserter);
+ }
+ return bytes;
+}
+
+struct Reader {
+ std::string contents;
+ std::size_t location;
+};
+
+extern "C" {
+static void PngReadContents(png_structp png_ptr, png_bytep out_bytes,
+ png_size_t count) {
+ Reader* reader = static_cast(png_get_io_ptr(png_ptr));
+ if (count + reader->location <= reader->contents.size()) {
+ std::copy_n(reader->contents.begin() + reader->location, count, out_bytes);
+ }
+ reader->location += count;
+}
+} // extern "C"
+
+lua::NResultsOr LoadPng(lua_State* L, std::string contents) {
+ Reader reader{std::move(contents), 0};
+ constexpr std::size_t png_header_size = 8;
+ if (reader.contents.size() < png_header_size) {
+ return "Invalid format. Contents too short.";
+ }
+
+ if (!png_check_sig(reinterpret_cast(&reader.contents[0]),
+ png_header_size))
+ return "Invalid format. Unrecognised signature.";
+
+ reader.location += png_header_size;
+
+ struct Png {
+ png_structp ptr;
+ png_infop info_ptr;
+ ~Png() {
+ png_destroy_read_struct(ptr ? &ptr : nullptr,
+ info_ptr ? &info_ptr : nullptr, nullptr);
+ }
+ };
+
+ Png png = {};
+
+ png.ptr =
+ png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ if (!png.ptr) return "Internal error.";
+
+ png.info_ptr = png_create_info_struct(png.ptr);
+ if (!png.info_ptr) return "Internal error.";
+
+ png_set_read_fn(png.ptr, &reader, &PngReadContents);
+
+ png_set_sig_bytes(png.ptr, png_header_size);
+ png_read_info(png.ptr, png.info_ptr);
+ png_uint_32 width = 0;
+ png_uint_32 height = 0;
+ int bitDepth = 0;
+ int colorType = -1;
+ png_uint_32 retval =
+ png_get_IHDR(png.ptr, png.info_ptr, &width, &height, &bitDepth,
+ &colorType, nullptr, nullptr, nullptr);
+
+ if (retval != 1) return "Invalid format. Corrupted header.";
+ if (bitDepth != 8)
+ return "Unsupported format. Image must have 8-bit channels.";
+
+ tensor::ShapeVector shape = {height, width, 0};
+
+ switch (colorType) {
+ case PNG_COLOR_TYPE_GRAY:
+ shape[2] = 1;
+ break;
+ case PNG_COLOR_TYPE_GRAY_ALPHA:
+ shape[2] = 2;
+ break;
+ case PNG_COLOR_TYPE_RGB:
+ shape[2] = 3;
+ break;
+ case PNG_COLOR_TYPE_RGB_ALPHA:
+ shape[2] = 4;
+ break;
+ default:
+ return "Unsupported format. Image must not be paletted.";
+ }
+
+ auto bytes = PngParsePixels(png.ptr, png.info_ptr, shape);
+ if (reader.location > reader.contents.size())
+ return "Invalid format. Contents too short.";
+ tensor::LuaTensor