diff --git a/.efrocachemap b/.efrocachemap index e398a06c..3878fa79 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/88/6c/03d5c4811e2ffb2f341a042f676e", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3f/c0/7025f06247748ad8f0516389e4f8", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c9/36/4393ce5c02ebf21a6a2dbb0614eb", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/13/7d/2c551af0ebc26c93f52a87b02ca0", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/b2/ff5199a4437852f09d2d7096896d", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/06/5a/d683bd2197262a9baec17b7306a6", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/07/08fe94d637f560acfd450d722393", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cc/ae/2855e9d714ea0c7ceaf4f42a4dc2", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/46/fc/16cf6c1cb45381b377c1d3bac058", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/81/de/d2b16c91eed65ab721149488f399", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/5d/89/1ec1c4058821e52e117142c7fbc4", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/9e/90/315e8edc3ab7bc1080d18e29cf8d", - "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ea/68/8d61d116af2df5617a11e5ae2d9d", - "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/62/fa796628f2840d880dd421f9c821", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f0/d0/e1e69b545cf166ce4e679621307f", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b1/d0/14df0a36c445e8a2e67eb8802911", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/26/eb/3f9b13ea38c9f7af8ff0532258e8", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cc/56/442362d6eab98a42da31ba8cc9d6", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/f9/dd209379992a04c479f4cb5e3e85", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b0/d7/5ec4bfbf10cf520d2c2b85530176" + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/82/ee9dbd4e5f7979376228d6953bf2", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8b/1b/28311c0a6459e5d4fba5b8646f3e", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/01/bb/1aa148fa278d31cb6674174b194f", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/98/7b/5f1bcd5a6ac1916ca35e10c28a30", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/64/03/694933268214ba930b6f38b9c6c0", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e1/49/88c92b0f65857a88f6eaaee04483", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a0/f6/39e7caccbbcd4cd442145c17a05f", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1d/c6/20d9325e37de411bc6fe3d41d503", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ab/14/4cf9664fb0de75dcb20b596ded74", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/c3/9a/39b182b32184ae177611078f03ff", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/70/e2/b993cf8dd5ede4d9e6ce42cd8c24", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/60/03/166e19fae3db8f6e4b5c6a59a520", + "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/ed/62a752dbeeca8f5e99725d42e7e7", + "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/a9/568023651355fdd0ce7a865c2872", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0d/25/2d023ef6acd6ae5aa09bf12158d3", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/42/be/79eec8bc7b2cc914cc6cb8ed0769", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/83/00/c2e4c96a434c1ac16e9f8b8a401e", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/03/94/b12016e90c0c650b3c3222bc2453", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f2/9e/fd1ff58204bf80d0b02edfa79293", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/26/5c/a738eaa3e553bb00814f2376cb5a" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 2b0fe477..917d442e 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1009,6 +1009,7 @@ installdir instancer interfacetype + internalsrc interstitials intex intp @@ -1370,8 +1371,10 @@ nospeak nosub nosyncdir + nosyncdirlist nosyncdirs nosyncfile + nosyncfilelist nosyncfiles nosynctool nosynctools @@ -1821,6 +1824,7 @@ setactivity setalpha setbuild + setconfig setdata setlanguage setmusic diff --git a/ballisticacore-cmake/CMakeLists.txt b/ballisticacore-cmake/CMakeLists.txt index a1e2aec1..8809727e 100644 --- a/ballisticacore-cmake/CMakeLists.txt +++ b/ballisticacore-cmake/CMakeLists.txt @@ -174,6 +174,441 @@ add_executable(ballisticacore ${BA_SRC_ROOT}/external/qr_code_generator/QrCode.cpp # AUTOGENERATED_PUBLIC_BEGIN (this section is managed by the "update_project" tool) ${BA_SRC_ROOT}/ballistica/app/app.cc + ${BA_SRC_ROOT}/ballistica/app/app.h + ${BA_SRC_ROOT}/ballistica/app/app_config.h + ${BA_SRC_ROOT}/ballistica/app/app_globals.h + ${BA_SRC_ROOT}/ballistica/app/headless_app.h + ${BA_SRC_ROOT}/ballistica/app/stress_test.h + ${BA_SRC_ROOT}/ballistica/app/vr_app.h + ${BA_SRC_ROOT}/ballistica/audio/al_sys.cc + ${BA_SRC_ROOT}/ballistica/audio/al_sys.h + ${BA_SRC_ROOT}/ballistica/audio/audio.cc + ${BA_SRC_ROOT}/ballistica/audio/audio.h + ${BA_SRC_ROOT}/ballistica/audio/audio_server.cc + ${BA_SRC_ROOT}/ballistica/audio/audio_server.h + ${BA_SRC_ROOT}/ballistica/audio/audio_source.cc + ${BA_SRC_ROOT}/ballistica/audio/audio_source.h + ${BA_SRC_ROOT}/ballistica/audio/audio_streamer.cc + ${BA_SRC_ROOT}/ballistica/audio/audio_streamer.h + ${BA_SRC_ROOT}/ballistica/audio/ogg_stream.cc + ${BA_SRC_ROOT}/ballistica/audio/ogg_stream.h + ${BA_SRC_ROOT}/ballistica/ballistica.cc + ${BA_SRC_ROOT}/ballistica/ballistica.h + ${BA_SRC_ROOT}/ballistica/config/config_cmake.h + ${BA_SRC_ROOT}/ballistica/config/config_common.h + ${BA_SRC_ROOT}/ballistica/core/context.cc + ${BA_SRC_ROOT}/ballistica/core/context.h + ${BA_SRC_ROOT}/ballistica/core/exception.cc + ${BA_SRC_ROOT}/ballistica/core/exception.h + ${BA_SRC_ROOT}/ballistica/core/fatal_error.cc + ${BA_SRC_ROOT}/ballistica/core/fatal_error.h + ${BA_SRC_ROOT}/ballistica/core/inline.cc + ${BA_SRC_ROOT}/ballistica/core/inline.h + ${BA_SRC_ROOT}/ballistica/core/logging.cc + ${BA_SRC_ROOT}/ballistica/core/logging.h + ${BA_SRC_ROOT}/ballistica/core/macros.cc + ${BA_SRC_ROOT}/ballistica/core/macros.h + ${BA_SRC_ROOT}/ballistica/core/module.cc + ${BA_SRC_ROOT}/ballistica/core/module.h + ${BA_SRC_ROOT}/ballistica/core/object.cc + ${BA_SRC_ROOT}/ballistica/core/object.h + ${BA_SRC_ROOT}/ballistica/core/thread.cc + ${BA_SRC_ROOT}/ballistica/core/thread.h + ${BA_SRC_ROOT}/ballistica/core/types.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics.cc + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_fuse.cc + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_fuse.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_fuse_data.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_height_cache.cc + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_height_cache.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_server.cc + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_server.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_shadow.cc + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_shadow.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_shadow_data.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_volume_light.cc + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_volume_light.h + ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h + ${BA_SRC_ROOT}/ballistica/dynamics/collision.h + ${BA_SRC_ROOT}/ballistica/dynamics/collision_cache.cc + ${BA_SRC_ROOT}/ballistica/dynamics/collision_cache.h + ${BA_SRC_ROOT}/ballistica/dynamics/dynamics.cc + ${BA_SRC_ROOT}/ballistica/dynamics/dynamics.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/impact_sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/impact_sound_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/material.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/material.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_component.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_component.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_condition_node.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_condition_node.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_context.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/material_context.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/node_message_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/node_message_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/node_mod_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/node_mod_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/node_user_message_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/node_user_message_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/part_mod_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/part_mod_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/python_call_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/python_call_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/roll_sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/roll_sound_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/skid_sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/skid_sound_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/material/sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/dynamics/material/sound_material_action.h + ${BA_SRC_ROOT}/ballistica/dynamics/part.cc + ${BA_SRC_ROOT}/ballistica/dynamics/part.h + ${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.cc + ${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.h + ${BA_SRC_ROOT}/ballistica/game/account.h + ${BA_SRC_ROOT}/ballistica/game/client_controller_interface.h + ${BA_SRC_ROOT}/ballistica/game/connection/connection.h + ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client.h + ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client_udp.h + ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host.h + ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host_udp.h + ${BA_SRC_ROOT}/ballistica/game/friend_score_set.h + ${BA_SRC_ROOT}/ballistica/game/game.h + ${BA_SRC_ROOT}/ballistica/game/game_stream.h + ${BA_SRC_ROOT}/ballistica/game/host_activity.h + ${BA_SRC_ROOT}/ballistica/game/player.h + ${BA_SRC_ROOT}/ballistica/game/player_spec.h + ${BA_SRC_ROOT}/ballistica/game/score_to_beat.h + ${BA_SRC_ROOT}/ballistica/game/session/client_session.h + ${BA_SRC_ROOT}/ballistica/game/session/host_session.h + ${BA_SRC_ROOT}/ballistica/game/session/net_client_session.h + ${BA_SRC_ROOT}/ballistica/game/session/replay_client_session.h + ${BA_SRC_ROOT}/ballistica/game/session/session.h + ${BA_SRC_ROOT}/ballistica/generic/base64.cc + ${BA_SRC_ROOT}/ballistica/generic/base64.h + ${BA_SRC_ROOT}/ballistica/generic/buffer.h + ${BA_SRC_ROOT}/ballistica/generic/huffman.cc + ${BA_SRC_ROOT}/ballistica/generic/huffman.h + ${BA_SRC_ROOT}/ballistica/generic/json.cc + ${BA_SRC_ROOT}/ballistica/generic/json.h + ${BA_SRC_ROOT}/ballistica/generic/lambda_runnable.h + ${BA_SRC_ROOT}/ballistica/generic/real_timer.h + ${BA_SRC_ROOT}/ballistica/generic/runnable.cc + ${BA_SRC_ROOT}/ballistica/generic/runnable.h + ${BA_SRC_ROOT}/ballistica/generic/timer.cc + ${BA_SRC_ROOT}/ballistica/generic/timer.h + ${BA_SRC_ROOT}/ballistica/generic/timer_list.cc + ${BA_SRC_ROOT}/ballistica/generic/timer_list.h + ${BA_SRC_ROOT}/ballistica/generic/utf8.cc + ${BA_SRC_ROOT}/ballistica/generic/utf8.h + ${BA_SRC_ROOT}/ballistica/generic/utils.cc + ${BA_SRC_ROOT}/ballistica/generic/utils.h + ${BA_SRC_ROOT}/ballistica/graphics/area_of_interest.cc + ${BA_SRC_ROOT}/ballistica/graphics/area_of_interest.h + ${BA_SRC_ROOT}/ballistica/graphics/camera.cc + ${BA_SRC_ROOT}/ballistica/graphics/camera.h + ${BA_SRC_ROOT}/ballistica/graphics/component/empty_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/object_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/object_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/post_process_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/post_process_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/render_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/render_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/shield_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/shield_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/simple_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/simple_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/smoke_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/smoke_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/special_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/special_component.h + ${BA_SRC_ROOT}/ballistica/graphics/component/sprite_component.cc + ${BA_SRC_ROOT}/ballistica/graphics/component/sprite_component.h + ${BA_SRC_ROOT}/ballistica/graphics/frame_def.cc + ${BA_SRC_ROOT}/ballistica/graphics/frame_def.h + ${BA_SRC_ROOT}/ballistica/graphics/framebuffer.h + ${BA_SRC_ROOT}/ballistica/graphics/gl/gl_sys.cc + ${BA_SRC_ROOT}/ballistica/graphics/gl/gl_sys.h + ${BA_SRC_ROOT}/ballistica/graphics/gl/renderer_gl.cc + ${BA_SRC_ROOT}/ballistica/graphics/gl/renderer_gl.h + ${BA_SRC_ROOT}/ballistica/graphics/graphics.cc + ${BA_SRC_ROOT}/ballistica/graphics/graphics.h + ${BA_SRC_ROOT}/ballistica/graphics/graphics_server.cc + ${BA_SRC_ROOT}/ballistica/graphics/graphics_server.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/image_mesh.cc + ${BA_SRC_ROOT}/ballistica/graphics/mesh/image_mesh.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_base.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data.cc + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data_client_handle.cc + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data_client_handle.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_index_buffer_16.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_index_buffer_32.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_base.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_object_split.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_simple_full.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_simple_split.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_smoke_full.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_non_indexed.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_renderer_data.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/sprite_mesh.h + ${BA_SRC_ROOT}/ballistica/graphics/mesh/text_mesh.cc + ${BA_SRC_ROOT}/ballistica/graphics/mesh/text_mesh.h + ${BA_SRC_ROOT}/ballistica/graphics/net_graph.cc + ${BA_SRC_ROOT}/ballistica/graphics/net_graph.h + ${BA_SRC_ROOT}/ballistica/graphics/render_command_buffer.h + ${BA_SRC_ROOT}/ballistica/graphics/render_pass.cc + ${BA_SRC_ROOT}/ballistica/graphics/render_pass.h + ${BA_SRC_ROOT}/ballistica/graphics/render_target.cc + ${BA_SRC_ROOT}/ballistica/graphics/render_target.h + ${BA_SRC_ROOT}/ballistica/graphics/renderer.cc + ${BA_SRC_ROOT}/ballistica/graphics/renderer.h + ${BA_SRC_ROOT}/ballistica/graphics/text/font_page_map_data.h + ${BA_SRC_ROOT}/ballistica/graphics/text/text_graphics.cc + ${BA_SRC_ROOT}/ballistica/graphics/text/text_graphics.h + ${BA_SRC_ROOT}/ballistica/graphics/text/text_group.cc + ${BA_SRC_ROOT}/ballistica/graphics/text/text_group.h + ${BA_SRC_ROOT}/ballistica/graphics/text/text_packer.cc + ${BA_SRC_ROOT}/ballistica/graphics/text/text_packer.h + ${BA_SRC_ROOT}/ballistica/graphics/texture/dds.cc + ${BA_SRC_ROOT}/ballistica/graphics/texture/dds.h + ${BA_SRC_ROOT}/ballistica/graphics/texture/ktx.cc + ${BA_SRC_ROOT}/ballistica/graphics/texture/ktx.h + ${BA_SRC_ROOT}/ballistica/graphics/texture/pvr.cc + ${BA_SRC_ROOT}/ballistica/graphics/texture/pvr.h + ${BA_SRC_ROOT}/ballistica/graphics/vr_graphics.cc + ${BA_SRC_ROOT}/ballistica/graphics/vr_graphics.h + ${BA_SRC_ROOT}/ballistica/input/device/client_input_device.cc + ${BA_SRC_ROOT}/ballistica/input/device/client_input_device.h + ${BA_SRC_ROOT}/ballistica/input/device/input_device.cc + ${BA_SRC_ROOT}/ballistica/input/device/input_device.h + ${BA_SRC_ROOT}/ballistica/input/device/joystick.cc + ${BA_SRC_ROOT}/ballistica/input/device/joystick.h + ${BA_SRC_ROOT}/ballistica/input/device/keyboard_input.cc + ${BA_SRC_ROOT}/ballistica/input/device/keyboard_input.h + ${BA_SRC_ROOT}/ballistica/input/device/test_input.cc + ${BA_SRC_ROOT}/ballistica/input/device/test_input.h + ${BA_SRC_ROOT}/ballistica/input/device/touch_input.cc + ${BA_SRC_ROOT}/ballistica/input/device/touch_input.h + ${BA_SRC_ROOT}/ballistica/input/input.cc + ${BA_SRC_ROOT}/ballistica/input/input.h + ${BA_SRC_ROOT}/ballistica/input/remote_app.cc + ${BA_SRC_ROOT}/ballistica/input/remote_app.h + ${BA_SRC_ROOT}/ballistica/input/std_input_module.cc + ${BA_SRC_ROOT}/ballistica/input/std_input_module.h + ${BA_SRC_ROOT}/ballistica/math/matrix44f.cc + ${BA_SRC_ROOT}/ballistica/math/matrix44f.h + ${BA_SRC_ROOT}/ballistica/math/point2d.h + ${BA_SRC_ROOT}/ballistica/math/random.cc + ${BA_SRC_ROOT}/ballistica/math/random.h + ${BA_SRC_ROOT}/ballistica/math/rect.h + ${BA_SRC_ROOT}/ballistica/math/vector2f.h + ${BA_SRC_ROOT}/ballistica/math/vector3f.cc + ${BA_SRC_ROOT}/ballistica/math/vector3f.h + ${BA_SRC_ROOT}/ballistica/math/vector4f.h + ${BA_SRC_ROOT}/ballistica/media/component/collide_model.cc + ${BA_SRC_ROOT}/ballistica/media/component/collide_model.h + ${BA_SRC_ROOT}/ballistica/media/component/cube_map_texture.cc + ${BA_SRC_ROOT}/ballistica/media/component/cube_map_texture.h + ${BA_SRC_ROOT}/ballistica/media/component/data.cc + ${BA_SRC_ROOT}/ballistica/media/component/data.h + ${BA_SRC_ROOT}/ballistica/media/component/media_component.cc + ${BA_SRC_ROOT}/ballistica/media/component/media_component.h + ${BA_SRC_ROOT}/ballistica/media/component/model.cc + ${BA_SRC_ROOT}/ballistica/media/component/model.h + ${BA_SRC_ROOT}/ballistica/media/component/sound.cc + ${BA_SRC_ROOT}/ballistica/media/component/sound.h + ${BA_SRC_ROOT}/ballistica/media/component/texture.cc + ${BA_SRC_ROOT}/ballistica/media/component/texture.h + ${BA_SRC_ROOT}/ballistica/media/data/collide_model_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/collide_model_data.h + ${BA_SRC_ROOT}/ballistica/media/data/data_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/data_data.h + ${BA_SRC_ROOT}/ballistica/media/data/media_component_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/media_component_data.h + ${BA_SRC_ROOT}/ballistica/media/data/model_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/model_data.h + ${BA_SRC_ROOT}/ballistica/media/data/model_renderer_data.h + ${BA_SRC_ROOT}/ballistica/media/data/sound_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/sound_data.h + ${BA_SRC_ROOT}/ballistica/media/data/texture_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/texture_data.h + ${BA_SRC_ROOT}/ballistica/media/data/texture_preload_data.cc + ${BA_SRC_ROOT}/ballistica/media/data/texture_preload_data.h + ${BA_SRC_ROOT}/ballistica/media/data/texture_renderer_data.h + ${BA_SRC_ROOT}/ballistica/media/media.cc + ${BA_SRC_ROOT}/ballistica/media/media.h + ${BA_SRC_ROOT}/ballistica/media/media_server.cc + ${BA_SRC_ROOT}/ballistica/media/media_server.h + ${BA_SRC_ROOT}/ballistica/networking/network_reader.h + ${BA_SRC_ROOT}/ballistica/networking/network_write_module.h + ${BA_SRC_ROOT}/ballistica/networking/networking.h + ${BA_SRC_ROOT}/ballistica/networking/networking_sys.h + ${BA_SRC_ROOT}/ballistica/networking/sockaddr.h + ${BA_SRC_ROOT}/ballistica/networking/telnet_server.cc + ${BA_SRC_ROOT}/ballistica/networking/telnet_server.h + ${BA_SRC_ROOT}/ballistica/platform/apple/platform_apple.h + ${BA_SRC_ROOT}/ballistica/platform/linux/platform_linux.cc + ${BA_SRC_ROOT}/ballistica/platform/linux/platform_linux.h + ${BA_SRC_ROOT}/ballistica/platform/min_sdl.h + ${BA_SRC_ROOT}/ballistica/platform/platform.cc + ${BA_SRC_ROOT}/ballistica/platform/platform.h + ${BA_SRC_ROOT}/ballistica/platform/sdl/sdl_app.cc + ${BA_SRC_ROOT}/ballistica/platform/sdl/sdl_app.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_activity_data.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_activity_data.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_collide_model.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_collide_model.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_context.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_context.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_context_call.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_context_call.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_data.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_data.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_input_device.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_input_device.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_material.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_material.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_model.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_model.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_node.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_node.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_data.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_data.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_player.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_player.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_sound.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_sound.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_texture.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_texture.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_timer.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_timer.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_vec3.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_vec3.h + ${BA_SRC_ROOT}/ballistica/python/class/python_class_widget.cc + ${BA_SRC_ROOT}/ballistica/python/class/python_class_widget.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_app.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_app.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_gameplay.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_gameplay.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_graphics.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_graphics.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_input.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_input.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_media.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_media.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_system.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_system.h + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_ui.cc + ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_ui.h + ${BA_SRC_ROOT}/ballistica/python/python.h + ${BA_SRC_ROOT}/ballistica/python/python_command.cc + ${BA_SRC_ROOT}/ballistica/python/python_command.h + ${BA_SRC_ROOT}/ballistica/python/python_context_call.cc + ${BA_SRC_ROOT}/ballistica/python/python_context_call.h + ${BA_SRC_ROOT}/ballistica/python/python_context_call_runnable.h + ${BA_SRC_ROOT}/ballistica/python/python_ref.cc + ${BA_SRC_ROOT}/ballistica/python/python_ref.h + ${BA_SRC_ROOT}/ballistica/python/python_sys.h + ${BA_SRC_ROOT}/ballistica/scene/node/anim_curve_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/anim_curve_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/bomb_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/bomb_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/combine_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/combine_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/explosion_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/explosion_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/flag_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/flag_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/flash_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/flash_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/globals_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/globals_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/image_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/image_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/light_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/light_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/locator_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/locator_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/math_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/math_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/node.h + ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute.cc + ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute.h + ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute_connection.cc + ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute_connection.h + ${BA_SRC_ROOT}/ballistica/scene/node/node_type.h + ${BA_SRC_ROOT}/ballistica/scene/node/null_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/null_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/player_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/player_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/prop_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/prop_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/region_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/region_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/scorch_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/scorch_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/session_globals_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/session_globals_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/shield_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/shield_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/sound_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/sound_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/spaz_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/spaz_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/terrain_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/terrain_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/text_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/text_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/texture_sequence_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/texture_sequence_node.h + ${BA_SRC_ROOT}/ballistica/scene/node/time_display_node.cc + ${BA_SRC_ROOT}/ballistica/scene/node/time_display_node.h + ${BA_SRC_ROOT}/ballistica/scene/scene.cc + ${BA_SRC_ROOT}/ballistica/scene/scene.h + ${BA_SRC_ROOT}/ballistica/ui/console.cc + ${BA_SRC_ROOT}/ballistica/ui/console.h + ${BA_SRC_ROOT}/ballistica/ui/root_ui.cc + ${BA_SRC_ROOT}/ballistica/ui/root_ui.h + ${BA_SRC_ROOT}/ballistica/ui/ui.cc + ${BA_SRC_ROOT}/ballistica/ui/ui.h + ${BA_SRC_ROOT}/ballistica/ui/widget/button_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/button_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/check_box_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/check_box_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/column_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/column_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/container_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/container_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/h_scroll_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/h_scroll_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/image_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/image_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/root_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/root_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/row_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/row_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/scroll_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/scroll_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/stack_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/stack_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/text_widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/text_widget.h + ${BA_SRC_ROOT}/ballistica/ui/widget/widget.cc + ${BA_SRC_ROOT}/ballistica/ui/widget/widget.h # AUTOGENERATED_PUBLIC_END ) diff --git a/src/ballistica/audio/al_sys.cc b/src/ballistica/audio/al_sys.cc new file mode 100644 index 00000000..11518df6 --- /dev/null +++ b/src/ballistica/audio/al_sys.cc @@ -0,0 +1,51 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/audio/al_sys.h" + +#include "ballistica/audio/audio_server.h" +#include "ballistica/generic/utils.h" + +// Need to move away from OpenAL on Apple stuff. +#if __clang__ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +#if BA_ENABLE_AUDIO + +namespace ballistica { + +void _check_al_error(const char* file, int line) { + if (g_audio_server->paused()) { + Log(Utils::BaseName(file) + ":" + std::to_string(line) + + ": Checking OpenAL error while paused."); + } + ALenum al_err = alGetError(); + if (al_err != AL_NO_ERROR) { + Log(Utils::BaseName(file) + ":" + std::to_string(line) + + ": OpenAL Error: " + GetALErrorString(al_err) + ";"); + } +} + +auto GetALErrorString(ALenum err) -> const char* { + static char undefErrStr[128]; +#define DO_AL_ERR_CASE(a) \ + case a: \ + return #a + switch (err) { + DO_AL_ERR_CASE(AL_INVALID_NAME); + DO_AL_ERR_CASE(AL_ILLEGAL_ENUM); + DO_AL_ERR_CASE(AL_INVALID_VALUE); + DO_AL_ERR_CASE(AL_ILLEGAL_COMMAND); + DO_AL_ERR_CASE(AL_OUT_OF_MEMORY); + default: { + snprintf(undefErrStr, sizeof(undefErrStr), "(unrecognized: 0x%X (%d))", + err, err); + return undefErrStr; + } + } +#undef DO_AL_ERR_CASE +} + +} // namespace ballistica + +#endif // BA_ENABLE_AUDIO diff --git a/src/ballistica/audio/al_sys.h b/src/ballistica/audio/al_sys.h new file mode 100644 index 00000000..44b1c740 --- /dev/null +++ b/src/ballistica/audio/al_sys.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_AUDIO_AL_SYS_H_ +#define BALLISTICA_AUDIO_AL_SYS_H_ + +#include + +#include "ballistica/ballistica.h" + +#if BA_ENABLE_AUDIO + +#if HAVE_FRAMEWORK_OPENAL +#include +#include +#else +#include +#include +#endif + +#define CHECK_AL_ERROR _check_al_error(__FILE__, __LINE__) +#if BA_DEBUG_BUILD +#define DEBUG_CHECK_AL_ERROR CHECK_AL_ERROR +#else +#define DEBUG_CHECK_AL_ERROR ((void)0) +#endif + +namespace ballistica { + +const int kAudioStreamBufferSize = 4096 * 8; +const int kAudioStreamBufferCount = 7; + +// Some OpenAL Error handling utils. +auto GetALErrorString(ALenum err) -> const char*; + +void _check_al_error(const char* file, int line); + +} // namespace ballistica + +#endif // BA_ENABLE_AUDIO + +#endif // BALLISTICA_AUDIO_AL_SYS_H_ diff --git a/src/ballistica/audio/audio.cc b/src/ballistica/audio/audio.cc new file mode 100644 index 00000000..4fcb6e2e --- /dev/null +++ b/src/ballistica/audio/audio.cc @@ -0,0 +1,180 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/audio/audio.h" + +#include "ballistica/audio/audio_server.h" +#include "ballistica/audio/audio_source.h" +#include "ballistica/media/data/sound_data.h" + +namespace ballistica { + +Audio::Audio() { assert(InGameThread()); } + +void Audio::Init() { + // Init our singleton. + assert(g_audio == nullptr); + g_audio = new Audio(); +} + +void Audio::Reset() { + assert(InGameThread()); + g_audio_server->PushResetCall(); +} + +void Audio::SetVolumes(float music_volume, float sound_volume) { + g_audio_server->PushSetVolumesCall(music_volume, sound_volume); +} + +void Audio::SetSoundPitch(float pitch) { + g_audio_server->PushSetSoundPitchCall(pitch); +} + +void Audio::SetListenerPosition(const Vector3f& p) { + g_audio_server->PushSetListenerPositionCall(p); +} + +void Audio::SetListenerOrientation(const Vector3f& forward, + const Vector3f& up) { + g_audio_server->PushSetListenerOrientationCall(forward, up); +} + +// This stops a particular sound play ID only. +void Audio::PushSourceStopSoundCall(uint32_t play_id) { + g_audio_server->PushCall( + [this, play_id] { g_audio_server->StopSound(play_id); }); +} + +void Audio::PushSourceFadeOutCall(uint32_t play_id, uint32_t time) { + g_audio_server->PushCall( + [this, play_id, time] { g_audio_server->FadeSoundOut(play_id, time); }); +} + +auto Audio::SourceBeginNew() -> AudioSource* { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + + AudioSource* s = nullptr; + { + // Gotta make sure to hold this until we've locked the source. + // Otherwise theoretically the audio thread could make our source available + // again before we can use it. + std::lock_guard lock(available_sources_mutex_); + + // If there's an available source, reserve and return it. + auto i = available_sources_.begin(); + if (i != available_sources_.end()) { + s = *i; + available_sources_.erase(i); + assert(s->available()); + assert(s->client_queue_size() == 0); + s->set_available(false); + } + if (s) { + s->Lock(1); + assert(!s->available()); + s->set_client_queue_size(s->client_queue_size() + 1); + } + } + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); + return s; +} + +auto Audio::IsSoundPlaying(uint32_t play_id) -> bool { + uint32_t source_id = AudioServer::source_id_from_play_id(play_id); + assert(client_sources_.size() > source_id); + client_sources_[source_id]->Lock(2); + bool result = (client_sources_[source_id]->play_id() == play_id); + client_sources_[source_id]->Unlock(); + return result; +} + +auto Audio::SourceBeginExisting(uint32_t play_id, uint32_t debug_id) + -> AudioSource* { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + uint32_t source_id = AudioServer::source_id_from_play_id(play_id); + + // Ok, the audio thread fills in this source list, + // so theoretically a client could call this before the audio thread + // has set it up. However no one should be trying to get a playing + // sound unless they've already started playing one which implies + // everything was set up already. I think we're good. + assert(g_audio->client_sources_.size() > source_id); + + // If this guy's still got the play id they're asking about, lock/return it. + client_sources_[source_id]->Lock(debug_id); + + if (client_sources_[source_id]->play_id() == play_id) { + assert(!client_sources_[source_id]->available()); + client_sources_[source_id]->set_client_queue_size( + client_sources_[source_id]->client_queue_size() + 1); + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); + return client_sources_[source_id]; + } + + // No-go; unlock and return empty-handed. + client_sources_[source_id]->Unlock(); + + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); + return nullptr; +} + +auto Audio::ShouldPlay(SoundData* sound) -> bool { + millisecs_t time = GetRealTime(); + assert(sound); + return (time - sound->last_play_time() > 50); +} + +void Audio::PlaySound(SoundData* sound, float volume) { + assert(InGameThread()); + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + assert(sound); + if (!ShouldPlay(sound)) { + return; + } + AudioSource* s = SourceBeginNew(); + if (s) { + // In vr mode, play non-positional sounds positionally in space roughly + // where the menu is. + if (IsVRMode()) { + s->SetGain(volume); + s->SetPositional(true); + float x = 0.0f; + float y = 4.5f; + float z = -3.0f; + s->SetPosition(x, y, z); + s->Play(sound); + s->End(); + } else { + s->SetGain(volume); + s->SetPositional(false); + s->Play(sound); + s->End(); + } + } + BA_DEBUG_FUNCTION_TIMER_END(20); +} + +void Audio::PlaySoundAtPosition(SoundData* sound, float volume, float x, + float y, float z) { + assert(sound); + if (!ShouldPlay(sound)) { + return; + } + // Run locally. + if (AudioSource* source = SourceBeginNew()) { + source->SetGain(volume); + source->SetPositional(true); + source->SetPosition(x, y, z); + source->Play(sound); + source->End(); + } +} + +void Audio::AddClientSource(AudioSource* source) { + client_sources_.push_back(source); +} + +void Audio::MakeSourceAvailable(AudioSource* source) { + available_sources_.push_back(source); +} + +} // namespace ballistica diff --git a/src/ballistica/audio/audio.h b/src/ballistica/audio/audio.h new file mode 100644 index 00000000..20ec7060 --- /dev/null +++ b/src/ballistica/audio/audio.h @@ -0,0 +1,82 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_AUDIO_AUDIO_H_ +#define BALLISTICA_AUDIO_AUDIO_H_ + +#include +#include +#include + +#include "ballistica/core/module.h" +#include "ballistica/core/object.h" + +namespace ballistica { + +/// Client class for audio operations; +/// used by the game and/or other threads. +class Audio { + public: + static void Init(); + void Reset(); + + void SetVolumes(float music_volume, float sound_volume); + + void SetListenerPosition(const Vector3f& p); + void SetListenerOrientation(const Vector3f& forward, const Vector3f& up); + void SetSoundPitch(float pitch); + + // Return a pointer to a locked sound source, or nullptr if they're all busy. + // The sound source will be reset to standard settings (no loop, fade 1, pos + // 0,0,0, etc). + // Send the source any immediate commands and then unlock it. + // For later modifications, re-retrieve the sound with GetPlayingSound() + auto SourceBeginNew() -> AudioSource*; + + // If a sound play id is playing, locks and returns its sound source. + // on success, you must unlock the source once done with it. + auto SourceBeginExisting(uint32_t play_id, uint32_t debug_id) -> AudioSource*; + + // Return true if the sound id is currently valid. This is not guaranteed + // to be super accurate, but can be used to determine if a sound is still + // playing. + auto IsSoundPlaying(uint32_t play_id) -> bool; + + // Simple one-shot play functions. + void PlaySound(SoundData* s, float volume = 1.0f); + void PlaySoundAtPosition(SoundData* sound, float volume, float x, float y, + float z); + + // Call this if you want to prevent repeated plays of the same sound. It'll + // tell you if the sound has been played recently. The one-shot sound-play + // functions use this under the hood. (PlaySound, PlaySoundAtPosition). + auto ShouldPlay(SoundData* s) -> bool; + + // Hmm; shouldn't these be accessed through the Source class? + void PushSourceFadeOutCall(uint32_t play_id, uint32_t time); + void PushSourceStopSoundCall(uint32_t play_id); + + void AddClientSource(AudioSource* source); + + void MakeSourceAvailable(AudioSource* source); + auto available_sources_mutex() -> std::mutex& { + return available_sources_mutex_; + } + + private: + Audio(); + + // Flat list of client sources indexed by id. + std::vector client_sources_; + + // List of sources that are ready to use. + // This is kept filled by the audio thread + // and used by the client. + std::vector available_sources_; + + // This must be locked whenever accessing the availableSources list. + std::mutex available_sources_mutex_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_AUDIO_AUDIO_H_ diff --git a/src/ballistica/audio/audio_server.cc b/src/ballistica/audio/audio_server.cc new file mode 100644 index 00000000..992bd619 --- /dev/null +++ b/src/ballistica/audio/audio_server.cc @@ -0,0 +1,1166 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/audio/audio_server.h" + +#include +#include +#include +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/al_sys.h" +#include "ballistica/audio/audio.h" +#include "ballistica/audio/audio_source.h" +#include "ballistica/audio/audio_streamer.h" +#include "ballistica/audio/ogg_stream.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/timer.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/media/data/sound_data.h" +#include "ballistica/media/media.h" + +// Need to move away from OpenAL on Apple stuff. +#if __clang__ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +namespace ballistica { + +// FIXME: move these to platform. +extern "C" void opensl_pause_playback(); +extern "C" void opensl_resume_playback(); + +#if BA_RIFT_BUILD +extern std::string g_rift_audio_device_name; +#endif + +const int kAudioProcessIntervalNormal = 500; +const int kAudioProcessIntervalFade = 50; +const int kAudioProcessIntervalPendingLoad = 1; +const bool kShowInUseSounds = false; + +int AudioServer::al_source_count_ = 0; + +/// Location for sound emission (server version). +class AudioServer::ThreadSource : public Object { + public: + // The id is returned as the lo-word of the identifier + // returned by "play". If valid is returned as false, there are no + // hardware channels available (or another error) and the source should + // not be used. + ThreadSource(AudioServer* audio_thread, int id, bool* valid); + ~ThreadSource() override; + void Reset() { + SetIsMusic(false); + SetPositional(true); + SetPosition(0, 0, 0); + SetGain(1.0f); + SetFade(1); + SetLooping(false); + } + + /// Set whether a sound is "music". + /// This influences which volume controls affect it. + void SetIsMusic(bool m); + + /// Set whether a source is positional. + /// A non-positional source's position coords are always relative to the + /// listener - ie: 0, 0, 0 will always be centered. + void SetPositional(bool p); + void SetPosition(float x, float y, float z); + void SetGain(float g); + void SetFade(float f); + void SetLooping(bool loop); + auto Play(const Object::Ref* s) -> uint32_t; + void Stop(); + auto play_count() -> uint32_t { return play_count_; } + auto is_streamed() const -> bool { return is_streamed_; } + auto current_is_music() const -> bool { return current_is_music_; } + auto want_to_play() const -> bool { return want_to_play_; } + auto is_actually_playing() const -> bool { return is_actually_playing_; } + auto play_id() const -> uint32_t { + return (play_count_ << 16u) | (static_cast(id_) & 0xFFFFu); + } + void UpdateAvailability(); + auto GetDefaultOwnerThread() const -> ThreadIdentifier override; + auto client_source() const -> AudioSource* { return client_source_.get(); } + auto source_sound() const -> SoundData* { + return source_sound_ ? source_sound_->get() : nullptr; + } + + void UpdatePitch(); + void UpdateVolume(); + void ExecStop(); + void ExecPlay(); + void Update(); + + private: + bool looping_ = false; + std::unique_ptr client_source_; + float fade_ = 1.0f; + float gain_ = 1.0f; + AudioServer* audio_thread_; + bool valid_ = false; + const Object::Ref* source_sound_ = nullptr; + int id_; + uint32_t play_count_ = 0; + bool is_actually_playing_ = false; + bool want_to_play_ = false; +#if BA_ENABLE_AUDIO + ALuint source_ = 0; +#endif + bool is_streamed_ = false; + + /// Whether we should be designated as "music" next time we play. + bool is_music_ = false; + + /// Whether currently playing as music. + bool current_is_music_ = false; + +#if BA_ENABLE_AUDIO + Object::Ref streamer_; +#endif + + friend class AudioServer; +}; // ThreadSource + +struct AudioServer::SoundFadeNode { + uint32_t play_id; + millisecs_t starttime; + millisecs_t endtime; + bool out; + SoundFadeNode(uint32_t play_id_in, millisecs_t duration_in, bool out_in) + : play_id(play_id_in), + starttime(GetRealTime()), + endtime(GetRealTime() + duration_in), + out(out_in) {} +}; + +void AudioServer::SetPaused(bool pause) { + if (!paused_) { + if (!pause) { + Log("Error: got audio unpause request when already unpaused."); + } else { +#if BA_OSTYPE_IOS_TVOS + // apple recommends this during audio-interruptions.. + // http://developer.apple.com/library/ios/#documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Cookbook/ + // Cookbook.html#//apple_ref/doc/uid/TP40007875-CH6-SW38 + alcMakeContextCurrent(nullptr); +#endif + +// On android lets tell open-sl to stop its processing. +#if BA_OSTYPE_ANDROID + opensl_pause_playback(); +#endif // BA_OSTYPE_ANDROID + + paused_ = true; + } + } else { + // unpause if requested.. + if (pause) { + Log("Error: Got audio pause request when already paused."); + } else { +#if BA_OSTYPE_IOS_TVOS + // apple recommends this during audio-interruptions.. + // http://developer.apple.com/library/ios/#documentation/Audio/ + // Conceptual/AudioSessionProgrammingGuide/Cookbook/ + // Cookbook.html#//apple_ref/doc/uid/TP40007875-CH6-SW38 +#if BA_ENABLE_AUDIO + alcMakeContextCurrent(alc_context_); // hmm is this necessary?.. +#endif +#endif +// On android lets tell openal-soft to stop processing. +#if BA_OSTYPE_ANDROID + opensl_resume_playback(); +#endif // BA_OSTYPE_ANDROID + + paused_ = false; +#if BA_ENABLE_AUDIO + CHECK_AL_ERROR; +#endif // BA_ENABLE_AUDIO + + // Go through all of our sources and stop any we've wanted to stop while + // paused. + for (auto&& i : sources_) { + if ((!i->want_to_play()) && (i->is_actually_playing())) { + i->ExecStop(); + } + } + } + } +} + +void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) { + PushCall([this, play_id, val] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->SetIsMusic(val); + } + }); +} + +void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) { + PushCall([this, play_id, val] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->SetPositional(val); + } + }); +} + +void AudioServer::PushSourceSetPositionCall(uint32_t play_id, + const Vector3f& p) { + PushCall([this, play_id, p] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->SetPosition(p.x, p.y, p.z); + } + }); +} + +void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) { + PushCall([this, play_id, val] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->SetGain(val); + } + }); +} + +void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) { + PushCall([this, play_id, val] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->SetFade(val); + } + }); +} + +void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) { + PushCall([this, play_id, val] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->SetLooping(val); + } + }); +} + +void AudioServer::PushSourcePlayCall(uint32_t play_id, + Object::Ref* sound) { + PushCall([this, play_id, sound] { + ThreadSource* s = GetPlayingSound(play_id); + + // If this play command is valid, pass it along. + // Otherwise return it immediately for deletion. + if (s) { + s->Play(sound); + } else { + AddSoundRefDelete(sound); + } + + // Let's take this opportunity to pass on newly available sources. + // This way the more things clients are playing, the more + // tight our source availability checking gets (instead of solely relying on + // our periodic process() calls). + UpdateAvailableSources(); + }); +} + +void AudioServer::PushSourceStopCall(uint32_t play_id) { + PushCall([this, play_id] { + ThreadSource* s = GetPlayingSound(play_id); + if (s) { + s->Stop(); + } + }); +} + +void AudioServer::PushSourceEndCall(uint32_t play_id) { + PushCall([this, play_id] { + ThreadSource* s = GetPlayingSound(play_id); + assert(s); + s->client_source()->Lock(5); + s->client_source()->set_client_queue_size( + s->client_source()->client_queue_size() - 1); + assert(s->client_source()->client_queue_size() >= 0); + s->client_source()->Unlock(); + }); +} + +void AudioServer::PushResetCall() { + PushCall([this] { Reset(); }); +} + +void AudioServer::PushSetListenerPositionCall(const Vector3f& p) { + PushCall([this, p] { +#if BA_ENABLE_AUDIO + if (!paused_) { + ALfloat lpos[3] = {p.x, p.y, p.z}; + alListenerfv(AL_POSITION, lpos); + CHECK_AL_ERROR; + } +#endif // BA_ENABLE_AUDIO + }); +} + +void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward, + const Vector3f& up) { + PushCall([this, forward, up] { +#if BA_ENABLE_AUDIO + if (!paused_) { + ALfloat lorient[6] = {forward.x, forward.y, forward.z, up.x, up.y, up.z}; + alListenerfv(AL_ORIENTATION, lorient); + CHECK_AL_ERROR; + } +#endif // BA_ENABLE_AUDIO + }); +} + +AudioServer::AudioServer(Thread* thread) : Module("audio", thread) { + // we're a singleton.. + assert(g_audio_server == nullptr); + g_audio_server = this; + + // Get our thread to give us periodic processing time. + process_timer_ = NewThreadTimer(kAudioProcessIntervalNormal, true, + NewLambdaRunnable([this] { Process(); })); + +#if BA_ENABLE_AUDIO + + // Bring up OpenAL stuff. + { + const char* alDeviceName = nullptr; + +// On the rift build in vr mode we need to make sure we open the rift audio +// device. +#if BA_RIFT_BUILD + if (IsVRMode()) { + ALboolean enumeration = + alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"); + if (enumeration == AL_FALSE) { + Log("OpenAL enumeration extensions missing."); + } else { + const ALCchar* devices = + alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + const ALCchar *device = devices, *next = devices + 1; + size_t len = 0; + + // If the string is blank, we weren't able to find the oculus + // audio device. In that case we'll just go with default. + if (g_rift_audio_device_name != "") { + // Log("AL Devices list:"); + // Log("----------"); + while (device && *device != '\0' && next && *next != '\0') { + // These names seem to be things like "OpenAL Soft on FOO" + // ..we should be able to search for FOO. + if (strstr(device, g_rift_audio_device_name.c_str())) { + alDeviceName = device; + } + len = strlen(device); + device += (len + 1); + next += (len + 2); + } + // Log("----------"); + } + } + } +#endif // BA_RIFT_BUILD + + ALCdevice* device; + device = alcOpenDevice(alDeviceName); + BA_PRECONDITION(device); + alc_context_ = alcCreateContext(device, nullptr); + BA_PRECONDITION(alc_context_); + BA_PRECONDITION(alcMakeContextCurrent(alc_context_)); + CHECK_AL_ERROR; + } + + ALfloat listener_pos[] = {0.0f, 0.0f, 0.0f}; + ALfloat listener_vel[] = {0.0f, 0.0f, 0.0f}; + ALfloat listener_ori[] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; + + alListenerfv(AL_POSITION, listener_pos); + alListenerfv(AL_VELOCITY, listener_vel); + alListenerfv(AL_ORIENTATION, listener_ori); + CHECK_AL_ERROR; + + // Create our sources. + int target_source_count = 30; + for (int i = 0; i < target_source_count; i++) { + bool valid = false; + auto s(Object::New(this, i, &valid)); + if (valid) { + s->client_source_ = std::make_unique(i); + g_audio->AddClientSource(&(*s->client_source_)); + sound_source_refs_.push_back(s); + sources_.push_back(&(*s)); + } else { + Log("Error: Made " + std::to_string(i) + " sources; (wanted " + + std::to_string(target_source_count) + ")."); + break; + } + } + CHECK_AL_ERROR; + + // Now make available any stopped sources (should be all of them). + UpdateAvailableSources(); + +#endif // BA_ENABLE_AUDIO +} + +AudioServer::~AudioServer() { +#if BA_ENABLE_AUDIO + sound_source_refs_.clear(); + + // Take down AL stuff. + { + ALCdevice* device; + BA_PRECONDITION_LOG(alcMakeContextCurrent(nullptr)); + device = alcGetContextsDevice(alc_context_); + alcDestroyContext(alc_context_); + assert(alcGetError(device) == ALC_NO_ERROR); + alcCloseDevice(device); + } + assert(streaming_sources_.empty()); + assert(al_source_count_ == 0); + +#endif // BA_ENABLE_AUDIO +} + +void AudioServer::UpdateAvailableSources() { + for (auto&& i : sources_) { + i->UpdateAvailability(); + } + +// Some sanity checking. Every now and then lets go through our sources +// and see how many are in use, how many are currently locked by the client, +// etc. +#if (BA_DEBUG_BUILD || BA_TEST_BUILD) + millisecs_t t = GetRealTime(); + if (t - last_sanity_check_time_ > 5000) { + last_sanity_check_time_ = t; + + int source_count = 0; + int in_use_source_count = 0; + std::list sounds; + for (auto&& i : sources_) { + source_count++; + + if (!i->client_source()->TryLock(4)) { + in_use_source_count++; + + // If this source has been locked for a long time, + // that probably means somebody's grabbing a source but never + // resubmitting it. + if (t - i->client_source()->last_lock_time() > 10000) { + Log("Error: Client audio source has been locked for too long; " + "probably leaked. (debug id " + + std::to_string(i->client_source()->lock_debug_id()) + ")"); + } + continue; + } + if (!i->client_source()->available()) { + in_use_source_count++; + + if (explicit_bool(kShowInUseSounds) && i->source_sound()) { + sounds.push_back((*i->source_sound()).file_name()); + } + } + i->client_source()->Unlock(); + } + + if (explicit_bool(kShowInUseSounds)) { + printf( + "------------------------------------------\n" + "%d out of %d sources in use\n", + in_use_source_count, source_count); + for (auto&& i : sounds) { + printf("%s\n", i.c_str()); + } + fflush(stdout); + } + } +#endif +} + +void AudioServer::StopSound(uint32_t play_id) { + uint32_t source = source_id_from_play_id(play_id); + uint32_t count = play_count_from_play_id(play_id); + if (source < sources_.size()) { + if (count == sources_[source]->play_count()) sources_[source]->Stop(); + } +} + +auto AudioServer::GetPlayingSound(uint32_t play_id) + -> AudioServer::ThreadSource* { + uint32_t source = source_id_from_play_id(play_id); + uint32_t count = play_count_from_play_id(play_id); + assert(source < sources_.size()); + if (source < sources_.size()) { + // If the sound has finished playing or whatnot, we + // want to make it available to the client as a new sound, + // not return it here. + sources_[source]->UpdateAvailability(); + + // If it still looks like its ours, return it.. + if (count == sources_[source]->play_count()) { + return sources_[source]; + } + } + return nullptr; +} + +void AudioServer::UpdateTimerInterval() { + // If we've got pending loads, go into uber-hyperactive mode. + if (have_pending_loads_) { + assert(process_timer_); + process_timer_->SetLength(kAudioProcessIntervalPendingLoad); + } else { + // If we're processing fades, run a bit higher-speed than usual + // for smoothness' sake. + if (!sound_fade_nodes_.empty()) { + assert(process_timer_); + process_timer_->SetLength(kAudioProcessIntervalFade); + } else { + // Nothing but normal activity; just run enough to keep + // buffers filled and whatnot. + assert(process_timer_); + process_timer_->SetLength(kAudioProcessIntervalNormal); + } + } +} + +void AudioServer::SetSoundPitch(float pitch) { + sound_pitch_ = pitch; + if (sound_pitch_ < 0.01f) sound_pitch_ = 0.01f; + for (auto&& i : sources_) { + i->UpdatePitch(); + } +} + +void AudioServer::SetSoundVolume(float volume) { + sound_volume_ = volume; + if (sound_volume_ > 3.0f) { + sound_volume_ = 3.0f; + } + if (sound_volume_ < 0) { + sound_volume_ = 0; + } + for (auto&& i : sources_) { + i->UpdateVolume(); + } +} + +void AudioServer::SetMusicVolume(float volume) { + music_volume_ = volume; + if (music_volume_ > 3.0f) music_volume_ = 3.0f; + if (music_volume_ < 0) music_volume_ = 0; + UpdateMusicPlayState(); + for (auto&& i : sources_) { + i->UpdateVolume(); + } +} + +// Start or stop music playback based on volume/pause-state/etc. +void AudioServer::UpdateMusicPlayState() { + bool should_be_playing = ((music_volume_ > 0.000001f) && !paused_); + + // Flip any playing music off. + if (!should_be_playing) { + for (auto&& i : sources_) { + if (i->current_is_music() && i->is_actually_playing()) { + i->ExecStop(); + } + } + } else { + // Flip music back on that should be playing. + for (auto&& i : sources_) { + if (i->current_is_music() && i->want_to_play() + && (!i->is_actually_playing())) { + i->ExecPlay(); + } + } + } +} + +void AudioServer::Process() { + millisecs_t real_time = GetRealTime(); + + assert(InAudioThread()); + + // If we're paused we don't do nothin'. + if (!paused_) { + // Do some loading... + have_pending_loads_ = g_media->RunPendingAudioLoads(); + + // Keep that available-sources list filled. + UpdateAvailableSources(); + + // Update our fading sound volumes. + if (real_time - last_sound_fade_process_time_ > 50) { + ProcessSoundFades(); + last_sound_fade_process_time_ = real_time; + } + + // Update streaming sources. + if (real_time - last_stream_process_time_ > 100) { + last_stream_process_time_ = real_time; + for (auto&& i : streaming_sources_) { + i->Update(); + } + } +#if BA_ENABLE_AUDIO + CHECK_AL_ERROR; +#endif + } + UpdateTimerInterval(); +} + +void AudioServer::Reset() { + // Stop all playing sounds. + for (auto&& i : sources_) { + i->Stop(); + } + SetSoundPitch(1.0f); +} + +void AudioServer::ProcessSoundFades() { + auto i = sound_fade_nodes_.begin(); + decltype(i) i_next; + while (i != sound_fade_nodes_.end()) { + i_next = i; + i_next++; + + AudioServer::ThreadSource* s = GetPlayingSound(i->second.play_id); + if (s) { + if (GetRealTime() > i->second.endtime) { + StopSound(i->second.play_id); + sound_fade_nodes_.erase(i); + } else { + float fade_val = + 1 + - (static_cast(GetRealTime() - i->second.starttime) + / static_cast(i->second.endtime - i->second.starttime)); + s->SetFade(fade_val); + } + } else { + sound_fade_nodes_.erase(i); + } + i = i_next; + } +} + +void AudioServer::FadeSoundOut(uint32_t play_id, uint32_t time) { + // Pop a new node on the list (this won't overwrite the old if there is one). + sound_fade_nodes_.insert( + std::make_pair(play_id, SoundFadeNode(play_id, time, true))); +} + +void AudioServer::DeleteMediaComponent(MediaComponentData* c) { + assert(InAudioThread()); + c->Unload(); + delete c; +} + +AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in, + bool* valid_out) + : id_(id_in), audio_thread_(audio_thread_in) { +#if BA_ENABLE_AUDIO + assert(valid_out != nullptr); + CHECK_AL_ERROR; + + // Generate our sources. + alGenSources(1, &source_); + ALenum err = alGetError(); + valid_ = (err == AL_NO_ERROR); + if (!valid_) { + Log(std::string("Error: AL Error ") + GetALErrorString(err) + + " on source creation."); + } else { + // In vr mode we keep the microphone a bit closer to the camera + // for realism purposes, so we need stuff louder in general. + if (IsVRMode()) { + alSourcef(source_, AL_MAX_DISTANCE, 100); + alSourcef(source_, AL_REFERENCE_DISTANCE, 7.5f); + } else { + // In regular mode our mic is stuck closer to the action + // so less loudness is needed. + alSourcef(source_, AL_MAX_DISTANCE, 100); + alSourcef(source_, AL_REFERENCE_DISTANCE, 5.0f); + } + alSourcef(source_, AL_ROLLOFF_FACTOR, 0.3f); + CHECK_AL_ERROR; + } + *valid_out = valid_; + if (valid_) al_source_count_++; + +#endif // BA_ENABLE_AUDIO +} + +AudioServer::ThreadSource::~ThreadSource() { +#if BA_ENABLE_AUDIO + + if (!valid_) { + return; + } + Stop(); + + // Remove us from sources list. + for (auto i = audio_thread_->sources_.begin(); + i != audio_thread_->sources_.end(); ++i) { + if (*i == this) { + audio_thread_->sources_.erase(i); + break; + } + } + + assert(!is_actually_playing_ && !want_to_play_); + assert(!source_sound_); + + alDeleteSources(1, &source_); + CHECK_AL_ERROR; + al_source_count_--; + +#endif // BA_ENABLE_AUDIO +} + +auto AudioServer::ThreadSource::GetDefaultOwnerThread() const + -> ThreadIdentifier { + return ThreadIdentifier::kAudio; +} + +void AudioServer::ThreadSource::UpdateAvailability() { +#if BA_ENABLE_AUDIO + + assert(InAudioThread()); + + // If its waiting to be picked up by a client or has pending client commands, + // skip. + if (!client_source_->TryLock(6)) { + return; + } + + // Already available or has pending client commands; don't change anything. + if (client_source_->available() || client_source_->client_queue_size() > 0) { + client_source_->Unlock(); + return; + } + + // We consider ourselves busy if there's an active looping play command + // (regardless of its actual physical play state - music could be turned off, + // stuttering, etc). + // If its non-looping, we check its play state and snatch it if its not + // playing. + bool busy; + if (looping_ || (is_streamed_ && streamer_.exists() && streamer_->loops())) { + busy = want_to_play_; + } else { + // If our context is paused, we know nothing is playing + // (and we cant ask AL cuz we have no context). + if (g_audio_server->paused()) { + busy = false; + } else { + ALint state; + alGetSourcei(source_, AL_SOURCE_STATE, &state); + CHECK_AL_ERROR; + busy = (state == AL_PLAYING); + } + } + + // Ok, now if we can get a lock on the availability list, go ahead and + // make this guy available; give him a new play id and reset his state. + // If we can't get a lock its no biggie.. we'll come back to this guy later. + + if (!busy) { + if (g_audio->available_sources_mutex().try_lock()) { + std::lock_guard lock(g_audio->available_sources_mutex(), + std::adopt_lock); + Stop(); + Reset(); +#if BA_DEBUG_BUILD + uint32_t old_play_id = play_id(); +#endif + // Needs to always be a 16 bit value. + play_count_ = (play_count_ + 1) % 30000; + assert(old_play_id != play_id()); + client_source_->MakeAvailable(play_id()); + } + } + client_source_->Unlock(); + +#endif // BA_ENABLE_AUDIO +} + +void AudioServer::ThreadSource::Update() { +#if BA_ENABLE_AUDIO + assert(is_streamed_ && is_actually_playing_); + streamer_->Update(); +#endif +} + +void AudioServer::ThreadSource::SetIsMusic(bool m) { is_music_ = m; } + +void AudioServer::ThreadSource::SetGain(float g) { + gain_ = g; + UpdateVolume(); +} + +void AudioServer::ThreadSource::SetFade(float f) { + fade_ = f; + UpdateVolume(); +} + +void AudioServer::ThreadSource::SetLooping(bool loop) { + looping_ = loop; + if (!g_audio_server->paused()) { +#if BA_ENABLE_AUDIO + alSourcei(source_, AL_LOOPING, loop); + CHECK_AL_ERROR; +#endif + } +} + +void AudioServer::ThreadSource::SetPositional(bool p) { +#if BA_ENABLE_AUDIO + if (!g_audio_server->paused()) { + // TODO(ericf): Don't allow setting of positional + // on stereo sounds - we check this at initial play() + // but should do it here too. + alSourcei(source_, AL_SOURCE_RELATIVE, !p); + CHECK_AL_ERROR; + } +#endif +} + +void AudioServer::ThreadSource::SetPosition(float x, float y, float z) { +#if BA_ENABLE_AUDIO + if (!g_audio_server->paused()) { + bool oob = false; + if (x < -500) { + oob = true; + x = -500; + } else if (x > 500) { + oob = true; + x = 500; + } + if (y < -500) { + oob = true; + y = -500; + } else if (y > 500) { + oob = true; + y = 500; + } + if (z < -500) { + oob = true; + z = -500; + } else if (z > 500) { + oob = true; + z = 500; + } + if (oob) { + BA_LOG_ONCE( + "Error: AudioServer::ThreadSource::SetPosition" + " got out-of-bounds value."); + } + ALfloat source_pos[] = {x, y, z}; + alSourcefv(source_, AL_POSITION, source_pos); + CHECK_AL_ERROR; + } +#endif // BA_ENABLE_AUDIO +} + +// Actually begin playback. +void AudioServer::ThreadSource::ExecPlay() { +#if BA_ENABLE_AUDIO + + assert(source_sound_->exists()); + assert((**source_sound_).valid()); + assert((**source_sound_).loaded()); + assert(!is_actually_playing_); + CHECK_AL_ERROR; + + if (is_streamed_) { + // Turn off looping on the source - the streamer handles looping for us. + alSourcei(source_, AL_LOOPING, false); + CHECK_AL_ERROR; + looping_ = false; + + // Push us on the list of streaming sources if we're not on it. + for (auto&& i : audio_thread_->streaming_sources_) { + if (i == this) { + throw Exception(); + } + } + audio_thread_->streaming_sources_.push_back(this); + + // Make sure stereo sounds aren't positional. + // This is default behavior on Mac/Win but we enforce it for linux. + // (though currently linux stereo sounds play in mono... eww)) + + bool do_normal = true; + // In vr mode, play non-positional sounds positionally in space roughly + // where the menu is. + if (IsVRMode()) { + do_normal = false; + SetPositional(true); + SetPosition(0.0f, 4.5f, -3.0f); + } + + if (do_normal) { + SetPositional(false); + SetPosition(0, 0, 0); + } + + // Play if we're supposed to. + if (!streamer_->Play()) { + throw Exception(); + } + + } else { // Not streamed + // Make sure stereo sounds aren't positional. + // This is default behavior on Mac/Win but we enforce it for linux. + // (though currently linux stereo sounds play in mono... eww)) + if ((**source_sound_).format() == AL_FORMAT_STEREO16) { + SetPositional(false); + SetPosition(0, 0, 0); + } + alSourcePlay(source_); + CHECK_AL_ERROR; + } + is_actually_playing_ = true; + +#endif // BA_ENABLE_AUDIO +} + +auto AudioServer::ThreadSource::Play(const Object::Ref* sound) + -> uint32_t { +#if BA_ENABLE_AUDIO + + // FatalError("Testing other thread."); + + assert(InAudioThread()); + assert(sound->exists()); + + // Stop whatever we were doing. + Stop(); + + assert(source_sound_ == nullptr); + source_sound_ = sound; + + if (!g_audio_server->paused()) { + // Ok, here's where we might start needing to access our media.. can't hold + // off any longer.. + (**source_sound_).Load(); + + is_streamed_ = (**source_sound_).is_streamed(); + current_is_music_ = is_music_; + + if (is_streamed_) { + streamer_ = Object::New( + (**source_sound_).file_name_full().c_str(), source_, looping_); + } else { + alSourcei(source_, AL_BUFFER, (**source_sound_).buffer()); + } + CHECK_AL_ERROR; + + // Always update our volume and pitch here (we may be changing from music to + // nonMusic, etc) + UpdateVolume(); + UpdatePitch(); + + bool music_should_play = ((g_audio_server->music_volume_ > 0.000001f) + && !g_audio_server->paused()); + if ((!current_is_music_) || music_should_play) { + ExecPlay(); + } + } + want_to_play_ = true; + +#endif // BA_ENABLE_AUDIO + + return play_id(); +} + +void AudioServer::ThreadSource::ExecStop() { +#if BA_ENABLE_AUDIO + assert(InAudioThread()); + assert(!g_audio_server->paused()); + assert(is_actually_playing_); + if (streamer_.exists()) { + assert(is_streamed_); + streamer_->Stop(); + for (auto i = audio_thread_->streaming_sources_.begin(); + i != audio_thread_->streaming_sources_.end(); ++i) { + if (*i == this) { + audio_thread_->streaming_sources_.erase(i); + break; + } + } + } else { + alSourceStop(source_); + CHECK_AL_ERROR; + } + CHECK_AL_ERROR; + is_actually_playing_ = false; + +#endif // BA_ENABLE_AUDIO +} + +// Do a complete stop.. take us off the music list, detach our source, etc. +void AudioServer::ThreadSource::Stop() { +#if BA_ENABLE_AUDIO + assert(g_audio_server); + + // If our context is paused we can't actually stop now; just record our + // intent. + if (g_audio_server->paused()) { + want_to_play_ = false; + } else { + if (is_actually_playing_) ExecStop(); + if (streamer_.exists()) { + streamer_.Clear(); + } + // If we've got an attached sound, toss it back to the main thread + // to free up... + // (we can't kill media-refs outside of the main thread) + if (source_sound_) { + assert(g_media); + g_audio_server->AddSoundRefDelete(source_sound_); + source_sound_ = nullptr; + } + want_to_play_ = false; + } +#endif // BA_ENABLE_AUDIO +} + +void AudioServer::ThreadSource::UpdateVolume() { +#if BA_ENABLE_AUDIO + assert(InAudioThread()); + if (!g_audio_server->paused()) { + float val = gain_ * fade_; + if (current_is_music()) { + val *= audio_thread_->music_volume() / 7.0f; + } else { + val *= audio_thread_->sound_volume(); + } + alSourcef(source_, AL_GAIN, std::max(0.0f, val)); + CHECK_AL_ERROR; + } +#endif // BA_ENABLE_AUDIO +} + +void AudioServer::ThreadSource::UpdatePitch() { +#if BA_ENABLE_AUDIO + assert(InAudioThread()); + if (!g_audio_server->paused()) { + float val = 1.0f; + if (current_is_music()) { + } else { + val *= audio_thread_->sound_pitch(); + } + alSourcef(source_, AL_PITCH, val); + CHECK_AL_ERROR; + } +#endif // BA_ENABLE_AUDIO +} + +void AudioServer::PushSetVolumesCall(float music_volume, float sound_volume) { + PushCall([this, music_volume, sound_volume] { + SetSoundVolume(sound_volume); + SetMusicVolume(music_volume); + }); +} + +void AudioServer::PushSetSoundPitchCall(float val) { + PushCall([this, val] { SetSoundPitch(val); }); +} + +void AudioServer::PushSetPausedCall(bool pause) { + PushCall([this, pause] { + if (g_buildconfig.ostype_android()) { + Log("Error: Shouldn't be getting SetPausedCall on android."); + } + SetPaused(pause); + }); +} + +void AudioServer::PushComponentUnloadCall( + const std::vector*>& components) { + PushCall([this, components] { + // Unload all components we were passed.. + for (auto&& i : components) { + (**i).Unload(); + } + // ..and then ship these pointers back to the game thread so it can free the + // references + g_game->PushFreeMediaComponentRefsCall(components); + }); +} + +void AudioServer::PushHavePendingLoadsCall() { + PushCall([this] { + have_pending_loads_ = true; + UpdateTimerInterval(); + }); +} + +void AudioServer::AddSoundRefDelete(const Object::Ref* c) { + { + std::lock_guard lock(sound_ref_delete_list_mutex_); + sound_ref_delete_list_.push_back(c); + } + // Now push a call to the game thread to do the deletes. + g_game->PushCall([] { g_audio_server->ClearSoundRefDeleteList(); }); +} + +void AudioServer::ClearSoundRefDeleteList() { + assert(InGameThread()); + std::lock_guard lock(sound_ref_delete_list_mutex_); + for (const Object::Ref* i : sound_ref_delete_list_) { + delete i; + } + sound_ref_delete_list_.clear(); +} + +void AudioServer::BeginInterruption() { + assert(!InAudioThread()); + g_audio_server->PushSetPausedCall(true); + + // Wait a reasonable amount of time for the thread to act on it. + millisecs_t t = GetRealTime(); + while (true) { + if (g_audio_server->paused()) { + break; + } + if (GetRealTime() - t > 1000) { + Log("Error: Timed out waiting for audio pause."); + break; + } + Platform::SleepMS(2); + } +} + +void AudioServer::HandleThreadPause() { SetPaused(true); } + +void AudioServer::HandleThreadResume() { SetPaused(false); } + +void AudioServer::EndInterruption() { + assert(!InAudioThread()); + g_audio_server->PushSetPausedCall(false); + + // Wait a reasonable amount of time for the thread to act on it. + millisecs_t t = GetRealTime(); + while (true) { + if (!g_audio_server->paused()) { + break; + } + if (GetRealTime() - t > 1000) { + Log("Error: Timed out waiting for audio unpause."); + break; + } + Platform::SleepMS(2); + } +} + +} // namespace ballistica diff --git a/src/ballistica/audio/audio_server.h b/src/ballistica/audio/audio_server.h new file mode 100644 index 00000000..68f89598 --- /dev/null +++ b/src/ballistica/audio/audio_server.h @@ -0,0 +1,142 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_AUDIO_AUDIO_SERVER_H_ +#define BALLISTICA_AUDIO_AUDIO_SERVER_H_ + +#include +#include +#include + +#include "ballistica/core/module.h" + +namespace ballistica { + +/// A module that handles audio processing. +class AudioServer : public Module { + public: + static auto source_id_from_play_id(uint32_t play_id) -> uint32_t { + return play_id & 0xFFFFu; + } + + static auto play_count_from_play_id(uint32_t play_id) -> uint32_t { + return play_id >> 16u; + } + + explicit AudioServer(Thread* o); + + void PushSetVolumesCall(float music_volume, float sound_volume); + void PushSetSoundPitchCall(float val); + void PushSetPausedCall(bool pause); + + void HandleThreadPause() override; + void HandleThreadResume() override; + + static void BeginInterruption(); + static void EndInterruption(); + + void PushSetListenerPositionCall(const Vector3f& p); + void PushSetListenerOrientationCall(const Vector3f& forward, + const Vector3f& up); + void PushResetCall(); + void PushHavePendingLoadsCall(); + void PushComponentUnloadCall( + const std::vector*>& components); + + /// For use by g_game_module(). + void ClearSoundRefDeleteList(); + + auto paused() const -> bool { return paused_; } + + private: + class ThreadSource; + ~AudioServer() override; + + // Client sources use these to pass settings to the server. + void PushSourceSetIsMusicCall(uint32_t play_id, bool val); + void PushSourceSetPositionalCall(uint32_t play_id, bool val); + void PushSourceSetPositionCall(uint32_t play_id, const Vector3f& p); + void PushSourceSetGainCall(uint32_t play_id, float val); + void PushSourceSetFadeCall(uint32_t play_id, float val); + void PushSourceSetLoopingCall(uint32_t play_id, bool val); + void PushSourcePlayCall(uint32_t play_id, Object::Ref* sound); + void PushSourceStopCall(uint32_t play_id); + void PushSourceEndCall(uint32_t play_id); + + void SetPaused(bool paused); + + // Fade a playing sound out over the given time. If it is already + // fading or does not exist, does nothing. + void FadeSoundOut(uint32_t play_id, uint32_t time); + + // Stop a sound from playing if it exists. + void StopSound(uint32_t play_id); + void SetMusicVolume(float volume); + void SetSoundVolume(float volume); + void SetSoundPitch(float pitch); + auto music_volume() -> float { return music_volume_; } + auto sound_volume() -> float { return sound_volume_; } + auto sound_pitch() -> float { return sound_pitch_; } + + /// If a sound play id is currently playing, return the sound. + auto GetPlayingSound(uint32_t play_id) -> ThreadSource*; + + void Reset(); + void Process(); + + /// Send a component to the audio thread to delete. + void DeleteMediaComponent(MediaComponentData* c); + + void UpdateTimerInterval(); + void UpdateAvailableSources(); + void UpdateMusicPlayState(); + void ProcessSoundFades(); + + // Some threads such as audio hold onto allocated Media-Component-Refs to keep + // media components alive that they need. Media-Component-Refs, however, must + // be disposed of in the game thread, so they are passed back to it through + // this function. + void AddSoundRefDelete(const Object::Ref* c); + + Timer* process_timer_{}; + bool have_pending_loads_{}; + bool paused_{}; + millisecs_t last_sound_fade_process_time_{}; + +#if BA_ENABLE_AUDIO + ALCcontext* alc_context_; +#endif + + float sound_volume_{1.0f}; + float sound_pitch_{1.0f}; + float music_volume_{1.0f}; + + /// Indexed list of sources. + std::vector sources_; + std::vector streaming_sources_; + millisecs_t last_stream_process_time_{}; + + // Holds refs to all sources. + // Use sources, not this, for faster iterating. + std::vector > sound_source_refs_; + struct SoundFadeNode; + std::map sound_fade_nodes_; + + // This mutex controls access to our list of media component shared ptrs to + // delete in the main thread. + std::mutex sound_ref_delete_list_mutex_; + + // Our list of sound media components to delete via the main thread. + std::vector*> sound_ref_delete_list_; + + millisecs_t last_sanity_check_time_{}; + + static int al_source_count_; + + // FIXME: Try to kill these. + friend class AudioSource; + friend class Audio; +}; + +} // namespace ballistica + +#endif // BALLISTICA_AUDIO_AUDIO_SERVER_H_ diff --git a/src/ballistica/audio/audio_source.cc b/src/ballistica/audio/audio_source.cc new file mode 100644 index 00000000..fe2d1127 --- /dev/null +++ b/src/ballistica/audio/audio_source.cc @@ -0,0 +1,133 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/audio/audio_source.h" + +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/audio/audio_server.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/media/data/sound_data.h" + +namespace ballistica { + +AudioSource::AudioSource(int id_in) : id_(id_in) {} + +AudioSource::~AudioSource() { assert(client_queue_size_ == 0); } + +void AudioSource::MakeAvailable(uint32_t play_id_new) { + assert(AudioServer::source_id_from_play_id(play_id_new) == id_); + assert(client_queue_size_ == 0); + assert(locked()); + play_id_ = play_id_new; + assert(!available_); + assert(g_audio); + g_audio->MakeSourceAvailable(this); + available_ = true; +} + +void AudioSource::SetIsMusic(bool val) { + assert(g_audio_server); + assert(client_queue_size_ > 0); + g_audio_server->PushSourceSetIsMusicCall(play_id_, val); +} + +void AudioSource::SetPositional(bool val) { + assert(g_audio_server); + assert(client_queue_size_ > 0); + g_audio_server->PushSourceSetPositionalCall(play_id_, val); +} + +void AudioSource::SetPosition(float x, float y, float z) { + assert(g_audio_server); + assert(client_queue_size_ > 0); +#if BA_DEBUG_BUILD + if (std::isnan(x) || std::isnan(y) || std::isnan(z)) { + Log("Error: Got nan value in AudioSource::SetPosition."); + } +#endif + g_audio_server->PushSourceSetPositionCall(play_id_, Vector3f(x, y, z)); +} + +void AudioSource::SetGain(float val) { + assert(g_audio_server); + assert(client_queue_size_ > 0); + g_audio_server->PushSourceSetGainCall(play_id_, val); +} + +void AudioSource::SetFade(float val) { + assert(g_audio_server); + assert(client_queue_size_ > 0); + g_audio_server->PushSourceSetFadeCall(play_id_, val); +} + +void AudioSource::SetLooping(bool val) { + assert(g_audio_server); + assert(client_queue_size_ > 0); + g_audio_server->PushSourceSetLoopingCall(play_id_, val); +} + +auto AudioSource::Play(SoundData* ptr_in) -> uint32_t { + assert(ptr_in); + assert(g_audio_server); + assert(client_queue_size_ > 0); + + // allocate a new reference to this guy and pass it along + // to the thread... (these refs can't be created or destroyed + // or have their ref-counts changed outside of the main thread...) + // the thread will then send back this allocated ptr when its done + // with it for the main thread to destroy. + + ptr_in->UpdatePlayTime(); + auto ptr = new Object::Ref(ptr_in); + g_audio_server->PushSourcePlayCall(play_id_, ptr); + return play_id_; +} + +void AudioSource::Stop() { + assert(g_audio_server); + assert(client_queue_size_ > 0); + g_audio_server->PushSourceStopCall(play_id_); +} + +void AudioSource::End() { + assert(client_queue_size_ > 0); + // send the thread a "this source is potentially free now" message + assert(g_audio_server); + g_audio_server->PushSourceEndCall(play_id_); + Unlock(); +} + +void AudioSource::Lock(int debug_id) { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + mutex_.lock(); +#if BA_DEBUG_BUILD + last_lock_time_ = GetRealTime(); + lock_debug_id_ = debug_id; + locked_ = true; +#endif + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); +} + +auto AudioSource::TryLock(int debug_id) -> bool { + bool locked = mutex_.try_lock(); +#if (BA_DEBUG_BUILD || BA_TEST_BUILD) + if (locked) { + locked_ = true; + last_lock_time_ = GetRealTime(); + lock_debug_id_ = debug_id; + } +#endif + return locked; +} + +void AudioSource::Unlock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + mutex_.unlock(); + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); +#if BA_DEBUG_BUILD || BA_TEST_BUILD + locked_ = false; +#endif +} + +} // namespace ballistica diff --git a/src/ballistica/audio/audio_source.h b/src/ballistica/audio/audio_source.h new file mode 100644 index 00000000..3663d77c --- /dev/null +++ b/src/ballistica/audio/audio_source.h @@ -0,0 +1,71 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_AUDIO_AUDIO_SOURCE_H_ +#define BALLISTICA_AUDIO_AUDIO_SOURCE_H_ + +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +// Location for sound emission (client version) +class AudioSource { + public: + // Sets whether a source is "music". + // This mainly just influences which volume controls + // affect it. + void SetIsMusic(bool m); + + // Sets whether a source is positional. + // A non-positional source's position coords are always + // relative to the listener. ie: 0,0,0 will always be centered. + void SetPositional(bool p); + void SetPosition(float x, float y, float z); + void SetGain(float g); + void SetFade(float f); + void SetLooping(bool loop); + auto Play(SoundData* ptr) -> uint32_t; + void Stop(); + + // Always call this when done sending commands to the source. + void End(); + ~AudioSource(); + + // Lock the source. Sources must be locked whenever calling any public func. + void Lock(int debug_id); + + // Attempt to lock the source, but will not block. Returns true if + // successful. + auto TryLock(int debug_id) -> bool; + void Unlock(); + explicit AudioSource(int id); + auto id() const -> int { return id_; } +#if BA_DEBUG_BUILD || BA_TEST_BUILD + auto last_lock_time() const -> millisecs_t { return last_lock_time_; } + auto lock_debug_id() const -> int { return lock_debug_id_; } + auto locked() const -> bool { return locked_; } +#endif + auto available() const -> bool { return available_; } + void set_available(bool val) { available_ = val; } + void MakeAvailable(uint32_t play_id); + auto client_queue_size() const -> int { return client_queue_size_; } + void set_client_queue_size(int val) { client_queue_size_ = val; } + auto play_id() const -> uint32_t { return play_id_; } + + private: + std::mutex mutex_; +#if BA_DEBUG_BUILD || BA_TEST_BUILD + millisecs_t last_lock_time_ = 0; + int lock_debug_id_ = 0; + bool locked_ = false; +#endif + int client_queue_size_ = 0; + bool available_ = false; + int id_ = 0; + uint32_t play_id_ = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_AUDIO_AUDIO_SOURCE_H_ diff --git a/src/ballistica/audio/audio_streamer.cc b/src/ballistica/audio/audio_streamer.cc new file mode 100644 index 00000000..4711e171 --- /dev/null +++ b/src/ballistica/audio/audio_streamer.cc @@ -0,0 +1,145 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/audio/audio_streamer.h" + +#include "ballistica/audio/audio.h" +#include "ballistica/audio/audio_server.h" + +// Need to move away from OpenAL on Apple stuff. +#if __clang__ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +namespace ballistica { + +#if BA_ENABLE_AUDIO +AudioStreamer::AudioStreamer(const char* file_name, ALuint source_in, bool loop) + : source_(source_in), file_name_(file_name), loops_(loop) { + assert(InAudioThread()); + alGenBuffers(kAudioStreamBufferCount, buffers_); + CHECK_AL_ERROR; +} + +AudioStreamer::~AudioStreamer() { + assert(!playing_); + assert(g_audio_server); + + alDeleteBuffers(kAudioStreamBufferCount, buffers_); + CHECK_AL_ERROR; +} + +auto AudioStreamer::Play() -> bool { + CHECK_AL_ERROR; + assert(!playing_); + playing_ = true; + + // In case the source is already attached to something. + DetachBuffers(); + + // Fill all our buffers with data. + for (unsigned int buffer : buffers_) { + if (!Stream(buffer)) { + return false; + } + } + + alSourceQueueBuffers(source_, kAudioStreamBufferCount, buffers_); + CHECK_AL_ERROR; + + alSourcePlay(source_); + CHECK_AL_ERROR; + + return true; +} + +void AudioStreamer::Stop() { + CHECK_AL_ERROR; + assert(playing_); + alSourceStop(source_); + CHECK_AL_ERROR; + playing_ = false; + DetachBuffers(); + DoStop(); +} + +void AudioStreamer::Update() { + if (eof_) return; + + CHECK_AL_ERROR; + + assert(playing_); + + ALint queued; + ALint processed; + + // See how many buffers have been processed. + alGetSourcei(source_, AL_BUFFERS_QUEUED, &queued); + CHECK_AL_ERROR; + alGetSourcei(source_, AL_BUFFERS_PROCESSED, &processed); + CHECK_AL_ERROR; + + // A fun anomaly in the linux version; we sometimes get more + // "processed" buffers than we have queued. + if (queued < processed) { + Log("Error: streamer oddness: queued(" + std::to_string(queued) + + "); processed(" + std::to_string(processed) + ")"); + processed = queued; + } + + // Pull the completed ones off, refill them, and queue them back up. + while (processed--) { + ALuint buffer; + alSourceUnqueueBuffers(source_, 1, &buffer); + CHECK_AL_ERROR; + Stream(buffer); + if (!eof_) { + alSourceQueueBuffers(source_, 1, &buffer); + CHECK_AL_ERROR; + } + } + + // Restart playback if need be. + ALenum state; + alGetSourcei(source_, AL_SOURCE_STATE, &state); + CHECK_AL_ERROR; + + if (state != AL_PLAYING) { + printf("AudioServer::Streamer: restarting playback\n"); + fflush(stdout); + + alSourcePlay(source_); + CHECK_AL_ERROR; + } +} + +void AudioStreamer::DetachBuffers() { +#if BA_DEBUG_BUILD + ALint state; + alGetSourcei(source_, AL_SOURCE_STATE, &state); + CHECK_AL_ERROR; + assert(state == AL_INITIAL || state == AL_STOPPED); +#endif + + // This should clear everything. + alSourcei(source_, AL_BUFFER, 0); + CHECK_AL_ERROR; +} + +auto AudioStreamer::Stream(ALuint buffer) -> bool { + char pcm[kAudioStreamBufferSize]; + int size = 0; + unsigned int rate; + CHECK_AL_ERROR; + DoStream(pcm, &size, &rate); + if (size > 0) { + alBufferData(buffer, al_format(), pcm, size, rate); + CHECK_AL_ERROR; + } else { + eof_ = true; + } + return true; +} + +#endif // BA_ENABLE_AUDIO + +} // namespace ballistica diff --git a/src/ballistica/audio/audio_streamer.h b/src/ballistica/audio/audio_streamer.h new file mode 100644 index 00000000..82ce85f0 --- /dev/null +++ b/src/ballistica/audio/audio_streamer.h @@ -0,0 +1,62 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_AUDIO_AUDIO_STREAMER_H_ +#define BALLISTICA_AUDIO_AUDIO_STREAMER_H_ + +#include +#include + +#include "ballistica/audio/al_sys.h" // FIXME: shouldn't need this here. +#include "ballistica/core/object.h" + +namespace ballistica { + +#if BA_ENABLE_AUDIO +// Provider for streamed audio data. +class AudioStreamer : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kAudio; + } + AudioStreamer(const char* file_name, ALuint source, bool loop); + ~AudioStreamer() override; + auto Play() -> bool; + void Stop(); + void Update(); + enum Format { INVALID_FORMAT, MONO16_FORMAT, STEREO16_FORMAT }; + auto al_format() const -> ALenum { + switch (format_) { + case MONO16_FORMAT: + return AL_FORMAT_MONO16; + case STEREO16_FORMAT: + return AL_FORMAT_STEREO16; + default: + break; + } + return INVALID_FORMAT; + } + auto loops() const -> bool { return loops_; } + auto file_name() const -> const std::string& { return file_name_; } + + protected: + virtual void DoStop() = 0; + virtual void DoStream(char* pcm, int* size, unsigned int* rate) = 0; + auto Stream(ALuint buffer) -> bool; + void DetachBuffers(); + void set_format(Format format) { format_ = format; } + + private: + Format format_ = INVALID_FORMAT; + bool playing_ = false; + ALuint buffers_[kAudioStreamBufferCount]{}; + ALuint source_ = 0; + std::string file_name_; + bool loops_ = false; + bool eof_ = false; +}; + +#endif // BA_ENABLE_AUDIO + +} // namespace ballistica + +#endif // BALLISTICA_AUDIO_AUDIO_STREAMER_H_ diff --git a/src/ballistica/audio/ogg_stream.cc b/src/ballistica/audio/ogg_stream.cc new file mode 100644 index 00000000..185a29a8 --- /dev/null +++ b/src/ballistica/audio/ogg_stream.cc @@ -0,0 +1,136 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/audio/ogg_stream.h" + +#include + +#include "ballistica/platform/platform.h" + +namespace ballistica { + +#if BA_ENABLE_AUDIO + +static auto CallbackRead(void* ptr, size_t size, size_t nmemb, + void* data_source) -> size_t { + return fread(ptr, size, nmemb, static_cast(data_source)); +} + +static auto CallbackSeek(void* data_source, ogg_int64_t offset, int whence) + -> int { + return fseek(static_cast(data_source), + static_cast_check_fit(offset), whence); // NOLINT +} +static auto CallbackClose(void* data_source) -> int { + return fclose(static_cast(data_source)); +} +static long CallbackTell(void* data_source) { // NOLINT (ogg wants long) + return ftell(static_cast(data_source)); +} + +OggStream::OggStream(const char* file_name, ALuint source, bool loop) + : AudioStreamer(file_name, source, loop), have_ogg_file_(false) { + int result; + FILE* f; + if (!(f = g_platform->FOpen(file_name, "rb"))) { + throw Exception("can't open ogg file: '" + std::string(file_name) + "'"); + } + ov_callbacks callbacks; + callbacks.read_func = CallbackRead; + callbacks.seek_func = CallbackSeek; + callbacks.close_func = CallbackClose; + callbacks.tell_func = CallbackTell; + + // Have to use callbacks here as codewarrior's FILE struct doesn't + // seem to agree with what vorbis expects... oh well. + // Ericf note Aug 2019: Wow I have comments here old enough to be referencing + // codewarrior; that's awesome! + result = ov_open_callbacks(f, &ogg_file_, nullptr, 0, callbacks); + if (result < 0) { + fclose(f); + throw Exception(GetErrorString(result)); + } + have_ogg_file_ = true; + + vorbis_info_ = ov_info(&ogg_file_, -1); + if (vorbis_info_->channels == 1) { + set_format(MONO16_FORMAT); + } else { + set_format(STEREO16_FORMAT); + } +} + +OggStream::~OggStream() { + if (have_ogg_file_) { + ov_clear(&ogg_file_); + } +} + +void OggStream::DoStop() { + if (have_ogg_file_) ov_pcm_seek(&ogg_file_, 0); +} + +void OggStream::DoStream(char* pcm, int* size, unsigned int* rate) { + int section; + int result; + while ((*size) < kAudioStreamBufferSize) { + // tremor's ov_read takes fewer args +#if (BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID) + result = static_cast(ov_read( + &ogg_file_, pcm + (*size), kAudioStreamBufferSize - (*size), §ion)); +#else + result = static_cast(ov_read(&ogg_file_, pcm + (*size), + kAudioStreamBufferSize - (*size), 0, 2, 1, + §ion)); +#endif // BA_OSTYPE_IOS_TVOS + + if (result > 0) { + (*size) += result; + } else { + if (result < 0) { + static bool reported_error = false; + if (!reported_error) { + reported_error = true; + Log("Error streaming ogg file: '" + file_name() + "'."); + } + if (loops()) { + ov_pcm_seek(&ogg_file_, 0); + } else { + return; + } + } else { + // we hit the end of the file; either reset and keep reading if we're + // looping or just return what we got + if (loops()) { + ov_pcm_seek(&ogg_file_, 0); + } else { + return; + } + } + } + } + if ((*size) == 0 && loops()) { + throw Exception(); + } + (*rate) = static_cast(vorbis_info_->rate); +} + +auto OggStream::GetErrorString(int code) -> std::string { + switch (code) { + case OV_EREAD: + return std::string("Read from media."); + case OV_ENOTVORBIS: + return std::string("Not Vorbis data."); + case OV_EVERSION: + return std::string("Vorbis version mismatch."); + case OV_EBADHEADER: + return std::string("Invalid Vorbis header."); + case OV_EFAULT: + return std::string("Internal logic fault (bug or heap/stack corruption."); + default: + return std::string("Unknown Ogg error."); + } +} + +#endif // BA_ENABLE_AUDIO + +} // namespace ballistica diff --git a/src/ballistica/audio/ogg_stream.h b/src/ballistica/audio/ogg_stream.h new file mode 100644 index 00000000..caec2b39 --- /dev/null +++ b/src/ballistica/audio/ogg_stream.h @@ -0,0 +1,43 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_AUDIO_OGG_STREAM_H_ +#define BALLISTICA_AUDIO_OGG_STREAM_H_ + +#include "ballistica/audio/audio_streamer.h" + +#if BA_ENABLE_AUDIO +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID +#include "ivorbisfile.h" // NOLINT +#else +#include +#endif // BA_OSTYPE_IOS_TVOS +#endif // BA_ENABLE_AUDIO + +#include + +namespace ballistica { + +#if BA_ENABLE_AUDIO + +// Handles streaming ogg audio. +class OggStream : public AudioStreamer { + public: + OggStream(const char* file_name, ALuint source, bool loop); + ~OggStream() override; + + protected: + void DoStop() override; + void DoStream(char* pcm, int* size, unsigned int* rate) override; + + private: + auto GetErrorString(int code) -> std::string; + OggVorbis_File ogg_file_{}; + bool have_ogg_file_; + vorbis_info* vorbis_info_; +}; + +#endif // BA_ENABLE_AUDIO + +} // namespace ballistica + +#endif // BALLISTICA_AUDIO_OGG_STREAM_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics.cc b/src/ballistica/dynamics/bg/bg_dynamics.cc new file mode 100644 index 00000000..ade14fdf --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics.cc @@ -0,0 +1,376 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/bg/bg_dynamics.h" + +#include +#include + +#include "ballistica/core/thread.h" +#include "ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h" +#include "ballistica/dynamics/bg/bg_dynamics_fuse_data.h" +#include "ballistica/dynamics/bg/bg_dynamics_shadow_data.h" +#include "ballistica/dynamics/bg/bg_dynamics_volume_light_data.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/smoke_component.h" +#include "ballistica/graphics/component/sprite_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/media/component/collide_model.h" + +namespace ballistica { + +void BGDynamics::Init() { + // Just init our singleton. + new BGDynamics(); +} + +BGDynamics::BGDynamics() { + assert(InGameThread()); + assert(g_bg_dynamics == nullptr); + g_bg_dynamics = this; +} + +void BGDynamics::AddTerrain(CollideModelData* o) { + assert(InGameThread()); + + // Allocate a fresh reference to keep this collide-model alive as long as + // we're using it. Once we're done, we'll pass the pointer back to the + // main thread to free. + auto* model_ref = new Object::Ref(o); + g_bg_dynamics_server->PushAddTerrainCall(model_ref); +} + +void BGDynamics::RemoveTerrain(CollideModelData* o) { + assert(InGameThread()); + g_bg_dynamics_server->PushRemoveTerrainCall(o); +} + +void BGDynamics::Emit(const BGDynamicsEmission& e) { + assert(InGameThread()); + g_bg_dynamics_server->PushEmitCall(e); +} + +// Call friend client to step our sim. +void BGDynamics::Step(const Vector3f& cam_pos) { + assert(InGameThread()); + + // The BG dynamics thread just processes steps as fast as it can; + // we need to throttle what we send or tell it to cut back if its behind + int step_count = g_bg_dynamics_server->step_count(); + + // If we're really getting behind, start pruning stuff. + if (step_count > 3) { + TooSlow(); + } + + // If we're slightly behind, just don't send this step; + // the bg dynamics will slow down a bit but nothing will disappear this way. + if (step_count > 1) return; + + // Pass a newly allocated raw pointer to the bg-dynamics thread; it takes care + // of disposing it when done. + auto d = Object::NewDeferred(); + d->cam_pos = cam_pos; + + { // Shadows. + BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_shadow_list_lock); + { + std::lock_guard lock( + g_bg_dynamics_server->shadow_list_mutex_); + auto size = g_bg_dynamics_server->shadows_.size(); + d->shadow_step_data_.resize(size); + if (size > 0) { + BGDynamicsShadowData** sd_client = &(g_bg_dynamics_server->shadows_[0]); + std::pair* sd = + &(d->shadow_step_data_[0]); + for (size_t i = 0; i < size; i++) { + // Set to nullptr (for ignore) if the client side is dead. + sd[i].first = sd_client[i]->client_dead ? nullptr : sd_client[i]; + sd[i].second.position = sd_client[i]->pos_client; + } + } + } + BA_DEBUG_TIME_CHECK_END(bg_dynamic_shadow_list_lock, 10); + } + { // Volume lights. + BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_volumelights_list_lock); + { + std::lock_guard lock( + g_bg_dynamics_server->volume_light_list_mutex_); + auto size = g_bg_dynamics_server->volume_lights_.size(); + d->volume_light_step_data_.resize(size); + if (size > 0) { + BGDynamicsVolumeLightData** vd_client = + &(g_bg_dynamics_server->volume_lights_[0]); + std::pair* vd = + &(d->volume_light_step_data_[0]); + for (size_t i = 0; i < size; i++) { + // Set to nullptr (for ignore) if the client side is dead. + vd[i].first = vd_client[i]->client_dead ? nullptr : vd_client[i]; + vd[i].second.pos = vd_client[i]->pos_client; + vd[i].second.radius = vd_client[i]->radius_client; + vd[i].second.r = vd_client[i]->r_client; + vd[i].second.g = vd_client[i]->g_client; + vd[i].second.b = vd_client[i]->b_client; + } + } + } + BA_DEBUG_TIME_CHECK_END(bg_dynamic_volumelights_list_lock, 10); + } + { // Fuses. + BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_fuse_list_lock); + { + std::lock_guard lock(g_bg_dynamics_server->fuse_list_mutex_); + auto size = g_bg_dynamics_server->fuses_.size(); + d->fuse_step_data_.resize(size); + if (size > 0) { + BGDynamicsFuseData** fd_client = &(g_bg_dynamics_server->fuses_[0]); + std::pair* fd = + &(d->fuse_step_data_[0]); + for (size_t i = 0; i < size; i++) { + // Set to nullptr (for ignore) if the client side is dead. + fd[i].first = fd_client[i]->client_dead_ ? nullptr : fd_client[i]; + fd[i].second.transform = fd_client[i]->transform_client_; + fd[i].second.have_transform = fd_client[i]->have_transform_client_; + fd[i].second.length = fd_client[i]->length_client_; + } + } + } + BA_DEBUG_TIME_CHECK_END(bg_dynamic_fuse_list_lock, 10); + } + + // Increase our step count and ship it. + { + std::lock_guard lock(g_bg_dynamics_server->step_count_mutex_); + g_bg_dynamics_server->step_count_++; + } + + // Ok send the thread on its way. + g_bg_dynamics_server->PushStepCall(d); +} + +void BGDynamics::SetDrawSnapshot(BGDynamicsDrawSnapshot* s) { + // We were passed a raw pointer; assign it to our unique_ptr which will + // take ownership of it and handle disposing it when we get the next one. + draw_snapshot_ = std::unique_ptr(s); +} + +void BGDynamics::TooSlow() { + if (!Thread::AreThreadsPaused()) { + g_bg_dynamics_server->PushTooSlowCall(); + } +} + +void BGDynamics::SetDebrisFriction(float val) { + assert(InGameThread()); + g_bg_dynamics_server->PushSetDebrisFrictionCall(val); +} + +void BGDynamics::SetDebrisKillHeight(float val) { + assert(InGameThread()); + g_bg_dynamics_server->PushSetDebrisKillHeightCall(val); +} + +void BGDynamics::Draw(FrameDef* frame_def) { + assert(InGameThread()); + + BGDynamicsDrawSnapshot* ds{draw_snapshot_.get()}; + if (!ds) { + return; + } + + // Draw sparks. + if (ds->spark_vertices.exists()) { + if (!sparks_mesh_.exists()) sparks_mesh_ = Object::New(); + sparks_mesh_->SetIndexData(ds->spark_indices); + sparks_mesh_->SetData( + Object::Ref>(ds->spark_vertices)); + + // In high-quality we draw in the overlay pass so we don't get wiped + // out by depth-of-field. + bool draw_in_overlay = (frame_def->quality() >= GraphicsQuality::kHigh); + SpriteComponent c(draw_in_overlay ? frame_def->overlay_3d_pass() + : frame_def->beauty_pass()); + c.SetCameraAligned(true); + c.SetColor(2.0f, 2.0f, 2.0f, 1.0f); + c.SetOverlay(draw_in_overlay); + c.SetTexture(g_media->GetTexture(SystemTextureID::kSparks)); + c.DrawMesh(sparks_mesh_.get(), kModelDrawFlagNoReflection); + c.Submit(); + } + + // Draw lights. + if (ds->light_vertices.exists()) { + assert(ds->light_indices.exists()); + assert(!ds->light_indices->elements.empty()); + assert(!ds->light_vertices->elements.empty()); + if (!lights_mesh_.exists()) lights_mesh_ = Object::New(); + lights_mesh_->SetIndexData(ds->light_indices); + lights_mesh_->SetData( + Object::Ref>(ds->light_vertices)); + SpriteComponent c(frame_def->light_shadow_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kLightSoft)); + c.DrawMesh(lights_mesh_.get()); + c.Submit(); + } + + // Draw shadows. + if (ds->shadow_vertices.exists()) { + assert(ds->shadow_indices.exists()); + if (!shadows_mesh_.exists()) shadows_mesh_ = Object::New(); + shadows_mesh_->SetIndexData(ds->shadow_indices); + shadows_mesh_->SetData( + Object::Ref>(ds->shadow_vertices)); + SpriteComponent c(frame_def->light_shadow_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kLight)); + c.DrawMesh(shadows_mesh_.get()); + c.Submit(); + } + + // Draw chunks. + DrawChunks(frame_def, &ds->rocks, BGDynamicsChunkType::kRock); + DrawChunks(frame_def, &ds->ice, BGDynamicsChunkType::kIce); + DrawChunks(frame_def, &ds->slime, BGDynamicsChunkType::kSlime); + DrawChunks(frame_def, &ds->metal, BGDynamicsChunkType::kMetal); + DrawChunks(frame_def, &ds->sparks, BGDynamicsChunkType::kSpark); + DrawChunks(frame_def, &ds->splinters, BGDynamicsChunkType::kSplinter); + DrawChunks(frame_def, &ds->sweats, BGDynamicsChunkType::kSweat); + DrawChunks(frame_def, &ds->flag_stands, BGDynamicsChunkType::kFlagStand); + + // Draw tendrils. + if (ds->tendril_vertices.exists()) { + if (!tendrils_mesh_.exists()) + tendrils_mesh_ = Object::New(); + tendrils_mesh_->SetIndexData(ds->tendril_indices); + tendrils_mesh_->SetData( + Object::Ref>(ds->tendril_vertices)); + bool draw_in_overlay = (frame_def->quality() >= GraphicsQuality::kHigh); + SmokeComponent c(draw_in_overlay ? frame_def->overlay_3d_pass() + : frame_def->beauty_pass()); + c.SetOverlay(draw_in_overlay); + c.SetColor(1.0f, 1.0f, 1.0f, 1.0f); + c.DrawMesh(tendrils_mesh_.get(), kModelDrawFlagNoReflection); + c.Submit(); + + // Shadows. + if (frame_def->quality() >= GraphicsQuality::kHigher) { + for (auto&& i : ds->tendril_shadows) { + if (i.density > 0.0001f) { + Vector3f& p(i.p); + g_graphics->DrawBlotch(p, 2.0f * i.density, 0.02f * i.density, + 0.01f * i.density, 0, 0.15f * i.density); + } + } + } + } + + // Draw fuses. + if (ds->fuse_vertices.exists()) { + // Update our mesh with this data. + if (!fuses_mesh_.exists()) + fuses_mesh_ = Object::New(); + fuses_mesh_->SetIndexData(ds->fuse_indices); + fuses_mesh_->SetData( + Object::Ref>(ds->fuse_vertices)); + { // Draw! + ObjectComponent c(frame_def->beauty_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kFuse)); + c.DrawMesh(fuses_mesh_.get(), kModelDrawFlagNoReflection); + c.Submit(); + } + } +} + +void BGDynamics::DrawChunks(FrameDef* frame_def, + std::vector* draw_snapshot, + BGDynamicsChunkType chunk_type) { + if (!draw_snapshot || draw_snapshot->empty()) { + return; + } + + // Draw ourself into the beauty pass. + ModelData* model; + switch (chunk_type) { + case BGDynamicsChunkType::kFlagStand: + model = g_media->GetModel(SystemModelID::kFlagStand); + break; + case BGDynamicsChunkType::kSplinter: + model = g_media->GetModel(SystemModelID::kShrapnelBoard); + break; + case BGDynamicsChunkType::kSlime: + model = g_media->GetModel(SystemModelID::kShrapnelSlime); + break; + default: + model = g_media->GetModel(SystemModelID::kShrapnel1); + break; + } + ObjectComponent c(frame_def->beauty_pass()); + + // Set up shading. + switch (chunk_type) { + case BGDynamicsChunkType::kRock: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.2f, 0.2f, 0.2f); + c.SetColor(0.6f, 0.6f, 0.5f); + break; + } + case BGDynamicsChunkType::kIce: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kSharp); + c.SetAddColor(0.5f, 0.5f, 0.9f); + break; + } + case BGDynamicsChunkType::kSlime: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kSharper); + c.SetReflectionScale(3.0f, 3.0f, 3.0f); + c.SetColor(0.0f, 0.0f, 0.0f); + c.SetAddColor(0.6f, 0.7f, 0.08f); + break; + } + case BGDynamicsChunkType::kMetal: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kPowerup); + c.SetColor(0.5f, 0.5f, 0.55f); + break; + } + case BGDynamicsChunkType::kSpark: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kSharp); + c.SetColor(0.0f, 0.0f, 0.0f, 1.0f); + c.SetReflectionScale(4.0f, 3.0f, 2.0f); + c.SetAddColor(3.0f, 0.8f, 0.6f); + break; + } + case BGDynamicsChunkType::kSplinter: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kSoft); + c.SetColor(1.0f, 0.8f, 0.5f); + break; + } + case BGDynamicsChunkType::kSweat: { + c.SetTransparent(true); + c.SetPremultiplied(true); + c.SetLightShadow(LightShadowType::kNone); + c.SetTexture(g_media->GetTexture(SystemTextureID::kShrapnel1)); + c.SetReflection(ReflectionType::kSharp); + c.SetReflectionScale(0.5f, 0.4f, 0.3f); + c.SetColor(0.2f, 0.15f, 0.15f, 0.07f); + c.SetAddColor(0.05f, 0.05f, 0.01f); + break; + } + case BGDynamicsChunkType::kFlagStand: { + c.SetTexture(g_media->GetTexture(SystemTextureID::kFlagPole)); + c.SetReflection(ReflectionType::kSharp); + c.SetColor(0.9f, 0.6f, 0.3f, 1.0f); + break; + } + default: + throw Exception(); + } + c.DrawModelInstanced(model, *draw_snapshot, kModelDrawFlagNoReflection); + c.Submit(); +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/bg/bg_dynamics.h b/src/ballistica/dynamics/bg/bg_dynamics.h new file mode 100644 index 00000000..42fe1a15 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics.h @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_H_ + +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +enum class BGDynamicsEmitType { + kChunks, + kStickers, + kTendrils, + kDistortion, + kFlagStand, + kFairyDust +}; + +enum class BGDynamicsTendrilType { kSmoke, kThinSmoke, kIce }; + +enum class BGDynamicsChunkType { + kRock, + kIce, + kSlime, + kMetal, + kSpark, + kSplinter, + kSweat, + kFlagStand +}; + +class BGDynamicsEmission { + public: + BGDynamicsEmitType emit_type = BGDynamicsEmitType::kChunks; + Vector3f position{0.0f, 0.0f, 0.0f}; + Vector3f velocity{0.0f, 0.0f, 0.0f}; + int count{0}; + float scale{1.0f}; + float spread{1.0f}; + BGDynamicsChunkType chunk_type{BGDynamicsChunkType::kRock}; + BGDynamicsTendrilType tendril_type{BGDynamicsTendrilType::kSmoke}; +}; + +// client (game thread) functionality for bg dynamics +class BGDynamics { + public: + static void Init(); + + void Emit(const BGDynamicsEmission& def); + void Step(const Vector3f& cam_pos); + + // can be called to inform the bg dynamics thread to kill off some + // smoke/chunks/etc if rendering is chugging or whatnot. + void TooSlow(); + + // Draws the last snapshot the bg-dynamics-server has delivered to us + void Draw(FrameDef* frame_def); + void SetDebrisFriction(float val); + void SetDebrisKillHeight(float val); + void AddTerrain(CollideModelData* o); + void RemoveTerrain(CollideModelData* o); + + // (sent to us by the bg dynamics server) + void SetDrawSnapshot(BGDynamicsDrawSnapshot* s); + + private: + BGDynamics(); + void DrawChunks(FrameDef* frame_def, std::vector* instances, + BGDynamicsChunkType chunk_type); + Object::Ref lights_mesh_; + Object::Ref shadows_mesh_; + Object::Ref sparks_mesh_; + Object::Ref tendrils_mesh_; + Object::Ref fuses_mesh_; + std::unique_ptr draw_snapshot_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h b/src/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h new file mode 100644 index 00000000..0fceae6c --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h @@ -0,0 +1,79 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ + +#include + +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +// Big chunk of data sent back from the bg-dynamics server thread +// to the game thread for drawing. +class BGDynamicsDrawSnapshot { + public: + struct TendrilShadow { + TendrilShadow(const Vector3f& p_in, float density_in) + : p(p_in), density(density_in) {} + Vector3f p; + float density; + }; + + // These are created in the bg-dynamics thread, and object ownership + // needs to be switched back the the game-thread default when it is passed + // over or else the debug thread-access-checks will error. + void SetGameThreadOwnership() { + if (g_buildconfig.debug_build()) { + for (Object* o : {static_cast(tendril_indices.get()), + static_cast(tendril_vertices.get()), + static_cast(fuse_indices.get()), + static_cast(fuse_vertices.get()), + static_cast(shadow_indices.get()), + static_cast(shadow_vertices.get()), + static_cast(light_indices.get()), + static_cast(light_vertices.get()), + static_cast(spark_indices.get()), + static_cast(spark_vertices.get())}) { + if (o) { + o->SetThreadOwnership(Object::ThreadOwnership::kClassDefault); + } + } + } + } + + // Particles. + std::vector rocks; + std::vector ice; + std::vector slime; + std::vector metal; + std::vector sparks; + std::vector splinters; + std::vector sweats; + std::vector flag_stands; + + // Tendrils. + Object::Ref tendril_indices; + Object::Ref tendril_vertices; + std::vector tendril_shadows; + + // Fuses. + Object::Ref fuse_indices; + Object::Ref fuse_vertices; + + // Shadows. + Object::Ref shadow_indices; + Object::Ref shadow_vertices; + + // Lights. + Object::Ref light_indices; + Object::Ref light_vertices; + + // Sparks. + Object::Ref spark_indices; + Object::Ref spark_vertices; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_fuse.cc b/src/ballistica/dynamics/bg/bg_dynamics_fuse.cc new file mode 100644 index 00000000..98689380 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_fuse.cc @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/bg/bg_dynamics_fuse.h" + +#include "ballistica/dynamics/bg/bg_dynamics_fuse_data.h" + +namespace ballistica { + +BGDynamicsFuse::BGDynamicsFuse() { + assert(g_bg_dynamics_server); + assert(InGameThread()); + + // Allocate our data. We'll pass this to the BGDynamics thread and + // it'll then own it. + data_ = new BGDynamicsFuseData(); + g_bg_dynamics_server->PushAddFuseCall(data_); +} + +BGDynamicsFuse::~BGDynamicsFuse() { + assert(g_bg_dynamics_server); + assert(InGameThread()); + + // Let the data know the client side is dead + // so we're no longer included in step messages. + // (since by the time the worker gets the the data will be gone). + data_->client_dead_ = true; + g_bg_dynamics_server->PushRemoveFuseCall(data_); +} + +void BGDynamicsFuse::SetTransform(const Matrix44f& t) { + assert(InGameThread()); + data_->transform_client_ = t; + data_->have_transform_client_ = true; +} + +void BGDynamicsFuse::SetLength(float length) { + assert(InGameThread()); + data_->length_client_ = length; +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/bg/bg_dynamics_fuse.h b/src/ballistica/dynamics/bg/bg_dynamics_fuse.h new file mode 100644 index 00000000..b0e8c9e6 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_fuse.h @@ -0,0 +1,24 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// Client controlled fuse. +class BGDynamicsFuse { + public: + BGDynamicsFuse(); + ~BGDynamicsFuse(); + void SetTransform(const Matrix44f& m); + void SetLength(float l); + + private: + BGDynamicsFuseData* data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_fuse_data.h b/src/ballistica/dynamics/bg/bg_dynamics_fuse_data.h new file mode 100644 index 00000000..5ffe4122 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_fuse_data.h @@ -0,0 +1,113 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ + +#include + +#include "ballistica/dynamics/bg/bg_dynamics_server.h" + +namespace ballistica { + +const int kFusePointCount = 4; + +struct BGDynamicsFuseData { + void Synchronize() { + transform_worker_ = transform_client_; + have_transform_worker_ = have_transform_client_; + length_worker_ = length_client_; + } + + void Update(BGDynamicsServer* dyn) { + // Do nothing if we havn't received an initial transform. + if (!have_transform_worker_) { + return; + } + seg_len_ = 0.2f * std::max(0.01f, length_worker_); + + if (!initial_position_set_) { + // Snap all our stuff into place on the initial transform. + Vector3f pt = transform_worker_.GetTranslate(); + target_pts_[0] = dyn_pts_[0] = pt; + auto up = Vector3f(&transform_worker_.m[4]); + for (int i = 1; i < kFusePointCount; i++) { + target_pts_[i] = target_pts_[i - 1] + up * seg_len_; + dyn_pts_[i] = target_pts_[i]; + up = (target_pts_[i] - target_pts_[i - 1]).Normalized(); + } + initial_position_set_ = true; + } else { + // ..otherwise dynamically update it. + Vector3f pt = transform_worker_.GetTranslate(); + target_pts_[0] = dyn_pts_[0] = pt; + auto up = Vector3f(&transform_worker_.m[4]); + auto back = Vector3f(&transform_worker_.m[8]); + up = (up + -0.03f * back).Normalized(); + float bAmt = 0.0f; + Vector3f oldTipPos = dyn_pts_[kFusePointCount - 1]; + for (int i = 1; i < kFusePointCount; i++) { + target_pts_[i] = dyn_pts_[i - 1] + up * seg_len_; + float thisFollowAmt = (i == 1 ? 0.5f : 0.2f); + dyn_pts_[i] += thisFollowAmt * (target_pts_[i] - dyn_pts_[i]); + dyn_pts_[i] += Vector3f(0, -0.014f * 0.2f * length_worker_, 0); + up = (dyn_pts_[i] - dyn_pts_[i - 1] - bAmt * back).Normalized(); + dyn_pts_[i] = dyn_pts_[i - 1] + up * seg_len_; + bAmt += 0.01f * length_worker_; + } + + // Spit out a spark. + float r, g, b, a; + if (length_worker_ > 0.66f) { + r = 1.6f; + g = 1.5f; + b = 0.4f; + a = 0.5f; + } else if (length_worker_ > 0.33f) { + r = 2.0f; + g = 0.7f; + b = 0.3f; + a = 0.2f; + } else { + r = 3.0f; + g = 0.5f; + b = 0.4f; + a = 0.3f; + } + int count = 2; + if (dyn->graphics_quality() <= GraphicsQuality::kLow) { + count = 1; + } + + for (int i = 0; i < count; i++) { + float rand_f = RandomFloat(); + float d_life = -0.08f; + float d_size = 0.000f + 0.04f * rand_f * rand_f; + + dyn->spark_particles()->Emit(dyn_pts_[kFusePointCount - 1], + dyn_pts_[kFusePointCount - 1] - oldTipPos, + r, g, b, a, d_life, 0.02f, d_size, + 0.8f); // Flicker. + } + } + } + + bool client_dead_{}; + float seg_len_{}; + Vector3f target_pts_[kFusePointCount]{}; + Vector3f dyn_pts_[kFusePointCount]{}; + float length_client_{1.0f}; + float length_worker_{1.0f}; + + // Values owned by the client. + Matrix44f transform_client_{kMatrix44fIdentity}; + + // Values owned by the worker thread. + Matrix44f transform_worker_{kMatrix44fIdentity}; + bool have_transform_client_{}; + bool have_transform_worker_{}; + bool initial_position_set_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_height_cache.cc b/src/ballistica/dynamics/bg/bg_dynamics_height_cache.cc new file mode 100644 index 00000000..36b7e38d --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_height_cache.cc @@ -0,0 +1,173 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/bg/bg_dynamics_height_cache.h" + +#include + +namespace ballistica { + +const int kBGDynamicsHeightCacheMaxContacts = 20; + +BGDynamicsHeightCache::BGDynamicsHeightCache() + : dirty_(true), + shadow_ray_(nullptr), + x_min_(-1.0f), + x_max_(1.0f), + y_min_(-1.0f), + y_max_(1.0f), + z_min_(-1.0f), + z_max_(1.0f) { + grid_width_ = 1; + grid_height_ = 1; +} + +BGDynamicsHeightCache::~BGDynamicsHeightCache() { + if (shadow_ray_) { + dGeomDestroy(shadow_ray_); + } +} + +auto BGDynamicsHeightCache::SampleCell(int x, int z) -> float { + int index = z * grid_width_ + x; + assert(index >= 0 && index < static_cast(heights_.size()) + && index < static_cast(heights_valid_.size())); + if (heights_valid_[index]) { + return heights_[index]; + } else { + Vector3f p( + x_min_ + + ((static_cast(x) + 0.5f) / static_cast(grid_width_)) + * (x_max_ - x_min_), + y_max_, + z_min_ + + ((static_cast(z) + 0.5f) + / static_cast(grid_height_)) + * (z_max_ - z_min_)); + assert(shadow_ray_); + dGeomSetPosition(shadow_ray_, p.x, p.y, p.z); + float shadow_dist = y_max_ - y_min_; + for (auto& geom : geoms_) { + dContact contact[1]; + if (dCollide(shadow_ray_, geom, kBGDynamicsHeightCacheMaxContacts, + &contact[0].geom, sizeof(dContact))) { + float len = p.y - contact[0].geom.pos[1]; + if (len < shadow_dist) { + shadow_dist = len; + } + } + } + float height = y_max_ - shadow_dist; + heights_[index] = height; + heights_valid_[index] = 1; + return height; + } +} + +auto BGDynamicsHeightCache::Sample(const Vector3f& pos) -> float { + if (dirty_) { + Update(); + } + + // Get sample point in grid coords. + float x = + static_cast(grid_width_) * ((pos.x - x_min_) / (x_max_ - x_min_)) + - 0.5f; + float z = + static_cast(grid_height_) * ((pos.z - z_min_) / (z_max_ - z_min_)) + - 0.5f; + + // Sample the 4 contributing cells. + int x_min = static_cast(floor(x)); + x_min = std::max(0, std::min(grid_width_ - 1, x_min)); + int x_max = static_cast(ceil(x)); + x_max = std::max(0, std::min(grid_width_ - 1, x_max)); + float x_blend = fmod(x, 1.0f); + int z_min = static_cast(floor(z)); + z_min = std::max(0, std::min(grid_height_ - 1, z_min)); + int z_max = static_cast(ceil(z)); + z_max = std::max(0, std::min(grid_height_ - 1, z_max)); + float zBlend = fmod(z, 1.0f); + + float xz = SampleCell(x_min, z_min); + float xZ = SampleCell(x_min, z_max); + float Xz = SampleCell(x_max, z_min); + float XZ = SampleCell(x_max, z_max); + + // Weighted blend per row. + float zFin = xz * (1.0f - x_blend) + Xz * x_blend; + float ZFin = xZ * (1.0f - x_blend) + XZ * x_blend; + + // Weighted blend of the two rows. + return zFin * (1.0f - zBlend) + ZFin * zBlend; +} + +void BGDynamicsHeightCache::SetGeoms(const std::vector& geoms) { + dirty_ = true; + geoms_ = geoms; +} + +void BGDynamicsHeightCache::Update() { + // Calc our full dimensions. + if (geoms_.empty()) { + x_min_ = -1.0f; + x_max_ = 1.0f; + y_min_ = -1.0f; + y_max_ = 1.0f; + z_min_ = -1.0f; + z_max_ = 1.0f; + } else { + auto i = geoms_.begin(); + dReal aabb[6]; + dGeomGetAABB(*i, aabb); + float x = aabb[0]; + float X = aabb[1]; + float y = aabb[2]; + float Y = aabb[3]; + float z = aabb[4]; + float Z = aabb[5]; + for (i++; i != geoms_.end(); i++) { + dGeomGetAABB(*i, aabb); + if (aabb[0] < x) x = aabb[0]; + if (aabb[1] > X) X = aabb[1]; + if (aabb[2] < y) y = aabb[2]; + if (aabb[3] > Y) Y = aabb[3]; + if (aabb[4] < z) z = aabb[4]; + if (aabb[5] > Z) Z = aabb[5]; + } + float buffer = 0.3f; + x_min_ = x - buffer; + x_max_ = X + buffer; + y_min_ = y - buffer; + y_max_ = Y + buffer; + z_min_ = z - buffer; + z_max_ = Z + buffer; + } + + // (Re)create our shadow ray with the new dimensions. + if (shadow_ray_) { + dGeomDestroy(shadow_ray_); + } + shadow_ray_ = dCreateRay(nullptr, y_max_ - y_min_); + dGeomRaySet(shadow_ray_, 0, 0, 0, 0, -1, 0); // Aim straight down. + dGeomRaySetClosestHit(shadow_ray_, true); + + // Update/clear our cell grid based on our dimensions. + grid_width_ = + std::max(1, std::min(256, static_cast((x_max_ - x_min_) * 8))); + grid_height_ = + std::max(1, std::min(256, static_cast((z_max_ - z_min_) * 8))); + + assert(grid_width_ >= 0 && grid_height_ >= 0); + auto cell_count_u = static_cast(grid_width_ * grid_height_); + if (cell_count_u != heights_.size()) { + heights_.clear(); + heights_.resize(cell_count_u); + heights_valid_.clear(); + heights_valid_.resize(cell_count_u); + } + memset(&heights_valid_[0], 0, cell_count_u); + + dirty_ = false; +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/bg/bg_dynamics_height_cache.h b/src/ballistica/dynamics/bg/bg_dynamics_height_cache.h new file mode 100644 index 00000000..71502ff5 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_height_cache.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ + +#include + +#include "ballistica/math/vector3f.h" +#include "ode/ode.h" + +namespace ballistica { + +// given geoms, creates/samples a height map on the fly +// for fast but not-perfectly-accurate height values +class BGDynamicsHeightCache { + public: + BGDynamicsHeightCache(); + ~BGDynamicsHeightCache(); + auto Sample(const Vector3f& pos) -> float; + void SetGeoms(const std::vector& geoms); + + private: + auto SampleCell(int x, int y) -> float; + void Update(); + std::vector geoms_; + std::vector heights_; + std::vector heights_valid_; + bool dirty_; + dGeomID shadow_ray_; + int grid_width_; + int grid_height_; + float x_min_; + float x_max_; + float y_min_; + float y_max_; + float z_min_; + float z_max_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_server.cc b/src/ballistica/dynamics/bg/bg_dynamics_server.cc new file mode 100644 index 00000000..a145c24e --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_server.cc @@ -0,0 +1,2657 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/bg/bg_dynamics_server.h" + +#include +#include +#include +#include + +#include "ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h" +#include "ballistica/dynamics/bg/bg_dynamics_fuse_data.h" +#include "ballistica/dynamics/bg/bg_dynamics_height_cache.h" +#include "ballistica/dynamics/bg/bg_dynamics_shadow_data.h" +#include "ballistica/dynamics/bg/bg_dynamics_volume_light_data.h" +#include "ballistica/dynamics/collision_cache.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +// Some triangle-on-box cases generate tons of contacts; lets try limiting it +// this way.. If that doesn't work we'll crank this up and add collision +// simplification. +const int kMaxBGDynamicsContacts = 20; + +// How far from the shadow will be max size and min density. +const float kMaxShadowGrowDist = 3.0f; + +// How far behind something a shadow caster has to be to go transparent. +const float kShadowOccludeDistance = 0.5f; + +// How big the shadow gets at its max dist. +const float kMaxShadowScale = 3.0f; + +const float kSmokeBaseGlow = 0.0f; +const float kSmokeGlow = 400.0f; + +// FIXME: Should get rid of this stuff. +#if BA_DEBUG_BUILD +struct DebugLine { + DebugLine(const Vector3f& p1_in, const Vector3f& p2_in, + const Vector3f& color_in) + : p1(p1_in), p2(p2_in), color(color_in) {} + Vector3f p1; + Vector3f p2; + Vector3f color; +}; + +// Eww these aren't thread-safe but they're just for debugging so whatever. +std::vector g_debug_lines; +std::vector g_debug_points; +#endif // BA_DEBUG_BUILD + +// FIXME: Move to a nice math-y place. +inline auto Reflect(const Vector3f& v, const Vector3f& normal) -> Vector3f { + Vector3f n_projected = normal * (v.Dot(normal.Normalized())); + return -(n_projected - (v - n_projected)); +} + +static void CalcERPCFM(dReal stiffness, dReal damping, dReal* erp, dReal* cfm) { + if (stiffness <= 0.0f && damping <= 0.0f) { + *erp = 0.0f; + // cfm = dInfinity; // doesn't seem to be happy... + *cfm = 9999999999.0f; + } else { + *erp = (kGameStepSeconds * stiffness) + / ((kGameStepSeconds * stiffness) + damping); + *cfm = 1.0f / ((kGameStepSeconds * stiffness) + damping); + } +} + +class BGDynamicsServer::Terrain { + public: + Terrain(BGDynamicsServer* t, Object::Ref* collide_model_in) + : collide_model_(collide_model_in) { + assert((**collide_model_).loaded()); + geom_ = dCreateTriMesh(nullptr, (**collide_model_).GetBGMeshData(), nullptr, + nullptr, nullptr); + } + + auto GetCollideModel() const -> CollideModelData* { + return collide_model_->get(); + } + + ~Terrain() { + dGeomDestroy(geom_); + // We were passed an allocated pointer to a CollideModelData strong-ref + // object to keep it alive while we're using it. We need to pass that + // back to the main thread to get freed. + if (collide_model_) { + Object::Ref* ref = collide_model_; + g_game->PushCall([ref] { + (**ref).set_last_used_time(GetRealTime()); + delete ref; + }); + collide_model_ = nullptr; + } + } + + auto geom() const -> dGeomID { return geom_; } + + private: + Object::Ref* collide_model_; + dGeomID geom_; +}; + +class BGDynamicsServer::Field { + public: + Field(BGDynamicsServer* t, const Vector3f& pos, float mag) : pos_(pos) { + rad_ = 5; + mag_ = mag; + birth_time_ = t->time(); + lifespan_ = 500; + } + ~Field() = default; + + auto rad() const -> dReal { return rad_; } + auto pos() const -> Vector3f { return pos_; } + auto amt() const -> dReal { return amt_; } + void set_amt(dReal val) { amt_ = val; } + auto birth_time() const -> uint32_t { return birth_time_; } + auto lifespan() const -> dReal { return lifespan_; } + auto mag() const -> dReal { return mag_; } + + private: + Vector3f pos_; + dReal rad_; + dReal mag_; + uint32_t birth_time_; + dReal lifespan_; + dReal amt_{}; +}; + +class BGDynamicsServer::Tendril { + public: + struct Point { + Vector3f p{0.0f, 0.0f, 0.0f}; + Vector3f v{0.0f, 0.0f, 0.0f}; + Vector3f p_distorted{0.0f, 0.0f, 0.0f}; + float tex_coords[2]{}; + float erode{}; + float erode_rate{}; + float bouyancy{}; + float brightness{}; + float fade{}; + float fade_rate{}; + float age{}; + float glow_r{}; + float glow_g{}; + float glow_b{}; + void Update(const Tendril& t) { + p += v * kGameStepSeconds; + age += kGameStepMilliseconds; + v *= 0.992f; + v.y -= 0.003f * bouyancy; // Bouyancy. + v.x += 0.005f * t.wind_amt_; // Slight side drift. + erode *= (1.0f - 0.06f * erode_rate); + if (age > 750 * fade_rate) fade *= 1.0f - 0.0085f * fade_rate; + } + void UpdateDistortion(const BGDynamicsServer& d) { + p_distorted = p; + for (auto&& fi : d.fields_) { + const Field& f(*fi); + float fRad = f.rad(); + float fRadSquared = fRad * fRad; + Vector3f diff = p_distorted - f.pos(); + float dist_squared = diff.LengthSquared(); + if (dist_squared <= fRadSquared) { + float dist = sqrtf(dist_squared); + + // Shift our point towards or away from the field by its calced mag. + float mag = f.amt(); + + // Points closer than MAG to the field are scaled by their + // ratio of dist to mag. + if (dist < -mag) mag *= (dist / -mag); + float falloff = + (1.0f - (dist / fRad)); // falloff with dist from field + mag *= falloff; + Vector3f diff_norm = diff.Normalized(); + p_distorted += diff_norm * mag; + + // Also apply a very slight amount of actual outward force to ourself + // (only if we're kinda old though - otherwise it screws with our + // initial shape too much). + if (age > 400) { + v += Vector3f(diff_norm.x * 0.03f, diff_norm.y * 0.01f, + diff_norm.z * 0.03f) + * falloff; + } + } + } + } + + void UpdateGlow(const BGDynamicsServer& d, float glow_scale) { + glow_r = glow_g = glow_b = 0.0f; + for (auto&& li : d.volume_lights_) { + BGDynamicsVolumeLightData& l(*li); + Vector3f& pLight(l.pos_worker); + float light_rad = l.radius_worker * 9.0f; // Lets grow it a bit. + float light_rad_squared = light_rad * light_rad; + float dist_squared = (pLight - p).LengthSquared(); + if (dist_squared <= light_rad_squared) { + float dist = sqrtf(dist_squared); + float val = (1.0f - dist / light_rad); + val = val * val; + glow_r += val * l.r_worker; + glow_g += val * l.g_worker; + glow_b += val * l.b_worker; + } + } + glow_r *= glow_scale; + glow_g *= glow_scale; + glow_b *= glow_scale; + } + }; + + struct Slice { + Point p1; + Point p2; + float emit_rate{}; // What the emit rate was at this slice. + float start_erode{}; // What the start-erode value was at this slice. + float start_spread{}; // What the start-erode value was at this slice. + auto GetCenter() const -> Vector3f { return (p1.p * 0.5f) + (p2.p * 0.5f); } + auto isFullyTransparent() const -> bool { + return (p1.fade < 0.01f && p2.fade < 0.01f); + } + }; + + explicit Tendril(BGDynamicsServer* t) + : has_updated_(false), controller_(nullptr), emitting_(true) { + emit_rate_ = 0.8f + 0.4f * RandomFloat(); + birth_time_ = t->time(); + radius_ = 0.1f + RandomFloat() * 0.1f; + tex_coord_ = RandomFloat(); + start_erode_ = 0.1f; + start_spread_ = 4.0f; + side_spread_rate_ = 1.0f; + point_rand_scale_ = 1.0f; + slice_rand_scale_ = 1.0f; + tex_change_rate_ = 1.0f; + emit_rate_falloff_rate_ = 1.0f; + start_brightness_max_ = 0.9f; + start_brightness_min_ = 0.3f; + brightness_rand_ = 0.5f; + start_fade_scale_ = 1.0f; + glow_scale_ = 1.0f; + } + void SetController(TendrilController* tc) { + assert((controller_ == nullptr) ^ (tc == nullptr)); + controller_ = tc; + } + void UpdateSlices(BGDynamicsServer* t) { + for (auto&& i : slices_) { + i.p1.Update(*this); + i.p2.Update(*this); + + // Push them together slightly if they're getting too far apart. + Vector3f diff = i.p1.p - i.p2.p; + if (diff.LengthSquared() > 2.5f) { + i.p1.v += diff * -0.1f; + i.p2.v += diff * 0.1f; + } + } + + // No shadows for thin tendrils. + if (type_ == BGDynamicsTendrilType::kThinSmoke) { + shadow_density_ = 0.0f; + } else { + float blend = 0.995f; + + auto i = slices_.begin(); + if (i == slices_.end()) { + shadow_density_ = 0.0f; + } + int count = 0; + while (i != slices_.end()) { + shadow_position_ = + blend * shadow_position_ + (1.0f - blend) * i->GetCenter(); + shadow_density_ = blend * shadow_density_ + + (1.0f - blend) * (i->p1.fade + i->p2.fade) * 0.5f; + count++; + if (count > 4) break; // only use first few.. + i++; + } + } + } + + // Clear out old fully transparent slices. + void PruneSlices() { + // Clip transparent ones off the front. + while (true) { + auto i = slices_.begin(); + if (i == slices_.end()) break; + auto i_next = i; + i_next++; + if (i_next != slices_.end() && i->isFullyTransparent() + && i_next->isFullyTransparent()) { + slices_.pop_front(); + } else { + break; + } + } + + // ...and back. + while (true) { + auto i = slices_.rbegin(); + if (i == slices_.rend()) break; + auto i_next = i; + i_next++; + if (i_next != slices_.rend() && i->isFullyTransparent() + && i_next->isFullyTransparent()) { + slices_.pop_back(); + } else { + break; + } + } + } + + ~Tendril(); + + auto type() const -> BGDynamicsTendrilType { return type_; } + + private: + TendrilController* controller_; + Vector3f shadow_position_{0.0f, 0.0f, 0.0f}; + bool shading_flip_{}; + float wind_amt_{}; + float shadow_density_{}; + float emit_rate_{}; + float start_erode_{}; + float start_spread_{}; + float side_spread_rate_{}; + float point_rand_scale_{}; + float slice_rand_scale_{}; + float tex_change_rate_{}; + float emit_rate_falloff_rate_{}; + float start_brightness_max_{}; + float start_brightness_min_{}; + float brightness_rand_{}; + float start_fade_scale_{}; + float glow_scale_{}; + bool emitting_{}; + bool has_updated_{}; + std::list slices_{}; + Slice cur_slice_{}; + Vector3f position_{0.0f, 0.0f, 0.0f}; + Vector3f prev_pos_{0.0f, 0.0f, 0.0f}; + Vector3f velocity_{0.0f, 0.0f, 0.0f}; + Vector3f medium_velocity_{0.0f, 0.0f, 0.0f}; + uint32_t birth_time_{}; + float tex_coord_{}; + float radius_{}; + BGDynamicsTendrilType type_{}; + friend class BGDynamicsServer; +}; + +class BGDynamicsServer::TendrilController { + public: + explicit TendrilController(Tendril* t) { + tendril_ = t; + tendril_->SetController(this); + } + ~TendrilController() { + // If we have a tendril, tell it we're dying and that its done emitting. + if (tendril_) { + tendril_->SetController(nullptr); + tendril_->emit_rate_ = 0.0f; + } + } + void update(const Vector3f& pos, const Vector3f& vel) { + if (tendril_) { + tendril_->prev_pos_ = tendril_->position_; + tendril_->position_ = pos; + tendril_->velocity_ = vel; + } + } + + private: + Tendril* tendril_; + friend class Tendril; +}; + +class BGDynamicsServer::Chunk { + public: + Chunk(BGDynamicsServer* t, const BGDynamicsEmission& event, bool dynamic, + bool can_die = true, const Vector3f& d_bias = kVector3f0) + : shadow_dist_(9999), + type_(event.chunk_type), + dynamic_(dynamic), + can_die_(can_die), + tendril_controller_(nullptr) { + birth_time_ = t->time(); + flicker_ = 1.0f; + flicker_scale_ = RandomFloat(); + flicker_scale_ = 1.0f - (flicker_scale_ * flicker_scale_); + if (type_ != BGDynamicsChunkType::kFlagStand) { + if (type_ == BGDynamicsChunkType::kSplinter) { + size_[0] = event.scale * 0.15f * (0.4f + 0.6f * RandomFloat()); + size_[1] = event.scale * 0.15f * (0.4f + 0.6f * RandomFloat()); + size_[2] = event.scale * 0.15f * (0.4f + 0.6f * RandomFloat()) * 5.0f; + } else { + size_[0] = event.scale * 0.15f * (0.3f + 0.7f * RandomFloat()); + size_[1] = event.scale * 0.15f * (0.3f + 0.7f * RandomFloat()); + size_[2] = event.scale * 0.15f * (0.3f + 0.7f * RandomFloat()); + } + } else { + size_[0] = size_[1] = size_[2] = 1.0f; + } + + lifespan_ = 10000; + if (type_ == BGDynamicsChunkType::kSpark) { + lifespan_ = 500 + RandomFloat() * 1500; + if (RandomFloat() < 0.1f) lifespan_ *= 3.0f; + } else if (type_ == BGDynamicsChunkType::kSweat) { + lifespan_ = 200 + RandomFloat() * 400; + if (RandomFloat() < 0.1f) lifespan_ *= 2.0f; + } else if (type_ == BGDynamicsChunkType::kFlagStand) { + lifespan_ = 99999999.0f; + } + + if (dynamic_) { + body_ = dBodyCreate(t->ode_world_); + geom_ = dCreateBox(nullptr, size_[0], size_[1], size_[2]); + dGeomSetBody(geom_, body_); + dMass m; + dMassSetBox(&m, 1.0f, size_[0], size_[1], size_[2]); + + dBodySetMass(body_, &m); + + Vector3f v = event.velocity; + float spread = event.spread; + Vector3f v_rand = (Utils::Sphrand() + d_bias).Normalized() * RandomFloat() + * 40.0f * spread; + + dBodySetPosition(body_, event.position.x, event.position.y, + event.position.z); + dBodySetLinearVel(body_, v.x + v_rand.x, v.y + v_rand.y, v.z + v_rand.z); + dBodySetAngularVel(body_, (RandomFloat() - 0.5f) * 5.0f, + (RandomFloat() - 0.5f) * 5.0f, + (RandomFloat() - 0.5f) * 5.0f); + } else { + Vector3f axis{}; + if (type_ == BGDynamicsChunkType::kFlagStand) { + axis = Vector3f(0, 1, 0); + } else { + axis = Utils::Sphrand(); + } + Matrix44f m = Matrix44fScale(Vector3f(size_[0], size_[1], size_[2])) + * Matrix44fRotate(axis, RandomFloat() * 360.0f) + * Matrix44fTranslate(event.position); + for (int i = 0; i < 16; i++) { + static_transform_[i] = m.m[i]; + } + + // Assume we're sitting on the ground. + shadow_dist_ = 0.0f; + } + } + auto body() const -> dBodyID { return body_; } + + auto geom() const -> dGeomID { return geom_; } + + auto type() const -> BGDynamicsChunkType { return type_; } + + ~Chunk() { + delete tendril_controller_; + if (dynamic_) { + dBodyDestroy(body_); + dGeomDestroy(geom_); + } + } + + void UpdateTendril() { + if (tendril_controller_) { + tendril_controller_->update(Vector3f(dBodyGetPosition(body_)), + Vector3f(dBodyGetLinearVel(body_))); + } + } + auto can_die() const -> bool { return can_die_; } + auto dynamic() const -> bool { return dynamic_; } + auto size() const -> const float* { return size_; } + auto static_transform() const -> const float* { return static_transform_; } + + private: + TendrilController* tendril_controller_; + bool dynamic_; + bool can_die_; + dReal lifespan_; + dReal flicker_; + dReal flicker_scale_; + float static_transform_[16]{}; + BGDynamicsChunkType type_{}; + uint32_t birth_time_{}; + dBodyID body_{}; + dGeomID geom_{}; + float size_[3]{}; + float shadow_dist_; + friend class BGDynamicsServer; +}; // Chunk + +// Contains 2 ping-ponging particle buffers. +void BGDynamicsServer::ParticleSet::Emit(const Vector3f& pos, + const Vector3f& vel, float r, float g, + float b, float a, float dlife, + float size, float d_size, + float flicker) { + particles[current_set].resize(particles[current_set].size() + 1); + Particle& p(particles[current_set].back()); + p.x = pos.x; + p.y = pos.y; + p.z = pos.z; + p.vx = vel.x * 1.0f + 0.02f * (RandomFloat() - 0.5f); + p.vy = vel.y * 1.0f + 0.02f * (RandomFloat() - 0.5f); + p.vz = vel.z * 1.0f + 0.02f * (RandomFloat() - 0.5f); + p.r = r; + p.g = g; + p.b = b; + p.a = a; + p.life = 1.0f; + assert(dlife < 0.0f); + p.d_life = dlife; + p.size = size; + p.flicker = 1.0f; + p.flicker_scale = flicker; + p.d_size = d_size; +} + +void BGDynamicsServer::ParticleSet::UpdateAndCreateSnapshot( + Object::Ref* index_buffer, + Object::Ref* buffer) { + auto p_count = static_cast(particles[current_set].size()); + + // Quick-out: return empty. + if (p_count == 0) { + return; + } + + Particle* p_src = &particles[current_set][0]; + + // Resize target to fit if all particles stay alive. + particles[!current_set].resize(particles[current_set].size()); + Particle* p_dst = &particles[!current_set][0]; + + auto* ibuf = Object::NewDeferred(p_count * 6); + + // Game thread is default owner; needs to be us until we hand it over. + ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + *index_buffer = Object::MakeRefCounted(ibuf); + auto* vbuf = Object::NewDeferred(p_count * 4); + + // Game thread is default owner; needs to be us until we hand it over. + vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + *buffer = Object::MakeRefCounted(vbuf); + + uint16_t* i_render = &(*index_buffer)->elements[0]; + VertexSprite* p_render = &(*buffer)->elements[0]; + uint32_t p_index = 0; + uint32_t p_count_remaining = 0; + uint32_t p_count_rendered = 0; + for (uint32_t i = 0; i < p_count; i++) { + float life = p_src->life + p_src->d_life; + + // Our opacity drops rapidly at the end. + float o = 1.0f - life; + o = 1.0f - (o * o * o); + float size = std::max(0.0f, p_src->size + p_src->d_size); + + // Kill the particle if life or size falls to 0. + if (life > 0.0f && size > 0) { + p_count_remaining++; + p_dst->life = life; + p_dst->size = size; + p_dst->x = p_src->x + p_src->vx; + p_dst->y = p_src->y + p_src->vy; + p_dst->z = p_src->z + p_src->vz; + p_dst->r = p_src->r; + p_dst->g = p_src->g; + p_dst->b = p_src->b; + p_dst->a = p_src->a; + p_dst->vx = p_src->vx; + p_dst->vy = p_src->vy - 0.00001f; + p_dst->vz = p_src->vz; + p_dst->d_life = p_src->d_life; + p_dst->d_size = p_src->d_size; + p_dst->flicker_scale = p_src->flicker_scale; + + // Every so often update our flicker value if we're flickering. + if (p_src->flicker_scale != 0.0f) { + if (RandomFloat() < 0.2f) { + p_dst->flicker = std::max( + 0.0f, 1.0f + (RandomFloat() - 0.5f) * p_src->flicker_scale); + } else { + p_dst->flicker = p_src->flicker; + } + } else { + p_dst->flicker = 1.0f; + } + + // Render this point if its got a positive size. + if (p_dst->flicker > 0.0f && p_dst->size > 0.0f) { + p_count_rendered++; + + // Add our 6 indices. + { + i_render[0] = static_cast(p_index); + i_render[1] = static_cast(p_index + 1); + i_render[2] = static_cast(p_index + 2); + i_render[3] = static_cast(p_index + 1); + i_render[4] = static_cast(p_index + 3); + i_render[5] = static_cast(p_index + 2); + } + + p_render[0].uv[0] = 0; + p_render[0].uv[1] = 0; + p_render[1].uv[0] = 0; + p_render[1].uv[1] = 65535; + p_render[2].uv[0] = 65535; + p_render[2].uv[1] = 0; + p_render[3].uv[0] = 65535; + p_render[3].uv[1] = 65535; + + p_render[0].position[0] = p_render[1].position[0] = + p_render[2].position[0] = p_render[3].position[0] = p_dst->x; + p_render[0].position[1] = p_render[1].position[1] = + p_render[2].position[1] = p_render[3].position[1] = p_dst->y; + p_render[0].position[2] = p_render[1].position[2] = + p_render[2].position[2] = p_render[3].position[2] = p_dst->z; + p_render[0].size = p_render[1].size = p_render[2].size = + p_render[3].size = p_dst->size * p_dst->flicker; + p_render[0].color[0] = p_render[1].color[0] = p_render[2].color[0] = + p_render[3].color[0] = p_dst->r * o; + p_render[0].color[1] = p_render[1].color[1] = p_render[2].color[1] = + p_render[3].color[1] = p_dst->g * o; + p_render[0].color[2] = p_render[1].color[2] = p_render[2].color[2] = + p_render[3].color[2] = p_dst->b * o; + p_render[0].color[3] = p_render[1].color[3] = p_render[2].color[3] = + p_render[3].color[3] = p_dst->a * o; + + i_render += 6; + p_render += 4; + p_index += 4; + } + p_dst++; + } + p_src++; + } + + // Clamp dst and render sets to account for deaths. + if (p_count != p_count_remaining) { + particles[!current_set].resize(p_count_remaining); + } + + if (p_count != p_count_rendered) { + // If we dropped all the way to zero, return empty. + // Otherwise return a downsized buffer. + if (p_count_rendered == 0) { + *index_buffer = Object::Ref(); + *buffer = Object::Ref(); + } else { + (*index_buffer)->elements.resize(p_count_rendered * 6); + (*buffer)->elements.resize(p_count_rendered * 4); + } + } + current_set = !current_set; +} + +BGDynamicsServer::BGDynamicsServer(Thread* thread) + : Module("bgDynamics", thread), + height_cache_(new BGDynamicsHeightCache()), + collision_cache_(new CollisionCache) { + BA_PRECONDITION(g_bg_dynamics_server == nullptr); + g_bg_dynamics_server = this; + + ode_world_ = dWorldCreate(); + assert(ode_world_); + dWorldSetGravity(ode_world_, 0.0f, -20.0f, 0.0f); + dWorldSetContactSurfaceLayer(ode_world_, 0.001f); + dWorldSetAutoDisableFlag(ode_world_, true); + dWorldSetAutoDisableSteps(ode_world_, 5); + dWorldSetAutoDisableLinearThreshold(ode_world_, 0.6f); + dWorldSetAutoDisableAngularThreshold(ode_world_, 0.6f); + dWorldSetAutoDisableSteps(ode_world_, 10); + dWorldSetAutoDisableTime(ode_world_, 0); + dWorldSetQuickStepNumIterations(ode_world_, 3); + ode_contact_group_ = dJointGroupCreate(0); + assert(ode_contact_group_); +} + +BGDynamicsServer::~BGDynamicsServer() = default; + +BGDynamicsServer::Tendril::~Tendril() { + // If we have a controller, tell them not to call us anymore. + if (controller_) { + controller_->tendril_ = nullptr; + } +} + +void BGDynamicsServer::UpdateFuses() { + for (auto&& i : fuses_) { + i->Update(this); + } +} + +void BGDynamicsServer::UpdateTendrils() { + int render_slice_count = 0; + + for (auto i = tendrils_.begin(); i != tendrils_.end();) { + Tendril& t(**i); + + // Kill off fully-dead tendrils. + if (!t.emitting_ && t.slices_.size() < 2) { + auto i_next = i; + i_next++; + if (t.type_ == BGDynamicsTendrilType::kThinSmoke) { + tendril_count_thin_--; + } else { + tendril_count_thick_--; + } + assert(tendril_count_thin_ >= 0 && tendril_count_thick_ >= 0); + delete *i; + tendrils_.erase(i); + i = i_next; + continue; + } + + // Clip transparent bits off the ends. + t.PruneSlices(); + + // Step existing tendril points. + t.UpdateSlices(this); + + // Update the tendrils physics if it is not being controlled. + if (t.controller_ == nullptr) { + t.prev_pos_ = t.position_; + t.velocity_ += Vector3f(0, -0.1f, 0); // Gravity. + t.position_ += t.velocity_ * kGameStepSeconds; + } + + // If we're still emitting, potentially drop in some new slices. + if (t.emitting_) { + // Step from our last slice to our current position, + // dropping in new slices as we go. + Vector3f p = {0.0f, 0.0f, 0.0f}; + float tex_coord{}; + float emit_rate{}; + float start_erode{}; + float start_spread{}; + int slice_count = static_cast(t.slices_.size()); + if (slice_count > 0) { + p = t.slices_.back().GetCenter(); + tex_coord = t.slices_.back().p1.tex_coords[1]; + emit_rate = t.slices_.back().emit_rate; + start_erode = t.slices_.back().start_erode; + start_spread = t.slices_.back().start_spread; + } else { + p = t.prev_pos_; + tex_coord = t.tex_coord_; + emit_rate = t.emit_rate_; + start_erode = t.start_erode_; + start_spread = t.start_spread_; + } + Vector3f march_dir = t.position_ - p; + float dist = march_dir.Length(); + + // We flip our shading depending on which way the tendril is pointing + // so that the light side is generally up. + float start_brightness{}; + float start_brightness_2{}; + if (t.shading_flip_) { + start_brightness = t.start_brightness_max_; + start_brightness_2 = t.start_brightness_min_; + } else { + start_brightness = t.start_brightness_min_; + start_brightness_2 = t.start_brightness_max_; + } + + float start_brightness_rand = t.brightness_rand_; + float erode_rate_randomness = 0.5f; + float fade_rate_randomness = 2.0f; + + if (dist > 0.001f) { + float span = 0.5f; + march_dir = march_dir.Normalized() * span; + Vector3f from_cam = cam_pos_ - p; + Vector3f side_vec = Vector3f::Cross(march_dir, from_cam).Normalized(); + + float inherit_velocity = 0.015f; + + // If this is our first step, drop down a span immediately. + if (!t.has_updated_) { + Vector3f r_uniform = Utils::Sphrand(0.2f * t.slice_rand_scale_); + float density = emit_rate > 0.1f ? 1.0f : emit_rate / 0.1f; + + t.slices_.emplace_back(); + Tendril::Slice& slice(t.slices_.back()); + slice.emit_rate = emit_rate; + slice.start_erode = start_erode; + slice.start_spread = start_spread; + slice.p1.p = p - t.radius_ * side_vec * start_spread; + slice.p1.v = t.medium_velocity_ * 0.3f + + t.velocity_ * inherit_velocity * 0.1f + - side_vec * t.radius_ * t.side_spread_rate_ + r_uniform + + Utils::Sphrand(0.13f * t.point_rand_scale_); + slice.p1.tex_coords[0] = 0.0f; + slice.p1.tex_coords[1] = tex_coord; + slice.p1.erode = t.start_erode_; + slice.p1.erode_rate = std::max( + 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); + slice.p1.age = 0.0f; + slice.p1.bouyancy = 0.3f + 0.2f * RandomFloat(); + slice.p1.brightness = std::max( + 0.0f, start_brightness + + (RandomFloat() - 0.5f) * start_brightness_rand); + slice.p1.fade = 0.0f; + slice.p1.glow_r = slice.p1.glow_g = slice.p1.glow_b = 0.0f; + slice.p1.fade_rate = 1.0f + fade_rate_randomness * (RandomFloat()); + + slice.p2.p = p + t.radius_ * side_vec * start_spread; + slice.p2.v = t.medium_velocity_ * 0.3f + + t.velocity_ * inherit_velocity * 0.1f + + side_vec * t.radius_ * t.side_spread_rate_ + r_uniform + + Utils::Sphrand(0.13f * t.point_rand_scale_); + slice.p2.tex_coords[0] = 0.25f; + slice.p2.tex_coords[1] = tex_coord; + slice.p2.erode = t.start_erode_; + slice.p2.erode_rate = std::max( + 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); + slice.p2.age = 0.0f; + slice.p2.bouyancy = 0.3f + 0.2f * RandomFloat(); + slice.p2.brightness = std::max( + 0.0f, start_brightness_2 + + (RandomFloat() - 0.5f) * start_brightness_rand); + slice.p2.fade = 0.0f; + slice.p2.glow_r = slice.p2.glow_g = slice.p2.glow_b = 0.0f; + slice.p2.fade_rate = 1.0f + fade_rate_randomness * (RandomFloat()); + } + + t.has_updated_ = true; + float tex_change_rate = 0.18f * t.tex_change_rate_; + float emit_change_rate = -0.4f * t.emit_rate_falloff_rate_; + float start_erode_change_rate = 1.0f; + float start_spread_change_rate = -0.35f; + + // Reset our tex coord to that of the last span for marching purposes. + for (; dist > span; dist -= span) { // NOLINT + p += march_dir; + tex_coord += span * tex_change_rate; + emit_rate = std::max(0.0f, emit_rate + span * emit_change_rate); + start_erode = + std::min(1.0f, start_erode + span * start_erode_change_rate); + start_spread = + std::max(1.0f, start_spread + span * start_spread_change_rate); + + // General density stays high until emit rate gets low. + float density = emit_rate > 0.1f ? 1.0f : emit_rate / 0.1f; + + Vector3f r_uniform = Utils::Sphrand(0.2f * t.slice_rand_scale_); + t.slices_.emplace_back(); + Tendril::Slice& slice(t.slices_.back()); + slice.emit_rate = emit_rate; + slice.start_erode = start_erode; + slice.start_spread = start_spread; + slice.p1.p = p - t.radius_ * side_vec * start_spread; + slice.p1.v = t.medium_velocity_ * 0.3f + + t.velocity_ * inherit_velocity + - side_vec * t.radius_ * t.side_spread_rate_ + r_uniform + + Utils::Sphrand(0.2f * t.point_rand_scale_); + slice.p1.tex_coords[0] = 0.0f; + slice.p1.tex_coords[1] = tex_coord; + slice.p1.erode = start_erode; + slice.p1.erode_rate = std::max( + 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); + slice.p1.age = 0.0f; + slice.p1.bouyancy = 0.3f + 0.2f * RandomFloat(); + slice.p1.brightness = std::max( + 0.0f, start_brightness + + (RandomFloat() - 0.5f) * start_brightness_rand); + slice.p1.fade = density * t.start_fade_scale_; + slice.p1.glow_r = slice.p1.glow_g = slice.p1.glow_b = 0.0f; + slice.p1.fade_rate = 1.0f + fade_rate_randomness * (RandomFloat()); + + slice.p2.p = p + t.radius_ * side_vec * start_spread; + slice.p2.v = t.medium_velocity_ * 0.3f + + t.velocity_ * inherit_velocity + + side_vec * t.radius_ * t.side_spread_rate_ + r_uniform + + Utils::Sphrand(0.2f * t.point_rand_scale_); + slice.p2.tex_coords[0] = 0.25f; + slice.p2.tex_coords[1] = tex_coord; + slice.p2.erode = start_erode; + slice.p2.erode_rate = std::max( + 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); + slice.p2.age = 0.0f; + slice.p2.bouyancy = 0.3f + 0.2f * RandomFloat(); + slice.p2.brightness = std::max( + 0.0f, start_brightness_2 + + (RandomFloat() - 0.5f) * start_brightness_rand); + slice.p2.fade = density * t.start_fade_scale_; + slice.p2.glow_r = slice.p2.glow_g = slice.p2.glow_b = 0.0f; + slice.p2.fade_rate = 1.0f + fade_rate_randomness * (RandomFloat()); + + // If our emit rate has dropped to zero, this will be our last span. + if (t.emit_rate_ <= 0.001f) t.emitting_ = false; + } + // Add leftover dist to wind up with our current tex-coord/emit-rate. + t.tex_coord_ = tex_coord + (dist * tex_change_rate); + t.emit_rate_ = emit_rate + (dist * emit_change_rate); + t.start_erode_ = start_erode + (dist * start_erode_change_rate); + t.start_spread_ = + std::max(1.0f, start_spread + dist * start_spread_change_rate); + + // Update our at-emitter slice. + float density = t.emit_rate_ > 0.1f ? 1.0f : t.emit_rate_ / 0.1f; + + t.cur_slice_.p1.p = + t.position_ - t.radius_ * side_vec * t.start_spread_; + t.cur_slice_.p1.tex_coords[0] = 0.0f; + t.cur_slice_.p1.tex_coords[1] = t.tex_coord_; + t.cur_slice_.p1.erode = t.start_erode_; + t.cur_slice_.p1.erode_rate = std::max( + 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); + t.cur_slice_.p1.age = 0.0f; + t.cur_slice_.p1.brightness = start_brightness; + t.cur_slice_.p1.fade = density * t.start_fade_scale_; + t.cur_slice_.p1.glow_r = t.cur_slice_.p1.glow_g = + t.cur_slice_.p1.glow_b = 0.0f; + t.cur_slice_.p1.fade_rate = + 1.0f + fade_rate_randomness * (RandomFloat()); + + t.cur_slice_.p2.p = + t.position_ + t.radius_ * side_vec * t.start_spread_; + t.cur_slice_.p2.tex_coords[0] = 0.25f; + t.cur_slice_.p2.tex_coords[1] = t.tex_coord_; + t.cur_slice_.p2.erode = t.start_erode_; + t.cur_slice_.p2.erode_rate = std::max( + 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); + t.cur_slice_.p2.age = 0.0f; + t.cur_slice_.p2.brightness = start_brightness_2; + t.cur_slice_.p2.fade = density * t.start_fade_scale_; + t.cur_slice_.p2.glow_r = t.cur_slice_.p2.glow_g = + t.cur_slice_.p2.glow_b = 0.0f; + t.cur_slice_.p2.fade_rate = + 1.0f + fade_rate_randomness * (RandomFloat()); + } + } + + // Ok now update lighting and distortion on our tendril points and store + // them for rendering. + { + for (auto&& s : t.slices_) { + render_slice_count++; + s.p1.UpdateGlow(*this, t.glow_scale_); + s.p2.UpdateGlow(*this, t.glow_scale_); + s.p1.UpdateDistortion(*this); + s.p2.UpdateDistortion(*this); + } + // Also update our in-progress ones. + render_slice_count++; + t.cur_slice_.p1.UpdateGlow(*this, t.glow_scale_); + t.cur_slice_.p2.UpdateGlow(*this, t.glow_scale_); + t.cur_slice_.p1.UpdateDistortion(*this); + t.cur_slice_.p2.UpdateDistortion(*this); + } + i++; + } +} + +void BGDynamicsServer::Clear() { + // Clear chunks. + { + auto i = chunks_.begin(); + while (i != chunks_.end()) { + delete *i; + chunks_.erase(i); + chunk_count_--; + i = chunks_.begin(); + assert(chunk_count_ >= 0); + } + assert(chunk_count_ == 0); + } + + // ..and tendrils. + { + auto i = tendrils_.begin(); + while (i != tendrils_.end()) { + if ((**i).type_ == BGDynamicsTendrilType::kThinSmoke) { + tendril_count_thin_--; + } else { + tendril_count_thick_--; + } + delete *i; + tendrils_.erase(i); + i = tendrils_.begin(); + } + assert(tendril_count_thin_ == 0 && tendril_count_thick_ == 0); + } +} + +void BGDynamicsServer::PushEmitCall(const BGDynamicsEmission& def) { + PushCall([this, def] { Emit(def); }); +} + +void BGDynamicsServer::Emit(const BGDynamicsEmission& def) { + assert(InBGDynamicsThread()); + + if (def.emit_type == BGDynamicsEmitType::kDistortion) { + fields_.push_back(new Field(this, def.position, def.spread)); + return; + } + + // First off, lets ramp down the number of things we're making depending + // on settings or how many we already have, etc. + int emit_count = def.count; + + int tendril_thick_max = 20; + int tendril_thin_max = 14; + int chunk_max = 200; + + // Scale our counts down based on a few things. + if (graphics_quality_ <= GraphicsQuality::kLow) { + emit_count = static_cast(static_cast(emit_count) * 0.35f); + tendril_thick_max = 0; + tendril_thin_max = 0; + chunk_max = static_cast(static_cast(chunk_max) * 0.5f); + } else if (graphics_quality_ <= GraphicsQuality::kMedium) { + tendril_thick_max = + static_cast(static_cast(tendril_thick_max) * 0.5f); + tendril_thin_max = + static_cast(static_cast(tendril_thin_max) * 0.5f); + chunk_max = static_cast(static_cast(chunk_max) * 0.75f); + } else if (graphics_quality_ == GraphicsQuality::kHigh) { + emit_count = static_cast(static_cast(emit_count) * 0.8f); + tendril_thick_max = + static_cast(static_cast(tendril_thick_max) * 0.6f); + tendril_thin_max = + static_cast(static_cast(tendril_thin_max) * 0.6f); + chunk_max = static_cast(static_cast(chunk_max) * 0.75f); + } else { +// (higher-quality) + +// On k1 android let's ramp things up even more. +#if BA_OSTYPE_ANDROID + if (g_platform->is_tegra_k1()) { + chunk_max = static_cast(static_cast(chunk_max) * 1.5f); + emit_count = static_cast(static_cast(emit_count) * 1.5f); + tendril_thin_max = + static_cast(static_cast(tendril_thin_max) * 1.25f); + } +#endif // BA_OSTYPE_ANDROID + +#if BA_RIFT_BUILD + // rift build is gonna be running on beefy hardware; let's go crazy + // here.. + chunk_max *= 2.5f; + emit_count *= 2.5f; + tendril_thin_max *= 2.5f; +#endif + +#if BA_DEMO_BUILD + // lets beef up our demo kiosk build too.. what the heck. + chunk_max *= 2.5f; + emit_count *= 2.5f; + tendril_thin_max *= 2.5f; +#endif + } + + if (def.emit_type == BGDynamicsEmitType::kTendrils) { + if (def.tendril_type == BGDynamicsTendrilType::kThinSmoke) { + // For thin tendrils, start scaling back once we pass 8 tendrils. + // Once we're at tendril_thin_max, stop adding completely. + int scale_count = tendril_thin_max / 3; + if (tendril_count_thin_ >= tendril_thin_max) { + emit_count = 0; + } else if (tendril_count_thin_ > scale_count) { + emit_count = static_cast( + static_cast(emit_count) + * (1.0f + - static_cast(tendril_count_thin_ - scale_count) + / static_cast(tendril_thin_max - scale_count))); + } + } else { + // For thick tendrils, start scaling back once we pass 8 tendrils. + // Once we're at tendril_thick_max, stop adding completely. + int scale_count = tendril_thick_max / 3; + if (tendril_count_thick_ >= tendril_thick_max) { + emit_count = 0; + } else if (tendril_count_thick_ > scale_count) { + emit_count = static_cast( + static_cast(emit_count) + * (1.0f + - static_cast(tendril_count_thick_ - scale_count) + / static_cast(tendril_thick_max - scale_count))); + } + } + } else { + // For debris, start scaling back once we pass 50.. at chunk_max lets + // stop. + if (chunk_count_ >= chunk_max) { + emit_count = 0; + } else if (chunk_count_ > 50) { + emit_count = + static_cast(static_cast(emit_count) + * (1.0f + - static_cast(chunk_count_ - 50) + / static_cast(chunk_max - 50))); + } + } + + bool near_surface = false; + Vector3f surface_normal = {0.0f, 0.0f, 0.0f}; + float surface_closeness = 0.0f; + + // For the chunks/tendrils case, lets throw down a ray in the provided + // velocity direction. If we hit something nearby, we can use that info + // to adjust our emission. + if (def.emit_type == BGDynamicsEmitType::kChunks + || def.emit_type == BGDynamicsEmitType::kTendrils) { + dGeomID ray = dCreateRay(nullptr, 2.0f); + dGeomRaySetClosestHit(ray, true); + Vector3f dir = def.velocity; + dir.y -= RandomFloat() * 10.0f; // bias downward + dGeomRaySet(ray, def.position.x, def.position.y, def.position.z, dir.x, + dir.y, dir.z); + dContact contact[1]; + for (auto&& t : terrains_) { + dGeomID t_geom = t->geom(); + if (dCollide(ray, t_geom, 1, &contact[0].geom, sizeof(dContact))) { + near_surface = true; + surface_normal = contact[0].geom.normal; + float len = (Vector3f(contact[0].geom.pos) - def.position).Length(); + // At length 0.1, closeness is 1; at 2 its 0. + surface_closeness = + 1.0f - std::min(1.0f, std::max(0.0f, (len - 0.2f) / (2.0f - 0.2f))); + break; + } + } + + dGeomDestroy(ray); + } + + Vector3f d_bias = {0.0f, 0.0f, 0.0f}; + if (near_surface) + d_bias = surface_normal * RandomFloat() * 6.0f * surface_closeness; + + switch (def.emit_type) { + case BGDynamicsEmitType::kChunks: { + // Tone down bias on splinters - we always want those flying + // every which way. + if (def.chunk_type == BGDynamicsChunkType::kSplinter) { + d_bias *= 0.3f; + } + + for (int i = 0; i < emit_count; i++) { + // Bias *most* of our chunks (looks too empty if *everything* is + // going one direction). + + auto* chunk = new Chunk(this, def, true, true, + RandomFloat() < 0.8f ? d_bias : kVector3f0); + + bool do_tendril = false; + if (def.chunk_type == BGDynamicsChunkType::kSpark + && RandomFloat() < 0.13f) { // NOLINT(bugprone-branch-clone) + do_tendril = true; + } else if (def.chunk_type == BGDynamicsChunkType::kSplinter + && RandomFloat() < 0.2f) { + do_tendril = true; + } + + // If we're emitting sparks, every now and then give one of them a + // smoke tendril. + if (do_tendril) { + // Create a tendril, create a controller for it, and store it + // with the chunk. + { + BGDynamicsTendrilType tendril_type = + BGDynamicsTendrilType::kThinSmoke; + auto* t = new Tendril(this); + t->type_ = tendril_type; + t->shading_flip_ = false; + t->wind_amt_ = 0.4f + RandomFloat() * 1.6f; + t->shadow_density_ = 1.0f; + { + t->radius_ *= 0.15f; + t->side_spread_rate_ = 0.3f; + t->point_rand_scale_ = 0.5f; + t->slice_rand_scale_ = 0.5f; + t->tex_change_rate_ = 1.5f + RandomFloat() * 2.0f; + t->emit_rate_falloff_rate_ = 0.2f + RandomFloat() * 0.6f; + t->start_brightness_max_ = 0.92f; + t->start_brightness_min_ = 0.9f; + t->brightness_rand_ = 0.1f; + t->start_fade_scale_ = 0.15f + RandomFloat() * 0.2f; + t->glow_scale_ = 1.0f; + } + tendrils_.push_back(t); + tendril_count_thin_++; + auto* c = new TendrilController(t); + chunk->tendril_controller_ = c; + chunk->UpdateTendril(); + } + } + chunks_.push_back(chunk); + chunk_count_++; + } + break; + } + case BGDynamicsEmitType::kStickers: { + BGDynamicsEmission edef = def; + dGeomID ray = dCreateRay(nullptr, 4.0f); + dGeomRaySetClosestHit(ray, true); + for (int i = 0; i < emit_count; i++) { + Vector3f dir = Utils::Sphrand(def.spread); + dir.y -= def.spread * 2.5f * RandomFloat(); // bias downward + dGeomRaySet(ray, def.position.x, def.position.y + 0.5f, def.position.z, + dir.x, dir.y, dir.z); + dContact contact[1]; + for (auto&& t : terrains_) { + dGeomID t_geom = t->geom(); + if (dCollide(ray, t_geom, 1, &contact[0].geom, sizeof(dContact))) { + // Create a static chunk at this hit point. + edef.position = Vector3f(contact[0].geom.pos); + chunks_.push_back(new Chunk(this, edef, false)); + chunk_count_++; + } + } + } + dGeomDestroy(ray); + break; + } + case BGDynamicsEmitType::kTendrils: { +#if BA_DEBUG_BUILD + g_debug_lines.clear(); + g_debug_points.clear(); + g_debug_points.push_back(def.position); +#endif + + float ray_len = 1.5f; + float ray_offset = 0.3f; + dGeomID ray = dCreateRay(nullptr, ray_len); + dGeomRaySetClosestHit(ray, true); + for (int i = 0; i < emit_count; i++) { + Vector3f dir = (Utils::Sphrand() + d_bias * 0.5f).Normalized(); + dGeomRaySet(ray, def.position.x, def.position.y + ray_offset, + def.position.z, dir.x, dir.y, dir.z); + dContact contact[1]; + Vector3f pos = {0.0f, 0.0f, 0.0f}; + Vector3f vel = {0.0f, 0.0f, 0.0f}; + bool hit = false; + for (auto&& t : terrains_) { + dGeomID t_geom = t->geom(); + if (dCollide(ray, t_geom, 1, &contact[0].geom, sizeof(dContact))) { + pos = Vector3f(contact[0].geom.pos); + vel = Reflect(dir, Vector3f(contact[0].geom.normal)); + // bias direction up a bit.. this way it'll hopefully be less + // likely to point underground when we smash it down on the + // camera plane + vel.y += RandomFloat() * def.spread * 1.0f; + hit = true; + break; + } + } + if (!hit) { + // since dbias pushes us all in a direction away from a surface, + // nudge our start pos in the opposite dir a bit so we butt up + // against the surface more + pos = def.position + d_bias * RandomFloat() * -0.3f; + vel = dir; + } +#if BA_DEBUG_BUILD + g_debug_lines.emplace_back( + def.position + Vector3f(0, ray_offset, 0), + def.position + Vector3f(0, ray_offset, 0) + (dir * ray_len), + hit ? Vector3f(1, 0, 0) : Vector3f(0, 1, 0)); +#endif + + Vector3f to_cam = (cam_pos_ - pos).Normalized(); + + // Push the velocity towards the camera z plane to minimize + // artifacts from moving towards/away from cam. + Vector3f cam_component = to_cam * (vel.Dot(to_cam)); + vel -= 0.8f * cam_component; + + // Let's also push our pos towards the cam a wee bit so less is + // clipped. + pos += to_cam * 0.8f; + + // Now that we've got direction, assign random velocity. + vel = vel.Normalized() * (10.0f + RandomFloat() * 30.0f); + + { + auto* t = new Tendril(this); + t->type_ = def.tendril_type; + t->prev_pos_ = t->position_ = pos; + t->shadow_position_ = pos; + t->shading_flip_ = (vel.x > 0.0f); + t->wind_amt_ = 0.4f + RandomFloat() * 1.6f; + t->shadow_density_ = 1.0f; + t->velocity_ = vel; + if (def.tendril_type == BGDynamicsTendrilType::kThinSmoke) { + t->radius_ *= 0.2f; + t->side_spread_rate_ = 0.3f; + t->point_rand_scale_ = 0.3f; + t->tex_change_rate_ = 1.0f + RandomFloat() * 2.0f; + t->emit_rate_falloff_rate_ = 0.45f + RandomFloat() * 0.2f; + t->start_brightness_max_ = 0.82f; + t->start_brightness_min_ = 0.8f; + t->brightness_rand_ = 0.1f; + t->start_fade_scale_ = 0.1f + RandomFloat() * 0.2f; + t->glow_scale_ = 0.15f; + } else { + t->radius_ *= 0.7f + RandomFloat() * 0.2f; + t->side_spread_rate_ = 0.2f + 4.0f * RandomFloat(); + t->emit_rate_falloff_rate_ = 0.9f + RandomFloat() * 0.6f; + t->glow_scale_ = 1.0f; + } + tendrils_.push_back(t); + if (def.tendril_type == BGDynamicsTendrilType::kThinSmoke) { + tendril_count_thin_++; + } else { + tendril_count_thick_++; + } + } + } + dGeomDestroy(ray); + break; + } + case BGDynamicsEmitType::kFlagStand: { + float ray_len = 10.0f; + dGeomID ray = dCreateRay(nullptr, ray_len); + dGeomRaySetClosestHit(ray, true); + Vector3f dir(0.0f, -1.0f, 0.0f); + dGeomRaySet(ray, def.position.x, def.position.y, def.position.z, dir.x, + dir.y, dir.z); + dContact contact[1]; + for (auto&& t : terrains_) { + dGeomID t_geom = t->geom(); + if (dCollide(ray, t_geom, 1, &contact[0].geom, sizeof(dContact))) { + BGDynamicsEmission edef = def; + edef.chunk_type = BGDynamicsChunkType::kFlagStand; + edef.position = Vector3f(contact[0].geom.pos); + chunks_.push_back(new Chunk(this, edef, false, false)); + chunk_count_++; + break; + } + } + dGeomDestroy(ray); + break; + } + case BGDynamicsEmitType::kFairyDust: { + spark_particles_->Emit( + Vector3f(def.position.x + 0.9f * (RandomFloat() - 0.5f), + def.position.y + 0.9f * (RandomFloat() - 0.5f), + def.position.z + 0.9f * (RandomFloat() - 0.5f)), + 0.001f * def.velocity, 0.8f + 3.0f * +RandomFloat(), + 0.8f + 3.0f * RandomFloat(), 0.8f + 3.0f * RandomFloat(), 0, + -0.01f, // dlife + 0.05f + 0.05f * RandomFloat(), // size + -0.001f, // dsize + 5.0f // flicker intensity + ); // NOLINT(whitespace/parens) + break; + } + default: { + int t = static_cast(def.emit_type); + BA_LOG_ONCE("Invalid bg-dynamics emit type: " + std::to_string(t)); + break; + } + } +} + +void BGDynamicsServer::PushRemoveTerrainCall(CollideModelData* collide_model) { + PushCall([this, collide_model] { + assert(collide_model != nullptr); + bool found = false; + for (auto i = terrains_.begin(); i != terrains_.end(); ++i) { + if ((**i).GetCollideModel() == collide_model) { + found = true; + delete *i; + terrains_.erase(i); + break; + } + } + if (!found) { + throw Exception("invalid RemoveTerrainCall"); + } + + // Rebuild geom list from our present terrains. + std::vector geoms; + geoms.reserve(terrains_.size()); + for (auto&& i : terrains_) { + geoms.push_back(i->geom()); + } + height_cache_->SetGeoms(geoms); + collision_cache_->SetGeoms(geoms); + + // Clear existing stuff whenever this changes. + Clear(); + }); +} + +void BGDynamicsServer::PushAddShadowCall(BGDynamicsShadowData* shadow_data) { + PushCall([this, shadow_data] { + assert(InBGDynamicsThread()); + std::lock_guard lock(shadow_list_mutex_); + shadows_.push_back(shadow_data); + }); +} + +void BGDynamicsServer::PushRemoveShadowCall(BGDynamicsShadowData* shadow_data) { + PushCall([this, shadow_data] { + assert(InBGDynamicsThread()); + bool found = false; + { + std::lock_guard lock(shadow_list_mutex_); + for (auto i = shadows_.begin(); i != shadows_.end(); ++i) { + if ((*i) == shadow_data) { + found = true; + shadows_.erase(i); + break; + } + } + } + assert(found); + delete shadow_data; + }); +} + +void BGDynamicsServer::PushAddVolumeLightCall( + BGDynamicsVolumeLightData* volume_light_data) { + PushCall([this, volume_light_data] { + // Add to our internal list. + std::lock_guard lock(volume_light_list_mutex_); + volume_lights_.push_back(volume_light_data); + }); +} + +void BGDynamicsServer::PushRemoveVolumeLightCall( + BGDynamicsVolumeLightData* volume_light_data) { + PushCall([this, volume_light_data] { + // Remove from our list and kill. + bool found = false; + { + std::lock_guard lock(volume_light_list_mutex_); + for (auto i = volume_lights_.begin(); i != volume_lights_.end(); ++i) { + if ((*i) == volume_light_data) { + found = true; + volume_lights_.erase(i); + break; + } + } + } + assert(found); + delete volume_light_data; + }); +} + +void BGDynamicsServer::PushAddFuseCall(BGDynamicsFuseData* fuse_data) { + PushCall([this, fuse_data] { + std::lock_guard lock(fuse_list_mutex_); + fuses_.push_back(fuse_data); + }); +} + +void BGDynamicsServer::PushRemoveFuseCall(BGDynamicsFuseData* fuse_data) { + PushCall([this, fuse_data] { + bool found = false; + { + std::lock_guard lock(fuse_list_mutex_); + for (auto i = fuses_.begin(); i != fuses_.end(); i++) { + if ((*i) == fuse_data) { + found = true; + fuses_.erase(i); + break; + } + } + } + assert(found); + delete fuse_data; + }); +} + +void BGDynamicsServer::PushSetDebrisFrictionCall(float friction) { + PushCall([this, friction] { debris_friction_ = friction; }); +} + +void BGDynamicsServer::PushSetDebrisKillHeightCall(float height) { + PushCall([this, height] { debris_kill_height_ = height; }); +} + +auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { + assert(InBGDynamicsThread()); + + auto* ss = new BGDynamicsDrawSnapshot(); + + uint32_t rock_count = 0; + uint32_t ice_count = 0; + uint32_t slime_count = 0; + uint32_t metal_count = 0; + uint32_t spark_count = 0; + uint32_t splinter_count = 0; + uint32_t sweat_count = 0; + uint32_t flag_stand_count = 0; + + uint32_t shadow_max_count = 0; + uint32_t light_max_count = 0; + uint32_t shadow_drawn_count = 0; + uint32_t light_drawn_count = 0; + + for (auto&& i : chunks_) { + BGDynamicsChunkType t = i->type(); + switch (t) { + case BGDynamicsChunkType::kRock: + rock_count++; + break; + case BGDynamicsChunkType::kIce: + ice_count++; + break; + case BGDynamicsChunkType::kSlime: + slime_count++; + break; + case BGDynamicsChunkType::kMetal: + metal_count++; + break; + case BGDynamicsChunkType::kSpark: + spark_count++; + break; + case BGDynamicsChunkType::kSplinter: + splinter_count++; + break; + case BGDynamicsChunkType::kSweat: + sweat_count++; + break; + case BGDynamicsChunkType::kFlagStand: + flag_stand_count++; + break; + } + // tally shadow/lights + switch (t) { + case BGDynamicsChunkType::kFlagStand: + case BGDynamicsChunkType::kSweat: + break; // these have no shadows + case BGDynamicsChunkType::kIce: + case BGDynamicsChunkType::kSpark: + light_max_count++; + break; + default: + shadow_max_count++; + break; + } + } + + Matrix44f* c_rock = nullptr; + Matrix44f* c_ice = nullptr; + Matrix44f* c_slime = nullptr; + Matrix44f* c_metal = nullptr; + Matrix44f* c_spark = nullptr; + Matrix44f* c_splinter = nullptr; + Matrix44f* c_sweat = nullptr; + Matrix44f* c_flag_stand = nullptr; + + if (rock_count > 0) { + ss->rocks.resize(rock_count); + c_rock = ss->rocks.data(); + } + if (ice_count > 0) { + ss->ice.resize(ice_count); + c_ice = ss->ice.data(); + } + if (slime_count > 0) { + ss->slime.resize(slime_count); + c_slime = ss->slime.data(); + } + if (metal_count > 0) { + ss->metal.resize(metal_count); + c_metal = ss->metal.data(); + } + if (spark_count > 0) { + ss->sparks.resize(spark_count); + c_spark = ss->sparks.data(); + } + if (splinter_count > 0) { + ss->splinters.resize(splinter_count); + c_splinter = ss->splinters.data(); + } + if (sweat_count > 0) { + ss->sweats.resize(sweat_count); + c_sweat = ss->sweats.data(); + } + if (flag_stand_count > 0) { + ss->flag_stands.resize(flag_stand_count); + c_flag_stand = ss->flag_stands.data(); + } + + // Allocate buffers as if we're drawing *all* lights/shadows for chunks. + // We may prune this down. + uint16_t *s_index = nullptr, *l_index = nullptr; + VertexSprite *s_vertex = nullptr, *l_vertex = nullptr; + uint32_t s_vertex_index = 0, l_vertex_index = 0; + if (shadow_max_count > 0) { + auto* ibuf = Object::NewDeferred(shadow_max_count * 6); + + // Game thread is default owner; needs to be us until we hand it over. + ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->shadow_indices = Object::MakeRefCounted(ibuf); + s_index = &ss->shadow_indices->elements[0]; + auto* vbuf = + Object::NewDeferred(shadow_max_count * 4); + + // Game thread is default owner; needs to be us until we hand it over. + vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->shadow_vertices = Object::MakeRefCounted(vbuf); + s_vertex = &ss->shadow_vertices->elements[0]; + s_vertex_index = 0; + } + if (light_max_count > 0) { + auto* ibuf = Object::NewDeferred(light_max_count * 6); + + // Game thread is default owner; needs to be us until we hand it over. + ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->light_indices = Object::MakeRefCounted(ibuf); + l_index = &ss->light_indices->elements[0]; + auto* vbuf = + Object::NewDeferred(light_max_count * 4); + + // Game thread is default owner; needs to be us until we hand it over. + vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->light_vertices = Object::MakeRefCounted(vbuf); + l_vertex = &ss->light_vertices->elements[0]; + l_vertex_index = 0; + } + + Matrix44f* c{}; + + for (auto&& i : chunks_) { + BGDynamicsChunkType type = i->type(); + switch (type) { + case BGDynamicsChunkType::kRock: + c = c_rock; + break; + case BGDynamicsChunkType::kIce: + c = c_ice; + break; + case BGDynamicsChunkType::kSlime: + c = c_slime; + break; + case BGDynamicsChunkType::kMetal: + c = c_metal; + break; + case BGDynamicsChunkType::kSpark: + c = c_spark; + break; + case BGDynamicsChunkType::kSplinter: + c = c_splinter; + break; + case BGDynamicsChunkType::kSweat: + c = c_sweat; + break; + case BGDynamicsChunkType::kFlagStand: + c = c_flag_stand; + break; + } + + const float* s = i->size(); + if (i->dynamic()) { + dBodyID b = i->body(); + const dReal* p = dBodyGetPosition(b); + const dReal* r = dBodyGetRotation(b); + (*c).m[0] = r[0] * s[0]; // NOLINT: clang-tidy says possible null + (*c).m[1] = r[4] * s[0]; + (*c).m[2] = r[8] * s[0]; + (*c).m[3] = 0; + (*c).m[4] = r[1] * s[1]; + (*c).m[5] = r[5] * s[1]; + (*c).m[6] = r[9] * s[1]; + (*c).m[7] = 0; + (*c).m[8] = r[2] * s[2]; + (*c).m[9] = r[6] * s[2]; + (*c).m[10] = r[10] * s[2]; + (*c).m[11] = 0; + (*c).m[12] = p[0]; + (*c).m[13] = p[1]; + (*c).m[14] = p[2]; + (*c).m[15] = 1; + } else { + // NOLINTNEXTLINE: clang-tidy complaining of possible null here. + memcpy((*c).m, i->static_transform(), sizeof((*c))); + } + + // Shadow size is just average of our dimensions. + float shadow_size = (s[0] + s[1] + s[2]) * 0.3333f; + + // These are elongated so shadows are a bit big by default. + if (type == BGDynamicsChunkType::kSplinter) shadow_size *= 0.65f; + float flicker = i->flicker_; + float shadow_dist = i->shadow_dist_; + float life = std::min( + 1.0f, (static_cast(time_) - i->birth_time_) / i->lifespan_); + + // Shrink our matrix down over time. + switch (type) { + case BGDynamicsChunkType::kSpark: + case BGDynamicsChunkType::kSweat: { + float shrink_scale = (1.0f - life) * flicker; + Matrix44f* m = &(*c); + (*m) = Matrix44fScale(shrink_scale) * (*m); + break; + } + default: { + // Regular chunks shrink only when on the ground. + float sd = shadow_dist; + Matrix44f* m = &(*c); + if (sd < 1.0f && sd >= 0) { + float sink = -sd * life; + (*m) = (*m) * Matrix44fTranslate(0, sink, 0); + } + float shrink_scale = 1.0f - life; + (*m) = Matrix44fScale(shrink_scale) * (*m); + break; + } + } + + // Go ahead and build a buffer for our lights/shadows so when it comes + // time to draw we just have to upload it. + float shadow_scale_mult = 1.0f; + float max_shadow_scale = 2.3f; + float max_shadow_grow_dist = 2.0f; + float max_shadow_dist = 1.0f; + bool draw_shadow{}; + bool draw_light{}; + switch (type) { + case BGDynamicsChunkType::kIce: + case BGDynamicsChunkType::kSpark: { + draw_shadow = false; + draw_light = true; + shadow_scale_mult *= 8.0f; + break; + } + case BGDynamicsChunkType::kFlagStand: + case BGDynamicsChunkType::kSweat: + draw_shadow = false; + draw_light = false; + break; // These have no shadows. + default: { + draw_shadow = true; + draw_light = false; + } + } + + if (draw_shadow || draw_light) { + // Only draw light/shadow if we're within our max/min distances + // from the ground. + if (shadow_dist > -kShadowOccludeDistance + && shadow_dist < max_shadow_dist) { + float sd = shadow_dist; + + // Ok we'll draw this fella. + uint16_t* this_i{}; + VertexSprite* this_v{}; + uint32_t this_v_index{}; + if (draw_shadow) { + shadow_drawn_count++; + this_i = s_index; + this_v = s_vertex; + this_v_index = s_vertex_index; + s_index += 6; + s_vertex += 4; + s_vertex_index += 4; + } else { + light_drawn_count++; + assert(draw_light); + this_i = l_index; + this_v = l_vertex; + this_v_index = l_vertex_index; + l_index += 6; + l_vertex += 4; + l_vertex_index += 4; + } + + float* m = c->m; + + // As we get farther from the ground, our shadow gets bigger and + // softer. + float shadow_scale{}; + float density{}; + + // Negative shadow_dist means some object is in front of our + // shadow-caster. In this case lets keep our scale the same + // as it would have been at zero dist but fade our density + // out gradually as we become more deeply submerged. + if (sd <= 0.0f) { + shadow_scale = 1.0f; + density = 1.0f - std::min(1.0f, -sd / kShadowOccludeDistance); + } else { + // Normal non-submerged shadow. + shadow_scale = + 1.0f + + std::max(0.0f, std::min(1.0f, (sd / max_shadow_grow_dist)) + * (max_shadow_scale - 1.0f)); + density = 0.5f * g_graphics->GetShadowDensity(m[12], m[13], m[14]) + * (1.0f - (sd / max_shadow_dist)); + } + + // Sink down over the course of our lifespan if we + // know where the ground is. + float sink = 0.0f; + if (sd < 1.0f && sd >= 0.0f) { + sink = -sd * life; + } + shadow_scale *= (1.0f - life); + assert(shadow_scale >= 0.0f); + + // Drop our density as our shadow scale grows. + // Do this *after* this is used to modulate density. + shadow_scale *= shadow_scale_mult; + + // Add our 6 indices. + { + this_i[0] = static_cast(this_v_index); + this_i[1] = static_cast(this_v_index + 1); + this_i[2] = static_cast(this_v_index + 2); + this_i[3] = static_cast(this_v_index + 1); + this_i[4] = static_cast(this_v_index + 3); + this_i[5] = static_cast(this_v_index + 2); + } + + // Add our 4 verts. + this_v[0].uv[0] = 0; + this_v[0].uv[1] = 0; + this_v[1].uv[0] = 0; + this_v[1].uv[1] = 65535; + this_v[2].uv[0] = 65535; + this_v[2].uv[1] = 0; + this_v[3].uv[0] = 65535; + this_v[3].uv[1] = 65535; + + switch (type) { + case BGDynamicsChunkType::kIce: { + this_v[0].color[0] = this_v[1].color[0] = this_v[2].color[0] = + this_v[3].color[0] = 0.1f * density; + this_v[0].color[1] = this_v[1].color[1] = this_v[2].color[1] = + this_v[3].color[1] = 0.1f * density; + this_v[0].color[2] = this_v[1].color[2] = this_v[2].color[2] = + this_v[3].color[2] = 0.2f * density; + this_v[0].color[3] = this_v[1].color[3] = this_v[2].color[3] = + this_v[3].color[3] = 0.2f * density; + break; + } + case BGDynamicsChunkType::kSpark: { + this_v[0].color[0] = this_v[1].color[0] = this_v[2].color[0] = + this_v[3].color[0] = 0.3f * density; + this_v[0].color[1] = this_v[1].color[1] = this_v[2].color[1] = + this_v[3].color[1] = 0.12f * density; + this_v[0].color[2] = this_v[1].color[2] = this_v[2].color[2] = + this_v[3].color[2] = 0.10f * density; + this_v[0].color[3] = this_v[1].color[3] = this_v[2].color[3] = + this_v[3].color[3] = 0.1f * density; + break; + } + default: { + this_v[0].color[0] = this_v[1].color[0] = this_v[2].color[0] = + this_v[3].color[0] = 0.0f; + this_v[0].color[1] = this_v[1].color[1] = this_v[2].color[1] = + this_v[3].color[1] = 0.0f; + this_v[0].color[2] = this_v[1].color[2] = this_v[2].color[2] = + this_v[3].color[2] = 0.0f; + this_v[0].color[3] = this_v[1].color[3] = this_v[2].color[3] = + this_v[3].color[3] = 0.4f * density; + break; + } + } + this_v[0].position[0] = this_v[1].position[0] = this_v[2].position[0] = + this_v[3].position[0] = m[12]; + this_v[0].position[1] = this_v[1].position[1] = this_v[2].position[1] = + this_v[3].position[1] = m[13] + sink; + this_v[0].position[2] = this_v[1].position[2] = this_v[2].position[2] = + this_v[3].position[2] = m[14]; + this_v[0].size = this_v[1].size = this_v[2].size = this_v[3].size = + 2.8f * shadow_size * shadow_scale; + } + } + c++; + switch (type) { + case BGDynamicsChunkType::kRock: + c_rock = c; + break; + case BGDynamicsChunkType::kIce: + c_ice = c; + break; + case BGDynamicsChunkType::kSlime: + c_slime = c; + break; + case BGDynamicsChunkType::kMetal: + c_metal = c; + break; + case BGDynamicsChunkType::kSpark: + c_spark = c; + break; + case BGDynamicsChunkType::kSplinter: + c_splinter = c; + break; + case BGDynamicsChunkType::kSweat: + c_sweat = c; + break; + case BGDynamicsChunkType::kFlagStand: + c_flag_stand = c; + break; + } + } + if (shadow_max_count > 0) { + if (shadow_drawn_count == 0) { + // If we didn't actually draw *any*, completely kill our buffers. + ss->shadow_indices.Clear(); + ss->shadow_vertices.Clear(); + } else if (shadow_drawn_count != shadow_max_count) { + // Otherwise resize our buffers down to what we actually used. + assert(s_index - (&ss->shadow_indices->elements[0]) + == shadow_drawn_count * 6); + assert(s_vertex - (&ss->shadow_vertices->elements[0]) + == shadow_drawn_count * 4); + assert(ss->shadow_indices->elements.size() == shadow_max_count * 6); + ss->shadow_indices->elements.resize(shadow_drawn_count * 6); + assert(ss->shadow_vertices->elements.size() == shadow_max_count * 4); + ss->shadow_vertices->elements.resize(shadow_drawn_count * 4); + } else { + assert(s_index - (&ss->shadow_indices->elements[0]) + == shadow_max_count * 6); + assert(s_vertex - (&ss->shadow_vertices->elements[0]) + == shadow_max_count * 4); + } + } + if (light_max_count > 0) { + // If we didn't actually draw *any*, clear our buffers. + if (light_drawn_count == 0) { + ss->light_indices.Clear(); + ss->light_vertices.Clear(); + } else if (light_drawn_count != light_max_count) { + // Otherwise resize our buffers down to what we actually used. + assert(l_index - (&ss->light_indices->elements[0]) + == light_drawn_count * 6); + assert(l_vertex - (&ss->light_vertices->elements[0]) + == light_drawn_count * 4); + assert(ss->light_indices->elements.size() == light_max_count * 6); + ss->light_indices->elements.resize(light_drawn_count * 6); + assert(ss->light_vertices->elements.size() == light_max_count * 4); + ss->light_vertices->elements.resize(light_drawn_count * 4); + } else { + assert(l_index - (&ss->light_indices->elements[0]) + == light_max_count * 6); + assert(l_vertex - (&ss->light_vertices->elements[0]) + == light_max_count * 4); + } + } + + // Now add tendrils. + { + int smoke_slice_count = 0; + int smoke_index_count = 0; + int shadow_count = 0; + for (auto&& i : tendrils_) { + const Tendril& t(*i); + if (!t.has_updated_) continue; + int slice_count = + static_cast(t.slices_.size() + (t.emitting_ ? 1 : 0)); + if (slice_count > 1) { + shadow_count++; + smoke_index_count += (slice_count - 1) * 6; + smoke_slice_count += slice_count; + } + } + if (smoke_slice_count > 0) { + auto* ibuf = Object::NewDeferred(smoke_index_count); + + // Game thread is default owner; needs to be us until we hand it over. + ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->tendril_indices = Object::MakeRefCounted(ibuf); + uint16_t* index = &ss->tendril_indices->elements[0]; + auto* vbuf = + Object::NewDeferred(smoke_slice_count * 2); + + // Game thread is default owner; needs to be us until we hand it over. + vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->tendril_vertices = Object::MakeRefCounted(vbuf); + VertexSmokeFull* v = &ss->tendril_vertices->elements[0]; + ss->tendril_shadows.reserve(static_cast(shadow_count)); + int v_num = 0; + for (auto&& i : tendrils_) { + const Tendril& t(*i); + if (!t.has_updated_) { + continue; + } + int slice_count = + static_cast(t.slices_.size() + (t.emitting_ ? 1 : 0)); + if (slice_count < 2) { + continue; + } + ss->tendril_shadows.emplace_back(t.shadow_position_, t.shadow_density_); + for (auto&& slc : t.slices_) { + v->position[0] = slc.p1.p_distorted.x; + v->position[1] = slc.p1.p_distorted.y; + v->position[2] = slc.p1.p_distorted.z; + v->uv[0] = slc.p1.tex_coords[0]; + v->uv[1] = slc.p1.tex_coords[1]; + v->erode = static_cast(std::max( + 0, std::min(255, static_cast(255.0f * slc.p1.erode)))); + float fade = std::min(1.0f, slc.p1.fade); + v->color[0] = static_cast(std::max( + 0, std::min(255, static_cast(kSmokeGlow * slc.p1.glow_r)))); + v->color[1] = static_cast(std::max( + 0, std::min(255, static_cast(kSmokeGlow * slc.p1.glow_g)))); + v->color[2] = static_cast(std::max( + 0, std::min(255, static_cast(kSmokeGlow * slc.p1.glow_b)))); + v->color[3] = static_cast( + std::max(0, std::min(255, static_cast(255.0f * fade)))); + v->diffuse = static_cast(std::max( + 0, std::min(255, static_cast(255.0f * slc.p1.brightness)))); + v++; + v->position[0] = slc.p2.p_distorted.x; + v->position[1] = slc.p2.p_distorted.y; + v->position[2] = slc.p2.p_distorted.z; + v->uv[0] = slc.p2.tex_coords[0]; + v->uv[1] = slc.p2.tex_coords[1]; + v->erode = static_cast(std::max( + 0, std::min(255, static_cast(255.0f * slc.p2.erode)))); + fade = std::min(1.0f, slc.p2.fade); + v->color[0] = static_cast(std::max( + 0, + std::min(255, static_cast(kSmokeBaseGlow + + kSmokeGlow * slc.p2.glow_r)))); + v->color[1] = static_cast(std::max( + 0, + std::min(255, static_cast(kSmokeBaseGlow + + kSmokeGlow * slc.p2.glow_g)))); + v->color[2] = static_cast(std::max( + 0, + std::min(255, static_cast(kSmokeBaseGlow + + kSmokeGlow * slc.p2.glow_b)))); + v->color[3] = static_cast( + std::max(0, std::min(255, static_cast(255.0f * fade)))); + v->diffuse = static_cast(std::max( + 0, std::min(255, static_cast(255.0f * slc.p2.brightness)))); + v++; + } + + // Spit out the in-progress slice if the tendril is still emitting. + if (t.emitting_) { + v->position[0] = t.cur_slice_.p1.p_distorted.x; + v->position[1] = t.cur_slice_.p1.p_distorted.y; + v->position[2] = t.cur_slice_.p1.p_distorted.z; + v->uv[0] = t.cur_slice_.p1.tex_coords[0]; + v->uv[1] = t.cur_slice_.p1.tex_coords[1]; + v->erode = std::max( + 0, + std::min(255, static_cast(255.0f * t.cur_slice_.p1.erode))); + float fade = std::min(1.0f, t.cur_slice_.p1.fade); + v->color[0] = std::max( + 0, std::min(255, static_cast( + kSmokeBaseGlow + + kSmokeGlow * t.cur_slice_.p1.glow_r))); + v->color[1] = std::max( + 0, std::min(255, static_cast( + kSmokeBaseGlow + + kSmokeGlow * t.cur_slice_.p1.glow_g))); + v->color[2] = std::max( + 0, std::min(255, static_cast( + kSmokeBaseGlow + + kSmokeGlow * t.cur_slice_.p1.glow_b))); + v->color[3] = static_cast( + std::max(0, std::min(255, static_cast(255.0f * fade)))); + v->diffuse = std::max( + 0, std::min(255, static_cast(255.0f + * t.cur_slice_.p1.brightness))); + v++; + v->position[0] = t.cur_slice_.p2.p_distorted.x; + v->position[1] = t.cur_slice_.p2.p_distorted.y; + v->position[2] = t.cur_slice_.p2.p_distorted.z; + v->uv[0] = t.cur_slice_.p2.tex_coords[0]; + v->uv[1] = t.cur_slice_.p2.tex_coords[1]; + v->erode = std::max( + 0, + std::min(255, static_cast(255.0f * t.cur_slice_.p2.erode))); + fade = std::min(1.0f, t.cur_slice_.p2.fade); + v->color[0] = std::max( + 0, std::min(255, static_cast( + kSmokeBaseGlow + + kSmokeGlow * t.cur_slice_.p2.glow_r))); + v->color[1] = std::max( + 0, std::min(255, static_cast( + kSmokeBaseGlow + + kSmokeGlow * t.cur_slice_.p2.glow_g))); + v->color[2] = std::max( + 0, std::min(255, static_cast( + kSmokeBaseGlow + + kSmokeGlow * t.cur_slice_.p2.glow_b))); + v->color[3] = static_cast( + std::max(0, std::min(255, static_cast(255.0f * fade)))); + v->diffuse = std::max( + 0, std::min(255, static_cast(255.0f + * t.cur_slice_.p2.brightness))); + v++; + } + + // Now write the tri indices for this slice. + for (int j = 0; j < slice_count - 1; j++) { + *index++ = static_cast(v_num); + *index++ = static_cast(v_num + 1); + *index++ = static_cast(v_num + 2); + *index++ = static_cast(v_num + 2); + *index++ = static_cast(v_num + 1); + *index++ = static_cast(v_num + 3); + v_num += 2; + } + v_num += 2; + } + assert(ss->tendril_shadows.size() == shadow_count); + assert(index == (&ss->tendril_indices->elements[0]) + smoke_index_count); + assert(v + == (&ss->tendril_vertices->elements[0]) + (smoke_slice_count * 2)); + } + } + + // Now add fuses. + { + int fuse_count = 0; + for (auto&& i : fuses_) { + if (i->initial_position_set_) { + fuse_count++; + } + } + + if (fuse_count > 0) { + auto index_count = + static_cast(6 * (kFusePointCount - 1) * fuse_count); + auto vertex_count = + static_cast(2 * kFusePointCount * fuse_count); + + auto* ibuf = Object::NewDeferred(index_count); + + // Game thread is default owner; needs to be us until we hand it over. + ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->fuse_indices = Object::MakeRefCounted(ibuf); + auto* vbuf = + Object::NewDeferred(vertex_count); + + // Game thread is default owner; needs to be us until we hand it over. + vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); + ss->fuse_vertices = Object::MakeRefCounted(vbuf); + + uint16_t* index = &ss->fuse_indices->elements[0]; + VertexSimpleFull* v = &ss->fuse_vertices->elements[0]; + int p_num = 0; + uint16_t uv_inc = 65535 / (kFusePointCount - 1); + + for (auto&& i : fuses_) { + BGDynamicsFuseData& fuse(*i); + if (!fuse.initial_position_set_) continue; + + for (int j = 0; j < kFusePointCount - 1; j++) { + *index++ = static_cast(p_num); + *index++ = static_cast(p_num + 1); + *index++ = static_cast(p_num + 2); + + *index++ = static_cast(p_num + 2); + *index++ = static_cast(p_num + 1); + *index++ = static_cast(p_num + 3); + p_num += 2; + } + p_num += 2; + + uint16_t uv = 65535; + + Vector3f from_cam = (cam_pos_ - fuse.dyn_pts_[0]).Normalized() * 0.2f; + Vector3f side{}; + + // We push fuse points slightly towards cam so they're less likely to + // get occluded by stuff. + Vector3f cam_offs = {0.0f, 0.0f, 0.0f}; + + for (int j = 0; j < kFusePointCount; j++) { + if (j == 0) { + side = + Vector3f::Cross(from_cam, (fuse.dyn_pts_[1] - fuse.dyn_pts_[0])) + .Normalized() + * 0.03f; + } else { + side = Vector3f::Cross(from_cam, + (fuse.dyn_pts_[j] - fuse.dyn_pts_[j - 1])) + .Normalized() + * 0.03f; + } + + v->position[0] = fuse.dyn_pts_[j].x + side.x + cam_offs.x; + v->position[1] = fuse.dyn_pts_[j].y + side.y + cam_offs.y; + v->position[2] = fuse.dyn_pts_[j].z + side.z + cam_offs.z; + v->uv[0] = 0; + v->uv[1] = uv; + v++; + v->position[0] = fuse.dyn_pts_[j].x - side.x + cam_offs.x; + v->position[1] = fuse.dyn_pts_[j].y - side.y + cam_offs.y; + v->position[2] = fuse.dyn_pts_[j].z - side.z + cam_offs.z; + v->uv[0] = 65535; + v->uv[1] = uv; + v++; + uv -= uv_inc; + } + } + assert(v == &ss->fuse_vertices->elements[0] + vertex_count); + assert(index == &ss->fuse_indices->elements[0] + index_count); + } + } + + // Now sparks. + if (!spark_particles_) { + spark_particles_ = std::make_unique(); + } + spark_particles_->UpdateAndCreateSnapshot(&ss->spark_indices, + &ss->spark_vertices); + + return ss; +} // NOLINT (yes this should be shorter) + +void BGDynamicsServer::PushTooSlowCall() { + PushCall([this] { + if (chunk_count_ > 0 || tendril_count_thick_ > 0 + || tendril_count_thin_ > 0) { + // Ok lets kill a small percentage of our oldest chunks. + int killcount = static_cast(0.1f * chunks_.size()); + int killed = 0; + auto i = chunks_.begin(); + while (i != chunks_.end()) { + if (killed >= killcount) break; + auto i_next = i; + i_next++; + + // Kill it if its killable; otherwise move to next. + if ((**i).can_die()) { + delete (*i); + chunks_.erase(i); + chunk_count_--; + killed++; + } + i = i_next; + } + // ...and tendrils. + killcount = static_cast(0.2f * tendrils_.size()); + for (int j = 0; j < killcount; j++) { + Tendril* t = *tendrils_.begin(); + if (t->type_ == BGDynamicsTendrilType::kThinSmoke) { + tendril_count_thin_--; + } else { + tendril_count_thick_--; + } + assert(tendril_count_thin_ >= 0 && tendril_count_thick_ >= 0); + delete t; + tendrils_.erase(tendrils_.begin()); + } + } + }); +} + +void BGDynamicsServer::Step(StepData* step_data) { + assert(InBGDynamicsThread()); + assert(step_data); + + // Grab a ref to the raw StepData pointer we were passed.. we now own the + // data. + auto ref(Object::MakeRefCounted(step_data)); + + // Keep this in sync with the game thread's. + graphics_quality_ = g_graphics_server->graphics_quality_requested(); + + cam_pos_ = step_data->cam_pos; + + // Apply all step data sent to us for our entities. + for (auto&& i : step_data->shadow_step_data_) { + BGDynamicsShadowData* shadow{i.first}; + if (shadow) { + const ShadowStepData& shadow_step(i.second); + shadow->pos_worker = shadow_step.position; + } + } + for (auto&& i : step_data->volume_light_step_data_) { + BGDynamicsVolumeLightData* volume_light{i.first}; + if (volume_light) { + const VolumeLightStepData& volume_light_step(i.second); + volume_light->pos_worker = volume_light_step.pos; + volume_light->radius_worker = volume_light_step.radius; + volume_light->r_worker = volume_light_step.r; + volume_light->g_worker = volume_light_step.g; + volume_light->b_worker = volume_light_step.b; + } + } + for (auto&& i : step_data->fuse_step_data_) { + BGDynamicsFuseData* fuse{i.first}; + if (fuse) { + const FuseStepData& fuse_step(i.second); + fuse->transform_worker_ = fuse_step.transform; + fuse->have_transform_worker_ = fuse_step.have_transform; + fuse->length_worker_ = fuse_step.length; + } + } + + // Handle shadows first since they need to get back to the client + // as soon as possible. + UpdateShadows(); + + // Go ahead and run this step for all our existing stuff. + dJointGroupEmpty(ode_contact_group_); + UpdateFields(); + UpdateChunks(); + UpdateTendrils(); + UpdateFuses(); + + // Step the world. + dWorldQuickStep(ode_world_, kGameStepSeconds); + + // Now generate a snapshot of our state and send it to the game thread + // so they can draw us. + BGDynamicsDrawSnapshot* snapshot = CreateDrawSnapshot(); + g_game->PushCall([snapshot] { + snapshot->SetGameThreadOwnership(); + g_bg_dynamics->SetDrawSnapshot(snapshot); + }); + + time_ += kGameStepMilliseconds; // milliseconds per step + + // Give our collision cache a bit of processing time here and + // there to fill itself in slowly. + collision_cache_->Precalc(); + + // Job's done! + { + std::lock_guard lock(step_count_mutex_); + step_count_--; + } + assert(step_count_ >= 0); +} + +void BGDynamicsServer::PushStepCall(StepData* data) { + PushCall([this, data] { Step(data); }); +} + +void BGDynamicsServer::PushAddTerrainCall( + Object::Ref* collide_model) { + PushCall([this, collide_model] { + assert(InBGDynamicsThread()); + assert(collide_model != nullptr); + + // Make sure its loaded (might not be when we get it). + (**collide_model).Load(); + + // (the terrain now owns the ref pointer passed in) + terrains_.push_back(new Terrain(this, collide_model)); + + // Rebuild geom list from our present terrains. + std::vector geoms; + geoms.reserve(terrains_.size()); + for (auto&& i : terrains_) { + geoms.push_back(i->geom()); + } + height_cache_->SetGeoms(geoms); + collision_cache_->SetGeoms(geoms); + + // Reset our chunks whenever anything changes. + Clear(); + }); +} + +void BGDynamicsServer::UpdateFields() { + auto i = fields_.begin(); + while (i != fields_.end()) { + Field& f(**i); + + // First off, kill this field if its time has come. + { + bool kill = false; + if (static_cast(time_ - f.birth_time()) > f.lifespan()) { + kill = true; + } + if (kill) { + auto i_next = i; + i_next++; + delete *i; + fields_.erase(i); + i = i_next; + continue; + } + } + + // Update its distortion amount based on age (get an age in 0-1). + float age = static_cast(time() - f.birth_time()) / f.lifespan(); + + float time_scale = 1.3f; + float start_mag = 0.0f; + float suck_mag = -0.4f; + float suck_end_time = 0.05f * time_scale; + float bulge_mag = 0.7f; + float bulge_end_time = 0.2f * time_scale; + float suck_2_mag = -0.05f; + float suck_2_end_time = 0.4f * time_scale; + float end_mag = 0.0f; + + // Ramp negative from 0 to 0.3. + if (age < suck_end_time) { + f.set_amt(start_mag + + (suck_mag - start_mag) + * Utils::SmoothStep(0.0f, suck_end_time, age)); + } else if (age < bulge_end_time) { + f.set_amt(suck_mag + + (bulge_mag - suck_mag) + * Utils::SmoothStep(suck_end_time, bulge_end_time, age)); + } else if (age < suck_2_end_time) { + f.set_amt( + bulge_mag + + (suck_2_mag - bulge_mag) + * Utils::SmoothStep(bulge_end_time, suck_2_end_time, age)); + } else { + f.set_amt(suck_2_mag + + (end_mag - suck_2_mag) + * Utils::SmoothStep(suck_2_end_time, 1.0f, age)); + } + f.set_amt(f.amt() * f.mag()); + i++; + } +} + +void BGDynamicsServer::TerrainCollideCallback(void* data, dGeomID geom1, + dGeomID geom2) { + auto* dyn = static_cast(data); + dContact contact[kMaxBGDynamicsContacts]; // max contacts per box-box + + if (int numc = dCollide(geom1, geom2, kMaxBGDynamicsContacts, + &contact[0].geom, sizeof(dContact))) { + BGDynamicsChunkType type = dyn->cb_type_; + dBodyID body = dyn->cb_body_; + float f_mult = type == BGDynamicsChunkType::kIce ? 0.04f : 1.0f; + + // Slime chunks just slow down on collisions. + if (type == BGDynamicsChunkType::kSlime) { + const dReal* vel = dBodyGetLinearVel(body); + dBodySetLinearVel(body, vel[0] * 0.1f, vel[1] * 0.1f, vel[2] * 0.1f); + vel = dBodyGetAngularVel(body); + dBodySetAngularVel(body, vel[0] * 0.8f, vel[1] * 0.8f, vel[2] * 0.8f); + } else { + // Only look at some contacts. + // If we restrict the number of contacts returned we seem to get + // lopsided contacts and failing collisions, but if we just increment + // through all contacts at > 1 it seems to work ok. + int contact_incr = 1; + if (numc > 4) { + contact_incr = 2; + if (numc > 9) { + contact_incr = 3; + if (numc > 14) { + contact_incr = 4; + } + } + } + + for (int i = 0; i < numc; i += contact_incr) { + // NOLINTNEXTLINE + contact[i].surface.mode = dContactBounce | dContactSoftCFM + | dContactSoftERP | dContactApprox1; + contact[i].surface.mu2 = 0; + contact[i].surface.bounce_vel = 0.1f; + contact[i].surface.mu = 0.5f * dyn->debris_friction_ * f_mult; + contact[i].surface.bounce = 0.4f; + contact[i].surface.soft_cfm = dyn->cb_cfm_; + contact[i].surface.soft_erp = dyn->cb_erp_; + dJointID constraint = dJointCreateContact( + dyn->ode_world_, dyn->ode_contact_group_, contact + i); + dJointAttach(constraint, body, nullptr); + } + } + } +} + +void BGDynamicsServer::UpdateChunks() { + dReal stiffness = 1000.0f; + dReal damping = 10.0f; + dReal erp{}, cfm{}; + CalcERPCFM(stiffness, damping, &erp, &cfm); + cb_erp_ = erp; + cb_cfm_ = cfm; + + // We don't use a space since we don't want everything to intercollide; + // rather we explicitly test everything against our terrain objects; + // this keeps things simple. + + for (auto i = chunks_.begin(); i != chunks_.end();) { + Chunk& c(**i); + + // first off, kill this chunk if its time has come + { + bool kill = false; + if (static_cast(time_ - c.birth_time_) > c.lifespan_) { + kill = true; + } + + // If we've fallen off the level. + if (c.dynamic()) { + const dReal* pos = dGeomGetPosition(c.geom_); + if (pos[1] < debris_kill_height_) kill = true; + } + if (kill) { + auto i_next = i; + i_next++; + delete *i; + chunks_.erase(i); + chunk_count_--; + assert(chunk_count_ >= 0); + i = i_next; + continue; + } + } + BGDynamicsChunkType type = c.type(); + + // Some spark-specific stuff. + if (type == BGDynamicsChunkType::kSpark) { + if (RandomFloat() < 0.1f) { + float fs = c.flicker_scale_; + c.flicker_ = fs * RandomFloat() + (1.0f - fs) * 0.8f; + } + } else if (type == BGDynamicsChunkType::kSweat) { + // Some sweat-specific stuff. + if (RandomFloat() < 0.25f) { + c.flicker_ = RandomFloat(); + } + } + + // Most stuff only applies to dynamic chunks. + if (c.dynamic()) { + dGeomID geom = c.geom(); + dBodyID body = c.body(); + if (type == BGDynamicsChunkType::kSlime) { + // add some drag on slime chunks + const dReal* vel = dBodyGetLinearVel(body); + dBodySetLinearVel(body, vel[0] * 0.99f, vel[1] * 0.99f, vel[2] * 0.99f); + } + if (type == BGDynamicsChunkType::kSpark) { + // Add some drag on spark. + const dReal* vel = dBodyGetLinearVel(body); + + // Also add a bit of upward to counteract gravity. + float vel_squared = vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]; + + // Slow down fast if we're going fast. + // Otherwise slow down more gradually. + if (vel_squared > 14) { + dBodySetLinearVel(body, vel[0] * 0.94f, 0.13f + vel[1] * 0.94f, + vel[2] * 0.94f); + } else { + dBodySetLinearVel(body, vel[0] * 0.99f, 0.07f + vel[1] * 0.99f, + vel[2] * 0.99f); + } + } else if (type == BGDynamicsChunkType::kSweat) { + // Add some drag on sweat. + const dReal* vel = dBodyGetLinearVel(body); + + // Also add a bit of upward to counteract gravity. + float vel_squared = vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]; + + // Slow down fast if we're going fast. + // Otherwise slow down more gradually. + if (vel_squared > 14) { + dBodySetLinearVel(body, vel[0] * 0.93f, 0.13f + vel[1] * 0.93f, + vel[2] * 0.93f); + } else { + dBodySetLinearVel(body, vel[0] * 0.97f, 0.11f + vel[1] * 0.97f, + vel[2] * 0.97f); + } + } else if (type == BGDynamicsChunkType::kSplinter) { + // Add some drag on slime chunks. + const dReal* vel = dBodyGetLinearVel(body); + dBodySetLinearVel(body, vel[0] * 0.995f, vel[1] * 0.995f, + vel[2] * 0.995f); + vel = dBodyGetAngularVel(body); + dBodySetAngularVel(body, vel[0] * 0.995f, vel[1] * 0.995f, + vel[2] * 0.995f); + } else { + const dReal* vel = dBodyGetAngularVel(body); + if (vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2] > 500) { + // Drastic slowdown for super fast stuff. + dBodySetAngularVel(body, vel[0] * 0.75f, vel[1] * 0.75f, + vel[2] * 0.75f); + } else { + dBodySetAngularVel(body, vel[0] * 0.995f, vel[1] * 0.995f, + vel[2] * 0.995f); + } + } + + // If this chunk is disabled, we don't need to do anything + // (since no terrain ever moves to wake us back up). + // Also we skip sweat since that neither casts shadows or collides. + if (dBodyIsEnabled(body) && type != BGDynamicsChunkType::kSweat) { + // Move our shadow ray to where we are and reset our shadow length. + const dReal* pos = dGeomGetPosition(geom); + // Update shadow dist. + c.shadow_dist_ = pos[1] - height_cache_->Sample(Vector3f(pos)); + cb_type_ = type; + cb_body_ = body; + collision_cache_->CollideAgainstGeom(geom, this, + TerrainCollideCallback); + // Tell it to update any tendril it might have. + c.UpdateTendril(); + } + } + i++; + } +} + +void BGDynamicsServer::UpdateShadows() { + // First go through and calculate distances for all shadows. + for (auto&& s : shadows_) { + float shadow_dist = s->pos_worker.y - height_cache_->Sample(s->pos_worker); + + // Update scale/density based on these values. + // Negative shadow_dist means some object is in front of our + // shadow-caster. In this case lets keep our scale the same as it would + // have been at zero dist but fade our density out gradually as we become + // more deeply submerged. + if (shadow_dist < 0.0f) { + s->shadow_scale_worker = 1.0f; + s->shadow_density_worker = + 1.0f - std::min(1.0f, -shadow_dist / kShadowOccludeDistance); + } else { + // Normal non-submerged shadow. + float max_scale = 1.0f + (kMaxShadowScale - 1.0f) * s->height_scaling; + float scale = + 1.0f + + std::max(0.0f, std::min(1.0f, (shadow_dist / kMaxShadowGrowDist)) + * (max_scale - 1.0f)); + s->shadow_scale_worker = scale; + s->shadow_density_worker = + 1.0f + - 0.7f + * std::max(0.0f, + std::min(1.0f, (shadow_dist / kMaxShadowGrowDist))); + } + } + + // Now plop this back onto the client side all at once. + { + BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_shadow_list_lock); + { + std::lock_guard lock(shadow_list_mutex_); + for (auto&& s : shadows_) { + s->UpdateClientData(); + } + } + BA_DEBUG_TIME_CHECK_END(bg_dynamic_shadow_list_lock, 10); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/bg/bg_dynamics_server.h b/src/ballistica/dynamics/bg/bg_dynamics_server.h new file mode 100644 index 00000000..d8d23ee6 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_server.h @@ -0,0 +1,170 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ + +#include +#include +#include +#include + +#include "ballistica/core/module.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/math/vector3f.h" +#include "ode/ode.h" + +namespace ballistica { + +class BGDynamicsServer : public Module { + public: + struct Particle { + float x; + float y; + float z; + // Note that velocities here are in units-per-step (avoids a mult). + float vx; + float vy; + float vz; + float r; + float g; + float b; + float a; + float life; + float d_life; + float flicker; + float flicker_scale; + float size; + float d_size; + }; + + class ParticleSet { + public: + std::vector particles[2]; + int current_set; + ParticleSet() : current_set(0) {} + void Emit(const Vector3f& pos, const Vector3f& vel, float r, float g, + float b, float a, float dlife, float size, float d_size, + float flicker); + void UpdateAndCreateSnapshot(Object::Ref* index_buffer, + Object::Ref* buffer); + }; + struct ShadowStepData { + Vector3f position; + }; + struct VolumeLightStepData { + Vector3f pos{}; + float radius{}; + float r{}; + float g{}; + float b{}; + }; + struct FuseStepData { + Matrix44f transform{}; + bool have_transform{}; + float length{}; + }; + class StepData : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kBGDynamics; + } + Vector3f cam_pos{0.0f, 0.0f, 0.0f}; + + // Basically a bit list of pointers to the current set of + // shadows/volumes/fuses and client values for them. + std::vector > + shadow_step_data_; + std::vector > + volume_light_step_data_; + std::vector > fuse_step_data_; + }; + + explicit BGDynamicsServer(Thread* thread); + ~BGDynamicsServer() override; + auto time() const -> uint32_t { return time_; } + auto graphics_quality() const -> GraphicsQuality { return graphics_quality_; } + + void PushAddVolumeLightCall(BGDynamicsVolumeLightData* volume_light_data); + void PushRemoveVolumeLightCall(BGDynamicsVolumeLightData* volume_light_data); + void PushAddFuseCall(BGDynamicsFuseData* fuse_data); + void PushRemoveFuseCall(BGDynamicsFuseData* fuse_data); + void PushAddShadowCall(BGDynamicsShadowData* shadow_data); + void PushRemoveShadowCall(BGDynamicsShadowData* shadow_data); + void PushAddTerrainCall(Object::Ref* collide_model); + void PushRemoveTerrainCall(CollideModelData* collide_model); + void PushEmitCall(const BGDynamicsEmission& def); + auto spark_particles() const -> ParticleSet* { + return spark_particles_.get(); + } + auto step_count() const -> int { return step_count_; } + + private: + class Terrain; + class Chunk; + class Field; + class Tendril; + class TendrilController; + + static void TerrainCollideCallback(void* data, dGeomID o1, dGeomID o2); + + void Emit(const BGDynamicsEmission& def); + void PushStepCall(StepData* data); + void Step(StepData* data); + void PushTooSlowCall(); + void PushSetDebrisFrictionCall(float friction); + void PushSetDebrisKillHeightCall(float height); + void Clear(); + void UpdateFields(); + void UpdateChunks(); + void UpdateTendrils(); + void UpdateFuses(); + void UpdateShadows(); + auto CreateDrawSnapshot() -> BGDynamicsDrawSnapshot*; + BGDynamicsChunkType cb_type_ = BGDynamicsChunkType::kRock; + dBodyID cb_body_{}; + float cb_cfm_{0.0f}; + float cb_erp_{0.0f}; + + // FIXME: We're assuming at the moment + // that collide-models passed to this thread never get deallocated. ew. + MeshIndexedSmokeFull* tendrils_smoke_mesh_{nullptr}; + MeshIndexedSimpleFull* fuses_mesh_{nullptr}; + SpriteMesh* shadows_mesh_{nullptr}; + SpriteMesh* lights_mesh_{nullptr}; + SpriteMesh* sparks_mesh_{nullptr}; + int miss_count_{0}; + Vector3f cam_pos_{0.0f, 0.0f, 0.0f}; + std::vector terrains_; + std::vector shadows_; + std::vector volume_lights_; + std::vector fuses_; + dWorldID ode_world_{nullptr}; + dJointGroupID ode_contact_group_{nullptr}; + + // Held by the dynamics module when changing any of these lists. + // Should be grabbed by a client if they need to access the list safely. + std::mutex shadow_list_mutex_; + std::mutex volume_light_list_mutex_; + std::mutex fuse_list_mutex_; + int step_count_{0}; + std::mutex step_count_mutex_; + std::unique_ptr spark_particles_{nullptr}; + std::list chunks_; + std::list fields_; + std::list tendrils_; + int tendril_count_thick_{0}; + int tendril_count_thin_{0}; + int chunk_count_{0}; + std::unique_ptr height_cache_; + std::unique_ptr collision_cache_; + uint32_t time_{0}; // Internal time step. + float debris_friction_{1.0f}; + float debris_kill_height_{-50.0f}; + GraphicsQuality graphics_quality_{GraphicsQuality::kLow}; + friend class BGDynamics; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_shadow.cc b/src/ballistica/dynamics/bg/bg_dynamics_shadow.cc new file mode 100644 index 00000000..e7d736bf --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_shadow.cc @@ -0,0 +1,53 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" + +#include "ballistica/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/dynamics/bg/bg_dynamics_shadow_data.h" +#include "ballistica/graphics/graphics.h" + +namespace ballistica { + +BGDynamicsShadow::BGDynamicsShadow(float height_scaling) { + assert(InGameThread()); + + // allocate our shadow data.. we'll pass this to the BGDynamics thread + // and it'll then own it + data_ = new BGDynamicsShadowData(height_scaling); + assert(g_bg_dynamics_server); + g_bg_dynamics_server->PushAddShadowCall(data_); +} + +BGDynamicsShadow::~BGDynamicsShadow() { + assert(InGameThread()); + assert(g_bg_dynamics_server); + + // let the data know the client side is dead + // so we're no longer included in step messages + // (since by the time the worker gets the the data will be gone) + data_->client_dead = true; + g_bg_dynamics_server->PushRemoveShadowCall(data_); +} + +void BGDynamicsShadow::SetPosition(const Vector3f& pos) { + assert(InGameThread()); + data_->pos_client = pos; +} + +auto BGDynamicsShadow::GetPosition() const -> const Vector3f& { + assert(InGameThread()); + return data_->pos_client; +} + +void BGDynamicsShadow::GetValues(float* scale, float* density) const { + assert(InGameThread()); + assert(scale); + assert(density); + + *scale = data_->shadow_scale_client; + *density = data_->shadow_density_client + * g_graphics->GetShadowDensity( + data_->pos_client.x, data_->pos_client.y, data_->pos_client.z); +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/bg/bg_dynamics_shadow.h b/src/ballistica/dynamics/bg/bg_dynamics_shadow.h new file mode 100644 index 00000000..e50ffb9f --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_shadow.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" + +namespace ballistica { + +// A utility class for client use which uses ray-testing and +// BG collision terrains to create a variably dense/soft shadow +// based on how high it is above terrain. +// Clients should give their current position information to the shadow +// at update time and then at render time it'll be all set to go. +// (shadows update in the bg dynamics stepping process) +class BGDynamicsShadow { + public: + explicit BGDynamicsShadow(float height_scaling = 1.0f); + ~BGDynamicsShadow(); + void SetPosition(const Vector3f& pos); + auto GetPosition() const -> const Vector3f&; + + // Return scale and density for the shadow. + // this also takes into account the height based shadow density + // (g_graphics->GetShadowDensity()) so you don't have to. + void GetValues(float* scale, float* density) const; + + private: + BGDynamicsShadowData* data_{}; + BA_DISALLOW_CLASS_COPIES(BGDynamicsShadow); +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_shadow_data.h b/src/ballistica/dynamics/bg/bg_dynamics_shadow_data.h new file mode 100644 index 00000000..1ec313ea --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_shadow_data.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ + +namespace ballistica { + +struct BGDynamicsShadowData { + explicit BGDynamicsShadowData(float height_scaling) + : height_scaling(height_scaling) {} + + void UpdateClientData() { + // Copy data over with a bit of smoothing + // (so our shadow doesn't jump instantly when we go over and edge/etc.) + float smoothing{0.8f}; + shadow_scale_client = smoothing * shadow_scale_client + + (1.0f - smoothing) * shadow_scale_worker; + shadow_density_client = smoothing * shadow_density_client + + (1.0f - smoothing) * shadow_density_worker; + } + + void Synchronize() { pos_worker = pos_client; } + + bool client_dead{}; + float height_scaling{}; + + // For use by worker: + + // position value owned by the client (write-only). + Vector3f pos_client{0.0f, 0.0f, 0.0f}; + + // Position value owned by the worker thread (read-only). + Vector3f pos_worker{0.0f, 0.0f, 0.0f}; + + // Calculated values owned by the worker thread (write-only). + float shadow_scale_worker{1.0f}; + float shadow_density_worker{0.0f}; + + // Result values owned by the client (read-only). + float shadow_scale_client{1.0f}; + float shadow_density_client{0.0f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_volume_light.cc b/src/ballistica/dynamics/bg/bg_dynamics_volume_light.cc new file mode 100644 index 00000000..768b2d3b --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_volume_light.cc @@ -0,0 +1,47 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/bg/bg_dynamics_volume_light.h" + +#include "ballistica/dynamics/bg/bg_dynamics_volume_light_data.h" + +namespace ballistica { + +BGDynamicsVolumeLight::BGDynamicsVolumeLight() { + assert(InGameThread()); + // allocate our light data.. we'll pass this to the BGDynamics thread + // and it'll then own it + data_ = new BGDynamicsVolumeLightData(); + assert(g_bg_dynamics_server); + g_bg_dynamics_server->PushAddVolumeLightCall(data_); +} + +BGDynamicsVolumeLight::~BGDynamicsVolumeLight() { + assert(InGameThread()); + + // let the data know the client side is dead + // so we're no longer included in step messages + // (since by the time the worker gets the the data will be gone) + data_->client_dead = true; + + assert(g_bg_dynamics_server); + g_bg_dynamics_server->PushRemoveVolumeLightCall(data_); +} + +void BGDynamicsVolumeLight::SetPosition(const Vector3f& pos) { + assert(InGameThread()); + data_->pos_client = pos; +} + +void BGDynamicsVolumeLight::SetRadius(float radius) { + assert(InGameThread()); + data_->radius_client = radius; +} + +void BGDynamicsVolumeLight::SetColor(float r, float g, float b) { + assert(InGameThread()); + data_->r_client = r; + data_->g_client = g; + data_->b_client = b; +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/bg/bg_dynamics_volume_light.h b/src/ballistica/dynamics/bg/bg_dynamics_volume_light.h new file mode 100644 index 00000000..20ebc2db --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_volume_light.h @@ -0,0 +1,25 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ + +#include "ballistica/core/object.h" + +namespace ballistica { + +// Client-controlled lights for bg smoke. +class BGDynamicsVolumeLight : public Object { + public: + BGDynamicsVolumeLight(); + ~BGDynamicsVolumeLight() override; + void SetPosition(const Vector3f& pos); + void SetRadius(float radius); + void SetColor(float r, float g, float b); + + private: + BGDynamicsVolumeLightData* data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h b/src/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h new file mode 100644 index 00000000..61e31941 --- /dev/null +++ b/src/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ +#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ + +#include "ballistica/dynamics/bg/bg_dynamics_server.h" + +namespace ballistica { + +struct BGDynamicsVolumeLightData { + bool client_dead{}; + + // Position value owned by the client. + Vector3f pos_client{0.0f, 0.0f, 0.0f}; + float radius_client{}; + float r_client{}; + float g_client{}; + float b_client{}; + + // Position value owned by the worker thread. + Vector3f pos_worker{0.0f, 0.0f, 0.0f}; + float radius_worker{}; + float r_worker{}; + float g_worker{}; + float b_worker{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ diff --git a/src/ballistica/dynamics/collision.h b/src/ballistica/dynamics/collision.h new file mode 100644 index 00000000..8f93fc47 --- /dev/null +++ b/src/ballistica/dynamics/collision.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_COLLISION_H_ +#define BALLISTICA_DYNAMICS_COLLISION_H_ + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/core/object.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ode/ode.h" + +namespace ballistica { + +// Stores info about an occurring collision. +// Note than just because a collision exists between two parts doesn't mean +// they're physically colliding in the simulation. It is just a shortcut to +// determine what behavior, if any, exists between two parts which are currently +// overlapping in the simulation. +class Collision : public Object { + public: + explicit Collision(Scene* scene) : src_context(scene), dst_context(scene) {} + int claim_count{}; // Used when checking for out-of-date-ness. + bool collide{true}; + int contact_count{}; // Current number of contacts. + float depth{}; // Current collision depth. + float x{}; + float y{}; + float z{}; + float impact{}; + float skid{}; + float roll{}; + Object::WeakRef src_part; // Ref to make sure still alive. + Object::WeakRef dst_part; // Ref to make sure still alive. + int body_id_1{-1}; + int body_id_2{-1}; + std::vector collide_feedback; + MaterialContext src_context; + MaterialContext dst_context; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_COLLISION_H_ diff --git a/src/ballistica/dynamics/collision_cache.cc b/src/ballistica/dynamics/collision_cache.cc new file mode 100644 index 00000000..63ab02b3 --- /dev/null +++ b/src/ballistica/dynamics/collision_cache.cc @@ -0,0 +1,355 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/collision_cache.h" + +#include + +#include "ballistica/graphics/component/simple_component.h" +#include "ode/ode_collision_kernel.h" +#include "ode/ode_collision_space_internal.h" +#include "ode/ode_collision_util.h" +#include "ode/ode_objects_private.h" + +namespace ballistica { + +CollisionCache::CollisionCache() + : dirty_(true), + shadow_ray_(nullptr), + x_min_(-1), + x_max_(1), + y_min_(-1), + y_max_(1), + z_min_(-1), + z_max_(1) { + grid_width_ = 1; + grid_height_ = 1; + test_box_ = dCreateBox(nullptr, 1, 1, 1); +} + +CollisionCache::~CollisionCache() { + if (shadow_ray_) { + dGeomDestroy(shadow_ray_); + } + dGeomDestroy(test_box_); +} + +void CollisionCache::SetGeoms(const std::vector& geoms) { + dirty_ = true; + geoms_ = geoms; +} + +void CollisionCache::Draw(FrameDef* frame_def) { + if (cells_.empty()) { + return; + } + SimpleComponent c(frame_def->beauty_pass()); + c.SetTransparent(true); + c.SetColor(0, 1, 0, 0.1f); + float cell_width = (1.0f / static_cast(grid_width_)); + float cell_height = (1.0f / static_cast(grid_height_)); + c.PushTransform(); + c.Translate((x_min_ + x_max_) * 0.5f, 0, (z_min_ + z_max_) * 0.5f); + c.Scale(x_max_ - x_min_, 1, z_max_ - z_min_); + c.PushTransform(); + c.Scale(1, 0.01f, 1); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + c.Translate(-0.5f + 0.5f * cell_width, 0, -0.5f + 0.5f * cell_height); + for (int x = 0; x < grid_width_; x++) { + for (int z = 0; z < grid_height_; z++) { + int cell_index = z * grid_width_ + x; + assert(cell_index >= 0 && cell_index < static_cast(glow_.size())); + if (glow_[cell_index]) { + c.SetColor(1, 1, 1, 0.2f); + } else { + c.SetColor(0, 0, 1, 0.2f); + } + c.PushTransform(); + c.Translate(static_cast(x) / static_cast(grid_width_), + cells_[cell_index].height_confirmed_collide_, + static_cast(z) / static_cast(grid_height_)); + c.Scale(0.95f * cell_width, 0.01f, 0.95f * cell_height); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + if (glow_[cell_index]) { + c.SetColor(1, 1, 1, 0.2f); + } else { + c.SetColor(1, 0, 0, 0.2f); + } + c.PushTransform(); + c.Translate(static_cast(x) / static_cast(grid_width_), + cells_[cell_index].height_confirmed_empty_, + static_cast(z) / static_cast(grid_height_)); + c.Scale(0.95f * cell_width, 0.01f, 0.95f * cell_height); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + glow_[cell_index] = 0; + } + } + c.PopTransform(); + c.Submit(); + + if (explicit_bool(false)) { + SimpleComponent c2(frame_def->overlay_3d_pass()); + c2.SetTransparent(true); + c2.SetColor(1, 0, 0, 1.0f); + float cell_width2 = (1.0f / static_cast(grid_width_)); + float cell_height2 = (1.0f / static_cast(grid_height_)); + c2.PushTransform(); + c2.Translate((x_min_ + x_max_) * 0.5f, 0, (z_min_ + z_max_) * 0.5f); + c2.Scale(x_max_ - x_min_, 1, z_max_ - z_min_); + c2.PushTransform(); + c2.Scale(1, 0.01f, 1); + c2.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c2.PopTransform(); + c2.Translate(-0.5f + 0.5f * cell_width2, 0, -0.5f + 0.5f * cell_height2); + for (int x = 0; x < grid_width_; x++) { + for (int z = 0; z < grid_height_; z++) { + int cell_index = z * grid_width_ + x; + assert(cell_index >= 0 && cell_index < static_cast(glow_.size())); + if (glow_[cell_index]) { + c2.SetColor(1, 1, 1, 0.2f); + } else { + c2.SetColor(1, 0, 0, 0.2f); + } + c2.PushTransform(); + c2.Translate(static_cast(x) / static_cast(grid_width_), + cells_[cell_index].height_confirmed_empty_, + static_cast(z) / static_cast(grid_height_)); + c2.Scale(0.95f * cell_width2, 0.01f, 0.95f * cell_height2); + c2.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c2.PopTransform(); + if (glow_[cell_index]) { + c2.SetColor(1, 1, 1, 0.2f); + } else { + c2.SetColor(0, 0, 1, 0.2f); + } + c2.PushTransform(); + c2.Translate(static_cast(x) / static_cast(grid_width_), + cells_[cell_index].height_confirmed_collide_, + static_cast(z) / static_cast(grid_height_)); + c2.Scale(0.95f * cell_width2, 0.01f, 0.95f * cell_height2); + c2.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c2.PopTransform(); + + glow_[cell_index] = 0; + } + } + c2.PopTransform(); + c2.Submit(); + } +} + +void CollisionCache::Precalc() { + Update(); + + if (precalc_index_ >= cells_.size()) { + precalc_index_ = 0; // Loop. + } + + auto x = static_cast(precalc_index_ % grid_width_); + auto z = static_cast(precalc_index_ / grid_width_); + assert(x >= 0 && x < grid_width_); + assert(z >= 0 && z < grid_height_); + TestCell(precalc_index_++, x, z); +} + +void CollisionCache::CollideAgainstGeom(dGeomID g1, void* data, + dNearCallback* callback) { + // Update bounds, test for quick out against our height map, + // and proceed to a full test on a positive result. + g1->recomputeAABB(); + + if (dirty_) Update(); + + // Do a quick out if its not within our cache bounds at all. + dReal* bounds1 = g1->aabb; + if (bounds1[0] > x_max_ || bounds1[1] < x_min_ || bounds1[2] > y_max_ + || bounds1[3] < y_min_ || bounds1[4] > z_max_ || bounds1[5] < z_min_) { + return; + } + + int x_min = static_cast(static_cast(grid_width_) + * ((g1->aabb[0] - x_min_) / (x_max_ - x_min_))); + x_min = std::max(0, std::min(grid_width_ - 1, x_min)); + int z_min = static_cast(static_cast(grid_height_) + * ((g1->aabb[4] - z_min_) / (z_max_ - z_min_))); + z_min = std::max(0, std::min(grid_height_ - 1, z_min)); + + int x_max = static_cast(static_cast(grid_width_) + * ((g1->aabb[1] - x_min_) / (x_max_ - x_min_))); + x_max = std::max(0, std::min(grid_width_ - 1, x_max)); + int z_max = static_cast(static_cast(grid_height_) + * ((g1->aabb[5] - z_min_) / (z_max_ - z_min_))); + z_max = std::max(0, std::min(grid_height_ - 1, z_max)); + + // If all cells are confirmed empty to the bottom of our AABB, we're done. + bool possible_hit = false; + for (int z = z_min; z <= z_max; z++) { + auto cell_index = static_cast(z * grid_width_); + for (int x = x_min; x <= x_max; x++) { + if (bounds1[2] <= cells_[cell_index + x].height_confirmed_empty_) { + possible_hit = true; + break; + } + } + if (possible_hit) { + break; + } + } + if (!possible_hit) { + return; + } + + // Ok looks like we need to run collisions. + int t_count = static_cast(geoms_.size()); + for (int i = 0; i < t_count; i++) { + dxGeom* g2 = geoms_[i]; + collideAABBs(g1, g2, data, callback); + } + + // While we're here, lets run one pass of tests on these cells to zero in on + // the actual collide/empty cutoff. + for (int z = z_min; z <= z_max; z++) { + int base_index = z * grid_width_; + for (int x = x_min; x <= x_max; x++) { + int cell_index = base_index + x; + assert(cell_index >= 0); + TestCell(static_cast(cell_index), x, z); + } + } +} + +void CollisionCache::TestCell(size_t cell_index, int x, int z) { + int t_count = static_cast(geoms_.size()); + float top = cells_[cell_index].height_confirmed_empty_; + + // Midway point. + float bottom = (cells_[cell_index].height_confirmed_collide_ + top) * 0.5f; + float height = top - bottom; + + // Don't wanna test with too thin a box.. may miss stuff. + float box_height = std::max(1.0f, height); + if (height > 0.01f) { + glow_[cell_index] = 1; + + dGeomSetPosition(test_box_, + x_min_ + cell_width_ * (0.5f + static_cast(x)), + bottom + box_height * 0.5f, + z_min_ + cell_height_ * (0.5f + static_cast(z))); + dGeomBoxSetLengths(test_box_, cell_width_, box_height, cell_height_); + + dContact contact[1]; + bool collided = false; + + // See if we collide with *any* terrain. + for (int i = 0; i < t_count; i++) { + if (dCollide(test_box_, geoms_[i], 1, &contact[0].geom, + sizeof(dContact))) { + collided = true; + break; + } + } + + // Ok, we collided. We can move our confirmed collide floor up to + // our bottom. + if (collided) { + cells_[cell_index].height_confirmed_collide_ = + std::max(cells_[cell_index].height_confirmed_collide_, bottom); + } else { + // Didn't collide. Move confirmed empty region to our bottom. + cells_[cell_index].height_confirmed_empty_ = + std::min(cells_[cell_index].height_confirmed_empty_, bottom); + } + // This shouldn' happen but just in case. + cells_[cell_index].height_confirmed_empty_ = + std::max(cells_[cell_index].height_confirmed_empty_, + cells_[cell_index].height_confirmed_collide_); + } +} + +void CollisionCache::CollideAgainstSpace(dSpaceID space, void* data, + dNearCallback* callback) { + // We handle our own testing against trimeshes so we can bring our fancy + // caching into play. + if (!geoms_.empty()) { + // Intersect all geoms in the space against all terrains. + for (dxGeom* g1 = space->first; g1; g1 = g1->next) { + CollideAgainstGeom(g1, data, callback); + } + } +} + +void CollisionCache::Update() { + if (!dirty_) { + return; + } + + // Calc our full dimensions. + if (geoms_.empty()) { + x_min_ = -1; + x_max_ = 1; + y_min_ = -1; + y_max_ = 1; + z_min_ = -1; + z_max_ = 1; + } else { + auto i = geoms_.begin(); + dReal aabb[6]; + dGeomGetAABB(*i, aabb); + float x = aabb[0]; + float X = aabb[1]; + float y = aabb[2]; + float Y = aabb[3]; + float z = aabb[4]; + float Z = aabb[5]; + for (i++; i != geoms_.end(); i++) { + dGeomGetAABB(*i, aabb); + if (aabb[0] < x) x = aabb[0]; + if (aabb[1] > X) X = aabb[1]; + if (aabb[2] < y) y = aabb[2]; + if (aabb[3] > Y) Y = aabb[3]; + if (aabb[4] < z) z = aabb[4]; + if (aabb[5] > Z) Z = aabb[5]; + } + float buffer = 0.3f; + x_min_ = x - buffer; + x_max_ = X + buffer; + y_min_ = y - buffer; + y_max_ = Y + buffer; + z_min_ = z - buffer; + z_max_ = Z + buffer; + } + + // (Re)create our shadow ray with the new dimensions. + if (shadow_ray_) { + dGeomDestroy(shadow_ray_); + } + shadow_ray_ = dCreateRay(nullptr, y_max_ - y_min_); + dGeomRaySet(shadow_ray_, 0, 0, 0, 0, -1, 0); // aim straight down + dGeomRaySetClosestHit(shadow_ray_, true); + + // Update/clear our cell grid based on our dimensions. + grid_width_ = + std::max(1, std::min(256, static_cast((x_max_ - x_min_) * 1.3f))); + grid_height_ = + std::max(1, std::min(256, static_cast((z_max_ - z_min_) * 1.3f))); + + assert(grid_width_ >= 0 && grid_height_ >= 0); + auto cell_count = static_cast(grid_width_ * grid_height_); + cells_.clear(); + cells_.resize(cell_count); + for (uint32_t i = 0; i < cell_count; i++) { + cells_[i].height_confirmed_empty_ = y_max_; + cells_[i].height_confirmed_collide_ = y_min_; + } + cell_width_ = (x_max_ - x_min_) / static_cast(grid_width_); + cell_height_ = (z_max_ - z_min_) / static_cast(grid_height_); + glow_.clear(); + glow_.resize(cell_count); + memset(&glow_[0], 0, cell_count); + precalc_index_ = 0; + dirty_ = false; +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/collision_cache.h b/src/ballistica/dynamics/collision_cache.h new file mode 100644 index 00000000..dffb3c5c --- /dev/null +++ b/src/ballistica/dynamics/collision_cache.h @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_COLLISION_CACHE_H_ +#define BALLISTICA_DYNAMICS_COLLISION_CACHE_H_ + +#include + +#include "ballistica/ballistica.h" +#include "ode/ode.h" + +namespace ballistica { + +// Given geoms, creates/samples a height map on the fly +// which can be used for very fast AABB tests against the geometry. +class CollisionCache { + public: + CollisionCache(); + ~CollisionCache(); + + // If returns true, the provided AABB *may* intersect the geoms. + void SetGeoms(const std::vector& geoms); + void Draw(FrameDef* f); // For debugging. + void CollideAgainstSpace(dSpaceID space, void* data, dNearCallback* callback); + void CollideAgainstGeom(dGeomID geom, void* data, dNearCallback* callback); + + // Call this periodically (once per cycle or so) to slowly fill in + // the cache so there's less to do during spurts of activity; + void Precalc(); + + private: + void TestCell(size_t cell_index, int x, int z); + void Update(); + uint32_t precalc_index_{}; + std::vector geoms_; + struct Cell { + float height_confirmed_empty_; + float height_confirmed_collide_; + }; + std::vector cells_; + std::vector glow_; + bool dirty_; + dGeomID shadow_ray_; + dGeomID test_box_; + int grid_width_; + int grid_height_; + float cell_width_{}; + float cell_height_{}; + float x_min_; + float x_max_; + float y_min_; + float y_max_; + float z_min_; + float z_max_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_COLLISION_CACHE_H_ diff --git a/src/ballistica/dynamics/dynamics.cc b/src/ballistica/dynamics/dynamics.cc new file mode 100644 index 00000000..a8837a3e --- /dev/null +++ b/src/ballistica/dynamics/dynamics.cc @@ -0,0 +1,1114 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/dynamics.h" + +#include +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/audio/audio_source.h" +#include "ballistica/dynamics/collision.h" +#include "ballistica/dynamics/collision_cache.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/scene/scene.h" +#include "ode/ode_collision_kernel.h" +#include "ode/ode_collision_util.h" + +namespace ballistica { + +// Max contacts for rigid body collisions. +// TODO(ericf): Probably a good idea to accept more than this +// and then randomly discard some - otherwise +// we may get contacts only at one end of an object, etc. +#define MAX_CONTACTS 20 + +// Given two parts, returns true if part1 is major in +// the storage order. +static auto IsInStoreOrder(int64_t node1, int part1, int64_t node2, int part2) + -> bool { + assert(node1 >= 0 && part1 >= 0 && node2 >= 0 && part2 >= 0); + + // Node with smaller id is primary search node. + if (node1 < node2) { + return true; + } else if (node1 > node2) { + return false; + } else { + // If nodes are same, classify by part id. + // If part ids are the same, it doesnt matter. + return (part1 < part2); + } +} + +// Modified version of dBodyGetPointVel - instead of applying the body's +// linear and angular velocities, we apply a provided force and torque +// to get its local equivalent. +void do_dBodyGetLocalFeedback(dBodyID b, dReal px, dReal py, dReal pz, + dReal lvx, dReal lvy, dReal lvz, dReal avx, + dReal avy, dReal avz, dVector3 result) { + dAASSERT(b); + dVector3 p; + p[0] = px - b->pos[0]; + p[1] = py - b->pos[1]; + p[2] = pz - b->pos[2]; + p[3] = 0; + result[0] = lvx; + result[1] = lvy; + result[2] = lvz; + dReal avel[4]; + avel[0] = avx; + avel[1] = avy; + avel[2] = avz; + avel[3] = 0; + dCROSS(result, +=, avel, p); +} + +// Stores info about a collision needing a reset +// (used when parts change materials). +class Dynamics::CollisionReset { + public: + int node1; + int node2; + int part1; + int part2; + CollisionReset(int node1_in, int part1_in, int node2_in, int part2_in) + : node1(node1_in), node2(node2_in), part1(part1_in), part2(part2_in) {} +}; + +class Dynamics::CollisionEvent { + public: + Object::Ref action; + Object::Ref collision; + Object::WeakRef node1; // first event node + Object::WeakRef node2; // second event node + CollisionEvent(Node* node1_in, Node* node2_in, + const Object::Ref& action_in, + const Object::Ref& collision_in) + : node1(node1_in), + node2(node2_in), + action(action_in), + collision(collision_in) {} +}; + +Dynamics::Dynamics(Scene* scene_in) + : scene_(scene_in), collision_cache_(new CollisionCache()) { + ResetODE(); +} + +Dynamics::~Dynamics() { + if (in_process_) { + Log("Error: Dynamics going down within Process() call;" + " should not happen."); + } + ShutdownODE(); +} + +void Dynamics::Draw(FrameDef* frame_def) { + // draw collisions if desired.. +#if BA_DEBUG_BUILD && 0 + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetColor(1, 0, 0); + c.SetTransparent(true); + for (auto&& i : debug_collisions_) { + c.PushTransform(); + c.Translate(i.x(), i.y(), i.z()); + c.scaleUniform(0.05f); + c.DrawModel(g_media->GetModel(Media::BOX_MODEL)); + c.PopTransform(); + } + c.Submit(); + debug_collisions_.clear(); +#endif // BA_DEBUG_BUILD +} + +void Dynamics::ResetCollision(int64_t node1, int part1, int64_t node2, + int part2) { + // Make sure this isn't called while we're in the middle of processing + // collides (it shouldn't be possible but just in case). + BA_PRECONDITION(!processing_collisions_); + + // We don't actually do any resetting here; we just store a notice that + // these two parts should be separated and the notice is sent out at + // collide process time. + collision_resets_.emplace_back(node1, part1, node2, part2); +} + +void Dynamics::AddTrimesh(dGeomID g) { + assert(dGeomGetClass(g) == dTriMeshClass); + trimeshes_.push_back(g); + + // Do a one-time bbox update; these never move so this should cover us. + g->recomputeAABB(); + g->gflags &= (~(GEOM_DIRTY | GEOM_AABB_BAD)); // NOLINT + + // Update our collision cache. + collision_cache_->SetGeoms(trimeshes_); +} + +void Dynamics::RemoveTrimesh(dGeomID g) { + assert(dGeomGetClass(g) == dTriMeshClass); + for (auto i = trimeshes_.begin(); i != trimeshes_.end(); i++) { + if ((*i) == g) { + trimeshes_.erase(i); + + // Update our collision cache. + collision_cache_->SetGeoms(trimeshes_); + return; + } + } + throw Exception("trimesh not found"); +} + +class Dynamics::SrcPartCollideMap { + public: + std::map > dst_part_collisions; +}; + +class Dynamics::DstNodeCollideMap { + public: + std::map src_parts; + int collideDisabled; + DstNodeCollideMap() : collideDisabled(0) {} + ~DstNodeCollideMap() = default; +}; + +class Dynamics::SrcNodeCollideMap { + public: + std::map dst_nodes; +}; + +auto Dynamics::AreColliding(const Part& p1_in, const Part& p2_in) -> bool { + const Part* p1; + const Part* p2; + if (IsInStoreOrder(p1_in.node()->id(), p1_in.id(), p2_in.node()->id(), + p2_in.id())) { + p1 = &p1_in; + p2 = &p2_in; + } else { + p1 = &p2_in; + p2 = &p1_in; + } + + // Go down the hierarchy until we either find a missing level or + // find the collision. + auto i = node_collisions_.find(p1->node()->id()); + if (i != node_collisions_.end()) { + auto j = i->second.dst_nodes.find(p2->node()->id()); + if (j != i->second.dst_nodes.end()) { + auto k = j->second.src_parts.find(p1->id()); + if (k != j->second.src_parts.end()) { + auto l = k->second.dst_part_collisions.find(p2->id()); + if (l != k->second.dst_part_collisions.end()) return true; + } + } + } + return false; +} + +auto Dynamics::GetCollision(Part* p1_in, Part* p2_in, MaterialContext** cc1, + MaterialContext** cc2) -> Collision* { + Part* p1; + Part* p2; + + if (IsInStoreOrder(p1_in->node()->id(), p1_in->id(), p2_in->node()->id(), + p2_in->id())) { + p1 = p1_in; + p2 = p2_in; + } else { + p1 = p2_in; + p2 = p1_in; + } + + std::pair >::iterator, bool> i = + node_collisions_[p1->node()->id()] + .dst_nodes[p2->node()->id()] + .src_parts[p1->id()] + .dst_part_collisions.insert( + std::make_pair(p2->id(), Object::Ref())); + + Collision* new_collision; + + // If it didnt exist, go ahead and set up the collision. + if (i.second) { + i.first->second = Object::New(scene_); + new_collision = i.first->second.get(); + } else { + new_collision = nullptr; + } + + (*cc1) = &i.first->second->src_context; + (*cc2) = &i.first->second->dst_context; + + // Continue setting it up. + if (new_collision) { + new_collision->src_part = p1; + new_collision->dst_part = p2; + + // Init contexts with parts' defaults. + (*cc1)->collide = p1->default_collides(); + (*cc2)->collide = p2->default_collides(); + + // Apply each part's materials to its context. + p1->ApplyMaterials(*cc1, p1, p2); + p2->ApplyMaterials(*cc2, p2, p1); + + // If either disabled collisions between these two nodes, store that. + DstNodeCollideMap* dncm = + &node_collisions_[p1->node()->id()].dst_nodes[p2->node()->id()]; + if (!(*cc1)->node_collide || !(*cc2)->node_collide) { + dncm->collideDisabled = true; + } + + // Don't collide if either context doesnt want us to or if the nodes + // aren't colliding (unless either context wants to ignore node + // collision status). + new_collision->collide = + ((*cc1)->collide && (*cc2)->collide + && (!dncm->collideDisabled || !(*cc1)->use_node_collide + || !(*cc2)->use_node_collide)); + + // If theres a physical collision involved, inform the parts + // so they can keep track of who they're touching. + if (new_collision->collide) { + bool physical = (*cc1)->physical && (*cc2)->physical; + p1->SetCollidingWith(p2->node()->id(), p2->id(), true, physical); + if (p1 != p2) { + p2->SetCollidingWith(p1->node()->id(), p1->id(), true, physical); + } + + // Also add all new-collide events to the global list + // (to be executed after all contacts are found). + for (auto& connect_action : (*cc1)->connect_actions) + collision_events_.emplace_back(p1->node(), p2->node(), connect_action, + Object::Ref(new_collision)); + for (auto& connect_action : (*cc2)->connect_actions) + collision_events_.emplace_back(p2->node(), p1->node(), connect_action, + Object::Ref(new_collision)); + } + } + + // Regardless, set it as claimed so we know its current. + i.first->second->claim_count++; + + return &(*(i.first->second)); +} + +void Dynamics::HandleDisconnect( + const std::map::iterator& + i, + const std::map::iterator& + j, + const std::map::iterator& k, + const std::map >::iterator& l) { + // Handle disconnect equivalents if they were colliding. + if (l->second->collide) { + // Add the contexts' disconnect commands to be executed. + for (auto m = l->second->src_context.disconnect_actions.begin(); + m != l->second->src_context.disconnect_actions.end(); m++) { + Part* src_part = l->second->src_part.get(); + Part* dst_part = l->second->dst_part.get(); + collision_events_.emplace_back(src_part ? src_part->node() : nullptr, + dst_part ? dst_part->node() : nullptr, *m, + l->second); + } + + for (auto m = l->second->dst_context.disconnect_actions.begin(); + m != l->second->dst_context.disconnect_actions.end(); m++) { + Part* src_part = l->second->src_part.get(); + Part* dst_part = l->second->dst_part.get(); + collision_events_.emplace_back(dst_part ? dst_part->node() : nullptr, + src_part ? src_part->node() : nullptr, *m, + l->second); + } + + // Now see if either of the two parts involved still exist and if they do, + // tell them they're no longer colliding with the other. + bool physical = + l->second->src_context.physical && l->second->dst_context.physical; + Part* p1 = l->second->dst_part.get(); + Part* p2 = l->second->src_part.get(); + if (p1) { + assert(p1 == l->second->dst_part.get()); + p1->SetCollidingWith(i->first, k->first, false, physical); // NOLINT + } + if (p2) { + assert(p2 == l->second->src_part.get()); + } + if (p2 && (p2 != p1)) { + p2->SetCollidingWith(j->first, l->first, false, physical); // NOLINT + } + } + + // Remove this particular collision. + k->second.dst_part_collisions.erase(l); +} + +void Dynamics::ProcessCollisions() { + processing_collisions_ = true; + + collision_count_ = 0; + + // First handle our explicitly reset collisions. + // For each reset request, we check if the surfaces are colliding and if so + // we separate them and add their separation commands to our to-do list. + if (!collision_resets_.empty()) { + for (auto& collision_reset : collision_resets_) { + int n1, n2; + int p1, p2; + + if (IsInStoreOrder(collision_reset.node1, collision_reset.part1, + collision_reset.node2, collision_reset.part2)) { + n1 = collision_reset.node1; + p1 = collision_reset.part1; + n2 = collision_reset.node2; + p2 = collision_reset.part2; + } else { + n1 = collision_reset.node2; + p1 = collision_reset.part2; + n2 = collision_reset.node1; + p2 = collision_reset.part1; + } + + // Go down the hierarchy until we either find a missing level or + // find the collision to reset. + { + auto i = node_collisions_.find(n1); + if (i != node_collisions_.end()) { + auto j = i->second.dst_nodes.find(n2); + if (j != i->second.dst_nodes.end()) { + auto k = j->second.src_parts.find(p1); + if (k != j->second.src_parts.end()) { + auto l = k->second.dst_part_collisions.find(p2); + if (l != k->second.dst_part_collisions.end()) { + // They were colliding - separate them. + HandleDisconnect(i, j, k, l); + } + + // Erase if none left. + if (k->second.dst_part_collisions.empty()) { + j->second.src_parts.erase(k); + } + } + + // Erase if none left. + if (j->second.src_parts.empty()) i->second.dst_nodes.erase(j); + } + + // Erase if none left. + if (i->second.dst_nodes.empty()) node_collisions_.erase(i); + } + } + } + collision_resets_.clear(); + } + + // Reset our claim counts. When we run collision tests, claim counts + // will be incremented for things that are still in contact. + for (auto& node_collision : node_collisions_) { + for (auto& dst_node : node_collision.second.dst_nodes) { + for (auto& src_part : dst_node.second.src_parts) { + for (auto& dst_part_collision : src_part.second.dst_part_collisions) { + dst_part_collision.second->claim_count = 0; + } + } + } + } + + // Process all standard collisions. This will trigger our callback which + // do the real work (add collisions to list, store commands to be + // called, etc). + dSpaceCollide(ode_space_, this, &DoCollideCallback); + + // Collide our trimeshes against everything. + collision_cache_->CollideAgainstSpace(ode_space_, this, &DoCollideCallback); + + // Do a bit of precalc each cycle. + collision_cache_->Precalc(); + + // Now go through our list of currently-colliding stuff, + // setting parts' currently-colliding-with lists + // based on current info, + // removing unclaimed collisions and empty groups. + std::map::iterator i_next; + std::map::iterator j_next; + std::map::iterator k_next; + std::map >::iterator l_next; + for (auto i = node_collisions_.begin(); i != node_collisions_.end(); + i = i_next) { + i_next = i; + i_next++; + for (auto j = i->second.dst_nodes.begin(); j != i->second.dst_nodes.end(); + j = j_next) { + j_next = j; + j_next++; + for (auto k = j->second.src_parts.begin(); k != j->second.src_parts.end(); + k = k_next) { + k_next = k; + k_next++; + for (auto l = k->second.dst_part_collisions.begin(); + l != k->second.dst_part_collisions.end(); l = l_next) { + l_next = l; + l_next++; + + // Not claimed; separating. + if (!l->second->claim_count) { + HandleDisconnect(i, j, k, l); + } + } + if (k->second.dst_part_collisions.empty()) { + j->second.src_parts.erase(k); + } + } + if (j->second.src_parts.empty()) { + i->second.dst_nodes.erase(j); + } + } + if (i->second.dst_nodes.empty()) { + node_collisions_.erase(i); + } + } + + // We're now done processing collisions - its now safe to reset + // collisions, etc. since we're no longer going through the lists. + processing_collisions_ = false; + + // Execute all events that we built up due to collisions. + for (auto&& i : collision_events_) { + active_collision_ = i.collision.get(); + active_collide_src_node_ = i.node1; + active_collide_dst_node_ = i.node2; + i.action->Execute(i.node1.get(), i.node2.get(), scene_); + } + active_collision_ = nullptr; + collision_events_.clear(); +} + +void Dynamics::process() { + in_process_ = true; + real_time_ = GetRealTime(); // Update this once so we can recycle results. + ProcessCollisions(); + dWorldQuickStep(ode_world_, kGameStepSeconds); + dJointGroupEmpty(ode_contact_group_); + in_process_ = false; +} + +void Dynamics::DoCollideCallback(void* data, dGeomID o1, dGeomID o2) { + auto* d = static_cast(data); + d->CollideCallback(o1, o2); +} + +// Run collisions for everything. Store any callbacks that will need to be made +// and run them after all collision constraints are made. +// This way we know all bodies and their associated nodes, etc are valid +// throughout collision processing. +void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { + dBodyID b1 = dGeomGetBody(o1); + dBodyID b2 = dGeomGetBody(o2); + + auto* r1 = static_cast(dGeomGetData(o1)); + auto* r2 = static_cast(dGeomGetData(o2)); + assert(r1 && r2); + + // If both of these guys are either terrain (a trimesh) or an inactive body, + // we can skip actually testing for a collision. + if ((dGeomGetClass(o1) == dTriMeshClass && b2 && !dBodyIsEnabled(b2)) + || (dGeomGetClass(o2) == dTriMeshClass && b1 && !dBodyIsEnabled(b1))) { + // We do, however, need to poke any existing collision so a disconnect event + // doesn't occur if we were colliding. + Part* p1_in = r1->part(); + Part* p2_in = r2->part(); + assert(p1_in && p2_in); + Part* p1; + Part* p2; + + if (IsInStoreOrder(p1_in->node()->id(), p1_in->id(), p2_in->node()->id(), + p2_in->id())) { + p1 = p1_in; + p2 = p2_in; + } else { + p1 = p2_in; + p2 = p1_in; + } + auto i = node_collisions_.find(p1->node()->id()); + if (i != node_collisions_.end()) { + auto j = i->second.dst_nodes.find(p2->node()->id()); + if (j != i->second.dst_nodes.end()) { + auto k = j->second.src_parts.find(p1->id()); + if (k != j->second.src_parts.end()) { + auto l = k->second.dst_part_collisions.find(p2->id()); + if (l != k->second.dst_part_collisions.end()) { +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnusedValue" + l->second->claim_count++; +#pragma clang diagnostic pop + } + } + } + } + return; + } + + // Check their overall types to count out some basics + // (landscapes never collide against landscapes, etc). + if (!((r1->collide_type() & r2->collide_mask()) + && (r2->collide_type() & r1->collide_mask()))) { // NOLINT + return; + } + + Part* p1 = r1->part(); + Part* p2 = r2->part(); + assert(p1 && p2); + + // Pre-filter collisions. + if (!(p1->node()->PreFilterCollision(r1, r2) + && p2->node()->PreFilterCollision(r2, r1))) { + return; + } + + // Perhaps an optimization could be to avoid collision testing + // if we're certain two materials will never result in a collision? + // I don't think calculating full material-states before each collision + // detection test would be economical but if there's a simple way to know + // they'll never collide. + dContact contact[MAX_CONTACTS]; // up to MAX_CONTACTS contacts per pair + if (int numc = + dCollide(o1, o2, MAX_CONTACTS, &contact[0].geom, sizeof(dContact))) { + MaterialContext* cc1; + MaterialContext* cc2; + + // Create or acquire a collision. + Collision* c = GetCollision(p1, p2, &cc1, &cc2); + + // If theres no physical collision between these two suckers we're done. + if (!c->collide) { + return; + } + + // Store body IDs for use in callback messages. + // There may be more than one body ID per part-on-part contact + // but we just keep one at the moment. + c->body_id_1 = r1->id(); + c->body_id_2 = r2->id(); + + // Get average depth for all contacts. + if (numc > 0) { + float d = 0; + for (int i = 0; i < numc; i++) { + d += contact[i].geom.depth; + } + c->depth = d / static_cast(numc); + } + + // Get average position for all contacts. + float apx = 0; + float apy = 0; + float apz = 0; + if (numc > 0) { + for (int i = 0; i < numc; i++) { + apx += contact[i].geom.pos[0]; + apy += contact[i].geom.pos[1]; + apz += contact[i].geom.pos[2]; + } + auto fnumc = static_cast(numc); + apx /= fnumc; + apy /= fnumc; + apz /= fnumc; + } + c->x = apx; + c->y = apy; + c->z = apz; + + // If theres an impact sound, skid sound, or roll sound attached to this + // collision, calculate applicable values. + // Impact is based on the component of the vector (force x relative + // velocity) that is parallel to the collision normal. + // Skid is the component tangential to the collision normal. + // Roll is based on tangential velocity multiplied by parallel force. + bool get_feedback_for_these_collisions = false; + + if (cc1->complex_sound || cc2->complex_sound) { + millisecs_t real_time = real_time_; + + // Its possible that we have more than one set of colliding things + // that resolve to the same collision record + // (multiple bodies in the same part, etc). + // However we can only calc feedback for the first one we come across + // (there's only one feedback buffer in the Collision). + if (c->claim_count == 1) { + get_feedback_for_these_collisions = true; + } + + dVector3 an; + an[0] = an[1] = an[2] = 0; + dVector3 b1v; + dVector3 b2v; + dVector3 b1cv; + dVector3 b2cv; + + // Get average collide normal for all contacts. + { + if (numc > 0) { + for (int i = 0; i < numc; i++) { + an[0] += contact[i].geom.normal[0]; + an[1] += contact[i].geom.normal[1]; + an[2] += contact[i].geom.normal[2]; + } + auto fnumc = static_cast(numc); + an[0] /= fnumc; + an[1] /= fnumc; + an[2] /= fnumc; + } + + const dReal* v; + + // Get body velocities at the avg contact point in global coords. + if (b1) { + v = dBodyGetLinearVel(b1); + b1cv[0] = v[0]; + b1cv[1] = v[1]; + b1cv[2] = v[2]; + dBodyGetPointVel(b1, apx, apy, apz, b1v); + } else { + b1cv[0] = b1cv[1] = b1cv[2] = 0; + b1v[0] = b1v[1] = b1v[2] = 0; + } + if (b2) { + v = dBodyGetLinearVel(b2); + b2cv[0] = v[0]; + b2cv[1] = v[1]; + b2cv[2] = v[2]; + dBodyGetPointVel(b2, apx, apy, apz, b2v); + } else { + b2cv[0] = b2cv[1] = b2cv[2] = 0; + b2v[0] = b2v[1] = b2v[2] = 0; + } + } + + dVector3 local_feedback; + if (!c->collide_feedback.empty()) { + assert(b1 || b2); + dBodyID fb; + float affx = 0; + float affy = 0; + float affz = 0; + float aftx = 0; + float afty = 0; + float aftz = 0; + + // Get one or the other force. Once we convert it to local + // it should be equal/opposite. + if (b1) { + fb = b1; + for (auto& i : c->collide_feedback) { + affx += i.f1[0]; + affy += i.f1[1]; + affz += i.f1[2]; + aftx += i.t1[0]; + afty += i.t1[1]; + aftz += i.t1[2]; + } + } else { + fb = b2; + for (auto& i : c->collide_feedback) { + affx += i.f2[0]; + affy += i.f2[1]; + affz += i.f2[2]; + aftx += i.t2[0]; + afty += i.t2[1]; + aftz += i.t2[2]; + } + } + dMass mass; + dBodyGetMass(fb, &mass); + + // Average them and divide by mass to normalize the force. + float count = c->collide_feedback.size(); + affx /= (count * mass.mass * 10.0f); + affy /= (count * mass.mass * 10.0f); + affz /= (count * mass.mass * 10.0f); + aftx /= (count * mass.mass * 10.0f); + afty /= (count * mass.mass * 10.0f); + aftz /= (count * mass.mass * 10.0f); + + // Get local feedback. + do_dBodyGetLocalFeedback(fb, apx, apy, apz, affx, affy, affz, aftx, + afty, aftz, local_feedback); + + // TODO(ericf): normalize feedback based on body mass so all bodies can + // use similar ranges? ... hmm maybe not a good idea.. larger object + // *should* be louder plus then we're using object mass, which doesnt + // account for objects + // connected to it via fixed constraints, etc + // the sound should simply have a impulse associated with it - + // anything less than that will scale appropriately + } else { + local_feedback[0] = 0; + local_feedback[1] = 0; + local_feedback[2] = 0; + } + + // Combine both velocities into one relative velocity for the contact + // point. + dVector3 rvel; + rvel[0] = b2v[0] - b1v[0]; + rvel[1] = b2v[1] - b1v[1]; + rvel[2] = b2v[2] - b1v[2]; + + // Get our overall relative velocity (at the objects' centers-of-gravity + // we use this to determine roll. + dVector3 crvel; + crvel[0] = b2cv[0] - b1cv[0]; + crvel[1] = b2cv[1] - b1cv[1]; + crvel[2] = b2cv[2] - b1cv[2]; + + // Now multiply our feedback force by our relative velocity and use the + // component of that which is parallel to our collide normal as "impact" + // and the tangential component as "skid". + { + dVector3 vec = {local_feedback[0] * rvel[0], + local_feedback[1] * rvel[1], + local_feedback[2] * rvel[2]}; + float cur_impact = std::abs(dDOT(an, vec)) / 3; + float vec_len = dVector3Length(vec); + float cur_skid = sqrtf(vec_len * vec_len - cur_impact * cur_impact) / 2; + + // Roll is calculated as the component of force parallel to the normal + // multiplied by the tangential velocity component (relative + // center-of-gravity velocities - not at the contact point). + float cur_roll; + { + float vparallel = dDOT(an, crvel); + float vec_len_2 = dVector3Length(crvel); + float vtangential = + sqrtf(vec_len_2 * vec_len_2 - vparallel * vparallel); + cur_roll = (vtangential); + } + cur_roll -= cur_impact; + cur_skid -= cur_impact; + if (cur_roll < 0) { + cur_roll = 0; + } + if (cur_skid < 0) { + cur_skid = 0; + } + + // Weigh our new values with previous ones to get more smooth consistent + // values over time. + float impact_weight = 0.3f; + float skid_weight = 0.1f; + float roll_weight = 0.1f; + + c->impact = + (1.0f - impact_weight) * c->impact + impact_weight * cur_impact; + c->skid = (1.0f - skid_weight) * c->skid + skid_weight * cur_skid; + c->roll = (1.0f - roll_weight) * c->roll + roll_weight * cur_roll; + + // Draw debugging lines - red for impact, green for skid, blue for roll. + // if (scene_->getShowCollisions()) { + // g_graphics_server->addDebugDrawObject( + // new GraphicsServer::DebugDrawLine( + // apx, apy, apz, + // apx+0*0.5f*c->impact, + // apy+1*0.5f*c->impact, + // apz+0*0.5f*c->impact, 15, 1, 0, 0)); + // g_graphics_server->addDebugDrawObject( + // new GraphicsServer::DebugDrawLine( + // apx, apy, apz, + // apx-0*0.5f*c->skid, + // apy-1*0.5f*c->skid, + // apz-0*0.5f*c->skid, 10, 0, 1, 0)); + // g_graphics_server->addDebugDrawObject( + // new GraphicsServer::DebugDrawLine( + // apx, apy, apz, + // apx+1*0.5f*c->roll, + // apy+0*0.5f*c->roll, + // apz+0*0.5f*c->roll, 15, 0, 0, 1)); + // } + + // Play impact sounds if its been long enough since last. + // Clip if impact value is low enough (otherwise we'd be running tiny + // little impact sounds constantly). + // Also only play impact sound when our current impact is less than + // our average (so that as impact spikes we hit it near the top instead + // of on the way up). + if ((real_time - p1->last_impact_sound_time() >= 500) + || (real_time - p2->last_impact_sound_time() > 500)) { + float clip = 0.15f; + MaterialContext* contexts[] = {cc1, cc2}; + for (auto context : contexts) { + for (auto&& i : context->impact_sounds) { + if (c->impact > i.target_impulse * clip + && cur_impact < c->impact) { + float volume = i.target_impulse > 0.0001f + ? (c->impact - (i.target_impulse * clip)) + / (i.target_impulse * (1.0f - clip)) + : 1.0f; + + if (volume > 1) volume = 1; + assert(i.sound.exists()); + if (AudioSource* source = g_audio->SourceBeginNew()) { + source->SetGain(volume * i.volume); + source->SetPosition(apx, apy, apz); + source->Play(i.sound->GetSoundData()); + p1->set_last_impact_sound_time(real_time); + p2->set_last_impact_sound_time(real_time); + last_impact_sound_time_ = real_time; + source->End(); + } + } + } + } + } + + // Play skid sounds. + { + float clip = 0.15f; + MaterialContext* contexts[] = {cc1, cc2}; + for (auto context : contexts) { + for (auto&& i : context->skid_sounds) { + if (c->skid > i.target_impulse * clip) { + float volume = i.target_impulse > 0.0001f + ? (c->skid - (i.target_impulse * clip)) + / (i.target_impulse * (1.0f - clip)) + : 1.0f; + if (volume > 1) volume = 1; + + // If we're already playing, just adjust volume + // and position - otherwise get a sound started. + if (i.playing) { + AudioSource* s = g_audio->SourceBeginExisting(i.play_id, 101); + if (s) { + s->SetGain(volume * i.volume); + s->SetPosition(apx, apy, apz); + s->End(); + } else { + // Spare ourself some trouble next time. + i.playing = false; + } + } else if (real_time - p1->last_skid_sound_time() >= 250 + || real_time - p2->last_skid_sound_time() > 250) { + assert(i.sound.exists()); + if (AudioSource* source = g_audio->SourceBeginNew()) { + source->SetLooping(true); + source->SetGain(volume * i.volume); + source->SetPosition(apx, apy, apz); + i.play_id = source->Play(i.sound->GetSoundData()); + i.playing = true; + p1->set_last_skid_sound_time(real_time); + p2->set_last_skid_sound_time(real_time); + source->End(); + } + } + } else { + // Skid values are low - stop any playing skid sounds. + if (i.playing) { + g_audio->PushSourceFadeOutCall(i.play_id, 200); + i.playing = false; + } + } + } + } + } + + // Play roll sounds. + { + float clip = 0.15f; + MaterialContext* contexts[] = {cc1, cc2}; + for (auto context : contexts) { + for (auto&& i : context->roll_sounds) { + if (c->roll > i.target_impulse * clip) { + float volume = i.target_impulse > 0.0001f + ? (c->roll - (i.target_impulse * clip)) + / (i.target_impulse * (1.0f - clip)) + : 1; + if (volume > 1) volume = 1; + + // If we're already playing, just adjust volume + // and position; otherwise get a sound started. + if (i.playing) { + AudioSource* s = g_audio->SourceBeginExisting(i.play_id, 102); + if (s) { + s->SetGain(volume * i.volume); + s->SetPosition(apx, apy, apz); + s->End(); + } else { + // spare ourself some trouble next time + i.playing = false; + } + } else if (real_time - p1->last_roll_sound_time() >= 250 + || real_time - p2->last_roll_sound_time() > 250) { + assert(i.sound.exists()); + if (AudioSource* source = g_audio->SourceBeginNew()) { + source->SetLooping(true); + source->SetGain(volume * i.volume); + source->SetPosition(apx, apy, apz); + i.play_id = source->Play(i.sound->GetSoundData()); + i.playing = true; + p1->set_last_roll_sound_time(real_time); + p2->set_last_roll_sound_time(real_time); + source->End(); + } + } + } else { + // roll values are low - stop any playing roll sounds + if (i.playing) { + g_audio->PushSourceFadeOutCall(i.play_id, 200); + i.playing = false; + } + } + } + } + } + } + if (get_feedback_for_these_collisions) { + assert(numc >= 0); + c->collide_feedback.resize(static_cast(numc)); + } + } + + // Play collide sounds when new contacts happen + // or when the averaged collide-position relative to + // both objects changes by a largeish amount. + // (in a normal rolling or sliding situation, the collide position + // will stay relatively constant in at least one of the object's + // frame-of-reference) + bool play_collide_sounds = false; + + // Normal sounds should just happen on initial contact creation. + if (c->contact_count == 0 && numc > 0) { + play_collide_sounds = true; + } + + c->contact_count = numc; + + if (play_collide_sounds) { + for (auto&& i : cc1->connect_sounds) { + assert(i.sound.exists()); + if (AudioSource* source = g_audio->SourceBeginNew()) { + source->SetPosition(apx, apy, apz); + source->SetGain(i.volume); + source->Play(i.sound->GetSoundData()); + source->End(); + } + } + for (auto&& i : cc2->connect_sounds) { + assert(i.sound.exists()); + if (AudioSource* source = g_audio->SourceBeginNew()) { + source->SetPosition(apx, apy, apz); + source->SetGain(i.volume); + source->Play(i.sound->GetSoundData()); + source->End(); + } + } + } + + // Set up collision constraints for this frame as long + // as theres at least one body involved. + if ((b1 || b2) && (cc1->physical && cc2->physical)) { + float friction = 1.2f * sqrtf(cc1->friction * cc2->friction); + float bounce = sqrtf(cc1->bounce * cc2->bounce); + float stiffness; + if (cc1->stiffness < 0.00000001f || cc2->stiffness < 0.00000001f) { + stiffness = 0.00000001f; + } else { + stiffness = 8000 * sqrtf(cc1->stiffness * cc2->stiffness); + } + float damping = 80 * cc1->damping + cc2->damping; + if ((stiffness < 0.00000001f) && (damping < 0.00000001f)) { + damping = 0.00000001f; + } + + // Cfm/erp (based off stiffness/damping). + float erp = (kGameStepSeconds * stiffness) + / ((kGameStepSeconds * stiffness) + damping); + float cfm = 1.0f / ((kGameStepSeconds * stiffness) + damping); + + // Normally a geom against a body does not automatically wake the body. + // However we explicitly do so in certain cases (if the geom is moving, + // etc). + if (r1->geom_wake_on_collide() || r2->geom_wake_on_collide()) { + if (b1) { + dBodyEnable(b1); + } + if (b2) { + dBodyEnable(b2); + } + } + bool do_collide = true; + + // Set up our contacts. + // FIXME should really do some merging in cases with > 15 or so contacts + // (which seem to occur often with boxes and such). + + for (int i = 0; i < numc; i += 1) { + // NOLINTNEXTLINE + contact[i].surface.mode = dContactBounce | dContactSoftCFM + | dContactSoftERP | dContactApprox1; + contact[i].surface.mu2 = 0; + contact[i].surface.bounce_vel = 0.1f; + contact[i].surface.mu = friction; + contact[i].surface.bounce = bounce; + contact[i].surface.soft_cfm = cfm; + contact[i].surface.soft_erp = erp; + } + + // Let each side of the collision modify our stuff. If any party objects + // to the collision occurring, we scrap the whole plan. + if ((!r1->CallCollideCallbacks(contact, numc, r2)) + || (!r2->CallCollideCallbacks(contact, numc, r1))) { + do_collide = false; + } + if (do_collide) { + collision_count_ += numc; + for (int i = 0; i < numc; i += 1) { + dJointID constraint = + dJointCreateContact(ode_world_, ode_contact_group_, contact + i); + dJointAttach(constraint, b1, b2); + if (get_feedback_for_these_collisions) { + dJointSetFeedback(constraint, &c->collide_feedback[i]); + } + } + } + } + } +} + +void Dynamics::ShutdownODE() { + if (ode_space_) { + dSpaceDestroy(ode_space_); + ode_space_ = nullptr; + } + if (ode_world_) { + dWorldDestroy(ode_world_); + ode_world_ = nullptr; + } + if (ode_contact_group_) { + dJointGroupDestroy(ode_contact_group_); + ode_contact_group_ = nullptr; + } +} + +void Dynamics::ResetODE() { + ShutdownODE(); + ode_world_ = dWorldCreate(); + assert(ode_world_); + dWorldSetGravity(ode_world_, 0, -20, 0); + dWorldSetContactSurfaceLayer(ode_world_, 0.001f); + dWorldSetAutoDisableFlag(ode_world_, true); + dWorldSetAutoDisableSteps(ode_world_, 5); + dWorldSetAutoDisableLinearThreshold(ode_world_, 0.1f); + dWorldSetAutoDisableAngularThreshold(ode_world_, 0.1f); + dWorldSetAutoDisableSteps(ode_world_, 10); + dWorldSetAutoDisableTime(ode_world_, 0); + dWorldSetQuickStepNumIterations(ode_world_, 10); + ode_space_ = dHashSpaceCreate(nullptr); + assert(ode_space_); + ode_contact_group_ = dJointGroupCreate(0); + assert(ode_contact_group_); + dRandSetSeed(5432); +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/dynamics.h b/src/ballistica/dynamics/dynamics.h new file mode 100644 index 00000000..ca9792f9 --- /dev/null +++ b/src/ballistica/dynamics/dynamics.h @@ -0,0 +1,128 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_DYNAMICS_H_ +#define BALLISTICA_DYNAMICS_DYNAMICS_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ode/ode.h" + +namespace ballistica { + +class Dynamics : public Object { + public: + explicit Dynamics(Scene* scene_in); + ~Dynamics() override; + void Draw(FrameDef* frame_def); // Draw any debug stuff, etc. + auto ode_world() -> dWorldID { return ode_world_; } + auto getContactGroup() -> dJointGroupID { return ode_contact_group_; } + auto space() -> dSpaceID { return ode_space_; } + + // Discontinues a collision. Used by parts when changing materials + // so that new collisions may enter effect. + void ResetCollision(int64_t node1, int part1, int64_t node2, int part2); + + // Used by collision callbacks - internal. + auto active_collision() const -> Collision* { return active_collision_; } + + // Used by collision callbacks - internal. + auto GetActiveCollideSrcNode() -> Node* { + assert(active_collision_); + return (collide_message_reverse_order_ ? active_collide_dst_node_ + : active_collide_src_node_) + .get(); + } + // Used by collision callbacks - internal. + auto GetActiveCollideDstNode() -> Node* { + assert(active_collision_); + return (collide_message_reverse_order_ ? active_collide_src_node_ + : active_collide_dst_node_) + .get(); + } + auto GetCollideMessageReverseOrder() const -> bool { + return collide_message_reverse_order_; + } + + // Used by collide message handlers. + void set_collide_message_state(bool inCollideMessageIn, + bool target_other_in = false) { + in_collide_message_ = inCollideMessageIn; + collide_message_reverse_order_ = target_other_in; + } + auto in_collide_message() const -> bool { return in_collide_message_; } + void process(); + void increment_skid_sound_count() { skid_sound_count_++; } + void decrement_skid_sound_count() { skid_sound_count_--; } + auto skid_sound_count() const -> int { return skid_sound_count_; } + void incrementRollSoundCount() { roll_sound_count_++; } + void decrement_roll_sound_count() { roll_sound_count_--; } + auto getRollSoundCount() const -> int { return roll_sound_count_; } + + // We do some fancy collision testing stuff for trimeshes instead + // of going through regular ODE space collision testing.. so we have + // to keep track of these ourself. + void AddTrimesh(dGeomID g); + void RemoveTrimesh(dGeomID g); + + auto collision_count() const -> int { return collision_count_; } + auto process_real_time() const -> millisecs_t { return real_time_; } + auto last_impact_sound_time() const -> millisecs_t { + return last_impact_sound_time_; + } + auto in_process() const -> bool { return in_process_; } + + private: + auto AreColliding(const Part& p1, const Part& p2) -> bool; + class SrcNodeCollideMap; + class DstNodeCollideMap; + class SrcPartCollideMap; + class CollisionEvent; + class CollisionReset; + std::vector collision_resets_; + + // Return a collision object between these two parts, + // creating a new one if need be. + auto GetCollision(Part* p1, Part* p2, MaterialContext** cc1, + MaterialContext** cc2) -> Collision*; + + // Contains in-progress collisions for current nodes. + std::map node_collisions_; + std::vector collision_events_; + void HandleDisconnect( + const std::map::iterator& i, + const std::map::iterator& j, + const std::map::iterator& k, + const std::map >::iterator& l); + void ResetODE(); + void ShutdownODE(); + static void DoCollideCallback(void* data, dGeomID o1, dGeomID o2); + void CollideCallback(dGeomID o1, dGeomID o2); + void ProcessCollisions(); + bool processing_collisions_ = false; + dWorldID ode_world_ = nullptr; + dJointGroupID ode_contact_group_ = nullptr; + dSpaceID ode_space_ = nullptr; + millisecs_t real_time_ = 0; + bool in_process_ = false; + std::vector trimeshes_; + millisecs_t last_impact_sound_time_ = 0; + int skid_sound_count_ = 0; + int roll_sound_count_ = 0; + int collision_count_ = 0; + Scene* scene_; + bool in_collide_message_ = false; + bool collide_message_reverse_order_ = false; + Collision* active_collision_ = nullptr; + Object::WeakRef active_collide_src_node_; + Object::WeakRef active_collide_dst_node_; + std::unique_ptr collision_cache_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_DYNAMICS_H_ diff --git a/src/ballistica/dynamics/material/impact_sound_material_action.cc b/src/ballistica/dynamics/material/impact_sound_material_action.cc new file mode 100644 index 00000000..8e5f1b30 --- /dev/null +++ b/src/ballistica/dynamics/material/impact_sound_material_action.cc @@ -0,0 +1,74 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/impact_sound_material_action.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/session/client_session.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +auto ImpactSoundMaterialAction::GetFlattenedSize() -> size_t { + // 1 byte for number of sounds plus 1 int per sound + return 1 + 4 * sounds.size() + 2 + 2; +} + +void ImpactSoundMaterialAction::Flatten(char** buffer, + GameStream* output_stream) { + assert(sounds.size() < 100); + auto sound_count{static_cast(sounds.size())}; + Utils::EmbedInt8(buffer, sound_count); + for (int i = 0; i < sound_count; i++) { + Utils::EmbedInt32NBO(buffer, + static_cast_check_fit( + output_stream->GetSoundID(sounds[i].get()))); + } + Utils::EmbedFloat16NBO(buffer, target_impulse_); + Utils::EmbedFloat16NBO(buffer, volume_); +} + +void ImpactSoundMaterialAction::Restore(const char** buffer, + ClientSession* cs) { + int count{Utils::ExtractInt8(buffer)}; + BA_PRECONDITION(count > 0 && count < 100); + sounds.clear(); + for (int i = 0; i < count; i++) { + sounds.emplace_back(cs->GetSound(Utils::ExtractInt32NBO(buffer))); + } + target_impulse_ = Utils::ExtractFloat16NBO(buffer); + volume_ = Utils::ExtractFloat16NBO(buffer); +} + +void ImpactSoundMaterialAction::Apply(MaterialContext* context, + const Part* src_part, + const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + assert(context->dynamics.exists()); + assert(context->dynamics->in_process()); + + // For now lets avoid this in low-quality graphics mode (should we make + // a low-quality sound mode?) + if (g_graphics_server + && g_graphics_server->quality() < GraphicsQuality::kMedium) { + return; + } + + // Let's only process impact-sounds a bit after the last one finished. + // (cut down on processing) + if (context->dynamics->process_real_time() + - context->dynamics->last_impact_sound_time() + > 100) { + assert(!sounds.empty()); + context->impact_sounds.emplace_back( + context, sounds[rand() % sounds.size()].get(), // NOLINT + target_impulse_, volume_); + context->complex_sound = true; + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/impact_sound_material_action.h b/src/ballistica/dynamics/material/impact_sound_material_action.h new file mode 100644 index 00000000..b64368c9 --- /dev/null +++ b/src/ballistica/dynamics/material/impact_sound_material_action.h @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +// A sound created based on collision forces parallel to the collision normal. +class ImpactSoundMaterialAction : public MaterialAction { + public: + ImpactSoundMaterialAction() = default; + ImpactSoundMaterialAction(const std::vector& sounds_in, + float target_impulse_in, float volume_in) + : sounds(PointersToRefs(sounds_in)), + target_impulse_(target_impulse_in), + volume_(volume_in) {} + std::vector > sounds; + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override { return Type::IMPACT_SOUND; } + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, GameStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; + + private: + float target_impulse_{}; + float volume_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/material.cc b/src/ballistica/dynamics/material/material.cc new file mode 100644 index 00000000..4be741ee --- /dev/null +++ b/src/ballistica/dynamics/material/material.cc @@ -0,0 +1,81 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/material.h" + +#include +#include + +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/python/python_sys.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +Material::Material(std::string name_in, Scene* scene) + : label_(std::move(name_in)), scene_(scene) { + // If we're being made in a scene with an output stream, + // write ourself to it. + assert(scene); + if (GameStream* os = scene->GetGameStream()) { + os->AddMaterial(this); + } +} + +void Material::MarkDead() { + if (dead_) { + return; + } + components_.clear(); + + // If we're in a scene with an output-stream, inform them of our demise. + Scene* scene = scene_.get(); + if (scene) { + if (GameStream* os = scene->GetGameStream()) { + os->RemoveMaterial(this); + } + } + dead_ = true; +} + +auto Material::GetPyRef(bool new_ref) -> PyObject* { + if (!py_object_) { + throw Exception("This material is not associated with a python object"); + } + if (new_ref) { + Py_INCREF(py_object_); + } + return py_object_; +} + +Material::~Material() { MarkDead(); } + +void Material::Apply(MaterialContext* s, const Part* src_part, + const Part* dst_part) { + // Apply all applicable components to the context. + for (auto& component : components_) { + if (component->eval_conditions(component->conditions, *this, src_part, + dst_part, *s)) { + component->Apply(s, src_part, dst_part); + } + } +} + +void Material::AddComponent(const Object::Ref& c) { + // If there's an output stream, push this to that first + if (GameStream* output_stream = scene()->GetGameStream()) { + output_stream->AddMaterialComponent(this, c.get()); + } + components_.push_back(c); +} + +void Material::DumpComponents(GameStream* out) { + for (auto& i : components_) { + assert(i.exists()); + out->AddMaterialComponent(this, i.get()); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/material.h b/src/ballistica/dynamics/material/material.h new file mode 100644 index 00000000..e1ea134b --- /dev/null +++ b/src/ballistica/dynamics/material/material.h @@ -0,0 +1,61 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_H_ + +#include +#include + +#include "ballistica/media/component/sound.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +/// A material defines actions that occur when a part collides with another part +/// (or separates from it after colliding). Materials can set up any number of +/// actions to occur dependent on what opposing materials are being hit, what +/// nodes are being hit, etc. +class Material : public Object { + public: + Material(std::string name, Scene* scene); + ~Material() override; + + /// Add a new component to the material. + /// Pass a component allocated via new. + void AddComponent(const Object::Ref& c); + + /// Apply the material to a context. + void Apply(MaterialContext* s, const Part* src_part, const Part* dst_part); + auto label() const -> const std::string& { return label_; } + auto hasPyObject() const -> bool { return (py_object_ != nullptr); } + auto NewPyRef() -> PyObject* { return GetPyRef(true); } + auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } + void MarkDead(); + auto scene() const -> Scene* { return scene_.get(); } + void DumpComponents(GameStream* out); + auto stream_id() const -> int64_t { return stream_id_; } + void set_stream_id(int64_t val) { + assert(stream_id_ == -1); + stream_id_ = val; + } + void clear_stream_id() { + assert(stream_id_ != -1); + stream_id_ = -1; + } + void set_py_object(PyObject* obj) { py_object_ = obj; } + auto py_object() const -> PyObject* { return py_object_; } + + private: + bool dead_ = false; + int64_t stream_id_ = -1; + Object::WeakRef scene_; + PyObject* py_object_ = nullptr; + auto GetPyRef(bool new_ref = true) -> PyObject*; + std::string label_; + std::vector > components_; + friend class ClientSession; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_H_ diff --git a/src/ballistica/dynamics/material/material_action.h b/src/ballistica/dynamics/material/material_action.h new file mode 100644 index 00000000..443992fb --- /dev/null +++ b/src/ballistica/dynamics/material/material_action.h @@ -0,0 +1,51 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ + +#include "ballistica/core/object.h" + +namespace ballistica { + +class MaterialAction : public Object { + public: + enum class Type { + NODE_MESSAGE, + SCRIPT_COMMAND, + SCRIPT_CALL, + SOUND, + IMPACT_SOUND, + SKID_SOUND, + ROLL_SOUND, + NODE_MOD, + PART_MOD, + NODE_USER_MESSAGE + }; + MaterialAction() = default; + virtual auto GetType() const -> Type = 0; + virtual void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) = 0; + virtual void Execute(Node* node1, Node* node2, Scene* scene) {} + virtual auto GetFlattenedSize() -> size_t { return 0; } + virtual void Flatten(char** buffer, GameStream* output_stream) {} + virtual void Restore(const char** buffer, ClientSession* cs) {} + auto IsNeededOnClient() -> bool { + switch (GetType()) { + case Type::NODE_MESSAGE: + case Type::SOUND: + case Type::IMPACT_SOUND: + case Type::SKID_SOUND: + case Type::ROLL_SOUND: + case Type::NODE_MOD: + case Type::PART_MOD: + return true; + default: + return false; + } + } +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/material_component.cc b/src/ballistica/dynamics/material/material_component.cc new file mode 100644 index 00000000..3284d5ff --- /dev/null +++ b/src/ballistica/dynamics/material/material_component.cc @@ -0,0 +1,227 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/material_component.h" + +#include "ballistica/dynamics/material/impact_sound_material_action.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/dynamics/material/node_message_material_action.h" +#include "ballistica/dynamics/material/node_mod_material_action.h" +#include "ballistica/dynamics/material/part_mod_material_action.h" +#include "ballistica/dynamics/material/python_call_material_action.h" +#include "ballistica/dynamics/material/roll_sound_material_action.h" +#include "ballistica/dynamics/material/skid_sound_material_action.h" +#include "ballistica/dynamics/material/sound_material_action.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/generic/utils.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +auto MaterialComponent::eval_conditions( + const Object::Ref& condition, const Material& c, + const Part* part, const Part* opposing_part, const MaterialContext& s) + -> bool { + // If there's no condition, succeed. + if (!condition.exists()) { + return true; + } + + // If we're a leaf node, evaluate. + if (condition->opmode == MaterialConditionNode::OpMode::LEAF_NODE) { + switch (condition->cond) { + case MaterialCondition::kTrue: + return true; + case MaterialCondition::kFalse: + return false; + case MaterialCondition::kDstIsMaterial: + return ( + (opposing_part->ContainsMaterial(condition->val1_material.get()))); + case MaterialCondition::kDstNotMaterial: + return ( + !(opposing_part->ContainsMaterial(condition->val1_material.get()))); + case MaterialCondition::kDstIsPart: + return ((opposing_part->id() == condition->val1)); + case MaterialCondition::kDstNotPart: + return opposing_part->id() != condition->val1; + case MaterialCondition::kSrcDstSameMaterial: + return ((opposing_part->ContainsMaterial(&c))); + case MaterialCondition::kSrcDstDiffMaterial: + return (!(opposing_part->ContainsMaterial(&c))); + case MaterialCondition::kSrcDstSameNode: + return ((opposing_part->node() == part->node())); + case MaterialCondition::kSrcDstDiffNode: + return opposing_part->node() != part->node(); + case MaterialCondition::kSrcYoungerThan: + return part->GetAge() < condition->val1; + case MaterialCondition::kSrcOlderThan: + return ((part->GetAge() >= condition->val1)); + case MaterialCondition::kDstYoungerThan: + return opposing_part->GetAge() < condition->val1; + case MaterialCondition::kDstOlderThan: + return ((opposing_part->GetAge() >= condition->val1)); + case MaterialCondition::kCollidingDstNode: + return (part->IsCollidingWith(opposing_part->node()->id())); + case MaterialCondition::kNotCollidingDstNode: + return (!(part->IsCollidingWith(opposing_part->node()->id()))); + case MaterialCondition::kEvalColliding: + return s.collide && s.node_collide; + case MaterialCondition::kEvalNotColliding: + return (!s.collide || !s.node_collide); + default: + throw Exception(); + } + } else { + // A trunk node; eval our left and right children and return + // the boolean operation between them. + assert(condition->left_child.exists()); + assert(condition->right_child.exists()); + + bool left_result = + eval_conditions(condition->left_child, c, part, opposing_part, s); + + // In some cases we don't even need to calc the right result. + switch (condition->opmode) { + case MaterialConditionNode::OpMode::AND_OPERATOR: + // AND can't succeed if left is false. + if (!left_result) return false; + break; + case MaterialConditionNode::OpMode::OR_OPERATOR: + // OR has succeeded if we've got a true. + if (left_result) return true; + break; + default: + break; + } + + bool right_result = + eval_conditions(condition->right_child, c, part, opposing_part, s); + + switch (condition->opmode) { + case MaterialConditionNode::OpMode::AND_OPERATOR: + return left_result && right_result; + case MaterialConditionNode::OpMode::OR_OPERATOR: + return left_result || right_result; + case MaterialConditionNode::OpMode::XOR_OPERATOR: + return ((left_result && !right_result) + || (!left_result && right_result)); + default: + throw Exception(); + } + } +} + +auto MaterialComponent::GetFlattenedSize() -> size_t { + size_t size{}; + + // Embed a byte telling whether we have conditions or not. + size += 1; + + // Embed the size of the condition tree. + if (conditions.exists()) { + size += conditions->GetFlattenedSize(); + } + + // An int32 for the action count. + size += sizeof(uint32_t); + + // Plus the total size of all actions. + for (auto& action : actions) { + if (action->IsNeededOnClient()) { + // 1 type byte plus the action's size. + size += 1 + action->GetFlattenedSize(); + } + } + return size; +} + +void MaterialComponent::Flatten(char** buffer, GameStream* output_stream) { + // Embed a byte telling whether we have conditions. + Utils::EmbedInt8(buffer, conditions.exists()); + + // If we have conditions, have the tree embed itself. + if (conditions.exists()) { + conditions->Flatten(buffer, output_stream); + } + + // Embed our action count; we have to manually go through and count + // actions that we'll be sending. + int count = 0; + for (auto& action : actions) { + if ((*action).IsNeededOnClient()) { + assert((*action).GetType() != MaterialAction::Type::NODE_USER_MESSAGE); + count++; + } + } + Utils::EmbedInt32NBO(buffer, count); + + // Embed our actions. + for (auto& action : actions) { + if ((*action).IsNeededOnClient()) { + Utils::EmbedInt8(buffer, static_cast((*action).GetType())); + (*action).Flatten(buffer, output_stream); + } + } +} + +void MaterialComponent::Restore(const char** buffer, ClientSession* cs) { + // Pull the byte telling us if we have conditions. + bool haveConditions = Utils::ExtractInt8(buffer); + + // If there's conditions, create a condition node and have it extract itself. + if (haveConditions) { + conditions = Object::New(); + conditions->Restore(buffer, cs); + } + + // Pull our action count. + int action_count = Utils::ExtractInt32NBO(buffer); + + // Restore all actions. + for (int i = 0; i < action_count; i++) { + // Pull the action type. + auto type = static_cast(Utils::ExtractInt8(buffer)); + Object::Ref action; + switch (type) { + case MaterialAction::Type::NODE_MESSAGE: + action = Object::New(); + break; + case MaterialAction::Type::SOUND: + action = Object::New(); + break; + case MaterialAction::Type::IMPACT_SOUND: + action = Object::New(); + break; + case MaterialAction::Type::SKID_SOUND: + action = Object::New(); + break; + case MaterialAction::Type::ROLL_SOUND: + action = Object::New(); + break; + case MaterialAction::Type::PART_MOD: + action = Object::New(); + break; + case MaterialAction::Type::NODE_MOD: + action = Object::New(); + break; + default: + Log("Error: Invalid material action: '" + + std::to_string(static_cast(type)) + "'."); + throw Exception(); + } + action->Restore(buffer, cs); + actions.push_back(action); + } +} + +void MaterialComponent::Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part) { + assert(context && src_part && dst_part); + for (auto& action : actions) { + (*action).Apply(context, src_part, dst_part, action); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/material_component.h b/src/ballistica/dynamics/material/material_component.h new file mode 100644 index 00000000..fba0df8f --- /dev/null +++ b/src/ballistica/dynamics/material/material_component.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ + +#include +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +// A component of a material - comprises one or more conditions and actions. +class MaterialComponent : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kGame; + } + + auto GetFlattenedSize() -> size_t; + void Flatten(char** buffer, GameStream* output_stream); + void Restore(const char** buffer, ClientSession* cs); + + // Actions are stored as shared pointers so references + // to them can be stored with pending events + // in case the component is deleted before they are run. + std::vector > actions; + Object::Ref conditions; + auto eval_conditions(const Object::Ref& condition, + const Material& c, const Part* part, + const Part* opposing_part, const MaterialContext& s) + -> bool; + + // Apply the component to a context. + void Apply(MaterialContext* c, const Part* src_part, const Part* dst_part); + MaterialComponent() = default; + MaterialComponent(const Object::Ref& conditions_in, + std::vector > actions_in) + : conditions(conditions_in), actions(std::move(actions_in)) {} +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ diff --git a/src/ballistica/dynamics/material/material_condition_node.cc b/src/ballistica/dynamics/material/material_condition_node.cc new file mode 100644 index 00000000..22300cdc --- /dev/null +++ b/src/ballistica/dynamics/material/material_condition_node.cc @@ -0,0 +1,90 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/material_condition_node.h" + +#include "ballistica/dynamics/material/material.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/session/client_session.h" +#include "ballistica/generic/utils.h" + +namespace ballistica { + +auto MaterialConditionNode::GetFlattenedSize() -> size_t { + // we need one byte for our opmode + // plus the condition byte and either 0, 1, or 2 values depending on our + // condition if we're a leaf node, otherwise add the size of our children + size_t size = 1; + if (opmode == OpMode::LEAF_NODE) { + size += 1 + sizeof(uint32_t) * GetValueCount(); + } else { + size += (left_child->GetFlattenedSize() + right_child->GetFlattenedSize()); + } + return size; +} + +void MaterialConditionNode::Flatten(char** buffer, GameStream* output_stream) { + // Pack our opmode in. Or if we're a leaf note stick zero in. + Utils::EmbedInt8(buffer, static_cast(opmode)); + if (opmode == OpMode::LEAF_NODE) { + Utils::EmbedInt8(buffer, static_cast(cond)); + switch (GetValueCount()) { + case 0: + break; + case 1: { + // If this condition uses the material val1, embed its stream ID + if (cond == MaterialCondition::kDstIsMaterial + || cond == MaterialCondition::kDstNotMaterial) { + Utils::EmbedInt32NBO( + buffer, static_cast_check_fit( + output_stream->GetMaterialID(val1_material.get()))); + } else { + Utils::EmbedInt32NBO(buffer, val1); + } + break; + } + case 2: + Utils::EmbedInt32NBO(buffer, val1); + Utils::EmbedInt32NBO(buffer, val2); + break; + default: + throw Exception(); + } + } else { + left_child->Flatten(buffer, output_stream); + right_child->Flatten(buffer, output_stream); + } +} + +void MaterialConditionNode::Restore(const char** buffer, ClientSession* cs) { + opmode = static_cast(Utils::ExtractInt8(buffer)); + if (opmode == OpMode::LEAF_NODE) { + cond = static_cast(Utils::ExtractInt8(buffer)); + int val_count = GetValueCount(); + switch (val_count) { + case 0: + break; + case 1: + if (cond == MaterialCondition::kDstIsMaterial + || cond == MaterialCondition::kDstNotMaterial) { + val1_material = cs->GetMaterial(Utils::ExtractInt32NBO(buffer)); + } else { + val1 = Utils::ExtractInt32NBO(buffer); + } + break; + case 2: + val1 = Utils::ExtractInt32NBO(buffer); + val2 = Utils::ExtractInt32NBO(buffer); + break; + default: + throw Exception(); + } + } else { + // not a leaf node - make ourself some children + left_child = Object::New(); + left_child->Restore(buffer, cs); + right_child = Object::New(); + right_child->Restore(buffer, cs); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/material_condition_node.h b/src/ballistica/dynamics/material/material_condition_node.h new file mode 100644 index 00000000..0764bf31 --- /dev/null +++ b/src/ballistica/dynamics/material/material_condition_node.h @@ -0,0 +1,59 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/core/object.h" + +namespace ballistica { + +class MaterialConditionNode : public Object { + public: + enum class OpMode { LEAF_NODE = 0, AND_OPERATOR, OR_OPERATOR, XOR_OPERATOR }; + Object::Ref left_child; + Object::Ref right_child; + OpMode opmode{}; + MaterialCondition cond{}; + int val1{}; + Object::Ref val1_material; + int val2{}; + + // Return the number of values used by this node + // assumes the node is a leaf node. + auto GetValueCount() -> int { + assert(opmode == OpMode::LEAF_NODE); + switch (cond) { + case MaterialCondition::kTrue: + case MaterialCondition::kFalse: + case MaterialCondition::kSrcDstSameMaterial: + case MaterialCondition::kSrcDstDiffMaterial: + case MaterialCondition::kSrcDstSameNode: + case MaterialCondition::kSrcDstDiffNode: + case MaterialCondition::kCollidingDstNode: + case MaterialCondition::kNotCollidingDstNode: + case MaterialCondition::kEvalColliding: + case MaterialCondition::kEvalNotColliding: + return 0; + case MaterialCondition::kDstIsMaterial: + case MaterialCondition::kDstNotMaterial: + case MaterialCondition::kSrcYoungerThan: + case MaterialCondition::kSrcOlderThan: + case MaterialCondition::kDstYoungerThan: + case MaterialCondition::kDstOlderThan: + return 1; + case MaterialCondition::kDstIsPart: + case MaterialCondition::kDstNotPart: + return 2; + default: + throw Exception(); + } + } + auto GetFlattenedSize() -> size_t; + void Flatten(char** buffer, GameStream* output_stream); + void Restore(const char** buffer, ClientSession* cs); +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ diff --git a/src/ballistica/dynamics/material/material_context.cc b/src/ballistica/dynamics/material/material_context.cc new file mode 100644 index 00000000..5c901ec0 --- /dev/null +++ b/src/ballistica/dynamics/material/material_context.cc @@ -0,0 +1,93 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/material_context.h" + +#include "ballistica/audio/audio.h" +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +MaterialContext::MaterialContext(Scene* scene) + : dynamics(scene->dynamics()), + friction(1.0f), + stiffness(1.0f), + damping(1.0f), + bounce(0), + collide(true), + node_collide(true), + use_node_collide(true), + physical(true), + complex_sound(false) {} + +MaterialContext::SkidSoundEntry::SkidSoundEntry( + const MaterialContext::SkidSoundEntry& other) { + *this = other; + assert(context); +#if BA_DEBUG_BUILD + assert(context->dynamics.exists()); +#endif + assert(context->dynamics->in_process()); + context->dynamics->increment_skid_sound_count(); +} + +MaterialContext::SkidSoundEntry::SkidSoundEntry(MaterialContext* context_in, + Sound* sound_in, + float target_impulse_in, + float volume_in) + : context(context_in), + sound(sound_in), + target_impulse(target_impulse_in), + volume(volume_in), + playing(false) { + assert(context); + assert(context->dynamics.exists()); + assert(context->dynamics->in_process()); + context->dynamics->increment_skid_sound_count(); +} + +MaterialContext::SkidSoundEntry::~SkidSoundEntry() { + assert(context); + assert(context->dynamics.exists()); + context->dynamics->decrement_skid_sound_count(); + if (playing) { + g_audio->PushSourceFadeOutCall(play_id, 200); + } +} + +MaterialContext::RollSoundEntry::RollSoundEntry(MaterialContext* context_in, + Sound* sound_in, + float target_impulse_in, + float volume_in) + : context(context_in), + sound(sound_in), + target_impulse(target_impulse_in), + volume(volume_in), + playing(false) { + assert(context); + assert(context->dynamics.exists()); + assert(context->dynamics->in_process()); + context->dynamics->incrementRollSoundCount(); +} + +MaterialContext::RollSoundEntry::RollSoundEntry( + const MaterialContext::RollSoundEntry& other) { + *this = other; + assert(context); + assert(context->dynamics.exists()); + assert(context->dynamics->in_process()); + context->dynamics->incrementRollSoundCount(); +} + +MaterialContext::RollSoundEntry::~RollSoundEntry() { + assert(context); + assert(context->dynamics.exists()); + context->dynamics->decrement_roll_sound_count(); + if (playing) { + g_audio->PushSourceFadeOutCall(play_id, 200); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/material_context.h b/src/ballistica/dynamics/material/material_context.h new file mode 100644 index 00000000..b25765b9 --- /dev/null +++ b/src/ballistica/dynamics/material/material_context.h @@ -0,0 +1,90 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ + +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +// Contexts materials use when getting and setting collision data +class MaterialContext { + public: + BA_DEBUG_PTR(Dynamics) dynamics; + float friction{}; + float stiffness{}; + float damping{}; + float bounce{}; + bool collide{}; + bool node_collide{}; + bool use_node_collide{}; + bool physical{}; + + // This should get set to true if + // anything is added to impact_sounds, skid_sounds, or roll_sounds. + // This way we know to calculate collision forces, relative velocities, etc. + bool complex_sound{}; + std::vector > connect_actions; + std::vector > disconnect_actions; + struct SoundEntry { + Object::Ref sound; + float volume; + SoundEntry(Sound* sound_in, float volume_in) + : sound(sound_in), volume(volume_in) {} + }; + class ImpactSoundEntry { + public: + MaterialContext* context; + Object::Ref sound; + float volume; + float target_impulse; + ImpactSoundEntry(MaterialContext* context, Sound* sound_in, + float target_impulse_in, float volume_in) + : context(context), + sound(sound_in), + target_impulse(target_impulse_in), + volume(volume_in) {} + }; + class SkidSoundEntry { + public: + MaterialContext* context{}; + Object::Ref sound; + float volume{}; + float target_impulse{}; + // Used to keep track of the playing sound. + uint32_t play_id{}; + bool playing{}; + SkidSoundEntry(MaterialContext* context, Sound* sound_in, + float target_impulse_in, float volume_in); + ~SkidSoundEntry(); + SkidSoundEntry(const SkidSoundEntry& other); + }; + class RollSoundEntry { + public: + MaterialContext* context{}; + Object::Ref sound; + float volume{}; + float target_impulse{}; + // Used to keep track of the playing sound. + uint32_t play_id{}; + bool playing{}; + RollSoundEntry(MaterialContext* context, Sound* sound_in, + float target_impulse_in, float volume_in); + RollSoundEntry(const RollSoundEntry& other); + ~RollSoundEntry(); + }; + std::vector connect_sounds; + std::vector impact_sounds; + std::vector skid_sounds; + std::vector roll_sounds; + explicit MaterialContext(Scene* scene_in); + + private: + BA_DISALLOW_CLASS_COPIES(MaterialContext); +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ diff --git a/src/ballistica/dynamics/material/node_message_material_action.cc b/src/ballistica/dynamics/material/node_message_material_action.cc new file mode 100644 index 00000000..3baf8190 --- /dev/null +++ b/src/ballistica/dynamics/material/node_message_material_action.cc @@ -0,0 +1,45 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/node_message_material_action.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { +NodeMessageMaterialAction::NodeMessageMaterialAction(bool target_other_in, + bool at_disconnect_in, + const char* data_in, + size_t length_in) + : target_other(target_other_in), + at_disconnect(at_disconnect_in), + data(data_in, length_in) { + assert(length_in > 0); +} + +void NodeMessageMaterialAction::Apply(MaterialContext* context, + const Part* src_part, + const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + if (at_disconnect) { + context->disconnect_actions.push_back(p); + } else { + context->connect_actions.push_back(p); + } +} + +void NodeMessageMaterialAction::Execute(Node* node1, Node* node2, + Scene* scene) { + Node* node = target_other ? node2 : node1; + if (node) { + scene->dynamics()->set_collide_message_state(true, target_other); + assert(node); + assert(data.data()); + node->DispatchNodeMessage(data.data()); + scene->dynamics()->set_collide_message_state(false); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/node_message_material_action.h b/src/ballistica/dynamics/material/node_message_material_action.h new file mode 100644 index 00000000..eb1ecd85 --- /dev/null +++ b/src/ballistica/dynamics/material/node_message_material_action.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/generic/buffer.h" + +namespace ballistica { + +// Regular message. +class NodeMessageMaterialAction : public MaterialAction { + public: + NodeMessageMaterialAction() = default; + NodeMessageMaterialAction(bool target_other_in, bool at_disconnect_in, + const char* data_in, size_t length_in); + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + void Execute(Node* node1, Node* node2, Scene* scene) override; + bool target_other{}; + bool at_disconnect{}; + Buffer data; + auto GetType() const -> Type override { return Type::NODE_MESSAGE; } + auto GetFlattenedSize() -> size_t override { + // 1 byte for bools + data + return static_cast(1 + data.GetFlattenedSize()); + } + void Flatten(char** buffer, GameStream* output_stream) override { + Utils::EmbedBools(buffer, target_other, at_disconnect); + data.embed(buffer); + } + void Restore(const char** buffer, ClientSession* cs) override { + Utils::ExtractBools(buffer, &target_other, &at_disconnect); + data.Extract(buffer); + } +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/node_mod_material_action.cc b/src/ballistica/dynamics/material/node_mod_material_action.cc new file mode 100644 index 00000000..f15a8c0b --- /dev/null +++ b/src/ballistica/dynamics/material/node_mod_material_action.cc @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/node_mod_material_action.h" + +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/generic/utils.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +auto NodeModMaterialAction::GetType() const -> MaterialAction::Type { + return Type::NODE_MOD; +} + +auto NodeModMaterialAction::GetFlattenedSize() -> size_t { return 1 + 4; } + +void NodeModMaterialAction::Flatten(char** buffer, GameStream* output_stream) { + Utils::EmbedInt8(buffer, static_cast(attr)); + Utils::EmbedFloat32(buffer, attr_val); +} + +void NodeModMaterialAction::Restore(const char** buffer, ClientSession* cs) { + attr = static_cast(Utils::ExtractInt8(buffer)); + attr_val = Utils::ExtractFloat32(buffer); +} + +void NodeModMaterialAction::Apply(MaterialContext* context, + const Part* src_part, const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + // Go ahead and make our modification to the context. + switch (attr) { + case NodeCollideAttr::kCollideNode: + context->node_collide = static_cast(attr_val); + break; + default: + throw Exception(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/node_mod_material_action.h b/src/ballistica/dynamics/material/node_mod_material_action.h new file mode 100644 index 00000000..27fd9a3d --- /dev/null +++ b/src/ballistica/dynamics/material/node_mod_material_action.h @@ -0,0 +1,28 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ + +#include "ballistica/dynamics/material/material_action.h" + +namespace ballistica { + +class NodeModMaterialAction : public MaterialAction { + public: + NodeModMaterialAction() = default; + NodeModMaterialAction(NodeCollideAttr attr_in, float attr_val_in) + : attr(attr_in), attr_val(attr_val_in) {} + NodeCollideAttr attr{}; + float attr_val{}; + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override; + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, GameStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/node_user_message_material_action.cc b/src/ballistica/dynamics/material/node_user_message_material_action.cc new file mode 100644 index 00000000..01a1f044 --- /dev/null +++ b/src/ballistica/dynamics/material/node_user_message_material_action.cc @@ -0,0 +1,61 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/node_user_message_material_action.h" + +#include "ballistica/core/context.h" +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/scene/node/node.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +NodeUserMessageMaterialAction::NodeUserMessageMaterialAction( + bool target_other_in, bool at_disconnect_in, PyObject* user_message_obj_in) + : target_other(target_other_in), at_disconnect(at_disconnect_in) { + user_message_obj.Acquire(user_message_obj_in); +} + +void NodeUserMessageMaterialAction::Apply( + MaterialContext* context, const Part* src_part, const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + if (at_disconnect) { + context->disconnect_actions.push_back(p); + } else { + context->connect_actions.push_back(p); + } +} + +NodeUserMessageMaterialAction::~NodeUserMessageMaterialAction() = default; + +void NodeUserMessageMaterialAction::Execute(Node* node1, Node* node2, + Scene* scene) { + // See who they want to send the message to. + Node* target_node = target_other ? node2 : node1; + + if (!at_disconnect) { + // Only deliver 'connect' messages if both nodes still exist. + // This way handlers can avoid having to deal with that ultra-rare + // corner case. + if (!node1 || !node2) { + return; + } + } else { + // Deliver 'disconnect' messages if the target node still exists + // even if the opposing one doesn't. Nodes should always know when + // they stop colliding even if it was through death. + if (!target_node) { + return; + } + } + + ScopedSetContext cp(target_node->context()); + scene->dynamics()->set_collide_message_state(true, target_other); + target_node->DispatchUserMessage(user_message_obj.get(), + "Material User-Message dispatch"); + scene->dynamics()->set_collide_message_state(false); +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/node_user_message_material_action.h b/src/ballistica/dynamics/material/node_user_message_material_action.h new file mode 100644 index 00000000..bed94691 --- /dev/null +++ b/src/ballistica/dynamics/material/node_user_message_material_action.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_NODE_USER_MESSAGE_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_NODE_USER_MESSAGE_MATERIAL_ACTION_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +// a user message - encapsulates a python object +class NodeUserMessageMaterialAction : public MaterialAction { + public: + NodeUserMessageMaterialAction(bool target_other, bool at_disconnect, + PyObject* user_message); + ~NodeUserMessageMaterialAction() override; + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + void Execute(Node* node1, Node* node2, Scene* scene) override; + bool target_other; + bool at_disconnect; + PythonRef user_message_obj; + auto GetType() const -> Type override { return Type::NODE_USER_MESSAGE; } +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_NODE_USER_MESSAGE_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/part_mod_material_action.cc b/src/ballistica/dynamics/material/part_mod_material_action.cc new file mode 100644 index 00000000..e5bb02c6 --- /dev/null +++ b/src/ballistica/dynamics/material/part_mod_material_action.cc @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/part_mod_material_action.h" + +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/generic/utils.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +auto PartModMaterialAction::GetType() const -> MaterialAction::Type { + return Type::PART_MOD; +} + +auto PartModMaterialAction::GetFlattenedSize() -> size_t { return 1 + 4; } + +void PartModMaterialAction::Flatten(char** buffer, GameStream* output_stream) { + Utils::EmbedInt8(buffer, static_cast(attr)); + Utils::EmbedFloat32(buffer, attr_val); +} + +void PartModMaterialAction::Restore(const char** buffer, ClientSession* cs) { + attr = static_cast(Utils::ExtractInt8(buffer)); + attr_val = Utils::ExtractFloat32(buffer); +} + +void PartModMaterialAction::Apply(MaterialContext* context, + const Part* src_part, const Part* dst_part, + const Object::Ref& p) { + // Go ahead and make our modification to the context. + switch (attr) { + case PartCollideAttr::kCollide: + context->collide = static_cast(attr_val); + break; + case PartCollideAttr::kUseNodeCollide: + context->use_node_collide = static_cast(attr_val); + break; + case PartCollideAttr::kPhysical: + context->physical = static_cast(attr_val); + break; + case PartCollideAttr::kFriction: + context->friction = attr_val; + break; + case PartCollideAttr::kStiffness: + context->stiffness = attr_val; + break; + case PartCollideAttr::kDamping: + context->damping = attr_val; + break; + case PartCollideAttr::kBounce: + context->bounce = attr_val; + break; + default: + throw Exception(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/part_mod_material_action.h b/src/ballistica/dynamics/material/part_mod_material_action.h new file mode 100644 index 00000000..151e0dde --- /dev/null +++ b/src/ballistica/dynamics/material/part_mod_material_action.h @@ -0,0 +1,28 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ + +#include "ballistica/dynamics/material/material_action.h" + +namespace ballistica { + +class PartModMaterialAction : public MaterialAction { + public: + PartModMaterialAction() = default; + PartModMaterialAction(PartCollideAttr attr_in, float attr_val_in) + : attr(attr_in), attr_val(attr_val_in) {} + PartCollideAttr attr{}; + float attr_val{}; + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override; + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, GameStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/python_call_material_action.cc b/src/ballistica/dynamics/material/python_call_material_action.cc new file mode 100644 index 00000000..d711b516 --- /dev/null +++ b/src/ballistica/dynamics/material/python_call_material_action.cc @@ -0,0 +1,51 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/python_call_material_action.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/dynamics/material/sound_material_action.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +PythonCallMaterialAction::PythonCallMaterialAction(bool at_disconnect_in, + PyObject* call_obj_in) + : at_disconnect(at_disconnect_in), + call(Object::New(call_obj_in)) {} + +void PythonCallMaterialAction::Apply(MaterialContext* context, + const Part* src_part, const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + if (at_disconnect) { + context->disconnect_actions.push_back(p); + } else { + context->connect_actions.push_back(p); + } +} + +void PythonCallMaterialAction::Execute(Node* node1, Node* node2, Scene* scene) { + scene->dynamics()->set_collide_message_state(true, false); + + // Only run connect commands if both nodes still exist. + // This way most collision commands can assume both + // members of the collision exist. + if (!at_disconnect) { + if (node1 && node2) { + call->Run(); + } + } else { + // Its a disconnect. Run it if the src node still exists + // (nodes should know if they've disconnected from others even if + // it was through death) + if (node1) { + call->Run(); + } + } + scene->dynamics()->set_collide_message_state(false); +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/python_call_material_action.h b/src/ballistica/dynamics/material/python_call_material_action.h new file mode 100644 index 00000000..0a0d86a8 --- /dev/null +++ b/src/ballistica/dynamics/material/python_call_material_action.h @@ -0,0 +1,26 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/python/python_context_call.h" + +namespace ballistica { + +class PythonCallMaterialAction : public MaterialAction { + public: + PythonCallMaterialAction(bool at_disconnect_in, PyObject* call_obj_in); + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + void Execute(Node* node1, Node* node2, Scene* scene) override; + bool at_disconnect; + Object::Ref call; + auto GetType() const -> Type override { return Type::SCRIPT_CALL; } +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/roll_sound_material_action.cc b/src/ballistica/dynamics/material/roll_sound_material_action.cc new file mode 100644 index 00000000..feffbaef --- /dev/null +++ b/src/ballistica/dynamics/material/roll_sound_material_action.cc @@ -0,0 +1,53 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/roll_sound_material_action.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/session/client_session.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +auto RollSoundMaterialAction::GetFlattenedSize() -> size_t { return 4 + 2 + 2; } + +void RollSoundMaterialAction::Flatten(char** buffer, + GameStream* output_stream) { + Utils::EmbedInt32NBO(buffer, static_cast_check_fit( + output_stream->GetSoundID(sound.get()))); + Utils::EmbedFloat16NBO(buffer, target_impulse); + Utils::EmbedFloat16NBO(buffer, volume); +} + +void RollSoundMaterialAction::Restore(const char** buffer, ClientSession* cs) { + sound = cs->GetSound(Utils::ExtractInt32NBO(buffer)); + target_impulse = Utils::ExtractFloat16NBO(buffer); + volume = Utils::ExtractFloat16NBO(buffer); +} + +void RollSoundMaterialAction::Apply(MaterialContext* context, + const Part* src_part, const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + assert(context->dynamics.exists()); + assert(context->dynamics->in_process()); + + // For now lets avoid this in low-quality graphics mode + // (should we make a low-quality sound mode?) + if (g_graphics && g_graphics_server->quality() < GraphicsQuality::kMedium) { + return; + } + + // Let's limit the amount of skid-sounds we spawn, otherwise we'll + // start using up all our sound resources on skids when things get messy + if (context->dynamics->getRollSoundCount() < 2) { + context->roll_sounds.emplace_back(context, sound.get(), target_impulse, + volume); + context->complex_sound = true; + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/roll_sound_material_action.h b/src/ballistica/dynamics/material/roll_sound_material_action.h new file mode 100644 index 00000000..93a373c2 --- /dev/null +++ b/src/ballistica/dynamics/material/roll_sound_material_action.h @@ -0,0 +1,32 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" + +namespace ballistica { + +// Sound created based on velocity perpendicular to the collision normal. +class RollSoundMaterialAction : public MaterialAction { + public: + RollSoundMaterialAction() = default; + RollSoundMaterialAction(Sound* sound_in, float target_impulse_in, + float volume_in) + : sound(sound_in), target_impulse(target_impulse_in), volume(volume_in) {} + Object::Ref sound; + float target_impulse{}; + float volume{}; + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override { return Type::ROLL_SOUND; } + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, GameStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/skid_sound_material_action.cc b/src/ballistica/dynamics/material/skid_sound_material_action.cc new file mode 100644 index 00000000..8cf6a3e3 --- /dev/null +++ b/src/ballistica/dynamics/material/skid_sound_material_action.cc @@ -0,0 +1,54 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/skid_sound_material_action.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/session/client_session.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +auto SkidSoundMaterialAction::GetFlattenedSize() -> size_t { return 4 + 2 + 2; } + +void SkidSoundMaterialAction::Flatten(char** buffer, + GameStream* output_stream) { + Utils::EmbedInt32NBO(buffer, static_cast_check_fit( + output_stream->GetSoundID(sound.get()))); + Utils::EmbedFloat16NBO(buffer, target_impulse); + Utils::EmbedFloat16NBO(buffer, volume); +} + +void SkidSoundMaterialAction::Restore(const char** buffer, ClientSession* cs) { + sound = cs->GetSound(Utils::ExtractInt32NBO(buffer)); + target_impulse = Utils::ExtractFloat16NBO(buffer); + volume = Utils::ExtractFloat16NBO(buffer); +} + +void SkidSoundMaterialAction::Apply(MaterialContext* context, + const Part* src_part, const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + assert(context->dynamics.exists()); + assert(context->dynamics->in_process()); + + // For now lets avoid this in low-quality graphics mode + // (should we make a low-quality sound mode?). + if (g_graphics_server + && g_graphics_server->quality() < GraphicsQuality::kMedium) { + return; + } + + // Let's limit the amount of skid-sounds we spawn, otherwise we'll start + // using up all our sound resources on skids when things get messy. + if (context->dynamics->skid_sound_count() < 2) { + context->skid_sounds.emplace_back(context, sound.get(), target_impulse, + volume); + context->complex_sound = true; + } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/skid_sound_material_action.h b/src/ballistica/dynamics/material/skid_sound_material_action.h new file mode 100644 index 00000000..2179e426 --- /dev/null +++ b/src/ballistica/dynamics/material/skid_sound_material_action.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +// sound created based on collision forces perpendicular to the collision normal +class SkidSoundMaterialAction : public MaterialAction { + public: + SkidSoundMaterialAction() = default; + SkidSoundMaterialAction(Sound* sound_in, float target_impulse_in, + float volume_in) + : sound(sound_in), target_impulse(target_impulse_in), volume(volume_in) {} + Object::Ref sound; + float target_impulse{}; + float volume{}; + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override { return Type::SKID_SOUND; } + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, GameStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/sound_material_action.cc b/src/ballistica/dynamics/material/sound_material_action.cc new file mode 100644 index 00000000..e8fefff9 --- /dev/null +++ b/src/ballistica/dynamics/material/sound_material_action.cc @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/material/sound_material_action.h" + +#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/session/client_session.h" +#include "ballistica/generic/utils.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +void SoundMaterialAction::Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) { + assert(context && src_part && dst_part); + context->connect_sounds.emplace_back(sound_.get(), volume_); +} + +auto SoundMaterialAction::GetFlattenedSize() -> size_t { return 4 + 2; } + +void SoundMaterialAction::Flatten(char** buffer, GameStream* output_stream) { + Utils::EmbedInt32NBO(buffer, static_cast_check_fit( + output_stream->GetSoundID(sound_.get()))); + Utils::EmbedFloat16NBO(buffer, volume_); +} + +void SoundMaterialAction::Restore(const char** buffer, ClientSession* cs) { + sound_ = cs->GetSound(Utils::ExtractInt32NBO(buffer)); + volume_ = Utils::ExtractFloat16NBO(buffer); +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/material/sound_material_action.h b/src/ballistica/dynamics/material/sound_material_action.h new file mode 100644 index 00000000..ddd46745 --- /dev/null +++ b/src/ballistica/dynamics/material/sound_material_action.h @@ -0,0 +1,32 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +class SoundMaterialAction : public MaterialAction { + public: + SoundMaterialAction() = default; + SoundMaterialAction(Sound* sound_in, float volume_in) + : sound_(sound_in), volume_(volume_in) {} + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override { return Type::SOUND; } + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, GameStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; + + private: + Object::Ref sound_; + float volume_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/part.cc b/src/ballistica/dynamics/part.cc new file mode 100644 index 00000000..2b7436b3 --- /dev/null +++ b/src/ballistica/dynamics/part.cc @@ -0,0 +1,131 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/part.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/generic/buffer.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +Part::Part(Node* node, bool default_collide) + : our_id_(node->AddPart(this)), + default_collides_(default_collide), + node_(node) { + assert(node_.exists()); + birth_time_ = node_->scene()->time(); + dynamics_ = node_->scene()->dynamics(); +} + +Part::~Part() = default; + +void Part::CheckBodies() { + for (auto&& i : rigid_bodies_) { + i->Check(); + } +} + +void Part::KillConstraints() { + for (auto&& i : rigid_bodies_) { + i->KillConstraints(); + } +} + +void Part::UpdateBirthTime() { birth_time_ = node_->scene()->time(); } + +auto Part::GetMaterials() const -> std::vector { + return RefsToPointers(materials_); +} + +void Part::SetMaterials(const std::vector& vals) { + assert(!Utils::HasNullMembers(vals)); + + // Hold strong refs to the materials passed. + materials_ = PointersToRefs(vals); + + // Wake us up in case our new materials make us stop colliding or whatnot. + // (we may be asleep resting on something we suddenly no longer hit) + Wake(); + + // Reset all of our active collisions so new collisions will take effect + // with the new materials. + for (auto&& i : collisions_) { + dynamics_->ResetCollision(node()->id(), id(), i.node, i.part); + } +} + +void Part::ApplyMaterials(MaterialContext* s, const Part* src_part, + const Part* dst_part) { + for (auto&& i : materials_) { + assert(i.exists()); + i->Apply(s, src_part, dst_part); + } +} + +auto Part::ContainsMaterial(const Material* m) const -> bool { + assert(m); + for (auto&& i : materials_) { + assert(i.exists()); + if (m == i.get()) { + return true; + } + } + return false; +} + +auto Part::IsCollidingWith(int64_t node, int part) const -> bool { + for (auto&& i : collisions_) { + if (i.node == node && i.part == part) return true; + } + return false; +} + +auto Part::IsCollidingWith(int64_t node) const -> bool { + for (auto&& i : collisions_) { + if (i.node == node) { + return true; + } + } + return false; +} + +void Part::SetCollidingWith(int64_t node_id, int part, bool colliding, + bool physical) { + if (colliding) { + // Add this to our list of collisions if its not on it. + for (auto&& i : collisions_) { + if (i.node == node_id && i.part == part) { + BA_PRECONDITION(node()); + Log("Error: Got SetCollidingWith for part already colliding with."); + return; + } + } + collisions_.emplace_back(node_id, part); + + } else { + // Make sure our bodies are awake - we may have been asleep + // resting on something that no longer exists. + if (physical) { + Wake(); + } + + // Remove the part from our colliding-with list. + for (auto i = collisions_.begin(); i != collisions_.end(); ++i) { + if (i->node == node_id && i->part == part) { + collisions_.erase(i); + return; + } + } + Log("Error: Got SetCollidingWith (separated) call for part we're " + "not colliding with."); + } +} + +auto Part::GetAge() const -> millisecs_t { + assert(node_.exists()); + assert(node_->scene()->time() >= birth_time_); + return node_->scene()->time() - birth_time_; +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/part.h b/src/ballistica/dynamics/part.h new file mode 100644 index 00000000..b0bc494f --- /dev/null +++ b/src/ballistica/dynamics/part.h @@ -0,0 +1,139 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_PART_H_ +#define BALLISTICA_DYNAMICS_PART_H_ + +#include + +#include "ballistica/core/object.h" +#include "ballistica/dynamics/rigid_body.h" + +namespace ballistica { + +// A categorized "part" of a node which contains collision and other grouping +// information for a set of rigid bodies composing the part. +// Each rigid body is contained in exactly one part. +class Part : public Object { + public: + explicit Part(Node* node, bool default_collide = true); + ~Part() override; + auto id() const -> int { return our_id_; } + + // Used by RigidBodies when adding themselves to the part. + void AddBody(RigidBody* rigid_body_in) { + rigid_bodies_.push_back(rigid_body_in); + } + + // Used by RigidBodies when removing themselves from the part. + void RemoveBody(RigidBody* rigid_body_in) { + for (auto i = rigid_bodies_.begin(); i != rigid_bodies_.end(); ++i) { + if (*i == rigid_body_in) { + rigid_bodies_.erase(i); + return; + } + } + throw Exception(); + } + + // Wakes up all rigid bodies in the part. + void Wake() { + for (auto&& i : rigid_bodies_) { + i->Wake(); + } + } + auto node() const -> Node* { + assert(node_.exists()); + return node_.get(); + } + + // Apply a set of materials to the part. + // Note than anytime a part's material set is changed, + // All collisions occurring between it and other parts are reset, + // so the old material set's separation commands will run and then + // the new material's collide commands will run (if there is still a + // collision) + void SetMaterials(const std::vector& vals); + auto GetMaterials() const -> std::vector; + + // Apply this part's materials to a context. + void ApplyMaterials(MaterialContext* s, const Part* src_part, + const Part* dst_part); + + // Returns true if the material is directly attached to the part + // note that having a material that calls the requested material does + // not count. + auto ContainsMaterial(const Material* m) const -> bool; + + // Returns whether the part is currently colliding with the specified node. + auto IsCollidingWith(int64_t node) const -> bool; + + // Returns whether the part is currently colliding with the specified + // node/part combo. + auto IsCollidingWith(int64_t node, int part) const -> bool; + + // Used by g_game to inform us we're now colliding with another part + // if colliding is false, we've stopped colliding with this part. + void SetCollidingWith(int64_t node_id, int part, bool colliding, + bool physical); + + // Kill constraints for all bodies in the part + // (useful when teleporting and things like that). + void KillConstraints(); + auto default_collides() const -> bool { return default_collides_; } + auto GetAge() const -> millisecs_t; + + // Birthtime can be used to prevent spawning or teleporting parts from + // colliding with things they are overlapping. + // Any part with teleporting parts should use this to + // reset their birth times. Nodes have a function to do so for all their + // contained parts as well. + void UpdateBirthTime(); + auto last_impact_sound_time() const -> millisecs_t { + return last_impact_sound_time_; + } + auto last_skid_sound_time() const -> millisecs_t { + return last_skid_sound_time_; + } + auto last_roll_sound_time() const -> millisecs_t { + return last_roll_sound_time_; + } + void set_last_impact_sound_time(millisecs_t t) { + last_impact_sound_time_ = t; + } + void set_last_skid_sound_time(millisecs_t t) { last_skid_sound_time_ = t; } + void set_last_roll_sound_time(millisecs_t t) { last_roll_sound_time_ = t; } + + auto rigid_bodies() const -> const std::vector& { + return rigid_bodies_; + } + + // Debugging: check for NaNs and whatnot. + void CheckBodies(); + + private: + Dynamics* dynamics_; + class Collision { + public: + int node; + int part; + Collision(int node_in, int part_in) : node(node_in), part(part_in) {} + }; + + // Collisions currently affecting us stored for quick access. + std::vector collisions_; + bool default_collides_; + millisecs_t birth_time_; + int our_id_; + Object::WeakRef node_; + std::vector > materials_; + std::vector rigid_bodies_; + + // Last time this part played a collide sound (used by the audio system). + millisecs_t last_impact_sound_time_ = 0; + millisecs_t last_skid_sound_time_ = 0; + millisecs_t last_roll_sound_time_ = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_PART_H_ diff --git a/src/ballistica/dynamics/rigid_body.cc b/src/ballistica/dynamics/rigid_body.cc new file mode 100644 index 00000000..95a70fa8 --- /dev/null +++ b/src/ballistica/dynamics/rigid_body.cc @@ -0,0 +1,693 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/dynamics/rigid_body.h" + +#include + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/scene/scene.h" +#include "ode/ode_collision_util.h" + +namespace ballistica { + +// whether to send our net states as half float format +#define USE_HALF_FLOATS 1 + +#define EMBED_POS_FLOAT Utils::EmbedFloat32 +#define EXTRACT_POS_FLOAT Utils::ExtractFloat32 +#define POS_FLOAT_DATA_SIZE 4 + +#if USE_HALF_FLOATS +#define FLOAT_DATA_SIZE 2 +#define EMBED_FLOAT Utils::EmbedFloat16NBO +#define EXTRACT_FLOAT Utils::ExtractFloat16NBO +#else +#define FLOAT_DATA_SIZE 4 +#define EMBED_FLOAT Utils::EmbedFloat32 +#define EXTRACT_FLOAT Utils::ExtractFloat32 +#endif + +#define ABSOLUTE_EPSILON 0.001f + +RigidBody::RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, + uint32_t collide_type_in, uint32_t collide_mask_in, + CollideModel* collide_model_in, uint32_t flags) + : type_(type_in), + id_(id_in), + creation_time_(part_in->node()->scene()->time()), + shape_(shape_in), + part_(part_in), + collide_model_(collide_model_in), + collide_type_(collide_type_in), + collide_mask_(collide_mask_in), + flags_(flags) { + blend_time_ = creation_time_; + +#if BA_DEBUG_BUILD + for (int i = 0; i < 3; i++) { + prev_pos_[i] = prev_vel_[i] = prev_a_vel_[i] = 0.0f; + } +#endif // BA_DEBUG_BUILD + + assert(part_.exists()); + birth_time_ = part_->node()->scene()->stepnum(); + dynamics_ = part_->node()->scene()->dynamics(); + + // Add ourself to the part. + part_->AddBody(this); + + // Create the geom(s). + switch (shape_) { + case Shape::kSphere: { + dimensions_[0] = dimensions_[1] = dimensions_[2] = 0.3f; + geoms_.resize(1); + geoms_[0] = dCreateSphere(dynamics_->space(), dimensions_[0]); + break; + } + + case Shape::kBox: { + dimensions_[0] = dimensions_[1] = dimensions_[2] = 0.6f; + geoms_.resize(1); + geoms_[0] = dCreateBox(dynamics_->space(), dimensions_[0], dimensions_[1], + dimensions_[2]); + break; + } + + case Shape::kCapsule: { + dimensions_[0] = dimensions_[1] = 0.3f; + geoms_.resize(1); + geoms_[0] = + dCreateCCylinder(dynamics_->space(), dimensions_[0], dimensions_[1]); + break; + } + + case Shape::kCylinder: { + int sphere_count = 8; + float inc = 360.0f / static_cast(sphere_count); + + // Transform geom and sphere. + geoms_.resize(static_cast(2 * sphere_count + 1)); + dimensions_[0] = dimensions_[1] = 0.3f; + float sub_rad = dimensions_[1] * 0.5f; + float offset = dimensions_[0] - sub_rad; + for (int i = 0; i < sphere_count; i++) { + Vector3f p = + Matrix44fRotate(Vector3f(0, 1, 0), static_cast(i) * inc) + * Vector3f(offset, 0, 0); + geoms_[i * 2] = dCreateGeomTransform(dynamics_->space()); + geoms_[i * 2 + 1] = dCreateSphere(nullptr, sub_rad); + dGeomTransformSetGeom(geoms_[i * 2], geoms_[i * 2 + 1]); + dGeomSetPosition(geoms_[i * 2 + 1], p.v[0], p.v[1], p.v[2]); + } + + // One last center sphere to keep stuff from getting stuck in our middle. + geoms_[geoms_.size() - 1] = dCreateSphere(dynamics_->space(), sub_rad); + + break; + } + + case Shape::kTrimesh: { + // NOTE - we don't add trimeshes do the collision space - we handle them + // specially.. + dimensions_[0] = dimensions_[1] = dimensions_[2] = 0.6f; + assert(collide_model_.exists()); + collide_model_->collide_model_data()->Load(); + dGeomID g = dCreateTriMesh( + nullptr, collide_model_->collide_model_data()->GetMeshData(), nullptr, + nullptr, nullptr); + geoms_.push_back(g); + dynamics_->AddTrimesh(g); + break; + } + + default: + throw Exception(); + } + + for (auto&& i : geoms_) { + dGeomSetData(i, this); + } + + if (type_ == Type::kBody) { + assert(body_ == nullptr); + body_ = dBodyCreate(dynamics_->ode_world()); + + // For cylinders we only set the transform geoms, not the spheres. + if (shape_ == Shape::kCylinder) { + for (size_t i = 0; i < geoms_.size(); i += 2) { + dGeomSetBody(geoms_[i], body_); + } + // Our center sphere. + dGeomSetBody(geoms_[geoms_.size() - 1], body_); + } else { + dGeomSetBody(geoms_[0], body_); + } + } + SetDimensions(dimensions_[0], dimensions_[1], dimensions_[2]); +} + +void RigidBody::Check() { + if (type_ == Type::kBody) { + const dReal* p = dBodyGetPosition(body_); + const dReal* q = dBodyGetQuaternion(body_); + const dReal* lv = dBodyGetLinearVel(body_); + const dReal* av = dBodyGetAngularVel(body_); + bool err = false; + for (int i = 0; i < 3; i++) { + if (std::isnan(p[i]) || std::isnan(q[i]) || std::isnan(lv[i]) + || std::isnan(av[i])) { + err = true; + break; + } + if (std::abs(p[i]) > 9999) err = true; + if (std::abs(lv[i]) > 99999) err = true; + if (std::abs(av[i]) > 9999) err = true; + } + if (std::isnan(q[3])) err = true; + + if (err) { + Log("Error: Got error in rbd values!"); + } +#if BA_DEBUG_BUILD + for (int i = 0; i < 3; i++) { + prev_pos_[i] = p[i]; + prev_vel_[i] = lv[i]; + prev_a_vel_[i] = av[i]; + } +#endif + } +} + +RigidBody::~RigidBody() { + if (shape_ == Shape::kTrimesh) { + assert(geoms_.size() == 1); + dynamics_->RemoveTrimesh(geoms_[0]); + } + + // if we have any joints attached, kill them + KillConstraints(); + + // remove ourself from our parent part if we have one + if (part_.exists()) { + part_->RemoveBody(this); + } + if (type_ == Type::kBody) { + assert(body_); + dBodyDestroy(body_); + body_ = nullptr; + } + assert(!geoms_.empty()); + for (auto&& i : geoms_) { + dGeomDestroy(i); + } +} + +void RigidBody::KillConstraints() { + while (joints_.begin() != joints_.end()) { + (**joints_.begin()).Kill(); + } +} + +auto RigidBody::GetEmbeddedSizeFull() -> int { + assert(type_ == Type::kBody); + + const dReal* lv = dBodyGetLinearVel(body_); + const dReal* av = dBodyGetAngularVel(body_); + + // always have 3 position, 4 quaternion, and 1 flag + int full_size = 3 * POS_FLOAT_DATA_SIZE + FLOAT_DATA_SIZE * 4 + 1; + + // we only send velocity values that are non-zero - calculate how many of + // them we have + for (int i = 0; i < 3; i++) { + full_size += FLOAT_DATA_SIZE * (std::abs(lv[i] - 0) > ABSOLUTE_EPSILON); + full_size += FLOAT_DATA_SIZE * (std::abs(av[i] - 0) > ABSOLUTE_EPSILON); + } + return full_size; +} + +// store a body to a buffer +// FIXME - theoretically we should embed birth-time +// as this can affect collisions with this object +void RigidBody::EmbedFull(char** buffer) { + assert(type_ == Type::kBody); + + const dReal* p = dBodyGetPosition(body_); + const dReal* q = dBodyGetQuaternion(body_); + const dReal* lv = dBodyGetLinearVel(body_); + const dReal* av = dBodyGetAngularVel(body_); + bool enabled = static_cast(dBodyIsEnabled(body_)); + bool lv_changed[3]; + bool av_changed[3]; + + // only send velocities that are non-zero. + // we always send position/rotation since that's not likely to be zero + for (int i = 0; i < 3; i++) { + lv_changed[i] = (std::abs(lv[i] - 0) > ABSOLUTE_EPSILON); + av_changed[i] = (std::abs(av[i] - 0) > ABSOLUTE_EPSILON); + } + + // embed a byte containing our enabled state as well as what velocities need + // to be sent + Utils::EmbedBools(buffer, lv_changed[0], lv_changed[1], lv_changed[2], + av_changed[0], av_changed[1], av_changed[2], enabled); + + EMBED_POS_FLOAT(buffer, p[0]); + EMBED_POS_FLOAT(buffer, p[1]); + EMBED_POS_FLOAT(buffer, p[2]); + + EMBED_FLOAT(buffer, q[0]); + EMBED_FLOAT(buffer, q[1]); + EMBED_FLOAT(buffer, q[2]); + EMBED_FLOAT(buffer, q[3]); + + for (int i = 0; i < 3; i++) { + if (lv_changed[i]) { + EMBED_FLOAT(buffer, lv[i]); + } + if (av_changed[i]) { + EMBED_FLOAT(buffer, av[i]); + } + } +} + +// Position a body from buffer data. +void RigidBody::ExtractFull(const char** buffer) { + assert(type_ == Type::kBody); + + dReal p[3], lv[3], av[3]; + dQuaternion q; + + bool lv_changed[3]; + bool av_changed[3]; + bool enabled; + + // Extract our byte telling which velocities are contained here as well as our + // enable state. + Utils::ExtractBools(buffer, &lv_changed[0], &lv_changed[1], &lv_changed[2], + &av_changed[0], &av_changed[1], &av_changed[2], &enabled); + + p[0] = EXTRACT_POS_FLOAT(buffer); + p[1] = EXTRACT_POS_FLOAT(buffer); + p[2] = EXTRACT_POS_FLOAT(buffer); + + q[0] = EXTRACT_FLOAT(buffer); + q[1] = EXTRACT_FLOAT(buffer); + q[2] = EXTRACT_FLOAT(buffer); + q[3] = EXTRACT_FLOAT(buffer); + + for (int i = 0; i < 3; i++) { + if (lv_changed[i]) { + lv[i] = EXTRACT_FLOAT(buffer); + } else { + lv[i] = 0; + } + + if (av_changed[i]) { + av[i] = EXTRACT_FLOAT(buffer); + } else { + av[i] = 0; + } + } + + dBodySetPosition(body_, p[0], p[1], p[2]); + dBodySetQuaternion(body_, q); + dBodySetLinearVel(body_, lv[0], lv[1], lv[2]); + dBodySetAngularVel(body_, av[0], av[1], av[2]); + + if (enabled) { + dBodyEnable(body_); + } else { + dBodyDisable(body_); + } +} + +void RigidBody::Draw(RenderPass* pass, bool shaded) { + assert(pass); + RenderPass::Type pass_type = pass->type(); + // only passes we draw in are light_shadow and beauty + if (pass_type != RenderPass::Type::kLightShadowPass + && pass_type != RenderPass::Type::kBeautyPass) { + return; + } + // assume trimeshes are landscapes and shouldn't be in shadow passes.. + if (shape_ == Shape::kTrimesh + && (pass_type != RenderPass::Type::kBeautyPass)) { + return; + } +} + +void RigidBody::AddCallback(CollideCallbackFunc callbackIn, void* data_in) { + CollideCallback c{}; + c.callback = callbackIn; + c.data = data_in; + collide_callbacks_.push_back(c); +} + +auto RigidBody::CallCollideCallbacks(dContact* contacts, int count, + RigidBody* opposingbody) -> bool { + for (auto&& i : collide_callbacks_) { + if (!i.callback(contacts, count, this, opposingbody, i.data)) { + return false; + } + } + return true; +} + +void RigidBody::SetDimensions(float d1, float d2, float d3, float m1, float m2, + float m3, float density_mult) { + dimensions_[0] = d1; + dimensions_[1] = d2; + dimensions_[2] = d3; + + if (m1 == 0.0f) m1 = d1; + if (m2 == 0.0f) m2 = d2; + if (m3 == 0.0f) m3 = d3; + + float density = 5.0f * density_mult; + + switch (shape_) { + case Shape::kSphere: + dGeomSphereSetRadius(geoms_[0], dimensions_[0]); + break; + case Shape::kBox: + dGeomBoxSetLengths(geoms_[0], dimensions_[0], dimensions_[1], + dimensions_[2]); + break; + case Shape::kCapsule: + dGeomCCylinderSetParams(geoms_[0], dimensions_[0], dimensions_[1]); + break; + case Shape::kCylinder: { + int sphere_count = static_cast(geoms_.size() / 2); + float inc = 360.0f / static_cast(sphere_count); + float sub_rad = dimensions_[1] * 0.5f; + float offset = dimensions_[0] - sub_rad; + for (int i = 0; i < sphere_count; i++) { + Vector3f p = + Matrix44fRotate(Vector3f(0, 0, 1), static_cast(i) * inc) + * Vector3f(offset, 0, 0); + dGeomSphereSetRadius(geoms_[i * 2 + 1], sub_rad); + dGeomSetPosition(geoms_[i * 2 + 1], p.v[0], p.v[1], p.v[2]); + } + // Resize our center sphere. + dGeomSphereSetRadius(geoms_[geoms_.size() - 1], sub_rad); + } + // A cylinder is really just a bunch of spheres - we just need to set the + // length of their offsets. + // dGeomBoxSetLengths(geoms[0],dimensions[0],dimensions[0],dimensions[1]); + break; + case Shape::kTrimesh: + break; + default: + throw Exception(); + } + + // Create the body and set mass properties. + if (type_ == Type::kBody) { + dMass m; + switch (shape_) { + case Shape::kSphere: + dMassSetSphere(&m, density, m1); + break; + case Shape::kBox: + dMassSetBox(&m, density, m1, m2, m3); + break; + case Shape::kCapsule: + dMassSetCappedCylinder(&m, density, 3, m1, m2); + break; + case Shape::kCylinder: + dMassSetCylinder(&m, density, 3, m1, m2); + break; + case Shape::kTrimesh: // NOLINT(bugprone-branch-clone) + // Trimesh bodies not supported yet. + throw Exception(); + default: + throw Exception(); + } + + // Need to handle groups here. + assert(geoms_.size() == 1 || shape_ == Shape::kCylinder); + dBodySetMass(body_, &m); + } +} + +auto RigidBody::ApplyImpulse(float px, float py, float pz, float vx, float vy, + float vz, float fdirx, float fdiry, float fdirz, + float mag, float v_mag, float radius, + bool calc_only) -> float { + assert(body_); + float total_mag = 0.0f; + + dMass mass; + dBodyGetMass(body_, &mass); + + bool horizontal_only = false; + + // FIXME - some hard-coded tweaks for the hockey-puck + if (shape_ == Shape::kCylinder) { + py -= 0.3f; + if (v_mag > 0.0f) { + v_mag *= 0.06f; // punches + } else { + mag *= 3.0f; // amp up explosions + } + horizontal_only = true; + } + + if (radius <= 0.0f) { + // Damage based on velocity difference.. lets just plug in our + // center-of-mass velocity (otherwise we might get crazy large velocity + // diffs due to spinning). + + // Ok for now we're not taking our velocity into account. + dVector3 our_velocity = {0, 0, 0}; + + dVector3 v_diff = {vx - our_velocity[0], vy - our_velocity[1], + vz - our_velocity[2]}; + + dVector3 f = {fdirx, fdiry, fdirz}; + + // normalize.. + float fDirLen = sqrtf(fdirx * fdirx + fdiry * fdiry + fdirz * fdirz); + if (fDirLen > 0.0f) { + f[0] /= fDirLen; + f[1] /= fDirLen; + f[2] /= fDirLen; + } else { + f[0] = 1.0f; // just use (1,0,0) + } + + // Lets only take large velocity diffs into account. + // float vLen = std::max(0.0f,dVector3Length(v_diff)-2.0f); + float vLen = dVector3Length(v_diff); + + total_mag = mag + vLen * v_mag; + + f[0] *= total_mag; + f[1] *= total_mag; + f[2] *= total_mag; + + // Exaggerate the force we apply in y (but don't count it towards damage). + f[1] *= 2.0f; + + // General scale up. + f[0] *= 1.8f; + f[1] *= 1.8f; + f[2] *= 1.8f; + + if (horizontal_only) { + f[1] = 0.0f; + py = dBodyGetPosition(body_)[1]; + } + + if (!calc_only) { + dBodyEnable(body_); + dBodyAddForceAtPos(body_, f[0], f[1], f[2], px, py, pz); + } + + } else { + // With radius. + Vector3f us(dBodyGetPosition(body_)); + Vector3f them(px, py, pz); + if (them == us) { + them = us + Vector3f(0.0f, 0.001f, 0.0f); + } + Vector3f diff = them - us; + float len = (them - us).Length(); + if (len == 0.0f) { + len = 0.0001f; + } + + if (len < radius) { + float amt = 1.0f - (len / radius); + + if (v_mag > 0.0f) { + throw Exception("FIXME - handle vmag for radius>0 case"); + } + + // Factor in our mass so a given impulse affects various sized things + // equally. + float this_mag = (mag * amt) * mass.mass; + + // amt *= amt; // squared falloff.. + // amt = pow(amt, 1.5f); // biased falloff + + total_mag += this_mag; + + Vector3f f = diff * (-this_mag / len); + + // Randomize applied force a bit to keep things from looking too clean and + // simple. + const dReal* pos = dBodyGetPosition(body_); + dReal apply_pos[3] = {pos[0] + 0.6f * (RandomFloat() - 0.5f), + pos[1] + 0.6f * (RandomFloat() - 0.5f), + pos[2] + 0.6f * (RandomFloat() - 0.5f)}; + + if (horizontal_only) { + f.y = 0.0f; + apply_pos[1] = us.y; + } + + // Exaggerate up/down component. + f.x *= 0.5f; + if (f.y > 0.0f) { + f.y *= 2.0f; + } + f.z *= 0.5f; + + if (!calc_only) { + dBodyEnable(body_); + dBodyAddForceAtPos(body_, f.x, f.y, f.z, apply_pos[0], apply_pos[1], + apply_pos[2]); + } + } + } + return total_mag; +} + +void RigidBody::ApplyGlobalImpulse(float px, float py, float pz, float fx, + float fy, float fz) { + if (type_ != Type::kBody) { + return; + } + dBodyEnable(body_); + dBodyAddForceAtPos(body_, fx / kGameStepSeconds, fy / kGameStepSeconds, + fz / kGameStepSeconds, px, py, pz); +} + +RigidBody::Joint::Joint() = default; + +void RigidBody::Joint::SetJoint(dxJointFixed* id_in, Scene* scene) { + Kill(); + creation_time_ = scene->time(); + id_ = id_in; +} + +RigidBody::Joint::~Joint() { Kill(); } + +void RigidBody::Joint::AttachToBodies(RigidBody* b1_in, RigidBody* b2_in) { + assert(id_); + b1_ = b1_in; + b2_ = b2_in; + dBodyID b_id_1 = nullptr; + dBodyID b_id_2 = nullptr; + if (b1_) { + b1_->Wake(); + b1_->AddJoint(this); + b_id_1 = b1_->body(); + } + if (b2_) { + b2_->Wake(); + b2_->AddJoint(this); + b_id_2 = b2_->body(); + } + dJointAttach(id_, b_id_1, b_id_2); +} + +void RigidBody::Joint::Kill() { + if (id_) { + if (b1_) { + b1_->RemoveJoint(this); + + // Also wake the body (this joint could be suspending it motionless). + assert(b1_->body()); + dBodyEnable(b1_->body()); + } + if (b2_) { + b2_->RemoveJoint(this); + + // Also wake the body (this joint could be suspending it motionless). + assert(b2_->body()); + dBodyEnable(b2_->body()); + } + dJointDestroy(id_); + id_ = nullptr; + b1_ = b2_ = nullptr; + } +} + +auto RigidBody::GetTransform() -> Matrix44f { + Matrix44f matrix{kMatrix44fIdentity}; + const dReal* pos_in; + const dReal* r_in; + if (type() == RigidBody::Type::kBody) { + pos_in = dBodyGetPosition(body()); + r_in = dBodyGetRotation(body()); + } else { + pos_in = dGeomGetPosition(geom()); + r_in = dGeomGetRotation(geom()); + } + float pos[3]; + float r[12]; + for (int x = 0; x < 3; x++) { + pos[x] = pos_in[x]; + } + pos[0] += blend_offset().x; + pos[1] += blend_offset().y; + pos[2] += blend_offset().z; + for (int x = 0; x < 12; x++) { + r[x] = r_in[x]; + } + matrix.m[0] = r[0]; + matrix.m[1] = r[4]; + matrix.m[2] = r[8]; + matrix.m[3] = 0; + matrix.m[4] = r[1]; + matrix.m[5] = r[5]; + matrix.m[6] = r[9]; + matrix.m[7] = 0; + matrix.m[8] = r[2]; + matrix.m[9] = r[6]; + matrix.m[10] = r[10]; + matrix.m[11] = 0; + matrix.m[12] = pos[0]; + matrix.m[13] = pos[1]; + matrix.m[14] = pos[2]; + matrix.m[15] = 1; + return matrix; +} + +void RigidBody::AddBlendOffset(float x, float y, float z) { + // blend_offset_.x += x; + // blend_offset_.y += y; + // blend_offset_.z += z; +} + +void RigidBody::UpdateBlending() { + // FIXME - this seems broken. We never update blend_time_ currently + // and its also set to time whereas we're comparing it with steps. + // Should revisit. + // millisecs_t diff = part()->node()->scene()->stepnum() - blend_time_; + // diff = std::min(millisecs_t{10}, diff); + // for (millisecs_t i = 0; i < diff; i++) { + // blend_offset_.x *= 0.995f; + // blend_offset_.y *= 0.995f; + // blend_offset_.z *= 0.995f; + // } +} + +} // namespace ballistica diff --git a/src/ballistica/dynamics/rigid_body.h b/src/ballistica/dynamics/rigid_body.h new file mode 100644 index 00000000..0bbb70c3 --- /dev/null +++ b/src/ballistica/dynamics/rigid_body.h @@ -0,0 +1,215 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_DYNAMICS_RIGID_BODY_H_ +#define BALLISTICA_DYNAMICS_RIGID_BODY_H_ + +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/math/matrix44f.h" +#include "ode/ode.h" +#include "ode/ode_joint.h" + +namespace ballistica { + +// Wrapper for ode rigid bodies which implements collision tracking, +// flattening/restoring, and other extras. +class RigidBody : public Object { + public: + // Function type for low level collision callbacks. + // These callbacks are called just before collision constraints + // are being created between rigid bodies. These callback + // should be used only for contact adjustment - things like + // changing friction depending on what part of the body was hit, etc. + // Never use these callbacks to run script command or anything high-level. + // Return false to cancel all constraint creation. + typedef bool (*CollideCallbackFunc)(dContact* contacts, int count, + RigidBody* collide_body, + RigidBody* opposingbody, + void* custom_data); + enum class Type { + // Collidable but not dynamically affected object. + // Used to generate collisions. + kGeomOnly, + // Collidable as well as dynamically affected object. + kBody + }; + + // Used to determine what kind of surface a body has and what surfaces it will + // collide against a body defines its own collide type(s) and its mask for + // what it will collide against collisions will only occur if each body's + // collide mask includes the opposite body's type(s). + enum CollideType { + kCollideNone = 0, + // Static background objects such as landscapes + // These never move and generally never need to test for collisions against + // other landscapes + kCollideBackground = 0x01u, + // Regions - these generally only test for collisions with active bodies + kCollideRegion = 0x01u << 2u, + // Active bodies - these generally collide against everything + kCollideActive = 0x01u << 3u, + // encapsulates all collide types + kCollideAll = kCollideBackground | kCollideRegion | kCollideActive + }; + + // Different kinds of geometry a body can be. + enum class Shape { + // Simple sphere shape + kSphere, + // Simple cube shape + kBox, + // Capsule + kCapsule, + // cylinder made from 4 cubes (8 sides) + kCylinder, + // Trimesh + kTrimesh + }; + + enum Flag { + // The body is a 'bumper' - something that under-control character bodies + // might want to collide with but most other stuff won't want to. + kIsBumper = 1u << 0u, + kIsRoller = 1u << 1u, + kIsTerrain = 1u << 2u + }; + + // these are needed for full states + auto GetEmbeddedSizeFull() -> int; + void ExtractFull(const char** buffer); + void EmbedFull(char** buffer); + RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, + uint32_t collide_type_in, uint32_t collide_mask_in, + CollideModel* collide_model_in = nullptr, uint32_t flags = 0); + ~RigidBody() override; + auto body() const -> dBodyID { return body_; } + auto geom(int i = 0) const -> dGeomID { return geoms_[i]; } + + // Draw a representation of the rigid body for debugging. + void Draw(RenderPass* pass, bool shaded = true); + auto part() const -> Part* { + assert(part_.exists()); + return part_.get(); + } + void Wake() { + if (body_) { + dBodyEnable(body_); + } + } + void AddCallback(CollideCallbackFunc callback_in, void* data_in); + auto CallCollideCallbacks(dContact* contacts, int count, + RigidBody* opposingbody) -> bool; + void SetDimensions( + float d1, float d2 = 0.0f, float d3 = 0.0f, // body dimensions + float m1 = 0.0f, float m2 = 0.0f, + float m3 = 0.0f, // Mass dimensions (default to regular if zero). + float density = 1.0f); + + // If geomWakeOnCollide is true, a GEOM_ONLY object colliding with a sleeping + // body will wake it up. Generally this should be true if the geom is moving + // or changing. + void set_geom_wake_on_collide(bool enable) { geom_wake_on_collide_ = enable; } + auto geom_wake_on_collide() const -> bool { return geom_wake_on_collide_; } + auto id() const -> int { return id_; } + void ApplyGlobalImpulse(float px, float py, float pz, float fx, float fy, + float fz); + auto ApplyImpulse(float px, float py, float pz, float vx, float vy, float vz, + float fdirx, float fdiry, float fdirz, float mag, + float v_mag, float radiusm, bool calc_only) -> float; + void KillConstraints(); + + // Rigid body joint wrapper. This takes ownership of joints it is passed + // all joints should use this mechanism so they are automatically + // cleaned up when bodies are destroyed. + class Joint { + public: + Joint(); + ~Joint(); + + // Attach this wrapper to a new ode joint. + // If already attached to a joint, that joint is first killed. + void SetJoint(dxJointFixed* id, Scene* sg); + + // Returns the ode joint id or nullptr if it has been killed + // (by the other body dying, etc). + auto joint() const -> dJointID { return id_; } + + // Always use this in place of dJointAttach to attach the joint to rigid + // bodies. + void AttachToBodies(RigidBody* b1, RigidBody* b2); + void Kill(); // Kills the joint if it is valid. + // Whether joint still exists. + auto IsAlive() const -> bool { return id_ != nullptr; } + + private: + millisecs_t creation_time_{}; + dxJointFixed* id_{}; + RigidBody* b1_{}; + RigidBody* b2_{}; + }; + + // Used by Joint. + void AddJoint(Joint* j) { joints_.push_back(j); } + void RemoveJoint(Joint* j) { + for (auto i = joints_.begin(); i != joints_.end(); i++) { + if ((*i) == j) { + joints_.erase(i); + return; + } + } + } + void Check(); + auto type() const -> Type { return type_; } + auto collide_type() const -> uint32_t { return collide_type_; } + auto collide_mask() const -> uint32_t { return collide_mask_; } + auto flags() const -> uint32_t { return flags_; } + void set_flags(uint32_t flags) { flags_ = flags; } + auto can_cause_impact_damage() const -> bool { + return can_cause_impact_damage_; + } + void set_can_cause_impact_damage(bool val) { can_cause_impact_damage_ = val; } + + // Applies to spheres. + auto radius() const -> float { return dimensions_[0]; } + auto GetTransform() -> Matrix44f; + void UpdateBlending(); + void AddBlendOffset([[maybe_unused]] float x, float y, float z); + auto blend_offset() const -> const Vector3f& { return blend_offset_; } + + private: + Vector3f blend_offset_{0.0f, 0.0f, 0.0f}; + millisecs_t blend_time_{}; +#if BA_DEBUG_BUILD + float prev_pos_[3]{}; + float prev_vel_[3]{}; + float prev_a_vel_[3]{}; +#endif + millisecs_t creation_time_{}; + bool can_cause_impact_damage_{}; + Dynamics* dynamics_{}; + uint32_t collide_type_{}; + uint32_t collide_mask_{}; + std::list joints_; + bool geom_wake_on_collide_{}; + int id_{}; + Object::Ref collide_model_; + float dimensions_[3]{}; + Type type_{}; + Shape shape_{}; + dBodyID body_{}; + std::vector geoms_; + millisecs_t birth_time_{}; + Object::WeakRef part_; + struct CollideCallback { + CollideCallbackFunc callback; + void* data; + }; + std::vector collide_callbacks_; + uint32_t flags_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_DYNAMICS_RIGID_BODY_H_ diff --git a/src/ballistica/game/connection/connection.h b/src/ballistica/game/connection/connection.h new file mode 100644 index 00000000..3629ac85 --- /dev/null +++ b/src/ballistica/game/connection/connection.h @@ -0,0 +1,145 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CONNECTION_CONNECTION_H_ +#define BALLISTICA_GAME_CONNECTION_CONNECTION_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/game/player_spec.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +// Start near the top of the range to make sure looping works as expected. +const int kFirstConnectionStateNum = 65520; + +/// Connection to a remote session; either as a host or client. +class Connection : public Object { + public: + Connection(); + + // Send a reliable message to the client + // these will always be delivered in the order sent + void SendReliableMessage(const std::vector& data); + + // Send an unreliable message to the client; these are not guaranteed + // to be delivered, but when they are, they're delivered properly in order + // between other unreliable/reliable messages. + void SendUnreliableMessage(const std::vector& data); + + // Send a json-based reliable message. + void SendJMessage(cJSON* val); + virtual void Update(); + + // Called with raw packets as they come in from the network. + virtual void HandleGamePacket(const std::vector& buffer); + + // Called when the next in-order message is available. + virtual void HandleMessagePacket(const std::vector& buffer) = 0; + + // Request an orderly disconnect. + virtual void RequestDisconnect() = 0; + + auto GetBytesOutPerSecond() const -> int64_t { return last_bytes_out_; } + auto GetBytesOutPerSecondCompressed() const -> int64_t { + return last_bytes_out_compressed_; + } + auto GetMessagesOutPerSecond() const -> int64_t { + return last_packet_count_out_; + } + auto GetMessageResendsPerSecond() const -> int64_t { + return last_resend_packet_count_; + } + auto GetBytesInPerSecond() const -> int64_t { return last_bytes_in_; } + auto GetBytesInPerSecondCompressed() const -> int64_t { + return last_bytes_in_compressed_; + } + auto GetMessagesInPerSecond() const -> int64_t { + return last_packet_count_in_; + } + auto GetBytesResentPerSecond() const -> int64_t { + return last_resend_bytes_out_; + } + auto average_ping() const -> float { return average_ping_; } + auto can_communicate() const -> bool { return can_communicate_; } + auto peer_spec() const -> const PlayerSpec& { return peer_spec_; } + void HandleGamePacketCompressed(const std::vector& data); + auto errored() const -> bool { return errored_; } + auto creation_time() const -> millisecs_t { return creation_time_; } + auto multipart_buffer_size() const -> size_t { + return multipart_buffer_.size(); + } + + protected: + void SendGamePacket(const std::vector& data); + virtual void SendGamePacketCompressed(const std::vector& data) = 0; + void ErrorSilent() { Error(""); } + virtual void Error(const std::string& error_msg); + void set_peer_spec(const PlayerSpec& spec) { peer_spec_ = spec; } + void set_can_communicate(bool val) { can_communicate_ = val; } + void set_connection_dying(bool val) { connection_dying_ = val; } + void set_errored(bool val) { errored_ = val; } + + private: + void ProcessWaitingMessages(); + void HandleResends(millisecs_t real_time, const std::vector& data, + int offset); + void EmbedAcks(millisecs_t real_time, std::vector* data, int offset); + std::vector multipart_buffer_; + + struct ReliableMessageIn { + std::vector data; + millisecs_t arrival_time; + }; + + struct ReliableMessageOut { + std::vector data; + millisecs_t first_send_time; + millisecs_t last_send_time; + millisecs_t resend_time; + bool acked; + }; + + // Leaf classes should set this when they start dying. + // This prevents any SendGamePacketCompressed() calls from happening. + bool connection_dying_{}; + float average_ping_{}; + int64_t last_resend_bytes_out_{}; + int64_t last_bytes_out_{}; + int64_t last_bytes_out_compressed_{}; + int64_t bytes_out_{}; + int64_t bytes_out_compressed_{}; + int64_t resend_bytes_out_{}; + int64_t last_packet_count_out_{}; + int64_t last_resend_packet_count_{}; + int64_t resend_packet_count_{}; + int64_t packet_count_out_{}; + int64_t last_bytes_in_{}; + int64_t last_bytes_in_compressed_{}; + int64_t bytes_in_{}; + int64_t bytes_in_compressed_{}; + int64_t last_packet_count_in_{}; + int64_t packet_count_in_{}; + millisecs_t last_average_update_time_{}; + millisecs_t creation_time_{}; + PlayerSpec peer_spec_; // Name of the account/device on the other end. + std::map in_messages_; + std::map out_messages_; + bool can_communicate_{}; + bool errored_{}; + millisecs_t last_prune_time_{}; + millisecs_t last_ack_send_time_{}; + + // These are explicitly 16 bit values. + uint16_t next_out_message_num_ = kFirstConnectionStateNum; + uint16_t next_out_unreliable_message_num_{}; + uint16_t next_in_message_num_ = kFirstConnectionStateNum; + uint16_t next_in_unreliable_message_num_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CONNECTION_CONNECTION_H_ diff --git a/src/ballistica/game/connection/connection_to_client.h b/src/ballistica/game/connection/connection_to_client.h new file mode 100644 index 00000000..c234bea5 --- /dev/null +++ b/src/ballistica/game/connection/connection_to_client.h @@ -0,0 +1,76 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CONNECTION_CONNECTION_TO_CLIENT_H_ +#define BALLISTICA_GAME_CONNECTION_CONNECTION_TO_CLIENT_H_ + +#include +#include +#include + +#include "ballistica/game/connection/connection.h" + +namespace ballistica { + +/// Connection to a party client if we're the host. +class ConnectionToClient : public Connection { + public: + explicit ConnectionToClient(int id); + ~ConnectionToClient() override; + void Update() override; + void HandleMessagePacket(const std::vector& buffer) override; + void HandleGamePacket(const std::vector& buffer) override; + auto id() const -> int { return id_; } + + // More efficient than dynamic_cast (hmm do we still want this?). + virtual auto GetAsUDP() -> ConnectionToClientUDP*; + void SetController(ClientControllerInterface* c); + auto GetPlayerProfiles() const -> PyObject* { return player_profiles_.get(); } + auto build_number() const -> int { return build_number_; } + void SendScreenMessage(const std::string& s, float r = 1.0f, float g = 1.0f, + float b = 1.0f); + auto token() const -> const std::string& { return token_; } + void HandleMasterServerClientInfo(PyObject* info_obj); + + /// Return the public id for this client. If they have not been verified + /// by the master-server, returns an empty string. + auto peer_public_account_id() const -> const std::string& { + return peer_public_account_id_; + } + + /// Return whether this client is an admin. Will only return true once their + /// account id has been verified by the master server. + auto IsAdmin() const -> bool; + + private: + virtual auto ShouldPrintIncompatibleClientErrors() const -> bool; + // Returns a spec for this client that incorporates their player names + // or their peer name if they have no players. + auto GetCombinedSpec() -> PlayerSpec; + auto GetClientInputDevice(int remote_id) -> ClientInputDevice*; + void Error(const std::string& error_msg) override; + std::string our_handshake_player_spec_str_; + std::string our_handshake_salt_; + std::string peer_public_account_id_; + ClientControllerInterface* controller_ = nullptr; + std::map client_input_devices_; + millisecs_t last_hand_shake_send_time_ = 0; + int id_ = -1; + int build_number_ = 0; + bool got_client_info_ = false; + bool kick_voted_ = false; + bool kick_vote_choice_ = false; + std::string token_; + std::string peer_hash_; + PythonRef player_profiles_; + bool got_info_from_master_server_ = false; + std::vector last_chat_times_; + millisecs_t next_kick_vote_allow_time_ = 0; + millisecs_t chat_block_time_ = 0; + millisecs_t last_remove_player_time_ = -99999; + int next_chat_block_seconds_ = 10; + friend class Game; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CONNECTION_CONNECTION_TO_CLIENT_H_ diff --git a/src/ballistica/game/connection/connection_to_client_udp.h b/src/ballistica/game/connection/connection_to_client_udp.h new file mode 100644 index 00000000..357017ed --- /dev/null +++ b/src/ballistica/game/connection/connection_to_client_udp.h @@ -0,0 +1,40 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ +#define BALLISTICA_GAME_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ + +#include +#include +#include + +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/networking/networking.h" + +namespace ballistica { + +// Connection to a party client if we're the host. +class ConnectionToClientUDP : public ConnectionToClient { + public: + ConnectionToClientUDP(const SockAddr& addr, std::string client_name, + uint8_t request_id, int client_id); + ~ConnectionToClientUDP() override; + void Update() override; + void HandleGamePacket(const std::vector& buffer) override; + auto client_name() const -> const std::string& { return client_name_; } + auto GetAsUDP() -> ConnectionToClientUDP* override; + void RequestDisconnect() override; + + protected: + uint8_t request_id_; + std::unique_ptr addr_; + std::string client_name_; + bool did_die_; + void Die(); + void SendDisconnectRequest(); + millisecs_t last_client_response_time_; + void SendGamePacketCompressed(const std::vector& data) override; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ diff --git a/src/ballistica/game/connection/connection_to_host.h b/src/ballistica/game/connection/connection_to_host.h new file mode 100644 index 00000000..40b0e74b --- /dev/null +++ b/src/ballistica/game/connection/connection_to_host.h @@ -0,0 +1,47 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CONNECTION_CONNECTION_TO_HOST_H_ +#define BALLISTICA_GAME_CONNECTION_CONNECTION_TO_HOST_H_ + +#include +#include + +#include "ballistica/game/connection/connection.h" + +namespace ballistica { + +// connection to the party host if we're a client +class ConnectionToHost : public Connection { + public: + ConnectionToHost(); + ~ConnectionToHost() override; + void Update() override; + void HandleMessagePacket(const std::vector& buffer) override; + void HandleGamePacket(const std::vector& buffer) override; + // more efficient than dynamic_cast?.. bad idea?.. + virtual auto GetAsUDP() -> ConnectionToHostUDP*; + auto build_number() const -> int { return build_number_; } + auto protocol_version() const -> int { return protocol_version_; } + void set_protocol_version(int val) { protocol_version_ = val; } + auto party_name() const -> std::string { + // FIXME should we return peer name as fallback?.. + return party_name_; + } + + private: + std::string party_name_; + std::string peer_hash_input_; + std::string peer_hash_; + bool printed_connect_message_ = false; + int protocol_version_ = kProtocolVersion; + int build_number_ = 0; + bool got_host_info_ = false; + // can remove once back-compat protocol is > 29 + bool ignore_old_attach_remote_player_packets_ = false; + // the client-session that we're driving + Object::WeakRef client_session_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CONNECTION_CONNECTION_TO_HOST_H_ diff --git a/src/ballistica/game/connection/connection_to_host_udp.h b/src/ballistica/game/connection/connection_to_host_udp.h new file mode 100644 index 00000000..a56de77f --- /dev/null +++ b/src/ballistica/game/connection/connection_to_host_udp.h @@ -0,0 +1,50 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CONNECTION_CONNECTION_TO_HOST_UDP_H_ +#define BALLISTICA_GAME_CONNECTION_CONNECTION_TO_HOST_UDP_H_ + +#include +#include +#include + +#include "ballistica/game/connection/connection_to_host.h" +#include "ballistica/networking/networking.h" + +namespace ballistica { + +class ConnectionToHostUDP : public ConnectionToHost { + public: + explicit ConnectionToHostUDP(const SockAddr& addr); + ~ConnectionToHostUDP() override; + void Update() override; + void HandleGamePacket(const std::vector& buffer) override; + auto GetAsUDP() -> ConnectionToHostUDP* override; + auto request_id() const -> uint8_t { return request_id_; } + void set_client_id(int val) { client_id_ = val; } + auto client_id() const -> int { return client_id_; } + + // Attempt connecting via a different protocol. If none are left to try, + // returns false. + auto SwitchProtocol() -> bool; + void RequestDisconnect() override; + + protected: + uint8_t request_id_{}; + std::unique_ptr addr_; + bool did_die_{}; + void Die(); + void SendDisconnectRequest(); + millisecs_t last_client_i_d_request_time_{}; + millisecs_t last_disconnect_request_time_{}; + int client_id_{}; + millisecs_t last_host_response_time_{}; + void SendGamePacketCompressed(const std::vector& data) override; + void Error(const std::string& error_msg) override; + + private: + void GetRequestID(); +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CONNECTION_CONNECTION_TO_HOST_UDP_H_ diff --git a/src/ballistica/platform/apple/platform_apple.h b/src/ballistica/platform/apple/platform_apple.h new file mode 100644 index 00000000..9d85341d --- /dev/null +++ b/src/ballistica/platform/apple/platform_apple.h @@ -0,0 +1,86 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PLATFORM_APPLE_PLATFORM_APPLE_H_ +#define BALLISTICA_PLATFORM_APPLE_PLATFORM_APPLE_H_ +#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS + +#include +#include +#include +#include + +#include "ballistica/platform/platform.h" + +namespace ballistica { + +class PlatformApple : public Platform { + public: + PlatformApple(); + auto GetDeviceUUIDPrefix() -> std::string override; + auto GetRealDeviceUUID(std::string* uuid) -> bool override; + auto GenerateUUID() -> std::string override; + auto GetDefaultConfigDir() -> std::string override; + auto GetLocale() -> std::string override; + auto DoGetDeviceName() -> std::string override; + auto DoHasTouchScreen() -> bool override; + auto GetInterfaceType() -> UIScale override; + auto IsRunningOnDesktop() -> bool override; + void HandleLog(const std::string& msg) override; + void SetupDataDirectory() override; + void GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width) override; + void FreeTextTexture(void* tex) override; + auto CreateTextTexture(int width, int height, + const std::vector& strings, + const std::vector& positions, + const std::vector& widths, float scale) + -> void* override; + auto GetTextTextureData(void* tex) -> uint8_t* override; + void GetFriendScores(const std::string& game, const std::string& game_version, + void* py_callback) override; + void SubmitScore(const std::string& game, const std::string& version, + int64_t score) override; + void ReportAchievement(const std::string& achievement) override; + auto HaveLeaderboard(const std::string& game, const std::string& config) + -> bool override; + void ShowOnlineScoreUI(const std::string& show, const std::string& game, + const std::string& game_version) override; + void Purchase(const std::string& item) override; + void RestorePurchases() override; + auto NewAutoReleasePool() -> void* override; + void DrainAutoReleasePool(void* pool) override; + void DoOpenURL(const std::string& url) override; + void ResetAchievements() override; + void GameCenterLogin() override; + void PurchaseAck(const std::string& purchase, + const std::string& order_id) override; + auto IsOSPlayingMusic() -> bool override; + void SetHardwareCursorVisible(bool visible) override; + void QuitApp() override; + void GetScoresToBeat(const std::string& level, const std::string& config, + void* py_callback) override; + void OpenFileExternally(const std::string& path) override; + void OpenDirExternally(const std::string& path) override; + void MacMusicAppInit() override; + auto MacMusicAppGetVolume() -> int override; + void MacMusicAppSetVolume(int volume) override; + void MacMusicAppGetLibrarySource() override; + void MacMusicAppStop() override; + auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool override; + auto MacMusicAppGetPlaylists() -> std::list override; + void StartListeningForWiiRemotes() override; + void StopListeningForWiiRemotes() override; + auto IsEventPushMode() -> bool override; + auto ContainsPythonDist() -> bool override; + auto GetPlatformName() -> std::string override; + auto GetSubplatformName() -> std::string override; + + private: + // std::mutex log_mutex_; + // std::string log_line_; +}; + +} // namespace ballistica + +#endif // BA_XCODE_BUILD || BA_OSTYPE_MACOS +#endif // BALLISTICA_PLATFORM_APPLE_PLATFORM_APPLE_H_ diff --git a/src/ballistica/python/methods/python_methods_networking.cc b/src/ballistica/python/methods/python_methods_networking.cc deleted file mode 100644 index d74a6d9e..00000000 --- a/src/ballistica/python/methods/python_methods_networking.cc +++ /dev/null @@ -1,610 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_networking.h" - -#include -#include -#include - -#include "ballistica/app/app_globals.h" -#include "ballistica/game/connection/connection_to_host.h" -#include "ballistica/game/game.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/master_server_config.h" -#include "ballistica/networking/network_reader.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/networking/telnet_server.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("getpublicpartyenabled"); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) - return nullptr; - assert(g_python); - if (g_game->public_party_enabled()) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("setpublicpartyenabled"); - int enable; - static const char* kwlist[] = {"enabled", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - assert(g_python); - g_game->SetPublicPartyEnabled(static_cast(enable)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyName(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("setpublicpartyname"); - PyObject* name_obj; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &name_obj)) { - return nullptr; - } - std::string name = Python::GetPyString(name_obj); - assert(g_python); - g_game->SetPublicPartyName(name); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyStatsURL(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("setpublicpartystatsurl"); - PyObject* url_obj; - static const char* kwlist[] = {"url", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &url_obj)) { - return nullptr; - } - // The call expects an empty string for the no-url option. - std::string url = (url_obj == Py_None) ? "" : Python::GetPyString(url_obj); - assert(g_python); - g_game->SetPublicPartyStatsURL(url); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("getpublicpartymaxsize"); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - assert(g_python); - return PyLong_FromLong(g_game->public_party_max_size()); - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("setpublicpartymaxsize"); - int max_size; - static const char* kwlist[] = {"max_size", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", - const_cast(kwlist), &max_size)) { - return nullptr; - } - assert(g_python); - g_game->SetPublicPartyMaxSize(max_size); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetAuthenticateClients(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("set_authenticate_clients"); - int enable; - static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - assert(g_game); - g_game->set_require_client_authentication(static_cast(enable)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetAdmins(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("set_admins"); - PyObject* admins_obj; - static const char* kwlist[] = {"admins", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &admins_obj)) { - return nullptr; - } - assert(g_game); - - auto admins = Python::GetPyStrings(admins_obj); - std::set adminset; - for (auto&& admin : admins) { - adminset.insert(admin); - } - g_game->set_admin_public_ids(adminset); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetEnableDefaultKickVoting(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("set_enable_default_kick_voting"); - int enable; - static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - assert(g_game); - g_game->set_kick_voting_enabled(static_cast(enable)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyConnectToParty(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("connect_to_party"); - std::string address; - PyObject* address_obj; - int port = kDefaultPort; - - // Whether we should print standard 'connecting...' and 'party full..' - // messages when false, only odd errors such as version incompatibility will - // be printed and most connection attempts will be silent todo: could - // generalize this to pass all results to a callback instead - int print_progress = 1; - static const char* kwlist[] = {"address", "port", "print_progress", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ip", - const_cast(kwlist), &address_obj, - &port, &print_progress)) { - return nullptr; - } - address = Python::GetPyString(address_obj); - - // Disallow in headless build (people were using this for spam-bots). - - if (HeadlessMode()) { - throw Exception("Not available in headless mode."); - } - - SockAddr s; - try { - s = SockAddr(address, port); - - // HACK: CLion currently flags our catch clause as unreachable even - // though SockAddr constructor can throw exceptions. Work around that here. - if (explicit_bool(false)) { - throw Exception(); - } - } catch (const std::exception&) { - ScreenMessage(g_game->GetResourceString("invalidAddressErrorText"), - {1, 0, 0}); - Py_RETURN_NONE; - } - g_game->PushHostConnectedUDPCall(s, static_cast(print_progress)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyAcceptPartyInvitation(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("accept_party_invitation"); - const char* invite_id; - static const char* kwlist[] = {"invite_id", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &invite_id)) { - return nullptr; - } - g_platform->AndroidGPGSPartyInviteAccept(invite_id); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetGooglePlayPartyClientCount(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("get_google_play_party_client_count"); - BA_PRECONDITION(InGameThread()); -#if BA_GOOGLE_BUILD - return PyLong_FromLong(g_game->GetGooglePlayClientCount()); -#else - return PyLong_FromLong(0); -#endif - BA_PYTHON_CATCH; -} - -auto PyClientInfoQueryResponse(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("client_info_query_response"); - const char* token; - PyObject* response_obj; - static const char* kwlist[] = {"token", "response", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "sO", - const_cast(kwlist), &token, - &response_obj)) { - return nullptr; - } - g_game->SetClientInfoFromMasterServer(token, response_obj); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("get_connection_to_host_info"); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - ConnectionToHost* hc = g_game->connection_to_host(); - if (hc) { - return Py_BuildValue("{sssi}", "name", hc->party_name().c_str(), - "build_number", hc->build_number()); - } else { - return Py_BuildValue("{}"); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyDisconnectFromHost(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("disconnect_from_host"); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_game->PushDisconnectFromHostCall(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyDisconnectClient(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("disconnect_client"); - int client_id; - int ban_time = 300; // Old default before we exposed this. - static const char* kwlist[] = {"client_id", "ban_time", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|i", - const_cast(kwlist), &client_id, - &ban_time)) { - return nullptr; - } - bool kickable = g_game->DisconnectClient(client_id, ban_time); - if (kickable) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PyGetGamePort(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("get_game_port"); - int port = 0; - if (g_network_reader != nullptr) { - // hmmm; we're just fetching the ipv4 port here; - // 6 could be different.... - port = g_network_reader->port4(); - } - return Py_BuildValue("i", port); - BA_PYTHON_CATCH; -} - -auto PyGetMasterServerAddress(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("get_master_server_address"); - int source = -1; // use default.. - if (!PyArg_ParseTuple(args, "|i", &source)) { - return nullptr; - } - // source -1 implies to use current one - if (source == -1) { - source = g_app_globals->master_server_source; - } - const char* addr; - if (source == 0) { - addr = BA_MASTER_SERVER_DEFAULT_ADDR; - } else if (source == 1) { - addr = BA_MASTER_SERVER_FALLBACK_ADDR; - } else { - BA_LOG_ONCE("Error: Got unexpected source: " + std::to_string(source) - + "."); - addr = BA_MASTER_SERVER_FALLBACK_ADDR; - } - return PyUnicode_FromString(addr); - BA_PYTHON_CATCH; -} - -auto PySetMasterServerSource(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("set_master_server_source"); - int source; - if (!PyArg_ParseTuple(args, "i", &source)) return nullptr; - if (source != 0 && source != 1) { - BA_LOG_ONCE("Error: Invalid server source: " + std::to_string(source) - + "."); - source = 1; - } - g_app_globals->master_server_source = source; - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetTelnetAccessEnabled(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("set_telnet_access_enabled"); - assert(InGameThread()); - int enable; - static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - if (g_app_globals->telnet_server) { - g_app_globals->telnet_server->SetAccessEnabled(static_cast(enable)); - } else { - throw Exception("Telnet server not enabled."); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyHostScanCycle(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("host_scan_cycle"); - g_networking->HostScanCycle(); - std::vector results = - g_networking->GetScanResults(); - PyObject* py_list = PyList_New(0); - for (auto&& i : results) { - PyList_Append(py_list, Py_BuildValue("{ssss}", "display_string", - i.display_string.c_str(), "address", - i.address.c_str())); - } - return py_list; - BA_PYTHON_CATCH; -} - -auto PyEndHostScanning(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("end_host_scanning"); - g_networking->EndHostScanning(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyHaveConnectedClients(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("have_connected_clients"); - if (g_game->GetConnectedClientCount() > 0) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PyInvitePlayers(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Platform::SetLastPyCall("invite_players"); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_platform->AndroidGPGSPartyInvitePlayers(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -PyMethodDef PythonMethodsNetworking::methods_def[] = { - {"invite_players", (PyCFunction)PyInvitePlayers, - METH_VARARGS | METH_KEYWORDS, - "invite_players() -> None\n" - "\n" - "(internal)" - "\n" - "Category: General Utility Functions"}, - - {"have_connected_clients", (PyCFunction)PyHaveConnectedClients, - METH_VARARGS | METH_KEYWORDS, - "have_connected_clients() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions"}, - - {"end_host_scanning", (PyCFunction)PyEndHostScanning, - METH_VARARGS | METH_KEYWORDS, - "end_host_scanning() -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions"}, - - {"host_scan_cycle", (PyCFunction)PyHostScanCycle, - METH_VARARGS | METH_KEYWORDS, - "host_scan_cycle() -> list\n" - "\n" - "(internal)"}, - - {"set_telnet_access_enabled", (PyCFunction)PySetTelnetAccessEnabled, - METH_VARARGS | METH_KEYWORDS, - "set_telnet_access_enabled(enable: bool)\n" - " -> None\n" - "\n" - "(internal)"}, - - {"set_master_server_source", PySetMasterServerSource, METH_VARARGS, - "set_master_server_source(source: int) -> None\n" - "\n" - "(internal)"}, - - {"get_master_server_address", PyGetMasterServerAddress, METH_VARARGS, - "get_master_server_address(source: int = -1) -> str\n" - "\n" - "(internal)\n" - "\n" - "Return the address of the master server."}, - - {"get_game_port", PyGetGamePort, METH_VARARGS, - "get_game_port() -> int\n" - "\n" - "(internal)\n" - "\n" - "Return the port ballistica is hosting on."}, - - {"disconnect_from_host", (PyCFunction)PyDisconnectFromHost, - METH_VARARGS | METH_KEYWORDS, - "disconnect_from_host() -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions"}, - - {"disconnect_client", (PyCFunction)PyDisconnectClient, - METH_VARARGS | METH_KEYWORDS, - "disconnect_client(client_id: int, ban_time: int = 300) -> bool\n" - "\n" - "(internal)"}, - - {"get_connection_to_host_info", (PyCFunction)PyGetConnectionToHostInfo, - METH_VARARGS | METH_KEYWORDS, - "get_connection_to_host_info() -> dict\n" - "\n" - "(internal)"}, - - {"client_info_query_response", (PyCFunction)PyClientInfoQueryResponse, - METH_VARARGS | METH_KEYWORDS, - "client_info_query_response(token: str, response: Any) -> None\n" - "\n" - "(internal)"}, - - {"get_google_play_party_client_count", - (PyCFunction)PyGetGooglePlayPartyClientCount, METH_VARARGS | METH_KEYWORDS, - "get_google_play_party_client_count() -> int\n" - "\n" - "(internal)"}, - - {"accept_party_invitation", (PyCFunction)PyAcceptPartyInvitation, - METH_VARARGS | METH_KEYWORDS, - "accept_party_invitation(invite_id: str) -> None\n" - "\n" - "(internal)"}, - - {"connect_to_party", (PyCFunction)PyConnectToParty, - METH_VARARGS | METH_KEYWORDS, - "connect_to_party(address: str, port: int = None,\n" - " print_progress: bool = True) -> None\n" - "\n" - "(internal)"}, - - {"set_authenticate_clients", (PyCFunction)PySetAuthenticateClients, - METH_VARARGS | METH_KEYWORDS, - "set_authenticate_clients(enable: bool) -> None\n" - "\n" - "(internal)"}, - - {"set_admins", (PyCFunction)PySetAdmins, METH_VARARGS | METH_KEYWORDS, - "set_admins(admins: List[str]) -> None\n" - "\n" - "(internal)"}, - - {"set_enable_default_kick_voting", - (PyCFunction)PySetEnableDefaultKickVoting, METH_VARARGS | METH_KEYWORDS, - "set_enable_default_kick_voting(enable: bool) -> None\n" - "\n" - "(internal)"}, - - {"set_public_party_max_size", (PyCFunction)PySetPublicPartyMaxSize, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_max_size(max_size: int) -> None\n" - "\n" - "(internal)"}, - - {"get_public_party_max_size", (PyCFunction)PyGetPublicPartyMaxSize, - METH_VARARGS | METH_KEYWORDS, - "get_public_party_max_size() -> int\n" - "\n" - "(internal)"}, - - {"set_public_party_stats_url", (PyCFunction)PySetPublicPartyStatsURL, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_stats_url(url: Optional[str]) -> None\n" - "\n" - "(internal)"}, - - {"set_public_party_name", (PyCFunction)PySetPublicPartyName, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_name(name: str) -> None\n" - "\n" - "(internal)"}, - - {"set_public_party_enabled", (PyCFunction)PySetPublicPartyEnabled, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_enabled(enabled: bool) -> None\n" - "\n" - "(internal)"}, - - {"get_public_party_enabled", (PyCFunction)PyGetPublicPartyEnabled, - METH_VARARGS | METH_KEYWORDS, - "get_public_party_enabled() -> bool\n" - "\n" - "(internal)"}, - - {nullptr, nullptr, 0, nullptr}}; - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_networking.h b/src/ballistica/python/methods/python_methods_networking.h deleted file mode 100644 index bb6a2c42..00000000 --- a/src/ballistica/python/methods/python_methods_networking.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ - -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -/// Networking related individual python methods for our module. -class PythonMethodsNetworking { - public: - static PyMethodDef methods_def[]; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ diff --git a/src/ballistica/scene/node/anim_curve_node.cc b/src/ballistica/scene/node/anim_curve_node.cc new file mode 100644 index 00000000..fb311f87 --- /dev/null +++ b/src/ballistica/scene/node/anim_curve_node.cc @@ -0,0 +1,136 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/anim_curve_node.h" + +#include +#include +#include + +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class AnimCurveNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS AnimCurveNode + BA_NODE_CREATE_CALL(CreateAnimCurve); + BA_FLOAT_ATTR(in, in, set_in); + BA_BOOL_ATTR(loop, loop, set_loop); + BA_INT64_ARRAY_ATTR(times, times, set_times); + BA_FLOAT_ARRAY_ATTR(values, values, set_values); + BA_FLOAT_ATTR(offset, offset, set_offset); + BA_FLOAT_ATTR_READONLY(out, GetOut); +#undef BA_NODE_TYPE_CLASS + + AnimCurveNodeType() + : NodeType("animcurve", CreateAnimCurve), + in(this), + loop(this), + times(this), + values(this), + offset(this), + out(this) {} +}; + +static NodeType* node_type{}; + +auto AnimCurveNode::InitType() -> NodeType* { + node_type = new AnimCurveNodeType(); + return node_type; +} + +AnimCurveNode::AnimCurveNode(Scene* scene) : Node(scene, node_type) {} + +AnimCurveNode::~AnimCurveNode() = default; + +auto AnimCurveNode::GetOut() -> float { + // Recreate our keyframes if need be. + if (keys_dirty_) { + keyframes_.clear(); + auto num = std::min(times_.size(), values_.size()); + if (num < 1) { + input_start_ = 0; + input_end_ = 0; + } + for (size_t i = 0; i < num; i++) { + if (i == 0) { + input_start_ = times_[i]; + } + if (i == (num - 1)) { + input_end_ = times_[i]; + } + keyframes_.emplace_back(times_[i], values_[i]); + } + keys_dirty_ = false; + out_dirty_ = true; + } + + // Now update out if need-be. + if (out_dirty_) { + float in_val = in_ - offset_; + if ((input_end_ - input_start_) > 0) { + if (keyframes_.size() < 2) { + assert(keyframes_.size() == 1); + out_ = keyframes_[0].value; + } else { + bool got; + if (loop_) { + in_val = fmodf(in_val, (input_end_ - input_start_)); + if (in_val < 0) in_val += (input_end_ - input_start_); + got = false; + } else { + if (in_val >= input_end_) { + out_ = keyframes_.back().value; + got = true; + } else if (in_val <= input_start_) { + out_ = keyframes_.front().value; + got = true; + } else { + got = false; + } + } + if (!got) { + out_ = keyframes_[0].value; + + // Ok we know we've got at least 2 keyframes. + auto i1 = keyframes_.begin(); + auto i2 = keyframes_.begin(); + auto i = keyframes_.begin(); + while (true) { + if (i == keyframes_.end()) { + break; + } + if (i->time < in_val) { + i++; + i1 = i2; + i2 = i; + } else { + break; + } + } + if (i2->time - i1->time == 0) { + out_ = i1->value; + } else { + out_ = i1->value + + ((in_val - i1->time) + / static_cast(i2->time - i1->time)) + * (i2->value - i1->value); + } + } + } + } else { + // No keyframes?.. hmm just go with 0. + if (keyframes_.empty()) { + out_ = 0.0f; + } else { + // We have one keyframe; hmm what to do. + out_ = keyframes_[0].value; + } + } + out_dirty_ = false; + } + return out_; +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/anim_curve_node.h b/src/ballistica/scene/node/anim_curve_node.h new file mode 100644 index 00000000..23ca037d --- /dev/null +++ b/src/ballistica/scene/node/anim_curve_node.h @@ -0,0 +1,75 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_ANIM_CURVE_NODE_H_ +#define BALLISTICA_SCENE_NODE_ANIM_CURVE_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// Node containing a keyframe graph associating an input value with an output +// value. +class AnimCurveNode : public Node { + public: + static auto InitType() -> NodeType*; + + explicit AnimCurveNode(Scene* scene); + ~AnimCurveNode() override; + + auto in() const -> float { return in_; } + void set_in(float value) { + in_ = value; + out_dirty_ = true; + } + + auto loop() const -> bool { return loop_; } + void set_loop(bool val) { + loop_ = val; + out_dirty_ = true; + } + + auto times() const -> const std::vector& { return times_; } + void set_times(const std::vector& vals) { + times_ = vals; + keys_dirty_ = true; + } + + auto values() const -> const std::vector& { return values_; } + void set_values(const std::vector& vals) { + values_ = vals; + keys_dirty_ = true; + } + + auto offset() const -> float { return offset_; } + void set_offset(float val) { + offset_ = val; + out_dirty_ = true; + } + + auto GetOut() -> float; + + private: + struct Keyframe { + Keyframe(uint32_t t, float v) : time(t), value(v) {} + uint32_t time; + float value; + }; + + float in_ = 0.0f; + std::vector times_; + std::vector values_; + bool keys_dirty_ = true; + bool out_dirty_ = true; + float out_ = 0.0f; + bool loop_ = true; + std::vector keyframes_; + float input_start_ = 0.0f; + float input_end_ = 0.0f; + float offset_ = 0.0f; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_ANIM_CURVE_NODE_H_ diff --git a/src/ballistica/scene/node/bomb_node.cc b/src/ballistica/scene/node/bomb_node.cc new file mode 100644 index 00000000..c39bfa3b --- /dev/null +++ b/src/ballistica/scene/node/bomb_node.cc @@ -0,0 +1,85 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/bomb_node.h" + +#include "ballistica/graphics/graphics.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +const float kFuseOffset = 0.35f; + +// Returns noise value between 0 and 1. +// TODO(ericf): Need to interpolate between 2 values. +static auto SimpleNoise(uint32_t x) -> float { + x = (x << 13u) ^ x; + return (0.5f + * static_cast((x * (x * x * 15731u + 789221u) + 1376312589u) + & 0x7fffffffu) + / 1073741824.0f); +} + +class BombNodeType : public PropNodeType { + public: +#define BA_NODE_TYPE_CLASS BombNode + BA_NODE_CREATE_CALL(CreateBomb); + BA_FLOAT_ATTR(fuse_length, fuse_length, set_fuse_length); +#undef BA_NODE_TYPE_CLASS + + BombNodeType() : PropNodeType("bomb", CreateBomb), fuse_length(this) {} +}; + +static NodeType* node_type{}; + +auto BombNode::InitType() -> NodeType* { + node_type = new BombNodeType(); + return node_type; +} + +BombNode::BombNode(Scene* scene) : PropNode(scene, node_type) {} + +void BombNode::OnCreate() { + // We can't do this in our constructor because + // it would prevent the user from setting density/etc attrs. + // (user attrs get applied after constructors fire) + SetBody("sphere"); +} + +void BombNode::Step() { + PropNode::Step(); + if (body_.exists()) { + // Update our fuse and light position. + dVector3 fuse_tip_pos; + dGeomGetRelPointPos(body_->geom(), 0, (fuse_length_ + kFuseOffset), 0, + fuse_tip_pos); + light_translate_ = fuse_tip_pos; + light_translate_.x += body_->blend_offset().x; + light_translate_.y += body_->blend_offset().y; + light_translate_.z += body_->blend_offset().z; +#if !BA_HEADLESS_BUILD + fuse_.SetTransform(Matrix44fTranslate(0, kFuseOffset * model_scale_, 0) + * body_->GetTransform()); + fuse_.SetLength(fuse_length_); +#endif // !BA_HEADLESS_BUILD + } +} + +void BombNode::Draw(FrameDef* frame_def) { +#if !BA_HEADLESS_BUILD + PropNode::Draw(frame_def); + float s_scale, s_density; + shadow_.GetValues(&s_scale, &s_density); + float intensity = SimpleNoise(static_cast(id() + scene()->time())) + * s_density * 0.2f; + float s = 4.0f * s_scale; + float r = 1.5f * intensity; + float g = 0.1f * intensity; + float b = 0.1f * intensity; + float a = 0.0f; + g_graphics->DrawBlotchSoft(light_translate_, s, r, g, b, a); + g_graphics->DrawBlotchSoftObj(light_translate_, s, r, g, b, a); +#endif // !BA_HEADLESS_BUILD +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/bomb_node.h b/src/ballistica/scene/node/bomb_node.h new file mode 100644 index 00000000..285a3586 --- /dev/null +++ b/src/ballistica/scene/node/bomb_node.h @@ -0,0 +1,31 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_BOMB_NODE_H_ +#define BALLISTICA_SCENE_NODE_BOMB_NODE_H_ + +#include "ballistica/dynamics/bg/bg_dynamics_fuse.h" +#include "ballistica/scene/node/prop_node.h" + +namespace ballistica { + +class BombNode : public PropNode { + public: + static auto InitType() -> NodeType*; + explicit BombNode(Scene* scene); + void Step() override; + void Draw(FrameDef* frame_def) override; + void OnCreate() override; + auto fuse_length() const -> float { return fuse_length_; } + void set_fuse_length(float val) { fuse_length_ = val; } + + protected: +#if !BA_HEADLESS_BUILD + BGDynamicsFuse fuse_; +#endif + float fuse_length_ = 1.0f; + Vector3f light_translate_ = {0.0f, 0.0f, 0.0f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_BOMB_NODE_H_ diff --git a/src/ballistica/scene/node/combine_node.cc b/src/ballistica/scene/node/combine_node.cc new file mode 100644 index 00000000..9109a80b --- /dev/null +++ b/src/ballistica/scene/node/combine_node.cc @@ -0,0 +1,68 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/combine_node.h" + +#include +#include + +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class CombineNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS CombineNode + BA_NODE_CREATE_CALL(CreateLocator); + BA_FLOAT_ATTR(input0, input_0, set_input_0); + BA_FLOAT_ATTR(input1, input_1, set_input_1); + BA_FLOAT_ATTR(input2, input_2, set_input_2); + BA_FLOAT_ATTR(input3, input_3, set_input_3); + BA_FLOAT_ARRAY_ATTR_READONLY(output, GetOutput); + BA_INT_ATTR(size, size, set_size); +#undef BA_NODE_TYPE_CLASS + CombineNodeType() + : NodeType("combine", CreateLocator), + input0(this), + input1(this), + input2(this), + input3(this), + output(this), + size(this) {} +}; + +static NodeType* node_type{}; + +auto CombineNode::InitType() -> NodeType* { + node_type = new CombineNodeType(); + return node_type; +} + +CombineNode::CombineNode(Scene* scene) : Node(scene, node_type) {} + +auto CombineNode::GetOutput() -> std::vector { + if (dirty_) { + if (do_size_unset_warning_) { + do_size_unset_warning_ = false; + BA_LOG_ONCE("ERROR: CombineNode size unset for " + label()); + } + int actual_size = std::min(4, std::max(0, size_)); + output_.resize(static_cast(actual_size)); + if (size_ > 0) { + output_[0] = input_0_; + } + if (size_ > 1) { + output_[1] = input_1_; + } + if (size_ > 2) { + output_[2] = input_2_; + } + if (size_ > 3) { + output_[3] = input_3_; + } + dirty_ = false; + } + return output_; +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/combine_node.h b/src/ballistica/scene/node/combine_node.h new file mode 100644 index 00000000..a547642f --- /dev/null +++ b/src/ballistica/scene/node/combine_node.h @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_COMBINE_NODE_H_ +#define BALLISTICA_SCENE_NODE_COMBINE_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// An node used to combine individual input values into one array output value +class CombineNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit CombineNode(Scene* scene); + auto size() const -> int { return size_; } + void set_size(int val) { + size_ = val; + dirty_ = true; + do_size_unset_warning_ = false; + } + auto input_0() const -> float { return input_0_; } + void set_input_0(float val) { + input_0_ = val; + dirty_ = true; + } + auto input_1() const -> float { return input_1_; } + void set_input_1(float val) { + input_1_ = val; + dirty_ = true; + } + auto input_2() const -> float { return input_2_; } + void set_input_2(float val) { + input_2_ = val; + dirty_ = true; + } + auto input_3() const -> float { return input_3_; } + void set_input_3(float val) { + input_3_ = val; + dirty_ = true; + } + auto GetOutput() -> std::vector; + + private: + bool do_size_unset_warning_ = true; + float input_0_ = 0.0f; + float input_1_ = 0.0f; + float input_2_ = 0.0f; + float input_3_ = 0.0f; + int size_ = 4; + std::vector output_; + bool dirty_ = true; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_COMBINE_NODE_H_ diff --git a/src/ballistica/scene/node/explosion_node.cc b/src/ballistica/scene/node/explosion_node.cc new file mode 100644 index 00000000..c37b71ba --- /dev/null +++ b/src/ballistica/scene/node/explosion_node.cc @@ -0,0 +1,224 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/explosion_node.h" + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/post_process_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class ExplosionNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS ExplosionNode + BA_NODE_CREATE_CALL(CreateExplosion); + BA_FLOAT_ARRAY_ATTR(position, position, set_position); + BA_FLOAT_ARRAY_ATTR(velocity, velocity, set_velocity); + BA_FLOAT_ATTR(radius, radius, set_radius); + BA_FLOAT_ARRAY_ATTR(color, color, set_color); + BA_BOOL_ATTR(big, big, set_big); +#undef BA_NODE_TYPE_CLASS + ExplosionNodeType() + : NodeType("explosion", CreateExplosion), + position(this), + velocity(this), + radius(this), + color(this), + big(this) {} +}; + +static NodeType* node_type{}; + +auto ExplosionNode::InitType() -> NodeType* { + node_type = new ExplosionNodeType(); + return node_type; +} + +ExplosionNode* gExplosionDistortLock = nullptr; + +ExplosionNode::ExplosionNode(Scene* scene) + : Node(scene, node_type), birth_time_(scene->time()) {} + +ExplosionNode::~ExplosionNode() { + if (draw_distortion_ && have_distortion_lock_) { + assert(gExplosionDistortLock == this); + gExplosionDistortLock = nullptr; + } +} + +void ExplosionNode::set_big(bool val) { + big_ = val; + + // big explosions try to steal the distortion pointer.. + if (big_) { + check_draw_distortion_ = true; + } +} + +void ExplosionNode::set_position(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of size 3 for position"); + position_ = vals; +} + +void ExplosionNode::set_velocity(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of size 3 for velocity"); + velocity_ = vals; +} + +void ExplosionNode::set_color(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of size 3 for color"); + color_ = vals; +} + +void ExplosionNode::Step() { + // update our position from our velocity + if (velocity_[0] != 0.0f || velocity_[1] != 0.0f || velocity_[2] != 0.0f) { + velocity_[0] *= 0.95f; + velocity_[1] *= 0.95f; + velocity_[2] *= 0.95f; + position_[0] += velocity_[0] * kGameStepSeconds; + position_[1] += velocity_[1] * kGameStepSeconds; + position_[2] += velocity_[2] * kGameStepSeconds; + } +} + +void ExplosionNode::Draw(FrameDef* frame_def) { + { + bool high_quality = (frame_def->quality() >= GraphicsQuality::kHigh); + // we only draw distortion if we're the only bomb.. + // (it gets expensive..) + if (check_draw_distortion_) { + check_draw_distortion_ = false; + { + if (big_) { + // Steal distortion handle. + if (gExplosionDistortLock != nullptr) { + gExplosionDistortLock->draw_distortion_ = false; + gExplosionDistortLock = this; + have_distortion_lock_ = true; + draw_distortion_ = true; + } + } else { + // Play nice and only distort if no one else currently is. + if (gExplosionDistortLock == nullptr) { + draw_distortion_ = true; + gExplosionDistortLock = this; + have_distortion_lock_ = true; + } else { + draw_distortion_ = false; + } + } + } + } + if (draw_distortion_) { + float age = scene()->time() - static_cast(birth_time_); + float amt = (1.0f - 0.00265f * age); + if (amt > 0.0001f) { + amt = pow(amt, 2.2f); + amt *= 2.0f; + if (big_) { + amt *= 4.0f; + } else { + amt *= 0.8f; + } + float s = 1.0f; + if (high_quality) { + PostProcessComponent c(frame_def->blit_pass()); + c.setNormalDistort(0.5f * amt); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(1.0f + s * 0.8f * 0.025f * age, + 1.0f + s * 0.8f * 0.0015f * age, + 1.0f + s * 0.8f * 0.025f * age); + c.Scale(0.7f, 0.7f, 0.7f); + c.DrawModel(g_media->GetModel(SystemModelID::kShockWave), + kModelDrawFlagNoReflection); + c.PopTransform(); + c.Submit(); + } else { + // simpler transparent shock wave + // draw our distortion wave in the overlay pass + ObjectComponent c(frame_def->beauty_pass()); + c.SetTransparent(true); + c.SetLightShadow(LightShadowType::kNone); + // eww hacky - the shock wave model uses color as distortion amount + c.SetColor(1.0f, 0.7f, 0.7f, 0.06f * amt); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(1.0f + s * 0.8f * 0.025f * age, + 1.0f + s * 0.8f * 0.0015f * age, + 1.0f + s * 0.8f * 0.025f * age); + c.Scale(0.7f, 0.7f, 0.7f); + c.DrawModel(g_media->GetModel(SystemModelID::kShockWave), + kModelDrawFlagNoReflection); + c.PopTransform(); + c.Submit(); + } + } + } + } + + float life = 1.0f * (big_ ? 350.0f : 260.0f); + float age = scene()->time() - static_cast(birth_time_); + if (age < life) { + float b = 2.0f; + if (big_) { + b = 2.0f; + } + float o = age / life; + if (big_) { + o = pow(1.0f - o, 1.4f); + } else { + o = pow(1.0f - o, 0.8f); + } + float s = 1.0f - (age / life); + s = 1.0f - (s * s); + s *= radius_; + if (big_) { + s *= 2.0f; + } else { + s *= 1.2f; + } + s *= 0.75f; + float cx, cy, cz; + g_graphics->camera()->get_position(&cx, &cy, &cz); + ObjectComponent c(frame_def->beauty_pass()); + c.SetTransparent(true); + c.SetLightShadow(LightShadowType::kNone); + c.SetPremultiplied(true); + c.SetTexture(g_media->GetTexture(SystemTextureID::kExplosion)); + c.SetColor(1.3f * o * color_[0] * b, o * color_[1] * b, o * color_[2] * b, + 0.0f); + c.PushTransform(); + Vector3f to_cam = + Vector3f(cx - position_[0], cy - position_[1], cz - position_[2]) + .Normalized(); + Matrix44f m = Matrix44fTranslate(position_[0], position_[1], position_[2]); + Vector3f right = Vector3f::Cross(to_cam, kVector3fY).Normalized(); + Vector3f up = Vector3f::Cross(right, to_cam).Normalized(); + Matrix44f om = Matrix44fOrient(right, to_cam, up); + c.MultMatrix((om * m).m); + c.Scale(0.9f * s, 0.9f * s, 0.9f * s); + c.DrawModel(g_media->GetModel(SystemModelID::kShield), + kModelDrawFlagNoReflection); + c.Scale(0.6f, 0.6f, 0.6f); + c.Rotate(33, 0, 1, 0); + c.SetColor(o * 7.0f * color_[0], o * 7.0f * color_[1], o * 7.0f * color_[2], + 0); + c.DrawModel(g_media->GetModel(SystemModelID::kShield), + kModelDrawFlagNoReflection); + c.PopTransform(); + c.Submit(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/explosion_node.h b/src/ballistica/scene/node/explosion_node.h new file mode 100644 index 00000000..05c8c7a7 --- /dev/null +++ b/src/ballistica/scene/node/explosion_node.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_EXPLOSION_NODE_H_ +#define BALLISTICA_SCENE_NODE_EXPLOSION_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class ExplosionNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit ExplosionNode(Scene* scene); + ~ExplosionNode() override; + void Draw(FrameDef* frame_def) override; + void Step() override; + auto position() const -> std::vector { return position_; } + void set_position(const std::vector& vals); + auto velocity() const -> std::vector { return velocity_; } + void set_velocity(const std::vector& vals); + auto radius() const -> float { return radius_; } + void set_radius(float val) { radius_ = val; } + auto color() const -> std::vector { return color_; } + void set_color(const std::vector& vals); + auto big() const -> bool { return big_; } + void set_big(bool val); + + private: + millisecs_t birth_time_; + bool check_draw_distortion_{true}; + bool big_{}; + bool draw_distortion_{}; + bool have_distortion_lock_{}; + float radius_{1.0f}; + std::vector position_{0.0f, 0.0f, 0.0f}; + std::vector velocity_{0.0f, 0.0f, 0.0f}; + std::vector color_{0.9f, 0.3f, 0.1f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_EXPLOSION_NODE_H_ diff --git a/src/ballistica/scene/node/flag_node.cc b/src/ballistica/scene/node/flag_node.cc new file mode 100644 index 00000000..88ed73a4 --- /dev/null +++ b/src/ballistica/scene/node/flag_node.cc @@ -0,0 +1,691 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/flag_node.h" + +#include + +#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/area_of_interest.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +const int kFlagSizeX{5}; +const int kFlagSizeY{5}; + +const float kFlagCanvasWidth{1.0f}; +const float kFlagCanvasHeight{1.0f}; + +const float kFlagCanvasScaleX{kFlagCanvasWidth / kFlagSizeX}; +const float kFlagCanvasScaleY{kFlagCanvasHeight / kFlagSizeY}; + +// NOLINTNEXTLINE(cert-err58-cpp) +const float kFlagCanvasScaleDiagonal{ + sqrtf(kFlagCanvasScaleX * kFlagCanvasScaleX + + kFlagCanvasScaleY * kFlagCanvasScaleY)}; + +const float kFlagRadius{0.1f}; +const float kFlagHeight{1.5f}; + +const float kFlagMassRadius{0.3f}; +const float kFlagMassHeight{1.0f}; + +const float kFlagDensity{1.0f}; + +const float kStiffness{0.4f}; +const float kWindStrength{0.002f}; +const float kGravityStrength{0.0012f}; +const float kDampingStrength{0.0f}; +const float kDragStrength{0.1f}; + +#if !BA_HEADLESS_BUILD +class FlagNode::FullShadowSet : public Object { + public: + BGDynamicsShadow shadow_pole_bottom_; + BGDynamicsShadow shadow_pole_middle_; + BGDynamicsShadow shadow_pole_top_; + BGDynamicsShadow shadow_flag_; +}; +class FlagNode::SimpleShadowSet : public Object { + public: + BGDynamicsShadow shadow_; +}; +#endif // !BA_HEADLESS_BUILD + +class FlagNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS FlagNode + BA_NODE_CREATE_CALL(CreateFlag); + BA_BOOL_ATTR(is_area_of_interest, is_area_of_interest, SetIsAreaOfInterest); + BA_FLOAT_ARRAY_ATTR(position, getPosition, SetPosition); + BA_TEXTURE_ATTR(color_texture, color_texture, set_color_texture); + BA_BOOL_ATTR(lightWeight, light_weight, SetLightWeight); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); +#undef BA_NODE_TYPE_CLASS + + FlagNodeType() + : NodeType("flag", CreateFlag), + is_area_of_interest(this), + position(this), + color_texture(this), + lightWeight(this), + color(this), + materials(this) {} +}; + +static NodeType* node_type{}; + +auto FlagNode::InitType() -> NodeType* { + node_type = new FlagNodeType(); + return node_type; +} + +enum FlagBodyType { kPoleBodyID }; + +FlagNode::FlagNode(Scene* scene) : Node(scene, node_type), part_(this) { + body_ = Object::New( + kPoleBodyID, &part_, RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + UpdateDimensions(); + dBodySetPosition(body_->body(), 0, 1.2f, 0); + dQuaternion iq; + dQFromAxisAndAngle(iq, 1, 0, 0, -90.0f * (kPi / 180.0f)); + dBodySetQuaternion(body_->body(), iq); + ResetFlagMesh(); + + // Set our mesh static data and indices once. + auto indices( + Object::New(6 * (kFlagSizeX - 1) * (kFlagSizeY - 1))); + uint16_t* index = &indices->elements[0]; + auto v_static(Object::New>(kFlagSizeX + * kFlagSizeY)); + VertexObjectSplitStatic* vs = &v_static->elements[0]; + + int x_inc = 65535 / (kFlagSizeX - 1); + int y_inc = 65535 / (kFlagSizeY - 1); + + for (int y = 0; y < kFlagSizeY - 1; y++) { + for (int x = 0; x < kFlagSizeX - 1; x++) { + *index++ = static_cast_check_fit(kFlagSizeX * y + x); + *index++ = static_cast_check_fit(kFlagSizeX * y + x + 1); + *index++ = static_cast_check_fit(kFlagSizeX * (y + 1) + x); + *index++ = static_cast_check_fit(kFlagSizeX * (y + 1) + x); + *index++ = static_cast_check_fit(kFlagSizeX * y + x + 1); + *index++ = static_cast_check_fit(kFlagSizeX * (y + 1) + x + 1); + } + } + for (int y = 0; y < kFlagSizeY; y++) { + for (int x = 0; x < kFlagSizeX; x++) { + vs[kFlagSizeX * y + x].uv[0] = static_cast_check_fit(x_inc * x); + vs[kFlagSizeX * y + x].uv[1] = static_cast_check_fit(y_inc * y); + } + } + + mesh_.SetIndexData(indices); + mesh_.SetStaticData(v_static); + + // Create our shadow set. + UpdateForGraphicsQuality(g_graphics_server->quality()); +} + +auto FlagNode::getPosition() const -> std::vector { + const dReal* p = dGeomGetPosition(body_->geom()); + std::vector f(3); + f[0] = p[0]; + f[1] = p[1]; + f[2] = p[2]; + return f; +} + +void FlagNode::SetColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for color"); + } + color_ = vals; +} + +void FlagNode::SetLightWeight(bool val) { + light_weight_ = val; + UpdateDimensions(); +} + +void FlagNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for position"); + } + dQuaternion iq; + dQFromAxisAndAngle(iq, 1, 0, 0, -90 * (kPi / 180.0f)); + dBodySetPosition(body_->body(), vals[0], vals[1], vals[2]); + dBodySetQuaternion(body_->body(), iq); + dBodySetLinearVel(body_->body(), 0, 0, 0); + dBodySetAngularVel(body_->body(), 0, 0, 0); + ResetFlagMesh(); +} + +void FlagNode::SetIsAreaOfInterest(bool val) { + if ((val && area_of_interest_ == nullptr) + || (!val && area_of_interest_ != nullptr)) { + // Either make one or kill the one we had. + if (val) { + assert(area_of_interest_ == nullptr); + area_of_interest_ = g_graphics->camera()->NewAreaOfInterest(false); + } else { + assert(area_of_interest_ != nullptr); + g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + area_of_interest_ = nullptr; + } + } +} + +auto FlagNode::GetMaterials() const -> std::vector { + return part_.GetMaterials(); +} + +void FlagNode::SetMaterials(const std::vector& vals) { + part_.SetMaterials(vals); +} + +void FlagNode::UpdateDimensions() { + float density_scale = + (g_graphics->camera()->happy_thoughts_mode()) ? 0.3f : 1.0f; + body_->SetDimensions(kFlagRadius, kFlagHeight - 2 * kFlagRadius, 0, + kFlagMassRadius, kFlagMassHeight, 0.0f, + kFlagDensity * density_scale); +} + +FlagNode::~FlagNode() { + if (area_of_interest_) + g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); +} + +void FlagNode::HandleMessage(const char* data_in) { + const char* data = data_in; + bool handled = true; + + switch (extract_node_message_type(&data)) { + case NodeMessageType::kFooting: { + footing_ += Utils::ExtractInt8(&data); + break; + } + + case NodeMessageType::kImpulse: { + float px = Utils::ExtractFloat16NBO(&data); + float py = Utils::ExtractFloat16NBO(&data); + float pz = Utils::ExtractFloat16NBO(&data); + + float vx = Utils::ExtractFloat16NBO(&data); + float vy = Utils::ExtractFloat16NBO(&data); + float vz = Utils::ExtractFloat16NBO(&data); + + float mag = Utils::ExtractFloat16NBO(&data); + float velocity_mag = Utils::ExtractFloat16NBO(&data); + float radius = Utils::ExtractFloat16NBO(&data); + Utils::ExtractInt16NBO(&data); // calc-force-only + + float force_dir_x = Utils::ExtractFloat16NBO(&data); + float force_dir_y = Utils::ExtractFloat16NBO(&data); + float force_dir_z = Utils::ExtractFloat16NBO(&data); + + float applied_mag = body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, + 0.2f * mag, 0.2f * velocity_mag, radius, false); + + Vector3f to_flag = + Vector3f(px, py, pz) - Vector3f(dBodyGetPosition(body_->body())); + to_flag *= -0.0001f * applied_mag / to_flag.Length(); + + flag_impulse_add_x_ += to_flag.x; + flag_impulse_add_y_ += to_flag.y; + flag_impulse_add_z_ += to_flag.z; + + have_flag_impulse_ = true; + break; + } + default: + handled = false; + break; + } + if (!handled) Node::HandleMessage(data_in); +} + +void FlagNode::Draw(FrameDef* frame_def) { +#if !BA_HEADLESS_BUILD + + // Flag cloth. + { + // Update the dynamic portion of our mesh data. + // FIXME - should move this all to BG dynamics thread + auto v_dynamic(Object::New>(25)); + + VertexObjectSplitDynamic* vd = &v_dynamic->elements[0]; + for (int i = 0; i < 25; i++) { + vd[i].position[0] = flag_points_[i].x; + vd[i].position[1] = flag_points_[i].y; + vd[i].position[2] = flag_points_[i].z; + vd[i].normal[0] = static_cast_check_fit(std::max( + -32767, + std::min(32767, static_cast(flag_normals_[i].x * 32767.0f)))); + vd[i].normal[1] = static_cast_check_fit(std::max( + -32767, + std::min(32767, static_cast(flag_normals_[i].y * 32767.0f)))); + vd[i].normal[2] = static_cast_check_fit(std::max( + -32767, + std::min(32767, static_cast(flag_normals_[i].z * 32767.0f)))); + } + mesh_.SetDynamicData(v_dynamic); + + // Render a subtle sharp shadow in higher quality modes. + if (frame_def->quality() > GraphicsQuality::kLow) { + SimpleComponent c(frame_def->light_shadow_pass()); + c.SetTransparent(true); + c.SetColor(color_[0] * 0.1f, color_[1] * 0.1f, color_[2] * 0.1f, 0.02f); + c.SetDoubleSided(true); + c.DrawMesh(&mesh_); + c.Submit(); + } + + // Now beauty pass. + { + ObjectComponent c(frame_def->beauty_pass()); + c.SetWorldSpace(true); + c.SetColor(color_[0], color_[1], color_[2]); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.05f, 0.05f, 0.05f); + c.SetDoubleSided(true); + c.SetTexture(color_texture_); + c.DrawMesh(&mesh_); + c.Submit(); + } + + float s_scale, s_density; + SimpleComponent c(frame_def->light_shadow_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kShadow)); + c.SetTransparent(true); + + FullShadowSet* full_shadows = full_shadow_set_.get(); + + if (full_shadows) { + // Pole bottom. + { + full_shadows->shadow_pole_bottom_.GetValues(&s_scale, &s_density); + const Vector3f& p(full_shadows->shadow_pole_bottom_.GetPosition()); + g_graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, s_density * 0.25f); + } + + // Pole middle. + { + full_shadows->shadow_pole_middle_.GetValues(&s_scale, &s_density); + const Vector3f& p(full_shadows->shadow_pole_middle_.GetPosition()); + g_graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, s_density * 0.25f); + } + + // Pole top. + { + full_shadows->shadow_pole_middle_.GetValues(&s_scale, &s_density); + const Vector3f& p(full_shadows->shadow_pole_top_.GetPosition()); + g_graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, s_density * 0.25f); + } + + // Flag center. + { + full_shadows->shadow_flag_.GetValues(&s_scale, &s_density); + const Vector3f& p(full_shadows->shadow_flag_.GetPosition()); + g_graphics->DrawBlotch(p, 0.8f * s_scale, 0, 0, 0, s_density * 0.3f); + } + } else { + SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + assert(simple_shadows); + simple_shadows->shadow_.GetValues(&s_scale, &s_density); + const Vector3f& p(simple_shadows->shadow_.GetPosition()); + g_graphics->DrawBlotch(p, 0.8f * s_scale, 0, 0, 0, s_density * 0.5f); + } + c.Submit(); + } + + // Flag pole. + { + ObjectComponent c(frame_def->beauty_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kFlagPole)); + c.SetReflection(ReflectionType::kSharp); + c.SetReflectionScale(0.1f, 0.1f, 0.1f); + c.PushTransform(); + c.TransformToBody(*body_); + c.DrawModel(g_media->GetModel(SystemModelID::kFlagPole)); + c.PopTransform(); + c.Submit(); + } + +#endif // !BA_HEADLESS_BUILD +} + +void FlagNode::UpdateAreaOfInterest() { + AreaOfInterest* aoi = area_of_interest_; + if (!aoi) return; + assert(body_.exists()); + aoi->set_position(Vector3f(dGeomGetPosition(body_->geom()))); + aoi->SetRadius(5.0f); +} + +void FlagNode::Step() { + // On happy thoughts, keep us on the 2d plane. + if (g_graphics->camera()->happy_thoughts_mode() && body_.exists()) { + dBodyID b; + const dReal *p, *v; + b = body_->body(); + p = dBodyGetPosition(b); + float smoothing = 0.98f; + dBodySetPosition( + b, p[0], p[1], + p[2] * smoothing + (1.0f - smoothing) * kHappyThoughtsZPlane); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0], v[1], v[2] * smoothing); + } + + // update our area-of-interest if we have one + UpdateAreaOfInterest(); + + // FIXME: This should probably happen for RBDs automatically? + body_->UpdateBlending(); + + // Update our shadow objects. + dBodyID b = body_->body(); + assert(b); + +#if !BA_HEADLESS_BUILD + dVector3 p; + FullShadowSet* full_shadows = full_shadow_set_.get(); + if (full_shadows) { + full_shadows->shadow_flag_.SetPosition( + flag_points_[kFlagSizeX * (kFlagSizeY / 2) + (kFlagSizeX / 2)]); + dBodyGetRelPointPos(b, 0, 0, kFlagHeight * -0.4f, p); + full_shadows->shadow_pole_bottom_.SetPosition(Vector3f(p)); + full_shadows->shadow_pole_middle_.SetPosition( + Vector3f(dBodyGetPosition(b))); + dBodyGetRelPointPos(b, 0, 0, kFlagHeight * 0.4f, p); + full_shadows->shadow_pole_top_.SetPosition(Vector3f(p)); + } else { + SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + assert(simple_shadows); + dBodyGetRelPointPos(b, 0, 0, kFlagHeight * -0.3f, p); + simple_shadows->shadow_.SetPosition(Vector3f(p)); + } +#endif // !BA_HEADLESS_BUILD + + if (dBodyIsEnabled(body_->body())) { + // Try to keep upright by pushing the top of the + // flag to be above the bottom. + { + float force_mag = 40.0f; + float force_max = 40.0f; + float min_dist = 0.05f; + + if (light_weight_) { + force_mag *= 0.3f; + force_max *= 0.3f; + } + dVector3 bottom, top; + dBodyGetRelPointPos(body_->body(), 0, 0, kFlagHeight / 2.0f, top); + dBodyGetRelPointPos(body_->body(), 0, 0, -kFlagHeight / 2.0f, bottom); + Vector3f top_v(top[0], top[1], top[2]); + Vector3f bot_v(bottom[0], bottom[1], bottom[2]); + Vector3f target_v(bot_v.x, bot_v.y + kFlagHeight, bot_v.z); + if ((std::abs(target_v.x - top_v.x) > min_dist) + || (std::abs(target_v.y - top_v.y) > min_dist) + || (std::abs(target_v.z - top_v.z) > min_dist)) { + dBodyEnable(body_->body()); + Vector3f fV((target_v - top_v) * force_mag); + float mag = fV.Length(); + if (mag > force_max) fV *= force_max / mag; + dBodyAddForceAtPos(body_->body(), fV.x, fV.y, fV.z, top_v.x, top_v.y, + top_v.z); + dBodyAddForceAtPos(body_->body(), -fV.x, -fV.y, -fV.z, bot_v.x, bot_v.y, + bot_v.z); + } + } + + // Apply damping force. + float linear_damping_x = 1.0f; + float linear_damping_y = 1.0f; + float linear_damping_z = 1.0f; + float rotational_damping_x = 1.0f; + float rotational_damping_y = 1.0f; + float rotational_damping_z = 1.0f; + + if (light_weight_) { + linear_damping_x *= 0.3f; + linear_damping_y *= 0.3f; + linear_damping_z *= 0.3f; + rotational_damping_x *= 0.3f; + rotational_damping_y *= 0.3f; + rotational_damping_z *= 0.3f; + } + + // Don't add forces if we're asleep otherwise we'll explode when we wake up. + dMass mass; + dBodyGetMass(body_->body(), &mass); + + const dReal* vel; + dReal force[3]; + vel = dBodyGetAngularVel(body_->body()); + force[0] = -1 * mass.mass * vel[0] * rotational_damping_x; + force[1] = -1 * mass.mass * vel[1] * rotational_damping_y; + force[2] = -1 * mass.mass * vel[2] * rotational_damping_z; + dBodyAddTorque(body_->body(), force[0], force[1], force[2]); + + vel = dBodyGetLinearVel(body_->body()); + force[0] = -1 * mass.mass * vel[0] * linear_damping_x; + force[1] = -1 * mass.mass * vel[1] * linear_damping_y; + force[2] = -1 * mass.mass * vel[2] * linear_damping_z; + dBodyAddForce(body_->body(), force[0], force[1], force[2]); + + // If we're out of bounds, arrange to have ourself informed. + { + const dReal* p2 = dBodyGetPosition(body_->body()); + if (scene()->IsOutOfBounds(p2[0], p2[1], p2[2])) { + scene()->AddOutOfBoundsNode(this); + } + } + } + UpdateFlagMesh(); +} + +auto FlagNode::GetRigidBody(int id) -> RigidBody* { return body_.get(); } + +static auto FlagPointIndex(int x, int y) -> int { + return kFlagSizeX * (y) + (x); +} + +void FlagNode::UpdateSpringPoint(int p1, int p2, float rest_length) { + Vector3f d = flag_points_[p2] - flag_points_[p1]; + float mag = d.Length(); + if (mag > (rest_length + 0.05f)) { + mag = rest_length + 0.05f; + } + Vector3f f = d / mag * kStiffness * (mag - rest_length); + flag_velocities_[p1] += f; + flag_velocities_[p2] -= f; + Vector3f vd = + kDampingStrength * (flag_velocities_[p1] - flag_velocities_[p2]); + flag_velocities_[p1] -= vd; + flag_velocities_[p2] += vd; +} + +void FlagNode::ResetFlagMesh() { + dVector3 up, side, top; + dBodyGetRelPointPos(body_->body(), 0, 0, kFlagHeight / 2, top); + dBodyVectorToWorld(body_->body(), 0, 0, 1, up); + dBodyVectorToWorld(body_->body(), 1, 0, 0, side); + Vector3f up_v(up); + Vector3f side_v(side); + Vector3f top_v(top); + up_v *= kFlagCanvasScaleY; + side_v *= kFlagCanvasScaleX; + for (int y = 0; y < kFlagSizeY; y++) { + for (int x = 0; x < kFlagSizeX; x++) { + int i = kFlagSizeX * y + x; + Vector3f p = + top_v - up_v * static_cast(y) + side_v * static_cast(x); + flag_points_[i].x = p.x; + flag_points_[i].y = p.y; + flag_points_[i].z = p.z; + flag_velocities_[i] = kVector3f0; + } + } + flag_impulse_add_x_ = flag_impulse_add_y_ = flag_impulse_add_z_ = 0; + have_flag_impulse_ = false; +} + +void FlagNode::UpdateFlagMesh() { + dVector3 up, top; + dBodyGetRelPointPos(body_->body(), 0, 0, kFlagHeight / 2, top); + dBodyVectorToWorld(body_->body(), 0, 0, 1, up); + Vector3f up_v(up); + Vector3f top_v(top); + up_v *= kFlagCanvasScaleY; + + // Move our attachment points into place. + for (int y = 0; y < kFlagSizeY; y++) { + int i = kFlagSizeX * y; + Vector3f p = top_v - up_v * static_cast(y); + flag_points_[i].x = p.x; + flag_points_[i].y = p.y; + flag_points_[i].z = p.z; + flag_velocities_[i] = kVector3f0; + } + + // Push our flag points around. + const dReal* flag_vel = dBodyGetLinearVel(body_->body()); + Vector3f wind_vec = {0.0f, 0.0f, 0.0f}; + bool do_wind = true; + if (RandomFloat() > 0.85f) { + wind_rand_x_ = 0.5f - RandomFloat(); + wind_rand_y_ = 0.5f - RandomFloat(); + if (scene()->stepnum() % 100 > 50) { + wind_rand_z_ = RandomFloat(); + } else { + wind_rand_z_ = -RandomFloat(); + } + wind_rand_ = static_cast(scene()->stepnum()); + } + + if (explicit_bool(do_wind)) { + wind_vec = -2.0f * Vector3f(flag_vel[0], flag_vel[1], flag_vel[2]); + + // If the flag is moving less than 1.0, add some ambient wind. + if (wind_vec.LengthSquared() < 1.0f) { + wind_vec += (1.0f - wind_vec.LengthSquared()) * Vector3f(5, 0, 0); + } + wind_vec += + 3.0f + * Vector3f(0.15f * wind_rand_x_, wind_rand_y_, 1.5f * wind_rand_z_); + } + + for (int y = 0; y < kFlagSizeY - 1; y++) { + for (int x = 0; x < kFlagSizeX - 1; x++) { + int top_left, top_right, bot_left, bot_right; + top_left = FlagPointIndex(x, y); + top_right = FlagPointIndex(x + 1, y); + bot_left = FlagPointIndex(x, y + 1); + bot_right = FlagPointIndex(x + 1, y + 1); + flag_velocities_[top_left].y -= kGravityStrength; + flag_velocities_[top_right].y -= kGravityStrength; + flag_velocities_[top_right].x *= (1.0f - kDragStrength); + flag_velocities_[top_right].y *= (1.0f - kDragStrength); + flag_velocities_[top_right].z *= (1.0f - kDragStrength); + if (have_flag_impulse_) { + flag_velocities_[top_left].x += flag_impulse_add_x_; + flag_velocities_[top_left].y += flag_impulse_add_y_; + flag_velocities_[top_left].z += flag_impulse_add_z_; + flag_velocities_[top_right].x += flag_impulse_add_x_; + flag_velocities_[top_right].y += flag_impulse_add_y_; + flag_velocities_[top_right].z += flag_impulse_add_z_; + } + + // Wind. + // FIXME - we can prolly move some of this out of the inner loop.. + if (explicit_bool(do_wind)) { + flag_velocities_[top_right].x += + wind_vec.x * kWindStrength + * (Utils::precalc_rands_1[wind_rand_ % kPrecalcRandsCount] - 0.3f); + flag_velocities_[top_right].y += + wind_vec.y * kWindStrength + * (Utils::precalc_rands_2[wind_rand_ % kPrecalcRandsCount] - 0.3f); + flag_velocities_[top_right].z += + wind_vec.z * kWindStrength + * (Utils::precalc_rands_3[wind_rand_ % kPrecalcRandsCount] - 0.3f); + } + UpdateSpringPoint(top_left, top_right, kFlagCanvasScaleX); + UpdateSpringPoint(bot_left, bot_right, kFlagCanvasScaleX); + UpdateSpringPoint(top_left, bot_left, kFlagCanvasScaleY); + UpdateSpringPoint(top_right, bot_right, kFlagCanvasScaleY); + UpdateSpringPoint(top_left, bot_right, kFlagCanvasScaleDiagonal); + UpdateSpringPoint(top_right, bot_left, kFlagCanvasScaleDiagonal); + } + } + + flag_impulse_add_x_ = flag_impulse_add_y_ = flag_impulse_add_z_ = 0; + + // Now update positions (except pole points). + for (int y = 0; y < kFlagSizeY; y++) { + for (int x = 0; x < kFlagSizeX; x++) { + int i = kFlagSizeX * y + x; + flag_points_[i] += flag_velocities_[i]; + } + } + + // Now calc normals. + for (int y = 0; y < kFlagSizeY; y++) { + for (int x = 0; x < kFlagSizeX; x++) { + // Calc the normal for this vert. + int xclamped = std::min(x, kFlagSizeX - 2); + int yclamped = std::min(y, kFlagSizeY - 2); + int i = kFlagSizeX * yclamped + xclamped; + flag_normals_[i] = + Vector3f::Cross(flag_points_[i + 1] - flag_points_[i], + flag_points_[i + kFlagSizeX] - flag_points_[i]) + .Normalized(); + } + } +} + +void FlagNode::GetRigidBodyPickupLocations(int id, float* obj, float* character, + float* hand1, float* hand2) { + obj[0] = 0; + obj[1] = 0; + obj[2] = -0.6f; + character[0] = 0; + character[1] = -0.4f; + character[2] = 0.3f; + hand1[0] = hand1[1] = hand1[2] = 0; + hand2[0] = hand2[1] = hand2[2] = 0; + hand2[0] = 0.05f; + hand2[2] = -0.1f; + hand1[0] = -0.05f; + hand1[2] = -0.05f; +} + +void FlagNode::OnGraphicsQualityChanged(GraphicsQuality q) { + UpdateForGraphicsQuality(q); +} + +void FlagNode::UpdateForGraphicsQuality(GraphicsQuality quality) { +#if !BA_HEADLESS_BUILD + if (quality >= GraphicsQuality::kMedium) { + full_shadow_set_ = Object::New(); + simple_shadow_set_.Clear(); + } else { + simple_shadow_set_ = Object::New(); + full_shadow_set_.Clear(); + } +#endif +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/flag_node.h b/src/ballistica/scene/node/flag_node.h new file mode 100644 index 00000000..19c19e8c --- /dev/null +++ b/src/ballistica/scene/node/flag_node.h @@ -0,0 +1,77 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_FLAG_NODE_H_ +#define BALLISTICA_SCENE_NODE_FLAG_NODE_H_ + +#include + +#include "ballistica/dynamics/part.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class FlagNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit FlagNode(Scene* scene); + ~FlagNode() override; + void HandleMessage(const char* data) override; + void Draw(FrameDef* frame_def) override; + void Step() override; + auto GetRigidBody(int id) -> RigidBody* override; + auto is_area_of_interest() const -> bool { + return (area_of_interest_ != nullptr); + } + void SetIsAreaOfInterest(bool val); + auto getPosition() const -> std::vector; + void SetPosition(const std::vector& vals); + auto color_texture() const -> Texture* { return color_texture_.get(); } + void set_color_texture(Texture* val) { color_texture_ = val; } + auto light_weight() const -> bool { return light_weight_; } + void SetLightWeight(bool val); + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& vals); + auto GetMaterials() const -> std::vector; + void SetMaterials(const std::vector& materials); + + private: + class FullShadowSet; + class SimpleShadowSet; + void UpdateAreaOfInterest(); + void GetRigidBodyPickupLocations(int id, float* obj, float* character, + float* hand1, float* hand2) override; + void UpdateDimensions(); + void ResetFlagMesh(); + void UpdateFlagMesh(); + void OnGraphicsQualityChanged(GraphicsQuality q) override; + void UpdateForGraphicsQuality(GraphicsQuality q); + void UpdateSpringPoint(int p1, int p2, float rest_length); + AreaOfInterest* area_of_interest_ = nullptr; + Part part_; + std::vector color_ = {1.0f, 1.0f, 1.0f}; + Object::Ref body_{nullptr}; + Object::Ref color_texture_; + MeshIndexedObjectSplit mesh_; +#if !BA_HEADLESS_BUILD + Object::Ref full_shadow_set_; + Object::Ref simple_shadow_set_; +#endif // !BA_HEADLESS_BUILD + int wind_rand_{}; + float wind_rand_x_{}; + float wind_rand_y_{}; + float wind_rand_z_{}; + float flag_impulse_add_x_{}; + float flag_impulse_add_y_{}; + float flag_impulse_add_z_{}; + bool have_flag_impulse_{}; + int footing_{}; + bool light_weight_{}; + Vector3f flag_points_[25]{}; + Vector3f flag_normals_[25]{}; + Vector3f flag_velocities_[25]{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_FLAG_NODE_H_ diff --git a/src/ballistica/scene/node/flash_node.cc b/src/ballistica/scene/node/flash_node.cc new file mode 100644 index 00000000..825ed2d7 --- /dev/null +++ b/src/ballistica/scene/node/flash_node.cc @@ -0,0 +1,60 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/flash_node.h" + +#include + +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class FlashNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS FlashNode + BA_NODE_CREATE_CALL(CreateFlash); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ATTR(size, size, set_size); + BA_FLOAT_ARRAY_ATTR(color, color, set_color); +#undef BA_NODE_TYPE_CLASS + + FlashNodeType() + : NodeType("flash", CreateFlash), + position(this), + size(this), + color(this) {} +}; +static NodeType* node_type{}; + +auto FlashNode::InitType() -> NodeType* { + node_type = new FlashNodeType(); + return node_type; +} + +FlashNode::FlashNode(Scene* scene) : Node(scene, node_type) {} + +FlashNode::~FlashNode() = default; + +void FlashNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for position"); + } + position_ = vals; +} + +void FlashNode::Draw(FrameDef* frame_def) { + ObjectComponent c(frame_def->beauty_pass()); + c.SetLightShadow(LightShadowType::kNone); + c.SetColor(color_[0], color_[1], color_[2], 1.0f); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(size_, size_, size_); + c.Rotate(RandomFloat() * 360.0f, 1, 1, 0); + c.DrawModel(g_media->GetModel(SystemModelID::kFlash)); + c.PopTransform(); + c.Submit(); +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/flash_node.h b/src/ballistica/scene/node/flash_node.h new file mode 100644 index 00000000..b86280ac --- /dev/null +++ b/src/ballistica/scene/node/flash_node.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_FLASH_NODE_H_ +#define BALLISTICA_SCENE_NODE_FLASH_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class FlashNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit FlashNode(Scene* scene); + ~FlashNode() override; + void Draw(FrameDef* frame_def) override; + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& vals); + auto size() const -> float { return size_; } + void set_size(float val) { size_ = val; } + auto color() const -> std::vector { return color_; } + void set_color(const std::vector& vals) { color_ = vals; } + + private: + std::vector position_ = {0.0f, 0.0f, 0.0f}; + float size_ = 1.0f; + std::vector color_ = {0.5f, 0.5f, 0.5f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_FLASH_NODE_H_ diff --git a/src/ballistica/scene/node/globals_node.cc b/src/ballistica/scene/node/globals_node.cc new file mode 100644 index 00000000..391dac28 --- /dev/null +++ b/src/ballistica/scene/node/globals_node.cc @@ -0,0 +1,444 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/globals_node.h" + +#include "ballistica/audio/audio.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/vr_graphics.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class GlobalsNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS GlobalsNode + BA_NODE_CREATE_CALL(CreateGlobals); + BA_INT64_ATTR_READONLY(real_time, GetRealTime); + BA_INT64_ATTR_READONLY(time, GetTime); + BA_INT64_ATTR_READONLY(step, GetStep); + BA_FLOAT_ATTR(debris_friction, debris_friction, SetDebrisFriction); + BA_BOOL_ATTR(floor_reflection, floor_reflection, SetFloorReflection); + BA_FLOAT_ATTR(debris_kill_height, debris_kill_height, SetDebrisKillHeight); + BA_STRING_ATTR(camera_mode, GetCameraMode, SetCameraMode); + BA_BOOL_ATTR(happy_thoughts_mode, happy_thoughts_mode, SetHappyThoughtsMode); + BA_FLOAT_ARRAY_ATTR(shadow_scale, shadow_scale, SetShadowScale); + BA_FLOAT_ARRAY_ATTR(area_of_interest_bounds, area_of_interest_bounds, + set_area_of_interest_bounds); + BA_FLOAT_ARRAY_ATTR(shadow_range, shadow_range, SetShadowRange); + BA_FLOAT_ARRAY_ATTR(shadow_offset, shadow_offset, SetShadowOffset); + BA_BOOL_ATTR(shadow_ortho, shadow_ortho, SetShadowOrtho); + BA_FLOAT_ARRAY_ATTR(tint, tint, SetTint); + BA_FLOAT_ARRAY_ATTR(vr_overlay_center, vr_overlay_center, SetVROverlayCenter); + BA_BOOL_ATTR(vr_overlay_center_enabled, vr_overlay_center_enabled, + SetVROverlayCenterEnabled); + BA_FLOAT_ARRAY_ATTR(ambient_color, ambient_color, SetAmbientColor); + BA_FLOAT_ARRAY_ATTR(vignette_outer, vignette_outer, SetVignetteOuter); + BA_FLOAT_ARRAY_ATTR(vignette_inner, vignette_inner, SetVignetteInner); + BA_BOOL_ATTR(allow_kick_idle_players, allow_kick_idle_players, + SetAllowKickIdlePlayers); + BA_BOOL_ATTR(slow_motion, slow_motion, SetSlowMotion); + BA_BOOL_ATTR(paused, paused, SetPaused); + BA_FLOAT_ARRAY_ATTR(vr_camera_offset, vr_camera_offset, SetVRCameraOffset); + BA_BOOL_ATTR(use_fixed_vr_overlay, use_fixed_vr_overlay, + SetUseFixedVROverlay); + BA_FLOAT_ATTR(vr_near_clip, vr_near_clip, SetVRNearClip); + BA_BOOL_ATTR(music_continuous, music_continuous, set_music_continuous); + BA_STRING_ATTR(music, music, set_music); + BA_INT_ATTR(music_count, music_count, SetMusicCount); +#undef BA_NODE_TYPE_CLASS + + GlobalsNodeType() + : NodeType("globals", CreateGlobals), + real_time(this), + time(this), + step(this), + debris_friction(this), + floor_reflection(this), + debris_kill_height(this), + camera_mode(this), + happy_thoughts_mode(this), + shadow_scale(this), + area_of_interest_bounds(this), + shadow_range(this), + shadow_offset(this), + shadow_ortho(this), + tint(this), + vr_overlay_center(this), + vr_overlay_center_enabled(this), + ambient_color(this), + vignette_outer(this), + vignette_inner(this), + allow_kick_idle_players(this), + slow_motion(this), + paused(this), + vr_camera_offset(this), + use_fixed_vr_overlay(this), + vr_near_clip(this), + music_continuous(this), + music(this), + music_count(this) {} +}; + +static NodeType* node_type{}; + +auto GlobalsNode::InitType() -> NodeType* { + node_type = new GlobalsNodeType(); + return node_type; +} + +GlobalsNode::GlobalsNode(Scene* scene) : Node(scene, node_type) { + // Set ourself as the current globals node for our scene. + this->scene()->set_globals_node(this); + + // If we're being made in a host-activity, also set ourself as the current + // globals node for our activity. (there should only be one, so complain if + // there already is one). + // FIXME: Need to update this for non-host activities at some point. + if (HostActivity* ha = context().GetHostActivity()) { + if (ha->globals_node()) { + Log("WARNING: more than one globals node created in HostActivity; this " + "shouldn't happen"); + } + ha->globals_node_ = this; + + // Set some values we always drive even when not the singleton 'current' + // globals (stuff that only affects our activity/scene). + ha->SetGameSpeed(slow_motion_ ? 0.32f : 1.0f); + ha->SetPaused(paused_); + ha->set_allow_kick_idle_players(allow_kick_idle_players_); + this->scene()->set_use_fixed_vr_overlay(use_fixed_vr_overlay_); + } + + // If our scene is currently the game's foreground one, go ahead and + // push our values globally. + if (g_game->GetForegroundScene() == this->scene()) { + SetAsForeground(); + } +} + +GlobalsNode::~GlobalsNode() { + // If we are the current globals node for our scene, clear it out. + if (scene()->globals_node() == this) { + scene()->set_globals_node(nullptr); + } +} + +// Called when we're being made the one foreground node and should push our +// values to the global state (since there can be multiple scenes in +// existence, there has to be a single "foreground" globals node in control). +void GlobalsNode::SetAsForeground() { +#if !BA_HEADLESS_BUILD + g_bg_dynamics->SetDebrisFriction(debris_friction_); + g_bg_dynamics->SetDebrisKillHeight(debris_kill_height_); +#endif + g_graphics->set_floor_reflection(floor_reflection_); + g_graphics->camera()->SetMode(camera_mode_); + g_graphics->camera()->set_vr_offset(Vector3f(vr_camera_offset_)); + g_graphics->camera()->set_happy_thoughts_mode(happy_thoughts_mode_); + g_graphics->set_shadow_scale(shadow_scale_[0], shadow_scale_[1]); + g_graphics->camera()->set_area_of_interest_bounds( + area_of_interest_bounds_[0], area_of_interest_bounds_[1], + area_of_interest_bounds_[2], area_of_interest_bounds_[3], + area_of_interest_bounds_[4], area_of_interest_bounds_[5]); + g_graphics->SetShadowRange(shadow_range_[0], shadow_range_[1], + shadow_range_[2], shadow_range_[3]); + g_graphics->set_shadow_offset(Vector3f(shadow_offset_)); + g_graphics->set_shadow_ortho(shadow_ortho_); + g_graphics->set_tint(Vector3f(tint_)); + +#if BA_VR_BUILD + if (IsVRMode()) { + auto* vrgraphics = VRGraphics::get(); + vrgraphics->set_vr_near_clip(vr_near_clip_); + vrgraphics->set_vr_overlay_center(Vector3f(vr_overlay_center_)); + vrgraphics->set_vr_overlay_center_enabled(vr_overlay_center_enabled_); + } +#endif + + g_graphics->set_ambient_color(Vector3f(ambient_color_)); + g_graphics->set_vignette_outer(Vector3f(vignette_outer_)); + g_graphics->set_vignette_inner(Vector3f(vignette_inner_)); + + g_audio->SetSoundPitch(slow_motion_ ? 0.4f : 1.0f); + + // Tell the scripting layer to play our current music. + g_python->PlayMusic(music_, music_continuous_); +} + +auto GlobalsNode::IsCurrentGlobals() const -> bool { + // We're current if our scene is the foreground one and we're the globals + // node for our scene. + Scene* scene = this->scene(); + assert(scene); + return (g_game->GetForegroundScene() == this->scene() + && scene->globals_node() == this); +} + +auto GlobalsNode::GetRealTime() -> millisecs_t { + // Pull this from our scene so we return consistent values throughout a step. + return scene()->last_step_real_time(); +} + +auto GlobalsNode::GetTime() -> millisecs_t { return scene()->time(); } +auto GlobalsNode::GetStep() -> int64_t { return scene()->stepnum(); } + +void GlobalsNode::SetDebrisFriction(float val) { + debris_friction_ = val; + if (IsCurrentGlobals()) { +#if !BA_HEADLESS_BUILD + g_bg_dynamics->SetDebrisFriction(debris_friction_); +#endif // !BA_HEADLESS_BUILD + } +} + +void GlobalsNode::SetVRNearClip(float val) { + vr_near_clip_ = val; +#if BA_VR_BUILD + if (IsVRMode()) { + if (IsCurrentGlobals()) { + VRGraphics::get()->set_vr_near_clip(vr_near_clip_); + } + } +#endif +} + +void GlobalsNode::SetFloorReflection(bool val) { + floor_reflection_ = val; + if (IsCurrentGlobals()) { + g_graphics->set_floor_reflection(floor_reflection_); + } +} + +void GlobalsNode::SetDebrisKillHeight(float val) { + debris_kill_height_ = val; + if (IsCurrentGlobals()) { +#if !BA_HEADLESS_BUILD + g_bg_dynamics->SetDebrisKillHeight(debris_kill_height_); +#endif // !BA_HEADLESS_BUILD + } +} + +void GlobalsNode::SetHappyThoughtsMode(bool val) { + happy_thoughts_mode_ = val; + if (IsCurrentGlobals()) { + g_graphics->camera()->set_happy_thoughts_mode(happy_thoughts_mode_); + } +} + +void GlobalsNode::SetShadowScale(const std::vector& vals) { + if (vals.size() != 2) { + throw Exception("Expected float array of length 2 for shadow_scale"); + } + shadow_scale_ = vals; + if (IsCurrentGlobals()) { + g_graphics->set_shadow_scale(shadow_scale_[0], shadow_scale_[1]); + } +} + +void GlobalsNode::set_area_of_interest_bounds(const std::vector& vals) { + if (vals.size() != 6) { + throw Exception( + "Expected float array of length 6 for area_of_interest_bounds"); + } + area_of_interest_bounds_ = vals; + + assert(g_graphics->camera()); + if (IsCurrentGlobals()) { + g_graphics->camera()->set_area_of_interest_bounds( + area_of_interest_bounds_[0], area_of_interest_bounds_[1], + area_of_interest_bounds_[2], area_of_interest_bounds_[3], + area_of_interest_bounds_[4], area_of_interest_bounds_[5]); + } +} + +void GlobalsNode::SetShadowRange(const std::vector& vals) { + if (vals.size() != 4) { + throw Exception("Expected float array of length 4 for shadow_range"); + } + shadow_range_ = vals; + if (IsCurrentGlobals()) { + g_graphics->SetShadowRange(shadow_range_[0], shadow_range_[1], + shadow_range_[2], shadow_range_[3]); + } +} + +void GlobalsNode::SetShadowOffset(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for shadow_offset"); + } + shadow_offset_ = vals; + if (IsCurrentGlobals()) { + g_graphics->set_shadow_offset(Vector3f(shadow_offset_)); + } +} + +void GlobalsNode::SetVRCameraOffset(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for vr_camera_offset"); + } + vr_camera_offset_ = vals; + if (IsCurrentGlobals()) { + g_graphics->camera()->set_vr_offset(Vector3f(vr_camera_offset_)); + } +} + +void GlobalsNode::SetShadowOrtho(bool val) { + shadow_ortho_ = val; + if (IsCurrentGlobals()) { + g_graphics->set_shadow_ortho(shadow_ortho_); + } +} + +void GlobalsNode::SetTint(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for tint"); + } + tint_ = vals; + if (IsCurrentGlobals()) { + g_graphics->set_tint(Vector3f(tint_[0], tint_[1], tint_[2])); + } +} + +void GlobalsNode::SetVROverlayCenter(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for vr_overlay_center"); + } + vr_overlay_center_ = vals; +#if BA_VR_BUILD + if (IsCurrentGlobals()) { + VRGraphics::get()->set_vr_overlay_center(Vector3f(vr_overlay_center_)); + } +#endif +} + +void GlobalsNode::SetVROverlayCenterEnabled(bool val) { + vr_overlay_center_enabled_ = val; +#if BA_VR_BUILD + if (IsCurrentGlobals()) { + VRGraphics::get()->set_vr_overlay_center_enabled( + vr_overlay_center_enabled_); + } +#endif +} + +void GlobalsNode::SetAmbientColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for ambient_color"); + } + ambient_color_ = vals; + if (IsCurrentGlobals()) { + g_graphics->set_ambient_color(Vector3f(ambient_color_)); + } +} + +void GlobalsNode::SetVignetteOuter(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for vignette_outer"); + } + vignette_outer_ = vals; + if (IsCurrentGlobals()) { + g_graphics->set_vignette_outer(Vector3f(vignette_outer_)); + } +} + +void GlobalsNode::SetVignetteInner(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for vignette_inner"); + } + vignette_inner_ = vals; + if (IsCurrentGlobals()) { + g_graphics->set_vignette_inner(Vector3f(vignette_inner_)); + } +} + +auto GlobalsNode::GetCameraMode() const -> std::string { + switch (camera_mode_) { + case CameraMode::kOrbit: + return "rotate"; + case CameraMode::kFollow: + return "follow"; + default: + Log("ERROR: Globals: Unrecognized camera_mode_: " + + std::to_string(static_cast(camera_mode_))); + return "unknown"; + } +} + +void GlobalsNode::SetCameraMode(const std::string& val) { + if (val == "rotate") { + camera_mode_ = CameraMode::kOrbit; + } else if (val == "follow") { + camera_mode_ = CameraMode::kFollow; + } else { + throw Exception("Invalid camera mode: '" + val + + R"('; expected "rotate" or "follow")"); + } + if (IsCurrentGlobals()) g_graphics->camera()->SetMode(camera_mode_); +} + +void GlobalsNode::SetAllowKickIdlePlayers(bool val) { + allow_kick_idle_players_ = val; + + // This only means something if we're in a host-activity. + if (HostActivity* ha = context().GetHostActivity()) { + // Set speed on our activity even if we're not the current globals node. + if (ha->globals_node() == this) { + ha->set_allow_kick_idle_players(allow_kick_idle_players_); + } + } +} + +void GlobalsNode::SetSlowMotion(bool val) { + slow_motion_ = val; + + // This only matters if we're in a host-activity. + // (clients are just driven by whatever steps are in the input-stream) + if (HostActivity* ha = context().GetHostActivity()) { + // Set speed on *our* activity regardless of whether we're the current + // globals node. + if (ha->globals_node() == this) { + ha->SetGameSpeed(slow_motion_ ? 0.32f : 1.0f); + } + } + + // Only set pitch if we are the current globals node. + // (FIXME - need to make this per-sound or something) + if (IsCurrentGlobals()) { + g_audio->SetSoundPitch(slow_motion_ ? 0.4f : 1.0f); + } +} + +void GlobalsNode::SetPaused(bool val) { + paused_ = val; + + // This only matters in a host-activity. + // (clients are just driven by whatever steps are in the input-stream) + if (HostActivity* ha = context().GetHostActivity()) { + // Set speed on our activity even if we're not the current globals node. + if (ha->globals_node() == this) { + ha->SetPaused(paused_); + } + } +} + +void GlobalsNode::SetUseFixedVROverlay(bool val) { + use_fixed_vr_overlay_ = val; + + // Always apply this value to our scene. + scene()->set_use_fixed_vr_overlay(val); +} + +void GlobalsNode::SetMusicCount(int val) { + if (music_count_ != val && IsCurrentGlobals()) { + g_python->PlayMusic(music_, music_continuous_); + } + music_count_ = val; +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/globals_node.h b/src/ballistica/scene/node/globals_node.h new file mode 100644 index 00000000..4d73a74a --- /dev/null +++ b/src/ballistica/scene/node/globals_node.h @@ -0,0 +1,130 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_GLOBALS_NODE_H_ +#define BALLISTICA_SCENE_NODE_GLOBALS_NODE_H_ + +#include +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class GlobalsNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit GlobalsNode(Scene* scene); + ~GlobalsNode() override; + void SetAsForeground(); + auto IsCurrentGlobals() const -> bool; + auto GetRealTime() -> millisecs_t; + auto GetTime() -> millisecs_t; + auto GetStep() -> int64_t; + auto debris_friction() const -> float { return debris_friction_; } + void SetDebrisFriction(float val); + auto floor_reflection() const -> bool { return floor_reflection_; } + void SetFloorReflection(bool val); + auto debris_kill_height() const -> float { return debris_kill_height_; } + void SetDebrisKillHeight(float val); + auto GetCameraMode() const -> std::string; + void SetCameraMode(const std::string& val); + void SetHappyThoughtsMode(bool val); + auto happy_thoughts_mode() const -> bool { return happy_thoughts_mode_; } + auto shadow_scale() const -> const std::vector& { + return shadow_scale_; + } + void SetShadowScale(const std::vector& vals); + auto area_of_interest_bounds() const -> const std::vector& { + return area_of_interest_bounds_; + } + void set_area_of_interest_bounds(const std::vector& vals); + auto shadow_range() const -> const std::vector& { + return shadow_range_; + } + void SetShadowRange(const std::vector& vals); + auto shadow_offset() const -> const std::vector& { + return shadow_offset_; + } + void SetShadowOffset(const std::vector& vals); + auto shadow_ortho() const -> bool { return shadow_ortho_; } + void SetShadowOrtho(bool val); + auto tint() const -> const std::vector& { return tint_; } + void SetTint(const std::vector& vals); + auto vr_overlay_center() const -> const std::vector& { + return vr_overlay_center_; + } + void SetVROverlayCenter(const std::vector& vals); + auto vr_overlay_center_enabled() const -> bool { + return vr_overlay_center_enabled_; + } + void SetVROverlayCenterEnabled(bool); + auto ambient_color() const -> const std::vector& { + return ambient_color_; + } + void SetAmbientColor(const std::vector& vals); + auto vignette_outer() const -> const std::vector& { + return vignette_outer_; + } + void SetVignetteOuter(const std::vector& vals); + auto vignette_inner() const -> const std::vector& { + return vignette_inner_; + } + void SetVignetteInner(const std::vector& vals); + auto allow_kick_idle_players() const -> bool { + return allow_kick_idle_players_; + } + void SetAllowKickIdlePlayers(bool allow); + auto slow_motion() const -> bool { return slow_motion_; } + void SetSlowMotion(bool val); + auto paused() const -> bool { return paused_; } + void SetPaused(bool val); + auto vr_camera_offset() const -> const std::vector& { + return vr_camera_offset_; + } + void SetVRCameraOffset(const std::vector& vals); + auto use_fixed_vr_overlay() const -> bool { return use_fixed_vr_overlay_; } + void SetUseFixedVROverlay(bool val); + auto vr_near_clip() const -> float { return vr_near_clip_; } + void SetVRNearClip(float val); + auto music_continuous() const -> bool { return music_continuous_; } + void set_music_continuous(bool val) { music_continuous_ = val; } + auto music() const -> const std::string& { return music_; } + void set_music(const std::string& val) { music_ = val; } + + // We actually change the song only when this value changes + // (allows us to restart the same song) + auto music_count() const -> int { return music_count_; } + void SetMusicCount(int val); + + protected: + CameraMode camera_mode_{CameraMode::kFollow}; + float vr_near_clip_{4.0f}; + float debris_friction_{1.0f}; + bool floor_reflection_{}; + float debris_kill_height_{-50.0f}; + bool happy_thoughts_mode_{}; + bool use_fixed_vr_overlay_{}; + int music_count_{}; + bool music_continuous_{}; + std::string music_; + std::vector vr_camera_offset_{0.0f, 0.0f, 0.0f}; + std::vector shadow_scale_{1.0f, 1.0f}; + std::vector area_of_interest_bounds_{-9999.0f, -9999.0f, -9999.0f, + 9999.0f, 9999.0f, 9999.0f}; + std::vector shadow_range_{-4.0f, 0.0f, 10.0f, 15.0f}; + std::vector shadow_offset_{0.0f, 0.0f, 0.0f}; + bool shadow_ortho_{}; + bool vr_overlay_center_enabled_{}; + std::vector vr_overlay_center_{0.0f, 4.0f, -3.0f}; + std::vector tint_{1.1f, 1.0f, 0.9f}; + std::vector ambient_color_{1.0f, 1.0f, 1.0f}; + std::vector vignette_outer_{0.6f, 0.6f, 0.6f}; + std::vector vignette_inner_{0.95f, 0.95f, 0.95f}; + bool allow_kick_idle_players_{}; + bool slow_motion_{}; + bool paused_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_GLOBALS_NODE_H_ diff --git a/src/ballistica/scene/node/image_node.cc b/src/ballistica/scene/node/image_node.cc new file mode 100644 index 00000000..68c8cd63 --- /dev/null +++ b/src/ballistica/scene/node/image_node.cc @@ -0,0 +1,400 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/image_node.h" + +#include +#include + +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/media/component/model.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class ImageNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS ImageNode + BA_NODE_CREATE_CALL(CreateImage); + BA_FLOAT_ARRAY_ATTR(scale, scale, SetScale); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ATTR(opacity, opacity, set_opacity); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_FLOAT_ARRAY_ATTR(tint_color, tint_color, SetTintColor); + BA_FLOAT_ARRAY_ATTR(tint2_color, tint2_color, SetTint2Color); + BA_BOOL_ATTR(fill_screen, fill_screen, SetFillScreen); + BA_BOOL_ATTR(has_alpha_channel, has_alpha_channel, set_has_alpha_channel); + BA_BOOL_ATTR(absolute_scale, absolute_scale, set_absolute_scale); + BA_FLOAT_ATTR(tilt_translate, tilt_translate, set_tilt_translate); + BA_FLOAT_ATTR(rotate, rotate, set_rotate); + BA_BOOL_ATTR(premultiplied, premultiplied, set_premultiplied); + BA_STRING_ATTR(attach, GetAttach, SetAttach); + BA_TEXTURE_ATTR(texture, texture, set_texture); + BA_TEXTURE_ATTR(tint_texture, tint_texture, set_tint_texture); + BA_TEXTURE_ATTR(mask_texture, mask_texture, set_mask_texture); + BA_MODEL_ATTR(model_opaque, model_opaque, set_model_opaque); + BA_MODEL_ATTR(model_transparent, model_transparent, set_model_transparent); + BA_FLOAT_ATTR(vr_depth, vr_depth, set_vr_depth); + BA_BOOL_ATTR(host_only, host_only, set_host_only); + BA_BOOL_ATTR(front, front, set_front); +#undef BA_NODE_TYPE_CLASS + + ImageNodeType() + : NodeType("image", CreateImage), + scale(this), + position(this), + opacity(this), + color(this), + tint_color(this), + tint2_color(this), + fill_screen(this), + has_alpha_channel(this), + absolute_scale(this), + tilt_translate(this), + rotate(this), + premultiplied(this), + attach(this), + texture(this), + tint_texture(this), + mask_texture(this), + model_opaque(this), + model_transparent(this), + vr_depth(this), + host_only(this), + front(this) {} +}; +static NodeType* node_type{}; + +auto ImageNode::InitType() -> NodeType* { + node_type = new ImageNodeType(); + return node_type; +} + +ImageNode::ImageNode(Scene* scene) : Node(scene, node_type) {} + +ImageNode::~ImageNode() { + if (fill_screen_) scene()->decrement_bg_cover_count(); +} + +auto ImageNode::GetAttach() const -> std::string { + switch (attach_) { + case Attach::CENTER: + return "center"; + case Attach::TOP_LEFT: + return "topLeft"; + case Attach::TOP_CENTER: + return "topCenter"; + case Attach::TOP_RIGHT: + return "topRight"; + case Attach::CENTER_RIGHT: + return "centerRight"; + case Attach::BOTTOM_RIGHT: + return "bottomRight"; + case Attach::BOTTOM_CENTER: + return "bottomCenter"; + case Attach::BOTTOM_LEFT: + return "bottomLeft"; + case Attach::CENTER_LEFT: + return "centerLeft"; + default: + throw Exception("Invalid attach val in ImageNode " + + std::to_string(static_cast(attach_))); + } +} + +void ImageNode::SetAttach(const std::string& val) { + dirty_ = true; + if (val == "center") { + attach_ = Attach::CENTER; + } else if (val == "topLeft") { + attach_ = Attach::TOP_LEFT; + } else if (val == "topCenter") { + attach_ = Attach::TOP_CENTER; + } else if (val == "topRight") { + attach_ = Attach::TOP_RIGHT; + } else if (val == "centerRight") { + attach_ = Attach::CENTER_RIGHT; + } else if (val == "bottomRight") { + attach_ = Attach::BOTTOM_RIGHT; + } else if (val == "bottomCenter") { + attach_ = Attach::BOTTOM_CENTER; + } else if (val == "bottomLeft") { + attach_ = Attach::BOTTOM_LEFT; + } else if (val == "centerLeft") { + attach_ = Attach::CENTER_LEFT; + } else { + throw Exception("Invalid attach value for ImageNode: " + val); + } +} + +void ImageNode::SetTint2Color(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for tint2_color"); + } + tint2_color_ = vals; + tint2_red_ = tint2_color_[0]; + tint2_green_ = tint2_color_[1]; + tint2_blue_ = tint2_color_[2]; +} + +void ImageNode::SetTintColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for tint_color"); + } + tint_color_ = vals; + tint_red_ = tint_color_[0]; + tint_green_ = tint_color_[1]; + tint_blue_ = tint_color_[2]; +} + +void ImageNode::SetColor(const std::vector& vals) { + if (vals.size() != 3 && vals.size() != 4) { + throw Exception("Got " + std::to_string(vals.size()) + + " values for 'color'; expected 3 or 4."); + } + red_ = vals[0]; + green_ = vals[1]; + blue_ = vals[2]; + if (vals.size() == 4) { + alpha_ = vals[3]; + } else { + alpha_ = 1.0f; + } + color_ = vals; +} + +void ImageNode::SetScale(const std::vector& vals) { + if (vals.size() != 1 && vals.size() != 2) { + throw Exception("Expected float array of length 1 or 2 for scale"); + } + dirty_ = true; + scale_ = vals; +} + +void ImageNode::SetPosition(const std::vector& vals) { + if (vals.size() != 2) { + throw Exception("Expected float array of length 2 for position"); + } + dirty_ = true; + position_ = vals; +} + +void ImageNode::OnScreenSizeChange() { dirty_ = true; } + +void ImageNode::SetFillScreen(bool val) { + bool old = fill_screen_; + fill_screen_ = val; + dirty_ = true; + + // Help the scene keep track of stuff that covers the whole background + // (so it knows it doesnt have to clear). + if (!old && fill_screen_) scene()->increment_bg_cover_count(); + if (old && !fill_screen_) scene()->decrement_bg_cover_count(); + + // We keep track of how many full-screen images are present at any given time. + // vr-mode uses this to lock down the overlay layer's position in that case. +} + +void ImageNode::Draw(FrameDef* frame_def) { + if (host_only_ && !context().GetHostSession()) return; + bool vr = (IsVRMode()); + + // In vr mode we use the fixed overlay position if our scene + // is set for that. + bool vr_use_fixed = (scene()->use_fixed_vr_overlay()); + + // Currently front and vr-fixed are mutually-exclusive.. need to fix. + if (front_) { + vr_use_fixed = false; + } + + RenderPass& pass(*(vr_use_fixed ? frame_def->GetOverlayFixedPass() + : front_ ? frame_def->overlay_front_pass() + : frame_def->overlay_pass())); + + // If the pass we're drawing into changes dimensions, recalc. + // Otherwise we break if a window is resized. + float screen_width = pass.virtual_width(); + float screen_height = pass.virtual_height(); + if (dirty_) { + float width = absolute_scale_ ? scale_[0] : screen_height * scale_[0]; + float height = + (scale_.size() > 1) + ? (absolute_scale_ ? scale_[1] : screen_height * scale_[1]) + : width; + float center_x, center_y; + float scale_mult_x = absolute_scale_ ? 1.0f : screen_width; + float scale_mult_y = absolute_scale_ ? 1.0f : screen_height; + float screen_center_x = screen_width / 2; + float screen_center_y = screen_height / 2; + float tx = position_[0]; + float ty = position_[1]; + if (!absolute_scale_) { + tx *= scale_mult_x; + ty *= scale_mult_y; + } + switch (attach_) { + case Attach::BOTTOM_LEFT: + case Attach::BOTTOM_CENTER: + case Attach::BOTTOM_RIGHT: { + center_y = ty; + break; + } + case Attach::TOP_LEFT: + case Attach::TOP_CENTER: + case Attach::TOP_RIGHT: { + center_y = screen_height + ty; + break; + } + case Attach::CENTER_LEFT: + case Attach::CENTER_RIGHT: + case Attach::CENTER: { + center_y = screen_center_y + ty; + break; + } + } + + switch (attach_) { + case Attach::TOP_LEFT: + case Attach::CENTER_LEFT: + case Attach::BOTTOM_LEFT: { + center_x = tx; + break; + } + case Attach::TOP_CENTER: + case Attach::CENTER: + case Attach::BOTTOM_CENTER: { + center_x = screen_center_x + tx; + break; + } + case Attach::TOP_RIGHT: + case Attach::CENTER_RIGHT: + case Attach::BOTTOM_RIGHT: { + center_x = screen_width + tx; + break; + } + } + if (fill_screen_) { + width_ = screen_width; + height_ = screen_height; + center_x_ = width_ * 0.5f; + center_y_ = height_ * 0.5f; + } else { + center_x_ = center_x; + center_y_ = center_y; + width_ = width; + height_ = height; + } + dirty_ = false; + } + float fin_center_x = center_x_; + float fin_center_y = center_y_; + float fin_width = width_; + float fin_height = height_; + + // Tilt-translate doesn't happen in vr mode. + if (tilt_translate_ != 0.0f && !vr) { + Vector3f tilt = g_graphics->tilt(); + fin_center_x -= tilt.y * tilt_translate_; + fin_center_y += tilt.x * tilt_translate_; + + // If we're fullscreen and are tilting, crank our dimensions up + // slightly to account for tiltage. +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + if (fill_screen_) { + float s = 1.0f - tilt_translate_ * 0.2f; + fin_width *= s; + fin_height *= s; + } +#endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + } + + bool has_alpha_channel = has_alpha_channel_; + float alpha = opacity_ * alpha_; + if (alpha < 0) { + alpha = 0; + } + ModelData* model_opaque_used = nullptr; + if (model_opaque_.exists()) model_opaque_used = model_opaque_->model_data(); + ModelData* model_transparent_used = nullptr; + if (model_transparent_.exists()) { + model_transparent_used = model_transparent_->model_data(); + } + + // If no meshes were provided, use default image models. + if (!model_opaque_.exists() && !model_transparent_.exists()) { + if (vr && fill_screen_) { +#if BA_VR_BUILD + model_opaque_used = + g_media->GetModel(SystemModelID::kImage1x1VRFullScreen); +#else + throw Exception(); +#endif // BA_VR_BUILD + } else { + SystemModelID m = fill_screen_ ? SystemModelID::kImage1x1FullScreen + : SystemModelID::kImage1x1; + if (has_alpha_channel) { + model_transparent_used = g_media->GetModel(m); + } else { + model_opaque_used = g_media->GetModel(m); + } + } + } + + // Draw opaque portion either opaque or transparent depending on our + // global opacity. + if (model_opaque_used) { + // Draw in opaque pass if possible. + SimpleComponent c(&pass); + bool draw_transparent = (alpha < 0.999f); + + // Stuff in the fixed vr overlay pass may inadvertently + // obscure the non-fixed overlay pass, so lets just always draw + // transparent to avoid that. + c.SetTransparent(draw_transparent); + c.SetPremultiplied(premultiplied_); + c.SetTexture(texture_); + c.SetColor(red_, green_, blue_, alpha); + if (tint_texture_.exists()) { + c.SetColorizeTexture(tint_texture_); + c.SetColorizeColor(tint_red_, tint_green_, tint_blue_); + c.SetColorizeColor2(tint2_red_, tint2_green_, tint2_blue_); + } + c.SetMaskTexture(mask_texture_); + c.PushTransform(); + c.Translate(fin_center_x, fin_center_y, + vr ? vr_depth_ : g_graphics->overlay_node_z_depth()); + if (rotate_ != 0.0f) c.Rotate(rotate_, 0, 0, 1); + c.Scale(fin_width, fin_height, fin_width); + c.DrawModel(model_opaque_used); + c.PopTransform(); + c.Submit(); + } + // Transparent portion. + if (model_transparent_used) { + SimpleComponent c(&pass); + c.SetTransparent(true); + c.SetPremultiplied(premultiplied_); + c.SetTexture(texture_); + c.SetColor(red_, green_, blue_, alpha); + if (tint_texture_.exists()) { + c.SetColorizeTexture(tint_texture_); + c.SetColorizeColor(tint_red_, tint_green_, tint_blue_); + c.SetColorizeColor2(tint2_red_, tint2_green_, tint2_blue_); + } + c.SetMaskTexture(mask_texture_); + c.PushTransform(); + c.Translate(fin_center_x, fin_center_y, + vr ? vr_depth_ : g_graphics->overlay_node_z_depth()); + if (rotate_ != 0.0f) c.Rotate(rotate_, 0, 0, 1); + c.Scale(fin_width, fin_height, fin_width); + c.DrawModel(model_transparent_used); + c.PopTransform(); + c.Submit(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/image_node.h b/src/ballistica/scene/node/image_node.h new file mode 100644 index 00000000..813f9b5b --- /dev/null +++ b/src/ballistica/scene/node/image_node.h @@ -0,0 +1,124 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_IMAGE_NODE_H_ +#define BALLISTICA_SCENE_NODE_IMAGE_NODE_H_ + +#include +#include + +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// Node used to draw 2d image overlays on-screen. +class ImageNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit ImageNode(Scene* scene); + ~ImageNode() override; + void Draw(FrameDef* frame_def) override; + auto scale() const -> std::vector { return scale_; } + void SetScale(const std::vector& scale); + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& val); + auto opacity() const -> float { return opacity_; } + void set_opacity(float val) { opacity_ = val; } + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& val); + auto tint_color() const -> std::vector { return tint_color_; } + void SetTintColor(const std::vector& val); + auto tint2_color() const -> std::vector { return tint2_color_; } + void SetTint2Color(const std::vector& val); + auto fill_screen() const -> bool { return fill_screen_; } + void SetFillScreen(bool val); + auto has_alpha_channel() const -> bool { return has_alpha_channel_; } + void set_has_alpha_channel(bool val) { has_alpha_channel_ = val; } + auto absolute_scale() const -> bool { return absolute_scale_; } + void set_absolute_scale(bool val) { + absolute_scale_ = val; + dirty_ = true; + } + auto tilt_translate() const -> float { return tilt_translate_; } + void set_tilt_translate(float val) { tilt_translate_ = val; } + auto rotate() const -> float { return rotate_; } + void set_rotate(float val) { rotate_ = val; } + auto premultiplied() const -> bool { return premultiplied_; } + void set_premultiplied(bool val) { premultiplied_ = val; } + auto GetAttach() const -> std::string; + void SetAttach(const std::string& val); + auto texture() const -> Texture* { return texture_.get(); } + void set_texture(Texture* t) { texture_ = t; } + auto tint_texture() const -> Texture* { return tint_texture_.get(); } + void set_tint_texture(Texture* t) { tint_texture_ = t; } + auto mask_texture() const -> Texture* { return mask_texture_.get(); } + void set_mask_texture(Texture* t) { mask_texture_ = t; } + auto model_opaque() const -> Model* { return model_opaque_.get(); } + void set_model_opaque(Model* m) { model_opaque_ = m; } + auto model_transparent() const -> Model* { return model_transparent_.get(); } + void set_model_transparent(Model* m) { + model_transparent_ = m; + dirty_ = true; + } + auto vr_depth() const -> float { return vr_depth_; } + void set_vr_depth(float val) { vr_depth_ = val; } + void OnScreenSizeChange() override; + auto host_only() const -> bool { return host_only_; } + void set_host_only(bool val) { host_only_ = val; } + auto front() const -> bool { return front_; } + void set_front(bool val) { front_ = val; } + + private: + enum class Attach { + CENTER, + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + CENTER_RIGHT, + BOTTOM_RIGHT, + BOTTOM_CENTER, + BOTTOM_LEFT, + CENTER_LEFT + }; + bool host_only_{}; + bool front_{}; + float vr_depth_{}; + std::vector scale_{1.0f, 1.0f}; + std::vector position_{0.0f, 0.0f}; + std::vector color_{1.0f, 1.0f, 1.0f}; + std::vector tint_color_{1.0f, 1.0f, 1.0f}; + std::vector tint2_color_{1.0f, 1.0f, 1.0f}; + Object::Ref texture_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref model_opaque_; + Object::Ref model_transparent_; + bool fill_screen_{}; + bool has_alpha_channel_{true}; + bool dirty_{true}; + float opacity_{1.0f}; + Attach attach_{Attach::CENTER}; + bool absolute_scale_{true}; + float center_x_{}; + float center_y_{}; + float width_{}; + float height_{}; + float tilt_translate_{}; + float rotate_{}; + bool premultiplied_{}; + float red_{1.0f}; + float green_{1.0f}; + float blue_{1.0f}; + float alpha_{1.0f}; + float tint_red_{1.0f}; + float tint_green_{1.0f}; + float tint_blue_{1.0f}; + float tint2_red_{1.0f}; + float tint2_green_{1.0f}; + float tint2_blue_{1.0f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_IMAGE_NODE_H_ diff --git a/src/ballistica/scene/node/light_node.cc b/src/ballistica/scene/node/light_node.cc new file mode 100644 index 00000000..cb9e6d86 --- /dev/null +++ b/src/ballistica/scene/node/light_node.cc @@ -0,0 +1,153 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/light_node.h" + +#include + +#include "ballistica/dynamics/bg/bg_dynamics_volume_light.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class LightNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS LightNode + BA_NODE_CREATE_CALL(CreateLight); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ATTR(intensity, intensity, SetIntensity); + BA_FLOAT_ATTR(volume_intensity_scale, volume_intensity_scale, + SetVolumeIntensityScale); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_FLOAT_ATTR(radius, radius, SetRadius); + BA_BOOL_ATTR(lights_volumes, lights_volumes, set_lights_volumes); + BA_BOOL_ATTR(height_attenuated, height_attenuated, set_height_attenuated); +#undef BA_NODE_TYPE_CLASS + LightNodeType() + : NodeType("light", CreateLight), + position(this), + intensity(this), + volume_intensity_scale(this), + color(this), + radius(this), + lights_volumes(this), + height_attenuated(this) {} +}; +static NodeType* node_type{}; + +auto LightNode::InitType() -> NodeType* { + node_type = new LightNodeType(); + return node_type; +} + +LightNode::LightNode(Scene* scene) : Node(scene, node_type) {} + +auto LightNode::GetVolumeLightIntensity() -> float { + return intensity_ * volume_intensity_scale_ * 0.02f; +} + +void LightNode::Step() { +#if !BA_HEADLESS_BUILD + // create or destroy our light-volume as needed + // (minimize redundant create/destroy/sets this way) + if (lights_volumes_ && !volume_light_.exists()) { + volume_light_ = Object::New(); + float i = GetVolumeLightIntensity(); + volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); + volume_light_->SetPosition( + Vector3f(position_[0], position_[1], position_[2])); + } else if (!lights_volumes_ && volume_light_.exists()) { + volume_light_.Clear(); + } +#endif // BA_HEADLESS_BUILD +} + +void LightNode::SetRadius(float val) { + radius_ = std::max(0.0f, val); +#if !BA_HEADLESS_BUILD + if (volume_light_.exists()) { + volume_light_->SetRadius(radius_); + } +#endif // BA_HEADLESS_BUILD +} + +void LightNode::SetColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("expected float array of size 3 for color"); + } + color_ = vals; +#if !BA_HEADLESS_BUILD + if (volume_light_.exists()) { + float i = GetVolumeLightIntensity(); + volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); + } +#endif // BA_HEADLESS_BUILD +} + +void LightNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("expected float array of size 3 for position"); + } + position_ = vals; + +#if !BA_HEADLESS_BUILD + shadow_.SetPosition(Vector3f(position_[0], position_[1], position_[2])); + if (volume_light_.exists()) { + volume_light_->SetPosition( + Vector3f(position_[0], position_[1], position_[2])); + } +#endif // BA_HEADLESS_BUILD +} + +void LightNode::SetIntensity(float val) { + intensity_ = std::max(0.0f, val); +#if !BA_HEADLESS_BUILD + if (volume_light_.exists()) { + float i = GetVolumeLightIntensity(); + volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); + } +#endif // BA_HEADLESS_BUILD +} + +void LightNode::SetVolumeIntensityScale(float val) { + volume_intensity_scale_ = std::max(0.0f, val); + +#if !BA_HEADLESS_BUILD + if (volume_light_.exists()) { + float i = GetVolumeLightIntensity(); + volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); + } +#endif // BA_HEADLESS_BUILD +} + +void LightNode::Draw(FrameDef* frame_def) { +#if !BA_HEADLESS_BUILD + // if we haven't gotten our initial attributes, dont draw + assert(position_.size() == 3); + // if (position_.size() == 0) return; + + float s_density, s_scale; + + if (height_attenuated_) { + shadow_.GetValues(&s_scale, &s_density); + } else { + s_density = 1.0f; + s_scale = 1.0f; + } + + float brightness = s_density * 0.65f * intensity_; + + // draw our light on both terrain and objects + g_graphics->DrawBlotchSoft(Vector3f(&position_[0]), 20.0f * radius_ * s_scale, + color_[0] * brightness, color_[1] * brightness, + color_[2] * brightness, 0.0f); + + g_graphics->DrawBlotchSoftObj(Vector3f(&position_[0]), + 20.0f * radius_ * s_scale, + color_[0] * brightness, color_[1] * brightness, + color_[2] * brightness, 0.0f); +#endif // BA_HEADLESS_BUILD +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/light_node.h b/src/ballistica/scene/node/light_node.h new file mode 100644 index 00000000..93a5a592 --- /dev/null +++ b/src/ballistica/scene/node/light_node.h @@ -0,0 +1,54 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_LIGHT_NODE_H_ +#define BALLISTICA_SCENE_NODE_LIGHT_NODE_H_ + +#include + +#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// A light source +class LightNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit LightNode(Scene* scene); + void Draw(FrameDef* frame_def) override; + void Step() override; + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& val); + auto intensity() const -> float { return intensity_; } + void SetIntensity(float val); + auto volume_intensity_scale() const -> float { + return volume_intensity_scale_; + } + void SetVolumeIntensityScale(float val); + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& val); + auto radius() const -> float { return radius_; } + void SetRadius(float val); + auto lights_volumes() const -> bool { return lights_volumes_; } + void set_lights_volumes(bool val) { lights_volumes_ = val; } + auto height_attenuated() const -> bool { return height_attenuated_; } + void set_height_attenuated(bool val) { height_attenuated_ = val; } + + private: + auto GetVolumeLightIntensity() -> float; +#if !BA_HEADLESS_BUILD + BGDynamicsShadow shadow_{0.2f}; + Object::Ref volume_light_; +#endif + std::vector position_ = {0.0f, 0.0f, 0.0f}; + std::vector color_ = {1.0f, 1.0f, 1.0f}; + float intensity_ = 1.0f; + float volume_intensity_scale_ = 1.0f; + float radius_ = 0.5f; + bool height_attenuated_ = true; + bool lights_volumes_ = true; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_LIGHT_NODE_H_ diff --git a/src/ballistica/scene/node/locator_node.cc b/src/ballistica/scene/node/locator_node.cc new file mode 100644 index 00000000..88fbb508 --- /dev/null +++ b/src/ballistica/scene/node/locator_node.cc @@ -0,0 +1,179 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/locator_node.h" + +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class LocatorNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS LocatorNode + BA_NODE_CREATE_CALL(CreateLocator); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_BOOL_ATTR(visibility, visibility, set_visibility); + BA_FLOAT_ARRAY_ATTR(size, size, SetSize); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_FLOAT_ATTR(opacity, opacity, set_opacity); + BA_BOOL_ATTR(draw_beauty, draw_beauty, set_draw_beauty); + BA_BOOL_ATTR(drawShadow, getDrawShadow, setDrawShadow); + BA_STRING_ATTR(shape, getShape, SetShape); + BA_BOOL_ATTR(additive, getAdditive, setAdditive); +#undef BA_NODE_TYPE_CLASS + LocatorNodeType() + : NodeType("locator", CreateLocator), + position(this), + visibility(this), + size(this), + color(this), + opacity(this), + draw_beauty(this), + drawShadow(this), + shape(this), + additive(this) {} +}; +static NodeType* node_type{}; + +auto LocatorNode::InitType() -> NodeType* { + node_type = new LocatorNodeType(); + return node_type; +} + +LocatorNode::LocatorNode(Scene* scene) : Node(scene, node_type) {} + +auto LocatorNode::getShape() const -> std::string { + switch (shape_) { + case Shape::kBox: + return "box"; + case Shape::kCircle: + return "circle"; + case Shape::kCircleOutline: + return "circleOutline"; + case Shape::kLocator: + return "locator"; + default: + throw Exception("Invalid shape val: " + + std::to_string(static_cast(shape_))); + } +} + +void LocatorNode::SetShape(const std::string& val) { + if (val == "box") { + shape_ = Shape::kBox; + } else if (val == "circle") { + shape_ = Shape::kCircle; + } else if (val == "circleOutline") { + shape_ = Shape::kCircleOutline; + } else if (val == "locator") { + shape_ = Shape::kLocator; + } else { + throw Exception("invalid locator shape: " + val); + } +} + +void LocatorNode::SetColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for color"); + } + color_ = vals; +} + +void LocatorNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of size 3 for position"); + position_ = vals; +} + +void LocatorNode::SetSize(const std::vector& vals) { + if (vals.size() != 1 && vals.size() != 3) + throw Exception("Expected float array of size 1 or 3 for size"); + size_ = vals; + if (size_.size() == 1) { + size_.push_back(size_[0]); + size_.push_back(size_[0]); + } +} + +void LocatorNode::Draw(FrameDef* frame_def) { + SystemModelID model; + if (shape_ == Shape::kBox) { + model = SystemModelID::kLocatorBox; + } else if (shape_ == Shape::kCircle) { + model = SystemModelID::kLocatorCircle; + } else if (shape_ == Shape::kCircleOutline) { + model = SystemModelID::kLocatorCircleOutline; + } else { + model = SystemModelID::kLocator; + } + + SystemTextureID texture; + if (shape_ == Shape::kCircle) { + texture = + additive_ ? SystemTextureID::kCircleNoAlpha : SystemTextureID::kCircle; + } else if (shape_ == Shape::kCircleOutline) { + texture = additive_ ? SystemTextureID::kCircleOutlineNoAlpha + : SystemTextureID::kCircleOutline; + } else { + texture = SystemTextureID::kRGBStripes; + } + + bool transparent = false; + if (shape_ == Shape::kCircle || shape_ == Shape::kCircleOutline) + transparent = true; + + // beauty + if (draw_beauty_) { + SimpleComponent c(frame_def->beauty_pass()); + if (transparent) c.SetTransparent(true); + c.SetColor(color_[0], color_[1], color_[2], opacity_); + c.SetTexture(g_media->GetTexture(texture)); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(size_[0], size_[1], size_[2]); + c.DrawModel(g_media->GetModel(model)); + c.PopTransform(); + c.Submit(); + } + + if (draw_shadow_) { + // colored shadow for circle + if (shape_ == Shape::kCircle || shape_ == Shape::kCircleOutline) { + SimpleComponent c(frame_def->light_shadow_pass()); + if (transparent) { + c.SetTransparent(true); + if (additive_) { + c.SetPremultiplied(true); + } + } + if (additive_) { + c.SetColor(color_[0] * opacity_, color_[1] * opacity_, + color_[2] * opacity_, 0.0f); + } else { + c.SetColor(color_[0], color_[1], color_[2], opacity_); + } + c.SetTexture(g_media->GetTexture(texture)); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(size_[0], size_[1], size_[2]); + c.DrawModel(g_media->GetModel(model)); + c.PopTransform(); + c.Submit(); + } else { + // simple black shadow for locator/box + SimpleComponent c(frame_def->light_shadow_pass()); + c.SetTransparent(true); + c.SetColor(0.4f, 0.4f, 0.4f, 0.7f); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(size_[0], size_[1], size_[2]); + c.DrawModel(g_media->GetModel(model)); + c.PopTransform(); + c.Submit(); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/locator_node.h b/src/ballistica/scene/node/locator_node.h new file mode 100644 index 00000000..0f8fcdde --- /dev/null +++ b/src/ballistica/scene/node/locator_node.h @@ -0,0 +1,63 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_LOCATOR_NODE_H_ +#define BALLISTICA_SCENE_NODE_LOCATOR_NODE_H_ + +#include +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class LocatorNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit LocatorNode(Scene* scene); + + void Draw(FrameDef* frame_def) override; + + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& vals); + + auto visibility() const -> bool { return visibility_; } + void set_visibility(bool val) { visibility_ = val; } + + auto size() const -> std::vector { return size_; } + void SetSize(const std::vector& vals); + + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& vals); + + auto opacity() const -> float { return opacity_; } + void set_opacity(float val) { opacity_ = val; } + + auto draw_beauty() const -> bool { return draw_beauty_; } + void set_draw_beauty(bool val) { draw_beauty_ = val; } + + auto getDrawShadow() const -> bool { return draw_shadow_; } + void setDrawShadow(bool val) { draw_shadow_ = val; } + + auto getShape() const -> std::string; + void SetShape(const std::string& val); + + auto getAdditive() const -> bool { return additive_; } + void setAdditive(bool val) { additive_ = val; } + + private: + enum class Shape { kLocator, kBox, kCircle, kCircleOutline }; + + Shape shape_ = Shape::kLocator; + bool additive_ = false; + std::vector position_ = {0.0f, 0.0f, 0.0f}; + std::vector size_ = {1.0f, 1.0f, 1.0f}; + std::vector color_ = {1.0f, 1.0f, 1.0f}; + bool visibility_ = true; + float opacity_ = 1.0f; + bool draw_beauty_ = true; + bool draw_shadow_ = true; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_LOCATOR_NODE_H_ diff --git a/src/ballistica/scene/node/math_node.cc b/src/ballistica/scene/node/math_node.cc new file mode 100644 index 00000000..016eb702 --- /dev/null +++ b/src/ballistica/scene/node/math_node.cc @@ -0,0 +1,116 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/math_node.h" + +#include +#include + +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class MathNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS MathNode + BA_NODE_CREATE_CALL(CreateMath); + BA_FLOAT_ARRAY_ATTR_READONLY(output, GetOutput); + BA_FLOAT_ARRAY_ATTR(input1, input_1, set_input_1); + BA_FLOAT_ARRAY_ATTR(input2, input_2, set_input_2); + BA_STRING_ATTR(operation, GetOperation, SetOperation); +#undef BA_NODE_TYPE_CLASS + + MathNodeType() + : NodeType("math", CreateMath), + output(this), + input1(this), + input2(this), + operation(this) {} +}; + +static NodeType* node_type{}; + +auto MathNode::InitType() -> NodeType* { + node_type = new MathNodeType(); + return node_type; +} + +MathNode::MathNode(Scene* scene) : Node(scene, node_type) {} + +auto MathNode::GetOperation() const -> std::string { + switch (operation_) { + case Operation::kAdd: + return "add"; + case Operation::kSubtract: + return "subtract"; + case Operation::kMultiply: + return "multiply"; + case Operation::kDivide: + return "divide"; + case Operation::kSin: + return "sin"; + default: + throw Exception("invalid operation: " + + std::to_string(static_cast(operation_))); + } +} + +void MathNode::SetOperation(const std::string& val) { + if (val == "add") { + operation_ = Operation::kAdd; + } else if (val == "subtract") { + operation_ = Operation::kSubtract; + } else if (val == "multiply") { + operation_ = Operation::kMultiply; + } else if (val == "divide") { + operation_ = Operation::kDivide; + } else if (val == "sin") { + operation_ = Operation::kSin; + } else { + throw Exception("Invalid math node op '" + val + "'"); + } +} + +auto MathNode::GetOutput() -> std::vector { + size_t val_count = std::min(input_1_.size(), input_2_.size()); + std::vector outputs(val_count); + switch (operation_) { + case Operation::kAdd: { + for (size_t i = 0; i < val_count; i++) { + outputs[i] = (input_1_[i] + input_2_[i]); + } + break; + } + case Operation::kSubtract: { + for (size_t i = 0; i < val_count; i++) { + outputs[i] = (input_1_[i] - input_2_[i]); + } + break; + } + case Operation::kMultiply: { + for (size_t i = 0; i < val_count; i++) { + outputs[i] = (input_1_[i] * input_2_[i]); + } + break; + } + case Operation::kDivide: { + for (size_t i = 0; i < val_count; i++) { + outputs[i] = (input_1_[i] / input_2_[i]); + } + break; + } + case Operation::kSin: { + for (size_t i = 0; i < val_count; i++) { + outputs[i] = (sinf(input_1_[i])); + } + break; + } + default: + BA_LOG_ONCE("Error: invalid math op in getOutput(): " + + std::to_string(static_cast(operation_))); + break; + } + return outputs; +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/math_node.h b/src/ballistica/scene/node/math_node.h new file mode 100644 index 00000000..c9f7b5ca --- /dev/null +++ b/src/ballistica/scene/node/math_node.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_MATH_NODE_H_ +#define BALLISTICA_SCENE_NODE_MATH_NODE_H_ + +#include +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// An node used to create simple mathematical relationships via +// attribute connections +class MathNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit MathNode(Scene* scene); + auto GetOutput() -> std::vector; + auto input_1() const -> const std::vector& { return input_1_; } + void set_input_1(const std::vector& vals) { input_1_ = vals; } + auto input_2() const -> std::vector { return input_2_; } + void set_input_2(const std::vector& vals) { input_2_ = vals; } + auto GetOperation() const -> std::string; + void SetOperation(const std::string& val); + + private: + enum class Operation { kAdd, kSubtract, kMultiply, kDivide, kSin }; + std::vector input_1_; + std::vector input_2_; + Operation operation_ = Operation::kAdd; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_MATH_NODE_H_ diff --git a/src/ballistica/scene/node/node.cc b/src/ballistica/scene/node/node.cc new file mode 100644 index 00000000..d346544e --- /dev/null +++ b/src/ballistica/scene/node/node.cc @@ -0,0 +1,425 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/node.h" + +#include "ballistica/dynamics/part.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/python/class/python_class_node.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_attribute_connection.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +NodeType::~NodeType() { + Log("ERROR: SHOULD NOT BE DESTRUCTING A TYPE type=(" + name_ + ")"); +} + +Node::Node(Scene* scene_in, NodeType* node_type) + : node_type_(node_type), scene_(scene_in) {} +void Node::AddToScene(Scene* scene) { + // we should have already set our scene ptr in our constructor; + // now we add ourself to its lists.. + // (can't create strong refs in constructors) + assert(scene_ == scene); + assert(id_ == 0); + id_ = scene->next_node_id_++; + our_iterator = + scene->nodes_.insert(scene->nodes_.end(), Object::Ref(this)); + if (GameStream* os = scene->GetGameStream()) { + os->AddNode(this); + } +} + +Node::~Node() { + // Kill any incoming/outgoing attr connections. + for (auto& i : attribute_connections_incoming_) { + NodeAttributeConnection* a = i.second.get(); + assert(a && a->src_node.exists()); + + // Remove from src node's outgoing list. + a->src_node->attribute_connections_.erase(a->src_iterator); + } + + // Kill all refs on our side; this should kill the connections. + attribute_connections_incoming_.clear(); + for (auto& attribute_connection : attribute_connections_) { + NodeAttributeConnection* a = attribute_connection.get(); + assert(a && a->dst_node.exists()); + + // Remove from dst node's incoming list. + auto j = + a->dst_node->attribute_connections_incoming_.find(a->dst_attr_index); + assert(j != a->dst_node->attribute_connections_incoming_.end()); + a->dst_node->attribute_connections_incoming_.erase(j); + } + + // Kill all refs on our side; should kill the connections. + attribute_connections_.clear(); + + // NOTE: We no longer run death-actions or kill dependent-nodes here in our + // destructor; we allow the scene to do that to keep things cleaner. + + // Release our ref to ourself if we have one. + if (py_ref_) { + Py_DECREF(py_ref_); + } + + // If we were going to an output stream, inform them of our demise. + assert(scene()); + if (GameStream* output_stream = scene()->GetGameStream()) { + output_stream->RemoveNode(this); + } +} + +auto Node::GetResyncDataSize() -> int { return 0; } +auto Node::GetResyncData() -> std::vector { + return std::vector(); +} + +void Node::ApplyResyncData(const std::vector& data) {} + +void Node::Draw(FrameDef* frame_def) {} +void Node::OnCreate() {} + +auto Node::GetObjectDescription() const -> std::string { + return "name() : label()) + "\">"; +} + +auto Node::HasAttribute(const std::string& name) const -> bool { + return type()->HasAttribute(name); +} + +auto Node::GetAttribute(const std::string& name) -> NodeAttribute { + assert(type()); + return {this, type()->GetAttribute(name)}; +} + +auto Node::GetAttribute(int index) -> NodeAttribute { + assert(type()); + return {this, type()->GetAttribute(index)}; +} + +void Node::ConnectAttribute(NodeAttributeUnbound* src_attr, Node* dst_node, + NodeAttributeUnbound* dst_attr) { + // This is a no-op if the scene is shutting down. + if (scene() == nullptr || scene()->shutting_down()) { + return; + } + + assert(dst_node); + assert(src_attr && dst_attr); + assert(src_attr->node_type() == type()); + assert(dst_node->type() == dst_attr->node_type()); + assert(!scene()->in_step()); + + bool allow = false; + + // Currently limiting to certain types; + // Will wait and see on other types. + // A texture/etc attr might not behave well if updated with the same + // value every step.. hmmm. + { + switch (src_attr->type()) { + // Allow bools, ints, and floats to connect to each other + case NodeAttributeType::kBool: + case NodeAttributeType::kInt: + case NodeAttributeType::kFloat: + switch (dst_attr->type()) { + case NodeAttributeType::kBool: + case NodeAttributeType::kInt: + case NodeAttributeType::kFloat: + allow = true; + break; + default: + break; + } + break; + case NodeAttributeType::kString: + // Allow strings to connect to other strings (new in protocol 31). + if (dst_attr->type() == NodeAttributeType::kString) { + allow = true; + } + break; + case NodeAttributeType::kIntArray: + case NodeAttributeType::kFloatArray: + case NodeAttributeType::kTexture: + // Allow these types to connect to other attrs of the same type. + if (src_attr->type() == dst_attr->type()) allow = true; + break; + default: + break; + } + } + if (!allow) { + throw Exception("Attribute connections from " + src_attr->GetTypeName() + + " to " + dst_attr->GetTypeName() + + " attrs are not allowed."); + } + + // Ok lets do this. + + // Disconnect any existing connection to the dst attr. + dst_attr->DisconnectIncoming(dst_node); + + auto a(Object::New()); + + // Store refs to the connection with both the source and dst nodes. + a->src_iterator = + attribute_connections_.insert(attribute_connections_.end(), a); + dst_node->attribute_connections_incoming_[dst_attr->index()] = a; + a->src_node = this; + a->src_attr_index = src_attr->index(); + a->dst_node = dst_node; + a->dst_attr_index = dst_attr->index(); + a->Update(); +} + +void Node::UpdateConnections() { + for (auto& attribute_connection : attribute_connections_) { + // Connections should go away when either node dies; make sure that's + // working. + assert(attribute_connection->src_node.exists() + && attribute_connection->dst_node.exists()); + attribute_connection->Update(); + } +} + +void Node::AddNodeDeathAction(PyObject* call_obj) { + death_actions_.push_back(Object::New(call_obj)); +} + +void Node::AddDependentNode(Node* node) { + assert(node); + if (node->scene() != scene()) { + throw Exception("Nodes belong to different Scenes"); + } + + // While we're here lets prune any dead nodes from our list. + // (so if we add/destroy dependents repeatedly we don't build up a giant + // vector of dead ones) + if (!dependent_nodes_.empty()) { + std::vector > live_nodes; + for (auto& dependent_node : dependent_nodes_) { + if (dependent_node.exists()) live_nodes.push_back(dependent_node); + } + dependent_nodes_.swap(live_nodes); + } + dependent_nodes_.emplace_back(node); +} + +void Node::SetDelegate(PyObject* delegate_obj) { + if (delegate_obj != nullptr && delegate_obj != Py_None) { + delegate_.Steal(PyWeakref_NewRef(delegate_obj, nullptr)); + } else { + delegate_.Release(); + } +} + +auto Node::GetPyRef(bool new_ref) -> PyObject* { + assert(InGameThread()); + if (py_ref_ == nullptr) { + py_ref_ = PythonClassNode::Create(this); + } + if (new_ref) { + Py_INCREF(py_ref_); + } + return py_ref_; +} + +auto Node::GetDelegate() -> PyObject* { + PyObject* ref = delegate_.get(); + if (!ref) { + return nullptr; + } + return PyWeakref_GetObject(ref); +} + +void Node::DispatchNodeMessage(const char* buffer) { + assert(this); + assert(buffer); + if (scene_->shutting_down()) { + return; + } + + // If noone else has handled it, pass it to our low-level handler. + HandleMessage(buffer); +} + +void Node::DispatchOutOfBoundsMessage() { + PythonRef instance; + { + Python::ScopedCallLabel label("OutOfBoundsMessage instantiation"); + instance = g_python->obj(Python::ObjID::kOutOfBoundsMessageClass).Call(); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node OutOfBoundsMessage dispatch"); + } else { + Log("Error creating OutOfBoundsMessage"); + } +} + +void Node::DispatchPickUpMessage(Node* node) { + assert(node); + PythonRef args(Py_BuildValue("(O)", node->BorrowPyRef()), PythonRef::kSteal); + PythonRef instance; + { + Python::ScopedCallLabel label("PickUpMessage instantiation"); + instance = g_python->obj(Python::ObjID::kPickUpMessageClass).Call(args); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node PickUpMessage dispatch"); + } else { + Log("Error creating PickUpMessage"); + } +} + +void Node::DispatchDropMessage() { + PythonRef instance; + { + Python::ScopedCallLabel label("DropMessage instantiation"); + instance = g_python->obj(Python::ObjID::kDropMessageClass).Call(); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node DropMessage dispatch"); + } else { + Log("Error creating DropMessage"); + } +} + +void Node::DispatchPickedUpMessage(Node* by_node) { + assert(by_node); + PythonRef args(Py_BuildValue("(O)", by_node->BorrowPyRef()), + PythonRef::kSteal); + PythonRef instance; + { + Python::ScopedCallLabel label("PickedUpMessage instantiation"); + instance = g_python->obj(Python::ObjID::kPickedUpMessageClass).Call(args); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node PickedUpMessage dispatch"); + } else { + Log("Error creating PickedUpMessage"); + } +} + +void Node::DispatchDroppedMessage(Node* by_node) { + assert(by_node); + PythonRef args(Py_BuildValue("(O)", by_node->BorrowPyRef()), + PythonRef::kSteal); + PythonRef instance; + { + Python::ScopedCallLabel label("DroppedMessage instantiation"); + instance = g_python->obj(Python::ObjID::kDroppedMessageClass).Call(args); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node DroppedMessage dispatch"); + } else { + Log("Error creating DroppedMessage"); + } +} + +void Node::DispatchShouldShatterMessage() { + PythonRef instance; + { + Python::ScopedCallLabel label("ShouldShatterMessage instantiation"); + instance = g_python->obj(Python::ObjID::kShouldShatterMessageClass).Call(); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node ShouldShatterMessage dispatch"); + } else { + Log("Error creating ShouldShatterMessage"); + } +} + +void Node::DispatchImpactDamageMessage(float intensity) { + PythonRef args(Py_BuildValue("(f)", intensity), PythonRef::kSteal); + PythonRef instance; + { + Python::ScopedCallLabel label("ImpactDamageMessage instantiation"); + instance = + g_python->obj(Python::ObjID::kImpactDamageMessageClass).Call(args); + } + if (instance.exists()) { + DispatchUserMessage(instance.get(), "Node ImpactDamageMessage dispatch"); + } else { + Log("Error creating ImpactDamageMessage"); + } +} + +void Node::DispatchUserMessage(PyObject* obj, const char* label) { + assert(InGameThread()); + if (scene_->shutting_down()) { + return; + } + + ScopedSetContext cp(context()); + PyObject* delegate = GetDelegate(); + if (delegate && delegate != Py_None) { + try { + PyObject* handlemessage_obj = + PyObject_GetAttrString(delegate, "handlemessage"); + if (!handlemessage_obj) { + PyErr_Clear(); + throw Exception("No 'handlemessage' found on delegate object for '" + + type()->name() + "' node (" + + Python::ObjToString(delegate) + ")"); + } + PythonRef c(handlemessage_obj, PythonRef::kSteal); + { + Python::ScopedCallLabel lscope(label); + c.Call(PythonRef(Py_BuildValue("(O)", obj), PythonRef::kSteal)); + } + } catch (const std::exception& e) { + Log(std::string("Error in handlemessage() with message ") + + PythonRef(obj, PythonRef::kAcquire).Str() + ": '" + e.what() + "'"); + } + } +} + +void Node::HandleMessage(const char* data_in) {} + +void Node::UpdatePartBirthTimes() { + for (auto&& i : parts_) { + i->UpdateBirthTime(); + } +} + +void Node::CheckBodies() { + for (auto&& i : parts_) { + i->CheckBodies(); + } +} + +auto NodeType::GetAttributeNames() const -> std::vector { + std::vector names; + for (auto&& i : attributes_by_name_) { + names.push_back(i.second->name()); + } + return names; +} + +void Node::ListAttributes(std::list* attrs) { + attrs->clear(); + + // New attrs. + std::vector type_attrs = type()->GetAttributeNames(); + for (auto&& i : type_attrs) { + attrs->push_back(i); + } +} + +void Node::GetRigidBodyPickupLocations(int id, float* pos_obj, float* pos_char, + float* hand_offset_1, + float* hand_offset_2) { + pos_obj[0] = pos_obj[1] = pos_obj[2] = 0; + pos_char[0] = pos_char[1] = pos_char[2] = 0; + hand_offset_1[0] = hand_offset_1[1] = hand_offset_1[2] = 0; + hand_offset_2[0] = hand_offset_2[1] = hand_offset_2[2] = 0; +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/node.h b/src/ballistica/scene/node/node.h new file mode 100644 index 00000000..a3586d20 --- /dev/null +++ b/src/ballistica/scene/node/node.h @@ -0,0 +1,226 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_NODE_H_ +#define BALLISTICA_SCENE_NODE_NODE_H_ + +#include +#include +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/core/context.h" +#include "ballistica/core/object.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +// Define a static creation call for this node type +#define BA_NODE_CREATE_CALL(FUNC) \ + static auto FUNC(Scene* sg)->Node* { \ + return Object::NewDeferred(sg); \ + } + +typedef std::list > NodeList; + +// Base node class. +class Node : public Object { + public: + Node(Scene* scene, NodeType* node_type); + ~Node() override; + auto id() const -> int64_t { + return id_; + } // Return the node's id in its scene. + virtual void Step() {} // Called for each step of the sim. + virtual void OnScreenSizeChange() {} // Called when screen size changes. + virtual void OnLanguageChange() {} // Called when the language changes. + virtual void OnGraphicsQualityChanged(GraphicsQuality q) {} + + // The node can rule out collisions between particular bodies using this. + virtual auto PreFilterCollision(RigidBody* b1, RigidBody* r2) -> bool { + return true; + } + + // Pull a node type out of a buffer. + static auto extract_node_message_type(const char** b) -> NodeMessageType { + auto t = static_cast(**b); + (*b) += 1; + return t; + } + + void ConnectAttribute(NodeAttributeUnbound* src_attr, Node* dst_node, + NodeAttributeUnbound* dst_attr); + + // Return an attribute by name. + auto GetAttribute(const std::string& name) -> NodeAttribute; + + // Return an attribute by index. + auto GetAttribute(int index) -> NodeAttribute; + + void SetDelegate(PyObject* delegate_obj); + + auto NewPyRef() -> PyObject* { return GetPyRef(true); } + auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } + + // Return the delegate, or nullptr if it doesn't have one + // (or if the delegate has since died). + auto GetDelegate() -> PyObject*; + + void AddNodeDeathAction(PyObject* call_obj); + + // Add a node to auto-kill when this one dies. + void AddDependentNode(Node* node); + + // Update birth times for all the node's parts. + // This should be done when teleporting or otherwise spawning at + // a new location. + void UpdatePartBirthTimes(); + + // Retrieve an existing part from a node. + auto GetPart(unsigned int id) -> Part* { + assert(id < parts_.size()); + return parts_[id]; + } + + // Used by RigidBodies when adding themselves to the part. + auto AddPart(Part* part_in) -> int { + parts_.push_back(part_in); + return static_cast(parts_.size() - 1); + } + + // Used to send messages to a node + void DispatchNodeMessage(const char* buffer); + + // Used to send custom user messages to a node + // returns true if handled. + void DispatchUserMessage(PyObject* obj, const char* label); + void DispatchOutOfBoundsMessage(); + void DispatchPickedUpMessage(Node* n); + void DispatchDroppedMessage(Node* n); + void DispatchPickUpMessage(Node* n); + void DispatchDropMessage(); + void DispatchShouldShatterMessage(); + void DispatchImpactDamageMessage(float intensity); + + // Utility function to get a rigid body. + virtual auto GetRigidBody(int id) -> RigidBody* { return nullptr; } + + // Given a rigid body, return the relative position where it should be picked + // up from. + virtual void GetRigidBodyPickupLocations(int id, float* posObj, + float* pos_char, + float* hand_offset_1, + float* hand_offset_2); + + // Called for each Node when it should render itself. + virtual void Draw(FrameDef* frame_def); + + // Called for each node once construction is completed + // this can be a good time to create things from the initial attr set, etc + virtual void OnCreate(); + + auto scene() const -> Scene* { + assert(scene_); + return scene_; + } + + // Used to re-sync client versions of a node from the host version. + virtual auto GetResyncDataSize() -> int; + virtual auto GetResyncData() -> std::vector; + virtual void ApplyResyncData(const std::vector& data); + auto context() const -> const Context& { return context_; } + + // Node labels are purely for local debugging - they aren't unique or sent + // across the network or anything. + void set_label(const std::string& label) { label_ = label; } + auto label() const -> const std::string& { return label_; } + + void ListAttributes(std::list* attrs); + auto type() const -> NodeType* { + assert(node_type_); + return node_type_; + } + auto HasAttribute(const std::string& name) const -> bool; + auto has_py_ref() -> bool { return (py_ref_ != nullptr); } + void UpdateConnections(); + auto iterator() -> NodeList::iterator { return our_iterator; } + + void CheckBodies(); + +#if BA_DEBUG_BUILD +#define BA_DEBUG_CHECK_BODIES() CheckBodies() +#else +#define BA_DEBUG_CHECK_BODIES() ((void)0) +#endif + + auto GetObjectDescription() const -> std::string override; + + auto parts() const -> const std::vector& { return parts_; } + auto death_actions() const + -> const std::vector >& { + return death_actions_; + } + auto dependent_nodes() const -> const std::vector >& { + return dependent_nodes_; + } + auto attribute_connections() const + -> const std::list >& { + return attribute_connections_; + } + auto attribute_connections_incoming() const + -> const std::map >& { + return attribute_connections_incoming_; + } + + auto stream_id() const -> int64_t { return stream_id_; } + void set_stream_id(int64_t val) { + assert(stream_id_ == -1); + stream_id_ = val; + } + void clear_stream_id() { + assert(stream_id_ != -1); + stream_id_ = -1; + } + + // Return a reference to a python wrapper for this node, + // creating one if need be. + auto GetPyRef(bool new_ref = true) -> PyObject*; + + void AddToScene(Scene* scene); + + // Called for each message received by an Node. + virtual void HandleMessage(const char* buffer); + + private: + int64_t stream_id_{-1}; + NodeType* node_type_ = nullptr; + + PyObject* py_ref_ = nullptr; + + // FIXME - We can get by with *just* a pointer to our scene + // if we add a way to pull context from a scene. + Context context_; + Scene* scene_{}; + std::string label_; + std::vector > dependent_nodes_; + std::vector parts_; + int64_t id_{}; + NodeList::iterator our_iterator; + + // Put this stuff at the bottom so it gets killed first + PythonRef delegate_; + std::vector > death_actions_; + + // Outgoing attr connections in order created. + std::list > attribute_connections_; + + // Incoming attr connections by attr index. + std::map > + attribute_connections_incoming_; + + friend class NodeAttributeUnbound; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_NODE_H_ diff --git a/src/ballistica/scene/node/node_attribute.cc b/src/ballistica/scene/node/node_attribute.cc new file mode 100644 index 00000000..a8bdd109 --- /dev/null +++ b/src/ballistica/scene/node/node_attribute.cc @@ -0,0 +1,270 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/node_attribute.h" + +#include + +#include "ballistica/scene/node/node.h" +#include "ballistica/scene/node/node_attribute_connection.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +auto NodeAttributeUnbound::GetNodeAttributeTypeName(NodeAttributeType t) + -> std::string { + switch (t) { + case NodeAttributeType::kFloat: + return "float"; + case NodeAttributeType::kFloatArray: + return "float-array"; + case NodeAttributeType::kInt: + return "int"; + case NodeAttributeType::kIntArray: + return "int-array"; + case NodeAttributeType::kBool: + return "bool"; + case NodeAttributeType::kString: + return "string"; + case NodeAttributeType::kNode: + return "node"; + case NodeAttributeType::kNodeArray: + return "node-array"; + case NodeAttributeType::kPlayer: + return "player"; + case NodeAttributeType::kMaterialArray: + return "material-array"; + case NodeAttributeType::kTexture: + return "texture"; + case NodeAttributeType::kTextureArray: + return "texture-array"; + case NodeAttributeType::kSound: + return "sound"; + case NodeAttributeType::kSoundArray: + return "sound-array"; + case NodeAttributeType::kModel: + return "model"; + case NodeAttributeType::kModelArray: + return "model-array"; + case NodeAttributeType::kCollideModel: + return "collide-model"; + case NodeAttributeType::kCollideModelArray: + return "collide-model-array"; + default: + Log("Error: Unknown attr type name: " + + std::to_string(static_cast(t))); + return "unknown"; + } +} + +NodeAttributeUnbound::NodeAttributeUnbound(NodeType* node_type, + NodeAttributeType type, + std::string name, uint32_t flags) + : node_type_(node_type), + type_(type), + name_(std::move(name)), + flags_(flags) { + assert(node_type); + node_type->attributes_by_name_[name_] = this; + index_ = static_cast(node_type->attributes_by_index_.size()); + node_type->attributes_by_index_.push_back(this); +} + +void NodeAttributeUnbound::NotReadableError(Node* node) { + throw Exception("Attribute '" + name() + "' on " + node->type()->name() + + " node is not readable"); +} + +void NodeAttributeUnbound::NotWritableError(Node* node) { + throw Exception("Attribute '" + name() + "' on " + node->type()->name() + + " node is not writable"); +} + +void NodeAttributeUnbound::DisconnectIncoming(Node* node) { + assert(node); + auto i = node->attribute_connections_incoming().find(index()); + if (i != node->attribute_connections_incoming().end()) { + NodeAttributeConnection* a = i->second.get(); + +#if BA_DEBUG_BUILD + Object::WeakRef test_ref(a); +#endif + + assert(a && a->src_node.exists()); + + // Remove from src node's outgoing list. + a->src_node->attribute_connections_.erase(a->src_iterator); + + // Remove from our incoming list; this should kill the connection. + node->attribute_connections_incoming_.erase(i); + +#if BA_DEBUG_BUILD + if (test_ref.exists()) { + Log("Error: Attr connection still exists after ref releases!"); + } +#endif + } +} + +auto NodeAttributeUnbound::GetAsFloat(Node* node) -> float { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a float."); +} +void NodeAttributeUnbound::Set(Node* node, float value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a float."); +} +auto NodeAttributeUnbound::GetAsInt(Node* node) -> int64_t { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as an int."); +} +void NodeAttributeUnbound::Set(Node* node, int64_t value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as an int."); +} +auto NodeAttributeUnbound::GetAsBool(Node* node) -> bool { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a bool."); +} +void NodeAttributeUnbound::Set(Node* node, bool value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a bool."); +} +auto NodeAttributeUnbound::GetAsString(Node* node) -> std::string { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a string."); +} +void NodeAttributeUnbound::Set(Node* node, const std::string& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a string."); +} + +auto NodeAttributeUnbound::GetAsFloats(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a float array."); +} +void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a float array."); +} +auto NodeAttributeUnbound::GetAsInts(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as an int array."); +} +void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as an int array."); +} + +auto NodeAttributeUnbound::GetAsNode(Node* node) -> Node* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a node."); +} +void NodeAttributeUnbound::Set(Node* node, Node* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a node."); +} + +auto NodeAttributeUnbound::GetAsNodes(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a node array."); +} +void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a node array."); +} + +auto NodeAttributeUnbound::GetAsPlayer(Node* node) -> Player* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a player."); +} +void NodeAttributeUnbound::Set(Node* node, Player* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a player."); +} + +auto NodeAttributeUnbound::GetAsMaterials(Node* node) + -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a material array."); +} +void NodeAttributeUnbound::Set(Node* node, + const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a material array."); +} + +auto NodeAttributeUnbound::GetAsTexture(Node* node) -> Texture* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a texture."); +} +void NodeAttributeUnbound::Set(Node* node, Texture* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a texture."); +} + +auto NodeAttributeUnbound::GetAsTextures(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a texture array."); +} +void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a texture array."); +} + +auto NodeAttributeUnbound::GetAsSound(Node* node) -> Sound* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound."); +} +void NodeAttributeUnbound::Set(Node* node, Sound* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound."); +} + +auto NodeAttributeUnbound::GetAsSounds(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound array."); +} +void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound array."); +} + +auto NodeAttributeUnbound::GetAsModel(Node* node) -> Model* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a model."); +} +void NodeAttributeUnbound::Set(Node* node, Model* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a model."); +} + +auto NodeAttributeUnbound::GetAsModels(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a model array."); +} +void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a model array."); +} + +auto NodeAttributeUnbound::GetAsCollideModel(Node* node) -> CollideModel* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collide-model."); +} +void NodeAttributeUnbound::Set(Node* node, CollideModel* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collide-model."); +} + +auto NodeAttributeUnbound::GetAsCollideModels(Node* node) + -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collide-model array."); +} +void NodeAttributeUnbound::Set(Node* node, + const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collide-model array."); +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/node_attribute.h b/src/ballistica/scene/node/node_attribute.h new file mode 100644 index 00000000..92f73064 --- /dev/null +++ b/src/ballistica/scene/node/node_attribute.h @@ -0,0 +1,1016 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_H_ +#define BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_H_ + +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCUnusedMacroInspection" + +// Unbound node attribute; these are statically stored in a node type +// and contain logic to get/set a particular attribute on a node +// in various ways. +class NodeAttributeUnbound { + public: + static auto GetNodeAttributeTypeName(NodeAttributeType) -> std::string; + + NodeAttributeUnbound(NodeType* node_type, NodeAttributeType type, + std::string name, uint32_t flags); + + // Attrs should override the calls they support; by default + // these all raise exceptions. + // Generally attrs are get/set as their native type, + // but in cases of attr connections a 'get' corresponding + // to the native type of the dst attr is made on the src attr + // (so if connecting float attr foo to int attr bar, + // the update will essentially be: bar.set(foo.GetAsInt()) ) + virtual auto GetAsFloat(Node* node) -> float; + virtual void Set(Node* node, float value); + + virtual auto GetAsInt(Node* node) -> int64_t; + virtual void Set(Node* node, int64_t value); + + virtual auto GetAsBool(Node* node) -> bool; + virtual void Set(Node* node, bool value); + + virtual auto GetAsString(Node* node) -> std::string; + virtual void Set(Node* node, const std::string& value); + + virtual auto GetAsFloats(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& value); + + virtual auto GetAsInts(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& value); + + virtual auto GetAsNode(Node* node) -> Node*; + virtual void Set(Node* node, Node* value); + + virtual auto GetAsNodes(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); + + virtual auto GetAsPlayer(Node* node) -> Player*; + virtual void Set(Node* node, Player* value); + + virtual auto GetAsMaterials(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& value); + + virtual auto GetAsTexture(Node* node) -> Texture*; + virtual void Set(Node* node, Texture* value); + + virtual auto GetAsTextures(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); + + virtual auto GetAsSound(Node* node) -> Sound*; + virtual void Set(Node* node, Sound* value); + + virtual auto GetAsSounds(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); + + virtual auto GetAsModel(Node* node) -> Model*; + virtual void Set(Node* node, Model* value); + + virtual auto GetAsModels(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); + + virtual auto GetAsCollideModel(Node* node) -> CollideModel*; + virtual void Set(Node* node, CollideModel* value); + + virtual auto GetAsCollideModels(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); + + auto is_read_only() const -> bool { + return static_cast(flags_ & kNodeAttributeFlagReadOnly); + } + auto type() const -> NodeAttributeType { return type_; } + auto GetTypeName() const -> std::string { + return GetNodeAttributeTypeName(type_); + } + auto name() const -> const std::string& { return name_; } + auto node_type() const -> NodeType* { return node_type_; } + auto index() const -> int { return index_; } + void DisconnectIncoming(Node* node); + + protected: + void NotReadableError(Node* node); + void NotWritableError(Node* node); + + private: + NodeType* node_type_; + NodeAttributeType type_; + std::string name_; + uint32_t flags_; + int index_; +}; + +// Simple node-attribute pair; used as a convenience measure. +// Note that this simply stores pointers; it does not check to +// ensure the node is still valid or anything like that. +class NodeAttribute { + public: + void assign(Node* node_in, NodeAttributeUnbound* attr_in) { + node = node_in; + attr = attr_in; + } + NodeAttribute() = default; + NodeAttribute(Node* node_in, NodeAttributeUnbound* attr_in) + : node(node_in), attr(attr_in) {} + Node* node = nullptr; + NodeAttributeUnbound* attr = nullptr; + auto type() const -> NodeAttributeType { return attr->type(); } + auto GetTypeName() const -> std::string { return attr->GetTypeName(); } + auto name() const -> const std::string& { return attr->name(); } + auto node_type() const -> NodeType* { return attr->node_type(); } + auto index() const -> int { return attr->index(); } + void DisconnectIncoming() { attr->DisconnectIncoming(node); } + auto is_read_only() const -> bool { return attr->is_read_only(); } + auto GetAsFloat() const -> float { return attr->GetAsFloat(node); } + void Set(float value) const { attr->Set(node, value); } + auto GetAsInt() const -> int64_t { return attr->GetAsInt(node); } + void Set(int64_t value) const { attr->Set(node, value); } + auto GetAsBool() const -> bool { return attr->GetAsBool(node); } + void Set(bool value) const { attr->Set(node, value); } + auto GetAsString() const -> std::string { return attr->GetAsString(node); } + void Set(const std::string& value) const { attr->Set(node, value); } + auto GetAsFloats() const -> std::vector { + return attr->GetAsFloats(node); + } + void Set(const std::vector& value) const { attr->Set(node, value); } + auto GetAsInts() const -> std::vector { + return attr->GetAsInts(node); + } + void Set(const std::vector& value) const { attr->Set(node, value); } + auto GetAsNode() const -> Node* { return attr->GetAsNode(node); } + void Set(Node* value) const { attr->Set(node, value); } + auto GetAsNodes() const -> std::vector { + return attr->GetAsNodes(node); + } + void Set(const std::vector& value) const { attr->Set(node, value); } + auto GetAsPlayer() const -> Player* { return attr->GetAsPlayer(node); } + void Set(Player* value) const { attr->Set(node, value); } + auto GetAsMaterials() const -> std::vector { + return attr->GetAsMaterials(node); + } + void Set(const std::vector& value) const { + attr->Set(node, value); + } + auto GetAsTexture() const -> Texture* { return attr->GetAsTexture(node); } + void Set(Texture* value) const { attr->Set(node, value); } + auto GetAsTextures() const -> std::vector { + return attr->GetAsTextures(node); + } + void Set(const std::vector& values) const { + attr->Set(node, values); + } + auto GetAsSound() const -> Sound* { return attr->GetAsSound(node); } + void Set(Sound* value) const { attr->Set(node, value); } + auto GetAsSounds() const -> std::vector { + return attr->GetAsSounds(node); + } + void Set(const std::vector& values) const { attr->Set(node, values); } + auto GetAsModel() const -> Model* { return attr->GetAsModel(node); } + void Set(Model* value) const { attr->Set(node, value); } + auto GetAsModels() const -> std::vector { + return attr->GetAsModels(node); + } + void Set(const std::vector& values) const { attr->Set(node, values); } + auto GetAsCollideModel() const -> CollideModel* { + return attr->GetAsCollideModel(node); + } + void Set(CollideModel* value) const { attr->Set(node, value); } + auto GetAsCollideModels() const -> std::vector { + return attr->GetAsCollideModels(node); + } + void Set(const std::vector& values) const { + attr->Set(node, values); + } +}; + +// Single float attr; subclasses just need to override float get/set +// and this will provide the other numeric get/sets based on that. +class NodeAttributeUnboundFloat : public NodeAttributeUnbound { + public: + NodeAttributeUnboundFloat(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kFloat, name, + flags) {} + + // Override these: + auto GetAsFloat(Node* node) -> float override { + NotReadableError(node); + return 0.0f; + } + void Set(Node* node, float val) override { NotWritableError(node); } + + // These are handled automatically: + auto GetAsInt(Node* node) -> int64_t final { + return static_cast(GetAsFloat(node)); + } + auto GetAsBool(Node* node) -> bool final { + return static_cast(GetAsFloat(node)); + } + void Set(Node* node, int64_t val) final { + Set(node, static_cast(val)); + } + void Set(Node* node, bool val) final { Set(node, static_cast(val)); } +}; + +// Float array attr. +class NodeAttributeUnboundFloatArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundFloatArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kFloatArray, name, + flags) {} + + // Override these: + auto GetAsFloats(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& vals) override { + NotWritableError(node); + } +}; + +// Single int attr; subclasses just need to override int get/set +// and this will provide the other numeric get/sets based on that. +class NodeAttributeUnboundInt : public NodeAttributeUnbound { + public: + NodeAttributeUnboundInt(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kInt, name, flags) {} + + // Override these: + auto GetAsInt(Node* node) -> int64_t override { + NotReadableError(node); + return 0; + } + void Set(Node* node, int64_t val) override { NotWritableError(node); } + + // These are handled automatically: + auto GetAsFloat(Node* node) -> float final { return GetAsInt(node); } + auto GetAsBool(Node* node) -> bool final { + return static_cast(GetAsInt(node)); + } + void Set(Node* node, float val) final { + Set(node, static_cast(val)); + } + void Set(Node* node, bool val) final { Set(node, static_cast(val)); } +}; + +// Int array attr. +class NodeAttributeUnboundIntArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundIntArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kIntArray, name, + flags) {} + + // Override these: + auto GetAsInts(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& vals) override = 0; +}; + +// Single bool attr; subclasses just need to override bool get/set +// and this will provide the other numeric get/sets based on that. +class NodeAttributeUnboundBool : public NodeAttributeUnbound { + public: + NodeAttributeUnboundBool(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kBool, name, flags) { + } + + // Override these: + auto GetAsBool(Node* node) -> bool override { + NotReadableError(node); + return false; + }; + void Set(Node* node, bool val) override { NotWritableError(node); } + + // These are handled automatically: + auto GetAsFloat(Node* node) -> float final { + return GetAsBool(node) ? 1.0f : 0.0f; + } + auto GetAsInt(Node* node) -> int64_t final { return GetAsBool(node) ? 1 : 0; } + void Set(Node* node, float val) final { Set(node, val != 0.0f); } + void Set(Node* node, int64_t val) final { Set(node, val != 0); } +}; + +// String attr. +class NodeAttributeUnboundString : public NodeAttributeUnbound { + public: + NodeAttributeUnboundString(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kString, name, + flags) {} + + // Override these: + auto GetAsString(Node* node) -> std::string override { + NotReadableError(node); + return ""; + }; + void Set(Node* node, const std::string& val) override { + NotWritableError(node); + } +}; + +// Node attr. +class NodeAttributeUnboundNode : public NodeAttributeUnbound { + public: + NodeAttributeUnboundNode(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kNode, name, flags) { + } + + // Override these: + auto GetAsNode(Node* node) -> Node* override { + NotReadableError(node); + return nullptr; + }; + void Set(Node* node, Node* val) override { NotWritableError(node); } +}; + +// Node array attr. +class NodeAttributeUnboundNodeArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundNodeArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kNodeArray, name, + flags) {} + // Override these: + auto GetAsNodes(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + }; + void Set(Node* node, const std::vector& vals) override { + NotWritableError(node); + } +}; + +// Player attr. +class NodeAttributeUnboundPlayer : public NodeAttributeUnbound { + public: + NodeAttributeUnboundPlayer(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kPlayer, name, + flags) {} + // override these: + auto GetAsPlayer(Node* node) -> Player* override { + NotReadableError(node); + return nullptr; + } + void Set(Node* node, Player* val) override { NotWritableError(node); } +}; + +// Material array attr. +class NodeAttributeUnboundMaterialArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundMaterialArray(NodeType* node_type, + const std::string& name, uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kMaterialArray, name, + flags) {} + // override these: + auto GetAsMaterials(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& materials) override { + NotWritableError(node); + } +}; + +// Texture attr. +class NodeAttributeUnboundTexture : public NodeAttributeUnbound { + public: + NodeAttributeUnboundTexture(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kTexture, name, + flags) {} + // Override these: + auto GetAsTexture(Node* node) -> Texture* override { + NotReadableError(node); + return nullptr; + } + void Set(Node* node, Texture* val) override { NotWritableError(node); } +}; + +// Texture array attr. +class NodeAttributeUnboundTextureArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundTextureArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kTextureArray, name, + flags) {} + // Override these: + auto GetAsTextures(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& vals) override { + NotWritableError(node); + } +}; + +// Sound attr. +class NodeAttributeUnboundSound : public NodeAttributeUnbound { + public: + NodeAttributeUnboundSound(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kSound, name, + flags) {} + // override these: + auto GetAsSound(Node* node) -> Sound* override { + NotReadableError(node); + return nullptr; + } + void Set(Node* node, Sound* val) override { NotWritableError(node); } +}; + +// Sound array attr. +class NodeAttributeUnboundSoundArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundSoundArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kSoundArray, name, + flags) {} + // Override these: + auto GetAsSounds(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& vals) override { + NotWritableError(node); + } +}; + +// Model attr. +class NodeAttributeUnboundModel : public NodeAttributeUnbound { + public: + NodeAttributeUnboundModel(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kModel, name, + flags) {} + // Override these: + auto GetAsModel(Node* node) -> Model* override { + NotReadableError(node); + return nullptr; + } + void Set(Node* node, Model* val) override { NotWritableError(node); } +}; + +// Model array attr. +class NodeAttributeUnboundModelArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundModelArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kModelArray, name, + flags) {} + // Override these: + auto GetAsModels(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& vals) override { + NotWritableError(node); + } +}; + +// Collide_model attr. +class NodeAttributeUnboundCollideModel : public NodeAttributeUnbound { + public: + NodeAttributeUnboundCollideModel(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kCollideModel, name, + flags) {} + // Override these: + auto GetAsCollideModel(Node* node) -> CollideModel* override { + NotReadableError(node); + return nullptr; + } + void Set(Node* node, CollideModel* val) override { NotWritableError(node); } +}; + +// Collide_model array attr. +class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { + public: + NodeAttributeUnboundCollideModelArray(NodeType* node_type, + const std::string& name, uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kCollideModelArray, + name, flags) {} + // Override these: + auto GetAsCollideModels(Node* node) -> std::vector override { + NotReadableError(node); + return std::vector(); + } + void Set(Node* node, const std::vector& vals) override { + NotWritableError(node); + } +}; + +// Defines a float attr subclass that interfaces with specific getter/setter +// calls. +#define BA_FLOAT_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundFloat { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundFloat(node_type, #NAME, 0) {} \ + auto GetAsFloat(Node* node) -> float override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, float val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a float attr subclass that interfaces with specific getter/setter +// calls. +#define BA_FLOAT_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundFloat { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundFloat(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsFloat(Node* node) -> float override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a float-array attr subclass that interfaces with specific +// getter/setter calls. +#define BA_FLOAT_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundFloatArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundFloatArray(node_type, #NAME, 0) {} \ + auto GetAsFloats(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a float-array attr subclass that interfaces with specific +// getter/setter calls. +#define BA_FLOAT_ARRAY_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundFloatArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundFloatArray(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsFloats(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines an int attr subclass that interfaces with specific getter/setter +// calls. +#define BA_INT_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundInt { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundInt(node_type, #NAME, 0) {} \ + auto GetAsInt(Node* node) -> int64_t override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, int64_t val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(static_cast_check_fit(val)); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines an int attr subclass that interfaces with specific getter/setter +// calls. +#define BA_INT_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundInt { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundInt(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsInt(Node* node) -> int64_t override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines an int attr subclass that interfaces with specific getter/setter +// calls. +#define BA_INT64_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundInt { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundInt(node_type, #NAME, 0) {} \ + auto GetAsInt(Node* node) -> int64_t override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, int64_t val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines an int attr subclass that interfaces with specific getter/setter +// calls. +#define BA_INT64_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundInt { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundInt(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsInt(Node* node) -> int64_t override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines an int-array attr subclass that interfaces with specific +// getter/setter calls. +#define BA_INT64_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundIntArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundIntArray(node_type, #NAME, 0) {} \ + auto GetAsInts(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a bool attr subclass that interfaces with specific getter/setter +// calls. +#define BA_BOOL_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundBool { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundBool(node_type, #NAME, 0) {} \ + auto GetAsBool(Node* node) -> bool override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, bool val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a bool attr subclass that interfaces with specific getter/setter +// calls. +#define BA_BOOL_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundBool { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundBool(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsBool(Node* node) -> bool override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a string attr subclass that interfaces with specific getter/setter +// calls. +#define BA_STRING_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundString { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundString(node_type, #NAME, 0) {} \ + auto GetAsString(Node* node) -> std::string override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::string& val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a string attr subclass that interfaces with specific getter/setter +// calls. +#define BA_STRING_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundString { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundString(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsString(Node* node) -> std::string override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a node attr subclass that interfaces with specific getter/setter +// calls. +#define BA_NODE_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundNode { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundNode(node_type, #NAME, 0) {} \ + auto GetAsNode(Node* node) -> Node* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, Node* val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a node-array attr subclass that interfaces with specific +// getter/setter calls. +#define BA_NODE_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundNodeArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundNodeArray(node_type, #NAME, 0) {} \ + auto GetAsNodes(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a player attr subclass that interfaces with specific getter/setter +// calls. +#define BA_PLAYER_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundPlayer { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundPlayer(node_type, #NAME, 0) {} \ + auto GetAsPlayer(Node* node) -> Player* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, Player* val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a material-array attr subclass that interfaces with specific +// getter/setter calls. +#define BA_MATERIAL_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundMaterialArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundMaterialArray(node_type, #NAME, 0) {} \ + auto GetAsMaterials(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a texture attr subclass that interfaces with specific getter/setter +// calls. +#define BA_TEXTURE_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundTexture { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundTexture(node_type, #NAME, 0) {} \ + auto GetAsTexture(Node* node) -> Texture* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, Texture* val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a texture attr subclass that interfaces with specific getter/setter +// calls. +#define BA_TEXTURE_ATTR_READONLY(NAME, GETTER) \ + class Attr_##NAME : public NodeAttributeUnboundTexture { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundTexture(node_type, #NAME, \ + kNodeAttributeFlagReadOnly) {} \ + auto GetAsTexture(Node* node) -> Texture* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a texture attr subclass that interfaces with specific getter/setter +// calls. +#define BA_TEXTURE_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundTextureArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundTextureArray(node_type, #NAME, 0) {} \ + auto GetAsTextures(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a sound attr subclass that interfaces with specific getter/setter +// calls. +#define BA_SOUND_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundSound { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundSound(node_type, #NAME, 0) {} \ + auto GetAsSound(Node* node) -> Sound* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, Sound* val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a sound attr subclass that interfaces with specific getter/setter +// calls. +#define BA_SOUND_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundSoundArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundSoundArray(node_type, #NAME, 0) {} \ + auto GetAsSounds(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a model attr subclass that interfaces with specific getter/setter +// calls. +#define BA_MODEL_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundModel { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundModel(node_type, #NAME, 0) {} \ + auto GetAsModel(Node* node) -> Model* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, Model* val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a model attr subclass that interfaces with specific getter/setter +// calls. +#define BA_MODEL_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundModelArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundModelArray(node_type, #NAME, 0) {} \ + auto GetAsModels(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a collide_model attr subclass that interfaces with specific +// getter/setter calls. +#define BA_COLLIDE_MODEL_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundCollideModel { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundCollideModel(node_type, #NAME, 0) {} \ + auto GetAsCollideModel(Node* node) -> CollideModel* override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, CollideModel* val) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(val); \ + } \ + }; \ + Attr_##NAME NAME; + +// Defines a collide_model attr subclass that interfaces with specific +// getter/setter calls. +#define BA_COLLIDE_MODEL_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundCollideModelArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundCollideModelArray(node_type, #NAME, 0) {} \ + auto GetAsCollideModels(Node* node) \ + -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ + Attr_##NAME NAME; + +#pragma clang diagnostic pop + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_H_ diff --git a/src/ballistica/scene/node/node_attribute_connection.cc b/src/ballistica/scene/node/node_attribute_connection.cc new file mode 100644 index 00000000..c926e1fc --- /dev/null +++ b/src/ballistica/scene/node/node_attribute_connection.cc @@ -0,0 +1,108 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/node_attribute_connection.h" + +#include + +#include "ballistica/scene/node/node.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +void NodeAttributeConnection::Update() { + assert(src_node.exists() && dst_node.exists()); + auto* src_node_p{src_node.get()}; + + // We no longer update after errors now. + // (the constant stream of exceptions slows things down too much) + if (have_error) { + return; + } + + try { + // Pull data from the src to match the dst type. + NodeAttributeUnbound* src_attr = + src_node->type()->GetAttribute(src_attr_index); + assert(src_attr); + NodeAttributeUnbound* dst_attr = + dst_node->type()->GetAttribute(dst_attr_index); + assert(dst_attr); + switch (dst_attr->type()) { + case NodeAttributeType::kFloat: + dst_attr->Set(dst_node.get(), src_attr->GetAsFloat(src_node_p)); + break; + case NodeAttributeType::kInt: + dst_attr->Set(dst_node.get(), src_attr->GetAsInt(src_node_p)); + break; + case NodeAttributeType::kBool: + dst_attr->Set(dst_node.get(), src_attr->GetAsBool(src_node_p)); + break; + case NodeAttributeType::kString: + dst_attr->Set(dst_node.get(), src_attr->GetAsString(src_node_p)); + break; + case NodeAttributeType::kIntArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsInts(src_node_p)); + break; + case NodeAttributeType::kFloatArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsFloats(src_node_p)); + break; + case NodeAttributeType::kNode: + dst_attr->Set(dst_node.get(), src_attr->GetAsNode(src_node_p)); + break; + case NodeAttributeType::kNodeArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsNodes(src_node_p)); + break; + case NodeAttributeType::kPlayer: + dst_attr->Set(dst_node.get(), src_attr->GetAsPlayer(src_node_p)); + break; + case NodeAttributeType::kMaterialArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsMaterials(src_node_p)); + break; + case NodeAttributeType::kTexture: + dst_attr->Set(dst_node.get(), src_attr->GetAsTexture(src_node_p)); + break; + case NodeAttributeType::kTextureArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsTextures(src_node_p)); + break; + case NodeAttributeType::kSound: + dst_attr->Set(dst_node.get(), src_attr->GetAsSound(src_node_p)); + break; + case NodeAttributeType::kSoundArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsSounds(src_node_p)); + break; + case NodeAttributeType::kModel: + dst_attr->Set(dst_node.get(), src_attr->GetAsModel(src_node_p)); + break; + case NodeAttributeType::kModelArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsModels(src_node_p)); + break; + case NodeAttributeType::kCollideModel: + dst_attr->Set(dst_node.get(), src_attr->GetAsCollideModel(src_node_p)); + break; + case NodeAttributeType::kCollideModelArray: + dst_attr->Set(dst_node.get(), src_attr->GetAsCollideModels(src_node_p)); + break; + default: + throw Exception("FIXME: unimplemented for attr type: '" + + dst_attr->GetTypeName() + "'"); + } + } catch (const std::exception& e) { + // Print errors only once per connection to avoid overwhelming the logs. + // (though we now stop updating after an error so this is redundant). + if (!have_error) { + have_error = true; + NodeAttributeUnbound* src_attr = + src_node->type()->GetAttribute(src_attr_index); + NodeAttributeUnbound* dst_attr = + dst_node->type()->GetAttribute(dst_attr_index); + Log("ERROR: attribute connection update: " + std::string(e.what()) + + "; srcAttr='" + src_attr->name() + "', src_node='" + + src_node->type()->name() + "', srcNodeName='" + src_node->label() + + "', dstAttr='" + dst_attr->name() + "', dstNode='" + + dst_node->type()->name() + "', dstNodeName='" + dst_node->label() + + "'"); + } + } +} +} // namespace ballistica diff --git a/src/ballistica/scene/node/node_attribute_connection.h b/src/ballistica/scene/node/node_attribute_connection.h new file mode 100644 index 00000000..07b3737b --- /dev/null +++ b/src/ballistica/scene/node/node_attribute_connection.h @@ -0,0 +1,26 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_CONNECTION_H_ +#define BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_CONNECTION_H_ + +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +class NodeAttributeConnection : public Object { + public: + NodeAttributeConnection() = default; + void Update(); + Object::WeakRef src_node; + int src_attr_index{}; + Object::WeakRef dst_node; + int dst_attr_index{}; + bool have_error{}; + std::list >::iterator src_iterator; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_CONNECTION_H_ diff --git a/src/ballistica/scene/node/node_type.h b/src/ballistica/scene/node/node_type.h new file mode 100644 index 00000000..7237b500 --- /dev/null +++ b/src/ballistica/scene/node/node_type.h @@ -0,0 +1,82 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_NODE_TYPE_H_ +#define BALLISTICA_SCENE_NODE_NODE_TYPE_H_ + +#include +#include +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// Type structure for a node, storing attribute lists and other static type +// data. +class NodeType { + public: + NodeType(std::string name, NodeCreateFunc* create_call) + : name_(std::move(name)), create_call_(create_call), id_(-1) {} + + /// Return an unbound attribute by name; if missing either throws an exception + /// or returns nullptr. + auto GetAttribute(const std::string& name, bool throw_if_missing = true) const + -> NodeAttributeUnbound* { + auto i = attributes_by_name_.find(name); + if (i == attributes_by_name_.end()) { + if (throw_if_missing) { + throw Exception("Attribute not found: '" + name + "'"); + } else { + return nullptr; + } + } + return i->second; + } + ~NodeType(); + + /// Return an unbound attribute by index. + auto GetAttribute(int index) const -> NodeAttributeUnbound* { + BA_PRECONDITION( + index >= 0 + && index < static_cast_check_fit(attributes_by_index_.size())); + return attributes_by_index_[index]; + } + + auto HasAttribute(const std::string& name) const -> bool { + return (GetAttribute(name, false) != nullptr); + } + + auto name() const -> std::string { return name_; } + + auto GetAttributeNames() const -> std::vector; + + auto Create(Scene* sg) -> Node* { + assert(create_call_); + return create_call_(sg); + } + + auto id() const -> int { + assert(id_ >= 0); + return id_; + } + + void set_id(int val) { id_ = val; } + auto attributes_by_index() const + -> const std::vector& { + return attributes_by_index_; + } + + private: + NodeCreateFunc* create_call_; + int id_; + std::string name_; + std::map attributes_by_name_; + std::vector attributes_by_index_; + friend class NodeAttributeUnbound; + friend class Node; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_NODE_TYPE_H_ diff --git a/src/ballistica/scene/node/null_node.cc b/src/ballistica/scene/node/null_node.cc new file mode 100644 index 00000000..e96f8cbd --- /dev/null +++ b/src/ballistica/scene/node/null_node.cc @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/null_node.h" + +#include "ballistica/game/game.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +// nothing to see here folks... move along +class NullNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS NullNode + BA_NODE_CREATE_CALL(CreateNull); +#undef BA_NODE_TYPE_CLASS + + NullNodeType() : NodeType("null", CreateNull) {} +}; + +static NodeType* node_type{}; + +auto NullNode::InitType() -> NodeType* { + node_type = new NullNodeType(); + return node_type; +} + +NullNode::NullNode(Scene* scene) : Node(scene, node_type) {} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/null_node.h b/src/ballistica/scene/node/null_node.h new file mode 100644 index 00000000..e135e113 --- /dev/null +++ b/src/ballistica/scene/node/null_node.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_NULL_NODE_H_ +#define BALLISTICA_SCENE_NODE_NULL_NODE_H_ + +#include "ballistica/scene/node/node.h" + +// empty node type - just used as a building block +namespace ballistica { + +class Scene; + +// An empty node. +class NullNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit NullNode(Scene* scene); +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_NULL_NODE_H_ diff --git a/src/ballistica/scene/node/player_node.cc b/src/ballistica/scene/node/player_node.cc new file mode 100644 index 00000000..e5b4eecb --- /dev/null +++ b/src/ballistica/scene/node/player_node.cc @@ -0,0 +1,48 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/player_node.h" + +#include + +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class PlayerNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS PlayerNode + BA_NODE_CREATE_CALL(CreatePlayer); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_INT_ATTR(playerID, player_id, SetPlayerID); +#undef BA_NODE_TYPE_CLASS + PlayerNodeType() + : NodeType("player", CreatePlayer), position(this), playerID(this) {} +}; + +static NodeType* node_type{}; + +auto PlayerNode::InitType() -> NodeType* { + node_type = new PlayerNodeType(); + return node_type; +} + +PlayerNode::PlayerNode(Scene* scene) : Node(scene, node_type) {} + +PlayerNode::~PlayerNode() = default; + +void PlayerNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for position"); + } + position_ = vals; +} + +void PlayerNode::SetPlayerID(int val) { + player_id_ = val; + // once this is set we also inform the scene of our existence.. + scene()->SetPlayerNode(player_id_, this); +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/player_node.h b/src/ballistica/scene/node/player_node.h new file mode 100644 index 00000000..45886b7e --- /dev/null +++ b/src/ballistica/scene/node/player_node.h @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_PLAYER_NODE_H_ +#define BALLISTICA_SCENE_NODE_PLAYER_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class PlayerNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit PlayerNode(Scene* scene); + ~PlayerNode() override; + auto position() const -> const std::vector& { return position_; } + void SetPosition(const std::vector& vals); + auto player_id() const -> int { return player_id_; } + void SetPlayerID(int val); + + private: + int player_id_{-1}; + std::vector position_{0.0f, 0.0f, 0.0f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_PLAYER_NODE_H_ diff --git a/src/ballistica/scene/node/prop_node.cc b/src/ballistica/scene/node/prop_node.cc new file mode 100644 index 00000000..a106a482 --- /dev/null +++ b/src/ballistica/scene/node/prop_node.cc @@ -0,0 +1,718 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/prop_node.h" + +#include + +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/area_of_interest.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/scene.h" +#include "ode/ode_joint.h" + +namespace ballistica { + +static void _doCalcERPCFM(float stiffness, float damping, float* erp, + float* cfm) { + if (stiffness <= 0.0f && damping <= 0.0f) { + (*erp) = 0.0f; + // (*cfm) = dInfinity; // doesn't seem to be happy... + (*cfm) = 9999999999.0f; + } else { + (*erp) = (kGameStepSeconds * stiffness) + / ((kGameStepSeconds * stiffness) + damping); + (*cfm) = 1.0f / ((kGameStepSeconds * stiffness) + damping); + } +} + +static NodeType* node_type{}; + +auto PropNode::InitType() -> NodeType* { + node_type = new PropNodeType(); + return node_type; +} + +PropNode::PropNode(Scene* scene, NodeType* override_node_type) + : Node(scene, override_node_type ? override_node_type : node_type), + part_(this) {} + +PropNode::~PropNode() { + if (area_of_interest_) { + g_graphics->camera()->DeleteAreaOfInterest( + static_cast(area_of_interest_)); + } +} + +void PropNode::SetExtraAcceleration(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("expected array of size 3 for extra_acceleration"); + } + extra_acceleration_ = vals; +} + +void PropNode::HandleMessage(const char* data_in) { + const char* data = data_in; + bool handled = true; + switch (extract_node_message_type(&data)) { + case NodeMessageType::kImpulse: { + float px = Utils::ExtractFloat16NBO(&data); + float py = Utils::ExtractFloat16NBO(&data); + float pz = Utils::ExtractFloat16NBO(&data); + float vx = Utils::ExtractFloat16NBO(&data); + float vy = Utils::ExtractFloat16NBO(&data); + float vz = Utils::ExtractFloat16NBO(&data); + float mag = Utils::ExtractFloat16NBO(&data); + float velocity_mag = Utils::ExtractFloat16NBO(&data); + float radius = Utils::ExtractFloat16NBO(&data); + Utils::ExtractInt16NBO(&data); // calc-force-only + float fdirx = Utils::ExtractFloat16NBO(&data); + float fdiry = Utils::ExtractFloat16NBO(&data); + float fdirz = Utils::ExtractFloat16NBO(&data); + body_->ApplyImpulse(px, py, pz, vx, vy, vz, fdirx, fdiry, fdirz, mag, + velocity_mag, radius, false); + break; + } + default: + handled = false; + break; + } + if (!handled) { + Node::HandleMessage(data_in); + } +} + +void PropNode::SetIsAreaOfInterest(bool val) { + if ((val && area_of_interest_ == nullptr) + || (!val && area_of_interest_ != nullptr)) { + // either make one or kill the one we had + if (val) { + assert(area_of_interest_ == nullptr); + area_of_interest_ = g_graphics->camera()->NewAreaOfInterest(false); + } else { + assert(area_of_interest_ != nullptr); + g_graphics->camera()->DeleteAreaOfInterest( + static_cast(area_of_interest_)); + area_of_interest_ = nullptr; + } + } +} + +void PropNode::Draw(FrameDef* frame_def) { +#if !BA_HEADLESS_BUILD + + // need our texture, model, and body to be present to draw.. + if ((!model_.exists()) || (!color_texture_.exists()) || (!body_.exists())) { + return; + } + + ObjectComponent c(frame_def->beauty_pass()); + c.SetTexture(color_texture_); + c.SetLightShadow(LightShadowType::kObject); + if (reflection_ != ReflectionType::kNone) { + c.SetReflection(reflection_); + c.SetReflectionScale(reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + } + if (flashing_ && frame_def->frame_number() % 10 < 5) { + c.SetColor(1.2f, 1.2f, 1.2f); + } + c.PushTransform(); + c.TransformToBody(*body_); + float s = model_scale_ * extra_model_scale_; + c.Scale(s, s, s); + c.DrawModel(model_->model_data()); + c.PopTransform(); + c.Submit(); + + { // shadow + assert(body_.exists()); + const dReal* pos_raw = dGeomGetPosition(body_->geom()); + float pos[3]; + pos[0] = pos_raw[0] + body_->blend_offset().x; + pos[1] = pos_raw[1] + body_->blend_offset().y; + pos[2] = pos_raw[2] + body_->blend_offset().z; + float s_scale, s_density; + shadow_.GetValues(&s_scale, &s_density); + if (body_type_ == BodyType::PUCK) { + s_density *= 2.4f; + s_scale *= 0.85f; + } else { + s_density *= 2.3f; + } + s_density *= 0.34f; + { + GraphicsQuality quality = frame_def->quality(); + + // fancy new cheap shadows + { + float rs = shadow_size_ * model_scale_ * extra_model_scale_ * s_scale; + float d = (quality == GraphicsQuality::kLow ? 1.1f : 0.8f) * s_density; + g_graphics->DrawBlotch(Vector3f(pos), rs * 2.0f, 0.22f * d, 0.16f * d, + 0.10f * d, d); + } + + if (quality > GraphicsQuality::kLow) { + // More sharp accurate shadow. + if (light_model_.exists()) { + SimpleComponent c2(frame_def->light_shadow_pass()); + c2.SetTransparent(true); + float dd = body_type_ == BodyType::LANDMINE ? 0.5f : 1.0f; + c2.SetColor(0.3f, 0.2f, 0.1f, 0.08f * s_density * dd); + c2.PushTransform(); + c2.TransformToBody(*body_); + float ss = body_type_ == BodyType::LANDMINE ? 0.9f : 1.0f; + for (int i = 0; i < 4; i++) { + c2.PushTransform(); + float s2 = ss * model_scale_ * extra_model_scale_ + * (1.3f - 0.08f * static_cast(i)); + c2.Scale(s2, s2, s2); + c2.DrawModel(light_model_->model_data()); + c2.PopTransform(); + } + c2.PopTransform(); + c2.Submit(); + } + + // In fancy-pants mode we can do a softened version of ourself + // for fake caustic effects. + if (light_model_.exists()) { + assert(color_texture_.exists()); + SimpleComponent c2(frame_def->light_shadow_pass()); + c2.SetTransparent(true); + c2.SetPremultiplied(true); + c2.SetTexture(color_texture_); + if (flashing_ && frame_def->frame_number() % 10 < 5) { + c2.SetColor(0.026f * s_density, 0.026f * s_density, + 0.026f * s_density, 0.0f); + } else { + c2.SetColor(0.022f * s_density, 0.022f * s_density, + 0.022f * s_density, 0.0f); + } + c2.PushTransform(); + c2.TransformToBody(*body_); + for (int i = 0; i < 4; i++) { + c2.PushTransform(); + float s2 = model_scale_ * extra_model_scale_ * 1.7f; + c2.Scale(s2, s2, s2); + c2.Rotate(-50.0f + 43.0f * static_cast(i), 0.2f, 0.4f, 0.6f); + c2.DrawModel(light_model_->model_data()); + c2.PopTransform(); + } + c2.PopTransform(); + c2.Submit(); + } + } + } + } +#endif // !BA_HEADLESS_BUILD +} + +auto PropNode::GetBody() const -> std::string { + switch (body_type_) { + case BodyType::UNSET: + return ""; + case BodyType::BOX: + return "box"; + case BodyType::SPHERE: + return "sphere"; + case BodyType::CRATE: + return "crate"; + case BodyType::LANDMINE: + return "landMine"; + case BodyType::CAPSULE: + return "capsule"; + case BodyType::PUCK: + return "puck"; + default: + throw Exception("Invalid body-type in prop-node: " + + std::to_string(static_cast(body_type_))); + } +} + +void PropNode::SetBodyScale(float val) { + // this can be set exactly once + if (body_.exists()) { + throw Exception("body_scale can't be set once body exists"); + } + body_scale_ = std::max(0.01f, val); +} + +void PropNode::SetBody(const std::string& val) { + BodyType body_type; + RigidBody::Shape shape; + if (val == "box") { + body_type = BodyType::BOX; + shape = RigidBody::Shape::kBox; + } else if (val == "sphere") { + body_type = BodyType::SPHERE; + shape = RigidBody::Shape::kSphere; + } else if (val == "crate") { + body_type = BodyType::CRATE; + shape = RigidBody::Shape::kBox; + } else if (val == "landMine") { + body_type = BodyType::LANDMINE; + shape = RigidBody::Shape::kBox; + } else if (val == "capsule") { + body_type = BodyType::CAPSULE; + shape = RigidBody::Shape::kCapsule; + } else if (val == "puck") { + body_type = BodyType::PUCK; + shape = RigidBody::Shape::kCylinder; + } else { + throw Exception("Invalid body type: '" + val + "'"); + } + + // we're ok with redundant sets, but complain/ignore if they try to switch.. + if (body_.exists()) { + if (body_type_ != body_type || shape_ != shape) { + Log("ERROR: body attr can not be changed from its initial value"); + return; + } + } + body_type_ = body_type; + shape_ = shape; + body_ = + Object::New(0, &part_, RigidBody::Type::kBody, shape_, + RigidBody::kCollideActive, RigidBody::kCollideAll); + + body_->set_can_cause_impact_damage(true); + body_->AddCallback(DoCollideCallback, this); + if (body_type_ == BodyType::LANDMINE) { + float bs1 = 0.7f * body_scale_; + float bs2 = 0.18f * body_scale_; + body_->SetDimensions(bs1, bs2, bs1, bs1, bs2, bs1, 2.0f * density_); + } else if (body_type_ == BodyType::CRATE) { + float s = 0.7f * body_scale_; + body_->SetDimensions(s, s, s, s, s, s, 0.7f * density_); + } else if (body_type_ == BodyType::SPHERE) { + float s = 0.3f * body_scale_; + body_->SetDimensions(s, 0, 0, s, 0, 0, density_); + } else if (body_type_ == BodyType::CAPSULE) { + float s = 0.3f * body_scale_; + body_->SetDimensions(s, s, 0, s, s, 0, density_); + } + + // in case we've had a translate or velocity set already.. + dBodySetPosition(body_->body(), position_[0], position_[1], position_[2]); + dBodySetLinearVel(body_->body(), velocity_[0], velocity_[1], velocity_[2]); + + // initial orientation: + // put pucks upright and make them big + if (body_type_ == BodyType::PUCK) { + dQuaternion iq; + dQFromAxisAndAngle(iq, 1, 0, 0, -90 * (kPi / 180.0f)); + dBodySetQuaternion(body_->body(), iq); + body_->SetDimensions(0.7f, 0.58f, 0, 0.7f, 0.48f, 0, 0.14f * density_); + } else { + // give other types random start rotations.. + dQuaternion iq; + int64_t gti = scene()->stepnum(); + dQFromAxisAndAngle( + iq, 0.05f, 1, 0, + Utils::precalc_rands_1[(stream_id() + gti) % kPrecalcRandsCount] + * 360.0f * (kPi / 180.0f)); + dBodySetQuaternion(body_->body(), iq); + } +} + +void PropNode::UpdateAreaOfInterest() { + auto* aoi = static_cast(area_of_interest_); + if (!aoi) { + return; + } + assert(body_.exists()); + aoi->set_position(Vector3f(dGeomGetPosition(body_->geom()))); + aoi->SetRadius(5.0f); +} + +void PropNode::SetReflectionScale(const std::vector& vals) { + if (vals.size() != 1 && vals.size() != 3) { + throw Exception( + "Expected float array of length" + " 1 or 3 for reflection_scale"); + } + reflection_scale_ = vals; + if (reflection_scale_.size() == 1) { + reflection_scale_r_ = reflection_scale_g_ = reflection_scale_b_ = + reflection_scale_[0]; + } else { + reflection_scale_r_ = reflection_scale_[0]; + reflection_scale_g_ = reflection_scale_[1]; + reflection_scale_b_ = reflection_scale_[2]; + } +} + +auto PropNode::GetReflection() const -> std::string { + return Graphics::StringFromReflectionType(reflection_); +} + +void PropNode::SetReflection(const std::string& val) { + reflection_ = Graphics::ReflectionTypeFromString(val); +} + +auto PropNode::GetMaterials() const -> std::vector { + return part_.GetMaterials(); +} + +void PropNode::SetMaterials(const std::vector& vals) { + part_.SetMaterials(vals); +} + +auto PropNode::GetVelocity() const -> std::vector { + // if we've got a body, return its velocity + if (body_.exists()) { + const dReal* v = dBodyGetLinearVel(body_->body()); + std::vector vv(3); + vv[0] = v[0]; + vv[1] = v[1]; + vv[2] = v[2]; + return vv; + } + // otherwise if we have an internally stored value, return that. + // (this way if we set velocity and then query it we'll get the right value + // even if the body hasn't been made yet) + return velocity_; +} + +void PropNode::SetVelocity(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for velocity"); + } + // if we've got a body, apply the velocity to that + if (body_.exists()) { + dBodySetLinearVel(body_->body(), vals[0], vals[1], vals[2]); + } else { + // otherwise just store it in our internal vector in + // case someone asks for it + velocity_ = vals; + } +} + +auto PropNode::GetPosition() const -> std::vector { + // if we've got a body, return its position + if (body_.exists()) { + const dReal* p = dGeomGetPosition(body_->geom()); + std::vector f(3); + f[0] = p[0]; + f[1] = p[1]; + f[2] = p[2]; + return f; + } + // otherwise if we have an internally stored value, return that. + // (this way if we set position and then query it we'll get the right value + // even if the body hasn't been made yet) + return position_; +} + +void PropNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for position (got " + + std::to_string(vals.size()) + ")"); + } + // if we've got a body, apply the position to that + if (body_.exists()) { + dBodySetPosition(body_->body(), vals[0], vals[1], vals[2]); + } else { + // otherwise just store it in our internal vector + // in case someone asks for it + position_ = vals; + } +} + +void PropNode::Step() { + if (body_type_ == BodyType::UNSET) { + if (!reported_unset_body_type_) { + reported_unset_body_type_ = true; + Log("ERROR: prop-node " + GetObjectDescription() + + " did not have its 'body' attr set."); + return; + } + } + BA_DEBUG_CHECK_BODIES(); + + assert(body_.exists()); + + // FIXME - this should probably happen for RBDs automatically?... + body_->UpdateBlending(); + + // on happy thoughts, keep us on the 2d plane.. + if (g_graphics->camera()->happy_thoughts_mode() && body_.exists()) { + dBodyID b; + const dReal *p, *v; + b = body_->body(); + p = dBodyGetPosition(b); + dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0], v[1], 0.0f); + } + + // update our area-of-interest if we have one + UpdateAreaOfInterest(); + + // update our shadow input positions + { +#if !BA_HEADLESS_BUILD + shadow_.SetPosition(Vector3f(dBodyGetPosition(body_->body()))); +#endif // !BA_HEADLESS_BUILD + } + + // clamp our max linear and angular velocities + { + dBodyID b = body_->body(); + float max_mag_squared = 400.f; + // float max_mag_squared_lin = 300.0f; + float max_mag_squared_lin = max_speed_ * max_speed_; + const dReal* aVel = dBodyGetAngularVel(b); + float mag_squared = + aVel[0] * aVel[0] + aVel[1] * aVel[1] + aVel[2] * aVel[2]; + if (mag_squared > max_mag_squared) { + float scale = max_mag_squared / mag_squared; + dBodySetAngularVel(b, aVel[0] * scale, aVel[1] * scale, aVel[2] * scale); + } + const dReal* lVel = dBodyGetLinearVel(b); + mag_squared = lVel[0] * lVel[0] + lVel[1] * lVel[1] + lVel[2] * lVel[2]; + if (mag_squared > max_mag_squared_lin) { + float scale = max_mag_squared_lin / mag_squared; + dBodySetLinearVel(b, lVel[0] * scale, lVel[1] * scale, lVel[2] * scale); + } + } + + // if we're out of bounds, arrange to have ourself informed + if (body_.exists()) { + const dReal* p = dBodyGetPosition(body_->body()); + if (scene()->IsOutOfBounds(p[0], p[1], p[2])) { + scene()->AddOutOfBoundsNode(this); + } + } + + // apply damping force + float rotationalDampingX = 0.02f; + float rotationalDampingY = 0.02f; + float rotationalDampingZ = 0.02f; + + // don't add forces if we're asleep otherwise we'll explode when we wake up + if (dBodyIsEnabled(body_->body())) { + dMass mass; + dBodyID b = body_->body(); + dBodyGetMass(b, &mass); + + const dReal* vel; + dReal force[3]; + vel = dBodyGetAngularVel(b); + force[0] = -1 * mass.mass * vel[0] * rotationalDampingX; + force[1] = -1 * mass.mass * vel[1] * rotationalDampingY; + force[2] = -1 * mass.mass * vel[2] * rotationalDampingZ; + dBodyAddTorque(b, force[0], force[1], force[2]); + if (damping_ > 0.0f) { + float damp = std::max(0.0f, 1.0f - damping_); + const dReal* vel2 = dBodyGetLinearVel(b); + dBodySetLinearVel(b, vel2[0] * damp, vel2[1] * damp, vel2[2] * damp); + } + if (extra_acceleration_[0] != 0.0f || extra_acceleration_[1] != 0.0f + || extra_acceleration_[2] != 0.0f) { + dBodyAddForce(b, extra_acceleration_[0] * mass.mass, + extra_acceleration_[1] * mass.mass, + extra_acceleration_[2] * mass.mass); + } + if (gravity_scale_ != 1.0f) { + dVector3 grav; + // the simplest way to do this is to just add a force to offset gravity + // to where we want it to be for this object.. + float amt = gravity_scale_ - 1.0f; + dWorldGetGravity(scene()->dynamics()->ode_world(), grav); + dBodyAddForce(b, mass.mass * amt * grav[0], mass.mass * amt * grav[1], + mass.mass * amt * grav[2]); + } + } + BA_DEBUG_CHECK_BODIES(); +} + +auto PropNode::GetRigidBody(int id) -> RigidBody* { + if (id == 0) { + return body_.get(); + } + return nullptr; +} + +auto PropNode::CollideCallback(dContact* c, int count, + RigidBody* colliding_body, + RigidBody* opposingbody) -> bool { + if (sticky_) { + uint32_t f = opposingbody->flags(); + + // dont collide at all with rollers.. + if (f & RigidBody::kIsRoller) { + return false; + } + // this should never happen, right?.. + assert(opposingbody->part()->node() != nullptr); + + if ((stick_to_owner_ || opposingbody->part()->node() != owner_.get()) + && !(f & RigidBody::kIsBumper)) { + if (body_.exists()) { + // stick to static stuff: + if (opposingbody->type() == RigidBody::Type::kGeomOnly) { + const dReal* v; + dBodyID b = body_->body(); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0] * 0.2f, v[1] * 0.2f, v[2] * 0.2f); + dBodySetAngularVel(b, 0, 0, 0); + } else { + // stick to dynamic stuff + dBodyID b2 = opposingbody->body(); + dBodyID b1 = body_->body(); + dBodyEnable(b1); // wake it up + dBodyEnable(b2); // wake it up + dMass m; + dBodyGetMass(b2, &m); + dJointID j = + dJointCreateFixed(scene()->dynamics()->ode_world(), + scene()->dynamics()->getContactGroup()); + dJointAttach(j, b1, b2); + dJointSetFixed(j); + dJointSetFixedSpringMode(j, 1, 1, false); + if (m.mass < 0.2f) { + dJointSetFixedParam(j, dParamLinearStiffness, 200); + dJointSetFixedParam(j, dParamLinearDamping, 0.2f); + dJointSetFixedParam(j, dParamAngularStiffness, 200); + dJointSetFixedParam(j, dParamAngularDamping, 0.2f); + } else { + dJointSetFixedParam(j, dParamLinearStiffness, 2000); + dJointSetFixedParam(j, dParamLinearDamping, 2); + dJointSetFixedParam(j, dParamAngularStiffness, 2000); + dJointSetFixedParam(j, dParamAngularDamping, 2); + } + + // ...now attractive forces. + // FIXME - currently we ignore small stuff like limb bits. + // We really should just vary our sticky strength based + // on the mass of what we're hitting though. + if (m.mass < 0.2f) { + return true; // Still collide; just not sticky. + } + + // Also exert a slight attractive force. + { + const dReal* p1 = dBodyGetPosition(b1); + const dReal* p2 = dBodyGetPosition(b2); + dReal f2[3]; + float stiffness = 200; + f2[0] = (p1[0] - p2[0]) * stiffness; + f2[1] = (p1[1] - p2[1]) * stiffness; + f2[2] = (p1[2] - p2[2]) * stiffness; + dBodyAddForce(b1, -f2[0], -f2[1], -f2[2]); + dBodyAddForce(b2, f2[0], f2[1], f2[2]); + } + } + } + } + } + + if (body_type_ == BodyType::CRATE) { + // Drop stiffness/damping/friction pretty low. + float stiffness = 800.0f; + float damping = 1.0f; + if (opposingbody->flags() & RigidBody::kIsTerrain) { + damping = 10.0f; + } + float erp, cfm; + _doCalcERPCFM(stiffness, damping, &erp, &cfm); + for (int i = 0; i < count; i++) { + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + c[i].surface.mu *= 0.7f; + } + } else if (body_type_ == BodyType::LANDMINE) { + // we wanna be laying flat down; if we're standing upright, topple over + dVector3 worldUp; + dBodyVectorToWorld(body_->body(), 0, 1, 0, worldUp); + if (std::abs(worldUp[1]) < 0.4f) { + float mag = -4.0f; + // push in the 2 horizontal axes only + const dReal* pos = dBodyGetPosition(body_->body()); + dBodyAddForceAtPos(body_->body(), mag * worldUp[0], 0, mag * worldUp[2], + pos[0], pos[1] + 1.0f, pos[2]); + dBodyAddForceAtPos(body_->body(), -mag * worldUp[0], 0, -mag * worldUp[2], + pos[0], pos[1] - 1.0f, pos[2]); + } + // drop stiffness/damping/friction pretty low.. + float stiffness = 1000.0f; + float damping = 10.0f; + float erp, cfm; + _doCalcERPCFM(stiffness, damping, &erp, &cfm); + + // if we're not lying flat, kill friction + float friction = 1.0f; + if (std::abs(worldUp[1]) < 0.7f) friction = 0.05f; + + for (int i = 0; i < count; i++) { + c[i].surface.mu *= friction; + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + } + + // lets also damp our velocity a tiny bit if we're hitting terrain + if (opposingbody->flags() & RigidBody::kIsTerrain) { + float damp = 0.98f; + const dReal* vel = dBodyGetLinearVel(body_->body()); + dBodySetLinearVel(body_->body(), vel[0] * damp, vel[1], vel[2] * damp); + } + } else { + // drop stiffness/damping/friction pretty low.. + float stiffness = 5000.0f; + float damping = 10.0f; + float erp, cfm; + _doCalcERPCFM(stiffness, damping, &erp, &cfm); + for (int i = 0; i < count; i++) { + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + c[i].surface.mu *= 0.2f; + } + } + + return true; +} + +void PropNode::GetRigidBodyPickupLocations(int id, float* obj, float* character, + float* hand1, float* hand2) { + if (body_type_ == BodyType::LANDMINE) { + obj[0] = 0; + obj[1] = -0.1f; + obj[2] = 0; + character[0] = character[1] = character[2] = 0.0f; + character[1] = -0.3f; + character[2] = 0; + hand1[0] = -0.15f; + hand1[1] = 0.00f; + hand1[2] = 0.0f; + hand2[0] = 0.15f; + hand2[1] = 0.00f; + hand2[2] = 0.0f; + } else { + obj[0] = 0; + obj[1] = -0.17f; + obj[2] = 0; + character[0] = character[1] = character[2] = 0.0f; + character[1] = -0.27f; + hand1[0] = -0.15f; + hand1[1] = 0.00f; + hand1[2] = 0.0f; + hand2[0] = 0.15f; + hand2[1] = 0.00f; + hand2[2] = 0.0f; + } +} + +void PropNode::SetDensity(float val) { + if (body_.exists()) { + throw Exception("can't set density after body has been set"); + } + density_ = std::max(0.01f, std::min(100.0f, val)); +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/prop_node.h b/src/ballistica/scene/node/prop_node.h new file mode 100644 index 00000000..b8ea5684 --- /dev/null +++ b/src/ballistica/scene/node/prop_node.h @@ -0,0 +1,188 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_PROP_NODE_H_ +#define BALLISTICA_SCENE_NODE_PROP_NODE_H_ + +#include +#include + +#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/scene/node/node.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class PropNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit PropNode(Scene* scene, NodeType* node_type = nullptr); + ~PropNode() override; + void HandleMessage(const char* data) override; + void Draw(FrameDef* frame_def) override; + void Step() override; + auto GetRigidBody(int id) -> RigidBody* override; + auto is_area_of_interest() const -> bool { + return (area_of_interest_ != nullptr); + } + void SetIsAreaOfInterest(bool val); + auto reflection_scale() const -> std::vector { + return reflection_scale_; + } + void SetReflectionScale(const std::vector& vals); + auto GetReflection() const -> std::string; + void SetReflection(const std::string& val); + auto color_texture() const -> Texture* { return color_texture_.get(); } + void set_color_texture(Texture* val) { color_texture_ = val; } + auto GetModel() const -> Model* { return model_.get(); } + void set_model(Model* val) { model_ = val; } + auto light_model() const -> Model* { return light_model_.get(); } + void set_light_model(Model* val) { light_model_ = val; } + auto sticky() const -> bool { return sticky_; } + void set_sticky(bool val) { sticky_ = val; } + auto shadow_size() const -> float { return shadow_size_; } + void set_shadow_size(float val) { shadow_size_ = val; } + auto stick_to_owner() const -> bool { return stick_to_owner_; } + void set_stick_to_owner(bool val) { stick_to_owner_ = val; } + auto model_scale() const -> float { return model_scale_; } + void set_model_scale(float val) { model_scale_ = val; } + auto flashing() const -> bool { return flashing_; } + void set_flashing(bool val) { flashing_ = val; } + auto owner() const -> Node* { return owner_.get(); } + void set_owner(Node* val) { owner_ = val; } + auto GetMaterials() const -> std::vector; + void SetMaterials(const std::vector& materials); + auto GetVelocity() const -> std::vector; + void SetVelocity(const std::vector& vals); + auto GetPosition() const -> std::vector; + void SetPosition(const std::vector& vals); + auto extra_acceleration() const -> std::vector { + return extra_acceleration_; + } + void SetExtraAcceleration(const std::vector& vals); + auto GetBody() const -> std::string; + void SetBody(const std::string& val); + auto density() const -> float { return density_; } + void SetDensity(float val); + auto body_scale() const -> float { return body_scale_; } + void SetBodyScale(float val); + auto damping() const -> float { return damping_; } + void set_damping(float val) { damping_ = val; } + auto max_speed() const -> float { return max_speed_; } + void set_max_speed(float val) { max_speed_ = val; } + auto gravity_scale() const -> float { return gravity_scale_; } + void set_gravity_scale(float val) { gravity_scale_ = val; } + + protected: + // FIXME - need to make all this private and add protected getters/setters + // as necessary + enum class BodyType { UNSET, SPHERE, BOX, LANDMINE, CRATE, CAPSULE, PUCK }; + void UpdateAreaOfInterest(); +#if !BA_HEADLESS_BUILD + BGDynamicsShadow shadow_; +#endif + Part part_; + void* area_of_interest_{}; + float model_scale_{1.0f}; + float shadow_size_{1.0f}; + int color_texture_Val{}; + float gravity_scale_{1.0f}; + Object::Ref body_; + RigidBody::Shape shape_{RigidBody::Shape::kSphere}; + Object::Ref color_texture_; + Object::Ref model_; + Object::Ref light_model_; + float density_{1.0f}; + float body_scale_{1.0f}; + float damping_{}; + float max_speed_{20.0f}; + std::vector velocity_{0.0f, 0.0f, 0.0f}; + std::vector position_{0.0f, 0.0f, 0.0f}; + std::vector extra_acceleration_{0.0, 0.0, 0.0}; + float extra_model_scale_{1.0f}; // For use by subclasses. + bool sticky_{}; + Object::WeakRef owner_; + bool flashing_{}; + bool stick_to_owner_{}; + BodyType body_type_{BodyType::UNSET}; + bool reported_unset_body_type_{}; + ReflectionType reflection_{ReflectionType::kNone}; + std::vector reflection_scale_{1.0f, 1.0f, 1.0f}; + float reflection_scale_r_{1.0f}; + float reflection_scale_g_{1.0f}; + float reflection_scale_b_{1.0f}; + static auto DoCollideCallback(dContact* c, int count, + RigidBody* colliding_body, + RigidBody* opposingbody, void* data) -> bool { + auto* a = static_cast(data); + return a->CollideCallback(c, count, colliding_body, opposingbody); + } + auto CollideCallback(dContact* c, int count, RigidBody* colliding_body, + RigidBody* opposingbody) -> bool; + void GetRigidBodyPickupLocations(int id, float* obj, float* character, + float* hand1, float* hand2) override; +}; + +class PropNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS PropNode + BA_NODE_CREATE_CALL(CreateProp); + BA_BOOL_ATTR(is_area_of_interest, is_area_of_interest, SetIsAreaOfInterest); + BA_FLOAT_ARRAY_ATTR(reflection_scale, reflection_scale, SetReflectionScale); + BA_STRING_ATTR(reflection, GetReflection, SetReflection); + BA_TEXTURE_ATTR(color_texture, color_texture, set_color_texture); + BA_MODEL_ATTR(model, GetModel, set_model); + BA_MODEL_ATTR(light_model, light_model, set_light_model); + BA_BOOL_ATTR(sticky, sticky, set_sticky); + BA_FLOAT_ATTR(shadow_size, shadow_size, set_shadow_size); + BA_BOOL_ATTR(stick_to_owner, stick_to_owner, set_stick_to_owner); + BA_FLOAT_ATTR(model_scale, model_scale, set_model_scale); + BA_BOOL_ATTR(flashing, flashing, set_flashing); + BA_NODE_ATTR(owner, owner, set_owner); + BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); + BA_FLOAT_ARRAY_ATTR(velocity, GetVelocity, SetVelocity); + BA_FLOAT_ARRAY_ATTR(position, GetPosition, SetPosition); + BA_FLOAT_ATTR(density, density, SetDensity); + BA_FLOAT_ATTR(damping, damping, set_damping); + BA_FLOAT_ATTR(body_scale, body_scale, SetBodyScale); + BA_FLOAT_ATTR(max_speed, max_speed, set_max_speed); + BA_FLOAT_ARRAY_ATTR(extra_acceleration, extra_acceleration, + SetExtraAcceleration); + BA_FLOAT_ATTR(gravity_scale, gravity_scale, set_gravity_scale); + BA_STRING_ATTR(body, GetBody, SetBody); +#undef BA_NODE_TYPE_CLASS + + explicit PropNodeType(const char* sub_type_name = nullptr, + NodeCreateFunc* sub_type_create = nullptr) + : NodeType(sub_type_name ? sub_type_name : "prop", + sub_type_create ? sub_type_create : CreateProp), + is_area_of_interest(this), + reflection_scale(this), + reflection(this), + color_texture(this), + model(this), + light_model(this), + sticky(this), + shadow_size(this), + stick_to_owner(this), + model_scale(this), + flashing(this), + owner(this), + materials(this), + velocity(this), + position(this), + density(this), + damping(this), + max_speed(this), + body_scale(this), + body(this), + extra_acceleration(this), + gravity_scale(this) {} +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_PROP_NODE_H_ diff --git a/src/ballistica/scene/node/region_node.cc b/src/ballistica/scene/node/region_node.cc new file mode 100644 index 00000000..a6663908 --- /dev/null +++ b/src/ballistica/scene/node/region_node.cc @@ -0,0 +1,106 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/region_node.h" + +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class RegionNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS RegionNode + BA_NODE_CREATE_CALL(CreateRegion); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ARRAY_ATTR(scale, scale, SetScale); + BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); + BA_STRING_ATTR(type, region_type, SetRegionType); +#undef BA_NODE_TYPE_CLASS + + RegionNodeType() + : NodeType("region", CreateRegion), + position(this), + scale(this), + materials(this), + type(this) {} +}; + +static NodeType* node_type{}; + +auto RegionNode::InitType() -> NodeType* { + node_type = new RegionNodeType(); + return node_type; +} + +RegionNode::RegionNode(Scene* scene) + : Node(scene, node_type), part_(this, false) {} + +void RegionNode::Draw(FrameDef* frame_def) { + if (g_graphics_server->renderer()->debug_draw_mode()) { + // if (frame_def->renderer()->debug_draw_mode()) { + if (body_.exists()) { + body_->Draw(frame_def->beauty_pass(), false); + } + } +} + +void RegionNode::SetRegionType(const std::string& val) { + if (val == region_type_) { + return; + } + region_type_ = val; + body_.Clear(); // will be recreated next step +} + +auto RegionNode::GetMaterials() const -> std::vector { + return part_.GetMaterials(); +} + +void RegionNode::SetMaterials(const std::vector& vals) { + part_.SetMaterials(vals); +} + +void RegionNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for position"); + } + position_ = vals; + size_or_pos_dirty_ = true; +} + +void RegionNode::SetScale(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for scale"); + } + scale_ = vals; + size_or_pos_dirty_ = true; +} + +void RegionNode::Step() { + // create our body if we have none + if (!body_.exists()) { + if (region_type_ == "sphere") { + body_ = Object::New( + 0, &part_, RigidBody::Type::kGeomOnly, RigidBody::Shape::kSphere, + RigidBody::kCollideRegion, RigidBody::kCollideActive); + } else { + if (region_type_ != "box") { + BA_LOG_ONCE("got unexpected region type: " + region_type_); + } + body_ = Object::New( + 0, &part_, RigidBody::Type::kGeomOnly, RigidBody::Shape::kBox, + RigidBody::kCollideRegion, RigidBody::kCollideActive); + } + size_or_pos_dirty_ = true; // always needs updating after create + } + if (size_or_pos_dirty_) { + dGeomSetPosition(body_->geom(), position_[0], position_[1], position_[2]); + body_->SetDimensions(scale_[0], scale_[1], scale_[2]); + size_or_pos_dirty_ = false; + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/region_node.h b/src/ballistica/scene/node/region_node.h new file mode 100644 index 00000000..2f53b53e --- /dev/null +++ b/src/ballistica/scene/node/region_node.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_REGION_NODE_H_ +#define BALLISTICA_SCENE_NODE_REGION_NODE_H_ + +#include +#include + +#include "ballistica/dynamics/part.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// A region node - used to detect if an object is in a certain area +class RegionNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit RegionNode(Scene* scene); + void Draw(FrameDef* frame_def) override; + void Step() override; + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& vals); + auto scale() const -> std::vector { return scale_; } + void SetScale(const std::vector& vals); + auto GetMaterials() const -> std::vector; + void SetMaterials(const std::vector& vals); + auto region_type() const -> std::string { return region_type_; } + void SetRegionType(const std::string& val); + + private: + bool size_or_pos_dirty_ = true; + Part part_; + std::vector position_ = {0.0f, 0.0f, 0.0f}; + std::vector scale_ = {1.0f, 1.0f, 1.0f}; + std::string region_type_ = "box"; + Object::Ref body_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_REGION_NODE_H_ diff --git a/src/ballistica/scene/node/scorch_node.cc b/src/ballistica/scene/node/scorch_node.cc new file mode 100644 index 00000000..2b4d995b --- /dev/null +++ b/src/ballistica/scene/node/scorch_node.cc @@ -0,0 +1,80 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/scorch_node.h" + +#include + +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class ScorchNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS ScorchNode + BA_NODE_CREATE_CALL(CreateScorch); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ATTR(presence, presence, set_presence); + BA_FLOAT_ATTR(size, size, set_size); + BA_BOOL_ATTR(big, big, set_big); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); +#undef BA_NODE_TYPE_CLASS + ScorchNodeType() + : NodeType("scorch", CreateScorch), + position(this), + presence(this), + size(this), + big(this), + color(this) {} +}; + +static NodeType* node_type{}; + +auto ScorchNode::InitType() -> NodeType* { + node_type = new ScorchNodeType(); + return node_type; +} + +ScorchNode::ScorchNode(Scene* scene) : Node(scene, node_type) { + rand_size_[0] = 0.7f + RandomFloat() * 0.6f; + rand_size_[1] = 0.7f + RandomFloat() * 0.6f; + rand_size_[2] = 0.7f + RandomFloat() * 0.6f; +} + +ScorchNode::~ScorchNode() = default; + +void ScorchNode::SetColor(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of length 3 for color"); + color_ = vals; +} + +void ScorchNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of length 3 for position"); + position_ = vals; +} + +void ScorchNode::Draw(FrameDef* frame_def) { + float o = presence_; + // modulate opacity by local shadow density + o *= g_graphics->GetShadowDensity(position_[0], position_[1], position_[2]); + SimpleComponent c(frame_def->light_shadow_pass()); + c.SetTransparent(true); + c.SetColor(color_[0], color_[1], color_[2], o * 0.35f); + c.SetTexture(g_media->GetTexture(big_ ? SystemTextureID::kScorchBig + : SystemTextureID::kScorch)); + c.PushTransform(); + c.Translate(position_[0], position_[1], position_[2]); + c.Scale(o * size_ * rand_size_[0], o * size_ * rand_size_[1], + o * size_ * rand_size_[2]); + c.Rotate(Utils::precalc_rands_1[id() % kPrecalcRandsCount] * 360.0f, 0, 1, 0); + c.DrawModel(g_media->GetModel(SystemModelID::kScorch)); + c.PopTransform(); + c.Submit(); +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/scorch_node.h b/src/ballistica/scene/node/scorch_node.h new file mode 100644 index 00000000..3859d60f --- /dev/null +++ b/src/ballistica/scene/node/scorch_node.h @@ -0,0 +1,40 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_SCORCH_NODE_H_ +#define BALLISTICA_SCENE_NODE_SCORCH_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class ScorchNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit ScorchNode(Scene* scene); + ~ScorchNode() override; + void Draw(FrameDef* frame_def) override; + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& vals); + auto presence() const -> float { return presence_; } + void set_presence(float val) { presence_ = val; } + auto size() const -> float { return size_; } + void set_size(float val) { size_ = val; } + auto big() const -> bool { return big_; } + void set_big(bool val) { big_ = val; } + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& vals); + + private: + std::vector position_{0.0f, 0.0f, 0.0f}; + std::vector color_{0.07f, 0.03f, 0.0f}; + float presence_{1.0f}; + float size_{1.0f}; + bool big_{}; + float rand_size_[3]{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_SCORCH_NODE_H_ diff --git a/src/ballistica/scene/node/session_globals_node.cc b/src/ballistica/scene/node/session_globals_node.cc new file mode 100644 index 00000000..aa21c49d --- /dev/null +++ b/src/ballistica/scene/node/session_globals_node.cc @@ -0,0 +1,50 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/session_globals_node.h" + +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class SessionGlobalsNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS SessionGlobalsNode + BA_NODE_CREATE_CALL(CreateSessionGlobals); + BA_INT64_ATTR_READONLY(real_time, GetRealTime); + BA_INT64_ATTR_READONLY(time, GetTime); + BA_INT64_ATTR_READONLY(step, GetStep); +#undef BA_NODE_TYPE_CLASS + SessionGlobalsNodeType() + : NodeType("sessionglobals", CreateSessionGlobals), + real_time(this), + time(this), + step(this) {} +}; + +static NodeType* node_type{}; + +auto SessionGlobalsNode::InitType() -> NodeType* { + node_type = new SessionGlobalsNodeType(); + return node_type; +} + +SessionGlobalsNode::SessionGlobalsNode(Scene* scene) : Node(scene, node_type) { + // We don't expose this as an attr, but we tell our scene to display stuff in + // the fixed overlay position by default when doing vr. + this->scene()->set_use_fixed_vr_overlay(true); +} + +SessionGlobalsNode::~SessionGlobalsNode() = default; + +auto SessionGlobalsNode::GetRealTime() -> millisecs_t { + // Pull this from our scene so we return consistent values throughout a step. + return scene()->last_step_real_time(); +} + +auto SessionGlobalsNode::GetTime() -> millisecs_t { return scene()->time(); } + +auto SessionGlobalsNode::GetStep() -> int64_t { return scene()->stepnum(); } + +} // namespace ballistica diff --git a/src/ballistica/scene/node/session_globals_node.h b/src/ballistica/scene/node/session_globals_node.h new file mode 100644 index 00000000..4ed2c303 --- /dev/null +++ b/src/ballistica/scene/node/session_globals_node.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_SESSION_GLOBALS_NODE_H_ +#define BALLISTICA_SCENE_NODE_SESSION_GLOBALS_NODE_H_ + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class SessionGlobalsNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit SessionGlobalsNode(Scene* scene); + ~SessionGlobalsNode() override; + auto GetRealTime() -> millisecs_t; + auto GetTime() -> millisecs_t; + auto GetStep() -> int64_t; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_SESSION_GLOBALS_NODE_H_ diff --git a/src/ballistica/scene/node/shield_node.cc b/src/ballistica/scene/node/shield_node.cc new file mode 100644 index 00000000..11788c24 --- /dev/null +++ b/src/ballistica/scene/node/shield_node.cc @@ -0,0 +1,287 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/shield_node.h" + +#include +#include + +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/post_process_component.h" +#include "ballistica/graphics/component/shield_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class ShieldNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS ShieldNode + BA_NODE_CREATE_CALL(CreateShield); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ATTR(radius, radius, set_radius); + BA_FLOAT_ATTR(hurt, hurt, SetHurt); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_BOOL_ATTR(always_show_health_bar, always_show_health_bar, + set_always_show_health_bar); +#undef BA_NODE_TYPE_CLASS + + ShieldNodeType() + : NodeType("shield", CreateShield), + position(this), + radius(this), + hurt(this), + color(this), + always_show_health_bar(this) {} +}; +static NodeType* node_type{}; + +auto ShieldNode::InitType() -> NodeType* { + node_type = new ShieldNodeType(); + return node_type; +} + +ShieldNode::ShieldNode(Scene* scene) + : Node(scene, node_type) +#if !BA_HEADLESS_BUILD + , + shadow_(0.2f) +#endif // !BA_HEADLESS_BUILD +{ + last_hurt_change_time_ = scene->time(); +} + +ShieldNode::~ShieldNode() = default; + +void ShieldNode::SetColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for color"); + } + color_ = vals; +} + +void ShieldNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of length 3 for position"); + } + position_ = vals; +} + +void ShieldNode::SetHurt(float val) { + float old_hurt = hurt_; + hurt_ = val; + if (hurt_ != old_hurt) { + // Only flash if we change by a significant amount + // (avoids flashing during regular drain). + if (std::abs(hurt_ - old_hurt) > 0.05f) { + flash_ = 1.0f; + last_hurt_change_time_ = scene()->time(); + } + } +} + +void ShieldNode::Step() { + float smoothing = 0.94f; + d_r_scale_ = smoothing * d_r_scale_ + (1.0f - smoothing) * (1.0f - r_scale_); + r_scale_ += d_r_scale_; + d_r_scale_ *= 0.92f; + + // Move our smoothed hurt value a short time after we get hit. + if (scene()->time() - last_hurt_change_time_ > 400) { + if (hurt_smoothed_ < hurt_) { + hurt_smoothed_ = std::min(hurt_, hurt_smoothed_ + 0.03f); + } else { + hurt_smoothed_ = std::max(hurt_, hurt_smoothed_ - 0.03f); + } + } + + flash_ -= 0.04f; + if (flash_ < 0.0f) { + flash_ = 0.0f; + } + hurt_rand_ = RandomFloat(); + rot_count_ = (rot_count_ + 1) % 256; + +#if !BA_HEADLESS_BUILD + shadow_.SetPosition(Vector3f(position_[0], position_[1], position_[2])); +#endif // !BA_HEADLESS_BUILD +} + +void ShieldNode::Draw(FrameDef* frame_def) { +#if !BA_HEADLESS_BUILD + + { + float o = (1.0f - hurt_) * 1.0f + + hurt_ * (1.0f * hurt_rand_ * hurt_rand_ * hurt_rand_); + float s_density, s_scale; + shadow_.GetValues(&s_scale, &s_density); + float brightness = s_density * 0.8f * o; + if (flash_ > 0.0f) { + brightness *= (1.0f + 6.0f * flash_); + } + float rs = (0.6f + hurt_rand_ * 0.05f) * radius_ * s_scale * r_scale_; + + // draw our light on both terrain and objects + g_graphics->DrawBlotchSoft(Vector3f(&position_[0]), 3.4f * rs, + color_[0] * brightness, color_[1] * brightness, + color_[2] * brightness, 0.0f); + // draw our light on both terrain and objects + g_graphics->DrawBlotchSoftObj( + Vector3f(&position_[0]), 3.4f * rs, color_[0] * brightness * 0.4f, + color_[1] * brightness * 0.4f, color_[2] * brightness * 0.4f, 0.0f); + } + + // Life bar. + { + uint32_t fade_time = 2000; + + millisecs_t since_last_hurt_change = + scene()->time() - last_hurt_change_time_; + + if (since_last_hurt_change < fade_time || always_show_health_bar_) { + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.PushTransform(); + float o = 1.0f - static_cast(since_last_hurt_change) / fade_time; + if (always_show_health_bar_) { + o = std::max(o, 0.5f); + } + o *= o; + float p_left, p_right; + if (hurt_ < hurt_smoothed_) { + p_left = 1.0f - hurt_smoothed_; + p_right = 1.0f - hurt_; + } else { + p_right = 1.0f - hurt_smoothed_; + p_left = 1.0f - hurt_; + } + + // For the first moment start p_left at p_right so they can see a glimpse + // of green before it goes away. + if (since_last_hurt_change < 100) { + p_left += + (p_right - p_left) + * (1.0f - static_cast(since_last_hurt_change) / 100.0f); + } + c.Translate(position_[0] - 0.25f, position_[1] + 1.25f, position_[2]); + c.Scale(0.5f, 0.5f, 0.5f); + float height = 0.1f; + float half_height = height * 0.5f; + c.SetColor(0, 0, 0.3f, 0.3f * o); + c.PushTransform(); + c.Translate(0.5f, half_height); + c.Scale(1.1f, height + 0.1f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.SetColor(0.4f * o, 0.4f * o, 0.8f * o, 0.0f * o); + c.PushTransform(); + c.Translate(p_left * 0.5f, half_height); + c.Scale(p_left, height); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.SetColor(1.0f * o, 1.0f * o, 1.0f * o, 0.0f); + c.PushTransform(); + c.Translate((p_left + p_right) * 0.5f, half_height); + c.Scale(p_right - p_left, height); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.SetColor(0.1f * o, 0.1f * o, 0.2f * o, 0.4f * o); + c.PushTransform(); + c.Translate((p_right + 1.0f) * 0.5f, half_height); + c.Scale(1.0f - p_right, height); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.PopTransform(); + c.Submit(); + } + } + + // main bubble + float r = hurt_rand_; + float o = (1.0f - hurt_) * 1.0f + hurt_ * (1.0f * r * r * r); + o *= 0.3f; + float cx, cy, cz; + g_graphics->camera()->get_position(&cx, &cy, &cz); + float col[4]; + col[0] = color_[0] * o; + col[1] = color_[1] * o; + col[2] = color_[2] * o; + float distort = 0.05f + RandomFloat() * 0.06f; + if (flash_ > 0.0f) { + distort += 0.9f * (RandomFloat() - 0.4f) * flash_; + col[0] += flash_; + col[1] += flash_; + col[2] += flash_; + } + + { + ObjectComponent c(frame_def->beauty_pass()); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.SetLightShadow(LightShadowType::kNone); + c.SetReflection(ReflectionType::kSharp); + c.SetReflectionScale(0.34f * o, 0.34f * o, 0.34f * o); + c.SetTexture(g_media->GetTexture(SystemTextureID::kShield)); + c.SetColor(col[0], col[1], col[2], 0.13f * o); + c.PushTransform(); + Vector3f to_cam = + Vector3f(cx - position_[0], cy - position_[1], cz - position_[2]) + .Normalized(); + Matrix44f m = + Matrix44fTranslate(position_[0], position_[1] + 0.1f, position_[2]); + Vector3f right = Vector3f::Cross(to_cam, kVector3fY).Normalized(); + Vector3f up = Vector3f::Cross(right, to_cam).Normalized(); + Matrix44f om = Matrix44fOrient(right, to_cam, up); + c.MultMatrix((om * m).m); + float s = radius_ * 0.53f; + c.Scale(s, s, s); + c.Rotate(Utils::precalc_rands_1[rot_count_ % kPrecalcRandsCount] * 360, 0, + 1, 0); + float r2 = + r_scale_ + * (0.97f + + 0.05f * Utils::precalc_rands_2[rot_count_ % kPrecalcRandsCount]); + c.Scale(r2, r2, r2); + c.DrawModel(g_media->GetModel(SystemModelID::kShield), + kModelDrawFlagNoReflection); + c.PopTransform(); + c.Submit(); + + // Nifty intersection effects in fancy graphics mode. + if (frame_def->has_depth_texture()) { + ShieldComponent c2(frame_def->overlay_3d_pass()); + c2.PushTransform(); + c2.MultMatrix((om * m).m); + c2.Scale(s, s, s); + c2.Rotate(Utils::precalc_rands_1[rot_count_ % kPrecalcRandsCount] * 360, + 0, 1, 0); + c2.Scale(r2, r2, r2); + c2.DrawModel(g_media->GetModel(SystemModelID::kShield)); + c2.PopTransform(); + c2.Submit(); + } + if (frame_def->has_depth_texture()) { + PostProcessComponent c2(frame_def->blit_pass()); + c2.setNormalDistort(distort); + c2.PushTransform(); + c2.MultMatrix((om * m).m); + c2.Scale(s, s, s); + c2.Rotate(Utils::precalc_rands_1[rot_count_ % kPrecalcRandsCount] * 360, + 0, 1, 0); + float sc = r2 * 1.1f; + c2.Scale(sc, sc, sc); + c2.DrawModel(g_media->GetModel(SystemModelID::kShield)); + c2.PopTransform(); + c2.Submit(); + } + } +#endif // BA_HEADLESS_BUILD +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/shield_node.h b/src/ballistica/scene/node/shield_node.h new file mode 100644 index 00000000..44c317da --- /dev/null +++ b/src/ballistica/scene/node/shield_node.h @@ -0,0 +1,53 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_SHIELD_NODE_H_ +#define BALLISTICA_SCENE_NODE_SHIELD_NODE_H_ + +#include + +#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class ShieldNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit ShieldNode(Scene* scene); + ~ShieldNode() override; + void Draw(FrameDef* frame_def) override; + void Step() override; + auto position() const -> std::vector { return position_; } + void SetPosition(const std::vector& vals); + auto radius() const -> float { return radius_; } + void set_radius(float val) { radius_ = val; } + auto hurt() const -> float { return hurt_; } + void SetHurt(float val); + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& vals); + auto always_show_health_bar() const -> bool { + return always_show_health_bar_; + } + void set_always_show_health_bar(bool val) { always_show_health_bar_ = val; } + + private: +#if !BA_HEADLESS_BUILD + BGDynamicsShadow shadow_; +#endif // BA_HEADLESS_BUILD + bool always_show_health_bar_ = false; + float hurt_smoothed_ = 1.0f; + millisecs_t last_hurt_change_time_ = 0; + float d_r_scale_ = 0.0f; + float r_scale_ = 0.0f; + std::vector position_ = {0.0f, 0.0f, 0.0f}; + std::vector color_ = {0.6f, 0.4f, 0.1f}; + float radius_ = 1.0f; + float hurt_ = 0.0f; + float flash_ = 0.0f; + float hurt_rand_ = 0.0f; + int rot_count_ = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_SHIELD_NODE_H_ diff --git a/src/ballistica/scene/node/sound_node.cc b/src/ballistica/scene/node/sound_node.cc new file mode 100644 index 00000000..fe3f021b --- /dev/null +++ b/src/ballistica/scene/node/sound_node.cc @@ -0,0 +1,145 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/sound_node.h" + +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/audio/audio_source.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class SoundNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS SoundNode + BA_NODE_CREATE_CALL(CreateSound); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_FLOAT_ATTR(volume, volume, SetVolume); + BA_BOOL_ATTR(positional, positional, SetPositional); + BA_BOOL_ATTR(music, music, SetMusic); + BA_BOOL_ATTR(loop, loop, SetLoop); + BA_SOUND_ATTR(sound, sound, SetSound); +#undef BA_NODE_TYPE_CLASS + SoundNodeType() + : NodeType("sound", CreateSound), + position(this), + volume(this), + positional(this), + music(this), + loop(this), + sound(this) {} +}; +static NodeType* node_type{}; + +auto SoundNode::InitType() -> NodeType* { + node_type = new SoundNodeType(); + return node_type; +} + +SoundNode::SoundNode(Scene* scene) : Node(scene, node_type) {} + +SoundNode::~SoundNode() { + if (playing_) { + g_audio->PushSourceStopSoundCall(play_id_); + } +} + +void SoundNode::SetPosition(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of size 3 for position"); + position_ = vals; + + // We don't actually update here; we just mark our position as dirty + // and then update it every now and then. + position_dirty_ = true; +} + +void SoundNode::SetVolume(float val) { + if (val == volume_) { + return; + } + volume_ = val; + + // FIXME we could probably update this in an infrequent manner in case its + // being driven by another attr. + if (playing_) { + AudioSource* s = g_audio->SourceBeginExisting(play_id_, 106); + if (s) { + s->SetGain(volume_); + s->End(); + } + } +} + +void SoundNode::SetLoop(bool val) { + if (loop_ == val) return; + loop_ = val; + + // We don't actually update looping on a playing sound. + if (playing_) + BA_LOG_ONCE("Error: can't set 'loop' attr on already-playing sound."); +} + +void SoundNode::SetSound(Sound* s) { + if (s == sound_.get()) return; + sound_ = s; + + // We'll start playing in our next Step; this allows + // time for other setAttrs to go through first such as looping. + // (which can't happen after we start playing) +} + +void SoundNode::SetPositional(bool val) { + if (val == positional_) return; + positional_ = val; + if (playing_) + BA_LOG_ONCE("Error: can't set 'positional' attr on already-playing sound"); +} + +void SoundNode::SetMusic(bool val) { + if (val == music_) return; + music_ = val; + if (playing_) { + AudioSource* s = g_audio->SourceBeginExisting(play_id_, 104); + if (s) { + s->SetIsMusic(music_); + s->End(); + } + } +} + +void SoundNode::Step() { + // If we want to start playing, do so. + if (!playing_ && sound_.exists()) { + AudioSource* s = g_audio->SourceBeginNew(); + if (s) { + assert(position_.size() == 3); + s->SetPosition(position_[0], position_[1], position_[2]); + s->SetLooping(loop_); + s->SetPositional(positional_); + s->SetGain(volume_); + s->SetIsMusic(music_); + play_id_ = s->Play(sound_->GetSoundData()); + playing_ = true; + s->End(); + } + } + if (positional_ && position_dirty_ && playing_) { + millisecs_t t = GetRealTime(); + if (t - last_position_update_time_ > 100) { + AudioSource* s = g_audio->SourceBeginExisting(play_id_, 107); + if (s) { + s->SetPosition(position_[0], position_[1], position_[2]); + s->End(); + } + last_position_update_time_ = t; + position_dirty_ = false; + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/sound_node.h b/src/ballistica/scene/node/sound_node.h new file mode 100644 index 00000000..7921415e --- /dev/null +++ b/src/ballistica/scene/node/sound_node.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_SOUND_NODE_H_ +#define BALLISTICA_SCENE_NODE_SOUND_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class SoundNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit SoundNode(Scene* scene); + ~SoundNode() override; + void Step() override; + auto position() const -> const std::vector& { return position_; } + void SetPosition(const std::vector& vals); + auto volume() const -> float { return volume_; } + void SetVolume(float val); + auto positional() const -> bool { return positional_; } + void SetPositional(bool val); + auto music() const -> bool { return music_; } + void SetMusic(bool val); + auto loop() const -> bool { return loop_; } + void SetLoop(bool val); + auto sound() const -> Sound* { return sound_.get(); } + void SetSound(Sound* s); + + private: + Object::Ref sound_; + millisecs_t last_position_update_time_{}; + std::vector position_{0.0f, 0.0f, 0.0f}; + float volume_{1.0f}; + bool positional_{true}; + bool position_dirty_{true}; + bool music_{}; + bool loop_{true}; + uint32_t play_id_{}; + bool playing_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_SOUND_NODE_H_ diff --git a/src/ballistica/scene/node/spaz_node.cc b/src/ballistica/scene/node/spaz_node.cc new file mode 100644 index 00000000..e4eab570 --- /dev/null +++ b/src/ballistica/scene/node/spaz_node.cc @@ -0,0 +1,6817 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/spaz_node.h" + +#include +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/audio/audio_source.h" +#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/dynamics/collision.h" +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/game/player.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/area_of_interest.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/post_process_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" +#include "ode/ode_collision_util.h" +#include "ode/ode_joint.h" + +namespace ballistica { + +// pull a random pointer from a ref-vector +template +auto GetRandomMedia(const std::vector >& list) -> T* { + if (list.empty()) return nullptr; + return list[rand() % list.size()].get(); // NOLINT yes I know; rand bad. +} + +const float kSantaEyeScale = 0.9f; +const float kSantaEyeTranslate = 0.03f; + +const float kRunJointLinearStiffness = 80.0f; +const float kRunJointLinearDamping = 2.0f; +const float kRunJointAngularStiffness = 0.2f; +const float kRunJointAngularDamping = 0.002f; + +const float kRollerBallLinearStiffness = 1000.0f; +const float kRollerBallLinearDamping = 0.2f; + +const float kPelvisDensity = 5.0f; +const float kPelvisLinearStiffness = 300.0f; +const float kPelvisLinearDamping = 20.0f; +const float kPelvisAngularStiffness = 1.5f; +const float kPelvisAngularDamping = 0.06f; + +const float kUpperLegDensity = 2.0f; +const float kUpperLegLinearStiffness = 300.0f; +const float kUpperLegLinearDamping = 5.0f; +const float kUpperLegAngularStiffness = 0.12f; +const float kUpperLegAngularDamping = 0.004f; +const float kUpperLegCollideStiffness = 100.0f; +const float kUpperLegCollideDamping = 100.0f; + +const float kLowerLegDensity = 2.0f; +const float kLowerLegLinearStiffness = 200.0f; +const float kLowerLegLinearDamping = 5.0f; +const float kLowerLegAngularStiffness = 0.12f; +const float kLowerLegAngularDamping = 0.004f; +const float kLowerLegCollideStiffness = 100.0f; +const float kLowerLegCollideDamping = 100.0f; + +const float kToesDensity = 0.5f; +const float kToesLinearStiffness = 50.0f; +const float kToesLinearDamping = 1.0f; +const float kToesAngularStiffness = 0.015f; +const float kToesAngularDamping = 0.0005f; +const float kToesCollideStiffness = 10.0f; +const float kToesCollideDamping = 10.0f; + +const float kUpperArmDensity = 2.0f; + +const float kUpperArmLinearStiffness = 30.0f; +const float kUpperArmLinearDamping = 1.2f; +const float kUpperArmAngularStiffness = 0.08f; +const float kUpperArmAngularDamping = 0.008f; + +const float kLowerArmDensity = 2.0f; +const float kLowerArmLinearStiffness = 80.0f; +const float kLowerArmLinearDamping = 1.0f; +const float kLowerArmAngularStiffness = 0.08f; +const float kLowerArmAngularDamping = 0.008f; + +const float kHairFrontLeftLinearStiffness = 0.2f; +const float kHairFrontLeftLinearDamping = 0.01f; +const float kHairFrontLeftAngularStiffness = 0.00025f; +const float kHairFrontLeftAngularDamping = 0.000001f; + +const float kHairFrontRightLinearStiffness = 0.2f; +const float kHairFrontRightLinearDamping = 0.01f; +const float kHairFrontRightAngularStiffness = 0.00025f; +const float kHairFrontRightAngularDamping = 0.000001f; + +const float kHairPonytailTopLinearStiffness = 1.0f; +const float kHairPonytailTopLinearDamping = 0.03f; +const float kHairPonytailTopAngularStiffness = 0.0015f; +const float kHairPonytailTopAngularDamping = 0.000003f; + +const float kHairPonytailBottomLinearStiffness = 0.4f; +const float kHairPonytailBottomLinearDamping = 0.02f; +const float kHairPonytailBottomAngularStiffness = 0.00025f; +const float kHairPonytailBottomAngularDamping = 0.000001f; + +const int kPunchDuration = 35; +const int kPickupCooldown = 40; + +const float kWingAttachX = 0.3f; +const float kWingAttachY = 0.0f; +const float kWingAttachZ = -0.45f; + +const float kWingAttachFlapX = 0.55f; +const float kWingAttachFlapY = 0.0f; +const float kWingAttachFlapZ = -0.35f; + +enum SpazBodyType { + kHeadBodyID, + kTorsoBodyID, + kPunchBodyID, + kPickupBodyID, + kPelvisBodyID, + kRollerBodyID, + kStandBodyID, + kUpperRightArmBodyID, + kLowerRightArmBodyID, + kUpperLeftArmBodyID, + kLowerLeftArmBodyID, + kUpperRightLegBodyID, + kLowerRightLegBodyID, + kUpperLeftLegBodyID, + kLowerLeftLegBodyID, + kLeftToesBodyID, + kRightToesBodyID, + kHairFrontRightBodyID, + kHairFrontLeftBodyID, + kHairPonyTailTopBodyID, + kHairPonyTailBottomBodyID +}; + +static auto AngleBetween2DVectors(dReal x1, dReal y1, dReal x2, dReal y2) + -> dReal { + dReal x1_norm, y1_norm, x2_norm, y2_norm; + dReal len1, len2; + len1 = sqrtf(x1 * x1 + y1 * y1); + len2 = sqrtf(x2 * x2 + y2 * y2); + x1_norm = x1 / len1; + y1_norm = y1 / len1; + x2_norm = x2 / len2; + y2_norm = y2 / len2; + dReal angle = atanf(y1_norm / x1_norm); + if (x1_norm < 0) { + if (y1_norm > 0.0f) { + angle = angle + 3.141592f; + } else { + angle = angle - 3.141592f; + } + } + dReal angle2 = atanf(y2_norm / x2_norm); + if (x2_norm < 0) { + if (y2_norm > 0.0f) { + angle2 = angle2 + 3.141592f; + } else { + angle2 = angle2 - 3.141592f; + } + } + dReal angle_diff = angle2 - angle; + if (angle_diff > 3.141592f) { + angle_diff -= 3.141592f * 2.0f; + } else if (angle_diff < -3.141592f) { + angle_diff += 3.141592f * 2.0f; + } + return angle_diff; +} + +static void RotationFrom2Axes(dMatrix3 r, dReal x_forward, dReal y_forward, + dReal z_forward, dReal x_up, dReal y_up, + dReal z_up) { + Vector3f forward(x_forward, y_forward, z_forward); + Vector3f up = Vector3f(x_up, y_up, z_up).Normalized(); + Vector3f side = Vector3f(Vector3f::Cross(forward, up)).Normalized(); + Vector3f forward2 = Vector3f::Cross(up, side); + r[0] = forward2.x; + r[4] = forward2.y; + r[8] = forward2.z; + r[1] = up.x; + r[5] = up.y; + r[9] = up.z; + r[2] = side.x; + r[6] = side.y; + r[10] = side.z; +} + +static void CalcERPCFM(float stiffness, float damping, float* erp, float* cfm) { + if (stiffness <= 0.0f && damping <= 0.0f) { + (*erp) = 0.0f; + // (*cfm) = dInfinity; // doesn't seem to be happy... + (*cfm) = 9999999999.0f; + } else { + (*erp) = (kGameStepSeconds * stiffness) + / ((kGameStepSeconds * stiffness) + damping); + (*cfm) = 1.0f / ((kGameStepSeconds * stiffness) + damping); + } +} + +struct JointFixedEF : public dxJoint { + dQuaternion qrel; // relative rotation body1 -> body2 + dVector3 anchor1; // anchor w.r.t first body + dVector3 anchor2; // anchor w.r.t second body + float linearStiffness; + float linearDamping; + float angularStiffness; + float angularDamping; + bool linearEnabled; + bool angularEnabled; +}; + +static void _fixedInit(JointFixedEF* j) { + dSetZero(j->qrel, 4); + dSetZero(j->anchor1, 3); + dSetZero(j->anchor2, 3); + j->linearStiffness = 0.0f; + j->linearDamping = 0.0f; + j->angularStiffness = 0.0f; + j->angularDamping = 0.0f; + + // testing + j->linearEnabled = true; + j->angularEnabled = true; +} + +static void _SetBall(JointFixedEF* joint, dxJoint::Info2* info, + dVector3 anchor1, dVector3 anchor2) { + assert(joint->node[1].body); + + // anchor points in global coordinates with respect to body PORs. + dVector3 a1, a2; + + int s = info->rowskip; + + // set jacobian + info->J1l[0] = 1; + info->J1l[s + 1] = 1; + info->J1l[2 * s + 2] = 1; + dMULTIPLY0_331(a1, joint->node[0].body->R, anchor1); + dCROSSMAT(info->J1a, a1, s, -, +); + info->J2l[0] = -1; + info->J2l[s + 1] = -1; + info->J2l[2 * s + 2] = -1; + dMULTIPLY0_331(a2, joint->node[1].body->R, anchor2); + dCROSSMAT(info->J2a, a2, s, +, -); + + // set right hand side + dReal k = info->fps * info->erp; + for (int j = 0; j < 3; j++) { + info->c[j] = k + * (a2[j] + joint->node[1].body->pos[j] - a1[j] + - joint->node[0].body->pos[j]); + } +} + +// FIXME this is duplicated a few times... +static void _SetFixedOrientation(JointFixedEF* joint, dxJoint::Info2* info, + dQuaternion qrel, int start_row) { + assert(joint->node[1].body); // we assume we're connected to 2 bodies.. + + int s = info->rowskip; + int start_index = start_row * s; + + // 3 rows to make body rotations equal + info->J1a[start_index] = 1; + info->J1a[start_index + s + 1] = 1; + info->J1a[start_index + s * 2 + 2] = 1; + info->J2a[start_index] = -1; + info->J2a[start_index + s + 1] = -1; + info->J2a[start_index + s * 2 + 2] = -1; + + // compute the right hand side. the first three elements will result in + // relative angular velocity of the two bodies - this is set to bring them + // back into alignment. the correcting angular velocity is + // |angular_velocity| = angle/time = erp*theta / stepsize + // = (erp*fps) * theta + // angular_velocity = |angular_velocity| * u + // = (erp*fps) * theta * u + // where rotation along unit length axis u by theta brings body 2's frame + // to qrel with respect to body 1's frame. using a small angle approximation + // for sin(), this gives + // angular_velocity = (erp*fps) * 2 * v + // where the quaternion of the relative rotation between the two bodies is + // q = [cos(theta/2) sin(theta/2)*u] = [s v] + + // get qerr = relative rotation (rotation error) between two bodies + dQuaternion qerr, e; + dQuaternion qq; + dQMultiply1(qq, joint->node[0].body->q, joint->node[1].body->q); + dQMultiply2(qerr, qq, qrel); + if (qerr[0] < 0) { + qerr[1] = -qerr[1]; // adjust sign of qerr to make theta small + qerr[2] = -qerr[2]; + qerr[3] = -qerr[3]; + } + dMULTIPLY0_331(e, joint->node[0].body->R, qerr + 1); // @@@ bad SIMD padding! + dReal k; + + k = info->fps * info->erp; + info->c[start_row] = 2 * k * e[0]; + info->c[start_row + 1] = 2 * k * e[1]; + info->c[start_row + 2] = 2 * k * e[2]; +} + +static void _fixedGetInfo1(JointFixedEF* j, dxJoint::Info1* info) { + info->m = 0; + info->nub = 0; + if (j->linearEnabled + && (j->linearStiffness > 0.0f || j->linearDamping > 0.0f)) { + info->m += 3; + info->nub += 3; + } + if (j->angularEnabled + && (j->angularStiffness > 0.0f || j->angularDamping > 0.0f)) { + info->m += 3; + info->nub += 3; + } +} + +static void _fixedGetInfo2(JointFixedEF* joint, dxJoint::Info2* info) { + assert(joint + && (joint->linearStiffness > 0.0f || joint->linearDamping > 0.0f + || joint->angularStiffness > 0.0f + || joint->angularDamping > 0.0f)); + dReal orig_erp = info->erp; + bool do_linear = + (joint->linearEnabled + && (joint->linearStiffness > 0.0f || joint->linearDamping > 0.0f)); + bool do_angular = + (joint->angularEnabled + && (joint->angularStiffness > 0.0f || joint->angularDamping > 0.0f)); + int offs = 0; + // linear component... + if (do_linear) { + float linear_erp = 0; + float linear_cfm = 0; + CalcERPCFM(joint->linearStiffness, joint->linearDamping, &linear_erp, + &linear_cfm); + info->erp = linear_erp; + _SetBall(joint, info, joint->anchor1, joint->anchor2); + info->cfm[0] = linear_cfm; + info->cfm[1] = linear_cfm; + info->cfm[2] = linear_cfm; + offs += 3; + } + // angular component... + if (do_angular) { + float angular_erp; + float angular_cfm; + CalcERPCFM(joint->angularStiffness, joint->angularDamping, &angular_erp, + &angular_cfm); + info->erp = angular_erp; + _SetFixedOrientation(joint, info, joint->qrel, offs); + info->cfm[offs] = angular_cfm; + info->cfm[offs + 1] = angular_cfm; + info->cfm[offs + 2] = angular_cfm; + } + info->erp = orig_erp; +} + +dxJoint::Vtable fixed_vtable_ = { + sizeof(JointFixedEF), (dxJoint::init_fn*)_fixedInit, + (dxJoint::getInfo1_fn*)_fixedGetInfo1, + (dxJoint::getInfo2_fn*)_fixedGetInfo2, dJointTypeNone}; + +#if !BA_HEADLESS_BUILD +class SpazNode::FullShadowSet : public Object { + public: + ~FullShadowSet() override = default; + BGDynamicsShadow torso_shadow_; + BGDynamicsShadow head_shadow_; + BGDynamicsShadow pelvis_shadow_; + BGDynamicsShadow lower_left_leg_shadow_; + BGDynamicsShadow lower_right_leg_shadow_; + BGDynamicsShadow upper_left_leg_shadow_; + BGDynamicsShadow upper_right_leg_shadow_; + BGDynamicsShadow lower_left_arm_shadow_; + BGDynamicsShadow lower_right_arm_shadow_; + BGDynamicsShadow upper_left_arm_shadow_; + BGDynamicsShadow upper_right_arm_shadow_; +}; + +class SpazNode::SimpleShadowSet : public Object { + public: + BGDynamicsShadow shadow_; +}; +#endif // !BA_HEADLESS_BUILD + +class SpazNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS SpazNode + BA_NODE_CREATE_CALL(CreateSpaz); + BA_BOOL_ATTR(fly, can_fly, set_can_fly); + BA_BOOL_ATTR(hockey, hockey, set_hockey); + BA_MATERIAL_ARRAY_ATTR(roller_materials, GetRollerMaterials, + SetRollerMaterials); + BA_MATERIAL_ARRAY_ATTR(extras_material, GetExtrasMaterials, + SetExtrasMaterials); + BA_MATERIAL_ARRAY_ATTR(punch_materials, GetPunchMaterials, SetPunchMaterials); + BA_MATERIAL_ARRAY_ATTR(pickup_materials, GetPickupMaterials, + SetPickupMaterials); + BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); + BA_FLOAT_ATTR(area_of_interest_radius, area_of_interest_radius, + set_area_of_interest_radius); + BA_STRING_ATTR(name, name, set_name); + BA_STRING_ATTR(counter_text, counter_text, set_counter_text); + BA_TEXTURE_ATTR(mini_billboard_1_texture, mini_billboard_1_texture, + set_mini_billboard_1_texture); + BA_TEXTURE_ATTR(mini_billboard_2_texture, mini_billboard_2_texture, + set_mini_billboard_2_texture); + BA_TEXTURE_ATTR(mini_billboard_3_texture, mini_billboard_3_texture, + set_mini_billboard_3_texture); + BA_INT64_ATTR(mini_billboard_1_start_time, mini_billboard_1_start_time, + set_mini_billboard_1_start_time); + BA_INT64_ATTR(mini_billboard_1_end_time, mini_billboard_1_end_time, + set_mini_billboard_1_end_time); + BA_INT64_ATTR(mini_billboard_2_start_time, mini_billboard_2_start_time, + set_mini_billboard_2_start_time); + BA_INT64_ATTR(mini_billboard_2_end_time, mini_billboard_2_end_time, + set_mini_billboard_2_end_time); + BA_INT64_ATTR(mini_billboard_3_start_time, mini_billboard_3_start_time, + set_mini_billboard_3_start_time); + BA_INT64_ATTR(mini_billboard_3_end_time, mini_billboard_3_end_time, + set_mini_billboard_3_end_time); + BA_TEXTURE_ATTR(billboard_texture, billboard_texture, set_billboard_texture); + BA_FLOAT_ATTR(billboard_opacity, billboard_opacity, set_billboard_opacity); + BA_TEXTURE_ATTR(counter_texture, counter_texture, set_counter_texture); + BA_BOOL_ATTR(invincible, invincible, set_invincible); + BA_FLOAT_ARRAY_ATTR(name_color, name_color, SetNameColor); + BA_FLOAT_ARRAY_ATTR(highlight, highlight, set_highlight); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_FLOAT_ATTR(hurt, hurt, SetHurt); + BA_BOOL_ATTR(boxing_gloves_flashing, boxing_gloves_flashing, + set_boxing_gloves_flashing); + BA_PLAYER_ATTR(source_player, source_player, set_source_player); + BA_BOOL_ATTR(frozen, frozen, SetFrozen); + BA_BOOL_ATTR(boxing_gloves, have_boxing_gloves, SetHaveBoxingGloves); + BA_INT64_ATTR(curse_death_time, curse_death_time, SetCurseDeathTime); + BA_INT_ATTR(shattered, shattered, SetShattered); + BA_BOOL_ATTR(dead, dead, SetDead); + BA_STRING_ATTR(style, style, SetStyle); + BA_FLOAT_ATTR_READONLY(knockout, GetKnockout); + BA_FLOAT_ATTR_READONLY(punch_power, punch_power); + BA_FLOAT_ATTR_READONLY(punch_momentum_angular, GetPunchMomentumAngular); + BA_FLOAT_ARRAY_ATTR_READONLY(punch_momentum_linear, GetPunchMomentumLinear); + BA_FLOAT_ATTR_READONLY(damage, damage_out); + BA_FLOAT_ATTR_READONLY(damage_smoothed, damage_smoothed); + BA_FLOAT_ARRAY_ATTR_READONLY(punch_velocity, GetPunchVelocity); + BA_BOOL_ATTR(is_area_of_interest, is_area_of_interest, SetIsAreaOfInterest); + BA_FLOAT_ARRAY_ATTR_READONLY(velocity, GetVelocity); + BA_FLOAT_ARRAY_ATTR_READONLY(position_forward, GetPositionForward); + BA_FLOAT_ARRAY_ATTR_READONLY(position_center, GetPositionCenter); + BA_FLOAT_ARRAY_ATTR_READONLY(punch_position, GetPunchPosition); + BA_FLOAT_ARRAY_ATTR_READONLY(torso_position, GetTorsoPosition); + BA_FLOAT_ARRAY_ATTR_READONLY(position, GetPosition); + BA_INT_ATTR(hold_body, hold_body, set_hold_body); + BA_NODE_ATTR(hold_node, hold_node, SetHoldNode); + BA_SOUND_ARRAY_ATTR(jump_sounds, GetJumpSounds, SetJumpSounds); + BA_SOUND_ARRAY_ATTR(attack_sounds, GetAttackSounds, SetAttackSounds); + BA_SOUND_ARRAY_ATTR(impact_sounds, GetImpactSounds, SetImpactSounds); + BA_SOUND_ARRAY_ATTR(death_sounds, GetDeathSounds, SetDeathSounds); + BA_SOUND_ARRAY_ATTR(pickup_sounds, GetPickupSounds, SetPickupSounds); + BA_SOUND_ARRAY_ATTR(fall_sounds, GetFallSounds, SetFallSounds); + BA_TEXTURE_ATTR(color_texture, color_texture, set_color_texture); + BA_TEXTURE_ATTR(color_mask_texture, color_mask_texture, + set_color_mask_texture); + BA_MODEL_ATTR(head_model, head_model, set_head_model); + BA_MODEL_ATTR(torso_model, torso_model, set_torso_model); + BA_MODEL_ATTR(pelvis_model, pelvis_model, set_pelvis_model); + BA_MODEL_ATTR(upper_arm_model, upper_arm_model, set_upper_arm_model); + BA_MODEL_ATTR(forearm_model, forearm_model, set_forearm_model); + BA_MODEL_ATTR(hand_model, hand_model, set_hand_model); + BA_MODEL_ATTR(upper_leg_model, upper_leg_model, set_upper_leg_model); + BA_MODEL_ATTR(lower_leg_model, lower_leg_model, set_lower_leg_model); + BA_MODEL_ATTR(toes_model, toes_model, set_toes_model); + BA_BOOL_ATTR(billboard_cross_out, billboard_cross_out, + set_billboard_cross_out); + BA_BOOL_ATTR(jump_pressed, jump_pressed, SetJumpPressed); + BA_BOOL_ATTR(punch_pressed, punch_pressed, SetPunchPressed); + BA_BOOL_ATTR(bomb_pressed, bomb_pressed, SetBombPressed); + BA_FLOAT_ATTR(run, run, SetRun); + BA_BOOL_ATTR(fly_pressed, fly_pressed, SetFlyPressed); + BA_BOOL_ATTR(pickup_pressed, pickup_pressed, SetPickupPressed); + BA_BOOL_ATTR(hold_position_pressed, hold_position_pressed, + SetHoldPositionPressed); + BA_FLOAT_ATTR(move_left_right, move_left_right, SetMoveLeftRight); + BA_FLOAT_ATTR(move_up_down, move_up_down, SetMoveUpDown); + BA_BOOL_ATTR(demo_mode, demo_mode, set_demo_mode); + BA_INT_ATTR(behavior_version, behavior_version, set_behavior_version); +#undef BA_NODE_TYPE_CLASS + + SpazNodeType() + : NodeType("spaz", CreateSpaz), + fly(this), + hockey(this), + roller_materials(this), + extras_material(this), + punch_materials(this), + pickup_materials(this), + materials(this), + area_of_interest_radius(this), + name(this), + counter_text(this), + mini_billboard_1_texture(this), + mini_billboard_2_texture(this), + mini_billboard_3_texture(this), + mini_billboard_1_start_time(this), + mini_billboard_1_end_time(this), + mini_billboard_2_start_time(this), + mini_billboard_2_end_time(this), + mini_billboard_3_start_time(this), + mini_billboard_3_end_time(this), + billboard_texture(this), + billboard_opacity(this), + counter_texture(this), + invincible(this), + name_color(this), + highlight(this), + color(this), + hurt(this), + boxing_gloves_flashing(this), + source_player(this), + frozen(this), + boxing_gloves(this), + curse_death_time(this), + shattered(this), + dead(this), + style(this), + knockout(this), + punch_power(this), + punch_momentum_angular(this), + punch_momentum_linear(this), + damage(this), + damage_smoothed(this), + punch_velocity(this), + is_area_of_interest(this), + velocity(this), + position_forward(this), + position_center(this), + punch_position(this), + torso_position(this), + position(this), + hold_body(this), + hold_node(this), + jump_sounds(this), + attack_sounds(this), + impact_sounds(this), + death_sounds(this), + pickup_sounds(this), + fall_sounds(this), + color_texture(this), + color_mask_texture(this), + head_model(this), + torso_model(this), + pelvis_model(this), + upper_arm_model(this), + forearm_model(this), + hand_model(this), + upper_leg_model(this), + lower_leg_model(this), + toes_model(this), + billboard_cross_out(this), + jump_pressed(this), + punch_pressed(this), + bomb_pressed(this), + run(this), + fly_pressed(this), + pickup_pressed(this), + hold_position_pressed(this), + move_left_right(this), + move_up_down(this), + demo_mode(this), + behavior_version(this) {} +}; +static NodeType* node_type{}; + +auto SpazNode::InitType() -> NodeType* { + node_type = new SpazNodeType(); + return node_type; +} + +SpazNode::SpazNode(Scene* scene) + : Node(scene, node_type), + birth_time_(scene->time()), + spaz_part_(this), + hair_part_(this), + punch_part_(this, false), + pickup_part_(this, false), + extras_part_(this, false), + roller_part_(this, true), + limbs_part_upper_(this, true), + limbs_part_lower_(this, true) { + // Head + body_head_ = + Object::New(kHeadBodyID, &spaz_part_, RigidBody::Type::kBody, + RigidBody::Shape::kSphere, + RigidBody::kCollideActive, RigidBody::kCollideAll); + body_head_->SetDimensions(0.23f, 0, 0, 0.28f, 0, 0, 1.0f); + body_head_->AddCallback(StaticCollideCallback, this); + + // Torso + body_torso_ = + Object::New(kTorsoBodyID, &spaz_part_, RigidBody::Type::kBody, + RigidBody::Shape::kSphere, + RigidBody::kCollideActive, RigidBody::kCollideAll); + body_torso_->SetDimensions(0.11f, 0, 0, 0.2f, 0, 0, 3.0f); + body_torso_->AddCallback(StaticCollideCallback, this); + + // Pelvis + body_pelvis_ = + Object::New(kPelvisBodyID, &spaz_part_, RigidBody::Type::kBody, + RigidBody::Shape::kBox, RigidBody::kCollideActive, + RigidBody::kCollideAll); + body_pelvis_->AddCallback(StaticCollideCallback, this); + + // Roller Ball + body_roller_ = Object::New( + kRollerBodyID, &roller_part_, RigidBody::Type::kBody, + RigidBody::Shape::kSphere, RigidBody::kCollideActive, + RigidBody::kCollideAll, nullptr, RigidBody::kIsRoller); + + body_roller_->SetDimensions(0.3f, 0, 0, 0, 0, 0, 0.1f); + body_roller_->AddCallback(StaticCollideCallback, this); + + // Stand Body + stand_body_ = + Object::New(kStandBodyID, &extras_part_, + RigidBody::Type::kBody, RigidBody::Shape::kSphere, + RigidBody::kCollideNone, RigidBody::kCollideNone); + dBodySetGravityMode(stand_body_->body(), 0); + stand_body_->SetDimensions(0.3f, 0, 0, 0, 0, 0, 1000.0f); + + // Upper Right Arm + upper_right_arm_body_ = + Object::New(kUpperRightArmBodyID, &limbs_part_upper_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + upper_right_arm_body_->AddCallback(StaticCollideCallback, this); + upper_right_arm_body_->SetDimensions(0.06f, 0.16f, 0, 0, 0, 0, + kUpperArmDensity); + + // Lower Right Arm + lower_right_arm_body_ = + Object::New(kLowerRightArmBodyID, &limbs_part_lower_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + lower_right_arm_body_->AddCallback(StaticCollideCallback, this); + lower_right_arm_body_->SetDimensions(0.06f, 0.13f, 0, 0.06f, 0.16f, 0, + kLowerArmDensity); + + // Upper Left Arm + upper_left_arm_body_ = + Object::New(kUpperLeftArmBodyID, &limbs_part_upper_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + upper_left_arm_body_->AddCallback(StaticCollideCallback, this); + upper_left_arm_body_->SetDimensions(0.06f, 0.16f, 0, 0, 0, 0, + kUpperArmDensity); + + // Lower Left Arm + lower_left_arm_body_ = + Object::New(kLowerLeftArmBodyID, &limbs_part_lower_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + lower_left_arm_body_->AddCallback(StaticCollideCallback, this); + lower_left_arm_body_->SetDimensions(0.06f, 0.13f, 0, 0.06f, 0.16f, 0, + kLowerArmDensity); + + // Upper Right Leg + upper_right_leg_body_ = + Object::New(kUpperRightLegBodyID, &limbs_part_upper_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + upper_right_leg_body_->AddCallback(StaticCollideCallback, this); + + // Lower Right leg + lower_right_leg_body_ = + Object::New(kLowerRightLegBodyID, &limbs_part_lower_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + lower_right_leg_body_->AddCallback(StaticCollideCallback, this); + + right_toes_body_ = + Object::New(kRightToesBodyID, &limbs_part_lower_, + RigidBody::Type::kBody, RigidBody::Shape::kSphere, + RigidBody::kCollideActive, RigidBody::kCollideAll); + right_toes_body_->AddCallback(StaticCollideCallback, this); + right_toes_body_->SetDimensions(0.075f, 0, 0, 0, 0, 0, kToesDensity); + + // Upper Left Leg + upper_left_leg_body_ = + Object::New(kUpperLeftLegBodyID, &limbs_part_upper_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + upper_left_leg_body_->AddCallback(StaticCollideCallback, this); + + // Lower Left leg + lower_left_leg_body_ = + Object::New(kLowerLeftLegBodyID, &limbs_part_lower_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideActive, RigidBody::kCollideAll); + lower_left_leg_body_->AddCallback(StaticCollideCallback, this); + + // Left Toes + left_toes_body_ = + Object::New(kLeftToesBodyID, &limbs_part_lower_, + RigidBody::Type::kBody, RigidBody::Shape::kSphere, + RigidBody::kCollideActive, RigidBody::kCollideAll); + left_toes_body_->AddCallback(StaticCollideCallback, this); + left_toes_body_->SetDimensions(0.075f, 0, 0, 0, 0, 0, kToesDensity); + + UpdateBodiesForStyle(); + + Stand(0, 0, 0, 0); + + // Attach head to torso. + neck_joint_ = CreateFixedJoint(body_head_.get(), body_torso_.get(), 1000, + 1, // linear stiff/damp + 20.0f, 0.3f); // angular stiff/damp + + // Drop the y angular stiffness/damping on our neck so our head can whip + // left/right a bit easier move connection point up away from torso a bit. + neck_joint_->anchor1[1] += 0.2f; + neck_joint_->anchor2[1] += 0.2f; + + // Attach torso to pelvis. + pelvis_joint_ = CreateFixedJoint(body_pelvis_.get(), body_torso_.get(), 0, + 0, // lin stiff/damp + 0, 0); // ang stiff/damp + + // Move anchor down a bit from torso towards pelvis. + pelvis_joint_->anchor1[1] -= 0.05f; + pelvis_joint_->anchor2[1] -= 0.05f; + + // Move anchor point forward a tiny bit (like the curvature of a spine). + pelvis_joint_->anchor2[2] += 0.05f; + + // Attach upper right arm to torso. + upper_right_arm_joint_ = + CreateFixedJoint(body_torso_.get(), upper_right_arm_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + // Move anchor to top of arm. + upper_right_arm_joint_->anchor2[2] = -0.1f; + + // Move anchor slightly in towards torso. + upper_right_arm_joint_->anchor2[0] += 0.02f; + + // Attach lower right arm to upper right arm. + lower_right_arm_joint_ = CreateFixedJoint(upper_right_arm_body_.get(), + lower_right_arm_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + lower_right_arm_joint_->anchor2[2] = -0.08f; + + // Attach upper left arm to torso. + upper_left_arm_joint_ = CreateFixedJoint( + body_torso_.get(), upper_left_arm_body_.get(), 0, 0, // linear stiff/damp + 0, 0); // Angular stiff/damp. + + // Move anchor to top of arm. + upper_left_arm_joint_->anchor2[2] = -0.1f; + + // Move anchor slightly in towards torso. + upper_left_arm_joint_->anchor2[0] += -0.02f; + + // Attach lower arm to upper arm. + lower_left_arm_joint_ = CreateFixedJoint(upper_left_arm_body_.get(), + lower_left_arm_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + lower_left_arm_joint_->anchor2[2] = -0.08f; + + // Attach upper right leg to leg-mass. + upper_right_leg_joint_ = + CreateFixedJoint(body_pelvis_.get(), upper_right_leg_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + upper_right_leg_joint_->anchor2[2] = -0.05f; + + // Attach lower right leg to upper right leg. + lower_right_leg_joint_ = CreateFixedJoint(upper_right_leg_body_.get(), + lower_right_leg_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + lower_right_leg_joint_->anchor2[2] = -0.05f; + + // Attach bottom of lower leg to pelvis. + right_leg_ik_joint_ = + CreateFixedJoint(body_pelvis_.get(), lower_right_leg_body_.get(), 0.3f, + 0.001f, // linear stiff/damp + 0, 0); // angular stiff/damp + dQFromAxisAndAngle(right_leg_ik_joint_->qrel, 1, 0, 0, 1.0f); + + // Move the anchor to the tip of our leg. + right_leg_ik_joint_->anchor2[2] = 0.05f; + + right_leg_ik_joint_->anchor1[0] = -0.1f; + right_leg_ik_joint_->anchor1[1] = -0.4f; + right_leg_ik_joint_->anchor1[2] = 0.0f; + + // Attach toes to lower right foot. + right_toes_joint_ = + CreateFixedJoint(lower_right_leg_body_.get(), right_toes_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + right_toes_joint_->anchor1[1] += -0.0f; + right_toes_joint_->anchor2[1] += -0.04f; + + // And an anchor off to the side to make it hinge-like. + right_toes_joint_2_ = nullptr; + right_toes_joint_2_ = + CreateFixedJoint(lower_right_leg_body_.get(), right_toes_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + right_toes_joint_2_->anchor1[1] += -0.0f; + right_toes_joint_2_->anchor2[1] += -0.04f; + + right_toes_joint_2_->anchor1[0] += -0.1f; + right_toes_joint_2_->anchor2[0] += -0.1f; + + // Attach upper left leg to leg-mass. + upper_left_leg_joint_ = + CreateFixedJoint(body_pelvis_.get(), upper_left_leg_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + upper_left_leg_joint_->anchor2[2] = -0.05f; + + // Attach lower left leg to upper left leg. + lower_left_leg_joint_ = CreateFixedJoint(upper_left_leg_body_.get(), + lower_left_leg_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + lower_left_leg_joint_->anchor2[2] = -0.05f; + + // Attach bottom of lower leg to pelvis. + left_leg_ik_joint_ = + CreateFixedJoint(body_pelvis_.get(), lower_left_leg_body_.get(), 0.3f, + 0.001f, // linear stiff/damp + 0, 0); // angular stiff/damp + + dQFromAxisAndAngle(left_leg_ik_joint_->qrel, 1, 0, 0, 1.0f); + + // Move the anchor to the tip of our leg. + left_leg_ik_joint_->anchor2[2] = 0.05f; + + left_leg_ik_joint_->anchor1[0] = 0.1f; + left_leg_ik_joint_->anchor1[1] = -0.4f; + left_leg_ik_joint_->anchor1[2] = 0.0f; + + // Attach toes to lower left foot. + left_toes_joint_ = + CreateFixedJoint(lower_left_leg_body_.get(), left_toes_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + right_toes_joint_->anchor1[1] += -0.0f; + left_toes_joint_->anchor2[1] += -0.04f; + + // And an anchor off to the side to make it hinge-like. + left_toes_joint_2_ = nullptr; + left_toes_joint_2_ = + CreateFixedJoint(lower_left_leg_body_.get(), left_toes_body_.get(), 0, + 0, // linear stiff/damp + 0, 0); // angular stiff/damp + + left_toes_joint_2_->anchor1[1] += -0.0f; + left_toes_joint_2_->anchor2[1] += -0.04f; + left_toes_joint_2_->anchor1[0] += 0.1f; + left_toes_joint_2_->anchor2[0] += 0.1f; + + // Attach end of right arm to torso. + right_arm_ik_joint_ = + CreateFixedJoint(body_torso_.get(), lower_right_arm_body_.get(), 0.0f, + 0.0f, // linear stiff/damp + 0, 0, // angular stiff/damp + -0.2f, -0.2f, 0.1f, // anchor1 + 0, 0, 0.07f, // anchor2 + false); + + left_arm_ik_joint_ = + CreateFixedJoint(body_torso_.get(), lower_left_arm_body_.get(), 0.0f, + 0.0f, // linear stiff/damp + 0, 0, // angular stiff/damp + 0.2f, -0.2f, 0.1f, // anchor1 + 0.0f, 0.0f, 0.07f, // anchor2 + false); + + // Roller ball joint. + roller_ball_joint_ = CreateFixedJoint(body_torso_.get(), body_roller_.get(), + kRollerBallLinearStiffness, + kRollerBallLinearDamping, 0, 0); + base_pelvis_roller_anchor_offset_ = roller_ball_joint_->anchor1[1]; + + // Stand joint on our torso. + stand_joint_ = + CreateFixedJoint(body_torso_.get(), stand_body_.get(), 100, 1, 200, 10); + + // Roller motor. + a_motor_roller_ = dJointCreateAMotor(scene->dynamics()->ode_world(), nullptr); + dJointAttach(a_motor_roller_, body_roller_->body(), nullptr); + dJointSetAMotorNumAxes(a_motor_roller_, 3); + dJointSetAMotorAxis(a_motor_roller_, 0, 0, 1, 0, 0); + dJointSetAMotorAxis(a_motor_roller_, 1, 0, 0, 1, 0); + dJointSetAMotorAxis(a_motor_roller_, 2, 0, 0, 0, 1); + dJointSetAMotorParam(a_motor_roller_, dParamFMax, 3.0f); + dJointSetAMotorParam(a_motor_roller_, dParamFMax2, 3.0f); + dJointSetAMotorParam(a_motor_roller_, dParamFMax3, 3.0f); + dJointSetAMotorParam(a_motor_roller_, dParamVel, 0.0f); + dJointSetAMotorParam(a_motor_roller_, dParamVel2, 0.0f); + dJointSetAMotorParam(a_motor_roller_, dParamVel3, 1.0f); + + // Attach brakes between our roller ball and our leg mass. + a_motor_brakes_ = dJointCreateAMotor(scene->dynamics()->ode_world(), nullptr); + dJointAttach(a_motor_brakes_, body_torso_->body(), body_roller_->body()); + dJointSetAMotorMode(a_motor_brakes_, dAMotorUser); + dJointSetAMotorNumAxes(a_motor_brakes_, 3); + dJointSetAMotorAxis(a_motor_brakes_, 0, 1, 1, 0, 0); + dJointSetAMotorAxis(a_motor_brakes_, 1, 1, 0, 1, 0); + dJointSetAMotorAxis(a_motor_brakes_, 2, 1, 0, 0, 1); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax, 10.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax2, 10.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax3, 10.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamVel, 0); + dJointSetAMotorParam(a_motor_brakes_, dParamVel2, 0); + dJointSetAMotorParam(a_motor_brakes_, dParamVel3, 0); + + // give joints initial vals + UpdateJoints(); + + // FIXME should do this on draw + UpdateForGraphicsQuality(g_graphics_server->quality()); + + // we want to have an area of interest by default.. + SetIsAreaOfInterest(true); + + // we want to update each step + BA_DEBUG_CHECK_BODIES(); +} + +void SpazNode::SetPickupPressed(bool val) { + if (val == pickup_pressed_) return; + pickup_pressed_ = val; + + // press + if (pickup_pressed_) { + if (frozen_ || knockout_) { + return; + } + if (holding_something_) { + Throw(false); + } else { + if ((pickup_ == 0) && (!knockout_) && (!frozen_)) + pickup_ = kPickupCooldown + 4; + } + } else { + // release + } +} + +void SpazNode::SetHoldPositionPressed(bool val) { + if (val == hold_position_pressed_) return; + hold_position_pressed_ = val; +} + +void SpazNode::SetMoveLeftRight(float val) { + if (val == move_left_right_) { + return; + } + move_left_right_ = val; + lr_ = static_cast_check_fit( + std::max(-127, std::min(127, static_cast(127.0f * val)))); +} + +void SpazNode::SetMoveUpDown(float val) { + if (val == move_up_down_) { + return; + } + move_up_down_ = val; + ud_ = static_cast_check_fit( + std::max(-127, std::min(127, static_cast(127.0f * val)))); +} + +void SpazNode::SetFlyPressed(bool val) { + if (val == fly_pressed_) return; + fly_pressed_ = val; + + // Press. + if (fly_pressed_) { + DoFlyPress(); + } else { + // Release. + } +} + +void SpazNode::SetRun(float val) { + if (val == run_) { + return; + } + run_ = val; +} + +void SpazNode::SetBombPressed(bool val) { + if (val == bomb_pressed_) { + return; + } + bomb_pressed_ = val; + if (bomb_pressed_) { + if (frozen_ || knockout_) { + return; + } + if (holding_something_) { + throwing_with_bomb_button_ = true; + Throw(true); + } + } else { + // Released. + } +} + +void SpazNode::SetPunchPressed(bool val) { + if (val == punch_pressed_) { + return; + } + punch_pressed_ = val; + if (punch_pressed_) { + if (frozen_ || knockout_) { + return; + } + + // If we're holding something, throw it. + if (holding_something_) { + Throw(false); + } else { + if (!holding_something_ && (!knockout_) && (!frozen_)) { + punch_ = kPunchDuration; + + // Left or right punch is determined by our spin. + if (std::abs(a_vel_y_smoothed_) < 0.3f) { + // At low rotational speeds lets do random. + punch_right_ = (RandomFloat() > 0.5f); + } else { + punch_right_ = a_vel_y_smoothed_ > 0.0f; + } + last_punch_time_ = scene()->time(); + if (Sound* sound = GetRandomMedia(attack_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + const dReal* p_head = dGeomGetPosition(body_head_->geom()); + g_audio->PushSourceStopSoundCall(voice_play_id_); + source->SetPosition(p_head[0], p_head[1], p_head[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + source->End(); + } + } + } + } + } else { + // Release. + } +} + +void SpazNode::SetJumpPressed(bool val) { + if (val == jump_pressed_) { + return; + } + jump_pressed_ = val; + if (jump_pressed_) { + if (!can_fly_ && !knockout_ && !frozen_) { + if (Sound* sound = GetRandomMedia(jump_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + const dReal* p_top = dGeomGetPosition(body_head_->geom()); + g_audio->PushSourceStopSoundCall(voice_play_id_); + source->SetPosition(p_top[0], p_top[1], p_top[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + source->End(); + } + } + if (demo_mode_) { + jump_ = 5; + } else { + jump_ = 7; + } + last_jump_time_ = scene()->time(); + } + jump_pressed_ = true; + } else { + // Release. + jump_pressed_ = false; + } +} + +static void FreezeJointAngle(JointFixedEF* j) { + dQMultiply1(j->qrel, j->node[0].body->q, j->node[1].body->q); +} + +void SpazNode::UpdateJoints() { + // (neck joint gets set every step so no update here) + + float l_still_scale = 1.0f; + float l_damp_scale = 1.0f; + float a_stiff_scale = 1.0f; + float a_damp_scale = 1.0f; + float leg_a_damp_scale = 1.0f; + + // When frozen, lock to our orientations and get more stiff. + if (frozen_) { + l_still_scale *= 5.0f; + l_damp_scale *= 0.2f; + a_stiff_scale *= 1000.0f; + a_damp_scale *= 0.2f; + leg_a_damp_scale *= 1.0f; + + FreezeJointAngle(pelvis_joint_); + FreezeJointAngle(upper_right_arm_joint_); + FreezeJointAngle(lower_right_arm_joint_); + FreezeJointAngle(upper_left_arm_joint_); + FreezeJointAngle(lower_left_arm_joint_); + FreezeJointAngle(upper_right_leg_joint_); + FreezeJointAngle(lower_right_leg_joint_); + FreezeJointAngle(upper_left_leg_joint_); + FreezeJointAngle(lower_left_leg_joint_); + FreezeJointAngle(right_toes_joint_); + FreezeJointAngle(left_toes_joint_); + if (hair_front_right_joint_) { + FreezeJointAngle(hair_front_right_joint_); + } + if (hair_front_left_joint_) { + FreezeJointAngle(hair_front_left_joint_); + } + if (hair_ponytail_top_joint_) { + FreezeJointAngle(hair_ponytail_top_joint_); + } + if (hair_ponytail_bottom_joint_) { + FreezeJointAngle(hair_ponytail_bottom_joint_); + } + } else { + // Not frozen; just normal setup. + + // Set normal joint angles. + + dQFromAxisAndAngle(pelvis_joint_->qrel, 1, 0.0f, 0.0f, -0.4f); + + dQFromAxisAndAngle(upper_right_arm_joint_->qrel, 1, 0.0f, -0.0f, 2.0f); + dQFromAxisAndAngle(lower_right_arm_joint_->qrel, 1, 0, 0, -1.7f); + + dQFromAxisAndAngle(upper_left_arm_joint_->qrel, 1, -0.0f, 0.0f, 2.0f); + dQFromAxisAndAngle(lower_left_arm_joint_->qrel, 1, 0, 0, -1.7f); + + dQFromAxisAndAngle(upper_right_leg_joint_->qrel, 1, 0.2f, 0.2f, 0.5f); + dQFromAxisAndAngle(lower_right_leg_joint_->qrel, 1, 0, 0, 1.0f); + dQSetIdentity(right_toes_joint_->qrel); + + dQFromAxisAndAngle(upper_left_leg_joint_->qrel, 1, -0.2f, -0.2f, 0.5f); + dQFromAxisAndAngle(lower_left_leg_joint_->qrel, 1, 0, 0, 3.1415f / 2.0f); + dQSetIdentity(left_toes_joint_->qrel); + } + + pelvis_joint_->linearStiffness = kPelvisLinearStiffness * l_still_scale; + pelvis_joint_->linearDamping = kPelvisLinearDamping * l_damp_scale; + pelvis_joint_->angularStiffness = kPelvisAngularStiffness * a_stiff_scale; + pelvis_joint_->angularDamping = kPelvisAngularDamping * a_damp_scale; + + upper_right_leg_joint_->linearStiffness = + kUpperLegLinearStiffness * l_still_scale; + upper_right_leg_joint_->linearDamping = kUpperLegLinearDamping * l_damp_scale; + upper_right_leg_joint_->angularStiffness = + kUpperLegAngularStiffness * a_stiff_scale; + upper_right_leg_joint_->angularDamping = + kUpperLegAngularDamping * a_damp_scale * leg_a_damp_scale; + + lower_right_leg_joint_->linearStiffness = + kLowerLegLinearStiffness * l_still_scale; + lower_right_leg_joint_->linearDamping = kLowerLegLinearDamping * l_damp_scale; + lower_right_leg_joint_->angularStiffness = + kLowerLegAngularStiffness * a_stiff_scale; + lower_right_leg_joint_->angularDamping = + kLowerLegAngularDamping * a_damp_scale * leg_a_damp_scale; + + right_toes_joint_->linearStiffness = kToesLinearStiffness * l_still_scale; + right_toes_joint_->linearDamping = kToesLinearDamping * l_damp_scale; + right_toes_joint_->angularStiffness = kToesAngularStiffness * a_stiff_scale; + right_toes_joint_->angularDamping = kToesAngularDamping * a_damp_scale; + + right_toes_joint_2_->linearStiffness = kToesLinearStiffness * l_still_scale; + right_toes_joint_2_->linearDamping = kToesLinearDamping * l_damp_scale; + right_toes_joint_2_->angularStiffness = 0; + right_toes_joint_2_->angularDamping = 0; + + upper_left_leg_joint_->linearStiffness = + kUpperLegLinearStiffness * l_still_scale; + upper_left_leg_joint_->linearDamping = kUpperLegLinearDamping * l_damp_scale; + upper_left_leg_joint_->angularStiffness = + kUpperLegAngularStiffness * a_stiff_scale; + upper_left_leg_joint_->angularDamping = + kUpperLegAngularDamping * a_damp_scale * leg_a_damp_scale; + + lower_left_leg_joint_->linearStiffness = + kLowerLegLinearStiffness * l_still_scale; + lower_left_leg_joint_->linearDamping = kLowerLegLinearDamping * l_damp_scale; + lower_left_leg_joint_->angularStiffness = + kLowerLegAngularStiffness * a_stiff_scale; + lower_left_leg_joint_->angularDamping = + kLowerLegAngularDamping * a_damp_scale * leg_a_damp_scale; + + left_toes_joint_->linearStiffness = kToesLinearStiffness * l_still_scale; + left_toes_joint_->linearDamping = kToesLinearDamping * l_damp_scale; + left_toes_joint_->angularStiffness = kToesAngularStiffness * a_stiff_scale; + left_toes_joint_->angularDamping = kToesAngularDamping * a_damp_scale; + + left_toes_joint_2_->linearStiffness = kToesLinearStiffness * l_still_scale; + left_toes_joint_2_->linearDamping = kToesLinearDamping * l_damp_scale; + left_toes_joint_2_->angularStiffness = 0; + left_toes_joint_2_->angularDamping = 0; + + // hair + if (hair_front_right_joint_) { + hair_front_right_joint_->linearStiffness = + kHairFrontRightLinearStiffness * l_still_scale; + hair_front_right_joint_->linearDamping = + kHairFrontRightLinearDamping * l_damp_scale; + hair_front_right_joint_->angularStiffness = + kHairFrontRightAngularStiffness * a_stiff_scale; + hair_front_right_joint_->angularDamping = + kHairFrontRightAngularDamping * a_damp_scale; + } + if (hair_front_left_joint_) { + hair_front_left_joint_->linearStiffness = + kHairFrontLeftLinearStiffness * l_still_scale; + hair_front_left_joint_->linearDamping = + kHairFrontLeftLinearDamping * l_damp_scale; + hair_front_left_joint_->angularStiffness = + kHairFrontLeftAngularStiffness * a_stiff_scale; + hair_front_left_joint_->angularDamping = + kHairFrontLeftAngularDamping * a_damp_scale; + } + if (hair_ponytail_top_joint_) { + hair_ponytail_top_joint_->linearStiffness = + kHairPonytailTopLinearStiffness * l_still_scale; + hair_ponytail_top_joint_->linearDamping = + kHairPonytailTopLinearDamping * l_damp_scale; + hair_ponytail_top_joint_->angularStiffness = + kHairPonytailTopAngularStiffness * a_stiff_scale; + hair_ponytail_top_joint_->angularDamping = + kHairPonytailTopAngularDamping * a_damp_scale; + } + if (hair_ponytail_bottom_joint_) { + hair_ponytail_bottom_joint_->linearStiffness = + kHairPonytailBottomLinearStiffness * l_still_scale; + hair_ponytail_bottom_joint_->linearDamping = + kHairPonytailBottomLinearDamping * l_damp_scale; + hair_ponytail_bottom_joint_->angularStiffness = + kHairPonytailBottomAngularStiffness * a_stiff_scale; + hair_ponytail_bottom_joint_->angularDamping = + kHairPonytailBottomAngularDamping * a_damp_scale; + } +} + +void SpazNode::UpdateBodiesForStyle() { + // Create hair bodies/joints if need be. + if (female_hair_) { + CreateHair(); + } else { + DestroyHair(); + } + + // Adjust torso size. + body_torso_->SetDimensions(torso_radius_, 0, 0, 0.2f, 0, 0, 3.0f); + + // Adjust hip and leg size. + body_pelvis_->SetDimensions(0.25f, 0.16f, 0.10f, 0.25f, 0.16f, 0.16f, + kPelvisDensity); + + float thigh_rad = female_ ? 0.06f : 0.04f; + upper_left_leg_body_->SetDimensions(thigh_rad, 0.12f, 0, 0.05f, 0.12f, 0, + kUpperLegDensity); + upper_right_leg_body_->SetDimensions(thigh_rad, 0.12f, 0, 0.05f, 0.12f, 0, + kUpperLegDensity); + + float ankle_rad = female_ ? 0.045f : 0.07f; + lower_left_leg_body_->SetDimensions(ankle_rad, 0.26f - ankle_rad * 2.0f, 0, + 0.07f, 0.12f, 0, kLowerLegDensity); + lower_right_leg_body_->SetDimensions(ankle_rad, 0.26f - ankle_rad * 2.0f, 0, + 0.07f, 0.12f, 0, kLowerLegDensity); +} + +static void InitObject(dObject* obj, dxWorld* w) { + obj->world = w; + obj->next = nullptr; + obj->tome = nullptr; + obj->userdata = nullptr; + obj->tag = 0; +} + +static void AddObjectToList(dObject* obj, dObject** first) { + obj->next = *first; + obj->tome = first; + if (*first) (*first)->tome = &obj->next; + (*first) = obj; +} +static void JointInit(dxWorld* w, dxJoint* j) { + dIASSERT(w && j); + InitObject(j, w); + j->vtable = nullptr; + j->flags = 0; + j->node[0].joint = j; + j->node[0].body = nullptr; + j->node[0].next = nullptr; + j->node[1].joint = j; + j->node[1].body = nullptr; + j->node[1].next = nullptr; + dSetZero(j->lambda, 6); + AddObjectToList(j, reinterpret_cast(&w->firstjoint)); + w->nj++; +} + +static void _dJointSetFixed(JointFixedEF* joint) { + dUASSERT(joint, "bad joint argument"); + dUASSERT(joint->vtable == &fixed_vtable_, "joint is not fixed"); + + // This code is taken from sJointSetSliderAxis(), we should really put the + // common code in its own function. + // compute the offset between the bodies + if (joint->node[0].body) { + if (joint->node[1].body) { + dQMultiply1(joint->qrel, joint->node[0].body->q, joint->node[1].body->q); + } else { + } + } +} + +static void _setAnchors(dxJoint* j, dReal x, dReal y, dReal z, dVector3 anchor1, + dVector3 anchor2) { + if (j->node[0].body) { + dReal q[4]; + q[0] = x - j->node[0].body->pos[0]; + q[1] = y - j->node[0].body->pos[1]; + q[2] = z - j->node[0].body->pos[2]; + q[3] = 0; + dMULTIPLY1_331(anchor1, j->node[0].body->R, q); + if (j->node[1].body) { + q[0] = x - j->node[1].body->pos[0]; + q[1] = y - j->node[1].body->pos[1]; + q[2] = z - j->node[1].body->pos[2]; + q[3] = 0; + dMULTIPLY1_331(anchor2, j->node[1].body->R, q); + } else { + anchor2[0] = x; + anchor2[1] = y; + anchor2[2] = z; + } + } + anchor1[3] = 0; + anchor2[3] = 0; +} + +// Position b relative to b2 based on. +void PositionBodyForJoint(JointFixedEF* j) { + dBodyID b1 = dJointGetBody(j, 0); + dBodyID b2 = dJointGetBody(j, 1); + assert(b1 && b2); + dBodySetQuaternion(b2, dBodyGetQuaternion(b1)); + dVector3 p; + dBodyGetRelPointPos(b1, j->anchor1[0] - j->anchor2[0], + j->anchor1[1] - j->anchor2[1], + j->anchor1[2] - j->anchor2[2], p); + dBodySetPosition(b2, p[0], p[1], p[2]); +} + +auto SpazNode::CreateFixedJoint(RigidBody* b1, RigidBody* b2, float ls, + float ld, float as, float ad) -> JointFixedEF* { + JointFixedEF* j; + j = static_cast( + dAlloc(static_cast(fixed_vtable_.size))); + JointInit(scene()->dynamics()->ode_world(), j); + j->vtable = &fixed_vtable_; + if (j->vtable->init) j->vtable->init(j); + j->feedback = nullptr; + + if (b1 && b2) { + dJointAttach(j, b1->body(), b2->body()); + _dJointSetFixed(j); + const dReal* p = dBodyGetPosition(b2->body()); + _setAnchors(j, p[0], p[1], p[2], j->anchor1, j->anchor2); + } + + j->linearStiffness = ls; + j->linearDamping = ld; + j->angularStiffness = as; + j->angularDamping = ad; + + return j; +} + +auto SpazNode::CreateFixedJoint(RigidBody* b1, RigidBody* b2, float ls, + float ld, float as, float ad, float a1x, + float a1y, float a1z, float a2x, float a2y, + float a2z, bool reposition) -> JointFixedEF* { + assert(b1 && b2); + + JointFixedEF* j; + j = static_cast( + dAlloc(static_cast(fixed_vtable_.size))); + JointInit(scene()->dynamics()->ode_world(), j); + j->vtable = &fixed_vtable_; + if (j->vtable->init) j->vtable->init(j); + j->feedback = nullptr; + + dJointAttach(j, b1->body(), b2->body()); + dQSetIdentity(j->qrel); + j->anchor1[0] = a1x; + j->anchor1[1] = a1y; + j->anchor1[2] = a1z; + j->anchor2[0] = a2x; + j->anchor2[1] = a2y; + j->anchor2[2] = a2z; + + // Ok lets move the second body to line up with the joint. + if (reposition) { + PositionBodyForJoint(j); + } + + j->linearStiffness = ls; + j->linearDamping = ld; + j->angularStiffness = as; + j->angularDamping = ad; + + return j; +} + +void SpazNode::UpdateAreaOfInterest() { + if (area_of_interest_) { + area_of_interest_->set_position( + Vector3f(dGeomGetPosition(body_head_->geom()))); + area_of_interest_->set_velocity( + Vector3f(dBodyGetLinearVel(body_head_->body()))); + area_of_interest_->SetRadius(area_of_interest_radius_); + } +} + +SpazNode::~SpazNode() { + // If we're holding something, tell that thing it's been dropped. + DropHeldObject(); + + if (area_of_interest_) { + g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + area_of_interest_ = nullptr; + } + + DestroyHair(); + + dJointDestroy(neck_joint_); + + dJointDestroy(upper_right_arm_joint_); + dJointDestroy(lower_right_arm_joint_); + dJointDestroy(upper_left_arm_joint_); + dJointDestroy(lower_left_arm_joint_); + + dJointDestroy(upper_right_leg_joint_); + dJointDestroy(lower_right_leg_joint_); + dJointDestroy(right_leg_ik_joint_); + dJointDestroy(upper_left_leg_joint_); + dJointDestroy(lower_left_leg_joint_); + dJointDestroy(left_leg_ik_joint_); + dJointDestroy(right_arm_ik_joint_); + dJointDestroy(left_arm_ik_joint_); + dJointDestroy(left_toes_joint_); + if (left_toes_joint_2_) { + dJointDestroy(left_toes_joint_2_); + } + dJointDestroy(right_toes_joint_); + if (right_toes_joint_2_) { + dJointDestroy(right_toes_joint_2_); + } + + dJointDestroy(pelvis_joint_); + dJointDestroy(roller_ball_joint_); + dJointDestroy(a_motor_brakes_); + dJointDestroy(stand_joint_); + dJointDestroy(a_motor_roller_); + + // stop any sounds that may be looping.. + if (tick_play_id_ != 0xFFFFFFFF) { + g_audio->PushSourceStopSoundCall(tick_play_id_); + } + if (voice_play_id_ != 0xFFFFFFFF) { + g_audio->PushSourceStopSoundCall(voice_play_id_); + } +} + +void SpazNode::ApplyTorque(float x, float y, float z) { + dBodyAddTorque(body_roller_->body(), x, y, z); +} + +// given coords within a (-1,-1) to (1,1) box, +// convert them such that their length is never greater than 1 +static void BoxNormalizeToCircle(float* lr, float* ud) { + if (std::abs((*lr)) < 0.0001f || std::abs((*ud)) < 0.0001f) { + return; // not worth doing anything + } + + // project them out to hit the border + float s; + if (std::abs((*lr)) > std::abs((*ud))) { + s = 1.0f / std::abs((*lr)); + } else { + s = 1.0f / std::abs((*ud)); + } + float proj_lr = (*lr) * s; + float proj_ud = (*ud) * s; + float proj_len = sqrtf(proj_lr * proj_lr + proj_ud * proj_ud); + float fin_scale = 1.0f / proj_len; + (*lr) *= fin_scale; + (*ud) *= fin_scale; +} + +static void BoxClampToCircle(float* lr, float* ud) { + float len_squared = (*lr) * (*lr) + (*ud) * (*ud); + if (len_squared > 1.0f) { + float len = sqrtf(len_squared); + float mult = 1.0f / len; + (*lr) *= mult; + (*ud) *= mult; + } +} + +void SpazNode::Throw(bool with_bomb_button) { + throwing_with_bomb_button_ = with_bomb_button; + + if (holding_something_ && !throwing_) { + throw_start_ = scene()->time(); + have_thrown_ = true; + + if (Sound* sound = GetRandomMedia(attack_sounds_)) { + if (AudioSource* s = g_audio->SourceBeginNew()) { + const dReal* p = dGeomGetPosition(body_head_->geom()); + g_audio->PushSourceStopSoundCall(voice_play_id_); + s->SetPosition(p[0], p[1], p[2]); + voice_play_id_ = s->Play(sound->GetSoundData()); + s->End(); + } + } + + // our throw can't actually start until we've held the thing + // for our min amount of time + float lrf = lr_smooth_; + float udf = ud_smooth_; + if (clamp_move_values_to_circle_) { + BoxClampToCircle(&lrf, &udf); + } else { + BoxNormalizeToCircle(&lrf, &udf); + } + + float scale = std::abs(sqrtf(lrf * lrf + udf * udf)); + throw_power_ = 0.8f * (0.6f + 0.4f * scale); + + // if we *just* picked it up, scale down our throw power slightly + // (otherwise we'll get an extra boost from the pick-up constraint and + // it'll fly farther than normal) + auto since_pick_up = static_cast(throw_start_ - last_pickup_time_); + if (since_pick_up < 500.0f) { + throw_power_ *= 0.4f + 0.6f * (since_pick_up / 500.0f); + } + + // lock in our throw direction.. otherwise it smooths out + // to the axes with dpads and we lose our fuzzy in-between aiming + + throw_lr_ = lr_smooth_; + throw_ud_ = ud_smooth_; + + // make ourself a note to drop the item as soon as possible with this power + throwing_ = true; + } +} + +void SpazNode::HandleMessage(const char* data_in) { + const char* data = data_in; + bool handled = true; + NodeMessageType type = extract_node_message_type(&data); + switch (type) { + case NodeMessageType::kScreamSound: { + if (dead_ || invincible_) break; + force_scream_ = true; + last_force_scream_time_ = scene()->time(); + break; + } + case NodeMessageType::kPickedUp: { + // lets instantly lose our balance in this case... + balance_ = 0; + break; + } + case NodeMessageType::kHurtSound: { + PlayHurtSound(); + break; + } + case NodeMessageType::kAttackSound: { + if (knockout_ || frozen_) { + break; + } + if (Sound* sound = GetRandomMedia(attack_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + const dReal* p_top = dGeomGetPosition(body_head_->geom()); + g_audio->PushSourceStopSoundCall(voice_play_id_); + source->SetPosition(p_top[0], p_top[1], p_top[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + source->End(); + } + } + break; + } + case NodeMessageType::kJumpSound: { + if (knockout_ || frozen_) { + break; + } + if (Sound* sound = GetRandomMedia(jump_sounds_)) { + if (AudioSource* s = g_audio->SourceBeginNew()) { + const dReal* p_top = dGeomGetPosition(body_head_->geom()); + g_audio->PushSourceStopSoundCall(voice_play_id_); + s->SetPosition(p_top[0], p_top[1], p_top[2]); + voice_play_id_ = s->Play(sound->GetSoundData()); + s->End(); + } + } + break; + } + case NodeMessageType::kKnockout: { + float amt = Utils::ExtractFloat16NBO(&data); + knockout_ = static_cast_check_fit( + std::min(40, std::max(static_cast(knockout_), + static_cast(amt * 0.07f)))); + trying_to_fly_ = false; + break; + } + case NodeMessageType::kCelebrate: { + int duration = Utils::ExtractInt16NBO(&data); + celebrate_until_time_left_ = celebrate_until_time_right_ = + scene()->time() + duration; + break; + } + case NodeMessageType::kCelebrateL: { + int duration = Utils::ExtractInt16NBO(&data); + celebrate_until_time_left_ = scene()->time() + duration; + break; + } + case NodeMessageType::kCelebrateR: { + int duration = Utils::ExtractInt16NBO(&data); + celebrate_until_time_right_ = scene()->time() + duration; + break; + } + case NodeMessageType::kImpulse: { + last_external_impulse_time_ = scene()->time(); + float dmg = 0.0f; + float px = Utils::ExtractFloat16NBO(&data); + float py = Utils::ExtractFloat16NBO(&data); + float pz = Utils::ExtractFloat16NBO(&data); + float vx = Utils::ExtractFloat16NBO(&data); + float vy = Utils::ExtractFloat16NBO(&data); + float vz = Utils::ExtractFloat16NBO(&data); + float mag = Utils::ExtractFloat16NBO(&data); + float velocity_mag = Utils::ExtractFloat16NBO(&data); + float radius = Utils::ExtractFloat16NBO(&data); + auto calc_force_only = static_cast(Utils::ExtractInt16NBO(&data)); + float force_dir_x = Utils::ExtractFloat16NBO(&data); + float force_dir_y = Utils::ExtractFloat16NBO(&data); + float force_dir_z = Utils::ExtractFloat16NBO(&data); + + // area of affect impulses apply to everything.. + if (radius > 0.0f) { + last_hit_was_punch_ = false; + float head_mag = + 5.0f + * body_head_->ApplyImpulse(px, py, pz, vx, vy, vz, force_dir_x, + force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += head_mag; + float torso_mag = body_torso_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += torso_mag; + float pelvis_mag = body_pelvis_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += pelvis_mag; + dmg += upper_right_arm_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += lower_right_arm_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += upper_left_arm_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += lower_left_arm_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += upper_right_leg_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += lower_right_leg_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += upper_left_leg_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += lower_left_leg_body_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + } else { + // single impulse.. + last_hit_was_punch_ = true; + const dReal* head_pos = dBodyGetPosition(body_head_->body()); + const dReal* torso_pos = dBodyGetPosition(body_torso_->body()); + const dReal* pelvis_pos = dBodyGetPosition(body_pelvis_->body()); + dVector3 to_head = {px - head_pos[0], py - head_pos[1], + pz - head_pos[2]}; + dVector3 to_torso = {px - torso_pos[0], py - torso_pos[1], + pz - torso_pos[2]}; + dVector3 to_pelvis = {px - pelvis_pos[0], py - pelvis_pos[1], + pz - pelvis_pos[2]}; + float to_head_length = dVector3Length(to_head); + float to_torso_length = dVector3Length(to_torso); + float to_pelvis_length = dVector3Length(to_pelvis); + if (to_head_length < to_torso_length + && to_head_length < to_pelvis_length) { + float head_mag = + 5.0f + * body_head_->ApplyImpulse(px, py, pz, vx, vy, vz, force_dir_x, + force_dir_y, force_dir_z, mag, + velocity_mag, radius, calc_force_only); + dmg += head_mag; + } else { + float torso_mag = + 5.0f + * body_torso_->ApplyImpulse( + px, py, pz, vx, vy, vz, force_dir_x, force_dir_y, force_dir_z, + mag, velocity_mag, radius, calc_force_only); + dmg += torso_mag; + } + } + + // store this in our damage attr so the user can know how much an impulse + // hurt us + damage_out_ = dmg; + + // also add it to our smoothed damage attr for things like body-explosions + if (!calc_force_only) { + damage_smoothed_ += dmg; + } + + // update knockout if we're applying this.. + if (!calc_force_only) { + knockout_ = static_cast_check_fit( + std::min(40, std::max(static_cast(knockout_), + static_cast(dmg * 0.02f) - 20))); + trying_to_fly_ = false; + } + break; + } + case NodeMessageType::kStand: { + float x = Utils::ExtractFloat16NBO(&data); + float y = Utils::ExtractFloat16NBO(&data); + float z = Utils::ExtractFloat16NBO(&data); + float angle = Utils::ExtractFloat16NBO(&data); + Stand(x, y, z, angle); + UpdatePartBirthTimes(); + break; + } + case NodeMessageType::kFooting: { + footing_ += Utils::ExtractInt8(&data); + trying_to_fly_ = false; + break; + } + case NodeMessageType::kKickback: { + float pos_x = Utils::ExtractFloat16NBO(&data); + float pos_y = Utils::ExtractFloat16NBO(&data); + float pos_z = Utils::ExtractFloat16NBO(&data); + float dir_x = Utils::ExtractFloat16NBO(&data); + float dir_y = Utils::ExtractFloat16NBO(&data); + float dir_z = Utils::ExtractFloat16NBO(&data); + float mag = Utils::ExtractFloat16NBO(&data); + Vector3f v = Vector3f(dir_x, dir_y, dir_z).Normalized() * mag; + dBodyID b = body_torso_->body(); + dBodyEnable(b); + dBodyAddForceAtPos(b, v.x, v.y, v.z, pos_x, pos_y, pos_z); + break; + } + case NodeMessageType::kFlash: { + flashing_ = 10; + break; + } + default: + handled = false; + break; + } + + if (!handled) { + Node::HandleMessage(data_in); + } +} + +void SpazNode::DoFlyPress() { + if (can_fly_ && !knockout_ && !frozen_) { + fly_power_ += 25.0f; + last_fly_time_ = scene()->time(); + trying_to_fly_ = true; + + // keep from doing too many sparkles.. + static millisecs_t last_sparkle_time = 0; + millisecs_t t = GetRealTime(); + if (t - last_sparkle_time > 200) { + last_sparkle_time = t; + AudioSource* s = g_audio->SourceBeginNew(); + if (s) { + const dReal* p_torso = dGeomGetPosition(body_torso_->geom()); + s->SetPosition(p_torso[0], p_torso[1], p_torso[2]); + s->SetGain(0.3f); + SystemSoundID s_id; + int r = rand() % 100; // NOLINT + if (r < 33) { + s_id = SystemSoundID::kSparkle; + } else if (r < 66) { + s_id = SystemSoundID::kSparkle2; + } else { + s_id = SystemSoundID::kSparkle3; + } + s->Play(g_media->GetSound(s_id)); + s->End(); + } + } + } +} + +// void SpazNode::update(uint32_t flags) { +void SpazNode::Step() { + BA_DEBUG_CHECK_BODIES(); + + // update our body blending values + { + Object::Ref* bodies[] = {&body_head_, + &body_torso_, + &body_pelvis_, + &body_roller_, + &stand_body_, + &upper_right_arm_body_, + &lower_right_arm_body_, + &upper_left_arm_body_, + &lower_left_arm_body_, + &upper_right_leg_body_, + &lower_right_leg_body_, + &upper_left_leg_body_, + &lower_left_leg_body_, + &left_toes_body_, + &right_toes_body_, + &hair_front_right_body_, + &hair_front_left_body_, + &hair_ponytail_top_body_, + &hair_ponytail_bottom_body_, + nullptr}; + + for (Object::Ref** body = bodies; *body != nullptr; body++) { + if (RigidBody* bodyptr = (**body).get()) { + bodyptr->UpdateBlending(); + } + } + } + + step_count_++; + + const dReal* p_head = dGeomGetPosition(body_head_->geom()); + const dReal* p_torso = dGeomGetPosition(body_torso_->geom()); + + bool running_fast = false; + + // if we're associated with a player, let the game know where that player is + // FIXME: this should simply be an attr connection established on the + // python layer... + if (source_player_.exists()) { + source_player_->SetPosition(Vector3f(p_torso)); + } + + // move our smoothed hurt value a short time after we get hit + if (scene()->time() - last_hurt_change_time_ > 400) { + if (hurt_smoothed_ < hurt_) { + hurt_smoothed_ = std::min(hurt_, hurt_smoothed_ + 0.03f); + } else { + hurt_smoothed_ = std::max(hurt_, hurt_smoothed_ - 0.03f); + } + } + + // update our smooth ud/lr vals + { + // lets use smoothing if all our input values are either -127, 0, or 127.. + // that implies that we're getting non-analog input where smoothing is + // useful to have. (so that we can throw bombs in non-axis-aligned + // directions, etc) + float smoothing; + if ((ud_ == -127 || ud_ == 0 || ud_ == 127) + && (lr_ == -127 || lr_ == 0 || lr_ == 127)) { + if (demo_mode_) { + smoothing = 0.9f; + } else { + smoothing = 0.5f; + } + } else { + smoothing = 0.0f; + } + ud_smooth_ = + smoothing * ud_smooth_ + + (1.0f - smoothing) + * (hold_position_pressed_ ? 0.0f + : ((static_cast(ud_) / 127.0f))); + lr_smooth_ = + smoothing * lr_smooth_ + + (1.0f - smoothing) + * (hold_position_pressed_ ? 0.0f + : ((static_cast(lr_) / 127.0f))); + } + // update our normalized values + { + float prev_ud = ud_norm_; + float prev_lr = lr_norm_; + + float this_ud_norm = + (hold_position_pressed_ ? 0.0f : ((static_cast(ud_) / 127.0f))); + float this_lr_norm = + (hold_position_pressed_ ? 0.0f : ((static_cast(lr_) / 127.0f))); + if (clamp_move_values_to_circle_) { + BoxClampToCircle(&this_lr_norm, &this_ud_norm); + } else { + BoxNormalizeToCircle(&this_lr_norm, &this_ud_norm); + } + + raw_lr_norm_ = this_lr_norm; + raw_ud_norm_ = this_ud_norm; + + // determine if we're running.. + running_ = ((run_ > 0.0f) && !hold_position_pressed_ && !holding_something_ + && !hockey_ && (std::abs(lr_) > 0 || std::abs(ud_) > 0) + && (!have_thrown_ || (scene()->time() - throw_start_ > 200))); + + if (running_) { + float run_target = sqrtf(run_); + float mag = (lr_smooth_ * lr_smooth_ + ud_smooth_ * ud_smooth_); + if (mag < 0.3f) { + run_target *= (mag / 0.3f); + } + float smoothing = run_target > run_gas_ ? 0.95f : 0.5f; + run_gas_ = smoothing * run_gas_ + (1.0f - smoothing) * run_target; + } else { + run_gas_ = std::max(0.0f, run_gas_ - 0.02f); // 120hz update + } + + if (holding_something_) + run_gas_ = std::max(0.0f, run_gas_ - 0.05f); // 120hz update + + if (!footing_) run_gas_ = std::max(0.0f, run_gas_ - 0.05f); + + // as we're running faster we simply filter our input values to prevent fast + // adjustments + + if (run_ > 0.05f) { + // strip out any component of the vector that is more than 90 degrees off + // of our current direction.. otherwise, extreme opposite directions will + // have a minimal effect on our actual run direction (a run dir blended + // with its 180-degree opposite then re-normalized won't really change) + { + dVector3 cur_dir = {ud_norm_, lr_norm_, 0}; + dVector3 new_dir = {this_ud_norm, this_lr_norm, 0}; + float dot = dDOT(new_dir, cur_dir); + if (dot < 0.0f) { + this_ud_norm -= run_gas_ * (ud_norm_ * dot); + this_lr_norm -= run_gas_ * (lr_norm_ * dot); + if (this_ud_norm == 0.0f) this_ud_norm = -0.001f; + if (this_lr_norm == 0.0f) this_lr_norm = -0.001f; + } + } + float orig_len, target_len; + float this_ud_norm_norm = this_ud_norm; + float this_lr_norm_norm = this_lr_norm; + { + // push our input towards a length of 1 if we're holding down the gas + orig_len = sqrtf(this_ud_norm_norm * this_ud_norm_norm + + this_lr_norm_norm * this_lr_norm_norm); + target_len = run_gas_ * 1.0f + (1.0f - run_gas_) * orig_len; + float mult = orig_len == 0.0f ? 1.0f : target_len / orig_len; + this_ud_norm_norm *= mult; + this_lr_norm_norm *= mult; + } + + const dReal* vel = dBodyGetLinearVel(body_torso_->body()); + dVector3 v = {vel[0], vel[1], vel[2]}; + float speed = dVector3Length(v); + + // we use this later for looking angry and stuff.. + if (speed >= 5.0f) running_fast = true; + // float smoothing = 0.97f; + + float smoothing = 0.975f * (0.9f + 0.1f * run_gas_); // change for 120hz + if (speed < 2.0f) smoothing *= (speed / 2.0f); + + // blend it with previous results but then re-normalize + // (we want to prevent sudden direction changes but keep it + // full-speed-ahead) + ud_norm_ = smoothing * ud_norm_ + (1.0f - smoothing) * this_ud_norm_norm; + lr_norm_ = smoothing * lr_norm_ + (1.0f - smoothing) * this_lr_norm_norm; + // ..and renormalize + float new_len = sqrtf(ud_norm_ * ud_norm_ + lr_norm_ * lr_norm_); + float mult = new_len == 0.0f ? 1.0f : target_len / new_len; + ud_norm_ *= mult; + lr_norm_ *= mult; + } else { + // not running.. can save some calculations.. + ud_norm_ = this_ud_norm; + lr_norm_ = this_lr_norm; + } + + // a sharper one for walking + float smoothing_diff = 0.93f; + ud_diff_smooth_ = smoothing_diff * ud_diff_smooth_ + + (1.0f - smoothing_diff) * (ud_norm_ - prev_ud); + lr_diff_smooth_ = smoothing_diff * lr_diff_smooth_ + + (1.0f - smoothing_diff) * (lr_norm_ - prev_lr); + + // a softer one for running + float smoothering_diff = 0.983f; + ud_diff_smoother_ = smoothering_diff * ud_diff_smoother_ + + (1.0f - smoothering_diff) * (ud_norm_ - prev_ud); + lr_diff_smoother_ = smoothering_diff * lr_diff_smoother_ + + (1.0f - smoothering_diff) * (lr_norm_ - prev_lr); + } + + float vel_length; + + // update smoothed avels and stuff + { + float avel = dBodyGetAngularVel(body_torso_->body())[1]; + float smoothing = 0.7f; + a_vel_y_smoothed_ = + smoothing * a_vel_y_smoothed_ + (1.0f - smoothing) * avel; + smoothing = 0.92f; + a_vel_y_smoothed_more_ = + smoothing * a_vel_y_smoothed_more_ + (1.0f - smoothing) * avel; + + float abs_a_vel = std::min(25.0f, std::abs(avel)); + + // angular punch momentum.. this goes up as we spin fast + punch_momentum_angular_d_ += abs_a_vel * 0.0004f; + punch_momentum_angular_d_ *= + 0.965f; // so our up/down rate tops off at some point.. + punch_momentum_angular_ += punch_momentum_angular_d_; + punch_momentum_angular_ *= + 0.92f; // so our absolute val tops off at some point... + + // drop down fast if we're spinning slower than 10.. + if (abs_a_vel < 5.0f) { + punch_momentum_angular_ *= 0.8f + 0.2f * (abs_a_vel / 5.0f); + } + + const dReal* vel = dBodyGetLinearVel(body_torso_->body()); + vel_length = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); + + punch_momentum_linear_d_ += vel_length * 0.004f; + punch_momentum_linear_d_ *= 0.95f; // suppress rate of upward change + punch_momentum_linear_ += punch_momentum_linear_d_; + punch_momentum_linear_ *= 0.96f; // suppress absolute value + if (vel_length < 5.0f) { + punch_momentum_linear_ *= 0.9f + 0.1f * (vel_length / 5.0f); + } + + millisecs_t since_last_punch = scene()->time() - last_punch_time_; + if (since_last_punch < 200) { + punch_power_ = (0.5f + + 0.5f + * (sinf((static_cast(since_last_punch) / 200) + * (2.0f * 3.1415f) + - (3.14159f * 0.5f)))); + // Let's go between 0.5f and 1 so there's a bit less variance. + punch_power_ = 0.7f + 0.3f * punch_power_; + } else { + punch_power_ = 0.0f; + } + } + + // Update shadows. +#if !BA_HEADLESS_BUILD + FullShadowSet* full_shadows = full_shadow_set_.get(); + if (full_shadows) { + full_shadows->torso_shadow_.SetPosition( + Vector3f(dBodyGetPosition(body_torso_->body()))); + full_shadows->head_shadow_.SetPosition( + Vector3f(dBodyGetPosition(body_head_->body()))); + full_shadows->pelvis_shadow_.SetPosition( + Vector3f(dBodyGetPosition(body_pelvis_->body()))); + full_shadows->lower_left_leg_shadow_.SetPosition( + Vector3f(dBodyGetPosition(lower_left_leg_body_->body()))); + full_shadows->lower_right_leg_shadow_.SetPosition( + Vector3f(dBodyGetPosition(lower_right_leg_body_->body()))); + full_shadows->upper_left_leg_shadow_.SetPosition( + Vector3f(dBodyGetPosition(upper_left_leg_body_->body()))); + full_shadows->upper_right_leg_shadow_.SetPosition( + Vector3f(dBodyGetPosition(upper_right_leg_body_->body()))); + full_shadows->lower_right_arm_shadow_.SetPosition( + Vector3f(dBodyGetPosition(lower_right_arm_body_->body()))); + full_shadows->upper_right_arm_shadow_.SetPosition( + Vector3f(dBodyGetPosition(upper_right_arm_body_->body()))); + full_shadows->lower_left_arm_shadow_.SetPosition( + Vector3f(dBodyGetPosition(lower_left_arm_body_->body()))); + full_shadows->upper_left_arm_shadow_.SetPosition( + Vector3f(dBodyGetPosition(upper_left_arm_body_->body()))); + } else { + SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + assert(simple_shadows); + simple_shadows->shadow_.SetPosition( + Vector3f(dBodyGetPosition(body_pelvis_->body()))); + } +#endif // !BA_HEADLESS_BUILD + + // Update wings if we've got 'em. + if (wings_) { + float maxDist = 0.8f; + Vector3f p_wing_l = {0.0f, 0.0f, 0.0f}; + Vector3f p_wing_r = {0.0f, 0.0f, 0.0f}; + float x, y, z; + millisecs_t cur_time = scene()->time(); + + // Left wing. + if ((flapping_ || jump_ > 0 || running_) && !frozen_ && !knockout_) { + flap_ = (cur_time % 200 < 100); + } + if (flap_) { + x = kWingAttachX; + y = kWingAttachY; + z = kWingAttachZ; + } else { + x = kWingAttachFlapX; + y = kWingAttachFlapY; + z = kWingAttachFlapZ; + } + dBodyGetRelPointPos(body_torso_->body(), x, y, z, p_wing_l.v); + Vector3f diff = (p_wing_l - wing_pos_left_); + if (diff.LengthSquared() > maxDist * maxDist) { + diff *= (maxDist / diff.Length()); + } + wing_vel_left_ += diff * 0.03f; + wing_vel_left_ *= 0.93f; + wing_pos_left_ += wing_vel_left_; + + // Right wing. + dBodyGetRelPointPos(body_torso_->body(), -x, y, z, p_wing_r.v); + diff = (p_wing_r - wing_pos_right_); + if (diff.LengthSquared() > maxDist * maxDist) { + diff *= (maxDist / diff.Length()); + } + + // Use slightly different values from left for some variation. + wing_vel_right_ += diff * 0.036f; + wing_vel_right_ *= 0.95f; + wing_pos_right_ += wing_vel_right_; + } + + // Toggle angular components of some joints off and on for increased + // efficiency 93 to 123. + + // Always on for punches or frozen. + bool always_on = (frozen_ || (scene()->time() - last_punch_time_ < 500)); + + if (always_on) { + upper_left_arm_joint_->angularEnabled = true; + upper_right_arm_joint_->angularEnabled = true; + lower_right_arm_joint_->angularEnabled = true; + lower_left_arm_joint_->angularEnabled = true; + + upper_right_leg_joint_->angularEnabled = true; + upper_left_leg_joint_->angularEnabled = true; + lower_right_leg_joint_->angularEnabled = true; + lower_left_leg_joint_->angularEnabled = true; + + right_toes_joint_->angularEnabled = true; + left_toes_joint_->angularEnabled = true; + + left_toes_joint_2_->linearEnabled = true; + right_toes_joint_2_->linearEnabled = true; + } else { + int64_t t = scene()->stepnum(); + + upper_left_arm_joint_->angularEnabled = (t % 2 == 0); + upper_right_arm_joint_->angularEnabled = (t % 2 == 1); + lower_right_arm_joint_->angularEnabled = (t % 2 == 1); + lower_left_arm_joint_->angularEnabled = (t % 2 == 0); + + upper_right_leg_joint_->angularEnabled = (t % 2 == 0); + upper_left_leg_joint_->angularEnabled = (t % 2 == 1); + lower_right_leg_joint_->angularEnabled = (t % 2 == 1); + lower_left_leg_joint_->angularEnabled = (t % 2 == 0); + + right_toes_joint_->angularEnabled = (t % 2 == 0); + left_toes_joint_->angularEnabled = (t % 2 == 1); + + left_toes_joint_2_->linearEnabled = (t % 3 == 0); + right_toes_joint_2_->linearEnabled = (t % 3 == 2); + } + + // Update our limb-self-collide value. + // In certain cases (such as slowly walking in a straight line) + // we can completely skip collision tests between ourself with no + // real visual difference. This is a nice efficiency boost. + + // (Turned this off at some point; don't remember why.) + // We inch self-collide down if we're moving steadily, not turning too + // fast, and not hurt or holding stuff. + // if (vel_length > 1.0f + // and (std::abs(lr_smooth_) > 0.5f or std::abs(ud_smooth_) > 0.5f)) { + // limb_self_collide_ -= 0.01f; + // } else { + // limb_self_collide_ += 0.1f; + // } + + // if (std::abs(_aVelYSmoothed) > 5.0f) limb_self_collide_ += 0.2f; + // if (knockout_ != 0 or holding_something_) limb_self_collide_ += 0.1f; + // limb_self_collide_ = std::min(1.0f,std::max(0.0f,limb_self_collide_)); + + // Keep track of how long we're off the ground. + if (footing_) { + fly_time_ = 0; + } else { + fly_time_++; + } + + // If we're not touching the ground and are moving fast enough, we can cause + // damage to things we hit. + { + const dReal* lVel = dBodyGetLinearVel(body_torso_->body()); + float mag_squared = + lVel[0] * lVel[0] + lVel[1] * lVel[1] + lVel[2] * lVel[2]; + bool can_damage = (mag_squared > 20 && fly_time_ > 60); + body_torso_->set_can_cause_impact_damage(can_damage); + body_pelvis_->set_can_cause_impact_damage(can_damage); + body_head_->set_can_cause_impact_damage(can_damage); + } + + // Make sure none of our bodies are spinning/moving too fast. + { + float max_mag_squared = 400.0f; + float max_mag_squared_lin = 300.0f; + + // Shattering frozen dudes always looks too fast. Let's keep it down. + if (frozen_ && shattered_) { + max_mag_squared_lin = 100.0f; + } + + dBodyID bodies[11]; + bodies[0] = body_head_->body(); + bodies[1] = body_torso_->body(); + bodies[2] = upper_right_arm_body_->body(); + bodies[3] = lower_right_arm_body_->body(); + bodies[4] = upper_left_arm_body_->body(); + bodies[5] = lower_left_arm_body_->body(); + bodies[6] = upper_right_leg_body_->body(); + bodies[7] = upper_left_leg_body_->body(); + bodies[8] = lower_right_leg_body_->body(); + bodies[9] = lower_left_leg_body_->body(); + bodies[10] = nullptr; + + for (dBodyID* body = bodies; *body != nullptr; body++) { + const dReal* aVel = dBodyGetAngularVel(*body); + float mag_squared = + aVel[0] * aVel[0] + aVel[1] * aVel[1] + aVel[2] * aVel[2]; + if (mag_squared > max_mag_squared) { + float scale = max_mag_squared / mag_squared; + dBodySetAngularVel(*body, aVel[0] * scale, aVel[1] * scale, + aVel[2] * scale); + } + const dReal* lVel = dBodyGetLinearVel(*body); + mag_squared = lVel[0] * lVel[0] + lVel[1] * lVel[1] + lVel[2] * lVel[2]; + if (mag_squared > max_mag_squared_lin) { + float scale = max_mag_squared_lin / mag_squared; + dBodySetLinearVel(*body, lVel[0] * scale, lVel[1] * scale, + lVel[2] * scale); + } + } + + { + // If we've got hair bodies, apply a wee bit of drag to them so it looks + // cool when we run + Object::Ref* bodies2[] = { + &hair_front_right_body_, &hair_front_left_body_, + &hair_ponytail_top_body_, &hair_ponytail_bottom_body_, nullptr}; + float drag = 0.94f; + for (Object::Ref** body = bodies2; *body != nullptr; body++) { + if ((**body).exists()) { + dBodyID b = (**body)->body(); + const dReal* lVel = dBodyGetLinearVel(b); + dBodySetLinearVel(b, lVel[0] * drag, lVel[1] * drag, lVel[2] * drag); + } + } + } + } + + // Update jolt stuff. If our head jolts suddenly we may knock ourself out for + // a bit or may shatter. + { + const dReal* head_vel = dBodyGetLinearVel(body_head_->body()); + + // TODO(ericf): average our jolt-head-vel towards the current vel a bit for + // smoothing. + dVector3 diff; + diff[0] = head_vel[0] - jolt_head_vel_[0]; + diff[1] = head_vel[1] - jolt_head_vel_[1]; + diff[2] = head_vel[2] - jolt_head_vel_[2]; + dReal len = dVector3Length(diff); + jolt_head_vel_[0] = head_vel[0]; + jolt_head_vel_[1] = head_vel[1]; + jolt_head_vel_[2] = head_vel[2]; + + millisecs_t cur_time = scene()->time(); + + // If we're jolting and have just been touched in the head and haven't been + // pushed on by anything external recently (explosion, punch, etc), lets add + // some shock damage to ourself. + if (len > 3.0f && cur_time - last_pickup_time_ >= 500 + && cur_time - last_head_collide_time_ <= 30 + && cur_time - last_external_impulse_time_ >= 300 + && cur_time - last_impact_damage_dispatch_time_ > 500) { + impact_damage_accum_ += len - 3.0f; + } else if (impact_damage_accum_ > 0.0f) { + // If we're no longer adding damage but have accumulated some, lets + // dispatch it. + DispatchImpactDamageMessage(impact_damage_accum_); + impact_damage_accum_ = 0.0f; + last_impact_damage_dispatch_time_ = cur_time; + } + + // Make it difficult (but not impossible) to shatter within the first second + // (so we hopefully survive falling over). + float shatter_len; + if (cur_time - last_shatter_test_time_ < 1000) { + shatter_len = 8.0f; + } else { + shatter_len = 2.0f; + } + + if (frozen_ && len > shatter_len) { + last_shatter_test_time_ = cur_time; + DispatchShouldShatterMessage(); + } + } + + bool head_turning = false; + + // If we're punching. + millisecs_t scenetime = scene()->time(); + millisecs_t since_last_punch = scenetime - last_punch_time_; + + // Breathing when not moving. + float breath = 0.0f; + if (!dead_ && !shattered_ && (hold_position_pressed_ || (!ud_ && !lr_))) { + breath = sinf(static_cast(scenetime) * 0.005f); + } + + // If we're shattered we just make sure our joints are ineffective. + if (shattered_) { + JointFixedEF* joints[20]; + + // Fill in our broken joints. + { + JointFixedEF** j = joints; + + *j = right_leg_ik_joint_; + j++; + *j = left_leg_ik_joint_; + j++; + *j = right_arm_ik_joint_; + j++; + *j = left_arm_ik_joint_; + j++; + if (shatter_damage_ & kUpperRightArmJointBroken) { + *j = upper_right_arm_joint_; + j++; + } + if (shatter_damage_ & kLowerRightArmJointBroken) { + *j = lower_right_arm_joint_; + j++; + } + if (shatter_damage_ & kUpperLeftArmJointBroken) { + *j = upper_left_arm_joint_; + j++; + } + if (shatter_damage_ & kLowerLeftArmJointBroken) { + *j = lower_left_arm_joint_; + j++; + } + if (shatter_damage_ & kUpperLeftLegJointBroken) { + *j = upper_left_leg_joint_; + j++; + } + if (shatter_damage_ & kLowerLeftLegJointBroken) { + *j = lower_left_leg_joint_; + j++; + } + if (shatter_damage_ & kUpperRightLegJointBroken) { + *j = upper_right_leg_joint_; + j++; + } + if (shatter_damage_ & kLowerRightLegJointBroken) { + *j = lower_right_leg_joint_; + j++; + } + if (shatter_damage_ & kNeckJointBroken) { + *j = neck_joint_; + j++; + } + if (shatter_damage_ & kPelvisJointBroken) { + *j = pelvis_joint_; + j++; + } + *j = nullptr; + } + + for (JointFixedEF** j = joints; *j != nullptr; j++) + (**j).linearStiffness = (**j).linearDamping = (**j).angularStiffness = + (**j).angularDamping = 0.0f; + + } else { + // Not shattered; do normal stuff. + + // Adjust neck strength. + { + JointFixedEF* j = neck_joint_; + if (j) { + if (knockout_) { + j->linearStiffness = 400.0f; + j->linearDamping = 1.0f; + j->angularStiffness = 5.0f; + j->angularDamping = 0.3f; + } else { + j->linearStiffness = 500.0f; + j->linearDamping = 1.0f; + j->angularStiffness = 13.0f; + j->angularDamping = 0.8f; + } + } + } + + // Update legs. + { + // Whether our feet are following the run ball or just hanging free. + if (knockout_ || balance_ == 0 || frozen_) { + // flail our legs when airborn and alive + if (!footing_ && balance_ == 0 && !dead_) { + left_leg_ik_joint_->linearStiffness = kRunJointLinearStiffness * 0.4f; + left_leg_ik_joint_->linearDamping = kRunJointLinearDamping * 0.2f; + left_leg_ik_joint_->angularStiffness = + kRunJointAngularStiffness * 0.2f; + left_leg_ik_joint_->angularDamping = kRunJointAngularDamping * 0.2f; + right_leg_ik_joint_->linearStiffness = + kRunJointLinearStiffness * 0.4f; + right_leg_ik_joint_->linearDamping = kRunJointLinearDamping * 0.2f; + right_leg_ik_joint_->angularStiffness = + kRunJointAngularStiffness * 0.2f; + right_leg_ik_joint_->angularDamping = kRunJointAngularDamping * 0.2f; + roll_amt_ -= 0.2f; + if (roll_amt_ < (-2.0f * 3.141592f)) { + roll_amt_ += 2.0f * 3.141592f; + } + float x = 0.1f; + float y = -0.3f; + float z = 0.22f * cosf(roll_amt_); + left_leg_ik_joint_->anchor1[0] = x; + left_leg_ik_joint_->anchor1[1] = y; + left_leg_ik_joint_->anchor1[2] = z; + right_leg_ik_joint_->anchor1[0] = -x; + right_leg_ik_joint_->anchor1[1] = y; + right_leg_ik_joint_->anchor1[2] = -z; + } else { + // we're frozen or knocked out; turn off run-joint connections... + left_leg_ik_joint_->linearStiffness = 0.0f; + left_leg_ik_joint_->linearDamping = 0.0f; + left_leg_ik_joint_->angularStiffness = 0.0f; + left_leg_ik_joint_->angularDamping = 0.0f; + right_leg_ik_joint_->linearStiffness = 0.0f; + right_leg_ik_joint_->linearDamping = 0.0f; + right_leg_ik_joint_->angularStiffness = 0.0f; + right_leg_ik_joint_->angularDamping = 0.0f; + } + } else { + // Do normal running updates. + + // In hockey mode lets transfer a bit of our momentum to the direction + // we're facing if our skates are on the ground. + if (hockey_ && footing_) { + const dReal* rollVel = dBodyGetLinearVel(body_roller_->body()); + + dVector3 rollVelNorm = {rollVel[0], rollVel[1], rollVel[2]}; + dNormalize3(rollVelNorm); + + dVector3 forward; + dBodyVectorToWorld(stand_body_->body(), 0, 0, 1, forward); + + float dot = dDOT(rollVelNorm, forward); + + float mag = -6.0f * std::abs(dot); + + dVector3 f = {mag * rollVel[0], mag * rollVel[1], mag * rollVel[2]}; + float fMag = dVector3Length(f); + + if (dot < 0.0f) fMag *= -1.0f; // if we're going backwards.. + + dBodyAddForce(body_roller_->body(), f[0], f[1], f[2]); + dBodyAddForce(body_roller_->body(), forward[0] * fMag, + forward[1] * fMag, forward[2] * fMag); + } + + left_leg_ik_joint_->linearStiffness = kRunJointLinearStiffness; + left_leg_ik_joint_->linearDamping = kRunJointLinearDamping; + left_leg_ik_joint_->angularStiffness = kRunJointAngularStiffness; + left_leg_ik_joint_->angularDamping = kRunJointAngularDamping; + right_leg_ik_joint_->linearStiffness = kRunJointLinearStiffness; + right_leg_ik_joint_->linearDamping = kRunJointLinearDamping; + right_leg_ik_joint_->angularStiffness = kRunJointAngularStiffness; + right_leg_ik_joint_->angularDamping = kRunJointAngularDamping; + + // Tighten things up for running. + left_leg_ik_joint_->linearStiffness *= + 2.0f * run_gas_ + (1.0f - run_gas_) * 1.0f; + left_leg_ik_joint_->linearDamping *= + 2.0f * run_gas_ + (1.0f - run_gas_) * 1.0f; + right_leg_ik_joint_->linearStiffness *= + 2.0f * run_gas_ + (1.0f - run_gas_) * 1.0f; + right_leg_ik_joint_->linearDamping *= + 2.0f * run_gas_ + (1.0f - run_gas_) * 1.0f; + + if (hockey_) { + if (hold_position_pressed_ || (!ud_ && !lr_)) { + left_leg_ik_joint_->linearStiffness *= 0.05f; + left_leg_ik_joint_->linearDamping *= 0.1f; + left_leg_ik_joint_->angularStiffness *= 0.05f; + left_leg_ik_joint_->angularDamping *= 0.1f; + right_leg_ik_joint_->linearStiffness *= 0.05f; + right_leg_ik_joint_->linearDamping *= 0.1f; + right_leg_ik_joint_->angularStiffness *= 0.05f; + right_leg_ik_joint_->angularDamping *= 0.1f; + } + } + + const dReal* ballAVel = dBodyGetAngularVel(body_roller_->body()); + const dReal aVelMag = + sqrtf(ballAVel[0] * ballAVel[0] + ballAVel[1] * ballAVel[1] + + ballAVel[2] * ballAVel[2]); + + // When we're stopped, press our feet downward. + float speed_stretch = std::min( + sqrtf(lr_norm_ * lr_norm_ + ud_norm_ * ud_norm_) * 2.0f, 1.0f); + + float rollScale = hockey_ ? 0.6f : 1.0f; + + // Push towards 0.8f when running. + rollScale = run_gas_ * 0.8f + (1.0f - run_gas_) * rollScale; + + // Clamp extremely low values so noise doesnt keep our feet moving + roll_amt_ -= rollScale * 0.021f * std::max(aVelMag - 0.1f, 0.0f); + + if (roll_amt_ < (-2.0f * 3.141592f)) { + roll_amt_ += 2.0f * 3.141592f; + } + + // We move our feet in a circle that is calculated + // relative to our stand-body; *not* our pelvis. + // this way our pelvis is free to sway and rotate and stuff + // in response to our feet without affecting their target arcs + + // LEFT LEG + float step_separation = female_ ? 0.03f : 0.08f; + if (ninja_) { + step_separation *= 0.7f; + } + { + // Take a point relative to stand-body and then find it in the space + // of our pelvis. *that* is our attach point for the constraint. + dVector3 p_world; + dVector3 p_pelvis; + float y = -0.4f + speed_stretch * 0.14f * sinf(roll_amt_) + + (1.0f - speed_stretch) * -0.2f; + if (jump_ > 0) y -= 0.3f; + float z = 0.22f * cosf(roll_amt_); + y += 0.06f * run_gas_; + z *= 1.4f * run_gas_ + (1.0f - run_gas_) * 1.0f; + dBodyGetRelPointPos(stand_body_->body(), step_separation, y, z, + p_world); + assert(body_pelvis_.exists()); + dBodyGetPosRelPoint(body_pelvis_->body(), p_world[0], p_world[1], + p_world[2], p_pelvis); + left_leg_ik_joint_->anchor1[0] = p_pelvis[0]; + left_leg_ik_joint_->anchor1[1] = p_pelvis[1]; + left_leg_ik_joint_->anchor1[2] = p_pelvis[2]; + } + // RIGHT LEG + { + // Take a point relative to stand-body and then find it in the space + // of our pelvis. *that* is our attach point for the constraint. + dVector3 p_world; + dVector3 p_pelvis; + float y = -0.4f + speed_stretch * 0.14f * -sinf(roll_amt_) + + (1.0f - speed_stretch) * -0.2f; + if (jump_ > 0) y -= 0.3f; + float z = 0.22f * -cosf(roll_amt_); + y += 0.05f * run_gas_; + z *= 1.3f * run_gas_ + (1.0f - run_gas_) * 1.0f; + dBodyGetRelPointPos(stand_body_->body(), -step_separation, y, z, + p_world); + assert(body_pelvis_.exists()); + dBodyGetPosRelPoint(body_pelvis_->body(), p_world[0], p_world[1], + p_world[2], p_pelvis); + right_leg_ik_joint_->anchor1[0] = p_pelvis[0]; + right_leg_ik_joint_->anchor1[1] = p_pelvis[1]; + right_leg_ik_joint_->anchor1[2] = p_pelvis[2]; + } + } + + // Arms. + { + // Adjust our joint strengths. + { + float l_still_scale = 1.0f; + float l_damp_scale = 1.0f; + float a_stiff_scale = 1.0f; + float a_damp_scale = 1.0f; + float lower_arm_a_scale = 1.0f; + + if (frozen_) { + l_still_scale *= 5.0f; + l_damp_scale *= 0.2f; + a_stiff_scale *= 1000.0f; + a_damp_scale *= 0.2f; + } else { + // Allow female arms to relax a bit more unless we're running. + if (female_) { + lower_arm_a_scale = + lower_arm_a_scale * run_gas_ + 0.2f * (1.0f - run_gas_); + } + + // Stiffen up during punches and celebrations. + if (since_last_punch < 500 || scenetime < celebrate_until_time_left_ + || scenetime < celebrate_until_time_right_) { + l_still_scale *= 2.0f; + a_stiff_scale *= 2.0f; + } + } + + upper_right_arm_joint_->linearStiffness = + kUpperArmLinearStiffness * l_still_scale; + upper_right_arm_joint_->linearDamping = + kUpperArmLinearDamping * l_damp_scale; + upper_right_arm_joint_->angularStiffness = + kUpperArmAngularStiffness * a_stiff_scale; + upper_right_arm_joint_->angularDamping = + kUpperArmAngularDamping * a_damp_scale; + + lower_right_arm_joint_->linearStiffness = + kLowerArmLinearStiffness * l_still_scale; + lower_right_arm_joint_->linearDamping = + kLowerArmLinearDamping * l_damp_scale; + lower_right_arm_joint_->angularStiffness = + kLowerArmAngularStiffness * a_stiff_scale * lower_arm_a_scale; + lower_right_arm_joint_->angularDamping = + kLowerArmAngularDamping * a_damp_scale * lower_arm_a_scale; + + upper_left_arm_joint_->linearStiffness = + kUpperArmLinearStiffness * l_still_scale; + upper_left_arm_joint_->linearDamping = + kUpperArmLinearDamping * l_damp_scale; + upper_left_arm_joint_->angularStiffness = + kUpperArmAngularStiffness * a_stiff_scale; + upper_left_arm_joint_->angularDamping = + kUpperArmAngularDamping * a_damp_scale; + + lower_left_arm_joint_->linearStiffness = + kLowerArmLinearStiffness * l_still_scale; + lower_left_arm_joint_->linearDamping = + kLowerArmLinearDamping * l_damp_scale; + lower_left_arm_joint_->angularStiffness = + kLowerArmAngularStiffness * a_stiff_scale * lower_arm_a_scale; + lower_left_arm_joint_->angularDamping = + kLowerArmAngularDamping * a_damp_scale * lower_arm_a_scale; + } + + // Adjust our shoulder position. + { + float x = -0.15f; + float y = 0.14f; + float z = 0.0f; + float leftZOffset = 0.0f; + float rightZOffset = 0.0f; + x += shoulder_offset_x_; + y += shoulder_offset_y_; + z += shoulder_offset_z_; + + if (punch_) { + if (punch_right_) { + leftZOffset = -0.05f; + rightZOffset = 0.05f; + } else { + leftZOffset = 0.05f; + rightZOffset = -0.05f; + } + } + + // Breathing if we're not moving. + if (!frozen_) y += breath * 0.012f; + + upper_right_arm_joint_->anchor1[0] = x; + upper_right_arm_joint_->anchor1[1] = y; + upper_right_arm_joint_->anchor1[2] = z + rightZOffset; + + upper_left_arm_joint_->anchor1[0] = -x; + upper_left_arm_joint_->anchor1[1] = y; + upper_left_arm_joint_->anchor1[2] = z + leftZOffset; + } + + // Now update ik stuff. + // If we're frozen, turn it all off. + + if (frozen_) { + right_arm_ik_joint_->linearStiffness = 0; + right_arm_ik_joint_->linearDamping = 0; + right_arm_ik_joint_->angularStiffness = 0; + right_arm_ik_joint_->angularDamping = 0; + left_arm_ik_joint_->linearStiffness = 0; + left_arm_ik_joint_->linearDamping = 0; + left_arm_ik_joint_->angularStiffness = 0; + left_arm_ik_joint_->angularDamping = 0; + } else { + bool haveHeldThing = false; + if (holding_something_ && hold_node_.exists()) { + Node* a = hold_node_.get(); + RigidBody* b = a->GetRigidBody(hold_body_); + if (b) { + haveHeldThing = true; + + right_arm_ik_joint_->linearStiffness = 40.0f; + right_arm_ik_joint_->linearDamping = 1.0f; + left_arm_ik_joint_->linearStiffness = 40.0f; + left_arm_ik_joint_->linearDamping = 1.0f; + JointFixedEF* jf; + + dBodyID heldBody = b->body(); + + // Find our target point relative to the held body and aim for + // that. + dVector3 p_world; + dVector3 p_torso2; + + jf = right_arm_ik_joint_; + dBodyGetRelPointPos(heldBody, hold_hand_offset_right_[0], + hold_hand_offset_right_[1], + hold_hand_offset_right_[2], p_world); + assert(body_torso_.exists()); + dBodyGetPosRelPoint(body_torso_->body(), p_world[0], p_world[1], + p_world[2], p_torso2); + jf->anchor1[0] = p_torso2[0]; + jf->anchor1[1] = p_torso2[1]; + jf->anchor1[2] = p_torso2[2]; + jf = left_arm_ik_joint_; + dBodyGetRelPointPos(heldBody, hold_hand_offset_left_[0], + hold_hand_offset_left_[1], + hold_hand_offset_left_[2], p_world); + assert(body_torso_.exists()); + dBodyGetPosRelPoint(body_torso_->body(), p_world[0], p_world[1], + p_world[2], p_torso2); + jf->anchor1[0] = p_torso2[0]; + jf->anchor1[1] = p_torso2[1]; + jf->anchor1[2] = p_torso2[2]; + } + } + + // Not holding something. + if (!haveHeldThing) { + // Punching. + if (since_last_punch < 300) { + JointFixedEF* punch_hand; + JointFixedEF* opposite_hand; + + JointFixedEF* shoulder_joint; + + float mirror_scale; + + if (punch_right_) { + punch_hand = right_arm_ik_joint_; + opposite_hand = left_arm_ik_joint_; + shoulder_joint = upper_right_arm_joint_; + mirror_scale = -1.0f; + } else { + punch_hand = left_arm_ik_joint_; + opposite_hand = right_arm_ik_joint_; + shoulder_joint = upper_left_arm_joint_; + mirror_scale = 1.0f; + } + + punch_hand->linearStiffness = 100.0f; + punch_hand->linearDamping = 1.0f; + opposite_hand->linearStiffness = 30.0f; + opposite_hand->linearDamping = 0.1f; + + // pull non-punch hand back.. + opposite_hand->anchor1[0] = -0.2f * mirror_scale; + opposite_hand->anchor1[1] = 0.1f; + opposite_hand->anchor1[2] = -0.0f; + + // anticipation + if (since_last_punch < 80) { + punch_hand->anchor1[0] = 0.4f * mirror_scale; + punch_hand->anchor1[1] = 0.0f; + punch_hand->anchor1[2] = -0.1f; + } else if (since_last_punch < 200) { + // Offset our punch-direction from our punch shoulder; that's + // our target point for our fist. + dVector3 p_world; + dVector3 p_torso2; + dBodyGetRelPointPos(body_torso_->body(), + shoulder_joint->anchor1[0], + shoulder_joint->anchor1[1], + shoulder_joint->anchor1[2], p_world); + + // Offset now that we're in world-space. + p_world[0] += punch_dir_x_ * 0.7f; + p_world[2] += punch_dir_z_ * 0.7f; + p_world[1] += 0.13f; + + // Now translate back to torso space for setting our anchor. + assert(body_torso_.exists()); + dBodyGetPosRelPoint(body_torso_->body(), p_world[0], p_world[1], + p_world[2], p_torso2); + + punch_hand->anchor1[0] = p_torso2[0]; + punch_hand->anchor1[1] = p_torso2[1]; + punch_hand->anchor1[2] = p_torso2[2]; + } + } else if (have_thrown_ && scenetime - throw_start_ < 100 + && scenetime >= throw_start_) { + // Pick-up gesture. + JointFixedEF* jf; + jf = left_arm_ik_joint_; + jf->anchor1[0] = 0.0f; + jf->anchor1[1] = 0.2f; + jf->anchor1[2] = 0.8f; + left_arm_ik_joint_->linearStiffness = 10.0f; + left_arm_ik_joint_->linearDamping = 0.1f; + + jf = right_arm_ik_joint_; + jf->anchor1[0] = -0.0f; + jf->anchor1[1] = 0.2f; + jf->anchor1[2] = 0.8f; + right_arm_ik_joint_->linearStiffness = 10.0f; + right_arm_ik_joint_->linearDamping = 0.1f; + } else if (!footing_ && balance_ == 0 && !dead_) { + // Wave arms when airborn. + float wave_amt = static_cast(scenetime) * -0.018f; + + left_arm_ik_joint_->linearStiffness = 6.0f; + left_arm_ik_joint_->linearDamping = 0.01f; + right_arm_ik_joint_->linearStiffness = 6.0f; + right_arm_ik_joint_->linearDamping = 0.01f; + + float v1 = sinf(wave_amt) * 0.34f; + float v2 = cosf(wave_amt) * 0.34f; + + JointFixedEF* jf; + jf = left_arm_ik_joint_; + jf->anchor1[0] = 0.4f; + jf->anchor1[1] = v1 + 0.6f; + jf->anchor1[2] = v2 + 0.2f; + + jf = right_arm_ik_joint_; + jf->anchor1[0] = -0.4f; + jf->anchor1[1] = -v1 + 0.6f; + jf->anchor1[2] = -v2 + 0.2f; + } else { + // Not airborn. + + // If we're looking to pick something up, wave our arms in front + // of us. + if (!knockout_ && pickup_ > 20) { + JointFixedEF* jf; + jf = left_arm_ik_joint_; + jf->anchor1[0] = 0.4f; + jf->anchor1[1] = 0.5f; + jf->anchor1[2] = 0.7f; + + jf = right_arm_ik_joint_; + jf->anchor1[0] = -0.4f; + jf->anchor1[1] = 0.2f; + jf->anchor1[2] = 0.7f; + + // Swipe across. + if (pickup_ < 30) { + left_arm_ik_joint_->anchor1[0] = -0.1f; + right_arm_ik_joint_->anchor1[0] = 0.1f; + } + + left_arm_ik_joint_->linearStiffness = 6.0f; + left_arm_ik_joint_->linearDamping = 0.1f; + right_arm_ik_joint_->linearStiffness = 6.0f; + right_arm_ik_joint_->linearDamping = 0.1f; + } else { + // Cursed - wave arms. + if (!knockout_ && curse_death_time_ != 0) { + left_arm_ik_joint_->linearStiffness = 30.0f; + left_arm_ik_joint_->linearDamping = 0.08f; + + right_arm_ik_joint_->linearStiffness = 30.0f; + right_arm_ik_joint_->linearDamping = 0.08f; + + float v1 = sinf(scenetime * 0.05f) * 0.12f; + float v2 = cosf(scenetime * 0.04f) * 0.12f; + + JointFixedEF* jf; + jf = left_arm_ik_joint_; + jf->anchor1[0] = 0.4f + v2; + jf->anchor1[1] = 0.4f; + jf->anchor1[2] = 0.3f + v1; + + jf = right_arm_ik_joint_; + jf->anchor1[0] = -0.4f - v2; + jf->anchor1[1] = 0.4f; + jf->anchor1[2] = 0.3f + v1; + } else if (!knockout_ + && (scenetime < celebrate_until_time_left_ + || scenetime < celebrate_until_time_right_)) { + // Celebrating - hold arms in air. + float v1 = sinf(scenetime * 0.04f) * 0.1f; + float v2 = cosf(scenetime * 0.03f) * 0.1f; + JointFixedEF* jf; + if (scenetime < celebrate_until_time_left_) { + left_arm_ik_joint_->linearStiffness = 30.0f; + left_arm_ik_joint_->linearDamping = 0.08f; + + jf = left_arm_ik_joint_; + jf->anchor1[0] = 0.4f + v2; + jf->anchor1[1] = 0.5f; + jf->anchor1[2] = 0.2f + v1; + } + if (scenetime < celebrate_until_time_right_) { + right_arm_ik_joint_->linearStiffness = 30.0f; + right_arm_ik_joint_->linearDamping = 0.08f; + + jf = right_arm_ik_joint_; + jf->anchor1[0] = -0.4f - v2; + jf->anchor1[1] = 0.5f; + jf->anchor1[2] = 0.2f + v1; + } + } else if (!knockout_ && !hold_position_pressed_ + && (ud_ || lr_)) { + // Sway arms gently when walking, and vigorously + // when running. + float blend = run_gas_ * run_gas_; + float inv_blend = 1.0f - run_gas_; + float wave_amt = roll_amt_; + + left_arm_ik_joint_->linearStiffness = + 14.0f * blend + 0.5f * inv_blend; + left_arm_ik_joint_->linearDamping = + 0.08f * blend + 0.001f * inv_blend; + + right_arm_ik_joint_->linearStiffness = + 14.0f * blend + 0.5f * inv_blend; + right_arm_ik_joint_->linearDamping = + 0.08f * blend + 0.001f * inv_blend; + + float v1run = sinf(wave_amt + 3.1415f * 0.5f) * 0.2f; + float v2run = cosf(wave_amt) * 0.3f; + float v1 = sinf(wave_amt) * 0.05f; + float v2 = cosf(wave_amt) * (female_ ? 0.3f : 0.6f); + + JointFixedEF* jf; + jf = left_arm_ik_joint_; + jf->anchor1[0] = 0.2f; + jf->anchor1[1] = + (-v1run - 0.15f) * blend + (-v1 - 0.1f) * inv_blend; + jf->anchor1[2] = + (-v2run + 0.15f) * blend + (-v2 + 0.1f) * inv_blend; + + jf = right_arm_ik_joint_; + jf->anchor1[0] = -0.2f; + jf->anchor1[1] = + (v1run - 0.15f) * blend + (v1 - 0.1f) * inv_blend; + jf->anchor1[2] = + (v2run + 0.15f) * blend + (v2 + 0.1f) * inv_blend; + } else { + // Hang freely. + left_arm_ik_joint_->linearStiffness = 0.0f; + left_arm_ik_joint_->linearDamping = 0.0f; + right_arm_ik_joint_->linearStiffness = 0.0f; + right_arm_ik_joint_->linearDamping = 0.0f; + } + } + } + } + } + } + + if (holding_something_) { + // look up to keep out of the way of our arms + dQFromAxisAndAngle(neck_joint_->qrel, 1, 0, 0, 0.5f); + head_back_ = true; + } else { + // if our head was back from holding something, whip it forward again.. + if (head_back_) { + dQSetIdentity(neck_joint_->qrel); + head_back_ = false; + } + + // if we're cursed, whip it about + if (curse_death_time_ != 0) { + if (scene()->stepnum() % 5 == 0 && RandomFloat() > 0.2f) { + head_turning = true; + dQFromAxisAndAngle(neck_joint_->qrel, RandomFloat() * 0.05f, + RandomFloat(), RandomFloat() * 0.08f, + 2.3f * (RandomFloat() - 0.5f)); + } + } else { + int64_t gti = scene()->stepnum(); + + // if we're moving or hurt, keep our head straight + if ((!hold_position_pressed_ && (ud_ || lr_)) || knockout_ + || frozen_) { + dQSetIdentity(neck_joint_->qrel); + + // rotate it slightly in the direction we're turning + dQFromAxisAndAngle( + neck_joint_->qrel, 0, 1, 0, + std::max(-1.0f, + std::min(1.0f, a_vel_y_smoothed_more_ * -0.14f))); + // dQFromAxisAndAngle(neck_joint_->qrel, + // 0,1,0, + // std::max(-0.5f,std::min(0.5f,a_vel_y_smoothed_more_*-0.07f))); + } else if (gti % 30 == 0 + && Utils::precalc_rands_1[(gti + stream_id() * 3 + 143) + % kPrecalcRandsCount] + > 0.9f) { + // otherwise, look around occasionally.. + // else if (getScene()->stepnum()%30 == 0 and + // RandomFloat() > 0.8f) { else if (gti%30 == 0 and + // g_utils->precalc_rands_1[(gti+stream_id_*3+143)%kPrecalcRandsCount] + // > 0.8f) { + + head_turning = true; + dQFromAxisAndAngle( + neck_joint_->qrel, + Utils::precalc_rands_1[(stream_id() - gti) + % (kPrecalcRandsCount - 3)] + * 0.05f, + Utils::precalc_rands_2[(stream_id() + 42 * gti) + % kPrecalcRandsCount], + Utils::precalc_rands_3[(stream_id() + 3 * gti) + % (kPrecalcRandsCount - 1)] + * 0.05f, + 1.5f + * (Utils::precalc_rands_2[(stream_id() + gti) + % kPrecalcRandsCount] + - 0.5f)); + // dQFromAxisAndAngle(neck_joint_->qrel, + // RandomFloat()*0.05f, + // RandomFloat(), + // RandomFloat()*0.05f, + // 1.5f*(RandomFloat()-0.5f)); + } + } + } + } + + // if we're flying, keep us on a 2d plane + if (can_fly_ && !dead_) { + // lets just force our few main bodies on to the plane we want + + dBodyID b; + const dReal *p, *v; + + b = body_torso_->body(); + p = dBodyGetPosition(b); + dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0], v[1], 0.0f); + + b = body_pelvis_->body(); + p = dBodyGetPosition(b); + dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0], v[1], 0.0f); + + b = body_head_->body(); + p = dBodyGetPosition(b); + dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0], v[1], 0.0f); + } + } + + // flap wings every now and then + if (wings_) { + if (scene()->stepnum() % 21 == 0 && RandomFloat() > 0.9f) { + flapping_ = true; + } + if (scene()->stepnum() % 20 == 0 && RandomFloat() > 0.7f) { + flapping_ = false; + } + } + + // update eyes.. + if (!frozen_) { + // Dart our eyes randomly (and always do it when we're turning our head. + bool spinning = (std::abs(a_vel_y_smoothed_) > 10.0f); + + if (scene()->stepnum() % 20 == 0 || head_turning || spinning) { + if (RandomFloat() > 0.7f || head_turning || spinning) { + eyes_ud_ = 20.0f * (RandomFloat() - 0.5f); + + // bias our eyes in the direction we're turning part of the time.. + float spinBias = RandomFloat() > 0.5f ? a_vel_y_smoothed_ * 0.16f : 0; + eyes_lr_ = + 70.0f + * std::max(-0.4f, + std::min(0.4f, ((RandomFloat() - 0.5f) + spinBias))); + } + } + if (scene()->stepnum() % 100 == 0 || head_turning) { + if (RandomFloat() > 0.7f || head_turning) { + eyelid_left_ud_ = 30.0f * (RandomFloat() - 0.5f); + eyelid_right_ud_ = 30.0f * (RandomFloat() - 0.5f); + } + } + // blink every now and then + if (scene()->stepnum() % 20 == 0 && RandomFloat() > 0.92f) { + blink_ = 2.0f; + } + + if (spinning) { + blink_ = 2.0f; + } + + // shut our eyes if we're knocked out (unless we're flying thru the air) + // if (knockout_ and footing_) blink_ = 2.0f; + if (knockout_) { + blink_ = 2.0f; + } + + if (dead_) { + blink_ = 2.0f; + } + + blink_ = std::max(0.0f, blink_ - 0.14f); + + blink_smooth_ += 0.25f * (std::min(1.0f, blink_) - blink_smooth_); + eyes_ud_smooth_ += 0.3f * (eyes_ud_ - eyes_ud_smooth_); + eyes_lr_smooth_ += 0.3f * (eyes_lr_ - eyes_lr_smooth_); + eyelid_left_ud_smooth_ += 0.1f * (eyelid_left_ud_ - eyelid_left_ud_smooth_); + eyelid_right_ud_smooth_ += + 0.1f * (eyelid_right_ud_ - eyelid_right_ud_smooth_); + + // eyelid tilt (angry look) + { + float smooth = 0.8f; + float this_angle; + if (running_fast || punch_) { + this_angle = 25.0f; + } else { + this_angle = default_eye_lid_angle_; + } + eye_lid_angle_ = smooth * eye_lid_angle_ + (1.0f - smooth) * this_angle; + } + } + + // if we're dead, fall over + if (dead_ && (knockout_ == 0)) { + knockout_ = 1; + } + + // so we dont get stuck up in the air if something under + // us goes away + if (footing_ == 0) { + dBodyEnable(body_head_->body()); + } + + // Newer behavior-versions have 'dizzy' functionality (we get knocked out if + // we spin too long) + if (behavior_version_ > 0) { + // Testing: lose balance while spinning fast. + if (std::abs(a_vel_y_smoothed_more_) > 10.0f) { + dizzy_ += 1; + if (dizzy_ > 120) { + dizzy_ = 0; + knockout_ = 40; + PlayHurtSound(); + } + } else { + dizzy_ = static_cast_check_fit( + std::max(0, static_cast(dizzy_) - 2)); + } + } + + if (knockout_ > 0 || frozen_) { + balance_ = 0; + } else { + if (footing_) { + if (balance_ < 100) { // NOLINT(bugprone-branch-clone) + balance_ += 20; + } else if (balance_ < 235) { + balance_ += 20; + } else if (balance_ < 255) { + balance_++; + } + } else { + if (balance_ > 100) { + balance_ -= 20; + } else if (balance_ > 10) { + balance_ -= 5; + } else if (balance_ > 0) { + balance_--; + } + } + } + + // knockout wears off more slowly if we're airborn + // (prevents landing on ones feet too much) + if (knockout_ > 0 && (scene()->stepnum() % (footing_ ? 5 : 10) == 0) + && !dead_) { + knockout_--; + if (knockout_ == 0) { + dBodyEnable(body_head_->body()); + } + } + + // if we're wanting to throw something... + if (throwing_) { + throwing_ = false; + DropHeldObject(); + } + + // if we're flying, spin based on the direction we're holding + if (can_fly_ && trying_to_fly_ && !footing_ && !frozen_ && !knockout_) { + const dReal* av = dBodyGetAngularVel(body_torso_->body()); + + float mag_scale = sqrtf(lr_smooth_ * lr_smooth_ + ud_smooth_ * ud_smooth_); + float mag; + if (mag_scale > 0.1f) { + float a = AngleBetween2DVectors(lr_smooth_, ud_smooth_, + (p_head[0] - p_torso[0]), + (p_head[1] - p_torso[1])); + if (a < 0) { + mag = mag_scale * 20.0f; + } else { + mag = -mag_scale * 20.0f; + } + if (std::abs(a) < 0.8f) { + mag *= std::abs(a) / 0.8f; + } + } else { + mag = 0.0f; + } + + mag += av[2] * -2.0f * mag_scale; // brakes + + dBodyAddTorque(body_torso_->body(), 0, 0, mag); + + // also slow down a bit in flight + dBodyID b; + const dReal* v; + + // get a velocity difference based on our speed and sub that from everything + // ...simpler than applying forces which might be uneven and spin us + float sub = dBodyGetLinearVel(body_torso_->body())[0] * -0.02f; + + b = body_torso_->body(); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0] + sub, v[1], v[2]); + + b = body_head_->body(); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0] + sub, v[1], v[2]); + + b = body_pelvis_->body(); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0] + sub, v[1], v[2]); + + b = body_roller_->body(); + v = dBodyGetLinearVel(b); + dBodySetLinearVel(b, v[0] + sub, v[1], v[2]); + } + + if (fly_power_ > 0.0001f && !knockout_) { + const dReal* p_top = dBodyGetPosition(body_torso_->body()); + const dReal* p_bot = dBodyGetPosition(body_roller_->body()); + dBodyEnable(body_torso_->body()); // wake it up + // float mag = 550*0.004f * fly_power_; + // float up_mag = 150*0.004f * fly_power_; + float mag = 550.0f * 0.005f * fly_power_; // 120hz change + float up_mag = 150.0f * 0.005f * fly_power_; // 120hz change + float fx = mag * (p_top[0] - p_bot[0]); + float fy = mag * (p_top[1] - p_bot[1]); + float head_scale = 0.5f; + dBodyAddForce(body_head_->body(), head_scale * fx, head_scale * fy, 0); + dBodyAddForce(body_head_->body(), 0, head_scale * up_mag, 0); + dBodyAddForce(body_torso_->body(), fx, fy, 0); + dBodyAddForce(body_torso_->body(), 0, up_mag, 0); + + // also add some force to what we're holding so popping out a bomb doesnt + // send us spiraling down to death + if (holding_something_) { + Node* a = hold_node_.get(); + if (a) { + float scale = 0.2f; + RigidBody* b = a->GetRigidBody(hold_body_); + if (b) { + dBodyAddForce(b->body(), fx * scale, fy * scale, 0); + dBodyAddForce(b->body(), 0, up_mag * scale, 0); + } + } + } + } + + // torso + { + dBodyID b = stand_body_->body(); + const dReal* p_torso2 = dBodyGetPosition(body_torso_->body()); + const dReal* p_bot = dBodyGetPosition(body_roller_->body()); + const dReal* lv = dBodyGetLinearVel(body_torso_->body()); + + dBodySetLinearVel(b, lv[0], lv[1], lv[2]); + dBodySetAngularVel(b, 0, 0, 0); + + // Update the orientation of our stand body. + // If we're pressing the joystick, that's the direction we use. + // The moment we stop, though, we instead use the direction our torso is + // pointing. (we dont wanna keep turning once we let off the joystick) The + // only alternative is to turn off angular stiffness on the constraint but + // then we spin and stuff. + + // Also let's calculate tilt. For this we guesstimate how fast we wanna be + // going given our UD/LR values and we tilt forward or back depending on + // where we are relative to that. + float tilt_lr, tilt_ud; + dBodySetPosition(b, p_torso2[0], p_bot[1] + 0.2f, p_torso2[2]); + + float rotate_tilt = 0.4f; + + if (hockey_) { + const dReal* b_vel_3 = dBodyGetLinearVel(body_roller_->body()); + float v_mag = std::max(5.0f, Vector3f(b_vel_3).Length()); + float accel_smoothing = 0.9f; + for (int i = 0; i < 3; i++) { + float avg_vel = (b_vel_3[i]); + accel_[i] = accel_smoothing * accel_[i] + + (1.0f - accel_smoothing) * (avg_vel - prev_vel_[i]); + prev_vel_[i] = avg_vel; + } + tilt_lr = std::min(1.0f, std::max(-1.0f, v_mag * accel_[0] * 1.4f)); + tilt_ud = std::min(1.0f, std::max(-1.0f, v_mag * accel_[2] * -1.4f)); + } else { + // non-hockey + + const dReal* b_vel_3 = dBodyGetLinearVel(body_roller_->body()); + float v_mag = std::max(7.0f, Vector3f(b_vel_3).Length()); + float accel_smoothing = 0.7f; + for (int i = 0; i < 3; i++) { + float avg_vel = (b_vel_3[i]); + accel_[i] = accel_smoothing * accel_[i] + + (1.0f - accel_smoothing) * (avg_vel - prev_vel_[i]); + prev_vel_[i] = avg_vel; + } + tilt_lr = (0.2f + 0.8f * run_gas_) + * std::min(0.9f, std::max(-0.9f, v_mag * accel_[0] * 0.3f)); + tilt_ud = (0.2f + 0.8f * run_gas_) + * std::min(0.9f, std::max(-0.9f, v_mag * accel_[2] * -0.3f)); + + float fast = std::min(1.0f, speed_smoothed_ / 5.0f); + + // A sharper tilt at low speeds (so we dont whiplash when walking). + tilt_lr += (1.0f - fast) * (lr_diff_smooth_ * 10.0f); + tilt_ud += (1.0f - fast) * (ud_diff_smooth_ * 10.0f); + + tilt_lr += fast * (lr_diff_smoother_ * 30.0f); + tilt_ud += fast * (ud_diff_smoother_ * 30.0f); + + rotate_tilt *= 1.2f; + } + if (holding_something_) { + rotate_tilt *= 0.5f; + } + + // Lean less if we're spinning. Otherwise we go jumping all crazy to the + // side. + const dReal spin = std::abs(dBodyGetAngularVel(body_torso_->body())[1]); + if (spin > 10.0f) { + rotate_tilt = 0.0f; + } + + float this_punch_dir_x{}; + float this_punch_dir_z{}; + + // If we're moving, we orient our stand-body to that exact direction. + if (lr_ || ud_) { + // If we're holding position we can't use lr_norm_/ud_norm_ here because + // they'll be zero (or close). So in that case just calc a normalized + // lr_/_ud here. + + float this_ud_norm, this_lr_norm; + if (hold_position_pressed_) { + this_ud_norm = (static_cast(ud_) / 127.0f); + this_lr_norm = (static_cast(lr_) / 127.0f); + if (clamp_move_values_to_circle_) { + BoxClampToCircle(&this_lr_norm, &this_ud_norm); + } else { + BoxNormalizeToCircle(&this_lr_norm, &this_ud_norm); + } + } else { + this_ud_norm = ud_norm_; + this_lr_norm = lr_norm_; + } + dMatrix3 r; + RotationFrom2Axes(r, -this_ud_norm, 0, -this_lr_norm, + rotate_tilt * tilt_lr, 1, -rotate_tilt * tilt_ud); + dBodySetRotation(b, r); + + // Also update our punch direction. + this_punch_dir_x = this_lr_norm; + this_punch_dir_z = -this_ud_norm; + } else { + // We're not moving; orient our stand body to match our torso. + dMatrix3 r; + dVector3 p_forward; + dBodyGetRelPointPos(body_torso_->body(), 1, 0, 0, p_forward); + + // Doing this repeatedly winds up turning us slowly in circles + // ..so lets recycle previous values if we haven't changed much. + float orientX = p_forward[0] - p_torso2[0]; + float orientZ = p_forward[2] - p_torso2[2]; + if (std::abs(orientX - last_stand_body_orient_x_) > 0.05f + || std::abs(orientZ - last_stand_body_orient_z_) > 0.05f) { + last_stand_body_orient_x_ = orientX; + last_stand_body_orient_z_ = orientZ; + } + + RotationFrom2Axes(r, last_stand_body_orient_x_, 0, + last_stand_body_orient_z_, rotate_tilt * tilt_lr, 1, + -rotate_tilt * tilt_ud); + + dBodySetRotation(b, r); + + this_punch_dir_z = (p_forward[0] - p_torso2[0]); + this_punch_dir_x = -(p_forward[2] - p_torso2[2]); + } + + // Update and re-normalize punch dir. + { + float blend = 0.5f; + punch_dir_x_ = (1.0f - blend) * this_punch_dir_x + blend * punch_dir_x_; + punch_dir_z_ = (1.0f - blend) * this_punch_dir_z + blend * punch_dir_z_; + + float len = + sqrtf(punch_dir_x_ * punch_dir_x_ + punch_dir_z_ * punch_dir_z_); + float mult = len == 0.0f ? 9999 : 1.0f / len; + punch_dir_x_ *= mult; + punch_dir_z_ *= mult; + } + + // Rotate our attach-point to give some sway while running. + { + float angle = + sinf(roll_amt_ - 3.141592f) + * (run_gas_ * 0.09f + (1.0f - run_gas_) * (female_ ? 0.02f : 0.05f)); + dQFromAxisAndAngle(stand_joint_->qrel, 0, 1, 1, angle); + } + + { + float bal = static_cast(balance_) / 255.0f; + + bal = 1.0f + - ((1.0f - bal) * (1.0f - bal) * (1.0f - bal) + * (1.0f - bal)); // push it towards 1 + float mult = bal; + + // Crank up our balance when we're holding something otherwise we get a + // bit soupy. + if (holding_something_) { + mult *= 0.9f; + } else { + mult *= 0.6f; + } + + { + stand_joint_->linearStiffness = 0.0f; + stand_joint_->linearDamping = 0.0f; + stand_joint_->angularStiffness = 180.0f * mult; + stand_joint_->angularDamping = 3.0f * mult; + } + + // Crank down angular forces at low speeds to keep from looking too stiff. + { + dVector3 f = {ud_norm_, 0, lr_norm_}; + float m = dVector3Length(f); + float blend_max = 1.0f; + if (m < blend_max) { + stand_joint_->angularDamping *= 0.3f + 0.7f * (m / blend_max); + stand_joint_->angularStiffness *= 0.6f + 0.4f * (m / blend_max); + } + } + } + } + + // Resize our run-ball based on our balance. + // (so when we're laying on the ground its not propping our legs up in the + // air) + { + if (knockout_ || frozen_) + ball_size_ = 0.0f; + else + ball_size_ = std::min(1.0f, ball_size_ + 0.05f); + + float sz = 0.1f + 0.9f * ball_size_; + body_roller_->SetDimensions( + 0.3f * sz, 0, 0, 0.3f, 0, + 0, // keep its mass the same as its full-size self though + 0.1f); + } + + // Push our roller-ball down for jumps and retract it when we're hurt. + { + // Retract it up as well so when it pops back up it doesnt start + // underground. + float offs = (1.0f - ball_size_) * 0.3f; + float ls_scale = 1.0f; + float ld_scale = 1.0f; + if (jump_ > 0 && !frozen_ && !knockout_) { + offs -= 0.3f; + ls_scale = 0.6f; + ld_scale = 0.2f; + } + roller_ball_joint_->linearStiffness = kRollerBallLinearStiffness * ls_scale; + roller_ball_joint_->linearDamping = kRollerBallLinearDamping * ld_scale; + offs -= breath * 0.02f; + roller_ball_joint_->anchor1[1] = base_pelvis_roller_anchor_offset_ + offs; + } + + // Roll our run-ball (new). + { + { + float mult; + if (frozen_ || hold_position_pressed_) { + mult = 0.0f; + } else { + mult = std::min(1.0f, static_cast(balance_) / 100.0f); + } + + // hockey.. + if (hockey_) { + dBodyEnable(body_roller_->body()); + dJointSetAMotorParam(a_motor_roller_, dParamFMax, 30.0f * mult); + dJointSetAMotorParam(a_motor_roller_, dParamFMax2, 10.0f * mult); + dJointSetAMotorParam(a_motor_roller_, dParamFMax3, 30.0f * mult); + dJointSetAMotorParam(a_motor_roller_, dParamVel, + -0.17f * 128.0f * ud_norm_); + dJointSetAMotorParam(a_motor_roller_, dParamVel2, 0.0f); + dJointSetAMotorParam(a_motor_roller_, dParamVel3, + -0.17f * 128.0f * lr_norm_); + } else { + const dReal* vel = dBodyGetLinearVel(body_roller_->body()); + dVector3 v = {vel[0], vel[1], vel[2]}; + + // Old settings to keep the demo working. + if (demo_mode_) { + // We want to speed up faster going downhill and slower going uphill + // (getting the base physics to do that leaves us with a + // hard-to-control character) + // So we fake it by skewing our smoothed speed faster on downhill + // and slower uphill. + float speed_scale = 1.0f; + float walk_scale; + + // Heading downhill: speed up. + if (v[1] < 0.0f) { + v[1] *= 2.0f; // just scale our downward component up to bias the + // speed calc + walk_scale = 1.0f - v[1] * 0.1f; + } else { + // Heading uphill: slow down. + speed_scale = std::max(0.0f, 1.0f - v[1] * 0.2f); + walk_scale = std::max(0.0f, 1.0f - v[1] * 0.2f); + v[1] = 0.0f; + } + + // Our smoothed spead increases slowly and decreases fast. + float speed = dVector3Length(v) * speed_scale; + float speed_smoothing = (speed > speed_smoothed_) ? 0.985f : 0.7f; + speed_smoothed_ = speed_smoothing * speed_smoothed_ + + (1.0f - speed_smoothing) * speed; + + float gear_high = std::min(1.0f, speed_smoothed_ / 7.0f); + float gear_low = 1.0f - gear_high; + + // As we 'shift up' in gears our max-force goes up and target velocity + // goes down. + float max_force = gear_low * 15.0f + gear_high * 15.0f; + float max_vel = walk_scale * 7.68f + gear_high * run_gas_ * 15.0f; + dBodyEnable(body_roller_->body()); + dJointSetAMotorParam(a_motor_roller_, dParamFMax, + max_force * mult); // change for 120hz + dJointSetAMotorParam(a_motor_roller_, dParamFMax2, + 500.0f * mult); // 120hz change + dJointSetAMotorParam(a_motor_roller_, dParamFMax3, + max_force * mult); // change for 120hz + dJointSetAMotorParam(a_motor_roller_, dParamVel, -max_vel * ud_norm_); + dJointSetAMotorParam(a_motor_roller_, dParamVel2, 0.0f); + dJointSetAMotorParam(a_motor_roller_, dParamVel3, + -max_vel * lr_norm_); + } else { + // We want to speed up faster going downhill and slower going uphill + // (getting the base physics to do that leaves us with a + // hard-to-control character) + // ...so we fake it by skewing our smoothed speed faster on downhill + // and slower uphill + float speed_scale = 1.0f; + float walk_scale = + 1.0f; // if we're just walking, how fast we'll go.. + // heading downhill - speed up + if (footing_) { + if (v[1] < 0.0f) { + v[1] *= 2.0f; // just scale our downward component up to bias the + // speed calc + walk_scale = 1.0f - v[1] * 0.1f; + } else { + // heading uphill - slow down + speed_scale = std::max(0.0f, 1.0f - v[1] * 0.2f); + walk_scale = std::max(0.0f, 1.0f - v[1] * 0.2f); + v[1] = 0.0f; // also don't count upward velocity towards our + // speed calc.. + } + } + + // our smoothed spead increases slowly and decreases fast + float speed = dVector3Length(v) * speed_scale; + float speed_smoothing = (speed > speed_smoothed_) ? 0.985f : 0.94f; + speed_smoothed_ = speed_smoothing * speed_smoothed_ + + (1.0f - speed_smoothing) * speed; + + float gear_high = std::min(1.0f, speed_smoothed_ / 7.0f); + float gear_low = 1.0f - gear_high; + + // as we 'shift up' in gears our max-force goes up and target velocity + // goes down + float max_force = gear_low * 15.0f + gear_high * 15.0f; + float max_vel = walk_scale * 7.68f + gear_high * run_gas_ * 15.0f; + dBodyEnable(body_roller_->body()); + dJointSetAMotorParam(a_motor_roller_, dParamFMax, + max_force * mult); // change for 120hz + dJointSetAMotorParam(a_motor_roller_, dParamFMax2, + 500.0f * mult); // 120hz change + dJointSetAMotorParam(a_motor_roller_, dParamFMax3, + max_force * mult); // change for 120hz + dJointSetAMotorParam(a_motor_roller_, dParamVel, -max_vel * ud_norm_); + dJointSetAMotorParam(a_motor_roller_, dParamVel2, 0.0f); + dJointSetAMotorParam(a_motor_roller_, dParamVel3, + -max_vel * lr_norm_); + } + } + } + } + + // Set brake motor strength. + if (footing_ || frozen_ || dead_) { + float amt; + // Full brakes if frozen. Otherwise crank up as our joystick magnitude goes + // down. + if (frozen_ || dead_) { + amt = 1.0f; + } else { + dVector3 f = {lr_norm_, 0, ud_norm_}; + amt = std::min(1.0f, dVector3Length(f) * 5.0f); + amt = 1.0f - (amt * amt * amt); + amt *= (1.0f - run_gas_); + amt *= 0.4f; + } + dJointSetAMotorParam(a_motor_brakes_, dParamFMax, 10.0f * amt); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax2, 10.0f * amt); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax3, 10.0f * amt); + dJointSetAMotorParam(a_motor_brakes_, dParamVel, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamVel2, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamVel3, 0.0f); + } else { + // if we're not on the ground we wanna just keep doing what we're doing + dJointSetAMotorParam(a_motor_brakes_, dParamFMax, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax2, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamFMax3, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamVel, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamVel2, 0.0f); + dJointSetAMotorParam(a_motor_brakes_, dParamVel3, 0.0f); + } + + // If we're knocked out, stop any mid-progress punch. + if (knockout_) { + punch_ = 0; + } + + if (punch_ > 0) { + if (!body_punch_.exists() && since_last_punch > 80 && !knockout_) { + body_punch_ = Object::New( + kPunchBodyID, &punch_part_, RigidBody::Type::kGeomOnly, + RigidBody::Shape::kSphere, RigidBody::kCollideRegion, + RigidBody::kCollideAll); + body_punch_->SetDimensions(0.25f); + } + + if (body_punch_.exists()) { + // Move the punch body to the end of our punching arm. + dBodyID fist_body = punch_right_ ? lower_right_arm_body_->body() + : lower_left_arm_body_->body(); + dVector3 p; + dBodyGetRelPointPos(fist_body, 0, 0, 0.01f, p); + + // Move it down a tiny bit since we're often trying to punch dudes laying + // on the ground. + p[1] -= 0.1f; + + dGeomSetPosition(body_punch_->geom(), p[0], p[1], p[2]); + } + + } else { + if (body_punch_.exists()) { + body_punch_.Clear(); + } + } + + // If we're flying through the air really fast (preferably not on purpose), + // scream. + const dReal* p_head_vel = dBodyGetLinearVel(body_head_->body()); + float vel_mag_squared = p_head_vel[0] * p_head_vel[0] + + p_head_vel[1] * p_head_vel[1] + + p_head_vel[2] * p_head_vel[2]; + + float scream_speed = can_fly_ ? 160.0f : 100.0f; + if ((force_scream_ && scene()->time() - last_force_scream_time_ < 3000) + || (scene()->time() - last_fly_time_ > 1000 + && vel_mag_squared > scream_speed && !footing_ + && std::abs(p_head_vel[1]) > 0.3f && !dead_)) { + if (scene()->time() - last_fall_time_ > 1000) { + // If we're not still screaming, start one up. + if (!(voice_play_id_ == fall_play_id_ + && g_audio->IsSoundPlaying(fall_play_id_))) { + if (Sound* sound = GetRandomMedia(fall_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + g_audio->PushSourceStopSoundCall(voice_play_id_); + source->SetPosition(p_head[0], p_head[1], p_head[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + fall_play_id_ = voice_play_id_; + source->End(); + } + } + } + last_fall_time_ = scene()->time(); + } + } + + // If theres a scream going on, update its position and stop it if we've + // slowed down alot. + if (voice_play_id_ == fall_play_id_) { + if ((footing_ && !force_scream_) + || (force_scream_ + && scene()->time() - last_force_scream_time_ > 2000)) { + g_audio->PushSourceStopSoundCall(voice_play_id_); + voice_play_id_ = 0xFFFFFFFF; + } else { + AudioSource* s = g_audio->SourceBeginExisting(fall_play_id_, 108); + if (s) { + s->SetPosition(p_head[0], p_head[1], p_head[2]); + s->End(); + } + } + } + + // Update ticking. + if (tick_play_id_ != 0xFFFFFFFF) { + AudioSource* s = g_audio->SourceBeginExisting(tick_play_id_, 109); + if (s) { + s->SetPosition(p_head[0], p_head[1], p_head[2]); + s->End(); + } + } + + // If we're in the process of throwing something + // ( we need to check have_thrown_ because otherwise we'll always think + // we're throwing at game-time 0 since throw_start_ inits to that.) + if (have_thrown_ && scene()->time() - throw_start_ < 50) { + Node* a = hold_node_.get(); + if (a) { + RigidBody* b = a->GetRigidBody(hold_body_); + if (b) { + dVector3 f; + float power; + if (throw_power_ < 0.1f) { + power = -0.2f - 1 * (0.1f - throw_power_); + } else { + power = (throw_power_ - 0.1f) * 1.0f; + } + + power *= 1.15f; // change for 120hz + dBodyVectorToWorld(body_torso_->body(), 0, 60, 60, f); + + // If we're pressing a direction, factor that in. + float lrf = throw_lr_; + float udf = throw_ud_; + if (clamp_move_values_to_circle_) { + BoxClampToCircle(&lrf, &udf); + } else { + BoxNormalizeToCircle(&lrf, &udf); + } + + // Blend based on magnitude of our locked in throw speed. + float d_len = sqrtf(lrf * lrf + udf * udf); + if (d_len > 0.0f) { + // Let's normalize our locked in throw direction. + // 'throwPower' should be our sole magnitude determinant. + float dist = sqrtf(throw_lr_ * throw_lr_ + throw_ud_ * throw_ud_); + float s = 1.0f / dist; + lrf *= s; + udf *= s; + + float f2[3]; + f2[0] = lrf * 50.0f; + f2[1] = 80.0f; + f2[2] = -udf * 50.0f; + if (d_len > 0.1f) { + f[0] = f2[0]; + f[1] = f2[1]; + f[2] = f2[2]; + } else { + float blend = d_len / 0.1f; + f[0] = blend * f2[0] + (1.0f - blend) * f[0]; + f[1] = blend * f2[1] + (1.0f - blend) * f[1]; + f[2] = blend * f2[2] + (1.0f - blend) * f[2]; + } + } + + dBodyEnable(body_torso_->body()); // wake it up + dBodyEnable(b->body()); // wake it up + const dReal* p = dBodyGetPosition(b->body()); + + float kick_back = -0.25f; + + // Pro trick: if we throw while still holding bomb down, we throw + // backwards lightly. + if (bomb_pressed_ && !throwing_with_bomb_button_) { + float neg = -0.2f; + dBodyAddForceAtPos(b->body(), neg * power * f[0], + std::abs(neg * power * f[1]), neg * power * f[2], + p[0], p[1] - 0.1f, p[2]); + dBodyAddForceAtPos(body_torso_->body(), -neg * power * f[0], + std::abs(-neg * power * f[1]), -neg * power * f[2], + p[0], p[1] - 0.1f, p[2]); + } else { + dBodyAddForceAtPos(b->body(), power * f[0], std::abs(power * f[1]), + power * f[2], p[0], p[1] - 0.1f, p[2]); + dBodyAddForceAtPos(body_torso_->body(), kick_back * power * f[0], + kick_back * (std::abs(power * f[1])), + kick_back * power * f[2], p[0], p[1] - 0.1f, p[2]); + } + } + } + } else { + // If we're no longer holding something and our throw is over, clear any ref + // we might have. + if (!holding_something_ && hold_node_.exists()) hold_node_.Clear(); + } + + if (pickup_ == kPickupCooldown - 4) { + if (!body_pickup_.exists()) { + body_pickup_ = Object::New( + kPickupBodyID, &pickup_part_, RigidBody::Type::kGeomOnly, + RigidBody::Shape::kSphere, RigidBody::kCollideRegion, + RigidBody::kCollideActive); + body_pickup_->SetDimensions(0.7f); + } + } else { + if (body_pickup_.exists()) { + body_pickup_.Clear(); + } + } + + if (body_pickup_.exists()) { + // A unit vector forward. + dVector3 f; + float z = 0.3f; + dBodyVectorToWorld(body_head_->body(), 0, 0, 1, f); + dGeomSetPosition(body_pickup_->geom(), + 0.5f * (p_head[0] + p_torso[0]) + z * f[0], + 0.5f * (p_head[1] + p_torso[1]) + z * f[1], + 0.5f * (p_head[2] + p_torso[2]) + z * f[2]); + } + + // If we're holding something and it died, tell userland. + if (holding_something_) { + if (!pickup_joint_.IsAlive()) { + holding_something_ = false; + DispatchDropMessage(); + } + } + + if (flashing_ > 0) flashing_--; + + if (jump_ > 0) { + // *always* reduce jump even if we're holding it. + jump_ -= 1; + // jump_ = std::max(0, static_cast(jump_) - 1); + // enforce a 'minimum-held-time' so that an instant press/release still + // results in a measurable jump (we tend to get these from remotes/etc) + // cout << "DIFF " << getScene().time()-last_jump_time_ << endl; + // if (!jump_pressed_ and (getScene().time()-last_jump_time_ > + // 1000)) jump_ = 0.0f; + } + + // Emit fairy dust if we're flying. +#if !BA_HEADLESS_BUILD + if (fly_power_ > 20.0f && scene()->stepnum() % 3 == 1) { + for (int i = 0; i < 1; i++) { + BGDynamicsEmission e; + e.emit_type = BGDynamicsEmitType::kFairyDust; + e.position = Vector3f(dGeomGetPosition(body_torso_->geom())); + e.velocity = Vector3f(dBodyGetLinearVel(body_torso_->body())); + e.count = 1; + e.scale = 1.0f; + e.spread = 1.0f; + g_bg_dynamics->Emit(e); + } + } +#endif // !BA_HEADLESS_BUILD + + fly_power_ *= 0.95f; + + if (punch_ > 0) { + punch_--; + } + if (pickup_ > 0) { + pickup_--; + } + + UpdateAreaOfInterest(); + + // Update our recent-damage tally. + damage_smoothed_ *= 0.8f; + + // If we're out of bounds, arrange to have ourself informed. + if (!dead_) { + const dReal* p = dBodyGetPosition(body_head_->body()); + if (scene()->IsOutOfBounds(p[0], p[1], p[2])) { + scene()->AddOutOfBoundsNode(this); + last_out_of_bounds_time_ = scene()->time(); + } + } + BA_DEBUG_CHECK_BODIES(); +} // NOLINT (yeah i know, this is too long) + +#if !BA_HEADLESS_BUILD +static void DrawShadow(const BGDynamicsShadow& shadow, float radius, + float density, const float* shadow_color) { + float s_scale, s_density; + shadow.GetValues(&s_scale, &s_density); + float d = s_density * density; + g_graphics->DrawBlotch(shadow.GetPosition(), radius * s_scale * 4.0f, + (0.08f + 0.04f * shadow_color[0]) * d, + (0.07f + 0.04f * shadow_color[1]) * d, + (0.065f + 0.04f * shadow_color[2]) * d, 0.32f * d); +} +static void DrawBrightSpot(const BGDynamicsShadow& shadow, float radius, + float density, const float* shadow_color) { + float s_scale, s_density; + shadow.GetValues(&s_scale, &s_density); + float d = s_density * density * 0.3f; + g_graphics->DrawBlotch(shadow.GetPosition(), radius * s_scale * 4.0f, + shadow_color[0] * d, shadow_color[1] * d, + shadow_color[2] * d, 0.0f); +} +#endif // !BA_HEADLESS_BUILD + +void SpazNode::DrawEyeBalls(RenderComponent* c, ObjectComponent* oc, + bool shading, float death_fade, float death_scale, + float* add_color) { + // Eyeballs. + if (blink_smooth_ < 0.9f) { + if (shading) { + oc->SetLightShadow(LightShadowType::kObject); + oc->SetTexture(g_media->GetTexture(SystemTextureID::kEye)); + oc->SetColorizeColor(eye_color_red_, eye_color_green_, eye_color_blue_); + oc->SetColorizeTexture(g_media->GetTexture(SystemTextureID::kEyeTint)); + oc->SetReflection(ReflectionType::kSharpest); + oc->SetReflectionScale(3, 3, 3); + oc->SetAddColor(add_color[0], add_color[1], add_color[2]); + oc->SetColor(eye_ball_color_red_, eye_ball_color_green_, + eye_ball_color_blue_); + } + c->PushTransform(); + c->TransformToBody(*body_head_); + if (eye_scale_ != 1.0f) c->Scale(eye_scale_, eye_scale_, eye_scale_); + c->PushTransform(); + c->Translate(eye_offset_x_, eye_offset_y_, eye_offset_z_); + c->Rotate(-10 + eyes_ud_smooth_, 1, 0, 0); + c->Rotate(eyes_lr_smooth_, 0, 1, 0); + c->Scale(0.09f, 0.09f, 0.09f); + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + + if (!frosty_ && !eyeless_) { + c->DrawModel(g_media->GetModel(SystemModelID::kEyeBall)); + if (shading) { + oc->SetReflectionScale(2, 2, 2); + } + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + c->DrawModel(g_media->GetModel(SystemModelID::kEyeBallIris)); + } + + c->PopTransform(); + + if (!pirate_ && !frosty_ && !eyeless_) { + if (shading) { + oc->SetReflectionScale(3, 3, 3); + } + c->PushTransform(); + c->Translate(-eye_offset_x_, eye_offset_y_, eye_offset_z_); + c->Rotate(-10 + eyes_ud_smooth_, 1, 0, 0); + c->Rotate(eyes_lr_smooth_, 0, 1, 0); + c->Scale(0.09f, 0.09f, 0.09f); + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + c->DrawModel(g_media->GetModel(SystemModelID::kEyeBall)); + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + if (shading) { + oc->SetReflectionScale(2, 2, 2); + } + c->DrawModel(g_media->GetModel(SystemModelID::kEyeBallIris)); + c->PopTransform(); + } + c->PopTransform(); + } +} + +void SpazNode::SetupEyeLidShading(ObjectComponent* c, float death_fade, + float* add_color) { + c->SetTexture(g_media->GetTexture(SystemTextureID::kEye)); + c->SetColorizeTexture(nullptr); + float r, g, b; + r = eye_lid_color_red_; + g = eye_lid_color_green_; + b = eye_lid_color_blue_; + + // Fade to reddish. + if (dead_ && !frozen_) { + r *= 0.3f + 0.7f * death_fade; + g *= 0.2f + 0.7f * (death_fade * 0.5f); + b *= 0.2f + 0.7f * (death_fade * 0.5f); + } + c->SetColor(r, g, b); + c->SetAddColor(add_color[0], add_color[1], add_color[2]); + c->SetReflection(ReflectionType::kChar); + c->SetReflectionScale(0.05f, 0.05f, 0.05f); +} + +void SpazNode::DrawEyeLids(RenderComponent* c, float death_fade, + float death_scale) { + if (!has_eyelids_ && blink_smooth_ < 0.1f) return; + + c->PushTransform(); + c->TransformToBody(*body_head_); + if (eye_scale_ != 1.0f) { + c->Scale(eye_scale_, eye_scale_, eye_scale_); + } + c->Translate(eye_offset_x_, eye_offset_y_, eye_offset_z_); + + float a = eyelid_left_ud_smooth_ + 0.5f * eyes_ud_smooth_; + if (blink_smooth_ > 0.001f) { + a = blink_smooth_ * 90.0f + (1.0f - blink_smooth_) * a; + } + c->Rotate(eye_lid_angle_, 0, 0, 1); + c->Rotate(a, 1, 0, 0); + c->Scale(0.09f, 0.09f, 0.09f); + + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + + if (!frosty_ && !eyeless_) { + c->DrawModel(g_media->GetModel(SystemModelID::kEyeLid)); + } + c->PopTransform(); + + // Left eyelid. + c->FlipCullFace(); + c->PushTransform(); + c->TransformToBody(*body_head_); + if (eye_scale_ != 1.0f) c->Scale(eye_scale_, eye_scale_, eye_scale_); + + c->Translate(-eye_offset_x_, eye_offset_y_, eye_offset_z_); + a = eyelid_right_ud_smooth_ + 0.5f * eyes_ud_smooth_; + if (blink_smooth_ > 0.001f) { + a = blink_smooth_ * 90.0f + (1.0f - blink_smooth_) * a; + } + c->Rotate(-eye_lid_angle_, 0, 0, 1); + c->Rotate(a, 1, 0, 0); + c->Scale(-0.09f, 0.09f, 0.09f); + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + if (!pirate_ && !frosty_ && !eyeless_) + c->DrawModel(g_media->GetModel(SystemModelID::kEyeLid)); + c->PopTransform(); + c->FlipCullFace(); // back to normal +} + +void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, + float death_scale, float* add_color) { + // Set up shading. + if (shading) { + c->SetTexture(color_texture_); + c->SetColorizeTexture(color_mask_texture_); + c->SetColorizeColor(color_[0], color_[1], color_[2]); + assert(highlight_.size() == 3); + c->SetColorizeColor2(highlight_[0], highlight_[1], highlight_[2]); + c->SetLightShadow(LightShadowType::kObject); + c->SetAddColor(add_color[0], add_color[1], add_color[2]); + + // Tint blueish when frozen. + if (frozen_) { + c->SetColor(0.9f, 0.9f, 1.2f); + } else if (dead_) { + // Fade to reddish when dead. + float r = 0.3f + 0.7f * death_fade; + float g = 0.1f + 0.5f * death_fade; + float b = 0.1f + 0.5f * death_fade; + c->SetColor(r, g, b); + } + + if (frozen_) { + c->SetReflection(ReflectionType::kSharper); + c->SetReflectionScale(1.5f, 1.5f, 1.5f); + } else { + if (dead_) { + // Go mostly matte when dead. + c->SetReflection(ReflectionType::kSoft); + c->SetReflectionScale(0.03f, 0.03f, 0.03f); + } else { + c->SetReflection(ReflectionType::kChar); + c->SetReflectionScale(reflection_scale_, reflection_scale_, + reflection_scale_); + } + } + } + + // Head. + c->PushTransform(); + c->TransformToBody(*body_head_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (head_model_.exists()) { + c->DrawModel(head_model_->model_data()); + } + c->PopTransform(); + + // Hair tuft 1. + if (hair_front_right_body_.exists()) { + c->PushTransform(); + c->TransformToBody(*hair_front_right_body_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + c->DrawModel(g_media->GetModel(SystemModelID::kHairTuft1)); + c->PopTransform(); + + // Hair tuft 1b; just reuse tuft 1 with some extra translating. + const dReal* m = dBodyGetRotation(body_head_->body()); + c->PushTransform(); + float offs[] = {-0.03f, 0.0f, -0.13f}; + c->Translate(offs[0] * m[0] + offs[1] * m[1] + offs[2] * m[2], + offs[0] * m[4] + offs[1] * m[5] + offs[2] * m[6], + offs[0] * m[8] + offs[1] * m[9] + offs[2] * m[10]); + c->TransformToBody(*hair_front_right_body_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + c->DrawModel(g_media->GetModel(SystemModelID::kHairTuft1b)); + c->PopTransform(); + } + + // Hair tuft 2. + if (hair_front_left_body_.exists()) { + c->PushTransform(); + c->TransformToBody(*hair_front_left_body_); + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + c->DrawModel(g_media->GetModel(SystemModelID::kHairTuft2)); + c->PopTransform(); + } + + // Hair tuft 3. + if (hair_ponytail_top_body_.exists()) { + c->PushTransform(); + c->TransformToBody(*hair_ponytail_top_body_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + c->DrawModel(g_media->GetModel(SystemModelID::kHairTuft3)); + c->PopTransform(); + } + + // Hair tuft 4. + if (hair_ponytail_bottom_body_.exists()) { + c->PushTransform(); + c->TransformToBody(*hair_ponytail_bottom_body_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + c->DrawModel(g_media->GetModel(SystemModelID::kHairTuft4)); + c->PopTransform(); + } + + // Torso. + c->PushTransform(); + c->TransformToBody(*body_torso_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (torso_model_.exists()) { + c->DrawModel(torso_model_->model_data()); + } + c->PopTransform(); + + // Pelvis. + c->PushTransform(); + c->TransformToBody(*body_pelvis_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (pelvis_model_.exists()) { + c->DrawModel(pelvis_model_->model_data()); + } + c->PopTransform(); + + // Right upper arm. + c->PushTransform(); + c->TransformToBody(*upper_right_arm_body_); + + // Get the distance between the shoulder joint socket and the fore-arm + // socket.. we'll use this to stretch our upper-arm to fill the gap. + float right_stretch = 1.0f; + + if (!shattered_) { + dVector3 p_shoulder; + dBodyGetRelPointPos(body_torso_->body(), upper_right_arm_joint_->anchor1[0], + upper_right_arm_joint_->anchor1[1], + upper_right_arm_joint_->anchor1[2], p_shoulder); + dVector3 p_forearm; + dBodyGetRelPointPos(lower_right_arm_body_->body(), + lower_right_arm_joint_->anchor2[0], + upper_right_arm_joint_->anchor2[1], + upper_right_arm_joint_->anchor2[2], p_forearm); + right_stretch = std::min( + 1.6f, (Vector3f(p_shoulder) - Vector3f(p_forearm)).Length() / 0.192f); + } + + // If we've got flippers instead of arms, shorten them if we've got gloves on + // so they don't intersect as badly. + if (flippers_ && have_boxing_gloves_) { + right_stretch *= 0.5f; + } + + c->Scale(1.0f, 1.0f, right_stretch); + + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (upper_arm_model_.exists()) { + c->DrawModel(upper_arm_model_->model_data()); + } + c->PopTransform(); + + // Right lower arm. + c->PushTransform(); + c->TransformToBody(*lower_right_arm_body_); + c->PushTransform(); + c->Translate(0, 0, 0.1f); + c->Scale(1.0f, 1.0f, right_stretch); + c->Translate(0.0f, 0.0f, -0.1f); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (forearm_model_.exists() && !flippers_) { + c->DrawModel(forearm_model_->model_data()); + } + c->PopTransform(); + if (!have_boxing_gloves_) { + c->Translate(0, 0, 0.04f); + if (holding_something_) { + c->Rotate(-50, 0, 1, 0); + } else { + c->Rotate(10, 0, 1, 0); + } + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (hand_model_.exists() && !flippers_) { + c->DrawModel(hand_model_->model_data()); + } + } + c->PopTransform(); + + // Right upper leg. + c->PushTransform(); + c->TransformToBody(*upper_right_leg_body_); + + // Apply stretching if still intact. + if (!shattered_) { + dVector3 p_pelvis; + dBodyGetRelPointPos(body_pelvis_->body(), + upper_right_leg_joint_->anchor1[0], + upper_right_leg_joint_->anchor1[1], + upper_right_leg_joint_->anchor1[2], p_pelvis); + dVector3 p_lower_leg; + dBodyGetRelPointPos(lower_right_leg_body_->body(), + lower_right_leg_joint_->anchor2[0], + upper_right_leg_joint_->anchor2[1], + upper_right_leg_joint_->anchor2[2], p_lower_leg); + float stretch = std::min( + 1.6f, (Vector3f(p_pelvis) - Vector3f(p_lower_leg)).Length() / 0.20f); + c->Scale(1.0f, 1.0f, stretch); + } + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (upper_leg_model_.exists()) { + c->DrawModel(upper_leg_model_->model_data()); + } + c->PopTransform(); + + // Right lower leg. + c->PushTransform(); + c->TransformToBody(*lower_right_leg_body_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (lower_leg_model_.exists()) { + c->DrawModel(lower_leg_model_->model_data()); + } + c->PopTransform(); + + c->PushTransform(); + c->TransformToBody(*right_toes_body_); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (toes_model_.exists()) { + c->DrawModel(toes_model_->model_data()); + } + c->PopTransform(); + + // OK NOW LEFT SIDE LIMBS: + c->FlipCullFace(); + + // Left upper arm. + c->PushTransform(); + c->TransformToBody(*upper_left_arm_body_); + float left_stretch = 1.0f; + + // Stretch if not shattered. + if (!shattered_) { + dVector3 p_shoulder; + dBodyGetRelPointPos(body_torso_->body(), upper_left_arm_joint_->anchor1[0], + upper_left_arm_joint_->anchor1[1], + upper_left_arm_joint_->anchor1[2], p_shoulder); + dVector3 p_forearm; + dBodyGetRelPointPos(lower_left_arm_body_->body(), + lower_left_arm_joint_->anchor2[0], + upper_left_arm_joint_->anchor2[1], + upper_left_arm_joint_->anchor2[2], p_forearm); + left_stretch = std::min( + 1.6f, (Vector3f(p_shoulder) - Vector3f(p_forearm)).Length() / 0.192f); + } + + // If we've got flippers instead of arms, shorten them if we've got gloves on + // so they don't intersect as badly. + if (flippers_ && have_boxing_gloves_) { + left_stretch *= 0.5f; + } + c->Scale(-1, 1, left_stretch); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (upper_arm_model_.exists()) c->DrawModel(upper_arm_model_->model_data()); + c->PopTransform(); + + // Left lower arm. + c->PushTransform(); + c->TransformToBody(*lower_left_arm_body_); + c->Scale(-1, 1, 1); + c->PushTransform(); + c->Translate(0, 0, 0.1f); + c->Scale(1, 1, left_stretch); + c->Translate(0, 0, -0.1f); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + } + if (forearm_model_.exists() && !flippers_) { + c->DrawModel(forearm_model_->model_data()); + } + c->PopTransform(); + if (!have_boxing_gloves_) { + c->Translate(0, 0, 0.04f); + if (holding_something_) { + c->Rotate(-50, 0, 1, 0); + } else { + c->Rotate(10, 0, 1, 0); + } + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (hand_model_.exists() && !flippers_) { + c->DrawModel(hand_model_->model_data()); + } + } + c->PopTransform(); + + // Left upper leg. + c->PushTransform(); + c->TransformToBody(*upper_left_leg_body_); + + // Stretch if not shattered. + if (!shattered_) { + dVector3 p_pelvis; + dBodyGetRelPointPos(body_pelvis_->body(), upper_left_leg_joint_->anchor1[0], + upper_left_leg_joint_->anchor1[1], + upper_left_leg_joint_->anchor1[2], p_pelvis); + dVector3 p_lower_leg; + dBodyGetRelPointPos(lower_left_leg_body_->body(), + lower_left_leg_joint_->anchor2[0], + upper_left_leg_joint_->anchor2[1], + upper_left_leg_joint_->anchor2[2], p_lower_leg); + float stretch = std::min( + 1.6f, (Vector3f(p_pelvis) - Vector3f(p_lower_leg)).Length() / 0.20f); + c->Scale(-1.0f, 1.0f, stretch); + } + if (death_scale != 1.0f) + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + if (upper_leg_model_.exists()) c->DrawModel(upper_leg_model_->model_data()); + c->PopTransform(); + + // Lower leg. + c->PushTransform(); + c->TransformToBody(*lower_left_leg_body_); + c->Scale(-1.0f, 1.0f, 1.0f); + if (death_scale != 1.0f) + c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); + if (lower_leg_model_.exists()) { + c->DrawModel(lower_leg_model_->model_data()); + } + c->PopTransform(); + + // Toes. + c->PushTransform(); + c->TransformToBody(*left_toes_body_); + c->Scale(-1, 1, 1); + if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + if (toes_model_.exists()) c->DrawModel(toes_model_->model_data()); + c->PopTransform(); + + // RESTORE CULL + c->FlipCullFace(); +} + +static void DrawRadialMeter(MeshIndexedSimpleFull* m, SimpleComponent* c, + float amt, bool flash) { + if (flash) { + c->SetColor(1, 1, 0.4f, 0.7f); + } else { + c->SetColor(1, 1, 1, 0.6f); + } + Graphics::DrawRadialMeter(m, amt); + c->DrawMesh(m); +} + +void SpazNode::Draw(FrameDef* frame_def) { +#if !BA_HEADLESS_BUILD + +#if BA_OSTYPE_MACOS + if (g_graphics_server->renderer()->debug_draw_mode()) { + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + c.SetDoubleSided(true); + c.SetColor(1, 0, 0, 0.5f); + + c.PushTransform(); + c.TransformToBody(*body_head_); + + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0.5f, 0); + c.Vertex(0, 0, 0.5f); + c.Vertex(0, 0, 0); + c.End(); + c.PopTransform(); + + c.PushTransform(); + c.TransformToBody(*body_torso_); + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0.2f, 0); + c.Vertex(0, 0, 0.2f); + c.Vertex(0, 0, 0); + c.End(); + c.PopTransform(); + + c.PushTransform(); + c.TransformToBody(*body_pelvis_); + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0.2f, 0); + c.Vertex(0, 0, 0.2f); + c.Vertex(0, 0, 0); + c.End(); + c.PopTransform(); + + c.SetColor(0.4f, 1.0f, 0.4f, 0.2f); + c.PushTransform(); + c.TransformToBody(*stand_body_); + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0.2f, 0); + c.Vertex(0, 0, 0.5f); + c.Vertex(0, 0, 0); + + c.Vertex(0, 2.0f, 0); + c.Vertex(0, 0, 0.1f); + c.Vertex(0, 0, 0); + + c.Vertex(0, 0.2f, 0); + c.Vertex(0.5f, 0, 0); + c.Vertex(0, 0, 0); + + c.Vertex(0, 2.0f, 0); + c.Vertex(0.1f, 0, 0.0f); + c.Vertex(0, 0, 0); + + c.End(); + c.PopTransform(); + + // Punch direction. + if (explicit_bool(true)) { + c.SetColor(1, 1, 0, 0.5f); + const dReal* p = dBodyGetPosition(body_torso_->body()); + c.PushTransform(); + c.Translate(p[0], p[1], p[2]); + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0, 0); + c.Vertex(2.0f * punch_dir_x_, 0, 2.0f * punch_dir_z_); + c.Vertex(0, 0.05f, 0); + c.Vertex(0, 0, 0); + c.Vertex(0, 0.05f, 0); + c.Vertex(2.0f * punch_dir_x_, 0, 2.0f * punch_dir_z_); + c.End(); + c.PopTransform(); + } + + // Run joint foot attach. + if (explicit_bool(true)) { + c.SetColor(1, 0, 0); + c.PushTransform(); + c.TransformToBody(*lower_left_leg_body_); + JointFixedEF* j = left_leg_ik_joint_; + c.Translate(j->anchor2[0], j->anchor2[1], j->anchor2[2]); + c.Rotate(90, 1, 0, 0); + c.Scale(0.5f, 0.5f, 0.5f); + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0.1f, 0.5f); + c.Vertex(0, 0, 0.5f); + c.Vertex(0, 0, 0); + c.Vertex(0, 0, 0); + c.Vertex(0, 0, 0.5f); + c.Vertex(0, 0.1f, 0.5f); + c.End(); + c.PopTransform(); + } + + // Run joint pelvis attach. + if (explicit_bool(true)) { + c.SetColor(0, 0, 1); + c.PushTransform(); + c.TransformToBody(*body_pelvis_); + JointFixedEF* j = left_leg_ik_joint_; + c.Translate(j->anchor1[0], j->anchor1[1], j->anchor1[2]); + c.Rotate(90, 1, 0, 0); + c.Scale(0.5f, 0.5f, 0.5f); + c.BeginDebugDrawTriangles(); + c.Vertex(0, 0.1f, 0.5f); + c.Vertex(0, 0, 0.5f); + c.Vertex(0, 0, 0); + c.Vertex(0, 0, 0); + c.Vertex(0, 0, 0.5f); + c.Vertex(0, 0.1f, 0.5f); + c.End(); + c.PopTransform(); + } + + c.Submit(); + } +#endif // BA_OSTYPE_MACOS + + millisecs_t scenetime = scene()->time(); + int64_t render_frame_count = frame_def->frame_number(); + RenderPass* beauty_pass = frame_def->beauty_pass(); + + float death_fade = 1.0f; + float death_scale = 1.0f; + millisecs_t since_death = 0; + float add_color[3] = {0, 0, 0}; + + if (dead_) { + since_death = scenetime - death_time_; + if (since_death > 2000) { + death_scale = 0.0f; + } else if (since_death > 1750) { + death_scale = 1.0f - (static_cast(since_death - 1750) / 250.0f); + } else { + death_scale = 1.0f; + } + + // Slowly fade down to black. + if (frozen_) { + death_fade = 1.0f; // except when frozen.. + } else { + if (since_death < 2000) { + death_fade = 1.0f - (static_cast(since_death) / 2000.0f); + } else { + death_fade = 0.0f; + } + } + } + + // Invincible! flash white. + if (invincible_) { + if (frame_def->frame_number() % 6 < 3) { + add_color[0] = 0.12f; + add_color[1] = 0.22f; + add_color[2] = 0.0f; + } + } else if (!dead_ && flashing_ > 0) { + // Flashing red. + float flash_amount = + 1.0f - std::abs(static_cast(flashing_) - 5.0f) / 5.0f; + add_color[0] = add_color[1] = 0.8f * flash_amount; + add_color[2] = 0.0f; + } else if (!dead_ && curse_death_time_ != 0) { + // Cursed. + if (scene()->stepnum() % (static_cast(100.0f - (90.0f * 1.0f))) < 5) { + if (frozen_) { + add_color[0] = 0.2f; + add_color[1] = 0.0f; + add_color[2] = 0.4f; + } else { + add_color[0] = 0.2f; + add_color[1] = 0.0f; + add_color[2] = 0.1f; + } + } else { + if (frozen_) { + add_color[0] = 0.15f; + add_color[1] = 0.15f; + add_color[2] = 0.5f; + } else { + add_color[0] = add_color[1] = add_color[2] = 0.0f; + } + } + } else if (!dead_ && (hurt_ > 0.0f) + && (scene()->stepnum() + % (static_cast(100.0f - (90.0f * hurt_))) + < 5)) { + // Flash red periodically when hurt but not dead. + if (frozen_) { + add_color[0] = 0.33f; + add_color[1] = 0.1f; + add_color[2] = 0.4f; + } else { + add_color[0] = 0.33f; + add_color[1] = 0.0f; + add_color[2] = 0.0f; + } + } else { + if (frozen_) { + if (dead_) { + // flash bright white momentarily when dying + // ..except when falling out of bounds.. its funnier to not flash then + // if ((since_death < 200) and (scene()->time() - + // last_out_of_bounds_time_ > 3000)) { + // if ((scene()->time() - last_fall_time_ < 3000) and + // (since_death < 50)) { + // } + if ((since_death < 200) + && (scene()->time() - last_out_of_bounds_time_ > 3000)) { + // if (since_death < 200) { + float flash = 1.0f - (static_cast(since_death) / 200.0f); + add_color[0] = 0.15f + flash * 0.9f; + add_color[1] = 0.15f + flash * 0.9f; + add_color[2] = 0.5f + flash * 0.6f; + } else { + add_color[0] = 0.15f; + add_color[1] = 0.15f; + add_color[2] = 0.6f; + } + } else { + // not dead.. just add a bit for frozen-ness + add_color[0] = 0.12f; + add_color[1] = 0.12f; + add_color[2] = 0.4f; + } + } else { + // not frozen. + if (dead_) { + if ((since_death < 300) + && (scene()->time() - last_out_of_bounds_time_ > 3000)) { + float flash_r = 1.0f - (static_cast(since_death) / 300.0f); + float flash_g = + std::max(0.0f, 1.0f - (static_cast(since_death) / 250.0f)); + float flash_b = + std::max(0.0f, 1.0f - (static_cast(since_death) / 170.0f)); + add_color[0] = 2.0f * flash_r; + add_color[1] = 0.25f * flash_g; + add_color[2] = 0.25f * flash_b; + } + } + } + } + + const dReal* torso_pos_raw = dBodyGetPosition(body_torso_->body()); + float torso_pos[3]; + torso_pos[0] = torso_pos_raw[0] + body_torso_->blend_offset().x; + torso_pos[1] = torso_pos_raw[1] + body_torso_->blend_offset().y; + torso_pos[2] = torso_pos_raw[2] + body_torso_->blend_offset().z; + + // Curse time. + if (curse_death_time_ > 0 && !dead_) { + millisecs_t diff = (curse_death_time_ - scenetime) / 1000 + 1; + if (diff < 9999 && diff > 0) { + char buffer[10]; + snprintf(buffer, sizeof(buffer), "%d", static_cast(diff)); + if (curse_timer_txt_ != buffer) { + curse_timer_txt_ = buffer; + curse_timer_text_group_.SetText(curse_timer_txt_); + } + float r, g, b; + if (render_frame_count % 6 < 3) { + r = 1.0f; + g = 0.7f; + b = 0.0f; + } else { + r = 0.5f; + g = 0.0f; + b = 0.0f; + } + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + c.SetColor(r, g, b); + + int elem_count = curse_timer_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(curse_timer_text_group_.GetElementTexture(e)); + c.SetShadow(-0.004f * curse_timer_text_group_.GetElementUScale(e), + -0.004f * curse_timer_text_group_.GetElementVScale(e), 0.0f, + 0.3f); + c.SetMaskUV2Texture( + curse_timer_text_group_.GetElementMaskUV2Texture(e)); + c.SetFlatness(1.0f); + c.PushTransform(); + c.Translate(torso_pos[0] - 0.2f, torso_pos[1] + 0.8f, + torso_pos[2] - 0.2f); + c.Scale(0.02f, 0.02f, 0.02f); + c.DrawMesh(curse_timer_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + + // Mini billboard 1. + if (scenetime < mini_billboard_1_end_time_ && !dead_) { + float amt = static_cast(mini_billboard_1_end_time_ - scenetime) + / static_cast(mini_billboard_1_end_time_ + - mini_billboard_1_start_time_); + if (amt > 0.0001f && amt <= 1.0f) { + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + bool flash = (scenetime - mini_billboard_1_start_time_ < 200 + && render_frame_count % 6 < 3); + if (!flash) { + c.SetTexture(mini_billboard_1_texture_); + } + c.PushTransform(); + c.Translate(torso_pos[0] - 0.2f, torso_pos[1] + 1.2f, + torso_pos[2] - 0.2f); + c.Scale(0.08f, 0.08f, 0.08f); + DrawRadialMeter(&billboard_1_mesh_, &c, amt, flash); + c.PopTransform(); + c.Submit(); + } + } + // mini billboard 2 + if (scenetime < mini_billboard_2_end_time_ && !dead_) { + float amt = static_cast(mini_billboard_2_end_time_ - scenetime) + / static_cast(mini_billboard_2_end_time_ + - mini_billboard_2_start_time_); + if (amt > 0.0001f && amt <= 1.0f) { + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + bool flash = (scenetime - mini_billboard_2_start_time_ < 200 + && render_frame_count % 6 < 3); + // if (!flash) + // c.SetTexture(mediaSet->GetTexture(mini_billboard_2_texture_)); + if (!flash) c.SetTexture(mini_billboard_2_texture_); + c.PushTransform(); + c.Translate(torso_pos[0], torso_pos[1] + 1.2f, torso_pos[2] - 0.2f); + c.Scale(0.09f, 0.09f, 0.09f); + DrawRadialMeter(&billboard_2_mesh_, &c, amt, flash); + c.PopTransform(); + c.Submit(); + } + } + // mini billboard 3 + if (scenetime < mini_billboard_3_end_time_ && !dead_) { + float amt = static_cast(mini_billboard_3_end_time_ - scenetime) + / static_cast(mini_billboard_3_end_time_ + - mini_billboard_3_start_time_); + if (amt > 0.0001f && amt <= 1.0f) { + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + bool flash = (scenetime - mini_billboard_3_start_time_ < 200 + && render_frame_count % 6 < 3); + if (!flash) { + c.SetTexture(mini_billboard_3_texture_); + } + c.PushTransform(); + c.Translate(torso_pos[0] + 0.2f, torso_pos[1] + 1.2f, + torso_pos[2] - 0.2f); + c.Scale(0.08f, 0.08f, 0.08f); + DrawRadialMeter(&billboard_3_mesh_, &c, amt, flash); + c.PopTransform(); + c.Submit(); + } + } + + /// draw our counter + if (!counter_text_.empty() && !dead_) { + { // icon + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + c.SetTexture(counter_texture_); + c.PushTransform(); + c.Translate(torso_pos[0] - 0.3f, torso_pos[1] + 1.47f, + torso_pos[2] - 0.2f); + c.Scale(1.5f * 0.2f, 1.5f * 0.2f, 1.5f * 0.2f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + } + { // text + if (counter_mesh_text_ != counter_text_) { + counter_mesh_text_ = counter_text_; + counter_text_group_.SetText(counter_mesh_text_); + } + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + int elem_count = counter_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(counter_text_group_.GetElementTexture(e)); + c.SetMaskUV2Texture(counter_text_group_.GetElementMaskUV2Texture(e)); + c.SetShadow(-0.004f * counter_text_group_.GetElementUScale(e), + -0.004f * counter_text_group_.GetElementVScale(e), 0.0f, + 0.3f); + c.SetFlatness(1.0f); + c.PushTransform(); + c.Translate(torso_pos[0] - 0.1f, torso_pos[1] + 1.34f, + torso_pos[2] - 0.2f); + c.Scale(0.01f, 0.01f, 0.01f); + c.DrawMesh(counter_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + + // draw our name + if (!name_.empty()) { + auto age = static_cast(scenetime - birth_time_); + if (explicit_bool(true)) { + if (name_mesh_txt_ != name_) { + name_mesh_txt_ = name_; + name_text_group_.SetText(name_mesh_txt_, TextMesh::HAlign::kCenter, + TextMesh::VAlign::kCenter); + } + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + float extra; + if (age < 200) { + extra = age / 200.0f; + } else { + extra = std::min(1.0f, std::max(0.0f, 1.0f - (age - 600.0f) / 200.0f)); + } + + // Make sure our max color channel is non-black. + assert(name_color_.size() == 3); + float r = name_color_[0]; + float g = name_color_[1]; + float b = name_color_[2]; + if (dead_) { + r = 0.45f + 0.2f * r; + g = 0.45f + 0.2f * g; + b = 0.45f + 0.2f * b; + } + c.SetColor(r, g, b, dead_ ? 0.7f : 1.0f); + + int elem_count = name_text_group_.GetElementCount(); + float s_extra = + (IsVRMode() || GetInterfaceType() == UIScale::kSmall) ? 1.2f : 1.0f; + + for (int e = 0; e < elem_count; e++) { + // Gracefully skip unloaded textures. + TextureData* t = name_text_group_.GetElementTexture(e); + if (!t->preloaded()) continue; + c.SetTexture(t); + c.SetMaskUV2Texture(name_text_group_.GetElementMaskUV2Texture(e)); + c.SetShadow(-0.0035f * name_text_group_.GetElementUScale(e), + -0.0035f * name_text_group_.GetElementVScale(e), 0.0f, + dead_ ? 0.25f : 0.5f); + c.SetFlatness(1.0f); + c.PushTransform(); + c.Translate(torso_pos[0] - 0.0f, torso_pos[1] + 0.89f + 0.4f * extra, + torso_pos[2] - 0.2f); + float s = (0.01f + 0.01f * extra) * death_scale; + float w = g_text_graphics->GetStringWidth(name_.c_str()); + if (w > 100.0f) s *= (100.0f / w); + s *= s_extra; + c.Scale(s, s, s); + c.DrawMesh(name_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + + // Draw our big billboard. + if (billboard_opacity_ > 0.001f && !dead_) { + float o = billboard_opacity_; + float s = o; + if (billboard_cross_out_) o *= (render_frame_count % 14 < 7) ? 0.8f : 0.2f; + const dReal* pos = dBodyGetPosition(body_torso_->body()); + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + c.SetColor(1, 1, 1, o); + c.SetTexture(billboard_texture_); + c.PushTransform(); + c.Translate(pos[0], pos[1] + 1.6f, pos[2] - 0.2f); + c.Scale(2.3f * 0.2f * s, 2.3f * 0.2f * s, 2.3f * 0.2f * s); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + + // Draw a red cross over it if they want. + if (billboard_cross_out_) { + float o2 = + billboard_opacity_ * ((render_frame_count % 14 < 7) ? 0.4f : 0.1f); + SimpleComponent c2(frame_def->overlay_3d_pass()); + c2.SetTransparent(true); + c2.SetColor(1, 0, 0, o2); + c2.PushTransform(); + c2.Translate(pos[0], pos[1] + 1.6f, pos[2] - 0.2f); + c2.Scale(2.3f * 0.2f * s, 2.3f * 0.2f * s, 2.3f * 0.2f * s); + c2.DrawModel(g_media->GetModel(SystemModelID::kCrossOut)); + c2.PopTransform(); + c2.Submit(); + } + } + + // Draw life bar if our life has changed recently. + { + millisecs_t fade_time = shattered_ ? 1000 : 2000; + float o{1.0f}; + millisecs_t since_last_hurt_change = scenetime - last_hurt_change_time_; + if (since_last_hurt_change < fade_time) { + SimpleComponent c(frame_def->overlay_3d_pass()); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.PushTransform(); + + o = 1.0f - static_cast(since_last_hurt_change) / fade_time; + o *= o; + const dReal* pos = dBodyGetPosition(body_torso_->body()); + + float p_left, p_right; + if (hurt_ < hurt_smoothed_) { + p_left = 1.0f - hurt_smoothed_; + p_right = 1.0f - hurt_; + } else { + p_right = 1.0f - hurt_smoothed_; + p_left = 1.0f - hurt_; + } + + // For the first moment start p_left at p_right so they can see a glimpse + // of green before it goes away. + if (since_last_hurt_change < 100) { + p_left += + (p_right - p_left) + * (1.0f - static_cast(since_last_hurt_change) / 100.0f); + } + + c.Translate(pos[0] - 0.25f, pos[1] + 1.35f, pos[2] - 0.2f); + c.Scale(0.5f, 0.5f, 0.5f); + + float height = 0.1f; + float half_height = height * 0.5f; + c.SetColor(0, 0, 0, 0.3f * o); + + c.PushTransform(); + c.Translate(0.5f, half_height); + c.Scale(1.1f, height + 0.1f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + + c.SetColor(0, 0.35f * o, 0, 0.3f * o); + + c.PushTransform(); + c.Translate(p_left * 0.5f, half_height); + c.Scale(p_left, height); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + + if (dead_ && scene()->stepnum() % 10 < 5) { + c.SetColor(1 * o, 0.3f, 0.0f, 1.0f * o); + } else { + c.SetColor(1 * o, 0.0f * o, 0.0f * o, 1.0f * o); + } + + c.PushTransform(); + c.Translate((p_left + p_right) * 0.5f, half_height); + c.Scale(p_right - p_left, height); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + + c.SetColor((dead_ && scene()->stepnum() % 10 < 5) ? 0.55f * o : 0.01f * o, + 0, 0, 0.4f * o); + + c.PushTransform(); + c.Translate((p_right + 1.0f) * 0.5f, half_height); + c.Scale(1.0f - p_right, height); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + + c.PopTransform(); + c.Submit(); + } + } + + // Draw all body parts with normal shading. + { + { + ObjectComponent c(beauty_pass); + DrawBodyParts(&c, true, death_fade, death_scale, add_color); + SetupEyeLidShading(&c, death_fade, add_color); + DrawEyeLids(&c, death_fade, death_scale); + c.Submit(); + } + { + ObjectComponent c(beauty_pass); + DrawEyeBalls(&c, &c, true, death_fade, death_scale, add_color); + c.Submit(); + } + + // In higher-quality mode, blur our eyeballs and eyelids a bit to look more + // fleshy. + if (frame_def->quality() >= GraphicsQuality::kHigher) { + PostProcessComponent c(frame_def->blit_pass()); + c.setEyes(true); + DrawEyeLids(&c, death_fade, death_scale); + DrawEyeBalls(&c, nullptr, false, death_fade, death_scale, add_color); + c.Submit(); + } + } + + // Wings. + if (wings_) { + ObjectComponent c(beauty_pass); + c.SetTransparent(false); + c.SetColor(1, 1, 1, 1.0f); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.4f, 0.4f, 0.4f); + c.SetTexture(g_media->GetTexture(SystemTextureID::kWings)); + + // Fade to reddish on death. + if (dead_ && !frozen_) { + float r = 0.3f + 0.7f * death_fade; + float g = 0.2f + 0.7f * (death_fade * 0.5f); + float b = 0.2f + 0.7f * (death_fade * 0.5f); + c.SetColor(r, g, b); + } + + // DEBUGGING: + if (explicit_bool(false)) { + dVector3 p_wing_l, p_wing_r; + + // Draw target. + dBodyGetRelPointPos(body_torso_->body(), kWingAttachX, kWingAttachY, + kWingAttachZ, p_wing_l); + c.PushTransform(); + c.Translate(p_wing_l[0], p_wing_l[1], p_wing_l[2]); + c.Scale(0.05f, 0.05f, 0.05f); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + + // Draw wing point. + c.PushTransform(); + c.Translate(wing_pos_left_.x, wing_pos_left_.y, wing_pos_left_.z); + c.Scale(0.1f, 0.1f, 0.1f); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + + // Draw target. + dBodyGetRelPointPos(body_torso_->body(), -kWingAttachX, kWingAttachY, + kWingAttachZ, p_wing_r); + c.PushTransform(); + c.Translate(p_wing_r[0], p_wing_r[1], p_wing_r[2]); + c.Scale(0.05f, 0.05f, 0.05f); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + + // Draw wing point. + c.PushTransform(); + c.Translate(wing_pos_right_.x, wing_pos_right_.y, wing_pos_right_.z); + c.Scale(0.1f, 0.1f, 0.1f); + c.DrawModel(g_media->GetModel(SystemModelID::kBox)); + c.PopTransform(); + } + + // To draw wings, we need a matrix positioned at our torso pointing at our + // wing points. + Vector3f torso_pos2(dBodyGetPosition(body_torso_->body())); + Vector3f torsoUp = {0.0f, 0.0f, 0.0f}; + dBodyGetRelPointPos(body_torso_->body(), 0.0f, 1.0f, 0.0f, torsoUp.v); + torsoUp -= torso_pos2; // needs to be relative to body + torsoUp.Normalize(); + + Vector3f to_left_wing = wing_pos_left_ - torso_pos2; + to_left_wing.Normalize(); + Vector3f left_wing_side = Vector3f::Cross(to_left_wing, torsoUp); + left_wing_side.Normalize(); + Vector3f left_wing_up = Vector3f::Cross(left_wing_side, to_left_wing); + left_wing_up.Normalize(); + + // Draw target. + c.PushTransform(); + c.Translate(torso_pos2.x, torso_pos2.y, torso_pos2.z); + c.MultMatrix(Matrix44fOrient(left_wing_side, left_wing_up, to_left_wing).m); + if (death_scale != 1.0f) { + c.Scale(death_scale, death_scale, death_scale); + } + c.DrawModel(g_media->GetModel(SystemModelID::kWing)); + c.PopTransform(); + + Vector3f to_right_wing = wing_pos_right_ - torso_pos2; + to_right_wing.Normalize(); + Vector3f right_wing_side = Vector3f::Cross(to_right_wing, torsoUp); + right_wing_side.Normalize(); + Vector3f right_wing_up = Vector3f::Cross(right_wing_side, to_right_wing); + right_wing_up.Normalize(); + + // Draw target. + c.PushTransform(); + c.Translate(torso_pos2.x, torso_pos2.y, torso_pos2.z); + c.MultMatrix( + Matrix44fOrient(right_wing_side, right_wing_up, to_right_wing).m); + if (death_scale != 1.0f) { + c.Scale(death_scale, death_scale, death_scale); + } + c.DrawModel(g_media->GetModel(SystemModelID::kWing)); + c.PopTransform(); + c.Submit(); + } + + // Boxing gloves. + if (have_boxing_gloves_) { + ObjectComponent c(beauty_pass); + if (frozen_) { + c.SetAddColor(0.1f, 0.1f, 0.4f); + c.SetReflection(ReflectionType::kSharper); + c.SetReflectionScale(1.4f, 1.4f, 1.4f); + } else { + c.SetReflection(ReflectionType::kChar); + c.SetReflectionScale(0.6f * death_fade, 0.55f * death_fade, + 0.55f * death_fade); + + // Add extra flash when we're new. + if (scenetime - last_got_boxing_gloves_time_ < 200) { + float amt = + (static_cast(scenetime - last_got_boxing_gloves_time_) + / 2000.0f); + amt = 1.0f - (amt * amt); + c.SetAddColor(add_color[0] + amt * 0.4f, add_color[1] + amt * 0.4f, + add_color[2] + amt * 0.1f); + c.SetColor(1.0f + amt * 6.0f, 1.0f + amt * 6.0f, 1.0f + amt * 3.0f); + } else { + c.SetAddColor(add_color[0], add_color[1], add_color[2]); + + if (boxing_gloves_flashing_ && render_frame_count % 6 < 2) { + c.SetColor(2.0f, 2.0f, 2.0f); + } else { + c.SetColor(death_fade, death_fade, death_fade); + } + } + } + c.SetLightShadow(LightShadowType::kObject); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBoxingGlove)); + + c.PushTransform(); + c.TransformToBody(*lower_right_arm_body_); + if (death_scale != 1.0f) { + c.Scale(death_scale, death_scale, death_scale); + } + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + + c.FlipCullFace(); + c.PushTransform(); + c.TransformToBody(*lower_left_arm_body_); + c.Scale(-1.0f, 1.0f, 1.0f); + if (death_scale != 1.0f) { + c.Scale(death_scale, death_scale, death_scale); + } + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.FlipCullFace(); + c.PopTransform(); + c.Submit(); + } + + // Light/shadows. + { + float sc[3] = {shadow_color_[0], shadow_color_[1], shadow_color_[2]}; + + if (frozen_) { + float freeze_color[] = {0.3f, 0.3f, 0.7f}; + float weight = 0.7f; + sc[0] = weight * freeze_color[0] + (1.0f - weight) * sc[0]; + sc[1] = weight * freeze_color[1] + (1.0f - weight) * sc[1]; + sc[2] = weight * freeze_color[2] + (1.0f - weight) * sc[2]; + } + + FullShadowSet* full_shadows = full_shadow_set_.get(); + if (full_shadows) { + DrawBrightSpot(full_shadows->lower_left_leg_shadow_, 0.3f * death_scale, + death_fade * (frozen_ ? 0.3f : 0.2f), sc); + DrawBrightSpot(full_shadows->lower_right_leg_shadow_, 0.3f * death_scale, + death_fade * (frozen_ ? 0.3f : 0.2f), sc); + DrawBrightSpot(full_shadows->head_shadow_, 0.45f * death_scale, + death_fade * (frozen_ ? 0.8f : 0.14f), sc); + } + + if (full_shadows) { + DrawShadow(full_shadows->torso_shadow_, 0.19f * death_scale, 0.9f, sc); + DrawShadow(full_shadows->head_shadow_, 0.15f * death_scale, 0.7f, sc); + DrawShadow(full_shadows->pelvis_shadow_, 0.15f * death_scale, 0.7f, sc); + DrawShadow(full_shadows->lower_left_leg_shadow_, 0.08f * death_scale, + 1.0f, sc); + DrawShadow(full_shadows->lower_right_leg_shadow_, 0.08f * death_scale, + 1.0f, sc); + DrawShadow(full_shadows->upper_left_leg_shadow_, 0.08f * death_scale, + 1.0f, sc); + DrawShadow(full_shadows->upper_right_leg_shadow_, 0.08f * death_scale, + 1.0f, sc); + DrawShadow(full_shadows->upper_left_arm_shadow_, 0.08f * death_scale, + 0.5f, sc); + DrawShadow(full_shadows->lower_left_arm_shadow_, 0.08f * death_scale, + 0.3f, sc); + DrawShadow(full_shadows->lower_right_arm_shadow_, 0.08f * death_scale, + 0.3f, sc); + DrawShadow(full_shadows->upper_right_arm_shadow_, 0.08f * death_scale, + 0.5f, sc); + } else { + SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + assert(simple_shadows); + DrawShadow(simple_shadows->shadow_, 0.2f * death_scale, 2.0f, sc); + } + } +#endif // !BA_HEADLESS_BUILD +} // NOLINT (yes i know this is too big) + +void SpazNode::OnGraphicsQualityChanged(GraphicsQuality q) { + UpdateForGraphicsQuality(q); +} + +void SpazNode::UpdateForGraphicsQuality(GraphicsQuality quality) { +#if !BA_HEADLESS_BUILD + if (quality >= GraphicsQuality::kMedium) { + full_shadow_set_ = Object::New(); + simple_shadow_set_.Clear(); + } else { + simple_shadow_set_ = Object::New(); + full_shadow_set_.Clear(); + } +#endif // !BA_HEADLESS_BUILD +} + +auto SpazNode::IsBrokenBodyPart(int id) -> bool { + switch (id) { + case kHeadBodyID: + return static_cast(shatter_damage_ & kNeckJointBroken); + case kUpperRightArmBodyID: + return static_cast(shatter_damage_ & kUpperRightArmJointBroken); + case kLowerRightArmBodyID: + return static_cast(shatter_damage_ & kLowerRightArmJointBroken); + case kUpperLeftArmBodyID: + return static_cast(shatter_damage_ & kUpperLeftArmJointBroken); + case kLowerLeftArmBodyID: + return static_cast(shatter_damage_ & kLowerLeftArmJointBroken); + case kUpperRightLegBodyID: + return static_cast(shatter_damage_ & kUpperRightLegJointBroken); + case kLowerRightLegBodyID: + return static_cast(shatter_damage_ & kLowerRightLegJointBroken); + case kUpperLeftLegBodyID: + return static_cast(shatter_damage_ & kUpperLeftLegJointBroken); + case kLowerLeftLegBodyID: + return static_cast(shatter_damage_ & kLowerLeftLegJointBroken); + case kPelvisBodyID: + return static_cast(shatter_damage_ & kPelvisJointBroken); + default: + return false; + } +} + +auto SpazNode::PreFilterCollision(RigidBody* colliding_body, + RigidBody* opposingbody) -> bool { + assert(colliding_body->part()->node() == this); + if (opposingbody->part()->node() == this) { + // If self-collide has gone down to zero we can just skip this completely. + // if (!frozen_ and limb_self_collide_ < 0.01f) return false; + + int ourID = colliding_body->id(); + int theirID = opposingbody->id(); + + // Special case - if we're a broken off bodypart, collide with anything. + if (shattered_ && IsBrokenBodyPart(ourID)) { + return true; + } + + // Get nitpicky with our self-collisions. + switch (ourID) { + case kHeadBodyID: + case kTorsoBodyID: + // Head and torso will collide with anyone who wants to + // (leave the decision up to them). + return true; + break; + case kLowerLeftArmBodyID: + // Lower arms collide with head, torso, and upper legs + // and upper arms if shattered. + switch (theirID) { + case kHeadBodyID: + case kTorsoBodyID: + case kUpperLeftLegBodyID: + return true; + default: + return false; + } + break; + case kLowerRightArmBodyID: + // Lower arms collide with head, torso, and upper legs. + switch (theirID) { + case kHeadBodyID: + case kTorsoBodyID: + case kUpperRightLegBodyID: + return true; + default: + return false; + } + break; + case kUpperLeftArmBodyID: // NOLINT(bugprone-branch-clone) + return false; + break; + case kUpperRightArmBodyID: + return false; + break; + case kUpperLeftLegBodyID: + // Collide with lower arm. + switch (theirID) { // NOLINT + case kLowerLeftArmBodyID: + return true; + default: + return false; + } + break; + case kUpperRightLegBodyID: + // collide with lower arm + switch (theirID) { // NOLINT + case kLowerRightArmBodyID: + return true; + default: + return false; + } + break; + case kLowerLeftLegBodyID: + // collide with opposite lower leg + switch (theirID) { // NOLINT + case kLowerRightLegBodyID: + return true; + default: + return false; + } + break; + case kLowerRightLegBodyID: + // lower right leg collides with lower left leg + switch (theirID) { // NOLINT + case kLowerLeftLegBodyID: + return true; + default: + return false; + } + break; + default: + // default to no collisions elsewhere + return false; + break; + } + } else { + // Non-us opposing node. + + // We ignore bumpers if we're injured, frozen, or if a non-roller-ball part + // of us is hitting it. + { + uint32_t f = opposingbody->flags(); + if (f & RigidBody::kIsBumper) { + if ((knockout_) || (frozen_) || (balance_ < 50) + || colliding_body->part() != &roller_part_) + return false; + } + } + } + + if (colliding_body->id() == kRollerBodyID) { + // Never collide against shrunken roller-ball. + if (ball_size_ <= 0.0f) return false; + } + return true; +} + +auto SpazNode::CollideCallback(dContact* c, int count, + RigidBody* colliding_body, + RigidBody* opposingbody) -> bool { + // Keep track of whether our toes are touching something besides us + // if (colliding_body == left_toes_body_.get() and opposingbody->getNode() != + // this) _toesTouchingL = true; if (colliding_body == right_toes_body_.get() + // and opposingbody->getNode() != this) _toesTouchingR = true; _toesTouchingL + // = (colliding_body == left_toes_body_.get() and opposingbody->getNode() != + // this); _toesTouchingR = (colliding_body == right_toes_body_.get() and + // opposingbody->getNode() != this); + + // hair collide with most anything but weakly.. + if (colliding_body->part() == &hair_part_ + || opposingbody->part() == &hair_part_) { + // Hair doesnt collide with hair. + if (colliding_body->part() == opposingbody->part()) return false; + + // ignore bumpers.. + if (opposingbody->flags() & RigidBody::kIsBumper) return false; + + // drop stiffness/damping/friction pretty low.. + float stiffness = 200.0f; + float damping = 10.0f; + + float erp, cfm; + CalcERPCFM(stiffness, damping, &erp, &cfm); + for (int i = 0; i < count; i++) { + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + c[i].surface.mu = 0.1f; + } + return true; + } + + if (colliding_body->part() == &limbs_part_lower_) { + // Drop friction if lower arms are hitting upper legs. + if ((colliding_body == lower_left_arm_body_.get() + || colliding_body == lower_right_arm_body_.get()) + && !shattered_) { + for (int i = 0; i < count; i++) { + c[i].surface.mu = 0.0f; + } + } + + // Now drop collision forces across the board. + float stiffness = 10.0f; + float damping = 1.0f; + + if (colliding_body == left_toes_body_.get() + || colliding_body == right_toes_body_.get()) { + stiffness *= kToesCollideStiffness; + damping *= kToesCollideDamping; + + // Also drop friction on toes. + for (int i = 0; i < count; i++) { + c[i].surface.mu *= 0.1f; + } + } + if (colliding_body == lower_right_leg_body_.get() + || colliding_body == lower_left_leg_body_.get()) { + stiffness *= kLowerLegCollideStiffness; + damping *= kLowerLegCollideDamping; + } + if (shattered_) { + stiffness *= 100.0f; + damping *= 10.0f; + } + + // If we're hitting ourself, drop all forces based on our self-collide + // level. + if (opposingbody->part()->node() == this && !frozen_) { + for (int i = 0; i < count; i++) { + c[i].surface.mu = 0.0f; + } + } + + // If we're punching, lets crank up stiffness on our punching hand + // so it looks like its responding to stuff its hitting. + if (punch_ && !dead_) { + if ((colliding_body == lower_right_arm_body_.get() && punch_right_) + || (colliding_body == lower_left_arm_body_.get() && !punch_right_)) { + stiffness *= 200.0f; + damping *= 20.0f; + } + } + + float erp, cfm; + CalcERPCFM(stiffness, damping, &erp, &cfm); + for (int i = 0; i < count; i++) { + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + } + } else if (colliding_body->part() == &limbs_part_upper_) { + float stiffness = 10; + float damping = 1; + float erp, cfm; + if (colliding_body == upper_right_leg_body_.get() + || colliding_body == upper_left_leg_body_.get()) { + stiffness *= kUpperLegCollideStiffness; + damping *= kUpperLegCollideDamping; + } + + // Keeps our arms from pushing into our head. + stiffness *= 10.0f; + if (shattered_) { + stiffness *= 100.0f; + damping *= 10.0f; + } + CalcERPCFM(stiffness, damping, &erp, &cfm); + for (int i = 0; i < count; i++) { + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + } + } + + if (colliding_body->part() == &spaz_part_) { + float stiffness = 5000; + float damping = 0.001f; + float erp, cfm; + CalcERPCFM(stiffness, damping, &erp, &cfm); + for (int i = 0; i < count; i++) { + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + } + } + + // If we're frozen and shattered, lets slide! + if (frozen_) { + for (int i = 0; i < count; i++) { + c[i].surface.mu = 0.4f; + } + } + + // Muck with roller friction. + if (colliding_body->id() == kRollerBodyID) { + // For non-bumper collisions, drop collision forces on the side. + // (we want more friction on the bottom of our roller ball than on the + // sides). + uint32_t f = opposingbody->flags(); + if (!(f & RigidBody::kIsBumper)) { + for (int i = 0; i < count; i++) { + // Let's use world-down instead. + dVector3 down = {0, 1, 0}; + float dot = std::abs(dDOT(c[i].geom.normal, down)); + if (dot > 1) { + dot = 1; + } else if (dot < 0) { + dot = 0; + } + + if (dot < 0.6f) { + // give our roller a kick away from vertical terrain surfaces + if ((f & RigidBody::kIsTerrain)) { + dBodyID b = body_roller_->body(); + dBodyAddForce(b, c[i].geom.normal[0] * 100.0f, + c[i].geom.normal[1] * 100.0f, + c[i].geom.normal[2] * 100.0f); + } +#if 1 + // Override stiffness and damping on our little parts + float stiffness = 800.0f; + float damping = 0.001f; + float erp, cfm; + CalcERPCFM(stiffness, damping, &erp, &cfm); + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + c[i].surface.mu = 0.0f; +#endif + } else { + // trying to get a well-behaved floor-response... + if (!hockey_) { + float stiffness = 7000.0f; + float damping = 7.0f; + float erp, cfm; + CalcERPCFM(stiffness, damping, &erp, &cfm); + c[i].surface.soft_erp = erp; + c[i].surface.soft_cfm = cfm; + c[i].surface.mu *= 1.0f; + } + } + } + } + } else if (colliding_body->id() != kRollerBodyID) { + // Drop friction on all our non-roller-ball parts. + for (int i = 0; i < count; i++) { + c[i].surface.mu *= 0.3f; + } + } + + // Keep track of when stuff is hitting our head, so we know when to calc + // damage from head whiplash. + if (colliding_body == body_head_.get() && opposingbody->part()->node() != this + && opposingbody->can_cause_impact_damage()) { + last_head_collide_time_ = scene()->time(); + } + + return true; +} + +void SpazNode::Stand(float x, float y, float z, float angle) { + y -= 0.7f; + + // If we're getting teleported we dont wanna pull things along with us. + DropHeldObject(); + spaz_part_.KillConstraints(); + hair_part_.KillConstraints(); + punch_part_.KillConstraints(); + pickup_part_.KillConstraints(); + extras_part_.KillConstraints(); + roller_part_.KillConstraints(); + limbs_part_upper_.KillConstraints(); + limbs_part_lower_.KillConstraints(); + + // So this doesn't trip our jolt mechanisms. + jolt_head_vel_[0] = jolt_head_vel_[1] = jolt_head_vel_[2] = 0.0f; + + dQuaternion iq; + dQFromAxisAndAngle(iq, 0, 1, 0, angle * (kPi / 180.0f)); + + dBodyID b; + + // Head + b = body_head_->body(); + dBodyEnable(b); + dBodySetPosition(b, x, y + 2.25f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Torso + b = body_torso_->body(); + dBodyEnable(b); + dBodySetPosition(b, x, y + 1.8f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // pelvis + b = body_pelvis_->body(); + dBodyEnable(b); + dBodySetPosition(b, x, y + 1.66f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Roller + b = body_roller_->body(); + dBodyEnable(b); + dBodySetPosition(b, x, y + 1.6f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Stand + b = stand_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x, y + 1.8f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Upper Right Arm + b = upper_right_arm_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x - 0.17f, y + 1.9f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Lower Right Arm + b = lower_right_arm_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x - 0.17f, y + 1.9f, z + 0.07f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Upper Left Arm + b = upper_left_arm_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x + 0.17f, y + 1.9f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Lower Left Arm + b = lower_left_arm_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x + 0.17f, y + 1.9f, z + 0.07f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Upper Right Leg + b = upper_right_leg_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x - 0.1f, y + 1.65f, z); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Lower Right Leg + b = lower_right_leg_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x - 0.1f, y + 1.65f, z + 0.05f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Right Toes + b = right_toes_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x - 0.1f, y + 1.7f, z + 0.1f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Upper Left Leg + b = upper_left_leg_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x + 0.1f, y + 1.65f, z + 0.00f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Lower Left Leg + b = lower_left_leg_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x + 0.1f, y + 1.65f, z + 0.05f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // Left Toes + b = left_toes_body_->body(); + dBodyEnable(b); + dBodySetPosition(b, x + 0.1f, y + 1.7f, z + 0.1f); + dBodySetLinearVel(b, 0, 0, 0); + dBodySetAngularVel(b, 0, 0, 0); + dBodySetQuaternion(b, iq); + dBodySetForce(b, 0, 0, 0); + + // If we have hair. + if (hair_front_right_joint_) PositionBodyForJoint(hair_front_right_joint_); + if (hair_front_left_joint_) PositionBodyForJoint(hair_front_left_joint_); + if (hair_ponytail_top_joint_) PositionBodyForJoint(hair_ponytail_top_joint_); + if (hair_ponytail_bottom_joint_) + PositionBodyForJoint(hair_ponytail_bottom_joint_); +} + +auto SpazNode::GetRigidBody(int id) -> RigidBody* { + // Ewwww this should be automatic. + switch (id) { + case kHeadBodyID: + return body_head_.get(); + break; + case kTorsoBodyID: + return body_torso_.get(); + break; + case kPunchBodyID: + return body_punch_.get(); + break; + case kPickupBodyID: + return body_pickup_.get(); + break; + case kPelvisBodyID: + return body_pelvis_.get(); + break; + case kRollerBodyID: + return body_roller_.get(); + break; + case kStandBodyID: + return stand_body_.get(); + break; + case kUpperRightArmBodyID: + return upper_right_arm_body_.get(); + break; + case kLowerRightArmBodyID: + return lower_right_arm_body_.get(); + break; + case kUpperLeftArmBodyID: + return upper_left_arm_body_.get(); + break; + case kLowerLeftArmBodyID: + return lower_left_arm_body_.get(); + break; + case kUpperRightLegBodyID: + return upper_right_leg_body_.get(); + break; + case kLowerRightLegBodyID: + return lower_right_leg_body_.get(); + break; + case kUpperLeftLegBodyID: + return upper_left_leg_body_.get(); + break; + case kLowerLeftLegBodyID: + return lower_left_leg_body_.get(); + break; + case kLeftToesBodyID: + return left_toes_body_.get(); + break; + case kRightToesBodyID: + return right_toes_body_.get(); + break; + case kHairFrontRightBodyID: + return hair_front_right_body_.get(); + break; + case kHairFrontLeftBodyID: + return hair_front_left_body_.get(); + break; + case kHairPonyTailTopBodyID: + return hair_ponytail_top_body_.get(); + break; + case kHairPonyTailBottomBodyID: + return hair_ponytail_bottom_body_.get(); + break; + default: + Log("Error: Request for unknown spaz body: " + std::to_string(id)); + break; + } + + return nullptr; +} + +void SpazNode::GetRigidBodyPickupLocations(int id, float* obj, float* character, + float* hand1, float* hand2) { + if (id == kHeadBodyID) { + obj[0] = 0; + obj[1] = 0; + obj[2] = 0; + } else { + obj[0] = obj[1] = obj[2] = 0; + } + + character[0] = character[1] = character[2] = 0.0f; + character[1] = -0.15f; + character[2] = 0.05f; + + hand1[0] = hand1[1] = hand1[2] = 0.0f; + hand2[0] = hand2[1] = hand2[2] = 0.0f; +} +void SpazNode::DropHeldObject() { + if (holding_something_) { + if (hold_node_.exists()) { + assert(pickup_joint_.IsAlive()); + pickup_joint_.Kill(); + } + assert(!pickup_joint_.IsAlive()); + + holding_something_ = false; + hold_body_ = 0; + + // Dispatch user messages last now that all is in place. + if (hold_node_.exists()) { + hold_node_->DispatchDroppedMessage(this); + } + DispatchDropMessage(); + } +} + +void SpazNode::CreateHair() { + // Assume all already exists in this case. + if (hair_front_right_body_.exists()) return; + + // Front right tuft. + hair_front_right_body_ = + Object::New(kHairFrontRightBodyID, &hair_part_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideAll, RigidBody::kCollideAll); + hair_front_right_body_->AddCallback(StaticCollideCallback, this); + hair_front_right_body_->SetDimensions(0.07f, 0.13f, 0, 0, 0, 0, 0.01f); + + hair_front_right_joint_ = CreateFixedJoint( + body_head_.get(), hair_front_right_body_.get(), 0, 0, // lin stiff/damp + 0, 0, // ang stiff/damp + -0.17f, 0.19f, 0.18f, // b1 anchor + 0, -0.08f, -0.12f // b2 anchor + ); // NOLINT (whitespace/parens) + + // Rotate it right a bit. + dQFromAxisAndAngle(hair_front_right_joint_->qrel, 0, 1, 0, -1.1f); + + // Front left tuft. + hair_front_left_body_ = + Object::New(kHairFrontLeftBodyID, &hair_part_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideAll, RigidBody::kCollideAll); + hair_front_left_body_->AddCallback(StaticCollideCallback, this); + hair_front_left_body_->SetDimensions(0.04f, 0.13f, 0, 0.07f, 0.13f, 0, 0.01f); + + hair_front_left_joint_ = CreateFixedJoint( + body_head_.get(), hair_front_left_body_.get(), 0, 0, // lin stiff/damp + 0, 0, // ang stiff/damp + 0.13f, 0.11f, 0.13f, // b1 anchor + 0, -0.08f, -0.12f // b2 anchor + ); // NOLINT (whitespace/parens) + + // Rotate it left a bit. + dQFromAxisAndAngle(hair_front_left_joint_->qrel, 0, 1, 0, 1.1f); + + // Pony tail top. + hair_ponytail_top_body_ = + Object::New(kHairPonyTailTopBodyID, &hair_part_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideAll, RigidBody::kCollideAll); + hair_ponytail_top_body_->AddCallback(StaticCollideCallback, this); + hair_ponytail_top_body_->SetDimensions(0.09f, 0.1f, 0, 0, 0, 0, 0.01f); + + hair_ponytail_top_joint_ = CreateFixedJoint( + body_head_.get(), hair_ponytail_top_body_.get(), 0, 0, // lin stiff/damp + 0, 0, // ang stiff/damp + 0, 0.3f, -0.21f, // b1 anchor + 0, -0.01f, 0.1f // b2 anchor + ); // NOLINT (whitespace/parens) + // rotate it up a bit.. + dQFromAxisAndAngle(hair_ponytail_top_joint_->qrel, 1, 0, 0, 1.1f); + + // Pony tail bottom. + hair_ponytail_bottom_body_ = + Object::New(kHairPonyTailBottomBodyID, &hair_part_, + RigidBody::Type::kBody, RigidBody::Shape::kCapsule, + RigidBody::kCollideNone, RigidBody::kCollideNone); + hair_ponytail_bottom_body_->AddCallback(StaticCollideCallback, this); + hair_ponytail_bottom_body_->SetDimensions(0.09f, 0.13f, 0, 0, 0, 0, 0.01f); + + hair_ponytail_bottom_joint_ = CreateFixedJoint( + hair_ponytail_top_body_.get(), hair_ponytail_bottom_body_.get(), 0, + 0, // lin stiff/damp + 0, 0, // ang stiff/damp + 0, 0.01f, -0.1f, // b1 anchor + 0, -0.01f, 0.12f // b2 anchor + ); // NOLINT (whitespace/parens) + + // Set joint values. + UpdateJoints(); +} +void SpazNode::DestroyHair() { + if (hair_front_right_joint_) dJointDestroy(hair_front_right_joint_); + hair_front_right_joint_ = nullptr; + + if (hair_front_left_joint_) dJointDestroy(hair_front_left_joint_); + hair_front_left_joint_ = nullptr; + + if (hair_ponytail_top_joint_) dJointDestroy(hair_ponytail_top_joint_); + hair_ponytail_top_joint_ = nullptr; + + if (hair_ponytail_bottom_joint_) dJointDestroy(hair_ponytail_bottom_joint_); + hair_ponytail_bottom_joint_ = nullptr; +} + +auto SpazNode::GetRollerMaterials() const -> std::vector { + return roller_part_.GetMaterials(); +} + +void SpazNode::SetRollerMaterials(const std::vector& vals) { + roller_part_.SetMaterials(vals); +} + +auto SpazNode::GetExtrasMaterials() const -> std::vector { + return extras_part_.GetMaterials(); +} + +void SpazNode::SetExtrasMaterials(const std::vector& vals) { + extras_part_.SetMaterials(vals); + limbs_part_upper_.SetMaterials(vals); + limbs_part_lower_.SetMaterials(vals); + hair_part_.SetMaterials(vals); +} + +auto SpazNode::GetPunchMaterials() const -> std::vector { + return punch_part_.GetMaterials(); +} + +void SpazNode::SetPunchMaterials(const std::vector& vals) { + punch_part_.SetMaterials(vals); +} + +auto SpazNode::GetPickupMaterials() const -> std::vector { + return pickup_part_.GetMaterials(); +} + +void SpazNode::SetPickupMaterials(const std::vector& vals) { + pickup_part_.SetMaterials(vals); +} + +auto SpazNode::GetMaterials() const -> std::vector { + return spaz_part_.GetMaterials(); +} + +void SpazNode::SetMaterials(const std::vector& vals) { + spaz_part_.SetMaterials(vals); +} + +void SpazNode::SetNameColor(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of length 3 for name_color"); + name_color_ = vals; +} + +void SpazNode::set_highlight(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of length 3 for highlight"); + highlight_ = vals; +} + +void SpazNode::SetColor(const std::vector& vals) { + if (vals.size() != 3) + throw Exception("Expected float array of length 3 for color"); + color_ = vals; + + // If this gets changed, make sure to change shadow-color in the constructor + // to match. + assert(shadow_color_.size() == 3); + shadow_color_[0] = color_[0] * 0.5f; + shadow_color_[1] = color_[1] * 0.5f; + shadow_color_[2] = color_[2] * 0.5f; +} + +void SpazNode::SetHurt(float val) { + float prev_hurt = hurt_; + hurt_ = std::min(1.0f, val); + if (prev_hurt != hurt_) { + last_hurt_change_time_ = scene()->time(); + } +} + +void SpazNode::SetFrozen(bool val) { + frozen_ = val; + + // Hmm; dont remember why this is necessary. + if (!frozen_) { + dBodyEnable(body_head_->body()); + } + + // Mark the time when we're newly frozen. We don't + // shatter based on impulse for a short time thereafter. + last_shatter_test_time_ = scene()->time(); + UpdateJoints(); +} + +void SpazNode::SetHaveBoxingGloves(bool val) { + have_boxing_gloves_ = val; + + // If we just got them (and aren't new ourself) lets flash. + if (have_boxing_gloves_ && (scene()->time() - birth_time_ > 100)) { + last_got_boxing_gloves_time_ = scene()->time(); + } +} + +void SpazNode::SetIsAreaOfInterest(bool val) { + // Create if need be. + if (val && area_of_interest_ == nullptr) { + area_of_interest_ = g_graphics->camera()->NewAreaOfInterest(); + UpdateAreaOfInterest(); + } + + // Destroy if need be. + if (!val && area_of_interest_) { + g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + area_of_interest_ = nullptr; + } +} + +void SpazNode::SetCurseDeathTime(millisecs_t val) { + curse_death_time_ = val; + + // Start ticking sound. + if (curse_death_time_ != 0) { + if (tick_play_id_ == 0xFFFFFFFF) { + AudioSource* s = g_audio->SourceBeginNew(); + if (s) { + s->SetLooping(true); + const dReal* p_head = dGeomGetPosition(body_head_->geom()); + s->SetPosition(p_head[0], p_head[1], p_head[2]); + tick_play_id_ = + s->Play(g_media->GetSound(SystemSoundID::kTickingCrazy)); + s->End(); + } + } + } else { + // Stop ticking sound. + if (tick_play_id_ != 0xFFFFFFFF) { + g_audio->PushSourceStopSoundCall(tick_play_id_); + tick_play_id_ = 0xFFFFFFFF; + } + } +} + +void SpazNode::SetShattered(int val) { + bool was_shattered = (shattered_ != 0); + shattered_ = val; + + if (shattered_) { + // Calc which parts are shattered. + shatter_damage_ = 0; + + float shatter_neck, shatter_pelvis, shatter_upper, shatter_lower; + // We have a few breakage patterns depending on how we died. + + // Shattering ice or curse explosions generally totally break us up. + bool extreme = (frozen_ || (shattered_ == 2)); + if (extreme) { + shatter_neck = 0.95f; + shatter_pelvis = 0.95f; + shatter_upper = 0.8f; + shatter_lower = 0.6f; + } else if (last_hit_was_punch_) { + // Punches mostly take heads off or break torsos in half. + if (Utils::precalc_rands_2[(stream_id() * 31 + 112) % kPrecalcRandsCount] + > 0.3f) { + shatter_neck = 0.9f; + shatter_pelvis = 0.1f; + } else { + shatter_neck = 0.1f; + shatter_pelvis = 0.9f; + } + shatter_upper = 0.05f; + shatter_lower = 0.025f; + } else { + shatter_neck = 0.9f; + shatter_pelvis = 0.8f; + shatter_upper = 0.4f; + shatter_lower = 0.07f; + } + + // in kid-friendly mode, don't shatter anything.. + if (explicit_bool(true)) { + float rand1 = + Utils::precalc_rands_1[(stream_id() * 3 + 1) % kPrecalcRandsCount]; + float rand2 = + Utils::precalc_rands_2[(stream_id() * 2 + 111) % kPrecalcRandsCount]; + float rand3 = + Utils::precalc_rands_3[(stream_id() * 4 + 7) % kPrecalcRandsCount]; + float rand4 = + Utils::precalc_rands_1[(stream_id() * 7 + 78) % kPrecalcRandsCount]; + float rand5 = Utils::precalc_rands_3[(stream_id()) % kPrecalcRandsCount]; + float rand6 = + Utils::precalc_rands_2[(stream_id() / 2 + 17) % kPrecalcRandsCount]; + float rand7 = + Utils::precalc_rands_1[(stream_id() * 10) % kPrecalcRandsCount]; + float rand8 = + Utils::precalc_rands_3[(stream_id() * 17 + 2) % kPrecalcRandsCount]; + float rand9 = + Utils::precalc_rands_2[(stream_id() * 13 + 22) % kPrecalcRandsCount]; + float rand10 = + Utils::precalc_rands_2[(stream_id() + 19) % kPrecalcRandsCount]; + + // Head/mid-torso are most common losses. + if (rand1 < shatter_neck) shatter_damage_ |= kNeckJointBroken; + if (rand2 < shatter_pelvis) shatter_damage_ |= kPelvisJointBroken; + + // Followed by upper arm/leg attaches. + if (rand3 < shatter_upper) shatter_damage_ |= kUpperRightArmJointBroken; + if (rand4 < shatter_upper) shatter_damage_ |= kUpperLeftArmJointBroken; + if (rand5 < shatter_upper) shatter_damage_ |= kUpperRightLegJointBroken; + if (rand6 < shatter_upper) shatter_damage_ |= kUpperLeftLegJointBroken; + + // Followed by mid arm/leg attaches. + if (rand7 < shatter_lower) shatter_damage_ |= kLowerRightArmJointBroken; + if (rand8 < shatter_lower) shatter_damage_ |= kLowerLeftArmJointBroken; + if (rand9 < shatter_lower) shatter_damage_ |= kLowerRightLegJointBroken; + if (rand10 < shatter_lower) shatter_damage_ |= kLowerLeftLegJointBroken; + } + + // Stop any sound we're making if we're shattering. + if (!was_shattered) { + g_audio->PushSourceStopSoundCall(voice_play_id_); + if (tick_play_id_ != 0xFFFFFFFF) { + g_audio->PushSourceStopSoundCall(tick_play_id_); + tick_play_id_ = 0xFFFFFFFF; + } + } + } +} + +void SpazNode::SetDead(bool val) { + bool was_dead = dead_; + dead_ = val; + if (dead_ && !was_dead) { + death_time_ = scene()->time(); + + // Lose our area-of-interest. + if (area_of_interest_) { + g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + area_of_interest_ = nullptr; + } + + // Drop whatever we're holding. + DropHeldObject(); + + // Scream on death unless we're already doing our fall scream, + // in which case we just keep on doing that. + if (voice_play_id_ != fall_play_id_ + || !g_audio->IsSoundPlaying(fall_play_id_)) { + g_audio->PushSourceStopSoundCall(voice_play_id_); + + // Only make sound if we're not shattered. + if (!shattered_) { + if (Sound* sound = GetRandomMedia(death_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + const dReal* p_head = dGeomGetPosition(body_head_->geom()); + source->SetPosition(p_head[0], p_head[1], p_head[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + source->End(); + } + } + } + } + if (tick_play_id_ != 0xFFFFFFFF) { + g_audio->PushSourceStopSoundCall(tick_play_id_); + tick_play_id_ = 0xFFFFFFFF; + } + } +} + +void SpazNode::SetStyle(const std::string& val) { + style_ = val; + dull_reflection_ = (style_ == "ninja" || style_ == "kronk"); + ninja_ = (style_ == "ninja"); + fat_ = (style_ == "mel" || style_ == "pirate" || style_ == "frosty" + || style_ == "santa"); + pirate_ = (style_ == "pirate"); + frosty_ = (style_ == "frosty"); + + // Start with defaults. + female_ = false; + female_hair_ = false; + eye_ball_color_red_ = 0.46f; + eye_ball_color_green_ = 0.38f; + eye_ball_color_blue_ = 0.36f; + torso_radius_ = 0.15f; + shoulder_offset_x_ = 0.0f; + shoulder_offset_y_ = 0.0f; + shoulder_offset_z_ = 0.0f; + has_eyelids_ = true; + eye_scale_ = 1.0f; + eye_lid_color_red_ = 0.5f; + eye_lid_color_green_ = 0.3f; + eye_lid_color_blue_ = 0.2f; + reflection_scale_ = 0.1f; + default_eye_lid_angle_ = 0.0f; + eye_offset_x_ = 0.065f; + eye_offset_y_ = -0.036f; + eye_offset_z_ = 0.205f; + eye_color_red_ = 0.5f; + eye_color_green_ = 0.5f; + eye_color_blue_ = 1.2f; + flippers_ = false; + wings_ = false; + + if (style_ == "bear") { + eye_ball_color_red_ = 0.5f; + eye_ball_color_green_ = 0.5f; + eye_ball_color_blue_ = 0.5f; + eye_lid_color_red_ = 0.2f; + eye_lid_color_green_ = 0.1f; + eye_lid_color_blue_ = 0.1f; + eye_color_red_ = 0.0f; + eye_color_green_ = 0.0f; + eye_color_blue_ = 0.0f; + torso_radius_ = 0.25f; + shoulder_offset_x_ = -0.02f; + shoulder_offset_y_ = -0.01f; + shoulder_offset_z_ = 0.01f; + eye_scale_ = 0.73f; + has_eyelids_ = false; + eye_offset_y_ += 0.1f; + reflection_scale_ = 0.05f; + } else if (style_ == "penguin") { + flippers_ = true; + eye_ball_color_red_ = 0.5f; + eye_ball_color_green_ = 0.5f; + eye_ball_color_blue_ = 0.5f; + eye_lid_color_red_ = 0.1f; + eye_lid_color_green_ = 0.1f; + eye_lid_color_blue_ = 0.1f; + eye_color_red_ = 0.0f; + eye_color_green_ = 0.0f; + eye_color_blue_ = 0.0f; + torso_radius_ = 0.25f; + shoulder_offset_x_ = -0.02f; + shoulder_offset_y_ = -0.01f; + shoulder_offset_z_ = 0.00f; + eye_scale_ = 0.65f; + has_eyelids_ = false; + eye_offset_y_ += 0.05f; + eye_offset_z_ -= 0.05f; + reflection_scale_ = 0.2f; + } else if (style_ == "mel") { + torso_radius_ = 0.23f; + shoulder_offset_x_ = -0.04f; + shoulder_offset_y_ = 0.03f; + eye_ball_color_red_ = 0.63f; + eye_ball_color_green_ = 0.53f; + eye_ball_color_blue_ = 0.49f; + eye_lid_color_red_ = 0.8f; + eye_lid_color_green_ = 0.55f; + eye_lid_color_blue_ = 0.45f; + eye_offset_x_ += 0.01f; + eye_offset_y_ += 0.01f; + eye_offset_z_ -= 0.04f; + eye_scale_ = 1.05f; + } else if (style_ == "ninja") { + eye_lid_color_red_ = 0.5f; + eye_lid_color_green_ = 0.3f; + eye_lid_color_blue_ = 0.2f; + reflection_scale_ = 0.15f; + default_eye_lid_angle_ = 20.0f; // angry eyes + eye_color_red_ = 0.2f; + eye_color_green_ = 0.1f; + eye_color_blue_ = 0.0f; + } else if (style_ == "agent") { + eyeless_ = true; + reflection_scale_ = 0.2f; + } else if (style_ == "cyborg") { + eyeless_ = true; + reflection_scale_ = 0.85f; + } else if (style_ == "santa") { + eye_scale_ = kSantaEyeScale; + torso_radius_ = 0.2f; + shoulder_offset_x_ = -0.04f; + shoulder_offset_y_ = 0.03f; + eye_lid_color_red_ = 0.5f; + eye_lid_color_green_ = 0.4f; + eye_lid_color_blue_ = 0.3f; + eye_offset_y_ += 0.02f; + eye_offset_z_ += kSantaEyeTranslate; + } else if (style_ == "pirate") { + torso_radius_ = 0.25f; + shoulder_offset_x_ = -0.04f; + shoulder_offset_y_ = 0.03f; + eye_lid_color_red_ = 0.3f; + eye_lid_color_green_ = 0.2f; + eye_lid_color_blue_ = 0.15f; + } else if (style_ == "kronk") { + eye_scale_ = 0.8f; + torso_radius_ = 0.2f; + shoulder_offset_x_ = -0.03f; + eye_lid_color_red_ = 0.3f; + eye_lid_color_green_ = 0.2f; + eye_lid_color_blue_ = 0.1f; + default_eye_lid_angle_ = 20.0f; // angry eyes + } else if (style_ == "frosty") { + torso_radius_ = 0.3f; + shoulder_offset_x_ = -0.04f; + shoulder_offset_y_ = 0.03f; + } else if (style_ == "female") { + female_ = true; + female_hair_ = true; + torso_radius_ = 0.11f; + shoulder_offset_x_ = 0.03f; + shoulder_offset_z_ = -0.02f; + eye_lid_color_red_ = 0.6f; + eye_lid_color_green_ = 0.35f; + eye_lid_color_blue_ = 0.31f; + default_eye_lid_angle_ = 15.0f; // sorta angry eyes + eye_color_red_ = 1.1f; + eye_color_green_ = 0.6f; + eye_color_blue_ = 1.4f; + eye_ball_color_red_ = 0.54f; + eye_ball_color_green_ = 0.51f; + eye_ball_color_blue_ = 0.55f; + eye_color_red_ = 0.55f; + eye_color_green_ = 0.3f; + eye_color_blue_ = 0.7f; + eye_scale_ = 0.95f; + eye_offset_x_ = 0.08f; + } else if (style_ == "pixie") { + wings_ = true; + female_ = true; + torso_radius_ = 0.11f; + shoulder_offset_x_ = 0.03f; + shoulder_offset_z_ = -0.02f; + eye_ball_color_red_ = 0.58f; + eye_ball_color_green_ = 0.55f; + eye_ball_color_blue_ = 0.6f; + eye_lid_color_red_ = 0.73f; + eye_lid_color_green_ = 0.53f; + eye_lid_color_blue_ = 0.6f; + default_eye_lid_angle_ = 10.0f; // sorta angry eyes + eye_color_red_ = 0.1f; + eye_color_green_ = 0.3f; + eye_color_blue_ = 0.1f; + eye_scale_ = 0.85f; + eye_offset_z_ = 0.2f; + eye_offset_y_ = 0.004f; + eye_offset_x_ = 0.083f; + reflection_scale_ = 0.35f; + } else if (style_ == "bones") { + eyeless_ = true; + // defaults.. + } else if (style_ == "spaz") { + // defaults.. + } else if (style_ == "ali") { + // defaults.. + eyeless_ = true; + torso_radius_ = 0.11f; + shoulder_offset_x_ = 0.03f; + shoulder_offset_y_ = -0.05f; + reflection_scale_ = 0.25f; + } else if (style_ == "bunny") { + torso_radius_ = 0.13f; + eye_scale_ = 1.2f; + eye_offset_z_ = 0.05f; + eye_offset_y_ = -0.08f; + eye_offset_x_ = 0.07f; + eye_lid_color_red_ = 0.6f; + eye_lid_color_green_ = 0.5f; + eye_lid_color_blue_ = 0.5f; + eye_ball_color_red_ = 0.6f; + eye_ball_color_green_ = 0.6f; + eye_ball_color_blue_ = 0.6f; + default_eye_lid_angle_ = -5.0f; // sorta angry eyes + shoulder_offset_x_ = 0.03f; + shoulder_offset_y_ = -0.05f; + reflection_scale_ = 0.02f; + } else { + BA_LOG_ONCE("Error: Unrecognized spaz style: '" + style_ + "'"); + } + UpdateBodiesForStyle(); +} + +auto SpazNode::GetVelocity() const -> std::vector { + const dReal* v = dBodyGetLinearVel(body_torso_->body()); + std::vector vv(3); + vv[0] = v[0]; + vv[1] = v[1]; + vv[2] = v[2]; + return vv; +} + +auto SpazNode::GetPositionForward() const -> std::vector { + dVector3 p_forward; + dBodyGetRelPointPos(body_torso_->body(), 0, 0.2f, -0.2f, p_forward); + std::vector vals(3); + vals[0] = p_forward[0] + body_torso_->blend_offset().x; + vals[1] = p_forward[1] + body_torso_->blend_offset().y; + vals[2] = p_forward[2] + body_torso_->blend_offset().z; + return vals; +} + +auto SpazNode::GetPositionCenter() const -> std::vector { + const dReal* p2 = dGeomGetPosition(body_torso_->geom()); + const dReal* p3 = dGeomGetPosition(body_head_->geom()); + std::vector vals(3); + if (shattered_) { + vals[0] = p2[0] + body_torso_->blend_offset().x; + vals[1] = p2[1] + body_torso_->blend_offset().y; + vals[2] = p2[2] + body_torso_->blend_offset().z; + } else { + vals[0] = (p2[0] + body_torso_->blend_offset().x) * 0.7f + + (p3[0] + body_head_->blend_offset().x) * 0.3f; + vals[1] = (p2[1] + body_torso_->blend_offset().y) * 0.7f + + (p3[1] + body_head_->blend_offset().y) * 0.3f; + vals[2] = (p2[2] + body_torso_->blend_offset().z) * 0.7f + + (p3[2] + body_head_->blend_offset().z) * 0.3f; + } + return vals; +} + +auto SpazNode::GetPunchPosition() const -> std::vector { + if (!body_punch_.exists()) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: querying spaz punch_position without punch body"); + return std::vector(3, 0.0f); + } + std::vector vals(3); + const dReal* p = dGeomGetPosition(body_punch_->geom()); + vals[0] = p[0]; + vals[1] = p[1]; + vals[2] = p[2]; + return vals; +} + +auto SpazNode::GetPunchVelocity() const -> std::vector { + if (!body_punch_.exists()) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: querying spaz punch_velocity without punch body"); + return std::vector(3, 0.0f); + } + std::vector vals(3); + const dReal* p = dGeomGetPosition(body_punch_->geom()); + dVector3 v; + dBodyGetPointVel( + (punch_right_ ? lower_right_arm_body_ : lower_left_arm_body_)->body(), + p[0], p[1], p[2], v); + vals[0] = v[0]; + vals[1] = v[1]; + vals[2] = v[2]; + return vals; +} + +auto SpazNode::GetPunchMomentumLinear() const -> std::vector { + if (!body_punch_.exists()) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: querying spaz punch_velocity without punch body"); + return std::vector(3, 0.0f); + } + std::vector vals(3); + + // our linear punch momentum is our base velocity with punchmomentumlinear as + // magnitude + const dReal* vel = dBodyGetLinearVel(body_torso_->body()); + float vel_mag = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); + if (vel_mag < 0.01f) { + vals[0] = vals[1] = vals[2] = 0; + } else { + vel_mag = punch_momentum_linear_ / vel_mag; + vals[0] = vel[0] * vel_mag; + vals[1] = vel[1] * vel_mag; + vals[2] = vel[2] * vel_mag; + } + return vals; +} + +auto SpazNode::GetTorsoPosition() const -> std::vector { + const dReal* p = dGeomGetPosition(body_torso_->geom()); + std::vector vals(3); + vals[0] = p[0] + body_torso_->blend_offset().x; + vals[1] = p[1] + body_torso_->blend_offset().y; + vals[2] = p[2] + body_torso_->blend_offset().z; + return vals; +} + +auto SpazNode::GetPosition() const -> std::vector { + const dReal* p = dGeomGetPosition(body_roller_->geom()); + std::vector vals(3); + vals[0] = p[0] + body_roller_->blend_offset().x; + vals[1] = p[1] + body_roller_->blend_offset().y; + vals[2] = p[2] + body_roller_->blend_offset().z; + return vals; +} + +void SpazNode::SetHoldNode(Node* val) { + // they passed a node + if (val != nullptr) { + Node* a = val; + assert(a); + RigidBody* b = a->GetRigidBody(hold_body_); + if (!b) { + // print some debugging info on the active collision.. + { + Dynamics* dynamics = scene()->dynamics(); + assert(dynamics); + Collision* c = dynamics->active_collision(); + if (c) { + Log("SRC NODE: " + ObjToString(dynamics->GetActiveCollideSrcNode())); + Log("OPP NODE: " + ObjToString(dynamics->GetActiveCollideDstNode())); + Log("SRC BODY " + + std::to_string(dynamics->GetCollideMessageReverseOrder() + ? c->body_id_1 + : c->body_id_2)); + Log("OPP BODY " + + std::to_string(dynamics->GetCollideMessageReverseOrder() + ? c->body_id_2 + : c->body_id_1)); + Log("REVERSE " + + std::to_string(dynamics->GetCollideMessageReverseOrder())); + } else { + Log(""); + } + } + throw Exception("specified hold_body (" + std::to_string(hold_body_) + + ") not found on hold_node: " + + a->GetObjectDescription()); + } + + hold_node_ = val; + holding_something_ = true; + last_pickup_time_ = scene()->time(); + + assert(a && b); + { + g_audio->PushSourceStopSoundCall(voice_play_id_); + if (Sound* sound = GetRandomMedia(pickup_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + const dReal* p_head = dGeomGetPosition(body_head_->geom()); + source->SetPosition(p_head[0], p_head[1], p_head[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + source->End(); + } + } + + float hold_height = 1.08f; + float hold_forward = -0.05f; + float hold_handle[3]; + float hold_handle2[3]; + + dBodyID b1 = body_torso_->body(); + dBodyID b2 = b->body(); + const dReal* p1 = dBodyGetPosition(b1); + const dReal* p2 = dBodyGetPosition(b2); + const dReal* q1 = dBodyGetQuaternion(b1); + const dReal* q2 = dBodyGetQuaternion(b2); + dReal p1_old[3]; + dReal p2_old[3]; + dReal q1_old[4]; + dReal q2_old[4]; + for (int i = 0; i < 3; i++) { + p1_old[i] = p1[i]; + p2_old[i] = p2[i]; + } + for (int i = 0; i < 4; i++) { + q1_old[i] = q1[i]; + q2_old[i] = q2[i]; + } + + a->GetRigidBodyPickupLocations(hold_body_, hold_handle, hold_handle2, + hold_hand_offset_right_, + hold_hand_offset_left_); + + // hand locations are relative to object pickup location.. add that in + hold_hand_offset_right_[0] += hold_handle[0]; + hold_hand_offset_right_[1] += hold_handle[1]; + hold_hand_offset_right_[2] += hold_handle[2]; + hold_hand_offset_left_[0] += hold_handle[0]; + hold_hand_offset_left_[1] += hold_handle[1]; + hold_hand_offset_left_[2] += hold_handle[2]; + + dBodySetPosition(b1, -hold_handle2[0], -hold_handle2[1], + -hold_handle2[2]); + dBodySetPosition(b2, -hold_handle[0], hold_height - hold_handle[1], + hold_forward - hold_handle[2]); + dQuaternion q; + dQSetIdentity(q); + dBodySetQuaternion(b1, q); + dBodySetQuaternion(b2, q); + auto* j = static_cast( + dJointCreateFixed(scene()->dynamics()->ode_world(), nullptr)); + pickup_joint_.SetJoint(j, scene()); + + pickup_joint_.AttachToBodies(body_torso_.get(), b); + dJointSetFixed(j); + dJointSetFixedSpringMode(j, 1, 1, true); + dJointSetFixedAnchor(j, 0, hold_height, hold_forward, false); + dJointSetFixedParam(j, dParamLinearStiffness, 180); + dJointSetFixedParam(j, dParamLinearDamping, 10); + + dJointSetFixedParam(j, dParamAngularStiffness, 4.0f); + dJointSetFixedParam(j, dParamAngularDamping, 0.3f); + + { + pickup_pos_1_[0] = p1_old[0]; + pickup_pos_1_[1] = p1_old[1]; + pickup_pos_1_[2] = p1_old[2]; + pickup_pos_2_[0] = p2_old[0]; + pickup_pos_2_[1] = p2_old[1]; + pickup_pos_2_[2] = p2_old[2]; + for (int i = 0; i < 4; i++) { + pickup_q1_[i] = q1_old[i]; + pickup_q2_[i] = q2_old[i]; + } + } + + dBodySetPosition(b1, p1_old[0], p1_old[1], p1_old[2]); + dBodySetPosition(b2, p2_old[0], p2_old[1], p2_old[2]); + dBodySetQuaternion(b1, q1_old); + dBodySetQuaternion(b2, q2_old); + } + // inform userland objects that they're picking up or have been picked up + DispatchPickUpMessage(a); + a->DispatchPickedUpMessage(this); + } else { + // user is clearing hold-node; just drop whatever we're holding.. + DropHeldObject(); + } +} + +auto SpazNode::GetJumpSounds() const -> std::vector { + return RefsToPointers(jump_sounds_); +} +void SpazNode::SetJumpSounds(const std::vector& vals) { + jump_sounds_ = PointersToRefs(vals); +} +auto SpazNode::GetAttackSounds() const -> std::vector { + return RefsToPointers(attack_sounds_); +} +void SpazNode::SetAttackSounds(const std::vector& vals) { + attack_sounds_ = PointersToRefs(vals); +} +auto SpazNode::GetImpactSounds() const -> std::vector { + return RefsToPointers(impact_sounds_); +} +void SpazNode::SetImpactSounds(const std::vector& vals) { + impact_sounds_ = PointersToRefs(vals); +} +auto SpazNode::GetDeathSounds() const -> std::vector { + return RefsToPointers(death_sounds_); +} +void SpazNode::SetDeathSounds(const std::vector& vals) { + death_sounds_ = PointersToRefs(vals); +} + +auto SpazNode::GetPickupSounds() const -> std::vector { + return RefsToPointers(pickup_sounds_); +} +void SpazNode::SetPickupSounds(const std::vector& vals) { + pickup_sounds_ = PointersToRefs(vals); +} + +void SpazNode::SetFallSounds(const std::vector& vals) { + fall_sounds_ = PointersToRefs(vals); +} + +auto SpazNode::GetResyncDataSize() -> int { + // 1 float for roll_amt_ + return 4; +} + +auto SpazNode::GetResyncData() -> std::vector { + std::vector data(4, 0); + char* ptr = reinterpret_cast(&(data[0])); + Utils::EmbedFloat32(&ptr, roll_amt_); + return data; +} + +void SpazNode::ApplyResyncData(const std::vector& data) { + const char* ptr = reinterpret_cast(&(data[0])); + roll_amt_ = Utils::ExtractFloat32(&ptr); +} + +void SpazNode::PlayHurtSound() { + if (dead_ || invincible_) { + return; + } + if (Sound* sound = GetRandomMedia(impact_sounds_)) { + if (AudioSource* source = g_audio->SourceBeginNew()) { + const dReal* p_top = dGeomGetPosition(body_head_->geom()); + g_audio->PushSourceStopSoundCall(voice_play_id_); + source->SetPosition(p_top[0], p_top[1], p_top[2]); + voice_play_id_ = source->Play(sound->GetSoundData()); + source->End(); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/spaz_node.h b/src/ballistica/scene/node/spaz_node.h new file mode 100644 index 00000000..f1e76af5 --- /dev/null +++ b/src/ballistica/scene/node/spaz_node.h @@ -0,0 +1,570 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_SPAZ_NODE_H_ +#define BALLISTICA_SCENE_NODE_SPAZ_NODE_H_ + +#include +#include + +#include "ballistica/dynamics/part.h" +#include "ballistica/game/player.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +// Current player character spaz node. +class SpazNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit SpazNode(Scene* scene); + ~SpazNode() override; + void Step() override; + void HandleMessage(const char* data) override; + void Draw(FrameDef* frame_def) override; + auto GetRigidBody(int id) -> RigidBody* override; + void GetRigidBodyPickupLocations(int id, float* obj, float* character, + float* hand1, float* hand2) override; + auto GetResyncDataSize() -> int override; + auto GetResyncData() -> std::vector override; + void ApplyResyncData(const std::vector& data) override; + auto can_fly() const -> bool { return can_fly_; } + void set_can_fly(bool val) { can_fly_ = val; } + auto hockey() const -> bool { return hockey_; } + void set_hockey(bool val) { hockey_ = val; } + auto GetRollerMaterials() const -> std::vector; + void SetRollerMaterials(const std::vector& vals); + auto GetExtrasMaterials() const -> std::vector; + void SetExtrasMaterials(const std::vector& vals); + auto GetPunchMaterials() const -> std::vector; + void SetPunchMaterials(const std::vector& vals); + auto GetPickupMaterials() const -> std::vector; + void SetPickupMaterials(const std::vector& vals); + auto GetMaterials() const -> std::vector; + void SetMaterials(const std::vector& vals); + auto area_of_interest_radius() const -> float { + return area_of_interest_radius_; + } + void set_area_of_interest_radius(float val) { + area_of_interest_radius_ = val; + } + auto name() const -> std::string { return name_; } + void set_name(const std::string& val) { name_ = val; } + auto counter_text() const -> std::string { return counter_text_; } + void set_counter_text(const std::string& val) { counter_text_ = val; } + auto mini_billboard_1_texture() const -> Texture* { + return mini_billboard_1_texture_.get(); + } + void set_mini_billboard_1_texture(Texture* val) { + mini_billboard_1_texture_ = val; + } + auto mini_billboard_2_texture() const -> Texture* { + return mini_billboard_2_texture_.get(); + } + void set_mini_billboard_2_texture(Texture* val) { + mini_billboard_2_texture_ = val; + } + auto mini_billboard_3_texture() const -> Texture* { + return mini_billboard_3_texture_.get(); + } + void set_mini_billboard_3_texture(Texture* val) { + mini_billboard_3_texture_ = val; + } + auto mini_billboard_1_start_time() const -> millisecs_t { + return mini_billboard_1_start_time_; + } + void set_mini_billboard_1_start_time(millisecs_t val) { + mini_billboard_1_start_time_ = val; + } + auto mini_billboard_1_end_time() const -> millisecs_t { + return mini_billboard_1_end_time_; + } + void set_mini_billboard_1_end_time(millisecs_t val) { + mini_billboard_1_end_time_ = val; + } + auto mini_billboard_2_start_time() const -> millisecs_t { + return mini_billboard_2_start_time_; + } + void set_mini_billboard_2_start_time(millisecs_t val) { + mini_billboard_2_start_time_ = val; + } + auto mini_billboard_2_end_time() const -> millisecs_t { + return mini_billboard_2_end_time_; + } + void set_mini_billboard_2_end_time(millisecs_t val) { + mini_billboard_2_end_time_ = val; + } + auto mini_billboard_3_start_time() const -> millisecs_t { + return mini_billboard_3_start_time_; + } + void set_mini_billboard_3_start_time(millisecs_t val) { + mini_billboard_3_start_time_ = val; + } + auto mini_billboard_3_end_time() const -> millisecs_t { + return mini_billboard_3_end_time_; + } + void set_mini_billboard_3_end_time(millisecs_t val) { + mini_billboard_3_end_time_ = val; + } + auto billboard_texture() const -> Texture* { + return billboard_texture_.get(); + } + void set_billboard_texture(Texture* val) { billboard_texture_ = val; } + auto billboard_opacity() const -> float { return billboard_opacity_; } + void set_billboard_opacity(float val) { billboard_opacity_ = val; } + auto counter_texture() const -> Texture* { return counter_texture_.get(); } + void set_counter_texture(Texture* val) { counter_texture_ = val; } + auto invincible() const -> bool { return invincible_; } + void set_invincible(bool val) { invincible_ = val; } + auto name_color() const -> std::vector { return name_color_; } + void SetNameColor(const std::vector& vals); + auto highlight() const -> std::vector { return highlight_; } + void set_highlight(const std::vector& vals); + auto color() const -> std::vector { return color_; } + void SetColor(const std::vector& vals); + auto hurt() const -> float { return hurt_; } + void SetHurt(float val); + auto boxing_gloves_flashing() const -> bool { + return boxing_gloves_flashing_; + } + void set_boxing_gloves_flashing(bool val) { boxing_gloves_flashing_ = val; } + auto source_player() const -> Player* { return source_player_.get(); } + void set_source_player(Player* val) { source_player_ = val; } + auto frozen() const -> bool { return frozen_; } + void SetFrozen(bool val); + auto have_boxing_gloves() const -> bool { return have_boxing_gloves_; } + void SetHaveBoxingGloves(bool val); + auto is_area_of_interest() const -> bool { + return (area_of_interest_ != nullptr); + } + void SetIsAreaOfInterest(bool val); + auto curse_death_time() const -> millisecs_t { return curse_death_time_; } + void SetCurseDeathTime(millisecs_t val); + auto shattered() const -> int { return shattered_; } + void SetShattered(int val); + auto dead() const -> bool { return dead_; } + void SetDead(bool val); + auto style() const -> std::string { return style_; } + void SetStyle(const std::string& val); + auto GetKnockout() const -> float { + return static_cast(knockout_) / 255.0f; + } + auto punch_power() const -> float { return punch_power_; } + auto GetPunchMomentumAngular() const -> float { + return 0.2f + punch_momentum_angular_; + } + auto GetPunchMomentumLinear() const -> std::vector; + auto damage_out() const -> float { return damage_out_; } + auto damage_smoothed() const -> float { return damage_smoothed_; } + auto GetPunchVelocity() const -> std::vector; + auto GetVelocity() const -> std::vector; + auto GetPositionForward() const -> std::vector; + auto GetPositionCenter() const -> std::vector; + auto GetPunchPosition() const -> std::vector; + auto GetTorsoPosition() const -> std::vector; + auto GetPosition() const -> std::vector; + auto hold_body() const -> int { return hold_body_; } + void set_hold_body(int val) { hold_body_ = val; } + auto hold_node() const -> Node* { return hold_node_.get(); } + void SetHoldNode(Node* val); + auto GetJumpSounds() const -> std::vector; + void SetJumpSounds(const std::vector& vals); + auto GetAttackSounds() const -> std::vector; + void SetAttackSounds(const std::vector& vals); + auto GetImpactSounds() const -> std::vector; + void SetImpactSounds(const std::vector& vals); + auto GetDeathSounds() const -> std::vector; + void SetDeathSounds(const std::vector& vals); + auto GetPickupSounds() const -> std::vector; + void SetPickupSounds(const std::vector& vals); + auto GetFallSounds() const -> std::vector { + return RefsToPointers(fall_sounds_); + } + void SetFallSounds(const std::vector& vals); + auto color_texture() const -> Texture* { return color_texture_.get(); } + void set_color_texture(Texture* val) { color_texture_ = val; } + auto color_mask_texture() const -> Texture* { + return color_mask_texture_.get(); + } + void set_color_mask_texture(Texture* val) { color_mask_texture_ = val; } + auto head_model() const -> Model* { return head_model_.get(); } + void set_head_model(Model* val) { head_model_ = val; } + auto torso_model() const -> Model* { return torso_model_.get(); } + void set_torso_model(Model* val) { torso_model_ = val; } + auto pelvis_model() const -> Model* { return pelvis_model_.get(); } + void set_pelvis_model(Model* val) { pelvis_model_ = val; } + auto upper_arm_model() const -> Model* { return upper_arm_model_.get(); } + void set_upper_arm_model(Model* val) { upper_arm_model_ = val; } + auto forearm_model() const -> Model* { return forearm_model_.get(); } + void set_forearm_model(Model* val) { forearm_model_ = val; } + auto hand_model() const -> Model* { return hand_model_.get(); } + void set_hand_model(Model* val) { hand_model_ = val; } + auto upper_leg_model() const -> Model* { return upper_leg_model_.get(); } + void set_upper_leg_model(Model* val) { upper_leg_model_ = val; } + auto lower_leg_model() const -> Model* { return lower_leg_model_.get(); } + void set_lower_leg_model(Model* val) { lower_leg_model_ = val; } + auto toes_model() const -> Model* { return toes_model_.get(); } + void set_toes_model(Model* val) { toes_model_ = val; } + auto billboard_cross_out() const -> bool { return billboard_cross_out_; } + void set_billboard_cross_out(bool val) { billboard_cross_out_ = val; } + auto jump_pressed() const -> bool { return jump_pressed_; } + void SetJumpPressed(bool val); + auto punch_pressed() const -> bool { return punch_pressed_; } + void SetPunchPressed(bool val); + auto bomb_pressed() const -> bool { return bomb_pressed_; } + void SetBombPressed(bool val); + auto run() const -> float { return run_; } + void SetRun(float val); + auto fly_pressed() const -> bool { return fly_pressed_; } + void SetFlyPressed(bool val); + auto behavior_version() const -> int { return behavior_version_; } + void set_behavior_version(int val) { + behavior_version_ = static_cast_check_fit(val); + } + auto pickup_pressed() const -> bool { return pickup_pressed_; } + void SetPickupPressed(bool val); + auto hold_position_pressed() const -> bool { return hold_position_pressed_; } + void SetHoldPositionPressed(bool val); + auto move_left_right() const -> float { return move_left_right_; } + void SetMoveLeftRight(float val); + auto move_up_down() const -> float { return move_up_down_; } + void SetMoveUpDown(float val); + + // Preserve some old behavior so we dont have to re-code the demo. + auto demo_mode() const -> bool { return demo_mode_; } + void set_demo_mode(bool val) { demo_mode_ = val; } + + private: + enum ShatterDamage { + kNeckJointBroken = 1u << 0u, + kPelvisJointBroken = 1u << 1u, + kUpperLeftLegJointBroken = 1u << 2u, + kUpperRightLegJointBroken = 1u << 3u, + kLowerLeftLegJointBroken = 1u << 4u, + kLowerRightLegJointBroken = 1u << 5u, + kUpperLeftArmJointBroken = 1u << 6u, + kUpperRightArmJointBroken = 1u << 7u, + kLowerLeftArmJointBroken = 1u << 8u, + kLowerRightArmJointBroken = 1u << 9u + }; + void PlayHurtSound(); + void DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, + float death_scale, float* add_color); + void SetupEyeLidShading(ObjectComponent* c, float death_fade, + float* add_color); + void DrawEyeLids(RenderComponent* c, float death_fade, float death_scale); + void DrawEyeBalls(RenderComponent* c, ObjectComponent* oc, bool shading, + float death_fade, float death_scale, float* add_color); + void DoFlyPress(); + + // Create a fixed joint between two bodies. + // The anchor is by default at the center of the first body. + auto CreateFixedJoint(RigidBody* b1, RigidBody* b2, float ls, float ld, + float as, float ad) -> JointFixedEF*; + + // Same but more explicit; provide anchor offsets for the two bodies. + // This also moves the second body based on those values so the anchor + // points line up. + auto CreateFixedJoint(RigidBody* b1, RigidBody* b2, float ls, float ld, + float as, float ad, float a1x, float a1y, float a1z, + float a2x, float a2y, float a2z, bool reposition = true) + -> JointFixedEF*; + void Throw(bool withBombButton); + + // Reset to a standing, non-moving state at the given point. + void Stand(float x, float y, float z, float angle); + void OnGraphicsQualityChanged(GraphicsQuality q) override; + void UpdateForGraphicsQuality(GraphicsQuality q); + void UpdateAreaOfInterest(); + auto CollideCallback(dContact* c, int count, RigidBody* colliding_body, + RigidBody* opposingbody) -> bool; + auto PreFilterCollision(RigidBody* r1, RigidBody* r2) -> bool override; + auto IsBrokenBodyPart(int id) -> bool; + static auto StaticCollideCallback(dContact* c, int count, + RigidBody* colliding_body, + RigidBody* opposingbody, void* data) + -> bool { + auto* a = static_cast(data); + return a->CollideCallback(c, count, colliding_body, opposingbody); + } + void DropHeldObject(); + void ApplyTorque(float x, float y, float z); + void CreateHair(); + void DestroyHair(); + void UpdateBodiesForStyle(); + void UpdateJoints(); +#if !BA_HEADLESS_BUILD + class FullShadowSet; + class SimpleShadowSet; + Object::Ref full_shadow_set_; + Object::Ref simple_shadow_set_; +#endif // !BA_HEADLESS_BUILD + float pickup_pos_1_[3]{0.0f, 0.0f, 0.0f}; + float pickup_pos_2_[3]{0.0f, 0.0f, 0.0f}; + float pickup_q1_[4]{0.0f, 0.0f, 0.0f, 0.0f}; + float pickup_q2_[4]{0.0f, 0.0f, 0.0f, 0.0f}; + uint32_t step_count_{}; + millisecs_t birth_time_{}; + Object::Ref color_texture_; + Object::Ref color_mask_texture_; + Object::Ref head_model_; + Object::Ref torso_model_; + Object::Ref pelvis_model_; + Object::Ref upper_arm_model_; + Object::Ref forearm_model_; + Object::Ref hand_model_; + Object::Ref upper_leg_model_; + Object::Ref lower_leg_model_; + Object::Ref toes_model_; + std::vector > jump_sounds_; + std::vector > attack_sounds_; + std::vector > impact_sounds_; + std::vector > death_sounds_; + std::vector > pickup_sounds_; + std::vector > fall_sounds_; + Object::WeakRef hold_node_; + std::string style_{"spaz"}; + Object::WeakRef source_player_; + bool clamp_move_values_to_circle_{true}; + bool demo_mode_{}; + std::string curse_timer_txt_; + TextGroup curse_timer_text_group_; + std::string counter_mesh_text_; + TextGroup counter_text_group_; + std::string counter_text_; + std::vector name_color_{1.0f, 1.0f, 1.0f}; + std::string name_; + std::string name_mesh_txt_; + TextGroup name_text_group_; + MeshIndexedSimpleFull billboard_1_mesh_; + MeshIndexedSimpleFull billboard_2_mesh_; + MeshIndexedSimpleFull billboard_3_mesh_; + float punch_power_{}; + float impact_damage_accum_{}; + Part spaz_part_; + Part hair_part_; + Part punch_part_; + Part pickup_part_; + Part roller_part_; + Part extras_part_; + Part limbs_part_upper_; + Part limbs_part_lower_; + bool dead_{}; + // 1 for partially-shattered, 2 for completely. + int shattered_{}; + bool invincible_{}; + bool trying_to_fly_{}; + bool throwing_with_bomb_button_{}; + bool can_fly_{}; + bool hockey_{}; + bool have_boxing_gloves_{}; + bool boxing_gloves_flashing_{}; + bool frozen_{}; + uint8_t flashing_{}; + float throw_power_{}; + millisecs_t throw_start_{}; + bool have_thrown_{}; + int hold_body_{}; + millisecs_t last_head_collide_time_{}; + millisecs_t last_external_impulse_time_{}; + millisecs_t last_impact_damage_dispatch_time_{}; + Object::Ref billboard_texture_; + float billboard_opacity_{}; + float area_of_interest_radius_{5.0f}; + Object::Ref counter_texture_; + Object::Ref mini_billboard_1_texture_; + millisecs_t mini_billboard_1_start_time_{}; + millisecs_t mini_billboard_1_end_time_{}; + Object::Ref mini_billboard_2_texture_; + millisecs_t mini_billboard_2_start_time_{}; + millisecs_t mini_billboard_2_end_time_{}; + Object::Ref mini_billboard_3_texture_; + millisecs_t mini_billboard_3_start_time_{}; + millisecs_t mini_billboard_3_end_time_{}; + millisecs_t curse_death_time_{}; + millisecs_t last_out_of_bounds_time_{}; + float base_pelvis_roller_anchor_offset_{}; + std::vector color_{1.0f, 1.0f, 1.0f}; + std::vector highlight_{0.5f, 0.5f, 0.5f}; + std::vector shadow_color_{0.5f, 0.5f, 0.5f}; + bool wings_{}; + Vector3f wing_pos_left_{0.0f, 0.0f, 0.0f}; + Vector3f wing_vel_left_{0.0f, 0.0f, 0.0f}; + Vector3f wing_pos_right_{0.0f, 0.0f, 0.0f}; + Vector3f wing_vel_right_{0.0f, 0.0f, 0.0f}; + uint32_t voice_play_id_{0xFFFFFFFF}; + uint32_t tick_play_id_{0xFFFFFFFF}; + millisecs_t last_fall_time_{}; + uint32_t fall_play_id_{}; + AreaOfInterest* area_of_interest_{}; + millisecs_t celebrate_until_time_left_{}; + millisecs_t celebrate_until_time_right_{}; + millisecs_t last_fly_time_{}; + int footing_{}; + int8_t lr_{}; + int8_t ud_{}; + float lr_norm_{}; + float raw_ud_norm_{}; + float raw_lr_norm_{}; + float ud_norm_{}; + float ud_smooth_{}; + float lr_smooth_{}; + float ud_diff_smooth_{}; + float lr_diff_smooth_{}; + float ud_diff_smoother_{}; + float lr_diff_smoother_{}; + float prev_vel_[3]{0.0f, 0.0f, 0.0f}; + float accel_[3]{0.0f, 0.0f, 0.0f}; + float throw_ud_{}; + float throw_lr_{}; + uint8_t behavior_version_{}; + uint8_t balance_{}; + uint8_t dizzy_{}; + uint8_t knockout_{}; + uint8_t jump_{}; + uint8_t punch_{}; + uint8_t pickup_{}; + float fly_power_{}; + float ball_size_{1.0f}; + float run_{}; + float move_left_right_{}; + float move_up_down_{}; + bool jump_pressed_{}; + bool punch_pressed_{}; + bool bomb_pressed_{}; + bool fly_pressed_{}; + bool pickup_pressed_{}; + bool hold_position_pressed_{}; + millisecs_t last_jump_time_{}; + RigidBody::Joint pickup_joint_; + float eyes_lr_{}; + float eyes_ud_{}; + float eyes_lr_smooth_{}; + float eyes_ud_smooth_{}; + float eyelid_left_ud_{}; + float eyelid_left_ud_smooth_{}; + float eyelid_right_ud_{}; + float eyelid_right_ud_smooth_{}; + float blink_{}; + float blink_smooth_{}; + bool flap_{}; + bool flapping_{}; + bool holding_something_{}; + millisecs_t last_pickup_time_{}; + millisecs_t last_punch_time_{}; + bool throwing_{}; + bool head_back_{}; + millisecs_t last_force_scream_time_{}; + bool force_scream_{}; + Object::Ref body_head_; + Object::Ref body_torso_; + Object::Ref body_pelvis_; + Object::Ref body_roller_; + Object::Ref body_punch_; + Object::Ref body_pickup_; + Object::Ref stand_body_; + Object::Ref upper_right_arm_body_; + Object::Ref lower_right_arm_body_; + Object::Ref upper_left_arm_body_; + Object::Ref lower_left_arm_body_; + Object::Ref upper_right_leg_body_; + Object::Ref lower_right_leg_body_; + Object::Ref upper_left_leg_body_; + Object::Ref lower_left_leg_body_; + Object::Ref left_toes_body_; + Object::Ref right_toes_body_; + JointFixedEF* upper_right_arm_joint_{}; + JointFixedEF* lower_right_arm_joint_{}; + JointFixedEF* upper_left_arm_joint_{}; + JointFixedEF* lower_left_arm_joint_{}; + JointFixedEF* upper_right_leg_joint_{}; + JointFixedEF* lower_right_leg_joint_{}; + JointFixedEF* upper_left_leg_joint_{}; + JointFixedEF* lower_left_leg_joint_{}; + JointFixedEF* left_toes_joint_{}; + JointFixedEF* left_toes_joint_2_{}; + JointFixedEF* right_toes_joint_{}; + JointFixedEF* right_toes_joint_2_{}; + JointFixedEF* right_leg_ik_joint_{}; + JointFixedEF* left_leg_ik_joint_{}; + JointFixedEF* right_arm_ik_joint_{}; + JointFixedEF* left_arm_ik_joint_{}; + float last_stand_body_orient_x_{}; + float last_stand_body_orient_z_{}; + JointFixedEF* neck_joint_{}; + JointFixedEF* pelvis_joint_{}; + JointFixedEF* roller_ball_joint_{}; + dJointID a_motor_brakes_{}; + JointFixedEF* stand_joint_{}; + dJointID a_motor_roller_{}; + bool female_{}; + bool female_hair_{}; + bool eyeless_{}; + bool fat_{}; + bool pirate_{}; + bool flippers_{}; + bool frosty_{}; + bool dull_reflection_{}; + bool ninja_{}; + bool punch_right_{}; + Object::Ref hair_front_right_body_; + JointFixedEF* hair_front_right_joint_{}; + Object::Ref hair_front_left_body_; + JointFixedEF* hair_front_left_joint_{}; + Object::Ref hair_ponytail_top_body_; + JointFixedEF* hair_ponytail_top_joint_{}; + Object::Ref hair_ponytail_bottom_body_; + JointFixedEF* hair_ponytail_bottom_joint_{}; + float hold_hand_offset_left_[3]{}; + float hold_hand_offset_right_[3]{}; + float jolt_head_vel_[3]{0.0f, 0.0f, 0.0f}; + millisecs_t last_shatter_test_time_{}; + float roll_amt_{}; + float damage_smoothed_{}; + float damage_out_{}; + float punch_dir_x_{1.0f}; + float punch_dir_z_{}; + float punch_momentum_angular_{}; + float punch_momentum_angular_d_{}; + float punch_momentum_linear_{}; + float punch_momentum_linear_d_{}; + float a_vel_y_smoothed_{}; + float a_vel_y_smoothed_more_{}; + float eye_lid_angle_{}; + bool last_hit_was_punch_{}; + int fly_time_{}; + float eye_ball_color_red_{0.5f}; + float eye_ball_color_green_{0.5f}; + float eye_ball_color_blue_{0.5f}; + float eye_lid_color_red_{0.5f}; + float eye_lid_color_green_{0.3f}; + float eye_lid_color_blue_{0.2f}; + float eye_color_red_{0.5f}; + float eye_color_green_{0.5f}; + float eye_color_blue_{1.2f}; + float torso_radius_{0.15f}; + float shoulder_offset_x_{}; + float shoulder_offset_y_{}; + float shoulder_offset_z_{}; + bool has_eyelids_{true}; + float eye_scale_{1.0f}; + float reflection_scale_{0.1f}; + float default_eye_lid_angle_{}; + float eye_offset_x_{}; + float eye_offset_y_{}; + float eye_offset_z_{}; + millisecs_t last_got_boxing_gloves_time_{}; + uint32_t shatter_damage_{}; + bool running_{}; + float speed_smoothed_{}; + float run_gas_{}; + float hurt_{}; + float hurt_smoothed_{}; + millisecs_t last_hurt_change_time_{}; + bool billboard_cross_out_{}; + millisecs_t death_time_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_SPAZ_NODE_H_ diff --git a/src/ballistica/scene/node/terrain_node.cc b/src/ballistica/scene/node/terrain_node.cc new file mode 100644 index 00000000..49fcd38a --- /dev/null +++ b/src/ballistica/scene/node/terrain_node.cc @@ -0,0 +1,259 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/terrain_node.h" + +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/model.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class TerrainNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS TerrainNode + BA_NODE_CREATE_CALL(createTerrain); + BA_BOOL_ATTR(visible_in_reflections, visible_in_reflections, + set_visible_in_reflections); + BA_BOOL_ATTR(affect_bg_dynamics, affects_bg_dynamics, + set_affects_bg_dynamics); + BA_BOOL_ATTR(bumper, bumper, SetBumper); + BA_BOOL_ATTR(background, background, set_background); + BA_BOOL_ATTR(overlay, overlay, set_overlay); + BA_FLOAT_ATTR(opacity, opacity, set_opacity); + BA_FLOAT_ATTR(opacity_in_low_or_medium_quality, + opacity_in_low_or_medium_quality, + set_opacity_in_low_or_medium_quality); + BA_STRING_ATTR(reflection, GetReflection, SetReflection); + BA_FLOAT_ARRAY_ATTR(reflection_scale, reflection_scale, SetReflectionScale); + BA_BOOL_ATTR(lighting, getLighting, setLighting); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_MODEL_ATTR(model, model, SetModel); + BA_TEXTURE_ATTR(color_texture, color_texture, SetColorTexture); + BA_COLLIDE_MODEL_ATTR(collide_model, collide_model, SetCollideModel); + BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); + BA_BOOL_ATTR(vr_only, vr_only, set_vr_only); +#undef BA_NODE_TYPE_CLASS + + TerrainNodeType() + : NodeType("terrain", createTerrain), + visible_in_reflections(this), + affect_bg_dynamics(this), + bumper(this), + background(this), + overlay(this), + opacity(this), + opacity_in_low_or_medium_quality(this), + reflection(this), + reflection_scale(this), + lighting(this), + color(this), + model(this), + color_texture(this), + collide_model(this), + materials(this), + vr_only(this) {} +}; +static NodeType* node_type{}; + +auto TerrainNode::InitType() -> NodeType* { + node_type = new TerrainNodeType(); + return node_type; +} + +TerrainNode::TerrainNode(Scene* scene) + : Node(scene, node_type), + visible_in_reflections_(true), + opacity_(1.0f), + opacity_in_low_or_medium_quality_(-1.0f), + terrain_part_(this), + background_(false), + overlay_(false), + lighting_(true), + bumper_(false), + affect_bg_dynamics_(true), + bg_dynamics_collide_model_(nullptr), + reflection_(ReflectionType::kNone), + reflection_scale_(3, 1.0f), + reflection_scale_r_(1.0f), + reflection_scale_g_(1.0f), + reflection_scale_b_(1.0f), + color_(3, 1.0f), + color_r_(1.0f), + color_g_(1.0f), + color_b_(1.0f), + vr_only_(false) { + scene->increment_bg_cover_count(); +} + +TerrainNode::~TerrainNode() { + scene()->decrement_bg_cover_count(); + RemoveFromBGDynamics(); + + // If we've got a collide-model, this is a good time to mark + // it as used since it may be getting opened up to pruning + // without our reference. + if (collide_model_.exists()) { + collide_model_->collide_model_data()->set_last_used_time(GetRealTime()); + } +} + +auto TerrainNode::GetMaterials() const -> std::vector { + return RefsToPointers(materials_); +} + +void TerrainNode::SetMaterials(const std::vector& vals) { + materials_ = PointersToRefs(vals); + terrain_part_.SetMaterials(vals); +} + +void TerrainNode::SetModel(Model* val) { model_ = val; } + +void TerrainNode::SetCollideModel(CollideModel* val) { + // if we had an old one, mark its last-used time so caching works properly.. + if (collide_model_.exists()) { + collide_model_->collide_model_data()->set_last_used_time(GetRealTime()); + } + collide_model_ = val; + + // remove any existing.. + RemoveFromBGDynamics(); + + if (collide_model_.exists()) { + uint32_t flags = bumper_ ? RigidBody::kIsBumper : 0; + flags |= RigidBody::kIsTerrain; + body_ = Object::New( + 0, &terrain_part_, RigidBody::Type::kGeomOnly, + RigidBody::Shape::kTrimesh, RigidBody::kCollideBackground, + RigidBody::kCollideAll ^ RigidBody::kCollideBackground, + collide_model_.get(), flags); + body_->set_can_cause_impact_damage(true); + + // also ship it to the BG-Dynamics thread.. + if (!bumper_ && affect_bg_dynamics_) { + AddToBGDynamics(); + } + } else { + body_.Clear(); + } +} + +void TerrainNode::SetColorTexture(Texture* val) { color_texture_ = val; } + +void TerrainNode::SetReflectionScale(const std::vector& vals) { + if (vals.size() != 1 && vals.size() != 3) + throw Exception("Expected float array of size 1 or 3 for reflection_scale"); + reflection_scale_ = vals; + if (reflection_scale_.size() == 1) { + reflection_scale_r_ = reflection_scale_g_ = reflection_scale_b_ = + reflection_scale_[0]; + } else { + reflection_scale_r_ = reflection_scale_[0]; + reflection_scale_g_ = reflection_scale_[1]; + reflection_scale_b_ = reflection_scale_[2]; + } +} + +void TerrainNode::SetColor(const std::vector& vals) { + if (vals.size() != 1 && vals.size() != 3) + throw Exception("Expected float array of size 1 or 3 for color"); + color_ = vals; + if (color_.size() == 1) { + color_r_ = color_g_ = color_b_ = color_[0]; + } else { + color_r_ = color_[0]; + color_g_ = color_[1]; + color_b_ = color_[2]; + } +} + +auto TerrainNode::GetReflection() const -> std::string { + return Graphics::StringFromReflectionType(reflection_); +} +void TerrainNode::SetReflection(const std::string& val) { + reflection_ = Graphics::ReflectionTypeFromString(val); +} + +void TerrainNode::SetBumper(bool val) { + bumper_ = val; + if (body_.exists()) { + uint32_t is_bumper{RigidBody::kIsBumper}; + if (bumper_) { + body_->set_flags(body_->flags() | is_bumper); // on + } else { + body_->set_flags(body_->flags() & ~is_bumper); // off + } + } +} + +void TerrainNode::AddToBGDynamics() { + assert(bg_dynamics_collide_model_ == nullptr && collide_model_.exists() + && !bumper_ && affect_bg_dynamics_); + bg_dynamics_collide_model_ = collide_model_.get(); +#if !BA_HEADLESS_BUILD + g_bg_dynamics->AddTerrain(bg_dynamics_collide_model_->collide_model_data()); +#endif // !BA_HEADLESS_BUILD +} + +void TerrainNode::RemoveFromBGDynamics() { + if (bg_dynamics_collide_model_ != nullptr) { +#if !BA_HEADLESS_BUILD + g_bg_dynamics->RemoveTerrain( + bg_dynamics_collide_model_->collide_model_data()); +#endif // !BA_HEADLESS_BUILD + bg_dynamics_collide_model_ = nullptr; + } +} + +void TerrainNode::Draw(FrameDef* frame_def) { + if (!model_.exists()) { + return; + } + if (vr_only_ && !IsVRMode()) { + return; + } + ObjectComponent c(overlay_ ? frame_def->overlay_3d_pass() + : background_ ? frame_def->beauty_pass_bg() + : frame_def->beauty_pass()); + c.SetWorldSpace(true); + c.SetTexture(color_texture_); + if (lighting_) { + c.SetLightShadow(LightShadowType::kTerrain); + } else { + c.SetLightShadow(LightShadowType::kNone); + } + if (reflection_ != ReflectionType::kNone) { + c.SetReflection(reflection_); + c.SetReflectionScale(reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + } + float opacity; + if (frame_def->quality() <= GraphicsQuality::kHigh + && opacity_in_low_or_medium_quality_ >= 0.0f) { + opacity = opacity_in_low_or_medium_quality_; + } else { + opacity = opacity_; + } + + // these options currently don't have a world-space-optimized version.. + if (opacity < 1.0f || overlay_) { + c.SetTransparent(true); + c.SetWorldSpace(false); + c.SetColor(color_r_, color_g_, color_b_, opacity); + } else { + c.SetColor(color_r_, color_g_, color_b_, 1.0f); + } + uint32_t drawFlags = 0; + if (!visible_in_reflections_) { + drawFlags |= kModelDrawFlagNoReflection; + } + c.DrawModel(model_->model_data(), drawFlags); + c.Submit(); +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/terrain_node.h b/src/ballistica/scene/node/terrain_node.h new file mode 100644 index 00000000..a8f2b87d --- /dev/null +++ b/src/ballistica/scene/node/terrain_node.h @@ -0,0 +1,89 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_TERRAIN_NODE_H_ +#define BALLISTICA_SCENE_NODE_TERRAIN_NODE_H_ + +#include +#include + +#include "ballistica/dynamics/part.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class TerrainNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit TerrainNode(Scene* scene); + ~TerrainNode() override; + void Draw(FrameDef* frame_def) override; + auto visible_in_reflections() const -> bool { + return visible_in_reflections_; + } + void set_visible_in_reflections(bool val) { visible_in_reflections_ = val; } + auto affects_bg_dynamics() const -> bool { return affect_bg_dynamics_; } + void set_affects_bg_dynamics(bool val) { affect_bg_dynamics_ = val; } + auto bumper() const -> bool { return bumper_; } + void SetBumper(bool val); + auto background() const -> bool { return background_; } + void set_background(bool val) { background_ = val; } + auto overlay() const -> bool { return overlay_; } + void set_overlay(bool val) { overlay_ = val; } + auto opacity() const -> float { return opacity_; } + void set_opacity(float val) { opacity_ = val; } + auto opacity_in_low_or_medium_quality() const -> float { + return opacity_in_low_or_medium_quality_; + } + void set_opacity_in_low_or_medium_quality(float val) { + opacity_in_low_or_medium_quality_ = val; + } + auto GetReflection() const -> std::string; + void SetReflection(const std::string& val); + auto reflection_scale() const -> std::vector { + return reflection_scale_; + } + void SetReflectionScale(const std::vector& vals); + auto getLighting() const -> bool { return lighting_; } + void setLighting(bool val) { lighting_ = val; } + auto color() const -> const std::vector& { return color_; } + void SetColor(const std::vector& vals); + auto model() const -> Model* { return model_.get(); } + void SetModel(Model* m); + auto color_texture() const -> Texture* { return color_texture_.get(); } + void SetColorTexture(Texture* val); + auto collide_model() const -> CollideModel* { return collide_model_.get(); } + void SetCollideModel(CollideModel* val); + auto GetMaterials() const -> std::vector; + void SetMaterials(const std::vector& vals); + auto vr_only() const -> bool { return vr_only_; } + void set_vr_only(bool val) { vr_only_ = val; } + + private: + void AddToBGDynamics(); + void RemoveFromBGDynamics(); + CollideModel* bg_dynamics_collide_model_; + bool vr_only_; + bool bumper_; + bool affect_bg_dynamics_; + bool lighting_; + bool background_; + bool overlay_; + float opacity_; + float opacity_in_low_or_medium_quality_; + Object::Ref model_; + Object::Ref collide_model_; + Object::Ref color_texture_; + std::vector > materials_; + Part terrain_part_; + Object::Ref body_; + bool visible_in_reflections_; + ReflectionType reflection_; + std::vector reflection_scale_; + float reflection_scale_r_, reflection_scale_g_, reflection_scale_b_; + std::vector color_; + float color_r_, color_g_, color_b_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_TERRAIN_NODE_H_ diff --git a/src/ballistica/scene/node/text_node.cc b/src/ballistica/scene/node/text_node.cc new file mode 100644 index 00000000..a5f1fc1d --- /dev/null +++ b/src/ballistica/scene/node/text_node.cc @@ -0,0 +1,646 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/text_node.h" + +#include + +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class TextNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS TextNode + BA_NODE_CREATE_CALL(CreateText); + BA_FLOAT_ATTR(opacity, opacity, set_opacity); + BA_FLOAT_ATTR(trail_opacity, trail_opacity, set_trail_opacity); + BA_FLOAT_ATTR(project_scale, project_scale, set_project_scale); + BA_FLOAT_ATTR(scale, scale, set_scale); + BA_FLOAT_ARRAY_ATTR(position, position, SetPosition); + BA_STRING_ATTR(text, getText, SetText); + BA_BOOL_ATTR(big, big, SetBig); + BA_BOOL_ATTR(trail, trail, set_trail); + BA_FLOAT_ARRAY_ATTR(color, color, SetColor); + BA_FLOAT_ARRAY_ATTR(trailcolor, trail_color, SetTrailColor); + BA_FLOAT_ATTR(trail_project_scale, trail_project_scale, + set_trail_project_scale); + BA_BOOL_ATTR(opacity_scales_shadow, opacity_scales_shadow, + set_opacity_scales_shadow); + BA_STRING_ATTR(h_align, GetHAlign, SetHAlign); + BA_STRING_ATTR(v_align, GetVAlign, SetVAlign); + BA_STRING_ATTR(h_attach, GetHAttach, SetHAttach); + BA_STRING_ATTR(v_attach, GetVAttach, SetVAttach); + BA_BOOL_ATTR(in_world, in_world, set_in_world); + BA_FLOAT_ATTR(tilt_translate, tilt_translate, set_tilt_translate); + BA_FLOAT_ATTR(maxwidth, max_width, set_max_width); + BA_FLOAT_ATTR(shadow, shadow, set_shadow); + BA_FLOAT_ATTR(flatness, flatness, set_flatness); + BA_BOOL_ATTR(client_only, client_only, set_client_only); + BA_BOOL_ATTR(host_only, host_only, set_host_only); + BA_FLOAT_ATTR(vr_depth, vr_depth, set_vr_depth); + BA_FLOAT_ATTR(rotate, rotate, set_rotate); + BA_BOOL_ATTR(front, front, set_front); +#undef BA_NODE_TYPE_CLASS + TextNodeType() + : NodeType("text", CreateText), + opacity(this), + trail_opacity(this), + project_scale(this), + scale(this), + position(this), + text(this), + big(this), + trail(this), + color(this), + trailcolor(this), + trail_project_scale(this), + opacity_scales_shadow(this), + h_align(this), + v_align(this), + h_attach(this), + v_attach(this), + in_world(this), + tilt_translate(this), + maxwidth(this), + shadow(this), + flatness(this), + client_only(this), + host_only(this), + vr_depth(this), + rotate(this), + front(this) {} +}; +static NodeType* node_type{}; + +auto TextNode::InitType() -> NodeType* { + node_type = new TextNodeType(); + return node_type; +} + +TextNode::TextNode(Scene* scene) : Node(scene, node_type) {} + +TextNode::~TextNode() = default; + +void TextNode::SetText(const std::string& val) { + if (text_raw_ != val) { + assert(Utils::IsValidUTF8(val)); + + // In some cases we want to make sure this is a valid resource-string + // since catching the error here is much more useful than if we catch + // it at draw-time. However this is expensive so we only do it for debug + // mode or if the string looks suspicious. + bool do_format_check{}; + bool print_false_positives{}; + + if (g_buildconfig.debug_build()) { + do_format_check = true; + } else { + if (val.size() > 1 && val[0] == '{' && val[val.size() - 1] == '}') { + // ok, its got bounds like json; now if its either missing quotes or a + // colon then let's check it.. + if (!strstr(val.c_str(), "\"") || !strstr(val.c_str(), ":")) { + do_format_check = true; + // we wanna avoid doing this check when we don't have to.. + // so lets print if we get a false positive + print_false_positives = true; + } + } + } + + if (do_format_check) { + bool valid; + g_game->CompileResourceString(val, "setText format check", &valid); + if (!valid) { + BA_LOG_ONCE("Invalid resource string: '" + val + "' on node '" + label() + + "'"); + Python::PrintStackTrace(); + } else if (print_false_positives) { + BA_LOG_ONCE("Got false positive for json check on '" + val + "'"); + Python::PrintStackTrace(); + } + } + text_translation_dirty_ = true; + text_raw_ = val; + } +} + +void TextNode::SetBig(bool val) { + big_ = val; + text_group_dirty_ = true; + text_width_dirty_ = true; +} + +auto TextNode::GetHAlign() const -> std::string { + if (h_align_ == HAlign::kLeft) { + return "left"; + } else if (h_align_ == HAlign::kRight) { + return "right"; + } else if (h_align_ == HAlign::kCenter) { + return "center"; + } else { + BA_LOG_ONCE("Error: Invalid h_align value in text-node: " + + std::to_string(static_cast(h_align_))); + return ""; + } +} + +void TextNode::SetHAlign(const std::string& val) { + text_group_dirty_ = true; + if (val == "left") { + h_align_ = HAlign::kLeft; + } else if (val == "right") { + h_align_ = HAlign::kRight; + } else if (val == "center") { + h_align_ = HAlign::kCenter; + } else { + throw Exception("Invalid h_align for text node: " + val); + } +} + +auto TextNode::GetVAlign() const -> std::string { + if (v_align_ == VAlign::kTop) { + return "top"; + } else if (v_align_ == VAlign::kBottom) { + return "bottom"; + } else if (v_align_ == VAlign::kCenter) { + return "center"; + } else if (v_align_ == VAlign::kNone) { + return "none"; + } else { + BA_LOG_ONCE("Error: Invalid v_align value in text-node: " + + std::to_string(static_cast(v_align_))); + return ""; + } +} + +void TextNode::SetVAlign(const std::string& val) { + text_group_dirty_ = true; + if (val == "top") { + v_align_ = VAlign::kTop; + } else if (val == "bottom") { + v_align_ = VAlign::kBottom; + } else if (val == "center") { + v_align_ = VAlign::kCenter; + } else if (val == "none") { + v_align_ = VAlign::kNone; + } else { + throw Exception("Invalid v_align for text node: " + val); + } +} + +auto TextNode::GetHAttach() const -> std::string { + if (h_attach_ == HAttach::kLeft) { + return "left"; + } else if (h_attach_ == HAttach::kRight) { + return "right"; + } else if (h_attach_ == HAttach::kCenter) { + return "center"; + } else { + BA_LOG_ONCE("Error: Invalid h_attach value in text-node: " + + std::to_string(static_cast(h_attach_))); + return ""; + } +} + +void TextNode::SetHAttach(const std::string& val) { + position_final_dirty_ = true; + if (val == "left") { + h_attach_ = HAttach::kLeft; + } else if (val == "right") { + h_attach_ = HAttach::kRight; + } else if (val == "center") { + h_attach_ = HAttach::kCenter; + } else { + throw Exception("Invalid h_attach for text node: " + val); + } +} + +auto TextNode::GetVAttach() const -> std::string { + if (v_attach_ == VAttach::kTop) { + return "top"; + } else if (v_attach_ == VAttach::kBottom) { + return "bottom"; + } else if (v_attach_ == VAttach::kCenter) { + return "center"; + } else { + BA_LOG_ONCE("Error: Invalid v_attach value in text-node: " + + std::to_string(static_cast(v_attach_))); + return ""; + } +} + +void TextNode::SetVAttach(const std::string& val) { + position_final_dirty_ = true; + if (val == "top") { + v_attach_ = VAttach::kTop; + } else if (val == "bottom") { + v_attach_ = VAttach::kBottom; + } else if (val == "center") { + v_attach_ = VAttach::kCenter; + } else { + throw Exception("Invalid v_attach for text node: " + val); + } +} + +void TextNode::SetColor(const std::vector& vals) { + if (vals.size() != 3 && vals.size() != 4) { + throw Exception("Expected float array of size 3 or 4 for color"); + } + color_ = vals; + if (color_.size() == 3) { + color_.push_back(1.0f); + } +} + +void TextNode::SetTrailColor(const std::vector& vals) { + if (vals.size() != 3) { + throw Exception("Expected float array of size 3 for trailcolor"); + } + trail_color_ = vals; +} + +void TextNode::SetPosition(const std::vector& val) { + if (val.size() != 2 && val.size() != 3) { + throw Exception("Expected float array of length 2 or 3 for position; got " + + std::to_string(val.size())); + } + position_ = val; + position_final_dirty_ = true; +} + +void TextNode::OnScreenSizeChange() { position_final_dirty_ = true; } + +void TextNode::Update() { + // Update our final translate if need be. + if (position_final_dirty_) { + float offset_h; + float offset_v; + + if (in_world_) { + offset_h = 0.0f; + offset_v = 0.0f; + } else { + // Screen space; apply alignment and stuff. + if (h_attach_ == HAttach::kLeft) { + offset_h = 0; + } else if (h_attach_ == HAttach::kRight) { + offset_h = g_graphics->screen_virtual_width(); + } else if (h_attach_ == HAttach::kCenter) { + offset_h = g_graphics->screen_virtual_width() / 2; + } else { + throw Exception("invalid h_attach"); + } + if (v_attach_ == VAttach::kTop) { + offset_v = g_graphics->screen_virtual_height(); + } else if (v_attach_ == VAttach::kBottom) { + offset_v = 0; + } else if (v_attach_ == VAttach::kCenter) { + offset_v = g_graphics->screen_virtual_height() / 2; + } else { + throw Exception("invalid v_attach"); + } + } + position_final_ = position_; + if (position_final_.size() == 2) { + position_final_.push_back(0.0f); + } + position_final_[0] += offset_h; + position_final_[1] += offset_v; + position_final_dirty_ = false; + } +} + +void TextNode::Draw(FrameDef* frame_def) { + if (client_only_ && context().GetHostSession()) { + return; + } + if (host_only_ && !context().GetHostSession()) { + return; + } + + // Apply subs/resources to get our actual text if need be. + if (text_translation_dirty_) { + text_translated_ = + g_game->CompileResourceString(text_raw_, "TextNode::OnDraw"); + text_translation_dirty_ = false; + text_group_dirty_ = true; + text_width_dirty_ = true; + } + + if (text_translated_.size() <= 0.0f) { + return; + } + + // recalc our text width if need be.. + if (text_width_dirty_) { + text_width_ = + g_text_graphics->GetStringWidth(text_translated_.c_str(), big_); + text_width_dirty_ = false; + } + + bool vr_2d_text = (IsVRMode() && !in_world_); + + // in vr mode we use the fixed overlay position if our scene is set for + // that + bool vr_use_fixed = (IsVRMode() && scene()->use_fixed_vr_overlay()); + + // FIXME - in VR, fixed and front are currently mutually exclusive; need to + // implement that. + if (front_) { + vr_use_fixed = false; + } + + // make sure we're up to date + Update(); + RenderPass& pass( + *(in_world_ ? frame_def->overlay_3d_pass() + : (vr_use_fixed ? frame_def->GetOverlayFixedPass() + : front_ ? frame_def->overlay_front_pass() + : frame_def->overlay_pass()))); + if (big_) { + if (text_group_dirty_) { + TextMesh::HAlign h_align; + switch (h_align_) { + case HAlign::kLeft: + h_align = TextMesh::HAlign::kLeft; + break; + case HAlign::kRight: + h_align = TextMesh::HAlign::kRight; + break; + case HAlign::kCenter: + h_align = TextMesh::HAlign::kCenter; + break; + default: + throw Exception(); + } + + TextMesh::VAlign v_align; + switch (v_align_) { + case VAlign::kNone: + v_align = TextMesh::VAlign::kNone; + break; + case VAlign::kCenter: + v_align = TextMesh::VAlign::kCenter; + break; + case VAlign::kTop: + v_align = TextMesh::VAlign::kTop; + break; + case VAlign::kBottom: + v_align = TextMesh::VAlign::kBottom; + break; + default: + throw Exception(); + } + + // update if need be + text_group_.SetText(text_translated_, h_align, v_align, true, 2.5f); + text_group_dirty_ = false; + } + + float z = vr_2d_text ? 0.0f : g_graphics->overlay_node_z_depth(); + + assert(!text_width_dirty_); + float tx = position_final_[0]; + float ty = position_final_[1]; + float tx_tilt = 0; + float ty_tilt = 0; + + // left/rigth shift from tilting the device + if (tilt_translate_ != 0.0f) { + Vector3f tilt = g_graphics->tilt(); + tx_tilt = -tilt.y * tilt_translate_; + ty_tilt = tilt.x * tilt_translate_; + } + + assert(!text_width_dirty_); + float extrascale; + float textWidth = text_width_; + float extrascale_2 = 3.5f; + + if (max_width_ > 0.0f && (textWidth * scale_ * extrascale_2) > max_width_) { + extrascale = max_width_ / (textWidth * scale_ * extrascale_2); + } else { + extrascale = 1.0f; + } + extrascale *= scale_; + + float pass_width = pass.virtual_width(); + float pass_height = pass.virtual_height(); + { + // new style + { + // draw trails.. + if (trail_) { + int passes = 2; + if (trail_project_scale_ != project_scale_) { + for (int i = 0; i < passes; i++) { + float o = trail_opacity_ * 0.5f; + { + auto i_f = static_cast(i); + auto passes_f = static_cast(passes); + float x = tx + tx_tilt * (i_f / passes_f) - pass_width / 2.0f; + float y = ty + ty_tilt * (i_f / passes_f) - pass_height / 2.0f; + float project_scale = + (trail_project_scale_ + + static_cast(i) + * (project_scale_ - trail_project_scale_) + / passes_f); + SimpleComponent c(&pass); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.SetColor(trail_color_[0] * o, trail_color_[1] * o, + trail_color_[2] * o, 0.0f); + c.setGlow(1.0f, 3.0f); + + // FIXME FIXME FIXME NEED A WAY TO BLUR IN THE SHADER + int elem_count = text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + // gracefully skip unloaded textures.. + TextureData* t = text_group_.GetElementTexture(e); + if (!t->preloaded()) continue; + c.SetTexture(t); + c.SetMaskUV2Texture(text_group_.GetElementMaskUV2Texture(e)); + c.PushTransform(); + if (vr_2d_text) { + c.Translate( + 0, 0, + vr_depth_ - 15.0f * static_cast(passes - i)); + } + + // Fudge factors to keep our old look.. ew. + c.Translate(pass_width / 2 + 7.0f, pass_height / 2 + 35.0f, + z); + c.Scale(project_scale, project_scale); + c.Translate(x, y + 70.0f, 0); + c.Scale(extrascale * extrascale_2, extrascale * extrascale_2); + c.DrawMesh(text_group_.GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + } + } + + SimpleComponent c(&pass); + c.SetTransparent(true); + c.SetColor(color_[0], color_[1], color_[2], color_[3] * opacity_); + + int elem_count = text_group_.GetElementCount(); + bool did_submit = false; + for (int e = 0; e < elem_count; e++) { + // Gracefully skip unloaded textures. + TextureData* t = text_group_.GetElementTexture(e); + if (!t->preloaded()) continue; + c.SetTexture(t); + float shadow_opacity = shadow_; + if (opacity_scales_shadow_) { + float o = color_[3] * opacity_; + shadow_opacity *= o * o; + } + c.SetShadow(-0.002f * text_group_.GetElementUScale(e), + -0.002f * text_group_.GetElementVScale(e), 2.5f, + shadow_opacity); + if (shadow_opacity > 0) { + c.SetMaskUV2Texture(text_group_.GetElementMaskUV2Texture(e)); + } else { + c.clearMaskUV2Texture(); + } + + c.PushTransform(); + if (vr_2d_text) { + c.Translate(0, 0, vr_depth_); + } + + // Fudge factors to keep our old look.. ew. + c.Translate(pass_width / 2 + 7.0f, pass_height / 2 + 35.0f, z); + c.Scale(project_scale_, project_scale_); + c.Translate(tx + tx_tilt - pass_width / 2, + ty + ty_tilt - pass_height / 2 + 70.0f, 0); + c.Scale(extrascale * extrascale_2, extrascale * extrascale_2); + c.DrawMesh(text_group_.GetElementMesh(e)); + c.PopTransform(); + // Any reason why we submit inside the loop here but not further + // down? + c.Submit(); + did_submit = true; + } + if (!did_submit) { + // Make sure we've got at least one. + c.Submit(); + } + } + } + } else { + // small text + if (text_group_dirty_) { + TextMesh::HAlign h_align; + switch (h_align_) { + case HAlign::kLeft: + h_align = TextMesh::HAlign::kLeft; + break; + case HAlign::kRight: + h_align = TextMesh::HAlign::kRight; + break; + case HAlign::kCenter: + h_align = TextMesh::HAlign::kCenter; + break; + default: + throw Exception(); + } + + TextMesh::VAlign v_align; + switch (v_align_) { + case VAlign::kNone: + v_align = TextMesh::VAlign::kNone; + break; + case VAlign::kCenter: + v_align = TextMesh::VAlign::kCenter; + break; + case VAlign::kTop: + v_align = TextMesh::VAlign::kTop; + break; + case VAlign::kBottom: + v_align = TextMesh::VAlign::kBottom; + break; + default: + throw Exception(); + } + + // Update if need be. + text_group_.SetText(text_translated_, h_align, v_align); + text_group_dirty_ = false; + } + float z = vr_2d_text ? 0.0f + : (in_world_ ? position_final_[2] + : g_graphics->overlay_node_z_depth()); + + assert(!text_width_dirty_); + float extrascale; + if (max_width_ > 0.0f && text_width_ > max_width_) { + extrascale = max_width_ / text_width_; + } else { + extrascale = 1.0f; + } + + SimpleComponent c(&pass); + c.SetTransparent(true); + float fin_a = color_[3] * opacity_; + int elem_count = text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + // Gracefully skip unloaded textures. + TextureData* t = text_group_.GetElementTexture(e); + if (!t->preloaded()) continue; + c.SetTexture(t); + float shadow_opacity = shadow_; + if (opacity_scales_shadow_) { + float o = color_[3] * opacity_; + shadow_opacity *= o * o; + } + c.SetShadow(-0.004f * text_group_.GetElementUScale(e), + -0.004f * text_group_.GetElementVScale(e), 0.0f, + shadow_opacity); + if (shadow_opacity > 0) { + c.SetMaskUV2Texture(text_group_.GetElementMaskUV2Texture(e)); + } else { + c.clearMaskUV2Texture(); + } + if (text_group_.GetElementCanColor(e)) { + c.SetColor(color_[0], color_[1], color_[2], fin_a); + } else { + c.SetColor(1, 1, 1, fin_a); + } + if (IsVRMode()) { + c.SetFlatness(text_group_.GetElementMaxFlatness(e)); + } else { + c.SetFlatness( + std::min(text_group_.GetElementMaxFlatness(e), flatness_)); + } + c.PushTransform(); + if (vr_2d_text) { + c.Translate(0, 0, vr_depth_); + } + c.Translate(position_final_[0], position_final_[1], z); + if (rotate_ != 0.0f) c.Rotate(rotate_, 0, 0, 1); + c.Scale(scale_ * extrascale, scale_ * extrascale, 1.0f * extrascale); + c.DrawMesh(text_group_.GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } +} + +void TextNode::OnLanguageChange() { + // All we do here is mark our translated text dirty so it'll get remade at the + // next draw. + text_translation_dirty_ = true; +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/text_node.h b/src/ballistica/scene/node/text_node.h new file mode 100644 index 00000000..2b9a7ae3 --- /dev/null +++ b/src/ballistica/scene/node/text_node.h @@ -0,0 +1,122 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_TEXT_NODE_H_ +#define BALLISTICA_SCENE_NODE_TEXT_NODE_H_ + +#include +#include + +#include "ballistica/graphics/renderer.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class TextNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit TextNode(Scene* scene); + ~TextNode() override; + void Draw(FrameDef* frame_def) override; + void OnLanguageChange() override; + void OnScreenSizeChange() override; + auto opacity() const -> float { return opacity_; } + void set_opacity(float val) { opacity_ = val; } + auto trail_opacity() const -> float { return trail_opacity_; } + void set_trail_opacity(float val) { trail_opacity_ = val; } + auto project_scale() const -> float { return project_scale_; } + void set_project_scale(float val) { project_scale_ = val; } + auto scale() const -> float { return scale_; } + void set_scale(float val) { scale_ = val; } + auto trail_project_scale() const -> float { return trail_project_scale_; } + void set_trail_project_scale(float val) { trail_project_scale_ = val; } + auto position() const -> const std::vector& { return position_; } + void SetPosition(const std::vector& val); + auto opacity_scales_shadow() const -> bool { return opacity_scales_shadow_; } + void set_opacity_scales_shadow(bool val) { opacity_scales_shadow_ = val; } + auto big() const -> bool { return big_; } + void SetBig(bool val); + auto trail() const -> bool { return trail_; } + void set_trail(bool val) { trail_ = val; } + auto getText() const -> std::string { return text_raw_; } + void SetText(const std::string& val); + auto GetHAlign() const -> std::string; + void SetHAlign(const std::string& val); + auto GetHAttach() const -> std::string; + void SetHAttach(const std::string& val); + auto GetVAttach() const -> std::string; + void SetVAttach(const std::string& val); + auto GetVAlign() const -> std::string; + void SetVAlign(const std::string& val); + auto color() const -> const std::vector& { return color_; } + void SetColor(const std::vector& vals); + auto trail_color() const -> std::vector { return trail_color_; } + void SetTrailColor(const std::vector& vals); + auto in_world() const -> bool { return in_world_; } + void set_in_world(bool val) { + in_world_ = val; + position_final_dirty_ = true; + } + auto tilt_translate() const -> float { return tilt_translate_; } + void set_tilt_translate(float val) { tilt_translate_ = val; } + auto max_width() const -> float { return max_width_; } + void set_max_width(float val) { max_width_ = val; } + auto shadow() const -> float { return shadow_; } + void set_shadow(float val) { shadow_ = val; } + auto flatness() const -> float { return flatness_; } + void set_flatness(float val) { flatness_ = val; } + auto client_only() const -> bool { return client_only_; } + void set_client_only(bool val) { client_only_ = val; } + auto host_only() const -> bool { return host_only_; } + void set_host_only(bool val) { host_only_ = val; } + auto vr_depth() const -> float { return vr_depth_; } + void set_vr_depth(float val) { vr_depth_ = val; } + auto rotate() const -> float { return rotate_; } + void set_rotate(float val) { rotate_ = val; } + auto front() const -> bool { return front_; } + void set_front(bool val) { front_ = val; } + + private: + enum class HAlign { kLeft, kCenter, kRight }; + enum class VAlign { kNone, kTop, kCenter, kBottom }; + enum class HAttach { kLeft, kCenter, kRight }; + enum class VAttach { kTop, kCenter, kBottom }; + void Update(); + TextGroup text_group_; + bool text_group_dirty_ = true; + bool text_width_dirty_ = true; + bool text_translation_dirty_ = true; + bool opacity_scales_shadow_ = true; + bool client_only_ = false; + bool host_only_ = false; + HAlign h_align_ = HAlign::kLeft; + VAlign v_align_ = VAlign::kNone; + HAttach h_attach_ = HAttach::kCenter; + VAttach v_attach_ = VAttach::kCenter; + float vr_depth_ = 0.0f; + bool in_world_ = false; + std::string text_translated_; + std::string text_raw_; + std::vector position_ = {0.0f, 0.0f, 0.0f}; + std::vector position_final_; + bool position_final_dirty_ = true; + float scale_ = 1.0f; + float rotate_ = 0.0f; + bool front_ = false; + std::vector color_ = {1.0f, 1.0f, 1.0f, 1.0f}; + std::vector trail_color_ = {1.0f, 1.0f, 1.0f}; + float project_scale_ = 1.0f; + float trail_project_scale_ = 1.0f; + float opacity_ = 1.0f; + float trail_opacity_ = 1.0f; + float shadow_ = 0.0f; + float flatness_ = 0.0f; + bool trail_ = false; + bool big_ = false; + float tilt_translate_ = 0.0f; + float max_width_ = 0.0f; + float text_width_ = 0.0f; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_TEXT_NODE_H_ diff --git a/src/ballistica/scene/node/texture_sequence_node.cc b/src/ballistica/scene/node/texture_sequence_node.cc new file mode 100644 index 00000000..6644f9dd --- /dev/null +++ b/src/ballistica/scene/node/texture_sequence_node.cc @@ -0,0 +1,76 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/texture_sequence_node.h" + +#include "ballistica/media/component/texture.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +class TextureSequenceNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS TextureSequenceNode + BA_NODE_CREATE_CALL(CreateTextureSequence); + BA_INT_ATTR(rate, rate, set_rate); + BA_TEXTURE_ARRAY_ATTR(input_textures, input_textures, set_input_textures); + BA_TEXTURE_ATTR_READONLY(output_texture, output_texture); +#undef BA_NODE_TYPE_CLASS + + TextureSequenceNodeType() + : NodeType("texture_sequence", CreateTextureSequence), + rate(this), + input_textures(this), + output_texture(this) {} +}; +static NodeType* node_type{}; + +auto TextureSequenceNode::InitType() -> NodeType* { + node_type = new TextureSequenceNodeType(); + return node_type; +} + +TextureSequenceNode::TextureSequenceNode(Scene* scene) + : Node(scene, node_type), index_(0), rate_(1000), sleep_count_(0) {} + +auto TextureSequenceNode::input_textures() const -> std::vector { + return RefsToPointers(input_textures_); +} + +void TextureSequenceNode::set_input_textures( + const std::vector& vals) { + input_textures_ = PointersToRefs(vals); + + // Make sure index_ doesnt go out of range. + if (!input_textures_.empty()) { + index_ = index_ % static_cast(input_textures_.size()); + } +} + +auto TextureSequenceNode::output_texture() const -> Texture* { + if (input_textures_.empty()) { + return nullptr; + } + assert(index_ < static_cast(input_textures_.size())); + return input_textures_[index_].get(); +} + +void TextureSequenceNode::Step() { + if (sleep_count_ <= 0) { + if (!input_textures_.empty()) { + index_ = (index_ + 1) % static_cast(input_textures_.size()); + } + sleep_count_ = rate_; + } + sleep_count_ -= kGameStepMilliseconds; +} + +void TextureSequenceNode::set_rate(int val) { + if (val != rate_) { + rate_ = val; + sleep_count_ = val; + } +} + +} // namespace ballistica diff --git a/src/ballistica/scene/node/texture_sequence_node.h b/src/ballistica/scene/node/texture_sequence_node.h new file mode 100644 index 00000000..72cf0ccd --- /dev/null +++ b/src/ballistica/scene/node/texture_sequence_node.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_TEXTURE_SEQUENCE_NODE_H_ +#define BALLISTICA_SCENE_NODE_TEXTURE_SEQUENCE_NODE_H_ + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class TextureSequenceNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit TextureSequenceNode(Scene* scene); + void Step() override; + auto rate() const -> int { return rate_; } + void set_rate(int val); + auto input_textures() const -> std::vector; + void set_input_textures(const std::vector& vals); + auto output_texture() const -> Texture*; + + private: + int sleep_count_{}; + int index_{}; + int rate_{}; + std::vector > input_textures_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_TEXTURE_SEQUENCE_NODE_H_ diff --git a/src/ballistica/scene/node/time_display_node.cc b/src/ballistica/scene/node/time_display_node.cc new file mode 100644 index 00000000..0c8d768f --- /dev/null +++ b/src/ballistica/scene/node/time_display_node.cc @@ -0,0 +1,137 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/node/time_display_node.h" + +#include +#include + +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class TimeDisplayNodeType : public NodeType { + public: +#define BA_NODE_TYPE_CLASS TimeDisplayNode + BA_NODE_CREATE_CALL(CreateTimeDisplayNode); + BA_STRING_ATTR_READONLY(output, GetOutput); + BA_INT64_ATTR(time2, time2, set_time2); + BA_INT64_ATTR(time1, time1, set_time1); + BA_INT64_ATTR(timemin, time_min, set_time_min); + BA_INT64_ATTR(timemax, time_max, set_time_max); + BA_BOOL_ATTR(showsubseconds, show_sub_seconds, set_show_sub_seconds); +#undef BA_NODE_TYPE_CLASS + + TimeDisplayNodeType() + : NodeType("timedisplay", CreateTimeDisplayNode), + output(this), + time2(this), + time1(this), + timemin(this), + timemax(this), + showsubseconds(this) {} +}; + +static NodeType* node_type{}; + +auto TimeDisplayNode::InitType() -> NodeType* { + node_type = new TimeDisplayNodeType(); + return node_type; +} + +TimeDisplayNode::TimeDisplayNode(Scene* scene) : Node(scene, node_type) {} + +TimeDisplayNode::~TimeDisplayNode() = default; + +auto TimeDisplayNode::GetOutput() -> std::string { + assert(InGameThread()); + if (translations_dirty_) { + time_suffix_hours_ = + g_game->CompileResourceString(R"({"r":"timeSuffixHoursText"})", "tda"); + time_suffix_minutes_ = g_game->CompileResourceString( + R"({"r":"timeSuffixMinutesText"})", "tdb"); + time_suffix_seconds_ = g_game->CompileResourceString( + R"({"r":"timeSuffixSecondsText"})", "tdc"); + translations_dirty_ = false; + output_dirty_ = true; + } + if (output_dirty_) { + millisecs_t t = time2_ - time1_; + t = std::min(t, time_max_); + t = std::max(t, time_min_); + output_ = ""; + bool is_negative = false; + if (t < 0) { + t = -t; + is_negative = true; + } + + // Drop the last digit to better line up with in-game math. + t = (t / 10) * 10; + + // Hours. + int h = static_cast_check_fit((t / 1000) / (60 * 60)); + if (h != 0) { + std::string s = time_suffix_hours_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%d", h); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output_.empty()) { + output_ += " "; + } + output_ += s; + } + + // Minutes. + int m = static_cast_check_fit(((t / 1000) / 60) % 60); + if (m != 0) { + std::string s = time_suffix_minutes_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%d", m); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output_.empty()) { + output_ += " "; + } + output_ += s; + } + + // Seconds (with hundredths). + if (show_sub_seconds_) { + float sec = fmod(static_cast(t) / 1000.0f, 60.0f); + if (sec >= 0.005f || output_.empty()) { + std::string s = time_suffix_seconds_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%.2f", sec); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output_.empty()) { + output_ += " "; + } + output_ += s; + } + } else { + // Seconds (integer). + int sec = static_cast_check_fit(t / 1000 % 60); + if (sec != 0 || output_.empty()) { + std::string s = time_suffix_seconds_; + char buffer[100]; + snprintf(buffer, sizeof(buffer), "%d", sec); + Utils::StringReplaceOne(&s, "${COUNT}", buffer); + if (!output_.empty()) { + output_ += " "; + } + output_ += s; + } + } + if (is_negative) { + output_ = "-" + output_; + } + output_dirty_ = false; + } + return output_; +} + +void TimeDisplayNode::OnLanguageChange() { translations_dirty_ = true; } + +} // namespace ballistica diff --git a/src/ballistica/scene/node/time_display_node.h b/src/ballistica/scene/node/time_display_node.h new file mode 100644 index 00000000..78cea7c9 --- /dev/null +++ b/src/ballistica/scene/node/time_display_node.h @@ -0,0 +1,71 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_NODE_TIME_DISPLAY_NODE_H_ +#define BALLISTICA_SCENE_NODE_TIME_DISPLAY_NODE_H_ + +#include + +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class TimeDisplayNode : public Node { + public: + static auto InitType() -> NodeType*; + auto GetOutput() -> std::string; + auto time2() const -> millisecs_t { return time2_; } + void set_time2(millisecs_t value) { + if (time2_ != value) { + time2_ = value; + output_dirty_ = true; + } + } + auto time1() const -> millisecs_t { return time1_; } + void set_time1(millisecs_t value) { + if (time1_ != value) { + time1_ = value; + output_dirty_ = true; + } + } + auto time_min() const -> millisecs_t { return time_min_; } + void set_time_min(millisecs_t val) { + if (time_min_ != val) { + time_min_ = val; + output_dirty_ = true; + } + } + auto time_max() const -> millisecs_t { return time_max_; } + void set_time_max(millisecs_t val) { + if (time_max_ != val) { + time_max_ = val; + output_dirty_ = true; + } + } + auto show_sub_seconds() const -> bool { return show_sub_seconds_; } + void set_show_sub_seconds(bool val) { + if (show_sub_seconds_ != val) { + show_sub_seconds_ = val; + output_dirty_ = true; + } + } + explicit TimeDisplayNode(Scene* scene); + ~TimeDisplayNode() override; + void OnLanguageChange() override; + + private: + bool output_dirty_ = true; + std::string output_; + millisecs_t time_min_ = -999999999; + millisecs_t time_max_ = 999999999; + millisecs_t time2_ = 0; + millisecs_t time1_ = 0; + bool show_sub_seconds_ = false; + std::string time_suffix_hours_; + std::string time_suffix_minutes_; + std::string time_suffix_seconds_; + bool translations_dirty_ = true; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_NODE_TIME_DISPLAY_NODE_H_ diff --git a/src/ballistica/scene/scene.cc b/src/ballistica/scene/scene.cc new file mode 100644 index 00000000..3d0038df --- /dev/null +++ b/src/ballistica/scene/scene.cc @@ -0,0 +1,624 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene/scene.h" + +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/player.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/networking/networking.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/scene/node/anim_curve_node.h" +#include "ballistica/scene/node/bomb_node.h" +#include "ballistica/scene/node/combine_node.h" +#include "ballistica/scene/node/explosion_node.h" +#include "ballistica/scene/node/flag_node.h" +#include "ballistica/scene/node/flash_node.h" +#include "ballistica/scene/node/globals_node.h" +#include "ballistica/scene/node/image_node.h" +#include "ballistica/scene/node/light_node.h" +#include "ballistica/scene/node/locator_node.h" +#include "ballistica/scene/node/math_node.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_attribute_connection.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/node/null_node.h" +#include "ballistica/scene/node/player_node.h" +#include "ballistica/scene/node/prop_node.h" +#include "ballistica/scene/node/region_node.h" +#include "ballistica/scene/node/scorch_node.h" +#include "ballistica/scene/node/session_globals_node.h" +#include "ballistica/scene/node/shield_node.h" +#include "ballistica/scene/node/sound_node.h" +#include "ballistica/scene/node/spaz_node.h" +#include "ballistica/scene/node/terrain_node.h" +#include "ballistica/scene/node/text_node.h" +#include "ballistica/scene/node/texture_sequence_node.h" +#include "ballistica/scene/node/time_display_node.h" + +namespace ballistica { + +void Scene::Init() { + NodeType* node_types[] = {NullNode::InitType(), + GlobalsNode::InitType(), + SessionGlobalsNode::InitType(), + PropNode::InitType(), + FlagNode::InitType(), + BombNode::InitType(), + ExplosionNode::InitType(), + ShieldNode::InitType(), + LightNode::InitType(), + TextNode::InitType(), + AnimCurveNode::InitType(), + ImageNode::InitType(), + TerrainNode::InitType(), + MathNode::InitType(), + LocatorNode::InitType(), + PlayerNode::InitType(), + CombineNode::InitType(), + SoundNode::InitType(), + SpazNode::InitType(), + RegionNode::InitType(), + ScorchNode::InitType(), + FlashNode::InitType(), + TextureSequenceNode::InitType(), + TimeDisplayNode::InitType()}; + + int next_type_id = 0; + assert(g_app_globals != nullptr); + for (auto* t : node_types) { + g_app_globals->node_types[t->name()] = t; + g_app_globals->node_types_by_id[next_type_id] = t; + t->set_id(next_type_id++); + } + + // Types: I is 32 bit int, i is 16 bit int, c is 8 bit int, + // F is 32 bit float, f is 16 bit float, + // s is string, b is bool. + SetupNodeMessageType("flash", NodeMessageType::kFlash, ""); + SetupNodeMessageType("footing", NodeMessageType::kFooting, "c"); + SetupNodeMessageType("impulse", NodeMessageType::kImpulse, "fffffffffifff"); + SetupNodeMessageType("kick_back", NodeMessageType::kKickback, "fffffff"); + SetupNodeMessageType("celebrate", NodeMessageType::kCelebrate, "i"); + SetupNodeMessageType("celebrate_l", NodeMessageType::kCelebrateL, "i"); + SetupNodeMessageType("celebrate_r", NodeMessageType::kCelebrateR, "i"); + SetupNodeMessageType("knockout", NodeMessageType::kKnockout, "f"); + SetupNodeMessageType("hurt_sound", NodeMessageType::kHurtSound, ""); + SetupNodeMessageType("picked_up", NodeMessageType::kPickedUp, ""); + SetupNodeMessageType("jump_sound", NodeMessageType::kJumpSound, ""); + SetupNodeMessageType("attack_sound", NodeMessageType::kAttackSound, ""); + SetupNodeMessageType("scream_sound", NodeMessageType::kScreamSound, ""); + SetupNodeMessageType("stand", NodeMessageType::kStand, "ffff"); +} + +void Scene::SetupNodeMessageType(const std::string& name, NodeMessageType val, + const std::string& format) { + assert(g_app_globals != nullptr); + g_app_globals->node_message_types[name] = val; + assert(static_cast(val) >= 0); + if (g_app_globals->node_message_formats.size() <= static_cast(val)) { + g_app_globals->node_message_formats.resize(static_cast(val) + 1); + } + g_app_globals->node_message_formats[static_cast(val)] = format; +} + +auto Scene::GetGameStream() const -> GameStream* { + return output_stream_.get(); +} + +void Scene::SetMapBounds(float xmin, float ymin, float zmin, float xmax, + float ymax, float zmax) { + bounds_min_[0] = xmin; + bounds_min_[1] = ymin; + bounds_min_[2] = zmin; + bounds_max_[0] = xmax; + bounds_max_[1] = ymax; + bounds_max_[2] = zmax; +} + +Scene::Scene(millisecs_t start_time) + : time_(start_time), + stepnum_(start_time / kGameStepMilliseconds), + last_step_real_time_(GetRealTime()) { + dynamics_ = Object::New(this); + + // Reset world bounds to default. + bounds_min_[0] = -30; + bounds_max_[0] = 30; + bounds_min_[1] = -10; + bounds_max_[1] = 100; + bounds_min_[2] = -30; + bounds_max_[2] = 30; +} + +Scene::~Scene() { + // This may already be set to true by a host_activity/etc, but + // make sure it is at this point. + shutting_down_ = true; + + // Manually kill our nodes so they can remove all their own dynamics stuff + // before dynamics goes down. + nodes_.clear(); + + dynamics_.Clear(); + + // If we were associated with an output-stream, inform it of our demise. + if (output_stream_.exists()) { + output_stream_->RemoveScene(this); + } +} + +void Scene::PlaySoundAtPosition(Sound* sound, float volume, float x, float y, + float z, bool host_only) { + if (output_stream_.exists() && !host_only) { + output_stream_->PlaySoundAtPosition(sound, volume, x, y, z); + } + g_audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, y, z); +} + +void Scene::PlaySound(Sound* sound, float volume, bool host_only) { + if (output_stream_.exists() && !host_only) { + output_stream_->PlaySound(sound, volume); + } + g_audio->PlaySound(sound->GetSoundData(), volume); +} + +auto Scene::IsOutOfBounds(float x, float y, float z) -> bool { + if (std::isnan(x) || std::isnan(y) || std::isnan(z) || std::isinf(x) + || std::isinf(y) || std::isinf(z)) + BA_LOG_ONCE("ERROR: got INF/NAN value on IsOutOfBounds() check"); + + return ((x < bounds_min_[0]) || (x > bounds_max_[0]) || (y < bounds_min_[1]) + || (y > bounds_max_[1]) || (z < bounds_min_[2]) + || (z > bounds_max_[2]) || std::isnan(x) || std::isnan(y) + || std::isnan(z) || std::isinf(x) || std::isinf(y) || std::isinf(z)); +} + +void Scene::Draw(FrameDef* frame_def) { + // Draw our nodes. + for (auto&& i : nodes_) { + g_graphics->PreNodeDraw(); + i->Draw(frame_def); + g_graphics->PostNodeDraw(); + } + + // Draw any dynamics debugging extras. + dynamics_->Draw(frame_def); +} + +auto Scene::GetNodeMessageType(const std::string& type) -> NodeMessageType { + assert(g_app_globals != nullptr); + auto i = g_app_globals->node_message_types.find(type); + if (i == g_app_globals->node_message_types.end()) { + throw Exception("Invalid node-message type: '" + type + "'"); + } + return i->second; +} + +auto Scene::GetNodeMessageTypeName(NodeMessageType t) -> std::string { + assert(g_app_globals != nullptr); + for (auto&& i : g_app_globals->node_message_types) { + if (i.second == t) { + return i.first; + } + } + return ""; +} + +void Scene::SetPlayerNode(int id, PlayerNode* n) { player_nodes_[id] = n; } + +auto Scene::GetPlayerNode(int id) -> PlayerNode* { + auto i = player_nodes_.find(id); + if (i != player_nodes_.end()) { + return i->second.get(); + } + return nullptr; +} + +void Scene::Step() { + out_of_bounds_nodes_.clear(); + + // Step all our nodes. + { + in_step_ = true; + last_step_real_time_ = GetRealTime(); + for (auto&& i : nodes_) { + Node* node = i.get(); + node->Step(); + + // Now that it's stepped, pump new values to any nodes it's connected to. + node->UpdateConnections(); + } + in_step_ = false; + } + bool is_foreground = (g_game->GetForegroundScene() == this); + + // Add a step command to the output stream. + if (output_stream_.exists()) { + output_stream_->StepScene(this); + } + + // And step things locally. + if (is_foreground) { + Vector3f cam_pos = {0.0f, 0.0f, 0.0f}; + g_graphics->camera()->get_position(&cam_pos.x, &cam_pos.y, &cam_pos.z); +#if !BA_HEADLESS_BUILD + g_bg_dynamics->Step(cam_pos); +#endif // !BA_HEADLESS_BUILD + } + + // Lastly step our sim. + dynamics_->process(); + + time_ += kGameStepMilliseconds; + stepnum_++; +} + +void Scene::DeleteNode(Node* node) { + assert(node); + + if (in_step_) { + throw Exception( + "Cannot delete nodes within a sim step." + " Consider a deferred call or timer. Node=" + + node->GetObjectDescription()); + } + + // Copy refs to its death-actions and dependent-nodes; we'll deal with these + // after the node is dead so we're sure they don't muck with the node. + std::vector > death_actions = + node->death_actions(); + std::vector > dependent_nodes = node->dependent_nodes(); + + // Sanity test to make sure it dies when we ask. +#if BA_DEBUG_BUILD + Object::WeakRef temp_weak_ref(node); + BA_PRECONDITION(temp_weak_ref.exists()); +#endif + + // Copy a strong ref to this node to keep it alive until we've wiped it from + // the list. (so in its destructor it won't see itself on the list). + Object::Ref temp_ref(node); + nodes_.erase(node->iterator()); + + temp_ref.Clear(); + + // Sanity test: at this point the node should be dead. +#if BA_DEBUG_BUILD + if (temp_weak_ref.exists()) { + Log("Error: node still exists after ref release!!"); + } +#endif // BA_DEBUG_BUILD + + // Lastly run any death actions the node had and kill dependent nodes. + if (!shutting_down()) { + for (auto&& i : death_actions) { + i->Run(); + } + for (auto&& i : dependent_nodes) { + Node* node2 = i.get(); + if (node2) { + node2->scene()->DeleteNode(node2); + } + } + } +} + +void Scene::GraphicsQualityChanged(GraphicsQuality q) { + assert(InGameThread()); + for (auto&& i : nodes_) { + i->OnGraphicsQualityChanged(q); + } +} + +void Scene::ScreenSizeChanged() { + assert(InGameThread()); + for (auto&& i : nodes_) { + i->OnScreenSizeChange(); // New. + } +} + +void Scene::LanguageChanged() { + assert(InGameThread()); + for (auto&& i : nodes_) { + i->OnLanguageChange(); // New. + } +} + +auto Scene::GetNodeMessageFormat(NodeMessageType type) -> const char* { + assert(g_app_globals != nullptr); + if ((unsigned int)type >= g_app_globals->node_message_formats.size()) { + return nullptr; + } + return g_app_globals->node_message_formats[static_cast(type)].c_str(); +} + +auto Scene::NewNode(const std::string& type_string, const std::string& name, + PyObject* delegate) -> Node* { + assert(InGameThread()); + + if (in_step_) { + throw Exception( + "Cannot create nodes within a sim step." + " Consider a deferred call or timer."); + } + + // Should never change the scene while we're stepping it. + assert(!in_step_); + assert(g_app_globals != nullptr); + auto i = g_app_globals->node_types.find(type_string); + if (i == g_app_globals->node_types.end()) { + throw Exception("Invalid node type: '" + type_string + "'"); + } + auto node = Object::MakeRefCounted(i->second->Create(this)); + assert(node.exists()); + node->AddToScene(this); + node->set_label(name); + node->SetDelegate(delegate); + return node.get(); // NOLINT +} + +void Scene::Dump(GameStream* stream) { + assert(InGameThread()); + stream->AddScene(this); + + // If we're the foreground one, communicate that fact as well. + if (g_game->GetForegroundScene() == this) { + stream->SetForegroundScene(this); + } +} + +void Scene::DumpNodes(GameStream* out) { + // Dumps commands to the output stream to recreate scene's nodes + // in their current state. + + // First we go through and create all nodes. + // We have to do this all at once before setting attrs since any node + // can refer to any other in an attr set. + for (auto&& i : nodes_) { + Node* node = i.get(); + assert(node); + + // add the node + out->AddNode(node); + } + + std::vector > node_attr_sets; + + // Now go through and set *most* node attr values. + for (auto&& i1 : nodes_) { + Node* node = i1.get(); + assert(node); + + // Now we need to set *all* of its attrs in order. + // FIXME: Could be nice to send only ones that have changed from + // defaults; would need to add that functionality to NodeType. + NodeType* node_type = node->type(); + for (auto&& i2 : node_type->attributes_by_index()) { + NodeAttribute attr; + attr.assign(node, i2); + if (!attr.is_read_only()) { + switch (attr.type()) { + case NodeAttributeType::kFloat: { + out->SetNodeAttr(attr, attr.GetAsFloat()); + break; + } + case NodeAttributeType::kInt: { + out->SetNodeAttr(attr, attr.GetAsInt()); + break; + } + case NodeAttributeType::kBool: { + out->SetNodeAttr(attr, attr.GetAsBool()); + break; + } + case NodeAttributeType::kFloatArray: { + out->SetNodeAttr(attr, attr.GetAsFloats()); + break; + } + case NodeAttributeType::kIntArray: { + out->SetNodeAttr(attr, attr.GetAsInts()); + break; + } + case NodeAttributeType::kString: { + out->SetNodeAttr(attr, attr.GetAsString()); + break; + } + case NodeAttributeType::kNode: { + // Node-attrs are a special case - we can't set them until after + // nodes are fully constructed. so lets just make a list of them + // and do it at the end. + node_attr_sets.emplace_back(attr, attr.GetAsNode()); + break; + } + case NodeAttributeType::kPlayer: { + out->SetNodeAttr(attr, attr.GetAsPlayer()); + break; + } + case NodeAttributeType::kMaterialArray: { + out->SetNodeAttr(attr, attr.GetAsMaterials()); + break; + } + case NodeAttributeType::kTexture: { + out->SetNodeAttr(attr, attr.GetAsTexture()); + break; + } + case NodeAttributeType::kTextureArray: { + out->SetNodeAttr(attr, attr.GetAsTextures()); + break; + } + case NodeAttributeType::kSound: { + out->SetNodeAttr(attr, attr.GetAsSound()); + break; + } + case NodeAttributeType::kSoundArray: { + out->SetNodeAttr(attr, attr.GetAsSounds()); + break; + } + case NodeAttributeType::kModel: { + out->SetNodeAttr(attr, attr.GetAsModel()); + break; + } + case NodeAttributeType::kModelArray: { + out->SetNodeAttr(attr, attr.GetAsModels()); + break; + } + case NodeAttributeType::kCollideModel: { + out->SetNodeAttr(attr, attr.GetAsCollideModel()); + break; + } + case NodeAttributeType::kCollideModelArray: { + out->SetNodeAttr(attr, attr.GetAsCollideModels()); + break; + } + default: + Log("Invalid attr type for Scene::DumpNodes() attr set: " + + std::to_string(static_cast(attr.type()))); + break; + } + } + } + } + + // Now run through all nodes once more and add an OnCreate() call + // so they can do any post-create setup they need to. + for (auto&& i : nodes_) { + Node* node = i.get(); + assert(node); + out->NodeOnCreate(node); + } + + // Set any node-attribute values now that all nodes are fully constructed. + for (auto&& i : node_attr_sets) { + out->SetNodeAttr(i.first, i.second); + } + + // And lastly re-establish node attribute-connections. + for (auto&& i : nodes_) { + Node* node = i.get(); + assert(node); + for (auto&& j : node->attribute_connections()) { + assert(j.exists()); + Node* src_node = j->src_node.get(); + assert(src_node); + Node* dst_node = j->dst_node.get(); + assert(dst_node); + NodeAttributeUnbound* src_attr = + src_node->type()->GetAttribute(j->src_attr_index); + NodeAttributeUnbound* dst_attr = + dst_node->type()->GetAttribute(j->dst_attr_index); + out->ConnectNodeAttribute(src_node, src_attr, dst_node, dst_attr); + } + } +} + +auto Scene::GetCorrectionMessage(bool blended) -> std::vector { + // Let's loop over our nodes sending a bit of correction data. + + // Go through until we find at least 1 node to send corrections for, + // or until we loop all the way through. + + // 1 byte type, 1 byte blending, 2 byte node count + std::vector message(4); + message[0] = BA_MESSAGE_SESSION_DYNAMICS_CORRECTION; + message[1] = static_cast(blended); + int node_count = 0; + + std::vector dynamic_bodies; + + for (auto&& i : nodes_) { + Node* n = i.get(); + assert(n); + if (n && !n->parts().empty()) { + dynamic_bodies.clear(); + for (auto&& j : n->parts()) { + if (!j->rigid_bodies().empty()) { + for (auto&& k : j->rigid_bodies()) { + if (k->type() == RigidBody::Type::kBody) { + dynamic_bodies.push_back(k); + } + } + } + } + if (!dynamic_bodies.empty()) { + int node_embed_size = 5; // 4 byte node-ID and 1 byte body-count + int body_count = 0; + for (auto&& i2 : dynamic_bodies) { + node_embed_size += 3 + i2->GetEmbeddedSizeFull(); + body_count++; + } + + // Lastly add custom data. + node_embed_size += 2; // size + int resync_data_size = n->GetResyncDataSize(); + node_embed_size += resync_data_size; + + // If this size puts us over our max packet size (and we've got + // something in the packet already) just ship what we've got. + // We'll come back to this one next time. + { + node_count++; + size_t old_size = message.size(); + message.resize(old_size + node_embed_size); + + // Embed node id. + auto stream_id_val = static_cast_check_fit(n->stream_id()); + memcpy(message.data() + old_size, &stream_id_val, + sizeof(stream_id_val)); + + // Embed body count. + message[old_size + 4] = static_cast_check_fit(body_count); + size_t offset = old_size + 5; + for (auto&& i2 : dynamic_bodies) { + // Embed body id. + message[offset++] = static_cast_check_fit(i2->id()); + int body_embed_size = i2->GetEmbeddedSizeFull(); + + // Embed body size. + auto val = static_cast_check_fit(body_embed_size); + memcpy(message.data() + offset, &val, sizeof(val)); + offset += 2; + char* p1 = reinterpret_cast(&(message[offset])); + char* p2 = p1; + i2->EmbedFull(&p2); + assert(p2 - p1 == body_embed_size); + offset += body_embed_size; + } + + // Lastly embed custom data size and custom data. + auto val = static_cast_check_fit(resync_data_size); + memcpy(message.data() + offset, &val, sizeof(val)); + offset += 2; + if (resync_data_size > 0) { + std::vector resync_data = n->GetResyncData(); + assert(resync_data.size() == resync_data_size); + memcpy(message.data() + offset, &(resync_data[0]), + resync_data.size()); + offset += resync_data_size; + } + assert(offset == message.size()); + } + } + } + } + + // If we embedded any nodes, send. + // Store node count in packet. + auto val = static_cast_check_fit(node_count); + memcpy(message.data() + 2, &val, sizeof(val)); + + return message; +} + +void Scene::SetOutputStream(GameStream* val) { output_stream_ = val; } + +} // namespace ballistica diff --git a/src/ballistica/scene/scene.h b/src/ballistica/scene/scene.h new file mode 100644 index 00000000..1fd71c12 --- /dev/null +++ b/src/ballistica/scene/scene.h @@ -0,0 +1,111 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_SCENE_H_ +#define BALLISTICA_SCENE_SCENE_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/game/game.h" +#include "ballistica/scene/node/node.h" + +namespace ballistica { + +class Scene : public Object { + public: + static void Init(); + explicit Scene(millisecs_t starttime); + ~Scene() override; + void Step(); + void Draw(FrameDef* frame_def); + auto NewNode(const std::string& type, const std::string& name, + PyObject* delegate) -> Node*; + void PlaySoundAtPosition(Sound* sound, float volume, float x, float y, + float z, bool host_only = false); + void PlaySound(Sound* sound, float volume, bool host_only = false); + static auto GetNodeMessageType(const std::string& type_name) + -> NodeMessageType; + static auto GetNodeMessageTypeName(NodeMessageType t) -> std::string; + static auto GetNodeMessageFormat(NodeMessageType type) -> const char*; + auto time() const -> millisecs_t { return time_; } + auto stepnum() const -> int64_t { return stepnum_; } + auto nodes() const -> const NodeList& { return nodes_; } + void AddOutOfBoundsNode(Node* n) { out_of_bounds_nodes_.emplace_back(n); } + auto IsOutOfBounds(float x, float y, float z) -> bool; + auto dynamics() const -> Dynamics* { + assert(dynamics_.exists()); + return dynamics_.get(); + } + auto in_step() const -> bool { return in_step_; } + void SetMapBounds(float x, float y, float z, float X, float Y, float Z); + void ScreenSizeChanged(); + void LanguageChanged(); + void GraphicsQualityChanged(GraphicsQuality q); + auto out_of_bounds_nodes() -> const std::vector >& { + return out_of_bounds_nodes_; + } + void DeleteNode(Node* node); + auto shutting_down() const -> bool { return shutting_down_; } + void set_shutting_down(bool val) { shutting_down_ = val; } + auto GetGameStream() const -> GameStream*; + void SetPlayerNode(int id, PlayerNode* n); + auto GetPlayerNode(int id) -> PlayerNode*; + auto use_fixed_vr_overlay() const -> bool { return use_fixed_vr_overlay_; } + void set_use_fixed_vr_overlay(bool val) { use_fixed_vr_overlay_ = val; } + void increment_bg_cover_count() { bg_cover_count_++; } + void decrement_bg_cover_count() { bg_cover_count_--; } + auto has_bg_cover() const -> bool { return (bg_cover_count_ > 0); } + void Dump(GameStream* out); + void DumpNodes(GameStream* out); + auto GetCorrectionMessage(bool blended) -> std::vector; + + void SetOutputStream(GameStream* val); + auto stream_id() const -> int64_t { return stream_id_; } + void set_stream_id(int64_t val) { + assert(stream_id_ == -1); + stream_id_ = val; + } + void clear_stream_id() { + assert(stream_id_ != -1); + stream_id_ = -1; + } + + auto last_step_real_time() const -> millisecs_t { + return last_step_real_time_; + } + auto globals_node() const -> GlobalsNode* { return globals_node_; } + void set_globals_node(GlobalsNode* node) { globals_node_ = node; } + + private: + static void SetupNodeMessageType(const std::string& name, NodeMessageType val, + const std::string& format); + + GlobalsNode* globals_node_{}; // Current globals node (if any). + std::map > player_nodes_; + int64_t stream_id_{-1}; + Object::WeakRef output_stream_; + bool use_fixed_vr_overlay_{}; + Context context_; // Context we were made in. + millisecs_t time_{}; + int64_t stepnum_{}; + bool in_step_{}; + int64_t next_node_id_{}; + + // For globals real_time attr (so is consistent through the step.) + millisecs_t last_step_real_time_{}; + int bg_cover_count_{}; + bool shutting_down_{}; + float bounds_min_[3]{}; + float bounds_max_[3]{}; + std::vector > out_of_bounds_nodes_; + NodeList nodes_; + Object::Ref dynamics_; + friend void Node::AddToScene(Scene*); + friend class ClientSession; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SCENE_SCENE_H_ diff --git a/src/ballistica/ui/console.cc b/src/ballistica/ui/console.cc new file mode 100644 index 00000000..15422ac4 --- /dev/null +++ b/src/ballistica/ui/console.cc @@ -0,0 +1,364 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/console.h" + +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/platform/min_sdl.h" + +namespace ballistica { + +// How much of the screen the console covers when it is at full size. +const float kConsoleSize = 0.9f; +const float kConsoleZDepth = 0.0f; +const int kConsoleLineLimit = 80; +const int kStringBreakUpSize = 1950; +const int kActivateKey1 = SDLK_BACKQUOTE; +const int kActivateKey2 = SDLK_F2; + +Console::Console() { + assert(InGameThread()); + std::string title = std::string("BallisticaCore ") + kAppVersion + " (" + + std::to_string(kAppBuildNumber) + ")"; + if (g_buildconfig.debug_build()) { + title += " (debug)"; + } + if (g_buildconfig.test_build()) { + title += " (test)"; + } + + title_text_group_.SetText(title); + built_text_group_.SetText("Built: " __DATE__ " " __TIME__); + prompt_text_group_.SetText(">"); + + // Print whatever is already in the log. + if (!g_app_globals->console_startup_messages.empty()) { + Print(g_app_globals->console_startup_messages); + g_app_globals->console_startup_messages = ""; + } +} + +Console::~Console() = default; + +auto Console::HandleKeyPress(const SDL_Keysym* keysym) -> bool { + assert(InGameThread()); + + // Handle our toggle buttons no matter whether we're active. + switch (keysym->sym) { + case kActivateKey1: + case kActivateKey2: { + if (!g_buildconfig.demo_build() && !g_buildconfig.arcade_build()) { + // (reset input so characters don't continue walking and stuff) + g_game->ResetInput(); + g_game->ToggleConsole(); + } + return true; + } + default: + break; + } + + if (state_ == State::kInactive) { + return false; + } + + // The rest of these presses we only handle while active. + switch (keysym->sym) { + case SDLK_ESCAPE: + ToggleState(); + break; + case SDLK_BACKSPACE: + case SDLK_DELETE: { + std::vector unichars = + Utils::UnicodeFromUTF8(input_string_, "fjco38"); + if (!unichars.empty()) { + unichars.resize(unichars.size() - 1); + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + } + break; + } + case SDLK_UP: + case SDLK_DOWN: { + if (input_history_.empty()) { + break; + } + if (keysym->sym == SDLK_UP) { + input_history_position_++; + } else { + input_history_position_--; + } + int input_history_position_used = + (input_history_position_ - 1) + % static_cast(input_history_.size()); + int j = 0; + for (auto& i : input_history_) { + if (j == input_history_position_used) { + input_string_ = i; + input_text_dirty_ = true; + break; + } + j++; + } + break; + } + case SDLK_KP_ENTER: + case SDLK_RETURN: { + input_history_position_ = 0; + if (input_string_ == "clear") { + last_line_.clear(); + lines_.clear(); + } else { + g_game->PushInGameConsoleScriptCommand(input_string_); + } + input_history_.push_front(input_string_); + if (input_history_.size() > 100) input_history_.pop_back(); + input_string_.resize(0); + input_text_dirty_ = true; + break; + } + default: { +#if BA_SDL2_BUILD || BA_MINSDL_BUILD + // (in SDL2/Non-SDL we dont' get chars from keypress events; + // they come through as text edit events) +#else // BA_SDL2_BUILD + if (keysym->unicode < 0x80 && keysym->unicode > 0) { + std::vector unichars = + Utils::UnicodeFromUTF8(input_string_, "cjofrh0"); + unichars.push_back(keysym->unicode); + input_string_ = Utils::GetValidUTF8( + Utils::UTF8FromUnicode(unichars).c_str(), "sdkr"); + input_text_dirty_ = true; + } +#endif // BA_SDL2_BUILD + break; + } + } + return true; +} + +void Console::ToggleState() { + switch (state_) { + case State::kInactive: + state_ = State::kMini; + break; + case State::kMini: + state_ = State::kFull; + break; + case State::kFull: + state_ = State::kInactive; + break; + } + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kBlip)); + transition_start_ = GetRealTime(); +} + +auto Console::HandleTextEditing(const std::string& text) -> bool { + assert(InGameThread()); + if (state_ == State::kInactive) { + return false; + } + + // Ignore back-tick because we use that key to toggle the console. + if (text == "`") { + return false; + } + input_string_ += text; + input_text_dirty_ = true; + return true; +} + +auto Console::HandleKeyRelease(const SDL_Keysym* keysym) -> bool { + // Always absorb our activate keys. + if (keysym->sym == kActivateKey1 || keysym->sym == kActivateKey2) { + return true; + } + + // Otherwise simply absorb all key-ups if we're active. + return state_ != State::kInactive; +} + +void Console::Print(const std::string& sIn) { + assert(InGameThread()); + std::string s = Utils::GetValidUTF8(sIn.c_str(), "cspr"); + last_line_ += s; + std::vector broken_up; + g_text_graphics->BreakUpString(last_line_.c_str(), kStringBreakUpSize, + &broken_up); + + // Spit out all completed lines and keep the last one as lastline. + for (size_t i = 0; i < broken_up.size() - 1; i++) { + lines_.emplace_back(broken_up[i], GetRealTime()); + if (lines_.size() > kConsoleLineLimit) lines_.pop_front(); + } + last_line_ = broken_up[broken_up.size() - 1]; + last_line_mesh_dirty_ = true; +} + +void Console::Draw(RenderPass* pass) { + millisecs_t transition_ticks = 100; + if ((transition_start_ != 0) + && (state_ != State::kInactive + || ((GetRealTime() - transition_start_) < transition_ticks))) { + float ratio = (static_cast(GetRealTime() - transition_start_) + / transition_ticks); + float bottom; + float mini_size = 90; + if (state_ == State::kMini) { + bottom = pass->virtual_height() - mini_size; + } else { + bottom = pass->virtual_height() - pass->virtual_height() * kConsoleSize; + } + if (GetRealTime() - transition_start_ < transition_ticks) { + if (state_ == State::kMini) { + bottom = pass->virtual_height() * (1.0f - ratio) + bottom * (ratio); + } else if (state_ == State::kFull) { + bottom = + (pass->virtual_height() - pass->virtual_height() * kConsoleSize) + * (ratio) + + (pass->virtual_height() - mini_size) * (1.0f - ratio); + } else { + bottom = pass->virtual_height() * ratio + bottom * (1.0f - ratio); + } + } + { + bg_mesh_.SetPositionAndSize(0, bottom, kConsoleZDepth, + pass->virtual_width(), + (pass->virtual_height() - bottom)); + stripe_mesh_.SetPositionAndSize(0, bottom + 15, kConsoleZDepth, + pass->virtual_width(), 15); + shadow_mesh_.SetPositionAndSize(0, bottom - 7, kConsoleZDepth, + pass->virtual_width(), 7); + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(0, 0, 0.1f, 0.9f); + c.DrawMesh(&bg_mesh_); + c.Submit(); + c.SetColor(1.0f, 1.0f, 1.0f, 0.1f); + c.DrawMesh(&stripe_mesh_); + c.Submit(); + c.SetColor(0, 0, 0, 0.1f); + c.DrawMesh(&shadow_mesh_); + c.Submit(); + } + if (input_text_dirty_) { + input_text_group_.SetText(input_string_); + input_text_dirty_ = false; + last_input_text_change_time_ = pass->frame_def()->real_time(); + } + { + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(0.5f, 0.5f, 0.7f, 1.0f); + int elem_count = built_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(built_text_group_.GetElementTexture(e)); + c.PushTransform(); + c.Translate(pass->virtual_width() - 175.0f, bottom + 0, kConsoleZDepth); + c.Scale(0.5f, 0.5f, 0.5f); + c.DrawMesh(built_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + elem_count = title_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(title_text_group_.GetElementTexture(e)); + c.PushTransform(); + c.Translate(20.0f, bottom + 0, kConsoleZDepth); + c.Scale(0.5f, 0.5f, 0.5f); + c.DrawMesh(title_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + elem_count = prompt_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(prompt_text_group_.GetElementTexture(e)); + c.SetColor(1, 1, 1, 1); + c.PushTransform(); + c.Translate(5.0f, bottom + 15.0f, kConsoleZDepth); + c.Scale(0.5f, 0.5f, 0.5f); + c.DrawMesh(prompt_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + elem_count = input_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(input_text_group_.GetElementTexture(e)); + c.PushTransform(); + c.Translate(15.0f, bottom + 15.0f, kConsoleZDepth); + c.Scale(0.5f, 0.5f, 0.5f); + c.DrawMesh(input_text_group_.GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + + // Carat. + millisecs_t real_time = pass->frame_def()->real_time(); + if (real_time % 200 < 100 + || (real_time - last_input_text_change_time_ < 100)) { + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1, 1, 1, 0.7f); + c.PushTransform(); + c.Translate(19.0f + g_text_graphics->GetStringWidth(input_string_) * 0.5f, + bottom + 23.0f, kConsoleZDepth); + c.Scale(5, 11, 1.0f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + } + + // Draw console messages. + { + float draw_scale = 0.5f; + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1, 1, 1, 1); + float h = 0.5f + * (g_graphics->screen_virtual_width() + - (kStringBreakUpSize * draw_scale)); + float v = bottom + 32.0f; + if (!last_line_.empty()) { + if (last_line_mesh_dirty_) { + if (!last_line_mesh_group_.exists()) { + last_line_mesh_group_ = Object::New(); + } + last_line_mesh_group_->SetText(last_line_); + last_line_mesh_dirty_ = false; + } + int elem_count = last_line_mesh_group_->GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(last_line_mesh_group_->GetElementTexture(e)); + c.PushTransform(); + c.Translate(h, v + 2, kConsoleZDepth); + c.Scale(draw_scale, draw_scale); + c.DrawMesh(last_line_mesh_group_->GetElementMesh(e)); + c.PopTransform(); + } + v += 14; + } + for (auto i = lines_.rbegin(); i != lines_.rend(); i++) { + int elem_count = i->getText().GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(i->getText().GetElementTexture(e)); + c.PushTransform(); + c.Translate(h, v + 2, kConsoleZDepth); + c.Scale(draw_scale, draw_scale); + c.DrawMesh(i->getText().GetElementMesh(e)); + c.PopTransform(); + } + v += 14; + if (v > pass->virtual_height() + 14) { + break; + } + } + c.Submit(); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/ui/console.h b/src/ballistica/ui/console.h new file mode 100644 index 00000000..466eb20f --- /dev/null +++ b/src/ballistica/ui/console.h @@ -0,0 +1,70 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_CONSOLE_H_ +#define BALLISTICA_UI_CONSOLE_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +class Console { + public: + Console(); + ~Console(); + auto active() const -> bool { return (state_ != State::kInactive); } + enum class State { kInactive, kMini, kFull }; + auto transition_start() const -> millisecs_t { return transition_start_; } + auto HandleTextEditing(const std::string& text) -> bool; + auto HandleKeyPress(const SDL_Keysym* keysym) -> bool; + auto HandleKeyRelease(const SDL_Keysym* keysym) -> bool; + void ToggleState(); + void Print(const std::string& s); + void Draw(RenderPass* pass); + + private: + ImageMesh bg_mesh_; + ImageMesh stripe_mesh_; + ImageMesh shadow_mesh_; + TextGroup built_text_group_; + TextGroup title_text_group_; + TextGroup prompt_text_group_; + TextGroup input_text_group_; + millisecs_t last_input_text_change_time_{}; + bool input_text_dirty_{true}; + millisecs_t transition_start_{}; + State state_{State::kInactive}; + + class Message { + public: + Message(std::string s_in, millisecs_t c) + : creation_time(c), s(std::move(s_in)) {} + millisecs_t creation_time; + std::string s; + auto getText() -> TextGroup& { + if (!s_mesh_.exists()) { + s_mesh_ = Object::New(); + s_mesh_->SetText(s); + } + return *s_mesh_; + } + + private: + Object::Ref s_mesh_; + }; + std::string input_string_; + std::list input_history_; + int input_history_position_{}; + std::list lines_; + std::string last_line_; + Object::Ref last_line_mesh_group_; + bool last_line_mesh_dirty_{true}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_CONSOLE_H_ diff --git a/src/ballistica/ui/root_ui.cc b/src/ballistica/ui/root_ui.cc new file mode 100644 index 00000000..078f78f0 --- /dev/null +++ b/src/ballistica/ui/root_ui.cc @@ -0,0 +1,394 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/root_ui.h" + +#include +#include + +#include "ballistica/game/game.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/keyboard_input.h" +#include "ballistica/input/device/touch_input.h" +#include "ballistica/input/input.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/container_widget.h" + +namespace ballistica { + +// Phasing these out; replaced by buttons in our rootwidget. +#define DO_OLD_MENU_PARTY_BUTTONS (!BA_TOOLBAR_TEST) + +const float kMenuButtonSize = 40.0f; +const float kMenuButtonDrawDepth = -0.07f; + +RootUI::RootUI() { + float base_scale; + switch (GetInterfaceType()) { + case UIScale::kLarge: + base_scale = 1.0f; + break; + case UIScale::kMedium: + base_scale = 1.5f; + break; + case UIScale::kSmall: + base_scale = 2.0f; + break; + default: + base_scale = 1.0f; + break; + } + menu_button_size_ = kMenuButtonSize * base_scale; +} +RootUI::~RootUI() = default; + +void RootUI::TogglePartyWindowKeyPress() { + assert(InGameThread()); + if (g_game->GetPartySize() > 1 || g_game->connection_to_host() + || always_draw_party_icon()) { + ActivatePartyIcon(); + } +} + +void RootUI::ActivatePartyIcon() const { + assert(InGameThread()); + ScopedSetContext cp(g_game->GetUIContext()); + + // Originate from center of party icon. If menu button is shown, it is to the + // left of that. + float icon_pos_h = + g_graphics->screen_virtual_width() * 0.5f - menu_button_size_ * 0.5f; + float icon_pos_v = + g_graphics->screen_virtual_height() * 0.5f - menu_button_size_ * 0.5f; + bool menu_active = !(g_ui && g_ui->screen_root_widget() + && g_ui->screen_root_widget()->HasChildren()); + if (menu_active) { + icon_pos_h -= menu_button_size_; + } + g_python->obj(Python::ObjID::kPartyIconActivateCall) + .Call(Vector2f(icon_pos_h, icon_pos_v)); +} + +auto RootUI::HandleMouseButtonDown(float x, float y) -> bool { + // Whether the menu button is visible/active. + bool menu_active = !(g_ui && g_ui->screen_root_widget() + && g_ui->screen_root_widget()->HasChildren()); + + // Handle party button presses (need to do this before UI since it + // floats over the top). Party button is to the left of menu button. + if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { + bool party_button_active = + (!party_window_open_ + && (g_game->GetConnectedClientCount() > 0 + || g_game->connection_to_host() || always_draw_party_icon())); + float party_button_left = + menu_active ? 2 * menu_button_size_ : menu_button_size_; + float party_button_right = menu_active ? menu_button_size_ : 0; + if (party_button_active + && (g_graphics->screen_virtual_width() - x < party_button_left) + && (g_graphics->screen_virtual_width() - x >= party_button_right) + && (g_graphics->screen_virtual_height() - y < menu_button_size_)) { + ActivatePartyIcon(); + return true; + } + } + // Menu button. + if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { + if (menu_active + && (g_graphics->screen_virtual_width() - x < menu_button_size_) + && (g_graphics->screen_virtual_height() - y < menu_button_size_)) { + menu_button_pressed_ = true; + menu_button_hover_ = true; + return true; + } + } + + return false; +} + +void RootUI::HandleMouseButtonUp(float x, float y) { + if (menu_button_pressed_) { + menu_button_pressed_ = false; + menu_button_hover_ = false; + + // If we've got a touch input, bring the menu up in its name.. + // otherwise go with keyboard input. + InputDevice* input_device = nullptr; + auto* touch_input = g_input->touch_input(); + auto* keyboard_input = g_input->keyboard_input(); + if (touch_input) { + input_device = touch_input; + } else if (keyboard_input) { + input_device = keyboard_input; + } + if ((g_graphics->screen_virtual_width() - x < menu_button_size_) + && (g_graphics->screen_virtual_height() - y < menu_button_size_)) { + g_game->PushMainMenuPressCall(input_device); + last_menu_button_press_time_ = GetRealTime(); + } + } +} + +void RootUI::HandleMouseMotion(float x, float y) { + // Menu button hover. + if (menu_button_pressed_) { + menu_button_hover_ = + ((g_graphics->screen_virtual_width() - x < menu_button_size_) + && (g_graphics->screen_virtual_height() - y < menu_button_size_)); + } +} + +void RootUI::Draw(FrameDef* frame_def) { + if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { + millisecs_t real_time = frame_def->real_time(); + + // Menu button. + // Update time-dependent stuff to this point. + bool active = !(g_ui && g_ui->screen_root_widget() + && g_ui->screen_root_widget()->HasChildren()); + if (real_time - menu_update_time_ > 500) { + menu_update_time_ = real_time - 500; + } + while (menu_update_time_ < real_time) { + menu_update_time_ += 10; + if (!active && (real_time - last_menu_button_press_time_ > 100)) { + menu_fade_ = std::max(0.0f, menu_fade_ - 0.05f); + } else { + menu_fade_ = std::min(1.0f, menu_fade_ + 0.05f); + } + } + + // Don't draw menu button on certain UIs such as TV or VR. + bool draw_menu_button = true; + + if (g_buildconfig.ostype_android()) { + // Draw if we have a touchscreen or are in desktop mode. + if (g_input->touch_input() == nullptr + && !g_platform->IsRunningOnDesktop()) { + draw_menu_button = false; + } + } else if (g_buildconfig.rift_build()) { + if (IsVRMode()) { + draw_menu_button = false; + } + } + + if (draw_menu_button) { + SimpleComponent c(frame_def->overlay_pass()); + c.SetTransparent(true); + c.SetTexture(g_media->GetTexture(SystemTextureID::kMenuButton)); + + // Draw menu button. + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + if ((menu_button_pressed_ && menu_button_hover_) + || real_time - last_menu_button_press_time_ < 100) { + c.SetColor(1, 2, 0.5f, 1); + } else { + c.SetColor(0.3f, 0.3f + 0.2f * menu_fade_, 0.2f, menu_fade_); + } + c.PushTransform(); + c.Translate(width - menu_button_size_ * 0.5f, + height - menu_button_size_ * 0.38f, kMenuButtonDrawDepth); + c.Scale(menu_button_size_ * 0.8f, menu_button_size_ * 0.8f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + } + + // To the left of the menu button, draw our connected-players indicator + // (this probably shouldn't live here). + bool draw_connected_players_icon = false; + int party_size = g_game->GetPartySize(); + bool is_host = (g_game->connection_to_host() == nullptr); + millisecs_t last_connection_to_client_join_time = + g_game->last_connection_to_client_join_time(); + + bool show_client_joined = + (is_host && last_connection_to_client_join_time != 0 + && real_time - last_connection_to_client_join_time < 5000); + + if (!party_window_open_ + && (party_size != 0 || g_game->connection_to_host() + || always_draw_party_icon_)) { + draw_connected_players_icon = true; + } + + if (draw_connected_players_icon) { + // Flash and show a message if we're in the main menu instructing the + // player to start a game. + bool flash = false; + HostSession* s = g_game->GetForegroundContext().GetHostSession(); + if (s && s->is_main_menu() && party_size > 0 && show_client_joined) + flash = true; + + SimpleComponent c(frame_def->overlay_pass()); + c.SetTransparent(true); + c.SetTexture(g_media->GetTexture(SystemTextureID::kUsersButton)); + + // Draw button. + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + c.PushTransform(); + float extra_offset = + (draw_menu_button && menu_fade_ > 0.0f) ? -menu_button_size_ : 0.0f; + { + float smoothing = 0.8f; + connected_client_extra_offset_smoothed_ = + smoothing * connected_client_extra_offset_smoothed_ + + (1.0f - smoothing) * extra_offset; + } + c.Translate(width - menu_button_size_ * 0.4f + + connected_client_extra_offset_smoothed_, + height - menu_button_size_ * 0.35f, kMenuButtonDrawDepth); + c.Scale(menu_button_size_ * 0.8f, menu_button_size_ * 0.8f); + if (flash && frame_def->base_time() % 250 < 125) { + c.SetColor(1.0f, 1.4f, 1.0f); + } + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + + // Based on who has menu control, we may show a key/button below the party + // icon. + if (!active) { + if (InputDevice* uiid = g_ui->GetUIInputDevice()) { + std::string party_button_name = uiid->GetPartyButtonName(); + if (!party_button_name.empty()) { + if (!party_button_text_group_.exists()) { + party_button_text_group_ = Object::New(); + } + if (party_button_name != party_button_text_group_->getText()) { + party_button_text_group_->SetText(party_button_name, + TextMesh::HAlign::kCenter, + TextMesh::VAlign::kTop); + } + int text_elem_count = party_button_text_group_->GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c.SetTexture(party_button_text_group_->GetElementTexture(e)); + c.SetMaskUV2Texture( + party_button_text_group_->GetElementMaskUV2Texture(e)); + c.SetShadow( + -0.003f * party_button_text_group_->GetElementUScale(e), + -0.003f * party_button_text_group_->GetElementVScale(e), 0.0f, + 1.0f); + c.SetFlatness(1.0f); + c.SetColor(0.8f, 1, 0.8f, 0.9f); + c.PushTransform(); + c.Translate(width - menu_button_size_ * 0.42f + + connected_client_extra_offset_smoothed_, + height - menu_button_size_ * 0.77f, + kMenuButtonDrawDepth); + c.Scale(menu_button_size_ * 0.015f, menu_button_size_ * 0.015f); + c.DrawMesh(party_button_text_group_->GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + } + + { + // Update party count text if party size has changed. + if (party_size_text_group_num_ != party_size) { + party_size_text_group_num_ = party_size; + if (!party_size_text_group_.exists()) { + party_size_text_group_ = Object::New(); + } + party_size_text_group_->SetText( + std::to_string(party_size_text_group_num_)); + + // ..we also may want to update our 'someone joined' message if we're + // host + if (is_host) { + if (!start_a_game_text_group_.exists()) { + start_a_game_text_group_ = Object::New(); + } + if (party_size == 2) { // (includes us as host) + start_a_game_text_group_->SetText( + g_game->GetResourceString("joinedPartyInstructionsText"), + TextMesh::HAlign::kRight, TextMesh::VAlign::kTop); + } else if (party_size > 2) { + start_a_game_text_group_->SetText( + std::to_string(party_size - 1) + + " friends have joined your party.\nGo to 'Play' to start " + "a game.", + TextMesh::HAlign::kRight, TextMesh::VAlign::kTop); + } + } + } + + // Draw party member count. + int text_elem_count = party_size_text_group_->GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c.SetTexture(party_size_text_group_->GetElementTexture(e)); + c.SetMaskUV2Texture( + party_size_text_group_->GetElementMaskUV2Texture(e)); + c.SetShadow(-0.003f * party_size_text_group_->GetElementUScale(e), + -0.003f * party_size_text_group_->GetElementVScale(e), + 0.0f, 1.0f); + c.SetFlatness(1.0f); + if (flash && frame_def->base_time() % 250 < 125) { + c.SetColor(1, 1, 0); + } else { + if (party_size > 0) { + c.SetColor(0.2f, 1.0f, 0.2f); + } else { + c.SetColor(0.5f, 0.65f, 0.5f); + } + } + c.PushTransform(); + c.Translate(width - menu_button_size_ * 0.49f + + connected_client_extra_offset_smoothed_, + height - menu_button_size_ * 0.6f, kMenuButtonDrawDepth); + c.Scale(menu_button_size_ * 0.01f, menu_button_size_ * 0.01f); + c.DrawMesh(party_size_text_group_->GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + + // Draw 'someone joined' text if applicable. + if (is_host) { + if (flash) { + float blend = 0.8f; + start_a_game_text_scale_ = + blend * start_a_game_text_scale_ + (1.0f - blend) * 1.0f; + } else { + float blend = 0.8f; + start_a_game_text_scale_ = + blend * start_a_game_text_scale_ + (1.0f - blend) * 0.0f; + } + + if (start_a_game_text_scale_ > 0.001f) { + // 'start a game' notice + int text_elem_count = start_a_game_text_group_->GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c.SetTexture(start_a_game_text_group_->GetElementTexture(e)); + c.SetMaskUV2Texture( + start_a_game_text_group_->GetElementMaskUV2Texture(e)); + c.SetShadow(-0.003f * start_a_game_text_group_->GetElementUScale(e), + -0.003f * start_a_game_text_group_->GetElementVScale(e), + 0.0f, 1.0f); + c.SetFlatness(1.0f); + if (flash && frame_def->base_time() % 250 < 125) { + c.SetColor(1, 1, 0); + } else { + c.SetColor(0, 1, 0); + } + c.PushTransform(); + c.Translate(width - 10, height - menu_button_size_ * 0.7f, -0.07f); + c.Scale(start_a_game_text_scale_, start_a_game_text_scale_); + c.DrawMesh(start_a_game_text_group_->GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/ui/root_ui.h b/src/ballistica/ui/root_ui.h new file mode 100644 index 00000000..77f81137 --- /dev/null +++ b/src/ballistica/ui/root_ui.h @@ -0,0 +1,49 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_ROOT_UI_H_ +#define BALLISTICA_UI_ROOT_UI_H_ + +#include "ballistica/graphics/frame_def.h" + +namespace ballistica { + +/// Manages root level UI such as the menu button, party button, etc. +/// This is set to be replaced by RootWidget. +class RootUI { + public: + RootUI(); + virtual ~RootUI(); + void Draw(FrameDef* frame_def); + + auto HandleMouseButtonDown(float x, float y) -> bool; + void HandleMouseButtonUp(float x, float y); + void HandleMouseMotion(float x, float y); + void set_party_window_open(bool val) { party_window_open_ = val; } + auto party_window_open() const -> bool { return party_window_open_; } + void set_always_draw_party_icon(bool val) { always_draw_party_icon_ = val; } + auto always_draw_party_icon() const -> bool { + return always_draw_party_icon_; + } + void TogglePartyWindowKeyPress(); + void ActivatePartyIcon() const; + + private: + millisecs_t last_menu_button_press_time_{}; + millisecs_t menu_update_time_{}; + bool menu_button_pressed_{}; + float menu_button_size_{}; + bool menu_button_hover_{}; + float menu_fade_{}; + bool party_window_open_{}; + bool always_draw_party_icon_{}; + float connected_client_extra_offset_smoothed_{}; + Object::Ref party_button_text_group_; + Object::Ref party_size_text_group_; + int party_size_text_group_num_{-1}; + Object::Ref start_a_game_text_group_; + float start_a_game_text_scale_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_ROOT_UI_H_ diff --git a/src/ballistica/ui/ui.cc b/src/ballistica/ui/ui.cc new file mode 100644 index 00000000..3a9b0727 --- /dev/null +++ b/src/ballistica/ui/ui.cc @@ -0,0 +1,454 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/ui.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/generic/lambda_runnable.h" +#include "ballistica/graphics/component/empty_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/input/input.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/scene.h" +#include "ballistica/ui/root_ui.h" +#include "ballistica/ui/widget/button_widget.h" +#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/ui/widget/stack_widget.h" + +namespace ballistica { + +static const int kUIOwnerTimeoutSeconds = 30; + +UI::UI() { + // Figure out our interface type. + assert(g_platform); + + // Allow overriding via an environment variable. + auto* ui_override = getenv("BA_FORCE_UI_SCALE"); + bool force_test_small{}; + bool force_test_medium{}; + bool force_test_large{}; + if (ui_override) { + if (ui_override == std::string("small")) { + force_test_small = true; + } else if (ui_override == std::string("medium")) { + force_test_medium = true; + } else if (ui_override == std::string("large")) { + force_test_large = true; + } + } + if (force_test_small) { + g_app_globals->ui_scale = UIScale::kSmall; + } else if (force_test_medium) { + g_app_globals->ui_scale = UIScale::kMedium; + } else if (force_test_large) { + g_app_globals->ui_scale = UIScale::kLarge; + } else { + // Use automatic val. + if (g_buildconfig.iircade_build()) { + g_app_globals->ui_scale = UIScale::kMedium; + } else if (IsVRMode() || g_platform->IsRunningOnTV()) { + // VR and tv builds always use medium. + g_app_globals->ui_scale = UIScale::kMedium; + } else { + g_app_globals->ui_scale = g_platform->GetInterfaceType(); + } + } + + // Make sure we know when forced-ui-scale is enabled. + if (force_test_small) { + ScreenMessage("FORCING SMALL UI FOR TESTING", Vector3f(1, 0, 0)); + Log("FORCING SMALL UI FOR TESTING"); + } else if (force_test_medium) { + ScreenMessage("FORCING MEDIUM UI FOR TESTING", Vector3f(1, 0, 0)); + Log("FORCING MEDIUM UI FOR TESTING"); + } else if (force_test_large) { + ScreenMessage("FORCING LARGE UI FOR TESTING", Vector3f(1, 0, 0)); + Log("FORCING LARGE UI FOR TESTING"); + } + + step_scene_timer_ = + base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, + NewLambdaRunnable([this] { StepScene(); })); + scene_ = Object::New(0); + root_ui_ = new RootUI(); +} + +// Currently the UI never dies so we don't bother doing a clean tear-down.. +// (verifying scene cleanup, etc) +UI::~UI() { + assert(root_ui_); + delete root_ui_; +} + +auto UI::IsWindowPresent() const -> bool { + return ((screen_root_widget_.exists() && screen_root_widget_->HasChildren()) + || (overlay_root_widget_.exists() + && overlay_root_widget_->HasChildren())); +} + +void UI::SetUIInputDevice(InputDevice* input_device) { + assert(InGameThread()); + + ui_input_device_ = input_device; + + // So they dont get stolen from immediately. + last_input_device_use_time_ = GetRealTime(); +} + +UI::UILock::UILock(bool write) { + assert(g_ui); + assert(InGameThread()); + + if (write && g_ui->ui_lock_count_ != 0) { + BA_LOG_ERROR_TRACE_ONCE("Illegal operation: UI is locked"); + } + g_ui->ui_lock_count_++; +} + +UI::UILock::~UILock() { + g_ui->ui_lock_count_--; + if (g_ui->ui_lock_count_ < 0) { + BA_LOG_ERROR_TRACE_ONCE("ui_lock_count_ < 0"); + g_ui->ui_lock_count_ = 0; + } +} + +void UI::StepScene() { + auto s = scene(); + sim_timers_.Run(s->time()); + s->Step(); +} + +void UI::Update(millisecs_t time_advance) { + assert(InGameThread()); + + millisecs_t target_base_time = base_time_ + time_advance; + while (!base_timers_.empty() + && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) + <= target_base_time)) { + base_time_ += base_timers_.GetTimeToNextExpire(base_time_); + base_timers_.Run(base_time_); + } + base_time_ = target_base_time; + + // Periodically prune various dead refs. + if (base_time_ > next_prune_time_) { + PruneDeadMapRefs(&textures_); + PruneDeadMapRefs(&sounds_); + PruneDeadMapRefs(&models_); + next_prune_time_ = base_time_ + 4920; + + // Since we never clear our scene, we need to watch for leaks. + // If there's more than a few nodes in existence for an extended period of + // time, complain. + if (scene_->nodes().size() > 10) { + node_warning_count_++; + if (node_warning_count_ > 3) { + static bool complained = false; + if (!complained) { + Log(">10 nodes in UI context!"); + complained = true; + } + } + } else { + node_warning_count_ = 0; + } + } +} + +void UI::Reset() { + // Hmm; technically we don't need to recreate these each time we reset. + root_widget_.Clear(); + + // Kill our screen-root widget. + screen_root_widget_.Clear(); + + // (Re)create our screen-root widget. + auto sw(Object::New()); + sw->set_is_main_window_stack(true); + sw->SetWidth(g_graphics->screen_virtual_width()); + sw->SetHeight(g_graphics->screen_virtual_height()); + sw->set_translate(0, 0); + screen_root_widget_ = sw; + + // (Re)create our screen-overlay widget. + auto ow(Object::New()); + ow->set_is_overlay_window_stack(true); + ow->SetWidth(g_graphics->screen_virtual_width()); + ow->SetHeight(g_graphics->screen_virtual_height()); + ow->set_translate(0, 0); + overlay_root_widget_ = ow; + + // (Re)create our abs-root widget. + auto rw(Object::New()); + root_widget_ = rw; + rw->SetWidth(g_graphics->screen_virtual_width()); + rw->SetHeight(g_graphics->screen_virtual_height()); + rw->SetScreenWidget(sw.get()); + rw->Setup(); + rw->SetOverlayWidget(ow.get()); + + sw->GlobalSelect(); +} + +auto UI::ShouldHighlightWidgets() const -> bool { + // Show selection highlights only if we've got controllers connected and only + // when the main UI is visible (dont want a selection highlight for toolbar + // buttons during a game). + return ( + g_input->have_non_touch_inputs() + && ((screen_root_widget_.exists() && screen_root_widget_->HasChildren()) + || (overlay_root_widget_.exists() + && overlay_root_widget_->HasChildren()))); +} + +auto UI::ShouldShowButtonShortcuts() const -> bool { + return g_input->have_non_touch_inputs(); +} + +void UI::AddWidget(Widget* w, ContainerWidget* parent) { + assert(InGameThread()); + + BA_PRECONDITION(parent != nullptr); + + // If they're adding an initial window/dialog to our screen-stack, + // send a reset-local-input message so that characters who have lost focus + // will not get stuck running or whatnot. + if (screen_root_widget_.exists() && !screen_root_widget_->HasChildren() + && parent == &(*screen_root_widget_)) { + g_game->ResetInput(); + } + + parent->AddWidget(w); +} + +auto UI::SendWidgetMessage(const WidgetMessage& m) -> int { + if (!root_widget_.exists()) { + // Log("SendWidgetMessage() called before root widget created"); + return false; + } + return root_widget_->HandleMessage(m); +} + +void UI::DeleteWidget(Widget* widget) { + assert(widget); + if (widget) { + ContainerWidget* parent = widget->parent_widget(); + if (parent) { + parent->DeleteWidget(widget); + } + } +} + +void UI::ScreenSizeChanged() { + if (root_widget_.exists()) { + root_widget_->SetWidth(g_graphics->screen_virtual_width()); + root_widget_->SetHeight(g_graphics->screen_virtual_height()); + } +} + +auto UI::GetUIInputDevice() const -> InputDevice* { + assert(InGameThread()); + return ui_input_device_.get(); +} + +auto UI::GetWidgetForInput(InputDevice* input_device) -> Widget* { + assert(input_device); + assert(InGameThread()); + + // We only allow input-devices to control the UI when there's a window/dialog + // on the screen (even though our top/bottom bars still exist). + if ((!screen_root_widget_.exists() || (!screen_root_widget_->HasChildren())) + && (!overlay_root_widget_.exists() + || (!overlay_root_widget_->HasChildren()))) { + return nullptr; + } + + millisecs_t time = GetRealTime(); + + bool print_menu_owner = false; + Widget* ret_val; + + // Ok here's the deal: + // Because having 10 controllers attached to the UI is pure chaos, + // we only allow one input device at a time to control the menu. + // However, if no events are received by that device for a long time, + // it is up for grabs to the next device that requests it. + + if ((GetUIInputDevice() == nullptr) || (input_device == GetUIInputDevice()) + || (time - last_input_device_use_time_ > (1000 * kUIOwnerTimeoutSeconds)) + || !g_input->HaveManyLocalActiveInputDevices()) { + // Don't actually assign yet; only update times and owners if there's a + // widget to be had (we don't want some guy who moved his character 3 + // seconds ago to automatically own a newly created widget). + last_input_device_use_time_ = time; + ui_input_device_ = input_device; + ret_val = screen_root_widget_.get(); + } else { + // For rejected input devices, play error sounds sometimes so they know + // they're not the chosen one. + if (time - last_widget_input_reject_err_sound_time_ > 5000) { + last_widget_input_reject_err_sound_time_ = time; + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kErrorBeep)); + print_menu_owner = true; + } + ret_val = nullptr; // Rejected! + } + + if (print_menu_owner) { + InputDevice* input = GetUIInputDevice(); + + if (input) { + millisecs_t timeout = + kUIOwnerTimeoutSeconds - (time - last_input_device_use_time_) / 1000; + std::string time_out_str; + if (timeout > 0 && timeout < (kUIOwnerTimeoutSeconds - 10)) { + time_out_str = " " + g_game->GetResourceString("timeOutText"); + Utils::StringReplaceOne(&time_out_str, "${TIME}", + std::to_string(timeout)); + } else { + time_out_str = " " + g_game->GetResourceString("willTimeOutText"); + } + + std::string name; + if (input->GetDeviceName() == "Keyboard") { + name = g_game->GetResourceString("keyboardText"); + } else if (input->GetDeviceName() == "TouchScreen") { + name = g_game->GetResourceString("touchScreenText"); + } else { + // We used to use player names here, but that's kinda sloppy and random; + // lets just go with device names/numbers. + auto devicesWithName = + g_input->GetInputDevicesWithName(input->GetDeviceName()); + if (devicesWithName.size() == 1) { + // If there's just one, no need to tack on the '#2' or whatever. + name = input->GetDeviceName(); + } else { + name = + input->GetDeviceName() + " " + input->GetPersistentIdentifier(); + } + } + + std::string b = g_game->GetResourceString("hasMenuControlText"); + Utils::StringReplaceOne(&b, "${NAME}", name); + ScreenMessage(b + time_out_str, {0.45f, 0.4f, 0.5f}); + } + } + return ret_val; +} + +auto UI::GetModel(const std::string& name) -> Object::Ref { + return Media::GetMedia(&models_, name, scene()); +} + +auto UI::GetTexture(const std::string& name) -> Object::Ref { + return Media::GetMedia(&textures_, name, scene()); +} + +auto UI::GetSound(const std::string& name) -> Object::Ref { + return Media::GetMedia(&sounds_, name, scene()); +} + +auto UI::GetData(const std::string& name) -> Object::Ref { + return Media::GetMedia(&datas_, name, scene()); +} + +auto UI::GetAsUIContext() -> UI* { return this; } + +auto UI::GetMutableScene() -> Scene* { + Scene* sg = scene_.get(); + assert(sg); + return sg; +} + +auto UI::NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int { + // All of our stuff is just real-time; lets just map all timer options to + // that. + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + case TimeType::kReal: + return g_game->NewRealTimer(length, repeat, runnable); + default: + // Fall back to default for descriptive error otherwise. + return ContextTarget::NewTimer(timetype, length, repeat, runnable); + } +} + +void UI::DeleteTimer(TimeType timetype, int timer_id) { + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + case TimeType::kReal: + g_game->DeleteRealTimer(timer_id); + break; + default: + // Fall back to default for descriptive error otherwise. + ContextTarget::DeleteTimer(timetype, timer_id); + break; + } +} + +auto UI::Draw(FrameDef* frame_def) -> void { + RenderPass* overlay_flat_pass = frame_def->GetOverlayFlatPass(); + + // Draw interface elements. + auto* root_widget = root_widget_.get(); + + if (root_widget && root_widget->HasChildren()) { + // Draw our opaque and transparent parts separately. + // This way we can draw front-to-back for opaque and back-to-front for + // transparent. + + g_graphics->set_drawing_opaque_only(true); + + // Do a wee bit of shifting based on tilt just for fun. + Vector3f tilt = 0.1f * g_graphics->tilt(); + { + EmptyComponent c(overlay_flat_pass); + c.SetTransparent(false); + c.PushTransform(); + c.Translate(-tilt.y, tilt.x, -0.5f); + + // We want our widgets to cover 0.1f in z space. + c.Scale(1.0f, 1.0f, 0.1f); + c.Submit(); + root_widget->Draw(overlay_flat_pass, false); + c.PopTransform(); + c.Submit(); + } + + g_graphics->set_drawing_opaque_only(false); + g_graphics->set_drawing_transparent_only(true); + + { + EmptyComponent c(overlay_flat_pass); + c.SetTransparent(true); + c.PushTransform(); + c.Translate(-tilt.y, tilt.x, -0.5f); + + // We want our widgets to cover 0.1f in z space. + c.Scale(1.0f, 1.0f, 0.1f); + c.Submit(); + root_widget->Draw(overlay_flat_pass, true); + c.PopTransform(); + c.Submit(); + } + + g_graphics->set_drawing_transparent_only(false); + } + if (root_ui_) { + root_ui_->Draw(frame_def); + } +} + +void UI::DrawExtras(FrameDef* frame_def) { root_ui_->Draw(frame_def); } + +} // namespace ballistica diff --git a/src/ballistica/ui/ui.h b/src/ballistica/ui/ui.h new file mode 100644 index 00000000..ac37e0f9 --- /dev/null +++ b/src/ballistica/ui/ui.h @@ -0,0 +1,149 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_UI_H_ +#define BALLISTICA_UI_UI_H_ + +#include +#include + +#include "ballistica/core/context.h" +#include "ballistica/generic/timer_list.h" +#include "ballistica/ui/widget/widget.h" + +// UI-Locks: make sure widget-lists don't change under you. +// Use a read-lock if you just need to ensure lists remain intact but won't be +// changing anything. Use a write-lock whenever modifying a list. +#if BA_DEBUG_BUILD +#define BA_DEBUG_UI_READ_LOCK UI::UILock ui_lock(false) +#define BA_DEBUG_UI_WRITE_LOCK UI::UILock ui_lock(true) +#else +#define BA_DEBUG_UI_READ_LOCK +#define BA_DEBUG_UI_WRITE_LOCK +#endif +#define BA_UI_READ_LOCK UI::UILock ui_lock(false) +#define BA_UI_WRITE_LOCK UI::UILock ui_lock(true) + +namespace ballistica { + +// All this stuff must be called from the game module. +class UI : public ContextTarget { + public: + UI(); + ~UI() override; + void Reset(); + + // Return the root widget containing all windows & dialogs + // Whenever this contains children, the UI is considered to be in focus + auto screen_root_widget() -> ContainerWidget* { + return screen_root_widget_.get(); + } + + auto overlay_root_widget() -> ContainerWidget* { + return overlay_root_widget_.get(); + } + + // Returns true if there is UI present in either the main or overlay + // stacks. Generally this implies the focus should be on the UI. + auto IsWindowPresent() const -> bool; + + // Return the absolute root widget; this includes persistent UI + // bits such as the top/bottom bars + auto root_widget() -> RootWidget* { return root_widget_.get(); } + + auto Draw(FrameDef* frame_def) -> void; + + // Returns the widget an input should send commands to, if any. + // Also potentially locks other inputs out of controlling the UI, + // so only call this if you intend on sending a message to that widget. + auto GetWidgetForInput(InputDevice* input_device) -> Widget*; + + // Add a widget to a container. + // If a parent is provided, the widget is added to it; otherwise it is added + // to the root widget. + void AddWidget(Widget* w, ContainerWidget* to); + + // Send message to the active widget. + auto SendWidgetMessage(const WidgetMessage& msg) -> int; + + // Use this to destroy any named widget (even those in containers). + void DeleteWidget(Widget* widget); + + void ScreenSizeChanged(); + + void SetUIInputDevice(InputDevice* input_device); + + // Returns the input-device that currently owns the menu; otherwise nullptr. + auto GetUIInputDevice() const -> InputDevice*; + + // Returns whether currently selected widgets should flash. + // This will be false in some situations such as when only touch screen + // control is active. + auto ShouldHighlightWidgets() const -> bool; + + // Same except for button shortcuts; these generally only get shown + // if a joystick of some form is present. + auto ShouldShowButtonShortcuts() const -> bool; + + void DrawExtras(FrameDef* frame_def); + + // Used to ensure widgets are not created or destroyed at certain times + // (while traversing widget hierarchy, etc). + class UILock { + public: + explicit UILock(bool write); + ~UILock(); + + private: + BA_DISALLOW_CLASS_COPIES(UILock); + }; + + auto GetSound(const std::string& name) -> Object::Ref override; + auto GetData(const std::string& name) -> Object::Ref override; + auto GetModel(const std::string& name) -> Object::Ref override; + auto GetTexture(const std::string& name) -> Object::Ref override; + auto GetAsUIContext() -> UI* override; + auto scene() -> Scene* { + assert(scene_.exists()); + return scene_.get(); + } + void Update(millisecs_t time_advance); + auto GetMutableScene() -> Scene* override; + + // Context-target timer support. + auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int override; + void DeleteTimer(TimeType timetype, int timer_id) override; + + RootUI* root_ui() const { + assert(root_ui_); + return root_ui_; + } + + private: + void StepScene(); + RootUI* root_ui_{}; + millisecs_t next_prune_time_{}; + int node_warning_count_{}; + Timer* step_scene_timer_{}; + millisecs_t base_time_{}; + TimerList sim_timers_; + TimerList base_timers_; + Object::Ref scene_; + Object::WeakRef ui_input_device_; + millisecs_t last_input_device_use_time_{}; + millisecs_t last_widget_input_reject_err_sound_time_{}; + Object::Ref screen_root_widget_; + Object::Ref overlay_root_widget_; + Object::Ref root_widget_; + int ui_lock_count_{}; + + // Media loaded in the UI context. + std::map > textures_; + std::map > sounds_; + std::map > datas_; + std::map > models_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_UI_H_ diff --git a/src/ballistica/ui/widget/button_widget.cc b/src/ballistica/ui/widget/button_widget.cc new file mode 100644 index 00000000..981ecc71 --- /dev/null +++ b/src/ballistica/ui/widget/button_widget.cc @@ -0,0 +1,594 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/button_widget.h" + +#include +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/generic/real_timer.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/component/empty_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/input/input.h" +#include "ballistica/media/component/model.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/ui/ui.h" + +namespace ballistica { + +ButtonWidget::ButtonWidget() { + text_ = Object::New(); + SetText("Button"); + text_->set_valign(TextWidget::VAlign::kCenter); + text_->set_halign(TextWidget::HAlign::kCenter); + text_->SetWidth(0.0f); + text_->SetHeight(0.0f); + birth_time_ = g_game->master_time(); +} + +ButtonWidget::~ButtonWidget() = default; + +void ButtonWidget::SetTextResScale(float val) { text_->set_res_scale(val); } + +void ButtonWidget::set_on_activate_call(PyObject* call_obj) { + on_activate_call_ = Object::New(call_obj); +} + +void ButtonWidget::SetText(const std::string& text_in) { + std::string text = Utils::GetValidUTF8(text_in.c_str(), "bwst"); + text_->SetText(text); + + // Also cache our current text width; don't want to calc this with each draw + // (especially now that we may have to ask the OS to do it). + text_width_dirty_ = true; +} + +void ButtonWidget::SetTexture(Texture* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("texture is not from the UI context: " + ObjToString(val)); + } + texture_ = val; +} + +void ButtonWidget::SetMaskTexture(Texture* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("texture is not from the UI context: " + ObjToString(val)); + } + mask_texture_ = val; +} + +void ButtonWidget::SetTintTexture(Texture* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("texture is not from the UI context: " + ObjToString(val)); + } + tint_texture_ = val; +} + +void ButtonWidget::SetIcon(Texture* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("icon texture is not from the UI context: " + + val->GetObjectDescription()); + } + icon_ = val; +} + +void ButtonWidget::HandleRealTimerExpired(RealTimer* t) { + // Repeat our action unless we somehow lost focus but didn't get a mouse-up. + if (IsHierarchySelected() && pressed_) { + DoActivate(true); + + // Speed up repeats after the first. + t->SetLength(150); + } else { + repeat_timer_.Clear(); + } +} + +void ButtonWidget::SetModelOpaque(Model* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("model_opaque is not from the UI context"); + } + model_opaque_ = val; +} + +void ButtonWidget::SetModelTransparent(Model* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("model_transparent is not from the UI context"); + } + model_transparent_ = val; +} + +auto ButtonWidget::GetWidth() -> float { return width_; } +auto ButtonWidget::GetHeight() -> float { return height_; } + +auto ButtonWidget::GetMult(millisecs_t current_time) const -> float { + float mult = 1.0f; + if ((pressed_ && mouse_over_) || (current_time - last_activate_time_ < 200)) { + if (pressed_ && mouse_over_) { + mult = 3.0f; + } else { + float x = static_cast(current_time - last_activate_time_) / 200.0f; + mult = 1.0f + 3.0f * (1.0f - x * x); + } + } else if ((IsHierarchySelected() && g_ui->ShouldHighlightWidgets())) { + mult = + 0.8f + + std::abs(sinf(static_cast(current_time) * 0.006467f)) * 0.2f; + + if (!texture_.exists()) { + mult *= 1.7f; + } else { + // Let's make custom textures pulsate brighter since they can be dark/etc. + mult *= 2.0f; + } + } else { + if (!texture_.exists()) { + } else { + // In desktop mode we want image buttons to light up when we + // mouse over them. + if (!g_platform->IsRunningOnDesktop()) { + if (mouse_over_) { + mult = 1.4f; + } + } + } + } + return mult; +} + +auto ButtonWidget::GetDrawBrightness(millisecs_t time) const -> float { + return GetMult(time); +} + +void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { + millisecs_t current_time = pass->frame_def()->base_time(); + + Vector3f tilt = 0.02f * g_graphics->tilt(); + float extra_offs_x = -tilt.y; + float extra_offs_y = tilt.x; + + assert(g_input); + bool show_icons = false; + + InputDevice* device = g_ui->GetUIInputDevice(); + + // If there's an explicit user-set icon we always show. + if (icon_.exists()) { + show_icons = true; + } + + bool ouya_icons = false; + bool remote_icons = false; + + // Phasing out ouya stuff. + if (explicit_bool(false)) { + ouya_icons = true; + } + if (icon_type_ == IconType::kCancel && device != nullptr + && device->IsRemoteControl()) { + remote_icons = true; + } + + // Simple transition. + float transition = (birth_time_ + transition_delay_) - current_time; + if (transition > 0) { + extra_offs_x -= transition * 4.0f; + } + + if (text_width_dirty_) { + text_width_ = text_->GetTextWidth(); + text_width_dirty_ = false; + } + + float string_scale = text_scale_; + + bool string_too_small_to_draw = false; + + // We should only need this in our transparent pass. + float string_width; + if (draw_transparent) { + string_width = std::max(0.0001f, text_width_); + + // Account for our icon if we have it. + float s_width_available = std::max(30.0f, width_ - 30); + if (show_icons) s_width_available -= (34.0f * icon_scale_); + + if ((string_width * string_scale) > s_width_available) { + float squish_scale = s_width_available / (string_width * string_scale); + if (squish_scale < 0.2f) string_too_small_to_draw = true; + string_scale *= squish_scale; + } + } else { + string_width = 0.0f; // Shouldn't be used. + } + + float mult = GetMult(current_time); + + { + float l = 0; + float r = l + width_; + float b = 0; + float t = b + height_; + + // Use these to pick styles so style doesnt + // change during mouse-over, etc. + float l_orig = l; + float r_orig = r; + float b_orig = b; + float t_orig = t; + + // For normal buttons we draw both transparent and opaque. + // With custom ones we only draw what we're given. + Object::Ref custom_model; + bool do_draw_model; + + // Normal buttons draw in both transparent and opaque passes. + if (!texture_.exists()) { + do_draw_model = true; + } else { + // If we're supplying any custom models, draw whichever is provided. + if (model_opaque_.exists() || model_transparent_.exists()) { + if (draw_transparent && model_transparent_.exists()) { + do_draw_model = true; + custom_model = model_transparent_; + } else if ((!draw_transparent) && model_opaque_.exists()) { + do_draw_model = true; + custom_model = model_opaque_; + } else { + do_draw_model = false; // Skip this pass. + } + } else { + // With no custom models we just draw a plain square in the + // transparent pass. + do_draw_model = draw_transparent; + } + } + + if (do_draw_model) { + SimpleComponent c(pass); + c.SetTransparent(draw_transparent); + + // We currently only support non-1.0 opacity values when using + // custom textures and no custom opaque model. + assert(opacity_ == 1.0f + || (texture_.exists() && !model_opaque_.exists())); + + c.SetColor(mult * color_red_, mult * color_green_, mult * color_blue_, + opacity_); + + float l_border, r_border, b_border, t_border; + + bool doDraw = true; + + ModelData* model; + + // Custom button texture. + if (texture_.exists()) { + if (!custom_model.exists()) { + model = g_media->GetModel(SystemModelID::kImage1x1); + } else { + model = custom_model->model_data(); + } + if (texture_->texture_data()->loaded() && model->loaded() + && (!mask_texture_.exists() + || mask_texture_->texture_data()->loaded()) + && (!tint_texture_.exists() + || tint_texture_->texture_data()->loaded())) { + c.SetTexture(texture_); + if (tint_texture_.exists()) { + c.SetColorizeTexture(tint_texture_); + c.SetColorizeColor(tint_color_red_, tint_color_green_, + tint_color_blue_); + c.SetColorizeColor2(tint2_color_red_, tint2_color_green_, + tint2_color_blue_); + } + c.SetMaskTexture(mask_texture_); + } else { + doDraw = false; + } + l_border = r_border = 0.04f * width_; + b_border = t_border = 0.04f * height_; + } else { + // Standard button texture. + SystemModelID model_id; + SystemTextureID tex_id; + + switch (style_) { + case Style::kBack: { + tex_id = SystemTextureID::kUIAtlas; + model_id = draw_transparent ? SystemModelID::kButtonBackTransparent + : SystemModelID::kButtonBackOpaque; + l_border = 10; + r_border = 6; + b_border = 6; + t_border = -1; + break; + } + case Style::kBackSmall: { + tex_id = SystemTextureID::kUIAtlas; + model_id = draw_transparent + ? SystemModelID::kButtonBackSmallTransparent + : SystemModelID::kButtonBackSmallOpaque; + l_border = 10; + r_border = 14; + b_border = 9; + t_border = 5; + break; + } + case Style::kTab: { + tex_id = SystemTextureID::kUIAtlas2; + model_id = draw_transparent ? SystemModelID::kButtonTabTransparent + : SystemModelID::kButtonTabOpaque; + l_border = 6; + r_border = 10; + b_border = 5; + t_border = 2; + break; + } + case Style::kSquare: { + tex_id = SystemTextureID::kButtonSquare; + model_id = draw_transparent + ? SystemModelID::kButtonSquareTransparent + : SystemModelID::kButtonSquareOpaque; + l_border = 6; + r_border = 9; + b_border = 6; + t_border = 6; + break; + } + default: { + if ((r_orig - l_orig) / (t_orig - b_orig) < 50.0f / 30.0f) { + tex_id = SystemTextureID::kUIAtlas; + model_id = draw_transparent + ? SystemModelID::kButtonSmallTransparent + : SystemModelID::kButtonSmallOpaque; + l_border = 10; + r_border = 14; + b_border = 9; + t_border = 5; + } else if ((r_orig - l_orig) / (t_orig - b_orig) < 200.0f / 35.0f) { + tex_id = SystemTextureID::kUIAtlas; + model_id = draw_transparent + ? SystemModelID::kButtonMediumTransparent + : SystemModelID::kButtonMediumOpaque; + l_border = 6; + r_border = 10; + b_border = 5; + t_border = 2; + } else if ((r_orig - l_orig) / (t_orig - b_orig) < 300.0f / 35.0f) { + tex_id = SystemTextureID::kUIAtlas; + model_id = draw_transparent + ? SystemModelID::kButtonLargeTransparent + : SystemModelID::kButtonLargeOpaque; + l_border = 7; + r_border = 10; + b_border = 10; + t_border = 5; + } else { + tex_id = SystemTextureID::kUIAtlas; + model_id = draw_transparent + ? SystemModelID::kButtonLargerTransparent + : SystemModelID::kButtonLargerOpaque; + l_border = 7; + r_border = 11; + b_border = 10; + t_border = 4; + } + break; + } + } + c.SetTexture(g_media->GetTexture(tex_id)); + model = g_media->GetModel(model_id); + } + if (doDraw) { + c.PushTransform(); + c.Translate((l - l_border + r + r_border) * 0.5f + extra_offs_x, + (b - b_border + t + t_border) * 0.5f + extra_offs_y, 0); + c.Scale(r - l + l_border + r_border, t - b + b_border + t_border, 1.0f); + c.DrawModel(model); + c.PopTransform(); + } + + // Draw icon. + if ((show_icons) && draw_transparent) { + bool doDrawIcon = true; + if (icon_type_ == IconType::kStart) { + c.SetColor(1.4f * mult * (color_red_), 1.4f * mult * (color_green_), + 1.4f * mult * (color_blue_), 1.0f); + c.SetTexture(g_media->GetTexture(SystemTextureID::kStartButton)); + } else if (icon_type_ == IconType::kCancel) { + if (remote_icons) { + c.SetColor(1.0f * mult * (1.0f), 1.0f * mult * (1.0f), + 1.0f * mult * (1.0f), 1.0f); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBackIcon)); + } else if (ouya_icons) { + c.SetColor(1.0f * mult * (1.0f), 1.0f * mult * (1.0f), + 1.0f * mult * (1.0f), 1.0f); + c.SetTexture(g_media->GetTexture(SystemTextureID::kOuyaAButton)); + } else { + c.SetColor(1.5f * mult * (color_red_), 1.5f * mult * (color_green_), + 1.5f * mult * (color_blue_), 1.0f); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBombButton)); + } + } else if (icon_.exists()) { + c.SetColor(icon_color_red_ + * (icon_tint_ * (1.7f * mult * (color_red_)) + + (1.0f - icon_tint_) * mult), + icon_color_green_ + * (icon_tint_ * (1.7f * mult * (color_green_)) + + (1.0f - icon_tint_) * mult), + icon_color_blue_ + * (icon_tint_ * (1.7f * mult * (color_blue_)) + + (1.0f - icon_tint_) * mult), + icon_color_alpha_); + if (!icon_->texture_data()->loaded()) { + doDrawIcon = false; + } else { + c.SetTexture(icon_); + } + } else { + c.SetColor(1, 1, 1); + c.SetTexture(g_media->GetTexture(SystemTextureID::kCircle)); + } + if (doDrawIcon) { + c.PushTransform(); + c.Translate((l + r) * 0.5f + extra_offs_x + - (string_width * string_scale) * 0.5f - 5.0f, + (b + t) * 0.5f + extra_offs_y, 0.001f); + c.Scale(34.0f * icon_scale_, 34.f * icon_scale_, 1.0f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + } + } + c.Submit(); + } + } + + // Draw our text at z depth 0.5-1. + if (!string_too_small_to_draw) { + EmptyComponent c(pass); + c.SetTransparent(draw_transparent); + c.PushTransform(); + c.Translate(1.0f * extra_offs_x, 1.0f * extra_offs_y, 0.5f); + c.Scale(1, 1, 0.5f); + c.Translate(width_ * 0.5f, height_ * 0.5f); + + // Shift over for our icon if we have it. + if (show_icons) { + c.Translate(17.0f * icon_scale_, 0, 0); + } + if (string_scale != 1.0f) { + c.Scale(string_scale, string_scale); + } + c.Submit(); + + text_->set_color(mult * text_color_r_, mult * text_color_g_, + mult * text_color_b_, text_color_a_); + text_->set_flatness(text_flatness_); + text_->Draw(pass, draw_transparent); + c.PopTransform(); + c.Submit(); + } +} + +auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { + // How far outside button touches register. + float left_overlap, top_overlap, right_overlap, bottom_overlap; + if (g_platform->IsRunningOnDesktop()) { + left_overlap = 3.0f; + top_overlap = 1.0f; + right_overlap = 0.0f; + bottom_overlap = 0.0f; + } else { + left_overlap = 3.0f + 9.0f * extra_touch_border_scale_; + top_overlap = 1.0f + 5.0f * extra_touch_border_scale_; + right_overlap = 7.0f * extra_touch_border_scale_; + bottom_overlap = 7.0f * extra_touch_border_scale_; + } + + switch (m.type) { + case WidgetMessage::Type::kMouseMove: { + float x = m.fval1; + float y = m.fval2; + bool claimed = (m.fval3 > 0.0f); + if (claimed || !enabled_) { + mouse_over_ = false; + } else { + mouse_over_ = + ((x >= (-left_overlap)) && (x < (width_ + right_overlap)) + && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))); + } + + return mouse_over_; + } + case WidgetMessage::Type::kMouseDown: { + float x = m.fval1; + float y = m.fval2; + if (enabled_ && (x >= (-left_overlap)) && (x < (width_ + right_overlap)) + && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))) { + mouse_over_ = true; + pressed_ = true; + + if (repeat_) { + repeat_timer_ = Object::New>(300, true, this); + // If we're a repeat button we trigger immediately. + // (waiting till mouse up sort of defeats the purpose here) + Activate(); + } + if (selectable_) { + GlobalSelect(); + } + return true; + } else { + return false; + } + } + case WidgetMessage::Type::kMouseUp: { + float x = m.fval1; + float y = m.fval2; + bool claimed = (m.fval3 > 0.0f); + if (pressed_) { + pressed_ = false; + + // Stop any repeats. + repeat_timer_.Clear(); + + // For non-repeat buttons, non-claimed mouse-ups within the + // button region trigger the action. + if (!repeat_) { + if (enabled_ && (x >= (0 - left_overlap)) + && (x < (0 + width_ + right_overlap)) + && (y >= (0 - bottom_overlap)) + && (y < (0 + height_ + top_overlap)) && !claimed) { + Activate(); + } + } + return true; // Pressed buttons always claim mouse-ups. + } + break; + } + default: + break; + } + return false; +} + +void ButtonWidget::Activate() { DoActivate(); } + +void ButtonWidget::DoActivate(bool isRepeat) { + if (!enabled_) { + Log("WARNING: ButtonWidget::DoActivate() called on disabled button"); + return; + } + + // We dont want holding down a repeat-button to keep flashing it. + if (!isRepeat) { + last_activate_time_ = g_game->master_time(); + } + if (sound_enabled_) { + int r = rand() % 3; // NOLINT + if (r == 0) { + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish)); + } else if (r == 1) { + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish2)); + } else { + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish3)); + } + } + if (on_activate_call_.exists()) { + // Call this in the next cycle (don't wanna risk mucking with UI from + // within a UI loop. + g_game->PushPythonWeakCall( + Object::WeakRef(on_activate_call_)); + return; + } +} + +void ButtonWidget::OnLanguageChange() { + text_->OnLanguageChange(); + text_width_dirty_ = true; +} + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/button_widget.h b/src/ballistica/ui/widget/button_widget.h new file mode 100644 index 00000000..ba170ee7 --- /dev/null +++ b/src/ballistica/ui/widget/button_widget.h @@ -0,0 +1,146 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_BUTTON_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_BUTTON_WIDGET_H_ + +#include + +#include "ballistica/ui/widget/text_widget.h" + +namespace ballistica { + +class ButtonWidget : public Widget { + public: + ButtonWidget(); + ~ButtonWidget() override; + void Draw(RenderPass* pass, bool transparent) override; + auto HandleMessage(const WidgetMessage& m) -> bool override; + void set_width(float width) { width_ = width; } + void set_height(float height) { height_ = height; } + auto GetWidth() -> float override; + auto GetHeight() -> float override; + void SetColor(float r, float g, float b) { + color_set_ = true; + color_red_ = r; + color_green_ = g; + color_blue_ = b; + } + void set_tint_color(float r, float g, float b) { + tint_color_red_ = r; + tint_color_green_ = g; + tint_color_blue_ = b; + } + void set_tint2_color(float r, float g, float b) { + tint2_color_red_ = r; + tint2_color_green_ = g; + tint2_color_blue_ = b; + } + void set_text_color(float r, float g, float b, float a) { + text_color_r_ = r; + text_color_g_ = g; + text_color_b_ = b; + text_color_a_ = a; + } + void set_icon_color(float r, float g, float b, float a) { + icon_color_red_ = r; + icon_color_green_ = g; + icon_color_blue_ = b; + icon_color_alpha_ = a; + } + void set_text_flatness(float f) { text_flatness_ = f; } + enum class Style { kRegular, kBack, kBackSmall, kTab, kSquare }; + void set_style(Style s) { style_ = s; } + enum class IconType { kNone, kCancel, kStart }; + void SetText(const std::string& text); + auto text() const -> std::string { return text_->text_raw(); } + void set_icon_type(IconType i) { icon_type_ = i; } + void set_repeat(bool repeat) { repeat_ = repeat; } + void set_text_scale(float val) { text_scale_ = val; } + void SetTexture(Texture* t); + void SetMaskTexture(Texture* t); + void SetTintTexture(Texture* val); + void SetIcon(Texture* t); + auto icon() const -> Texture* { return icon_.get(); } + void set_on_activate_call(PyObject* call_obj); + void Activate() override; + auto IsSelectable() -> bool override { return selectable_; } + auto GetWidgetTypeName() -> std::string override { return "button"; } + void set_enable_sound(bool enable) { sound_enabled_ = enable; } + void SetModelTransparent(Model* val); + void SetModelOpaque(Model* val); + void set_transition_delay(float val) { transition_delay_ = val; } + void HandleRealTimerExpired(RealTimer* t); + void set_extra_touch_border_scale(float scale) { + extra_touch_border_scale_ = scale; + } + void set_selectable(bool s) { selectable_ = s; } + void set_icon_scale(float s) { icon_scale_ = s; } + void set_icon_tint(float tint) { icon_tint_ = tint; } + void SetTextResScale(float val); + + // Disabled buttons can't be clicked or otherwise activated. + void set_enabled(bool val) { enabled_ = val; } + auto enabled() const -> bool { return enabled_; } + void set_opacity(float val) { opacity_ = val; } + auto GetDrawBrightness(millisecs_t time) const -> float override; + auto is_color_set() const -> bool { return color_set_; } + void OnLanguageChange() override; + + private: + bool text_width_dirty_ = true; + bool color_set_ = false; + void DoActivate(bool isRepeat = false); + auto GetMult(millisecs_t current_time) const -> float; + IconType icon_type_ = IconType::kNone; + bool enabled_ = true; + bool selectable_ = true; + float icon_tint_ = 0.0f; + Style style_ = Style::kRegular; + bool sound_enabled_ = true; + bool mouse_over_ = false; + bool repeat_ = false; + bool pressed_ = false; + float extra_touch_border_scale_ = 1.0f; + float width_ = 50.0f; + float height_ = 30.0f; + float text_scale_ = 1.0f; + float text_width_ = 0.0f; + float color_red_ = 0.5f; + float color_green_ = 0.7f; + float color_blue_ = 0.2f; + float icon_color_red_ = 1.0f; + float icon_color_green_ = 1.0f; + float icon_color_blue_ = 1.0f; + float icon_color_alpha_ = 1.0f; + Object::Ref texture_; + Object::Ref icon_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref model_transparent_; + Object::Ref model_opaque_; + float icon_scale_ = 1.0f; + millisecs_t last_activate_time_ = 0; + millisecs_t birth_time_ = 0; + float transition_delay_ = 0.0f; + float opacity_ = 1.0f; + float text_flatness_ = 0.5f; + float text_color_r_ = 0.75f; + float text_color_g_ = 1.0f; + float text_color_b_ = 0.7f; + float text_color_a_ = 1.0f; + float tint_color_red_ = 1.0f; + float tint_color_green_ = 1.0f; + float tint_color_blue_ = 1.0f; + float tint2_color_red_ = 1.0f; + float tint2_color_green_ = 1.0f; + float tint2_color_blue_ = 1.0f; + + // Keep these at the bottom so they're torn down first. + Object::Ref text_; + Object::Ref on_activate_call_; + Object::Ref > repeat_timer_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_WIDGET_BUTTON_WIDGET_H_ diff --git a/src/ballistica/ui/widget/check_box_widget.cc b/src/ballistica/ui/widget/check_box_widget.cc new file mode 100644 index 00000000..988ef58d --- /dev/null +++ b/src/ballistica/ui/widget/check_box_widget.cc @@ -0,0 +1,325 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/check_box_widget.h" + +#include "ballistica/audio/audio.h" +#include "ballistica/game/game.h" +#include "ballistica/graphics/component/empty_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/python/python_sys.h" +#include "ballistica/ui/ui.h" + +namespace ballistica { + +CheckBoxWidget::CheckBoxWidget() { + SetText("CheckBox"); + text_.set_owner_widget(this); + text_.set_valign(TextWidget::VAlign::kCenter); + text_.set_halign(TextWidget::HAlign::kLeft); +} + +CheckBoxWidget::~CheckBoxWidget() = default; + +void CheckBoxWidget::SetOnValueChangeCall(PyObject* call_tuple) { + on_value_change_call_ = Object::New(call_tuple); +} + +void CheckBoxWidget::SetText(const std::string& text) { + text_.SetText(text); + have_text_ = (!text.empty()); +} + +void CheckBoxWidget::SetWidth(float width_in) { + highlight_dirty_ = box_dirty_ = check_dirty_ = true; + width_ = width_in; + text_.SetWidth(width_in - (2 * box_padding_ + box_size_ + 4)); +} + +void CheckBoxWidget::SetHeight(float height_in) { + highlight_dirty_ = box_dirty_ = check_dirty_ = true; + height_ = height_in; + text_.SetHeight(height_in); +} + +void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { + millisecs_t real_time = GetRealTime(); + + have_drawn_ = true; + float l = 0.0f; + float r = l + width_; + float b = 0.0f; + float t = b + height_; + + Vector3f tilt = 0.01f * g_graphics->tilt(); + if (draw_control_parent()) { + tilt += 0.02f * g_graphics->tilt(); + } + float extra_offs_x = -tilt.y; + float extra_offs_y = tilt.x; + + if (have_text_ && draw_transparent + && ((selected() && g_ui->ShouldHighlightWidgets()) + || (pressed_ && mouse_over_))) { + // Draw glow (at depth 0.9f). + float m; + if (pressed_ && mouse_over_) { + m = 2.0f; + } else if (IsHierarchySelected()) { + m = 0.5f + + std::abs(sinf(static_cast(real_time) * 0.006467f) * 0.4f); + } else { + m = 0.25f; + } + + if (highlight_dirty_) { + float l_border, r_border, b_border, t_border; + l_border = 10.0f; + r_border = 0.0f; + b_border = 11.0f; + t_border = 11.0f; + highlight_width_ = r - l + l_border + r_border; + highlight_height_ = t - b + b_border + t_border; + highlight_center_x_ = l - l_border + highlight_width_ * 0.5f; + highlight_center_y_ = b - b_border + highlight_height_ * 0.5f; + highlight_dirty_ = false; + } + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.SetColor(0.25f * m, 0.3f * m, 0, 0.3f * m); + c.SetTexture(g_media->GetTexture(SystemTextureID::kGlow)); + c.PushTransform(); + c.Translate(highlight_center_x_, highlight_center_y_); + c.Scale(highlight_width_, highlight_height_); + c.DrawModel(g_media->GetModel(SystemModelID::kImage4x1)); + c.PopTransform(); + c.Submit(); + } + + float glow_amt = 1.0f; + + { + float box_l = l + box_padding_; + float box_r = box_l + box_size_; + float box_b = b + (t - b) / 2 - box_size_ / 2; + float box_t = box_b + box_size_; + + if (pressed_ && mouse_over_) { + glow_amt = 2.0f; + } else if (IsHierarchySelected() && g_ui->ShouldHighlightWidgets()) { + glow_amt = + 0.8f + + std::abs(sinf(static_cast(real_time) * 0.006467f) * 0.3f); + } + + // Button portion (depth 0.1f-0.5f). + { + if (box_dirty_) { + float l_border, r_border, b_border, t_border; + l_border = 8; + r_border = 12; + b_border = 6; + t_border = 6; + box_width_ = box_r - box_l + l_border + r_border; + box_height_ = box_t - box_b + b_border + t_border; + box_center_x_ = box_l - l_border + box_width_ * 0.5f; + box_center_y_ = box_b - b_border + box_height_ * 0.5f; + box_dirty_ = false; + } + + SimpleComponent c(pass); + c.SetTransparent(draw_transparent); + c.SetColor(glow_amt * color_r_, glow_amt * color_g_, glow_amt * color_b_, + 1); + c.SetTexture(g_media->GetTexture(SystemTextureID::kUIAtlas)); + c.PushTransform(); + c.Translate(box_center_x_ + extra_offs_x, box_center_y_ + extra_offs_y, + 0.1f); + c.Scale(box_width_, box_height_, 0.4f); + c.DrawModel(g_media->GetModel(draw_transparent + ? SystemModelID::kButtonSmallTransparent + : SystemModelID::kButtonSmallOpaque)); + c.PopTransform(); + c.Submit(); + } + + // Check portion. + if (draw_transparent) { + if (check_dirty_) { + float s = 1; + if (real_time - last_change_time_ < 100) { + s = static_cast(real_time - last_change_time_) / 100; + } + if (!checked_) s = 1.0f - s; + + float check_offset_h = -2; + float check_offset_v = -2; + + check_width_ = 45 * s; + check_height_ = 45 * s; + check_center_x_ = + box_l + 11 - 18 * s + check_offset_h + check_width_ * 0.5f; + check_center_y_ = + box_b + 10 - 18 * s + check_offset_v + check_height_ * 0.5f; + + // Only set clean once our transition is over. + if (real_time - last_change_time_ > 100) check_dirty_ = false; + } + + // Draw check in z depth from 0.5f to 1. + SimpleComponent c(pass); + c.SetTransparent(draw_transparent); + if (is_radio_button_) { + c.SetTexture(g_media->GetTexture(SystemTextureID::kNub)); + } else { + c.SetTexture(g_media->GetTexture(SystemTextureID::kUIAtlas)); + } + + if (mouse_over_ && g_platform->IsRunningOnDesktop()) { + c.SetColor(1.0f * glow_amt, 0.7f * glow_amt, 0, 1); + } else { + c.SetColor(1.0f * glow_amt, 0.6f * glow_amt, 0, 1); + } + c.PushTransform(); + + if (is_radio_button_) { + c.Translate(check_center_x_ + 1 + 3.0f * extra_offs_x, + check_center_y_ + 2 + 3.0f * extra_offs_y, 0.5f); + c.Scale(check_width_ * 0.45f, check_height_ * 0.45f, 0.5f); + c.Translate(-0.17f, -0.17f, 0.5f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + } else { + c.Translate(check_center_x_ + 3.0f * extra_offs_x, + check_center_y_ + 3.0f * extra_offs_y, 0.5f); + c.Scale(check_width_, check_height_, 0.5f); + c.DrawModel(g_media->GetModel(SystemModelID::kCheckTransparent)); + } + c.PopTransform(); + c.Submit(); + } + } + + // Draw our text in z depth 0.5f to 1. + EmptyComponent c(pass); + c.SetTransparent(draw_transparent); + c.PushTransform(); + c.Translate(2 * box_padding_ + box_size_ + 10, 0, 0.5f); + c.Scale(1, 1, 0.5f); + c.Submit(); + float cs = glow_amt; + text_.set_color(cs * text_color_r_, cs * text_color_g_, cs * text_color_b_, + text_color_a_); + text_.Draw(pass, draw_transparent); + c.PopTransform(); + c.Submit(); +} + +// for our center we return something near center of the checkbox; not our text +void CheckBoxWidget::GetCenter(float* x, float* y) { + *x = tx() + scale() * GetWidth() * 0.2f; + *y = ty() + scale() * GetHeight() * 0.5f; +} + +void CheckBoxWidget::SetValue(bool value) { + if (value == checked_) { + return; + } + check_dirty_ = true; + + // Don't animate if we're setting initial values. + if (checked_ != value && have_drawn_) { + last_change_time_ = GetRealTime(); + } + checked_ = value; +} + +void CheckBoxWidget::Activate() { + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish3)); + checked_ = !checked_; + check_dirty_ = true; + last_change_time_ = GetRealTime(); + if (on_value_change_call_.exists()) { + PythonRef args(Py_BuildValue("(O)", checked_ ? Py_True : Py_False), + PythonRef::kSteal); + + // Call this in the next cycle (don't wanna risk mucking with UI from within + // a UI loop) + g_game->PushPythonWeakCallArgs( + Object::WeakRef(on_value_change_call_), args); + } +} + +auto CheckBoxWidget::HandleMessage(const WidgetMessage& m) -> bool { + // How far outside button touches register. + float left_overlap, top_overlap, right_overlap, bottom_overlap; + if (g_platform->IsRunningOnDesktop()) { + left_overlap = 3.0f; + top_overlap = 1.0f; + right_overlap = 0.0f; + bottom_overlap = 0.0f; + } else { + left_overlap = 12.0f; + top_overlap = 10.0f; + right_overlap = 13.0f; + bottom_overlap = 15.0f; + } + + switch (m.type) { + case WidgetMessage::Type::kMouseMove: { + float x = m.fval1; + float y = m.fval2; + bool claimed = (m.fval3 > 0.0f); + if (claimed) + mouse_over_ = false; + else + mouse_over_ = + ((x >= (-left_overlap)) && (x < (width_ + right_overlap)) + && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))); + return mouse_over_; + } + case WidgetMessage::Type::kMouseDown: { + float x = m.fval1; + float y = m.fval2; + if ((x >= (-left_overlap)) && (x < (width_ + right_overlap)) + && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))) { + GlobalSelect(); + pressed_ = true; + return true; + } else { + return false; + } + } + case WidgetMessage::Type::kMouseUp: { + float x = m.fval1; + float y = m.fval2; + bool claimed = (m.fval3 > 0.0f); + + // Radio-style boxes can't be un-checked. + if (pressed_) { + pressed_ = false; + + // if they're still over us and unclaimed, toggle. + if ((x >= (-left_overlap)) && (x < (width_ + right_overlap)) + && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap)) + && !claimed) { + // Radio-style buttons don't allow unchecking. + if (!is_radio_button_ || !checked_) { + Activate(); + } + } + return true; // If we're pressed, claim any mouse-ups presented to us. + } + break; + } + default: + break; + } + return false; +} + +void CheckBoxWidget::OnLanguageChange() { text_.OnLanguageChange(); } + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/check_box_widget.h b/src/ballistica/ui/widget/check_box_widget.h new file mode 100644 index 00000000..581ff724 --- /dev/null +++ b/src/ballistica/ui/widget/check_box_widget.h @@ -0,0 +1,92 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_CHECK_BOX_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_CHECK_BOX_WIDGET_H_ + +#include + +#include "ballistica/graphics/renderer.h" +#include "ballistica/ui/widget/text_widget.h" + +namespace ballistica { + +// Check box interface widget. +class CheckBoxWidget : public Widget { + public: + CheckBoxWidget(); + ~CheckBoxWidget() override; + void Draw(RenderPass* pass, bool transparent) override; + void SetWidth(float widthIn); + void SetHeight(float heightIn); + auto GetWidth() -> float override { return width_; } + auto GetHeight() -> float override { return height_; } + void SetText(const std::string& text); + void SetValue(bool value); + void SetMaxWidth(float w) { text_.set_max_width(w); } + void SetTextScale(float val) { text_.set_center_scale(val); } + void set_text_color(float r, float g, float b, float a) { + text_color_r_ = r; + text_color_g_ = g; + text_color_b_ = b; + text_color_a_ = a; + } + void set_color(float r, float g, float b) { + color_r_ = r; + color_g_ = g; + color_b_ = b; + } + auto HandleMessage(const WidgetMessage& m) -> bool override; + void Activate() override; + auto IsSelectable() -> bool override { return true; } + auto GetWidgetTypeName() -> std::string override { return "checkbox"; } + void SetOnValueChangeCall(PyObject* call_tuple); + void SetIsRadioButton(bool enabled) { is_radio_button_ = enabled; } + void GetCenter(float* x, float* y) override; + void OnLanguageChange() override; + + private: + bool have_text_{true}; + float text_color_r_{0.75f}; + float text_color_g_{1.0f}; + float text_color_b_{0.7f}; + float text_color_a_{1.0f}; + float color_r_{0.4f}; + float color_g_{0.6f}; + float color_b_{0.2f}; + ImageMesh box_image_mesh_; + float check_width_{}; + float check_height_{}; + float check_center_x_{}; + float check_center_y_{}; + float box_width_{}; + float box_height_{}; + float box_center_x_{}; + float box_center_y_{}; + float highlight_width_{}; + float highlight_height_{}; + float highlight_center_x_{}; + float highlight_center_y_{}; + bool highlight_dirty_{true}; + bool box_dirty_{true}; + bool check_dirty_{true}; + bool click_select_{}; + bool mouse_over_{}; + bool checked_{true}; + bool have_drawn_{}; + millisecs_t last_change_time_{}; + float box_size_{20.0f}; + float box_padding_{6.0f}; + float width_{400.0f}; + float height_{24.0f}; + TextWidget text_; + std::string command_; + bool pressed_{}; + bool is_radio_button_{}; + + // Keep these at the bottom so they're torn down first. + Object::Ref on_value_change_call_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_WIDGET_CHECK_BOX_WIDGET_H_ diff --git a/src/ballistica/ui/widget/column_widget.cc b/src/ballistica/ui/widget/column_widget.cc new file mode 100644 index 00000000..43488a12 --- /dev/null +++ b/src/ballistica/ui/widget/column_widget.cc @@ -0,0 +1,61 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/column_widget.h" + +#include "ballistica/ui/ui.h" + +namespace ballistica { + +ColumnWidget::ColumnWidget() { + set_background(false); // Influences default event handling; ew. + set_claims_left_right(false); + set_claims_tab(false); + set_draggable(false); + set_selection_loops(false); +} + +ColumnWidget::~ColumnWidget() = default; + +auto ColumnWidget::HandleMessage(const WidgetMessage& m) -> bool { + switch (m.type) { + case WidgetMessage::Type::kShow: { + // Told to show something.. send this along to our parent (we can't do + // anything). + Widget* w = parent_widget(); + if (w) { + w->HandleMessage(m); + } + return true; + } + default: + break; + } + return ContainerWidget::HandleMessage(m); +} + +void ColumnWidget::UpdateLayout() { + BA_DEBUG_UI_READ_LOCK; + + float total_height{2.0f * margin_}; + for (const auto& i : widgets()) { + float wh = (*i).GetHeight() * (*i).scale(); + total_height += 2.0f * border_ + wh + top_border_ + bottom_border_; + } + float b{total_height - margin_}; + float l{border_ + left_border_ + margin_}; + for (auto&& i : widgets()) { + float w_scale = (*i).scale(); + float wh = (*i).GetHeight() * w_scale; + b -= border_; + b -= top_border_; + b -= wh; + (*i).set_translate(l, b); + b -= bottom_border_; + b -= border_; + } + if (height() != total_height) { + set_height(total_height); + } +} + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/column_widget.h b/src/ballistica/ui/widget/column_widget.h new file mode 100644 index 00000000..91d2e432 --- /dev/null +++ b/src/ballistica/ui/widget/column_widget.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_COLUMN_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_COLUMN_WIDGET_H_ + +#include + +#include "ballistica/ui/widget/container_widget.h" + +namespace ballistica { + +// Widget that arranges its children in a column. +class ColumnWidget : public ContainerWidget { + public: + ColumnWidget(); + ~ColumnWidget() override; + auto HandleMessage(const WidgetMessage& m) -> bool override; + auto GetWidgetTypeName() -> std::string override { return "column"; } + + auto set_left_border(float val) { left_border_ = val; } + auto left_border() const { return left_border_; } + auto set_top_border(float val) { top_border_ = val; } + auto top_border() const { return top_border_; } + auto set_bottom_border(float val) { bottom_border_ = val; } + auto bottom_border() const { return bottom_border_; } + auto set_border(float val) { border_ = val; } + auto border() const { return border_; } + auto set_margin(float val) { margin_ = val; } + auto margin() const { return margin_; } + + protected: + void UpdateLayout() override; + float border_{}; + float margin_{10.0f}; + float left_border_{}; + float top_border_{}; + float bottom_border_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_WIDGET_COLUMN_WIDGET_H_ diff --git a/src/ballistica/ui/widget/container_widget.cc b/src/ballistica/ui/widget/container_widget.cc new file mode 100644 index 00000000..bf1ba4c7 --- /dev/null +++ b/src/ballistica/ui/widget/container_widget.cc @@ -0,0 +1,1965 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/container_widget.h" + +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/game/game.h" +#include "ballistica/graphics/component/empty_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/input/input.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/button_widget.h" +#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/ui/widget/stack_widget.h" +#include "ballistica/ui/widget/text_widget.h" + +namespace ballistica { + +// Set this to -100 or so to make sure culling is active +// (things should visibly pop in & out of existence in that case). +#define SIMPLE_CULLING_V_OFFSET 0 +#define SIMPLE_CULLING_H_OFFSET 0 + +#define AUTO_SELECT_SLOPE_CLAMP 4.0f +#define AUTO_SELECT_MIN_SLOPE 0.1f +#define AUTO_SELECT_SLOPE_OFFSET 1.0f +#define AUTO_SELECT_SLOPE_WEIGHT 0.5f + +#define TRANSITION_DURATION 120 + +ContainerWidget::ContainerWidget(float width_in, float height_in) + : width_(width_in), + height_(height_in), + dynamics_update_time_(g_game->master_time()) {} + +ContainerWidget::~ContainerWidget() { + BA_DEBUG_UI_READ_LOCK; + // Wipe out our children. + widgets_.clear(); +} + +void ContainerWidget::SetOnActivateCall(PyObject* c) { + on_activate_call_ = Object::New(c); +} + +void ContainerWidget::SetOnOutsideClickCall(PyObject* c) { + on_outside_click_call_ = Object::New(c); +} + +void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, + float x_offset, float y_offset, + float scale) { + BA_DEBUG_UI_READ_LOCK; + + // We're expected to fill z space 0..1 when we draw... so we need to divide + // that space between our child widgets plus our bg layer. + float layer_thickness = 0.0f; + float layer_spacing = 0.0f; + float base_offset = 0.0f; + float layer_thickness1 = 0.0f; + float layer_thickness2 = 0.0f; + float layer_thickness3 = 0.0f; + float layer_spacing1 = 0.0f; + float layer_spacing2 = 0.0f; + float layer_spacing3 = 0.0f; + float base_offset1 = 0.0f; + float base_offset2 = 0.0f; + float base_offset3 = 0.0f; + + // In single-depth mode we draw all widgets at the same depth so they each get + // our full depth resolution. however they may overlap incorrectly. + if (background_) { + assert(!single_depth_root_); + if (single_depth_) { + // Reserve a sliver of 0.2 for our backing geometry. + layer_thickness = 0.8f; + base_offset = 0.2f; + layer_spacing = 0.0f; + } else { + layer_thickness = 1.0f / static_cast(widgets_.size() + 1); + layer_spacing = layer_thickness; + base_offset = layer_thickness; + } + } else { + if (single_depth_) { + // Single-depth-root is a special mode for our root container + // where the first child (the screen stack) gets most of the depth range, + // the last child (the overlay stack) gets a bit of the rest, and the + // remainder is shared between root widget children (toolbars, etc). + if (single_depth_root_) { + layer_thickness1 = 0.9f; + base_offset1 = 0.0f; + layer_spacing1 = 0.0f; + layer_thickness2 = 0.05f; + base_offset2 = 0.9f; + layer_spacing2 = 0.0f; + layer_thickness3 = 0.05f; + base_offset3 = 0.95f; + layer_spacing3 = 0.0f; + } else { + layer_thickness = 1.0f; + base_offset = 0.0f; + layer_spacing = 0.0f; + } + } else { + layer_thickness = 1.0f / (widgets_.size()); + layer_spacing = layer_thickness; + base_offset = 0; + } + } + + size_t w_count = widgets_.size(); + bool doing_culling_v = false; + bool doing_culling_h = false; + Widget* pw = parent_widget(); + float cull_top = 0.0f; + float cull_bottom = 0.0f; + float cull_left = 0.0f; + float cull_right = 0.0f; + float cull_offset_v = 0.0f; + float cull_offset_h = 0.0f; + + // FIXME: need to test/update this to support scaling. + if (pw && pw->simple_culling_v() >= 0.0f) { + doing_culling_v = true; + cull_top = pw->simple_culling_top() - ty(); + cull_bottom = pw->simple_culling_bottom() - ty(); + cull_offset_v = pw->simple_culling_v(); + } + if (pw && pw->simple_culling_h() >= 0.0f) { + doing_culling_h = true; + cull_right = pw->simple_culling_right() - tx(); + cull_left = pw->simple_culling_left() - tx(); + cull_offset_h = pw->simple_culling_h(); + } + + // In opaque mode, draw our child widgets immediately front-to-back to best + // make use of the z buffer. + if (draw_transparent) { + EmptyComponent c(pass); + c.SetTransparent(true); + + for (size_t i = 0; i < w_count; i++) { + if (single_depth_root_) { + if (i == 0) { + layer_thickness = layer_thickness1; + base_offset = base_offset1; + layer_spacing = layer_spacing1; + } else if (i == w_count - 1) { + layer_thickness = layer_thickness3; + base_offset = base_offset3; + layer_spacing = layer_spacing3; + } else { + layer_thickness = layer_thickness2; + base_offset = base_offset2; + layer_spacing = layer_spacing2; + } + } + + Widget& w(*widgets_[i]); + + if (!w.visible_in_container()) { + continue; + } + + float tx = w.tx(); + float ty = w.ty(); + float s = w.scale(); + + // Some bare-bones culling to keep large scroll areas responsive. + if (doing_culling_v) { + if ((y_offset + ty > cull_top + cull_offset_v + SIMPLE_CULLING_V_OFFSET) + || (y_offset + ty + s * w.GetHeight() + < cull_bottom - cull_offset_v - SIMPLE_CULLING_V_OFFSET)) { + continue; + } + } + if (doing_culling_h) { + if ((x_offset + tx + > cull_right + cull_offset_h + SIMPLE_CULLING_H_OFFSET) + || (x_offset + tx + s * w.GetWidth() + < cull_left - cull_offset_h - SIMPLE_CULLING_H_OFFSET)) { + continue; + } + } + c.PushTransform(); + float z_offs = base_offset + i * layer_spacing; + if (transition_scale_ != 1.0f) { + c.Translate(bg_center_x_, bg_center_y_, 0); + c.Scale(transition_scale_, transition_scale_, 1.0f); + c.Translate(-bg_center_x_, -bg_center_y_, 0); + } + + // Widgets can opt to use a subset of their allotted depth slice. + float d_min = w.depth_range_min(); + float d_max = w.depth_range_max(); + if (d_min != 0.0f || d_max != 1.0f) { + z_offs += layer_thickness * d_min; + layer_thickness *= (d_max - d_min); + } + c.Translate(x_offset + tx, y_offset + ty, z_offs); + c.Scale(s, s, layer_thickness); + c.Submit(); + w.Draw(pass, draw_transparent); + c.PopTransform(); + c.Submit(); + } + c.Submit(); + + } else { + EmptyComponent c(&(*pass)); + c.SetTransparent(false); + + for (int i = static_cast(w_count - 1); i >= 0; i--) { + if (single_depth_root_) { + if (i == 0) { + layer_thickness = layer_thickness1; + base_offset = base_offset1; + layer_spacing = layer_spacing1; + } else if (i == w_count - 1) { + layer_thickness = layer_thickness3; + base_offset = base_offset3; + layer_spacing = layer_spacing3; + } else { + layer_thickness = layer_thickness2; + base_offset = base_offset2; + layer_spacing = layer_spacing2; + } + } + + Widget& w(*widgets_[i]); + + if (!w.visible_in_container()) { + continue; + } + + float tx = w.tx(); + float ty = w.ty(); + float s = w.scale(); + + // Some bare-bones culling to keep large scroll areas responsive. + if (doing_culling_v) { + if ((y_offset + ty > cull_top + cull_offset_v + SIMPLE_CULLING_V_OFFSET) + || (y_offset + ty + s * w.GetHeight() + < cull_bottom - cull_offset_v - SIMPLE_CULLING_V_OFFSET)) { + continue; + } + } + if (doing_culling_h) { + if ((x_offset + tx + > cull_right + cull_offset_h + SIMPLE_CULLING_H_OFFSET) + || (x_offset + tx + s * w.GetWidth() + < cull_left - cull_offset_h - SIMPLE_CULLING_H_OFFSET)) { + continue; + } + } + + c.PushTransform(); + float z_offs = base_offset + static_cast(i) * layer_spacing; + if (transition_scale_ != 1.0f) { + c.Translate(bg_center_x_, bg_center_y_, 0); + c.Scale(transition_scale_, transition_scale_, 1.0f); + c.Translate(-bg_center_x_, -bg_center_y_, 0); + } + + // Widgets can opt to use a subset of their allotted depth slice. + float d_min = w.depth_range_min(); + float d_max = w.depth_range_max(); + if (d_min != 0.0f || d_max != 1.0f) { + z_offs += layer_thickness * d_min; + layer_thickness *= (d_max - d_min); + } + c.Translate(x_offset + tx, y_offset + ty, z_offs); + c.Scale(s, s, layer_thickness); + c.Submit(); + w.Draw(pass, draw_transparent); + c.PopTransform(); + c.Submit(); + } + c.Submit(); + } +} + +auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { + BA_DEBUG_UI_READ_LOCK; + + bool claimed = false; + if (ignore_input_) { + return claimed; + } + + switch (m.type) { + case WidgetMessage::Type::kTextInput: + case WidgetMessage::Type::kKey: + if (selected_widget_) { + bool val = selected_widget_->HandleMessage(m); + if (val != 0) { + return true; + } + } + break; + + // Ewww we dont want subclasses to do this + // but we need to ourself for standalone containers + // ...reaaaly need to make ba.container() a subclass. + case WidgetMessage::Type::kShow: { + // Told to show something.. send this along to our parent (we can't do + // anything). + Widget* w = parent_widget(); + if (w) { + w->HandleMessage(m); + } + return true; + break; + } + + case WidgetMessage::Type::kStart: { + if (selected_widget_) { + if (selected_widget_->HandleMessage(m)) { + claimed = true; + } + } + if (!claimed && start_button_.exists()) { + claimed = true; + start_button_->Activate(); + } + break; + } + + case WidgetMessage::Type::kCancel: { + if (selected_widget_) { + if (selected_widget_->HandleMessage(m)) { + claimed = true; + } + } + if (!claimed) { + if (cancel_button_.exists()) { + claimed = true; + cancel_button_->Activate(); + } else if (on_cancel_call_.exists()) { + claimed = true; + + // Call this in the next cycle (don't wanna risk mucking with UI from + // within a UI loop). + g_game->PushPythonWeakCall( + Object::WeakRef(on_cancel_call_)); + } else { + OnCancelCustom(); + } + } + break; + } + + case WidgetMessage::Type::kTabNext: + case WidgetMessage::Type::kMoveRight: + case WidgetMessage::Type::kMoveDown: { + if (m.type == WidgetMessage::Type::kTabNext && !claims_tab_) { + break; + } + if (m.type == WidgetMessage::Type::kMoveRight && !claims_left_right_) { + break; + } + if (m.type == WidgetMessage::Type::kMoveDown && !claims_up_down_) { + break; + } + if (selected_widget_) { + if (selected_widget_->HandleMessage(m)) { + claimed = true; + } + } + if (!claimed) { + if (!root_selectable_) { + if (m.type == WidgetMessage::Type::kMoveDown) { + SelectDownWidget(); + } else if (m.type == WidgetMessage::Type::kMoveRight) { + SelectRightWidget(); + } else { + SelectNextWidget(); + } + if (IsHierarchySelected()) { + ShowWidget(selected_widget()); + } + claimed = true; + } + } + break; + } + + case WidgetMessage::Type::kTabPrev: + case WidgetMessage::Type::kMoveLeft: + case WidgetMessage::Type::kMoveUp: { + if (m.type == WidgetMessage::Type::kTabPrev && !claims_tab_) { + break; + } + if (m.type == WidgetMessage::Type::kMoveLeft && !claims_left_right_) { + break; + } + if (m.type == WidgetMessage::Type::kMoveUp && !claims_up_down_) { + break; + } + if (selected_widget_) { + if (selected_widget_->HandleMessage(m)) { + claimed = true; + } + } + if (!claimed) { + if (!root_selectable_) { + if (m.type == WidgetMessage::Type::kMoveUp) { + SelectUpWidget(); + } else if (m.type == WidgetMessage::Type::kMoveLeft) { + SelectLeftWidget(); + } else { + SelectPrevWidget(); + } + if (IsHierarchySelected()) { + ShowWidget(selected_widget()); + } + claimed = true; + } + } + break; + } + + case WidgetMessage::Type::kActivate: { + if (root_selectable_) { + Activate(); + claimed = true; + } else { + if (selected_widget_) { + if (selected_widget_->HandleMessage(m)) { + claimed = true; + } + } + if (!claimed) { + if (selected_widget_) { + selected_widget_->Activate(); + } + claimed = true; + } + } + break; + } + + case WidgetMessage::Type::kMouseMove: { + CheckLayout(); + + // Ignore mouse stuff while transitioning out. + if (transitioning_ && transitioning_out_) break; + + float x = m.fval1; + float y = m.fval2; + float l = 0.0f; + float r = width_; + float b = 0.0f; + float t = height_; + + // If we're dragging, the drag claims all attention. + if (dragging_) { + bg_dirty_ = glow_dirty_ = true; + set_translate(tx() + (x - drag_x_) * scale(), + ty() + (y - drag_y_) * scale()); + break; + } + + if (!root_selectable_) { + // Go through all widgets backwards until one claims the cursor position + // (we still send it to other widgets even then though in case they + // case). + for (auto i = widgets_.rbegin(); i != widgets_.rend(); i++) { + float cx = x; + float cy = y; + TransformPointToChild(&cx, &cy, **i); + if ((**i).HandleMessage( + WidgetMessage(m.type, nullptr, cx, cy, claimed))) { + claimed = true; + } + if (modal_children_) { + break; + } + } + } + + // If its not yet claimed, see if its within our contained region, in + // which case we claim it (only for regular taps). + if (!claimed) { + if (background_ || root_selectable_) { + if (x >= l && x < r && y >= b && y < t) { + claimed = true; + mouse_over_ = true; + } else { + mouse_over_ = false; + } + } + } else { + mouse_over_ = false; + } + break; + } + + case WidgetMessage::Type::kMouseWheel: + case WidgetMessage::Type::kMouseWheelH: + case WidgetMessage::Type::kMouseWheelVelocity: + case WidgetMessage::Type::kMouseWheelVelocityH: { + CheckLayout(); + + // Ignore mouse stuff while transitioning. + if (transitioning_ && transitioning_out_) break; + + float x = m.fval1; + float y = m.fval2; + float amount = m.fval3; + float momentum = m.fval4; + + float l = 0; + float r = width_; + float b = 0; + float t = height_; + + // Go through all widgets backwards until one claims the wheel. + for (auto i = widgets_.rbegin(); i != widgets_.rend(); i++) { + float cx = x; + float cy = y; + TransformPointToChild(&cx, &cy, ((**i))); + if ((**i).HandleMessage( + WidgetMessage(m.type, nullptr, cx, cy, amount, momentum))) { + claimed = true; + break; + } + if (modal_children_) break; + } + + // If its not yet claimed, see if its within our contained region, in + // which case we claim it but do nothing. + if (!claimed) { + if (background_) { + if (x >= l && x < r && y >= b && y < t) { + claimed = true; + } + } + } + break; + } + case WidgetMessage::Type::kScrollMouseDown: + case WidgetMessage::Type::kMouseDown: { + CheckLayout(); + + // Ignore mouse stuff while transitioning. + if (transitioning_ && transitioning_out_) break; + + float x = m.fval1; + float y = m.fval2; + auto click_count = static_cast(m.fval3); + + float l = 0; + float r = width_; + float b = 0; + float t = height_; + + if (!root_selectable_) { + // Go through all widgets backwards until one claims the click. + for (auto i = widgets_.rbegin(); i != widgets_.rend(); i++) { + float cx = x; + float cy = y; + TransformPointToChild(&cx, &cy, **i); + if ((**i).HandleMessage( + WidgetMessage(m.type, nullptr, cx, cy, click_count))) { + claimed = true; + break; + } + if (modal_children_) { + claimed = true; + break; + } + } + } + + // If its not yet claimed, see if its within our contained region, in + // which case we claim it (only for regular mouse-downs). + if (!claimed && m.type == WidgetMessage::Type::kMouseDown) { + float bottom_overlap = 2; + float top_overlap = 2; + + if (background_ || root_selectable_) { + if (x >= l && x < r && y >= b - bottom_overlap + && y < t + top_overlap) { + claimed = true; + mouse_over_ = true; + + if (root_selectable_) { + GlobalSelect(); + + pressed_ = true; + + pressed_activate_ = click_count == 2 || click_activate_; + + // First click just selects. + if (click_count == 1) { + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + } + } else { + // Special case: If we've got a child text widget that's + // selected, clicking on our background de-selects it. This is a + // common way of getting rid of a screen keyboard on ios, etc. + if (dynamic_cast(selected_widget_) != nullptr) { + SelectWidget(nullptr); + } + + if (draggable_) { + dragging_ = true; + drag_x_ = x; + drag_y_ = y; + } + } + } + } + + // Call our outside-click callback if unclaimed. + if (!claimed && on_outside_click_call_.exists()) { + // Call this in the next cycle (don't wanna risk mucking with UI from + // within a UI loop). + g_game->PushPythonWeakCall( + Object::WeakRef(on_outside_click_call_)); + } + + // Always claim if they want. + if (claims_outside_clicks_) { + claimed = true; + } + } + break; + } + case WidgetMessage::Type::kMouseUp: { + CheckLayout(); + dragging_ = false; + float x = m.fval1; + float y = m.fval2; + claimed = (m.fval3 > 0.0f); + float l = 0; + float r = width_; + float b = 0; + float t = height_; + if (!root_selectable_) { + // Go through all widgets backwards until one claims the click. + // We then send it to everyone else too; just marking it as claimed. + // (this helps prevent widgets getting 'stuck' because someone else + // claimed their mouse-up). + for (auto i = widgets_.rbegin(); i != widgets_.rend(); i++) { + float cx = x; + float cy = y; + TransformPointToChild(&cx, &cy, ((**i))); + if ((**i).HandleMessage( + WidgetMessage(m.type, nullptr, cx, cy, claimed))) { + claimed = true; + } + if (modal_children_) { + break; + } + } + } + float bottom_overlap = 2; + float top_overlap = 2; + + // When pressed, we *always* claim mouse-ups. + if (pressed_) { + pressed_ = false; + + // If we're pressed, mouse-ups within our region trigger activation. + if (pressed_activate_ && !claimed && x >= l && x < r + && y >= b - bottom_overlap && y < t + top_overlap) { + Activate(); + pressed_activate_ = false; + } + return true; + } + // If its not yet claimed, see if its within our contained region, in + // which case we claim it but do nothing. + if (!claimed) { + if (background_) + if (x >= l && x < r && y >= b - bottom_overlap && y < t + top_overlap) + claimed = true; + } + break; + } + default: + break; + } + return claimed; +} + +auto ContainerWidget::GetMult(millisecs_t current_time, bool for_glow) const + -> float { + if (root_selectable_ && selected()) { + float m; + + // Only pulsate if regular widget highlighting is on and we're selected. + if (g_ui->ShouldHighlightWidgets()) { + if (IsHierarchySelected()) { + m = 0.5f + + std::abs(sinf(static_cast(current_time) * 0.006467f) + * 0.4f); + } else { + m = 0.7f; + } + } else { + m = 0.7f; + } + + // Extra brightness for draw dependents. + float m2 = 1.0f; + + // Current or recent presses jack things up. + if ((mouse_over_ && pressed_) + || (current_time - last_activate_time_ < 200)) { + m *= 1.7f; + m2 *= 1.1f; + } else if (g_ui->ShouldHighlightWidgets()) { + // Otherwise if we're supposed to always highlight all widgets, pulsate + // when directly selected and glow softly when indirectly. + if (IsHierarchySelected()) { + // Pulsate. + m = 0.5f + + std::abs(sinf(static_cast(current_time) * 0.006467f) + * 0.4f); + } else { + // Not directly selected; highlight only if we're always supposed to. + if (always_highlight_) { + m = 0.7f; + } else { + if (for_glow) + m = 0.0f; + else + m = 0.7f; + } + } + } else if (always_highlight_) { + // Otherwise if we're specifically set to always highlight, do so. + m *= 1.3f; + m2 *= 1.0f; + } else { + // Otherwise no glow. + // For glow we return 0 in this case. For other purposes 1. + if (for_glow) { + m = 0.0f; + } else { + m = 0.7f; + } + } + return (1.0f / 0.7f) * m * m2; // Anyone linked to us uses this. + } else { + return 1.0f; + } +} + +auto ContainerWidget::GetDrawBrightness(millisecs_t current_time) const + -> float { + return GetMult(current_time); +} + +void ContainerWidget::SetOnCancelCall(PyObject* call_tuple) { + on_cancel_call_ = Object::New(call_tuple); +} + +void ContainerWidget::SetRootSelectable(bool enable) { + root_selectable_ = enable; + + // If *we* are selectable, can't have selected children. + if (root_selectable_) { + SelectWidget(nullptr); + } +} + +void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { + BA_DEBUG_UI_READ_LOCK; + + CheckLayout(); + millisecs_t net_time = pass->frame_def()->base_time(); + float offset_h = 0.0f; + + // If we're transitioning, update our offsets in the first (opaque) pass. + if (transitioning_) { + bg_dirty_ = true; + + if (!draw_transparent) { + if (transition_type_ == TRANSITION_IN_SCALE) { + if (net_time - dynamics_update_time_ > 1000) + dynamics_update_time_ = net_time - 1000; + while (net_time - dynamics_update_time_ > 5) { + dynamics_update_time_ += 5; + d_transition_scale_ += + std::min(0.2f, (1.0f - transition_scale_)) * 0.04f; + d_transition_scale_ *= 0.87f; + transition_scale_ += d_transition_scale_; + if (std::abs(transition_scale_ - 1.0f) < 0.001 + && std::abs(d_transition_scale_) < 0.0001f) { + transition_scale_ = 1.0f; + transitioning_ = false; + } + } + } else if (transition_type_ == TRANSITION_OUT_SCALE) { + if (net_time - dynamics_update_time_ > 1000) + dynamics_update_time_ = net_time - 1000; + while (net_time - dynamics_update_time_ > 5) { + dynamics_update_time_ += 5; + transition_scale_ -= 0.04f; + if (transition_scale_ <= 0.0f) { + transition_scale_ = 0.0f; + + // Probably not safe to delete ourself here since we're in + // the draw loop, but we can push a call to do it. + Object::WeakRef weakref(this); + g_game->PushCall([weakref] { + Widget* w = weakref.get(); + if (w) g_ui->DeleteWidget(w); + }); + return; + } + } + } else { + // Step our dynamics up to the present. + if (net_time - dynamics_update_time_ > 1000) + dynamics_update_time_ = net_time - 1000; + while (net_time - dynamics_update_time_ > 5) { + dynamics_update_time_ += 5; + + if (transitioning_) { + millisecs_t t = dynamics_update_time_; + if (t - transition_start_time_ < TRANSITION_DURATION) { + float amt = static_cast(t - transition_start_time_) + / TRANSITION_DURATION; + if (transitioning_out_) { + amt = pow(amt, 1.1f); + } else { + amt = 1.0f - pow(1.0f - amt, 1.1f); + } + transition_offset_x_ = transition_start_offset_ * (1.0f - amt) + + transition_target_offset_ * amt; + offset_h += transition_offset_x_; + } else { + // Transition is done when we come to a stop. + if (transitioning_out_) { + transition_offset_x_ = transition_target_offset_; + } else { + transition_offset_x_ = 0.0f; + } + + // If going out, we're done as soon. + bool done; + if (transitioning_out_) { + done = (std::abs(transition_offset_x_smoothed_ + - transition_offset_x_) + < 1000.0f); + } else { + done = ((std::abs(transition_offset_x_vel_) < 0.05f) + && (std::abs(transition_offset_y_vel_) < 0.05f) + && (std::abs(transition_offset_x_smoothed_) < 0.05f) + && (std::abs(transition_offset_y_smoothed_) < 0.05f)); + } + if (done) { + transitioning_ = false; + transition_offset_x_smoothed_ = 0.0f; + transition_offset_y_smoothed_ = 0.0f; + if (transitioning_out_) { + // Probably not safe to delete ourself here since we're in the + // draw loop, but we can set up an event to do it. + Object::WeakRef weakref(this); + g_game->PushCall([weakref] { + Widget* w = weakref.get(); + if (w) g_ui->DeleteWidget(w); + }); + return; + } + } + } + + // Update our springy smoothed values. + float diff = transition_offset_x_ - transition_offset_x_smoothed_; + if (transitioning_out_) { + transition_offset_x_vel_ += diff * 0.03f; + transition_offset_x_vel_ *= 0.5f; + } else { + transition_offset_x_vel_ += diff * 0.04f; + transition_offset_x_vel_ *= 0.805f; + } + transition_offset_x_smoothed_ += transition_offset_x_vel_; + diff = transition_offset_y_ - transition_offset_y_smoothed_; + transition_offset_y_vel_ += diff * 0.04f; + transition_offset_y_vel_ *= 0.98f; + transition_offset_y_smoothed_ += transition_offset_y_vel_; + } + } + } + + // If we're scaling in or out, update our transition offset + // (so we can zoom from a point somewhere else on screen). + if (transition_type_ == TRANSITION_IN_SCALE + || transition_type_ == TRANSITION_OUT_SCALE) { + // Add a fudge factor since our scale point isn't exactly in our center. + // :-( + float xdiff = scale_origin_stack_offset_x_ - stack_offset_x() + + GetWidth() * -0.05f; + float ydiff = scale_origin_stack_offset_y_ - stack_offset_y(); + transition_scale_offset_x_ = + ((1.0f - transition_scale_) * xdiff) / scale(); + transition_scale_offset_y_ = + ((1.0f - transition_scale_) * ydiff) / scale(); + } + } + } + + // Don't draw if we've fully transitioned out. + if (transitioning_out_ && !transitioning_) return; + + float l = transition_offset_x_smoothed_ + transition_scale_offset_x_; + float r = l + width_; + float b = transition_offset_y_smoothed_ + transition_scale_offset_y_; + float t = b + height_; + + float w = width_; + float h = height_; + + // Update bg vals if need be + // (we may need these even if bg is turned off so always calc them). + if (bg_dirty_) { + SystemTextureID tex_id; + float l_border, r_border, b_border, t_border; + float width = r - l; + float height = t - b; + if (height > width * 0.6f) { + tex_id = SystemTextureID::kWindowHSmallVMed; + bg_model_transparent_i_d_ = SystemModelID::kWindowHSmallVMedTransparent; + bg_model_opaque_i_d_ = SystemModelID::kWindowHSmallVMedOpaque; + l_border = width * 0.07f; + r_border = width * 0.19f; + b_border = height * 0.1f; + t_border = height * 0.07f; + } else { + tex_id = SystemTextureID::kWindowHSmallVSmall; + bg_model_transparent_i_d_ = SystemModelID::kWindowHSmallVSmallTransparent; + bg_model_opaque_i_d_ = SystemModelID::kWindowHSmallVSmallOpaque; + l_border = width * 0.12f; + r_border = width * 0.19f; + b_border = height * 0.45f; + t_border = height * 0.23f; + } + bg_width_ = r - l + l_border + r_border; + bg_height_ = t - b + b_border + t_border; + bg_center_x_ = l - l_border + bg_width_ * 0.5f; + bg_center_y_ = b - b_border + bg_height_ * 0.5f; + if (background_) { + tex_ = g_media->GetTexture(tex_id); + } + bg_dirty_ = false; + } + + // In opaque mode, draw our child widgets immediately front-to-back to best + // make use of the z buffer. + if (!draw_transparent) { + DrawChildren(pass, draw_transparent, l, b, transition_scale_); + } + + // Draw our window backing if we have one. + if ((w > 0) && (h > 0)) { + if (background_) { + SimpleComponent c(pass); + c.SetTransparent(draw_transparent); + float s = 1.0f; + if (transition_scale_ <= 0.9f && !transitioning_out_) { + float amt = transition_scale_ / 0.9f; + s = std::min((1.0f - amt) * 4.0f, 2.5f) + amt * 1.0f; + } + c.SetColor(red_ * s, green_ * s, blue_ * s, alpha_); + c.SetTexture(tex_.get()); + c.PushTransform(); + c.Translate(bg_center_x_, bg_center_y_); + c.Scale(bg_width_ * transition_scale_, bg_height_ * transition_scale_); + c.DrawModel(g_media->GetModel(draw_transparent ? bg_model_transparent_i_d_ + : bg_model_opaque_i_d_)); + c.PopTransform(); + c.Submit(); + } + } + + // Draw our widgets here back-to-front in transparent mode. + if (draw_transparent) { + DrawChildren(pass, draw_transparent, l, b, transition_scale_); + } + + // Draw overlay glow. + if (root_selectable_ && selected()) { + float m = GetMult(net_time, true); + if (draw_transparent) { + if (glow_dirty_) { + float l_border, r_border, b_border, t_border; + l_border = 18; + r_border = 10; + b_border = 18; + t_border = 18; + glow_width_ = r - l + l_border + r_border; + glow_height_ = t - b + b_border + t_border; + glow_center_x_ = l - l_border + glow_width_ * 0.5f; + glow_center_y_ = b - b_border + glow_height_ * 0.5f; + glow_dirty_ = false; + } + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.SetTexture(g_media->GetTexture(SystemTextureID::kGlow)); + c.SetColor(0.25f * m, 0.25f * m, 0, 0.3f * m); + c.PushTransform(); + c.Translate(glow_center_x_, glow_center_y_); + c.Scale(glow_width_, glow_height_); + c.DrawModel(g_media->GetModel(SystemModelID::kImage4x1)); + c.PopTransform(); + c.Submit(); + } + } +} + +void ContainerWidget::TransformPointToChild(float* x, float* y, + const Widget& child) const { + assert(child.parent_widget() == this); + if (child.scale() == 1.0f) { + (*x) -= child.tx(); + (*y) -= child.ty(); + } else { + (*x) -= child.tx(); + (*y) -= child.ty(); + (*x) /= child.scale(); + (*y) /= child.scale(); + } +} + +void ContainerWidget::TransformPointFromChild(float* x, float* y, + const Widget& child) const { + assert(child.parent_widget() == this); + if (child.scale() == 1.0f) { + (*x) += child.tx(); + (*y) += child.ty(); + } else { + (*x) *= child.scale(); + (*y) *= child.scale(); + (*x) += child.tx(); + (*y) += child.ty(); + } +} + +void ContainerWidget::Activate() { + last_activate_time_ = g_game->master_time(); + if (on_activate_call_.exists()) { + // Call this in the next cycle (don't wanna risk mucking with UI from within + // a UI loop). + g_game->PushPythonWeakCall( + Object::WeakRef(on_activate_call_)); + } +} + +void ContainerWidget::AddWidget(Widget* w) { + BA_PRECONDITION(InGameThread()); + Object::WeakRef weakthis(this); + { + BA_DEBUG_UI_WRITE_LOCK; + w->set_parent_widget(this); + widgets_.insert(widgets_.end(), Object::Ref(w)); + } + + // If we're not selectable ourself and our child is, select it. + if (!root_selectable_ + && ((selected_widget_ == nullptr) || is_window_stack_)) { + if (w->IsSelectable()) { + // A change on the main or overlay window stack changes the global + // selection (unless its on the main window stack and there's already + // something on the overlay stack) in all other cases we just shift our + // direct selected child (which may not affect the global selection). + if (is_window_stack_ + && (is_overlay_window_stack_ + || !g_ui->root_widget()->overlay_window_stack()->HasChildren())) { + w->GlobalSelect(); + + // Special case for the main window stack; whenever a window is added, + // update the toolbar state for the topmost living container. + if (is_main_window_stack_) { + g_ui->root_widget()->UpdateForFocusedWindow(); + } + } else { + SelectWidget(w); + } + } + } + + // Select actions we run above may trigger user code which may kill us. + if (!weakthis.exists()) { + return; + } + + MarkForUpdate(); +} + +auto ContainerWidget::IsAcceptingInput() const -> bool { + return (!ignore_input_); +} + +// Delete all widgets. +void ContainerWidget::Clear() { + BA_DEBUG_UI_WRITE_LOCK; + widgets_.clear(); + selected_widget_ = nullptr; + prev_selected_widget_ = nullptr; +} + +void ContainerWidget::SetCancelButton(ButtonWidget* button) { + assert(button); + + if (!button->is_color_set()) { + button->SetColor(0.7f, 0.4f, 0.34f); + button->set_text_color(0.9f, 0.9f, 1.0f, 1.0f); + } + cancel_button_ = button; + + // Don't give it a back icon if it has a custom assigned one.. + // FIXME: This should be dynamic. + if (button->icon() == nullptr) { + button->set_icon_type(ButtonWidget::IconType::kCancel); + } +} + +void ContainerWidget::SetStartButton(ButtonWidget* button) { + assert(button); + if (!button->is_color_set()) { + button->SetColor(0.2f, 0.8f, 0.55f); + } + start_button_ = button; + + button->set_icon_type(ButtonWidget::IconType::kStart); +} + +void ContainerWidget::SetTransition(TransitionType t) { + BA_DEBUG_UI_READ_LOCK; + + bg_dirty_ = glow_dirty_ = true; + ContainerWidget* parent = parent_widget(); + if (parent == nullptr) return; + parent->CheckLayout(); + millisecs_t net_time = g_game->master_time(); + transition_type_ = t; + + // Scale transitions are simpler. + if (t == TRANSITION_IN_SCALE) { + transition_start_time_ = net_time; + dynamics_update_time_ = net_time; + transitioning_ = true; + transitioning_out_ = false; + transition_scale_ = 0.0f; + d_transition_scale_ = 0.0f; + } else if (t == TRANSITION_OUT_SCALE) { + transition_start_time_ = net_time; + dynamics_update_time_ = net_time; + transitioning_ = true; + transitioning_out_ = true; + } else { + // Calculate the screen size in our own local space - we'll + // animate an offset to slide on/off screen. + float screen_min_x = 0.0f; + float screen_min_y = 0.0f; + float screen_max_x = g_graphics->screen_virtual_width(); + float screen_max_y = g_graphics->screen_virtual_height(); + ScreenPointToWidget(&screen_min_x, &screen_min_y); + ScreenPointToWidget(&screen_max_x, &screen_max_y); + + // In case we're mid-transition, this avoids hitches. + float y_offs = 2.0f; + if (t == TRANSITION_IN_LEFT) { + transition_start_time_ = net_time; + transition_start_offset_ = screen_min_x - width_ - 100; + transition_offset_x_smoothed_ = transition_start_offset_; + transition_offset_y_smoothed_ = (RandomFloat() > 0.5f) ? y_offs : -y_offs; + transition_target_offset_ = 0; + transitioning_ = true; + dynamics_update_time_ = net_time; + transitioning_out_ = false; + } else if (t == TRANSITION_IN_RIGHT) { + transition_start_time_ = net_time; + transition_start_offset_ = screen_max_x + 100; + transition_offset_x_smoothed_ = transition_start_offset_; + transition_offset_y_smoothed_ = (RandomFloat() > 0.5f) ? y_offs : -y_offs; + transition_target_offset_ = 0; + transitioning_ = true; + dynamics_update_time_ = net_time; + transitioning_out_ = false; + } else if (t == TRANSITION_OUT_LEFT) { + transition_start_time_ = net_time; + transition_start_offset_ = transition_offset_x_; + transition_target_offset_ = -2.0f * (screen_max_x - screen_min_x); + transition_offset_x_smoothed_ = transition_start_offset_; + transition_offset_y_smoothed_ = 0.0f; + transitioning_ = true; + dynamics_update_time_ = net_time; + transitioning_out_ = true; + ignore_input_ = true; + } else if (t == TRANSITION_OUT_RIGHT) { + transition_start_time_ = net_time; + transition_start_offset_ = transition_offset_x_; + transition_target_offset_ = 2.0f * (screen_max_x - screen_min_x); + transition_offset_x_smoothed_ = transition_start_offset_; + transition_offset_y_smoothed_ = 0.0f; + transitioning_ = true; + dynamics_update_time_ = net_time; + transitioning_out_ = true; + ignore_input_ = true; + } + } + + // If we're transitioning out in some way and our parent is the main window + // stack, update the toolbar for the new topmost input-accepting window + // *immediately* (otherwise we'd have to wait for our transition to complete + // before the toolbar switches). + if (transitioning_ && transitioning_out_ && parent != nullptr + && parent->is_main_window_stack_) { + g_ui->root_widget()->UpdateForFocusedWindow(); + } +} + +void ContainerWidget::ReselectLastSelectedWidget() { + if (prev_selected_widget_ != nullptr + && prev_selected_widget_ != selected_widget_) { + SelectWidget(prev_selected_widget_); + } +} + +// Remove the widget from our list which should kill it. +void ContainerWidget::DeleteWidget(Widget* w) { + bool found = false; + { + BA_DEBUG_UI_WRITE_LOCK; + // Hmmm couldn't we do this without having to iterate here? + // (at least in release build). + for (auto i = widgets_.begin(); i != widgets_.end(); i++) { + if (&(**i) == w) { + if (selected_widget_ == w) { + selected_widget_ = nullptr; + } + if (prev_selected_widget_ == w) { + prev_selected_widget_ = nullptr; + } + // Grab a ref until we clear it off the list to avoid funky recursion + // issues. + Object::Ref w2 = *i; + widgets_.erase(i); + found = true; + break; + } + } + } + + assert(found); + + // Special case: if we're the overlay stack and we've deleted our last widget, + // try to reselect whatever was last selected before the overlay stack. + if (is_overlay_window_stack_) { + if (widgets_.empty()) { + // Eww this logic should be in some sort of controller. + g_ui->root_widget()->ReselectLastSelectedWidget(); + return; + } + } + + // in some cases we want to auto select a new child widget + if (selected_widget_ == nullptr || is_window_stack_) { + BA_DEBUG_UI_READ_LOCK; + // no UI lock needed here.. we don't change anything until SelectWidget, + // at which point we exit the loop.. + for (auto i = widgets_.rbegin(); i != widgets_.rend(); i++) { + if ((**i).IsSelectable()) { + // A change on the main or overlay window stack changes the global + // selection (unless its on the main window stack and there's already + // something on the overlay stack) in all other cases we just shift our + // direct selected child (which may not affect the global selection). + if (is_window_stack_ + && (is_overlay_window_stack_ + || !g_ui->root_widget() + ->overlay_window_stack() + ->HasChildren())) { + (**i).GlobalSelect(); + } else { + SelectWidget(&(**i)); + } + break; + } + } + } + + // Special case: if we're the main window stack, + // update the active toolbar/etc. + if (is_main_window_stack_) { + g_ui->root_widget()->UpdateForFocusedWindow(); + } +} + +auto ContainerWidget::GetTopmostToolbarInfluencingWidget() -> Widget* { + // Look for the first window that is accepting input (filters out windows that + // are transitioning out) and also set to affect the toolbar state. + for (auto w = widgets_.rbegin(); w != widgets_.rend(); ++w) { + if ((**w).IsAcceptingInput() + && (**w).toolbar_visibility() != ToolbarVisibility::kInherit) { + return &(**w); + } + } + return nullptr; +} + +void ContainerWidget::ShowWidget(Widget* w) { + if (!w) { + return; + } + + // Hacky exception; scroll-widgets don't respond directly to this + // (it always arrives via a child's child.. need to clean this up) + // it causes double-shows to happen otherwise and odd jumpy behavior. + if (GetWidgetTypeName() == "scroll") { + return; + } + + CheckLayout(); + float s = scale(); + float buffer_top = w->show_buffer_top(); + float buffer_bottom = w->show_buffer_bottom(); + float buffer_right = w->show_buffer_right(); + float buffer_left = w->show_buffer_left(); + float tx = (w->tx() - buffer_left) * s; + float ty = (w->ty() - buffer_bottom) * s; + float width = (w->GetWidth() + buffer_left + buffer_right) * s; + float height = (w->GetHeight() + buffer_bottom + buffer_top) * s; + HandleMessage(WidgetMessage(WidgetMessage::Type::kShow, nullptr, tx, ty, + width, height)); +} + +void ContainerWidget::SelectWidget(Widget* w, SelectionCause c) { + BA_DEBUG_UI_READ_LOCK; + + if (w == nullptr) { + if (selected_widget_) { + prev_selected_widget_ = selected_widget_; + selected_widget_->SetSelected(false, SelectionCause::NONE); + selected_widget_ = nullptr; + } + } else { + if (root_selectable_) { + Log("Error: SelectWidget() called on a ContainerWidget which is itself " + "selectable. Ignoring."); + return; + } + for (auto& widget : widgets_) { + if (&(*widget) == w) { + Widget* prevSelectedWidget = selected_widget_; + + // Deactivate old selected widget. + if (selected_widget_) { + selected_widget_->SetSelected(false, SelectionCause::NONE); + selected_widget_ = nullptr; + } + if ((*widget).IsSelectable()) { + (*widget).SetSelected(true, c); + selected_widget_ = &(*widget); + + // Store the old one as prev-selected if its not the one we're + // selecting now. (otherwise re-selecting repeatedly kills our prev + // mechanism). + if (prevSelectedWidget != selected_widget_) { + prev_selected_widget_ = prevSelectedWidget; + } + } else { + static bool printed = false; + if (!printed) { + Log("Warning: SelectWidget called on unselectable widget: " + + w->GetWidgetTypeName()); + Python::PrintStackTrace(); + printed = true; + } + } + break; + } + } + } +} + +void ContainerWidget::SetSelected(bool s, SelectionCause cause) { + BA_DEBUG_UI_READ_LOCK; + + Widget::SetSelected(s, cause); + + // If we've got selection-looping-to-parent enabled, being selected via + // next/prev snaps our sub-selection to our first or last widget. + if (s) { + if (selection_loops_to_parent()) { + if (cause == SelectionCause::NEXT_SELECTED) { + for (auto& widget : widgets_) { + if ((*widget).IsSelectable()) { + ShowWidget(&(*widget)); + SelectWidget(&(*widget), cause); + break; + } + } + } else if (cause == SelectionCause::PREV_SELECTED) { + for (auto i = widgets_.rbegin(); i != widgets_.rend(); i++) { + if ((**i).IsSelectable()) { + ShowWidget(&(**i)); + SelectWidget(&(**i), cause); + break; + } + } + } + } + } else { + // if we're being deselected and we have a selected child, tell them they're + // deselected + // if (selected_widget_) { + // } + } +} + +auto ContainerWidget::GetClosestLeftWidget(float our_x, float our_y, + Widget* ignore_widget) -> Widget* { + Widget* w = nullptr; + float x, y; + float closest_val = 9999.0f; + for (auto i = widgets_.begin(); i != widgets_.end(); i++) { + assert(i->exists()); + (**i).GetCenter(&x, &y); + float slope = std::abs(x - our_x) / (std::max(0.001f, std::abs(y - our_y))); + slope = std::min( + slope, AUTO_SELECT_SLOPE_CLAMP); // Beyond this, just go by distance. + float slope_weighted = AUTO_SELECT_SLOPE_WEIGHT * slope + + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; + if (i->get() != ignore_widget && x < our_x && slope > AUTO_SELECT_MIN_SLOPE + && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + // Take distance diff and multiply by our slope. + float xdist = x - our_x; + float ydist = y - our_y; + float dist = sqrtf(xdist * xdist + ydist * ydist); + float val = + dist / std::max(0.001f, slope_weighted + AUTO_SELECT_SLOPE_OFFSET); + if (val < closest_val || w == nullptr) { + closest_val = val; + w = i->get(); + } + } + } + return w; +} + +auto ContainerWidget::GetClosestRightWidget(float our_x, float our_y, + Widget* ignore_widget) -> Widget* { + Widget* w = nullptr; + float x, y; + float closest_val = 9999.0f; + for (auto i = widgets_.begin(); i != widgets_.end(); i++) { + assert(i->exists()); + (**i).GetCenter(&x, &y); + float slope = std::abs(x - our_x) / (std::max(0.001f, std::abs(y - our_y))); + slope = std::min( + slope, AUTO_SELECT_SLOPE_CLAMP); // beyond this, just go by distance + float slopeWeighted = AUTO_SELECT_SLOPE_WEIGHT * slope + + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; + if (i->get() != ignore_widget && x > our_x && slope > AUTO_SELECT_MIN_SLOPE + && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + // Take distance diff and multiply by our slope. + float xDist = x - our_x; + float yDist = y - our_y; + float dist = sqrtf(xDist * xDist + yDist * yDist); + float val = + dist / std::max(0.001f, slopeWeighted + AUTO_SELECT_SLOPE_OFFSET); + if (val < closest_val || w == nullptr) { + closest_val = val; + w = i->get(); + } + } + } + return w; +} + +auto ContainerWidget::GetClosestUpWidget(float our_x, float our_y, + Widget* ignoreWidget) -> Widget* { + Widget* w = nullptr; + float x, y; + float closest_val = 9999.0f; + for (auto i = widgets_.begin(); i != widgets_.end(); i++) { + assert(i->exists()); + (**i).GetCenter(&x, &y); + float slope = std::abs(y - our_y) / (std::max(0.001f, std::abs(x - our_x))); + slope = std::min( + slope, AUTO_SELECT_SLOPE_CLAMP); // Beyond this, just go by distance. + float slopeWeighted = AUTO_SELECT_SLOPE_WEIGHT * slope + + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; + if (i->get() != ignoreWidget && y > our_y && slope > AUTO_SELECT_MIN_SLOPE + && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + // Take distance diff and multiply by our slope. + float xDist = x - our_x; + float yDist = y - our_y; + float dist = sqrtf(xDist * xDist + yDist * yDist); + float val = + dist / std::max(0.001f, slopeWeighted + AUTO_SELECT_SLOPE_OFFSET); + if (val < closest_val || w == nullptr) { + closest_val = val; + w = i->get(); + } + } + } + return w; +} + +auto ContainerWidget::GetClosestDownWidget(float our_x, float our_y, + Widget* ignoreWidget) -> Widget* { + Widget* w = nullptr; + float x, y; + float closest_val = 9999.0f; + for (auto i = widgets_.begin(); i != widgets_.end(); i++) { + assert(i->exists()); + (**i).GetCenter(&x, &y); + float slope = std::abs(y - our_y) / (std::max(0.001f, std::abs(x - our_x))); + slope = std::min( + slope, AUTO_SELECT_SLOPE_CLAMP); // Beyond this, just go by distance. + float slopeWeighted = AUTO_SELECT_SLOPE_WEIGHT * slope + + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; + if (i->get() != ignoreWidget && y < our_y && slope > AUTO_SELECT_MIN_SLOPE + && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + // Take distance diff and multiply by our slope. + float xDist = x - our_x; + float yDist = y - our_y; + float dist = sqrtf(xDist * xDist + yDist * yDist); + float val = + dist / std::max(0.001f, slopeWeighted + AUTO_SELECT_SLOPE_OFFSET); + if (val < closest_val || w == nullptr) { + closest_val = val; + w = i->get(); + } + } + } + return w; +} + +void ContainerWidget::SelectDownWidget() { + BA_DEBUG_UI_READ_LOCK; + + if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + BA_LOG_ONCE("SelectDownWidget called before UI init."); + return; + } + + // If the current widget has an explicit down-widget set, go to it. + if (selected_widget_) { + Widget* w = selected_widget_->down_widget(); + + // If its auto-select, find our closest child widget. + if (!w && selected_widget_->auto_select()) { + float our_x, our_y; + selected_widget_->GetCenter(&our_x, &our_y); + w = GetClosestDownWidget(our_x, our_y, selected_widget_); + if (!w) { + // If we found no viable children and we're under the main window stack, + // see if we should pass focus to a toolbar widget. + if (IsInMainStack()) { + float x = our_x; + float y = our_y; + WidgetPointToScreen(&x, &y); + g_ui->root_widget()->ScreenPointToWidget(&x, &y); + w = g_ui->root_widget()->GetClosestDownWidget( + x, y, g_ui->screen_root_widget()); + } + // When we find no viable targets for an autoselect widget we do + // nothing. + if (!w) { + return; + } + } + } + if (w) { + if (!w->IsSelectable()) { + Log("Error: Down_widget is not selectable."); + } else { + w->Show(); + // Avoid tap sounds and whatnot if we're just re-selecting ourself. + if (w != selected_widget_) { + w->GlobalSelect(); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + } + } + } else { + // Have a selected widget but no specific 'down' widget; revert to just + // doing 'next'. + SelectNextWidget(); + } + } else { + // If nothing is selected, either do a select-next if we have + // something selectable or call our parent's select-down otherwise. + if (HasKeySelectableChild()) { + SelectNextWidget(); + } else { + if (ContainerWidget* parent = parent_widget()) { + parent->SelectDownWidget(); + } + } + } +} + +void ContainerWidget::SelectUpWidget() { + BA_DEBUG_UI_READ_LOCK; + + if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + BA_LOG_ONCE("SelectUpWidget called before UI init."); + return; + } + + // If the current widget has an explicit up-widget set, go to it. + if (selected_widget_) { + Widget* w = selected_widget_->up_widget(); + + // If its auto-select, find the closest widget. + if (!w && selected_widget_->auto_select()) { + float our_x, our_y; + selected_widget_->GetCenter(&our_x, &our_y); + w = GetClosestUpWidget(our_x, our_y, selected_widget_); + if (!w) { + // If we found no viable children and we're on the main window stack, + // see if we should pass focus to a toolbar widget. + if (IsInMainStack()) { + float x = our_x; + float y = our_y; + WidgetPointToScreen(&x, &y); + g_ui->root_widget()->ScreenPointToWidget(&x, &y); + w = g_ui->root_widget()->GetClosestUpWidget( + x, y, g_ui->screen_root_widget()); + } + // When we find no viable targets for an autoselect widget we do + // nothing. + if (!w) { + return; + } + } + } + if (w) { + if (!w->IsSelectable()) { + Log("Error: up_widget is not selectable."); + } else { + w->Show(); + // Avoid tap sounds and whatnot if we're just re-selecting ourself. + if (w != selected_widget_) { + w->GlobalSelect(); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + } + } + } else { + // Have a selected widget but no specific 'up' widget; revert to just + // doing prev. + SelectPrevWidget(); + } + } else { + // If nothing is selected, either do a select-prev if we have + // something selectable or call our parent's select-up otherwise. + if (HasKeySelectableChild()) { + SelectPrevWidget(); + } else { + if (ContainerWidget* parent = parent_widget()) { + parent->SelectUpWidget(); + } + } + } +} + +void ContainerWidget::SelectLeftWidget() { + BA_DEBUG_UI_READ_LOCK; + + if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + BA_LOG_ONCE("SelectLeftWidget called before UI init."); + return; + } + + // If the current widget has an explicit left-widget set, go to it. + if (selected_widget_) { + Widget* w = selected_widget_->left_widget(); + + // If its auto-select, find the closest widget. + if (!w && selected_widget_->auto_select()) { + float our_x, our_y; + selected_widget_->GetCenter(&our_x, &our_y); + w = GetClosestLeftWidget(our_x, our_y, selected_widget_); + // When we find no viable targets for an autoselect widget we do nothing. + if (!w) { + return; + } + } + if (w) { + if (!w->IsSelectable()) { + Log("Error: left_widget is not selectable."); + } else { + w->Show(); + // Avoid tap sounds and whatnot if we're just re-selecting ourself. + if (w != selected_widget_) { + w->GlobalSelect(); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + } + } + } else { + // Have a selected widget but no specific 'left' widget; revert to just + // doing prev. + SelectPrevWidget(); + } + } else { + // If nothing is selected, either do a select-prev if we have + // something selectable or call our parent's select-left otherwise. + if (HasKeySelectableChild()) { + SelectPrevWidget(); + } else { + if (ContainerWidget* parent = parent_widget()) { + parent->SelectLeftWidget(); + } + } + } +} +void ContainerWidget::SelectRightWidget() { + BA_DEBUG_UI_READ_LOCK; + + if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + BA_LOG_ONCE("SelectRightWidget called before UI init."); + return; + } + + // If the current widget has an explicit right-widget set, go to it. + if (selected_widget_) { + Widget* w = selected_widget_->right_widget(); + + // If its auto-select, find the closest widget. + if (!w && selected_widget_->auto_select()) { + float our_x, our_y; + selected_widget_->GetCenter(&our_x, &our_y); + w = GetClosestRightWidget(our_x, our_y, selected_widget_); + + // For autoselect widgets, if we find no viable targets, we do nothing. + if (!w) { + return; + } + } + if (w) { + if (!w->IsSelectable()) { + Log("Error: right_widget is not selectable."); + } else { + w->Show(); + // Avoid tap sounds and whatnot if we're just re-selecting ourself. + if (w != selected_widget_) { + w->GlobalSelect(); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + } + } + } else { + // Have a selected widget but no specific 'right' widget; revert to just + // doing next. + SelectNextWidget(); + } + } else { + // If nothing is selected, either do a select-next if we have + // something selectable or call our parent's select-right otherwise. + if (HasKeySelectableChild()) { + SelectNextWidget(); + } else { + if (ContainerWidget* parent = parent_widget()) { + parent->SelectRightWidget(); + } + } + } +} + +void ContainerWidget::SelectNextWidget() { + BA_DEBUG_UI_READ_LOCK; + + if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + BA_LOG_ONCE("SelectNextWidget called before UI init."); + return; + } + + millisecs_t old_last_prev_next_time = last_prev_next_time_; + if (should_print_list_exit_instructions_) { + last_prev_next_time_ = g_game->master_time(); + } + + // Grab the iterator for our selected widget if possible. + auto i = widgets_.begin(); + if (selected_widget_) { + for (; i != widgets_.end(); i++) { + if ((&(**i) == selected_widget_)) { + break; + } + } + } + + if (selected_widget_) { + // If we have a selection we should have been able to find its iterator. + assert((&(**i) == selected_widget_)); + i++; + } + + while (true) { + if (i == widgets_.end()) { + // Loop around if we allow it; otherwise abort. + if (selection_loops_to_parent()) { + ContainerWidget* w = parent_widget(); + if (w) { + w->SelectNextWidget(); + w->ShowWidget(w->selected_widget()); + } + return; + } else if (selected_widget_ + == nullptr) { // NOLINT(bugprone-branch-clone) + // We've got no selection and we've scanned the whole list to no avail, + // fail. + PrintExitListInstructions(old_last_prev_next_time); + return; + } else if (selection_loops()) { + i = widgets_.begin(); + } else { + PrintExitListInstructions(old_last_prev_next_time); + return; + } + } + + // If we had a selection, we abort if we've looped back to it. + if (&(**i) == selected_widget_) { + return; + } + if ((**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + SelectWidget(&(**i), SelectionCause::NEXT_SELECTED); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + return; + } + i++; + } +} + +// FIXME: should kill this. +void ContainerWidget::PrintExitListInstructions( + millisecs_t old_last_prev_next_time) { + if (should_print_list_exit_instructions_) { + millisecs_t t = g_game->master_time(); + if ((t - old_last_prev_next_time > 250) + && (t - last_list_exit_instructions_print_time_ > 5000)) { + last_list_exit_instructions_print_time_ = t; + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kErrorBeep)); + std::string s = g_game->GetResourceString("arrowsToExitListText"); + { + // Left arrow. + Utils::StringReplaceOne(&s, "${LEFT}", + g_game->CharStr(SpecialChar::kLeftArrow)); + } + { + // Right arrow. + Utils::StringReplaceOne(&s, "${RIGHT}", + g_game->CharStr(SpecialChar::kRightArrow)); + } + ScreenMessage(s); + } + } +} + +void ContainerWidget::SelectPrevWidget() { + BA_DEBUG_UI_READ_LOCK; + + millisecs_t old_last_prev_next_time = last_prev_next_time_; + if (should_print_list_exit_instructions_) { + last_prev_next_time_ = g_game->master_time(); + } + + // Grab the iterator for our selected widget if possible. + auto i = widgets_.rbegin(); + if (selected_widget_) { + for (; i != widgets_.rend(); i++) { + if ((&(**i) == selected_widget_)) { + break; + } + } + } + + if (selected_widget_) { + // If we have a selection we should have been able to find its iterator. + assert(&(**i) == selected_widget_); + i++; // Start with next one if we had this selected. + } + + while (true) { + if (i == widgets_.rend()) { + // Loop around if we allow it; otherwise abort. + if (selection_loops_to_parent()) { + ContainerWidget* w = parent_widget(); + if (w) { + w->SelectPrevWidget(); + w->ShowWidget(w->selected_widget()); + } + return; + } else if (selected_widget_ + == nullptr) { // NOLINT(bugprone-branch-clone) + // If we've got no selection and we've scanned the whole list to no + // avail, fail. + PrintExitListInstructions(old_last_prev_next_time); + return; + } else if (selection_loops()) { + i = widgets_.rbegin(); + } else { + PrintExitListInstructions(old_last_prev_next_time); + return; + } + } + + // If we had a selection, we abort if we loop back to it. + if (&(**i) == selected_widget_) { + return; + } + + if ((**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + SelectWidget(&(**i), SelectionCause::PREV_SELECTED); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + return; + } + i++; + } +} + +auto ContainerWidget::HasKeySelectableChild() const -> bool { + for (auto i = widgets_.begin(); i != widgets_.end(); i++) { + assert(i->exists()); + if ((**i).IsSelectable() && (**i).IsSelectableViaKeys()) { + return true; + } + } + return false; +} + +void ContainerWidget::CheckLayout() { + if (needs_update_) { + managed_ = false; + UpdateLayout(); + managed_ = true; + needs_update_ = false; + } +} + +void ContainerWidget::MarkForUpdate() { + ContainerWidget* w = this; + while (w) { + if (!w->managed_) { + return; + } + w->needs_update_ = true; + w = w->parent_widget(); + } +} + +void ContainerWidget::OnLanguageChange() { + for (auto&& widget : widgets_) { + if (widget.exists()) { + widget->OnLanguageChange(); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/container_widget.h b/src/ballistica/ui/widget/container_widget.h new file mode 100644 index 00000000..b8fa92f7 --- /dev/null +++ b/src/ballistica/ui/widget/container_widget.h @@ -0,0 +1,277 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_CONTAINER_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_CONTAINER_WIDGET_H_ + +#include +#include + +#include "ballistica/ui/widget/widget.h" + +namespace ballistica { + +// Base class for widgets that contain other widgets. +class ContainerWidget : public Widget { + public: + explicit ContainerWidget(float width = 0, float height = 0); + ~ContainerWidget() override; + + void Draw(RenderPass* pass, bool transparent) override; + + auto HandleMessage(const WidgetMessage& m) -> bool override; + + enum TransitionType { + TRANSITION_OUT_LEFT, + TRANSITION_OUT_RIGHT, + TRANSITION_IN_LEFT, + TRANSITION_IN_RIGHT, + TRANSITION_IN_SCALE, + TRANSITION_OUT_SCALE + }; + + void SetTransition(TransitionType t); + void SetCancelButton(ButtonWidget* button); + void SetStartButton(ButtonWidget* button); + void SetOnCancelCall(PyObject* call_tuple); + + // Set a widget to selected (must already have been added to dialog) + // Pass nullptr to deselect widgets. + void SelectWidget(Widget* w, SelectionCause s = SelectionCause::NONE); + void ReselectLastSelectedWidget(); + void ShowWidget(Widget* w); + void set_background(bool enable) { background_ = enable; } + void SetRootSelectable(bool enable); + void set_selectable(bool val) { selectable_ = val; } + + virtual void SetWidth(float w) { + bg_dirty_ = glow_dirty_ = true; + width_ = w; + MarkForUpdate(); + } + virtual void SetHeight(float h) { + bg_dirty_ = glow_dirty_ = true; + height_ = h; + MarkForUpdate(); + } + + void SetScaleOriginStackOffset(float x, float y) { + scale_origin_stack_offset_x_ = x; + scale_origin_stack_offset_y_ = y; + } + + // Note: Don't call these on yourself from within your CheckLayout() func. + // (reason is obvious if you look) - just use your values directly in that + // case. + auto GetWidth() -> float override { + CheckLayout(); + return width_; + } + auto GetHeight() -> float override { + CheckLayout(); + return height_; + } + + auto IsSelectable() -> bool override { return selectable_; } + + auto HasKeySelectableChild() const -> bool; + + void set_is_window_stack(bool a) { is_window_stack_ = a; } + auto is_window_stack() const -> bool { return is_window_stack_; } + + auto GetChildCount() const -> int { + assert(InGameThread()); + return static_cast(widgets_.size()); + } + void Clear(); + + void Activate() override; + + // Add a newly allocated widget to the container. + // This widget is now owned by the container and will be disposed by it. + void AddWidget(Widget* w); + + // Remove a widget from the container. + void DeleteWidget(Widget* w); + + // Select the next widget in the container's list. + void SelectNextWidget(); + + // Select the previous widget in the container's list. + void SelectPrevWidget(); + + void SelectDownWidget(); + void SelectUpWidget(); + void SelectLeftWidget(); + void SelectRightWidget(); + + // Return the currently selected widget, or nullptr if none selected. + auto selected_widget() -> Widget* { return selected_widget_; } + + auto GetWidgetTypeName() -> std::string override { return "container"; } + auto HasChildren() const -> bool override { return (!widgets_.empty()); } + + // Whether hitting 'next' at the last widget should loop back to the first. + // (generally true but list containers may not want) + auto selection_loops() const -> bool { return selection_loops_; } + + // If the selection doesn't loop, returns whether a selection loop transfers + // the message to the parent instead. + auto selection_loops_to_parent() const -> bool { + return selection_loops_to_parent_; + } + + void SetOnActivateCall(PyObject* c); + void SetOnOutsideClickCall(PyObject* c); + + auto widgets() const -> const std::vector >& { + return widgets_; + } + + void set_draggable(bool d) { draggable_ = d; } + void set_claims_tab(bool c) { claims_tab_ = c; } + void set_claims_left_right(bool c) { claims_left_right_ = c; } + void set_claims_up_down(bool c) { claims_up_down_ = c; } + void set_selection_loops_to_parent(bool d) { selection_loops_to_parent_ = d; } + auto claims_tab() const -> bool { return claims_tab_; } + auto claims_left_right() const -> bool { return claims_left_right_; } + auto claims_up_down() const -> bool { return claims_up_down_; } + void set_single_depth(bool s) { single_depth_ = s; } + + // Translate a point in-place into the space of a given child widget. + void TransformPointToChild(float* x, float* y, const Widget& child) const; + void TransformPointFromChild(float* x, float* y, const Widget& child) const; + + void set_color(float r, float g, float b, float a) { + red_ = r; + green_ = g; + blue_ = b; + alpha_ = a; + } + void set_should_print_list_exit_instructions(bool v) { + should_print_list_exit_instructions_ = v; + } + void set_selection_loops(bool loops) { selection_loops_ = loops; } + void set_click_activate(bool enabled) { click_activate_ = enabled; } + void set_always_highlight(bool enable) { always_highlight_ = enable; } + auto GetDrawBrightness(millisecs_t time) const -> float override; + auto IsAcceptingInput() const -> bool override; + void set_claims_outside_clicks(bool val) { claims_outside_clicks_ = val; } + void OnLanguageChange() override; + + void set_is_overlay_window_stack(bool val) { is_overlay_window_stack_ = val; } + void set_is_main_window_stack(bool val) { is_main_window_stack_ = val; } + + // Return the topmost widget that is accepting input. + // (used for toolbar focusing; may not always equal selected widget + // if the topmost one is transitioning out, etc.) + auto GetTopmostToolbarInfluencingWidget() -> Widget*; + + protected: + virtual void OnCancelCustom() {} + void set_single_depth_root(bool s) { single_depth_root_ = s; } + + // Note that the offsets here are purely for visual transitions and things; + // the UI itself only knows about the standard widget transform values. + void DrawChildren(RenderPass* pass, bool transparent, float x_offset, + float y_offset, float scale); + void SetSelected(bool s, SelectionCause cause) override; + void MarkForUpdate(); + + // Move/resize the contained widgets. + virtual void UpdateLayout() {} + void CheckLayout(); + + void set_modal_children(bool val) { modal_children_ = val; } + + auto width() const -> float { return width_; } + auto height() const -> float { return height_; } + void set_width(float val) { width_ = val; } + void set_height(float val) { height_ = val; } + + private: + // Given a container and a point, returns a selectable widget in the downward + // direction or nullptr. + auto GetClosestDownWidget(float x, float y, Widget* ignoreWidget) -> Widget*; + auto GetClosestUpWidget(float x, float y, Widget* ignoreWidget) -> Widget*; + auto GetClosestRightWidget(float x, float y, Widget* ignoreWidget) -> Widget*; + auto GetClosestLeftWidget(float x, float y, Widget* ignoreWidget) -> Widget*; + auto GetMult(millisecs_t current_time, bool for_glow = false) const -> float; + void PrintExitListInstructions(millisecs_t old_last_prev_next_time); + std::vector > widgets_; + float width_{}; + float height_{}; + bool modal_children_{}; + bool selection_loops_{true}; + bool is_main_window_stack_{}; + bool is_overlay_window_stack_{}; + float scale_origin_stack_offset_x_{}; + float scale_origin_stack_offset_y_{}; + float transition_scale_offset_x_{}; + float transition_scale_offset_y_{}; + bool pressed_{}; + bool mouse_over_{}; + bool pressed_activate_{}; + bool always_highlight_{}; + bool click_activate_{}; + float red_{0.4f}; + float green_{0.37f}; + float blue_{0.49f}; + float alpha_{1.0f}; + Object::Ref tex_; + SystemModelID bg_model_transparent_i_d_{}; + SystemModelID bg_model_opaque_i_d_{}; + float glow_width_{}, glow_height_{}, glow_center_x_{}, glow_center_y_{}; + float bg_width_{}, bg_height_{}, bg_center_x_{}, bg_center_y_{}; + millisecs_t last_activate_time_{}; + millisecs_t transition_start_time_{}; + float transition_target_offset_{}; + float drag_x_{}, drag_y_{}; + float transition_offset_x_{}; + float transition_offset_x_vel_{}; + float transition_offset_x_smoothed_{}; + float transition_offset_y_{}; + float transition_offset_y_vel_{}; + float transition_offset_y_smoothed_{}; + float transition_start_offset_{}; + float transition_scale_{1.0f}; + float d_transition_scale_{}; + millisecs_t dynamics_update_time_{}; + bool bg_dirty_{true}; + bool glow_dirty_{true}; + bool transitioning_{}; + TransitionType transition_type_{}; + bool transitioning_out_{}; + bool draggable_{}; + bool dragging_{}; + bool managed_{true}; + bool needs_update_{}; + bool claims_tab_{true}; + bool claims_left_right_{true}; + bool claims_up_down_{true}; + bool selection_loops_to_parent_{}; + bool is_window_stack_{}; + bool background_{true}; + bool root_selectable_{}; + bool selectable_{true}; + bool ignore_input_{}; + bool single_depth_{true}; + bool single_depth_root_{}; + bool should_print_list_exit_instructions_{}; + millisecs_t last_prev_next_time_{}; + millisecs_t last_list_exit_instructions_print_time_{}; + Widget* selected_widget_{}; + Widget* prev_selected_widget_{}; + Object::WeakRef cancel_button_; + Object::WeakRef start_button_; + bool claims_outside_clicks_{}; + + // Keep these at the bottom so they're torn down first. + // ...hmm that seems fragile; should I add explicit code to kill them? + Object::Ref on_activate_call_; + Object::Ref on_outside_click_call_; + Object::Ref on_cancel_call_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_WIDGET_CONTAINER_WIDGET_H_ diff --git a/src/ballistica/ui/widget/h_scroll_widget.cc b/src/ballistica/ui/widget/h_scroll_widget.cc new file mode 100644 index 00000000..a970d86f --- /dev/null +++ b/src/ballistica/ui/widget/h_scroll_widget.cc @@ -0,0 +1,810 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/h_scroll_widget.h" + +#include + +#include "ballistica/generic/real_timer.h" +#include "ballistica/graphics/component/empty_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/platform/platform.h" +#include "ballistica/ui/ui.h" + +namespace ballistica { + +const float kHMargin = 5.0f; + +HScrollWidget::HScrollWidget() + : touch_mode_(!g_platform->IsRunningOnDesktop()) { + set_draggable(false); + set_claims_left_right(false); + set_claims_tab(false); +} + +HScrollWidget::~HScrollWidget() = default; + +void HScrollWidget::HandleRealTimerExpired(RealTimer* t) { + if (touch_held_) { + // Pass a mouse-down event if we haven't moved. + if (!touch_is_scrolling_ && !touch_down_sent_) { + ContainerWidget::HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseDown, nullptr, touch_x_, + touch_y_, touch_held_click_count_)); + touch_down_sent_ = true; + } else { + } + } + + // Clean ourself out. + touch_delay_timer_.Clear(); +} + +void HScrollWidget::ClampThumb(bool velocity_clamp, bool position_clamp) { + BA_DEBUG_UI_READ_LOCK; + bool is_scrolling = (touch_held_ || !has_momentum_); + float strong_force; + float weak_force; + if (touch_mode_) { + strong_force = -0.12f; + weak_force = -0.004f; + } else { + strong_force = -0.012f; + weak_force = -0.004f; + } + auto i = widgets().begin(); + if (i != widgets().end()) { + float child_w = (**i).GetWidth(); + + if (velocity_clamp) { + if (child_offset_h_ < 0) { + // even in velocity case do some sane clamping + float diff = child_offset_h_; + inertia_scroll_rate_ += + diff * (is_scrolling ? strong_force : weak_force); + inertia_scroll_rate_ *= 0.9f; + + } else if (child_offset_h_ + > child_w - (width() - 2 * (border_width_ + kHMargin))) { + float diff = + child_offset_h_ + - (child_w + - std::min(child_w, (width() - 2 * (border_width_ + kHMargin)))); + inertia_scroll_rate_ += + diff * (is_scrolling ? strong_force : weak_force); + inertia_scroll_rate_ *= 0.9f; + } + } + + // hard clipping if we're dragging the scrollbar + if (position_clamp) { + if (child_offset_h_smoothed_ + > child_w - (width() - 2 * (border_width_ + kHMargin))) { + child_offset_h_smoothed_ = + child_w - (width() - 2 * (border_width_ + kHMargin)); + } + if (child_offset_h_smoothed_ < 0) { + child_offset_h_smoothed_ = 0; + } + if (child_offset_h_ + > child_w - (width() - 2 * (border_width_ + kHMargin))) { + child_offset_h_ = child_w - (width() - 2 * (border_width_ + kHMargin)); + } + if (child_offset_h_ < 0) { + child_offset_h_ = 0; + } + } + } +} + +auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { + BA_DEBUG_UI_READ_LOCK; + bool claimed = false; + bool pass = true; + float bottom_overlap = 3; + switch (m.type) { + case WidgetMessage::Type::kShow: { + claimed = true; + pass = false; + auto i = widgets().begin(); + if (i == widgets().end()) break; + float child_w = (**i).GetWidth(); + + // see where we'd have to scroll to get selection at left and right + float child_offset_left = + child_w - m.fval1 - (width() - 2 * (border_width_ + kHMargin)); + float child_offset_right = child_w - m.fval1 - m.fval3; + + // if we're in the middle, dont do anything + if (child_offset_h_ > child_offset_left + && child_offset_h_ < child_offset_right) { + } else { + float prev_child_offset = child_offset_h_; + // do whatever offset is less of a move + if (std::abs(child_offset_left - child_offset_h_) + < std::abs(child_offset_right - child_offset_h_)) { + child_offset_h_ = child_offset_left; + } else { + child_offset_h_ = child_offset_right; + } + + // if we're moving left, stop at the end + { + float max_val = child_w - (width() - 2 * (border_width_ + kHMargin)); + if (child_offset_h_ > max_val) child_offset_h_ = max_val; + } + // if we're moving right, stop at the top + { + if (child_offset_h_ < prev_child_offset) { + if (child_offset_h_ < 0) child_offset_h_ = 0; + } + } + } + + // Go into smooth mode momentarily. + smoothing_amount_ = 1.0f; + + // Snap our smoothed value to this *only* if we haven't drawn yet + // (keeps new widgets from inexplicably scrolling around). + if (!have_drawn_) { + child_offset_h_smoothed_ = child_offset_h_; + } + MarkForUpdate(); + break; + } + case WidgetMessage::Type::kMouseMove: { + float x = m.fval1; + float y = m.fval2; + bool claimed2 = (m.fval3 > 0.0f); + + if (touch_mode_) { + mouse_over_ = false; + } else { + mouse_over_ = + ((y >= 0.0f) && (y < height()) && (x >= 0.0f) && (x < width())); + } + + if (!mouse_over_) { + pass = false; + } + + if (claimed2) { + mouse_over_thumb_ = false; + } else { + if (touch_mode_) { + if (touch_held_) { + touch_x_ = x; + touch_y_ = y; + + // if this is a new scroll-touch, see which direction the drag is + // happening; if it's primarily vertical lets disown it so it can + // get handled by the scroll widget above us (presumably a vertical + // scroll widget) + if (new_scroll_touch_) { + float x_diff = std::abs(touch_x_ - touch_start_x_); + float y_diff = std::abs(touch_y_ - touch_start_y_); + + float dist = x_diff * x_diff + y_diff * y_diff; + + // if they're somehow equal, wait and look at the next one.. + if (x_diff != y_diff && dist > 30.0f) { + new_scroll_touch_ = false; + + // If they haven't moved far enough yet, ignore it. + if (x_diff < y_diff) { + return false; + } + } + } + + // Handle generating delayed press/releases. + if (static_cast(m.type)) { // <- FIXME WHAT IS THIS FOR?? + // If we move more than a slight amount it means our touch isn't a + // click. + if (!touch_is_scrolling_ + && ((std::abs(touch_x_ - touch_start_x_) > 10.0f) + || (std::abs(touch_y_ - touch_start_y_) > 10.0f))) { + touch_is_scrolling_ = true; + + // Go ahead and send a mouse-up to the sub-widgets; in their + // eyes the click is canceled. + if (touch_down_sent_ && !touch_up_sent_) { + ContainerWidget::HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, + m.fval1, m.fval2, true)); + touch_up_sent_ = true; + } + } + } + return true; + } + } + + if (touch_mode_) { + mouse_over_thumb_ = false; + } else { + float s_right = width() - border_width_; + float s_left = border_width_; + float sb_thumb_width = + amount_visible_ * (width() - 2.0f * border_width_); + float sb_thumb_right = s_right + - child_offset_h_ / child_max_offset_ + * (s_right - (s_left + sb_thumb_width)); + + mouse_over_thumb_ = + (((y >= 0) && (y < scroll_bar_height_ + bottom_overlap) + && x < sb_thumb_right && x >= sb_thumb_right - sb_thumb_width)); + } + } + + // If we're dragging. + if (mouse_held_thumb_) { + auto i = widgets().begin(); + if (i == widgets().end()) { + break; + } + float child_w = (**i).GetWidth(); + float sRight = width() - border_width_; + float sLeft = border_width_; + float rate = + (child_w - (sRight - sLeft)) + / ((1.0f - ((sRight - sLeft) / child_w)) * (sRight - sLeft)); + child_offset_h_ = thumb_click_start_child_offset_h_ + - rate * (x - thumb_click_start_h_); + + ClampThumb(false, true); + + MarkForUpdate(); + } + break; + } + case WidgetMessage::Type::kMouseUp: { + mouse_held_scroll_down_ = false; + mouse_held_scroll_up_ = false; + mouse_held_thumb_ = false; + mouse_held_page_down_ = false; + mouse_held_page_up_ = false; + + if (touch_mode_) { + if (touch_held_) { + bool m_claimed = (m.fval3 > 0.0f); + + touch_held_ = false; + + // If we moved at all, we mark it as claimed to keep + // sub-widgets from acting on it (since we used it for scrolling). + bool claimed2 = touch_is_scrolling_ || m_claimed; + + // If we're not claiming it and we haven't sent a mouse_down yet due + // to our delay, send that first. + if (!claimed2 && !touch_down_sent_) { + ContainerWidget::HandleMessage(WidgetMessage( + WidgetMessage::Type::kMouseDown, nullptr, m.fval1, m.fval2, + static_cast(touch_held_click_count_))); + touch_down_sent_ = true; + } + if (touch_down_sent_ && !touch_up_sent_) { + ContainerWidget::HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, m.fval1, + m.fval2, claimed2)); + touch_up_sent_ = true; + } + return true; + } + } + + // If coords are outside of our bounds, pass a mouse-up along for anyone + // tracking a drag, but mark it as claimed so it doesn't actually get + // acted on. + float x = m.fval1; + float y = m.fval2; + if (!((y >= 0.0f) && (y < height()) && (x >= 0.0f) && (x < width()))) { + pass = false; + ContainerWidget::HandleMessage(WidgetMessage( + WidgetMessage::Type::kMouseUp, nullptr, m.fval1, m.fval2, true)); + } + + break; + } + + case WidgetMessage::Type::kMouseWheelVelocityH: { + float x = m.fval1; + float y = m.fval2; + if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())) { + claimed = true; + pass = false; + has_momentum_ = static_cast(m.fval4); + + // We only set velocity from events when not in momentum mode; we + // handle momentum ourself. + if (std::abs(m.fval3) > 0.001f && !has_momentum_) { + float scroll_speed = 2.2f; + float smoothing = 0.8f; + float new_val; + if (m.fval3 < 0.0f) { + // Apply less if we're past the end. + if (child_offset_h_ < 0) { + new_val = scroll_speed * 0.1f * m.fval3; + } else { + new_val = scroll_speed * m.fval3; + } + } else { + // Apply less if we're past the end. + bool past_end = false; + + // Calc our total height. + auto i = widgets().begin(); + if (i != widgets().end()) { + float child_h = (**i).GetWidth(); + float diff = + child_offset_h_ + - (child_h + - std::min(child_h, + (width() - 2 * (border_width_ + kHMargin)))); + if (diff > 0) past_end = true; + } + if (past_end) { + new_val = scroll_speed * 0.1f * m.fval3; + } else { + new_val = scroll_speed * m.fval3; + } + } + inertia_scroll_rate_ = + smoothing * inertia_scroll_rate_ + (1.0f - smoothing) * new_val; + } + last_velocity_event_time_ = g_game->master_time(); + MarkForUpdate(); + } else { + // Not within our widget; dont allow children to claim. + pass = false; + } + break; + } + case WidgetMessage::Type::kMouseWheelH: { + float x = m.fval1; + float y = m.fval2; + if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())) { + claimed = true; + pass = false; + inertia_scroll_rate_ -= m.fval3 * 0.003f; + MarkForUpdate(); + } else { + // Not within our widget; dont allow children to claim. + pass = false; + } + break; + } + case WidgetMessage::Type::kScrollMouseDown: + case WidgetMessage::Type::kMouseDown: { + float x = m.fval1; + float y = m.fval2; + + // If its in our overall scroll region at all. + if ((y >= 0.0f) && (y < height()) && (x >= 0.0f) && (x < width())) { + // On touch devices, clicks begin scrolling, (and eventually can count + // as clicks if they don't move) + if (touch_mode_) { + touch_held_ = true; + auto click_count = static_cast(m.fval3); + touch_held_click_count_ = click_count; + touch_down_sent_ = false; + touch_up_sent_ = false; + touch_start_x_ = x; + touch_start_y_ = y; + touch_x_ = x; + touch_y_ = y; + touch_down_x_ = x - child_offset_h_; + touch_is_scrolling_ = false; + + // If there's significant scrolling happening we never pass touches. + // they're only used to scroll more/less. + if (std::abs(inertia_scroll_rate_) > 0.05f) { + touch_is_scrolling_ = true; + } + + pass = false; + claimed = true; + + // Top level touches eventually get passed as mouse-downs if no + // scrolling has started. + if (static_cast(m.type)) { + touch_delay_timer_ = + Object::New>(150, false, this); + } + + // If we're handling a scroll-touch, take note that we need to + // decide whether to disown the touch or not. + if (m.type == WidgetMessage::Type::kScrollMouseDown) { + new_scroll_touch_ = true; + } + } + + // On desktop, allow clicking on the scrollbar. + if (!touch_mode_) { + if (y <= scroll_bar_height_ + bottom_overlap) { + claimed = true; + pass = false; + + float sRight = width() - border_width_; + float sLeft = border_width_; + float sb_thumb_width = + amount_visible_ * (width() - 2 * border_width_); + float sb_thumb_right = sRight + - child_offset_h_ / child_max_offset_ + * (sRight - (sLeft + sb_thumb_width)); + + // To right of thumb (page-right). + if (x >= sb_thumb_right) { + smoothing_amount_ = 1.0f; // So we can see the transition. + child_offset_h_ -= (width() - 2 * (border_width_ + kHMargin)); + MarkForUpdate(); + ClampThumb(false, true); + } else if (x >= sb_thumb_right - sb_thumb_width) { + // On thumb. + mouse_held_thumb_ = true; + thumb_click_start_h_ = x; + thumb_click_start_child_offset_h_ = child_offset_h_; + } else if (x >= sLeft) { + // To left of thumb (page left). + smoothing_amount_ = 1.0f; // So we can see the transition. + child_offset_h_ += (width() - 2 * (border_width_ + kHMargin)); + MarkForUpdate(); + ClampThumb(false, true); + } + } + } + } else { + pass = false; // Not in the scroll box; dont allow children to claim. + } + break; + } + default: + break; + } + + // Normal container event handling. + if (pass) { + if (ContainerWidget::HandleMessage(m)) claimed = true; + } + + // If it was a mouse-down and we claimed it, set ourself as selected. + if (m.type == WidgetMessage::Type::kMouseDown && claimed) { + GlobalSelect(); + } + return claimed; +} + +void HScrollWidget::UpdateLayout() { + BA_DEBUG_UI_READ_LOCK; + + // Move everything based on our offset. + auto i = widgets().begin(); + if (i == widgets().end()) { + amount_visible_ = 0; + return; + } + float child_w = (**i).GetWidth(); + child_max_offset_ = child_w - (width() - 2 * (border_width_ + kHMargin)); + amount_visible_ = (width() - 2 * (border_width_ + kHMargin)) / child_w; + if (amount_visible_ > 1) { + amount_visible_ = 1; + if (center_small_content_) { + center_offset_x_ = child_max_offset_ * 0.5f; + } else { + center_offset_x_ = 0; + } + } else { + center_offset_x_ = 0; + } + if (mouse_held_thumb_) { + if (child_offset_h_ + > child_w - (width() - 2 * (border_width_ + kHMargin))) { + child_offset_h_ = child_w - (width() - 2 * (border_width_ + kHMargin)); + inertia_scroll_rate_ = 0; + } + if (child_offset_h_ < 0) { + child_offset_h_ = 0; + inertia_scroll_rate_ = 0; + } + } + (**i).set_translate(width() - (border_width_ + kHMargin) + + child_offset_h_smoothed_ - child_w + + center_offset_x_, + 4 + border_height_); + thumb_dirty_ = true; +} + +void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { + have_drawn_ = true; + millisecs_t current_time = pass->frame_def()->base_time(); + float prev_child_offset_h_smoothed = child_offset_h_smoothed_; + + // Ok, lets update our inertial scrolling during the opaque pass. + // (we really should have some sort of update() function for this but widgets + // don't have that currently) + if (!draw_transparent) { + // (skip huge differences) + if (current_time - inertia_scroll_update_time_ > 1000) + inertia_scroll_update_time_ = current_time - 1000; + while (current_time - inertia_scroll_update_time_ > 5) { + inertia_scroll_update_time_ += 5; + + if (touch_mode_) { + if (touch_held_) { + float diff = (touch_x_ - child_offset_h_) - touch_down_x_; + float smoothing = 0.7f; + inertia_scroll_rate_ = smoothing * inertia_scroll_rate_ + + (1.0f - smoothing) * 0.2f * diff; + } else { + inertia_scroll_rate_ *= 0.98f; + } + } else { + inertia_scroll_rate_ *= 0.98f; + } + + ClampThumb(true, mouse_held_thumb_); + child_offset_h_ += inertia_scroll_rate_; + + if (!has_momentum_ + && (current_time - last_velocity_event_time_ > 1000 / 30)) { + inertia_scroll_rate_ = 0; + } + + // Lastly we apply smoothing so that if we're snapping to a specific place + // we don't go instantly there we blend between smoothed and non-smoothed + // depending on whats driving us (we dont want to add smoothing on top of + // inertial scrolling for example or it'll feel muddy) + float diff = child_offset_h_ - child_offset_h_smoothed_; + if (std::abs(diff) < 1.0f) + child_offset_h_smoothed_ = child_offset_h_; + else + child_offset_h_smoothed_ += (1.0f - 0.95f * smoothing_amount_) * diff; + smoothing_amount_ = std::max(0.0f, smoothing_amount_ - 0.005f); + } + + // Only re-layout our widgets if we've moved a significant amount. + if (std::abs(prev_child_offset_h_smoothed - child_offset_h_smoothed_) + > 0.01f) { + MarkForUpdate(); + } + } + + CheckLayout(); + + Vector3f tilt = 0.02f * g_graphics->tilt(); + float extra_offs_x = tilt.y; + float extra_offs_y = -tilt.x; + + float b = 0; + float t = b + height(); + float l = 0; + float r = l + width(); + + // Begin clipping for children. + { + EmptyComponent c(pass); + c.SetTransparent(draw_transparent); + c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1, + l + (width() - border_width_ - 0), + b + (height() - border_height_) - 1)); + c.Submit(); + } + + set_simple_culling_left(l + border_width_); + set_simple_culling_right(l + (width() - border_height_)); + + // Draw all our widgets at our z level. + DrawChildren(pass, draw_transparent, l + extra_offs_x, b + extra_offs_y, + 1.0f); + + // End clipping. + { + EmptyComponent c(pass); + c.SetTransparent(draw_transparent); + c.ScissorPop(); + c.Submit(); + } + + // scroll trough (depth 0.7f to 0.8f) + if (draw_transparent && border_opacity_ > 0.0f) { + if (trough_dirty_) { + float b2 = b + 4; + float t2 = b2 + scroll_bar_height_; + float l2; + float r2; + l2 = l + (border_width_); + r2 = r - (border_width_); + float b_border, t_border, l_border, r_border; + b_border = 3; + t_border = 0; + l_border = width() * 0.006f; + r_border = width() * 0.002f; + trough_height_ = t2 - b2 + b_border + t_border; + trough_width_ = r2 - l2 + l_border + r_border; + trough_center_y_ = b2 - b_border + trough_height_ * 0.5f; + trough_center_x_ = l2 - l_border + trough_width_ * 0.5f; + trough_dirty_ = false; + } + + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1, 1, 1, border_opacity_); + c.SetTexture(g_media->GetTexture(SystemTextureID::kUIAtlas)); + c.PushTransform(); + c.Translate(trough_center_x_, trough_center_y_, 0.7f); + c.Scale(trough_width_, trough_height_, 0.1f); + c.Rotate(-90, 0, 0, 1); + c.DrawModel(g_media->GetModel(SystemModelID::kScrollBarTroughTransparent)); + c.PopTransform(); + c.Submit(); + } + + // scroll bars + if (amount_visible_ > 0 && amount_visible_ < 1) { + // scroll thumb at depth 0.8f-0.9 + { + float sb_thumb_width = amount_visible_ * (width() - 2 * border_width_); + if (thumb_dirty_) { + float sb_thumb_right = + r - border_width_ + - ((width() - (border_width_ * 2) - sb_thumb_width) + * child_offset_h_smoothed_ / child_max_offset_); + float b2 = 4; + float t2 = b2 + scroll_bar_height_; + float r2 = sb_thumb_right; + float l2 = r2 - sb_thumb_width; + float b_border, t_border, l_border, r_border; + b_border = 6; + t_border = 3; + if (sb_thumb_width > 100) { + auto wd = r2 - l2; + l_border = wd * 0.04f; + r_border = wd * 0.06f; + } else { + auto wd = r2 - l2; + r_border = wd * 0.12f; + l_border = wd * 0.08f; + } + thumb_height_ = t2 - b2 + b_border + t_border; + thumb_width_ = r2 - l2 + l_border + r_border; + + thumb_center_y_ = b2 - b_border + thumb_height_ * 0.5f; + thumb_center_x_ = l2 - l_border + thumb_width_ * 0.5f; + thumb_dirty_ = false; + } + + SimpleComponent c(pass); + c.SetTransparent(draw_transparent); + // float c_scale = 1.0f; + // if (mouse_held_thumb_) { + // c_scale = 1.8f; + // } else if (mouse_over_thumb_) { + // c_scale = 1.25f; + // } + + bool smooth_diff = + (std::abs(child_offset_h_smoothed_ - child_offset_h_) > 0.01f); + if (touch_mode_) { + if (smooth_diff || (touch_held_ && touch_is_scrolling_) + || std::abs(inertia_scroll_rate_) > 1.0f) { + touch_fade_ = std::min(1.5f, touch_fade_ + 0.02f); + } else { + // FIXME: Shouldn't be frame based. + touch_fade_ = std::max(0.0f, touch_fade_ - 0.015f); + } + } else { + if (smooth_diff || (touch_held_ && touch_is_scrolling_) + || std::abs(inertia_scroll_rate_) > 1.0f || mouse_over_) { + touch_fade_ = std::min(1.5f, touch_fade_ + 0.02f); + } else { + // FIXME: Shouldn't be frame based. + touch_fade_ = std::max(0.0f, touch_fade_ - 0.015f); + } + } + c.SetColor(0, 0, 0, std::min(1.0f, 0.3f * touch_fade_)); + + c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1, + l + (width()), b + (height() * 0.995f))); + c.PushTransform(); + c.Translate(thumb_center_x_, thumb_center_y_, 0.8f); + c.Scale(-thumb_width_, thumb_height_, 0.1f); + c.FlipCullFace(); + c.Rotate(-90, 0, 0, 1); + + // on touch, just draw these transiently +#if 1 + if (draw_transparent) { + c.DrawModel(g_media->GetModel( + sb_thumb_width > 100 ? SystemModelID::kScrollBarThumbSimple + : SystemModelID::kScrollBarThumbShortSimple)); + } +#else + if (draw_transparent) { + c.DrawModel(g_media->GetModel( + sb_thumb_width > 100 + ? Media::SCROLL_BAR_THUMB_TRANSPARENT_MODEL + : Media::SCROLL_BAR_THUMB_SHORT_TRANSPARENT_MODEL)); + } else { + c.DrawModel(g_media->GetModel( + sb_thumb_width > 100 ? Media::SCROLL_BAR_THUMB_OPAQUE_MODEL + : Media::SCROLL_BAR_THUMB_SHORT_OPAQUE_MODEL)); + } +#endif + + c.FlipCullFace(); + c.PopTransform(); + c.ScissorPop(); + c.Submit(); + } + } + + // outline shadow (depth 0.9 to 1.0) + if (draw_transparent && border_opacity_ > 0.0f) { + if (shadow_dirty_) { + float r2 = l + width(); + float l2 = l; + float b2 = b; + float t2 = t; + float l_border, r_border, b_border, t_border; + l_border = (r2 - l2) * 0.005f; + r_border = (r2 - l2) * 0.001f; + b_border = (t2 - b2) * 0.006f; + t_border = (t2 - b2) * 0.002f; + outline_width_ = r2 - l2 + l_border + r_border; + outline_height_ = t2 - b2 + b_border + t_border; + outline_center_x_ = l2 - l_border + 0.5f * outline_width_; + outline_center_y_ = b2 - b_border + 0.5f * outline_height_; + shadow_dirty_ = false; + } + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1, 1, 1, border_opacity_); + c.SetTexture(g_media->GetTexture(SystemTextureID::kScrollWidget)); + c.PushTransform(); + c.Translate(outline_center_x_, outline_center_y_, 0.9f); + c.Scale(outline_width_, outline_height_, 0.1f); + c.DrawModel(g_media->GetModel(SystemModelID::kSoftEdgeOutside)); + c.PopTransform(); + c.Submit(); + } + + // if selected, do glow at depth 0.9-1.0 + if (draw_transparent && IsHierarchySelected() + && g_ui->ShouldHighlightWidgets() && highlight_ + && border_opacity_ > 0.0f) { + float m = 0.8f + + std::abs(sinf(static_cast(current_time) * 0.006467f)) + * 0.2f * border_opacity_; + + if (glow_dirty_) { + float r2 = l + width(); + float l2 = l; + float b2 = b; + float t2 = t; + float l_border, r_border, b_border, t_border; + l_border = (r2 - l2) * 0.02f; + r_border = (r2 - l2) * 0.02f; + b_border = (t2 - b2) * 0.015f; + t_border = (t2 - b2) * 0.01f; + glow_width_ = r2 - l2 + l_border + r_border; + glow_height_ = t2 - b2 + b_border + t_border; + glow_center_x_ = l2 - l_border + 0.5f * glow_width_; + glow_center_y_ = b2 - b_border + 0.5f * glow_height_; + glow_dirty_ = false; + } + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetPremultiplied(true); + c.SetColor(0.4f * m, 0.5f * m, 0.05f * m, 0.0f); + c.SetTexture(g_media->GetTexture(SystemTextureID::kScrollWidgetGlow)); + c.PushTransform(); + c.Translate(glow_center_x_, glow_center_y_, 0.9f); + c.Scale(glow_width_, glow_height_, 0.1f); + c.DrawModel(g_media->GetModel(SystemModelID::kSoftEdgeOutside)); + c.PopTransform(); + c.Submit(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/h_scroll_widget.h b/src/ballistica/ui/widget/h_scroll_widget.h new file mode 100644 index 00000000..6ff5a375 --- /dev/null +++ b/src/ballistica/ui/widget/h_scroll_widget.h @@ -0,0 +1,120 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_H_SCROLL_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_H_SCROLL_WIDGET_H_ + +#include + +#include "ballistica/ui/widget/container_widget.h" + +namespace ballistica { + +template +class RealTimer; + +// A scroll-box container widget. +class HScrollWidget : public ContainerWidget { + public: + HScrollWidget(); + ~HScrollWidget() override; + void Draw(RenderPass* pass, bool transparent) override; + auto HandleMessage(const WidgetMessage& m) -> bool override; + auto GetWidgetTypeName() -> std::string override { return "scroll"; } + void set_capture_arrows(bool val) { capture_arrows_ = val; } + void SetWidth(float w) override { + trough_dirty_ = shadow_dirty_ = glow_dirty_ = thumb_dirty_ = true; + set_width(w); + MarkForUpdate(); + } + void SetHeight(float h) override { + trough_dirty_ = shadow_dirty_ = glow_dirty_ = thumb_dirty_ = true; + set_height(h); + MarkForUpdate(); + } + void SetCenterSmallContent(bool val) { + center_small_content_ = val; + MarkForUpdate(); + } + void HandleRealTimerExpired(RealTimer* t); + void setColor(float r, float g, float b) { + color_red_ = r; + color_green_ = g; + color_blue_ = b; + } + void set_highlight(bool val) { highlight_ = val; } + auto highlight() const -> bool { return highlight_; } + void setBorderOpacity(float val) { border_opacity_ = val; } + auto getBorderOpacity() const -> float { return border_opacity_; } + + protected: + void UpdateLayout() override; + + private: + void ClampThumb(bool velocity_clamp, bool position_clamp); + + bool touch_mode_{}; + float color_red_{0.55f}; + float color_green_{0.47f}; + float color_blue_{0.67f}; + bool has_momentum_{true}; + bool trough_dirty_{true}; + bool shadow_dirty_{true}; + bool glow_dirty_{true}; + bool thumb_dirty_{true}; + millisecs_t last_velocity_event_time_{}; + float touch_fade_{}; + bool center_small_content_{}; + float center_offset_x_{}; + bool touch_held_{}; + int touch_held_click_count_{}; + float touch_down_x_{}; + float touch_x_{}; + float touch_y_{}; + float touch_start_x_{}; + float touch_start_y_{}; + bool touch_is_scrolling_{}; + bool touch_down_sent_{}; + bool touch_up_sent_{}; + bool new_scroll_touch_{}; + float trough_width_{}; + float trough_height_{}; + float trough_center_x_{}; + float trough_center_y_{}; + float thumb_width_{}, thumb_height_{}, thumb_center_x_{}, thumb_center_y_{}; + float smoothing_amount_{1.0f}; + bool highlight_{true}; + float glow_width_{}; + float glow_height_{}; + float glow_center_x_{}; + float glow_center_y_{}; + float outline_width_{}; + float outline_height_{}; + float outline_center_x_{}; + float outline_center_y_{}; + float border_opacity_{1.0f}; + bool capture_arrows_{}; + bool mouse_held_scroll_down_{}; + bool mouse_held_scroll_up_{}; + bool mouse_held_thumb_{}; + float thumb_click_start_h_{}; + float thumb_click_start_child_offset_h_{}; + bool mouse_held_page_down_{}; + bool mouse_held_page_up_{}; + bool mouse_over_thumb_{}; + bool mouse_over_{}; + float scroll_bar_height_{10.0f}; + float border_width_{2.0f}; + float border_height_{2.0f}; + float child_offset_h_{-9999.0f}; + float child_offset_h_smoothed_{}; + float child_max_offset_{}; + float amount_visible_{}; + bool have_drawn_{}; + millisecs_t inertia_scroll_update_time_{}; + float inertia_scroll_rate_{}; + Object::Ref > touch_delay_timer_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_WIDGET_H_SCROLL_WIDGET_H_ diff --git a/src/ballistica/ui/widget/image_widget.cc b/src/ballistica/ui/widget/image_widget.cc new file mode 100644 index 00000000..906b7ff3 --- /dev/null +++ b/src/ballistica/ui/widget/image_widget.cc @@ -0,0 +1,180 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/image_widget.h" + +#include "ballistica/game/game.h" +#include "ballistica/graphics/component/simple_component.h" + +namespace ballistica { + +ImageWidget::ImageWidget() { birth_time_ = g_game->master_time(); } + +ImageWidget::~ImageWidget() = default; + +auto ImageWidget::GetWidth() -> float { return width_; } +auto ImageWidget::GetHeight() -> float { return height_; } + +void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { + if (opacity_ < 0.001f) { + return; + } + + millisecs_t current_time = pass->frame_def()->base_time(); + + Vector3f tilt = tilt_scale_ * 0.01f * g_graphics->tilt(); + if (draw_control_parent()) tilt += 0.02f * g_graphics->tilt(); + float extra_offs_x = -tilt.y; + float extra_offs_y = tilt.x; + + // Simple transition. + float transition = (birth_time_ + transition_delay_) - current_time; + if (transition > 0) { + extra_offs_x -= transition * 4.0f; + } + + float l = 0; + float r = l + width_; + float b = 0; + float t = b + height_; + + if (texture_.exists()) { + if (texture_->texture_data()->loaded() + && ((!tint_texture_.exists()) + || tint_texture_->texture_data()->loaded()) + && ((!mask_texture_.exists()) + || mask_texture_->texture_data()->loaded())) { + if (image_dirty_) { + image_width_ = r - l; + image_height_ = t - b; + image_center_x_ = l + image_width_ * 0.5f; + image_center_y_ = b + image_height_ * 0.5f; + image_dirty_ = false; + } + + Object::Ref model_opaque_used; + if (model_opaque_.exists()) { + model_opaque_used = model_opaque_->model_data(); + } + Object::Ref model_transparent_used; + if (model_transparent_.exists()) { + model_transparent_used = model_transparent_->model_data(); + } + + bool draw_radial_opaque = false; + bool draw_radial_transparent = false; + + // if no meshes were provided, use default image models + if ((!model_opaque_.exists()) && (!model_transparent_.exists())) { + if (has_alpha_channel_) { + if (radial_amount_ < 1.0f) { + draw_radial_transparent = true; + } else { + model_transparent_used = + g_media->GetModel(SystemModelID::kImage1x1); + } + } else { + if (radial_amount_ < 1.0f) { + draw_radial_opaque = true; + } else { + model_opaque_used = g_media->GetModel(SystemModelID::kImage1x1); + } + } + } + + // Draw brightness. + float db = 1.0f; + if (Widget* draw_controller = draw_control_parent()) { + db *= draw_controller->GetDrawBrightness(current_time); + } + + // Opaque portion may get drawn transparent or opaque depending on our + // global opacity. + if (model_opaque_used.exists() || draw_radial_opaque) { + bool should_draw = false; + bool should_draw_transparent = false; + + // Draw our opaque model in the opaque pass. + if (!draw_transparent && opacity_ > 0.999f) { + should_draw = true; + should_draw_transparent = false; + } else if (draw_transparent && opacity_ <= 0.999f) { + // Draw our opaque model in the transparent pass. + should_draw = true; + should_draw_transparent = true; + } + + if (should_draw) { + SimpleComponent c(pass); + c.SetTransparent(should_draw_transparent); + c.SetColor(color_red_ * db, color_green_ * db, color_blue_ * db, + opacity_); + c.SetTexture(texture_); + if (tint_texture_.exists()) { + c.SetColorizeTexture(tint_texture_); + c.SetColorizeColor(tint_color_red_, tint_color_green_, + tint_color_blue_); + c.SetColorizeColor2(tint2_color_red_, tint2_color_green_, + tint2_color_blue_); + } + c.SetMaskTexture(mask_texture_); + c.PushTransform(); + c.Translate(image_center_x_ + extra_offs_x, + image_center_y_ + extra_offs_y); + c.Scale(image_width_, image_height_, 1.0f); + if (draw_radial_opaque) { + if (!radial_mesh_.exists()) { + radial_mesh_ = Object::NewDeferred(); + } + Graphics::DrawRadialMeter(&(*radial_mesh_), radial_amount_); + c.Scale(0.5f, 0.5f, 1.0f); + c.DrawMesh(radial_mesh_.get()); + } else { + c.DrawModel(model_opaque_used.get()); + } + c.PopTransform(); + c.Submit(); + } + } + + // Always-transparent portion. + if ((model_transparent_used.exists() || draw_radial_transparent) + && draw_transparent) { + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(color_red_ * db, color_green_ * db, color_blue_ * db, + opacity_); + c.SetTexture(texture_); + if (tint_texture_.exists()) { + c.SetColorizeTexture(tint_texture_); + c.SetColorizeColor(tint_color_red_, tint_color_green_, + tint_color_blue_); + c.SetColorizeColor2(tint2_color_red_, tint2_color_green_, + tint2_color_blue_); + } + c.SetMaskTexture(mask_texture_); + c.PushTransform(); + c.Translate(image_center_x_ + extra_offs_x, + image_center_y_ + extra_offs_y); + c.Scale(image_width_, image_height_, 1.0f); + if (draw_radial_transparent) { + if (!radial_mesh_.exists()) { + radial_mesh_ = Object::New(); + } + Graphics::DrawRadialMeter(&(*radial_mesh_), radial_amount_); + c.Scale(0.5f, 0.5f, 1.0f); + c.DrawMesh(radial_mesh_.get()); + } else { + c.DrawModel(model_transparent_used.get()); + } + c.PopTransform(); + c.Submit(); + } + } + } +} + +auto ImageWidget::HandleMessage(const WidgetMessage& m) -> bool { + return false; +} + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/image_widget.h b/src/ballistica/ui/widget/image_widget.h new file mode 100644 index 00000000..00979437 --- /dev/null +++ b/src/ballistica/ui/widget/image_widget.h @@ -0,0 +1,118 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_IMAGE_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_IMAGE_WIDGET_H_ + +#include + +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/ui/widget/widget.h" + +namespace ballistica { + +class ImageWidget : public Widget { + public: + ImageWidget(); + ~ImageWidget() override; + void Draw(RenderPass* pass, bool transparent) override; + auto HandleMessage(const WidgetMessage& m) -> bool override; + void set_width(float width) { + image_dirty_ = true; + width_ = width; + } + void set_height(float val) { + image_dirty_ = true; + height_ = val; + } + auto GetWidth() -> float override; + auto GetHeight() -> float override; + void set_has_alpha_channel(bool val) { has_alpha_channel_ = val; } + void set_color(float r, float g, float b) { + color_red_ = r; + color_green_ = g; + color_blue_ = b; + } + void set_tint_color(float r, float g, float b) { + tint_color_red_ = r; + tint_color_green_ = g; + tint_color_blue_ = b; + } + void set_tint2_color(float r, float g, float b) { + tint2_color_red_ = r; + tint2_color_green_ = g; + tint2_color_blue_ = b; + } + void set_opacity(float o) { opacity_ = o; } + void SetTexture(Texture* val) { + if (val && !val->IsFromUIContext()) + throw Exception("texture is not from the UI context: " + + val->GetObjectDescription()); + texture_ = val; + } + void SetTintTexture(Texture* val) { + if (val && !val->IsFromUIContext()) + throw Exception("texture is not from the UI context: " + + val->GetObjectDescription()); + tint_texture_ = val; + } + void SetMaskTexture(Texture* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("texture is not from the UI context: " + + val->GetObjectDescription()); + } + mask_texture_ = val; + } + void SetModelTransparent(Model* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("model_transparent is not from UI context"); + } + image_dirty_ = true; + model_transparent_ = val; + } + void SetModelOpaque(Model* val) { + if (val && !val->IsFromUIContext()) { + throw Exception("model_opaque is not from UI context"); + } + image_dirty_ = true; + model_opaque_ = val; + } + auto GetWidgetTypeName() -> std::string override { return "image"; } + void set_transition_delay(float val) { transition_delay_ = val; } + void set_tilt_scale(float s) { tilt_scale_ = s; } + void set_radial_amount(float val) { radial_amount_ = val; } + + private: + float tilt_scale_{1.0f}; + float transition_delay_{}; + millisecs_t birth_time_{}; + Object::Ref texture_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref model_transparent_; + Object::Ref model_opaque_; + Object::Ref radial_mesh_; + float image_width_{}; + float image_height_{}; + float image_center_x_{}; + float image_center_y_{}; + float radial_amount_{1.0f}; + bool image_dirty_{true}; + float width_{50.0f}; + float height_{30.0f}; + bool has_alpha_channel_{true}; + float color_red_{1.0f}; + float color_green_{1.0f}; + float color_blue_{1.0f}; + float tint_color_red_{1.0f}; + float tint_color_green_{1.0f}; + float tint_color_blue_{1.0f}; + float tint2_color_red_{1.0f}; + float tint2_color_green_{1.0f}; + float tint2_color_blue_{1.0f}; + float opacity_{1.0f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_UI_WIDGET_IMAGE_WIDGET_H_ diff --git a/src/ballistica/ui/widget/root_widget.cc b/src/ballistica/ui/widget/root_widget.cc new file mode 100644 index 00000000..af5738f9 --- /dev/null +++ b/src/ballistica/ui/widget/root_widget.cc @@ -0,0 +1,1138 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui/widget/root_widget.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/input/input.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/button_widget.h" +#include "ballistica/ui/widget/image_widget.h" +#include "ballistica/ui/widget/stack_widget.h" + +namespace ballistica { + +// color we mult toolbars by in medium and large ui modes +// (in small mode we keep them more the normal window color since everything +// overlaps) +#define TOOLBAR_COLOR_R 0.75f +#define TOOLBAR_COLOR_G 0.85f +#define TOOLBAR_COLOR_B 0.85f + +#define TOOLBAR_BACK_COLOR_R 0.8f +#define TOOLBAR_BACK_COLOR_G 0.8f +#define TOOLBAR_BACK_COLOR_B 0.8f + +// opacity in med/large +#define TOOLBAR_OPACITY 1.0f + +// opacity in small +#define TOOLBAR_OPACITY_2 1.0f + +#define BOT_LEFT_COLOR_R 0.6 +#define BOT_LEFT_COLOR_G 0.6 +#define BOT_LEFT_COLOR_B 0.8 + +// for defining toolbar buttons. +struct RootWidget::ButtonDef { + float h_align{}; + VAlign v_align{VAlign::kTop}; + float x{}; + float y{}; + float width{100.0f}; + float height{30.0f}; + float scale{1.0f}; + float depth_min{}; + float depth_max{1.0f}; + std::string label; + std::string img; + std::string model_transparent; + std::string model_opaque; + Python::ObjID call{Python::ObjID::kEmptyCall}; + float color_r{1.0f}; + float color_g{1.0f}; + float color_b{1.0f}; + float opacity{1.0f}; + bool selectable{true}; + uint32_t visibility_mask{}; +}; + +struct RootWidget::Button { + Object::Ref widget; + float h_align{}; + VAlign v_align{VAlign::kTop}; + float x{}; // user provided x + float y{}; // user provided y + float x_target{}; // final target x (accounting for visibility, etc) + float y_target{}; // final target y (accounting for visibility, etc) + float x_smoothed{}; // current x (on way to target) + float y_smoothed{}; // current y (on way to target) + float width{100.0f}; + float height{30.0f}; + float scale{1.0f}; + bool selectable{true}; + int visibility_mask{}; +}; + +// for adding text label decorations to buttons +struct RootWidget::TextDef { + Button* button = nullptr; + float x = 0.0f; + float y = 0.0f; + float width = -1.0f; + float scale = 1.0f; + float depth_min = 0.0f; + float depth_max = 1.0f; + float color_r = 1.0f; + float color_g = 1.0f; + float color_b = 1.0f; + float color_a = 1.0f; + float flatness = 0.5f; + float shadow = 0.5f; + std::string text; +}; + +struct RootWidget::Text { + Button* button{}; + Object::Ref widget; + float x{}; + float y{}; +}; + +RootWidget::RootWidget() { + // we enable a special 'single-depth-root' mode + // in which we use most of our depth range for our first child + // (our screen stack) and the small remaining bit for the rest + set_single_depth(true); + set_single_depth_root(true); + set_background(false); +} + +RootWidget::~RootWidget() = default; + +auto RootWidget::AddCover(float h_align, VAlign v_align, float x, float y, + float w, float h, float o) -> RootWidget::Button* { + // currently just not doing these in vr mode + if (IsVRMode()) { + return nullptr; + } + + ButtonDef bd; + bd.h_align = h_align; + bd.v_align = v_align; + bd.width = w; + bd.height = h; + bd.x = x; + bd.y = y; + bd.img = "softRect"; + bd.selectable = false; + bd.color_r = 0.0f; + bd.color_g = 0.0f; + bd.color_b = 0.0f; + bd.opacity = o; + bd.call = Python::ObjID::kEmptyCall; + + bd.visibility_mask = + static_cast(Widget::ToolbarVisibility::kMenuFullRoot); + // when the user specifies no backing it means they intend to cover the screen + // with a flat-ish window texture.. however this only applies to phone-size; + // for other sizes we always draw a backing. + if (GetInterfaceType() != UIScale::kSmall) { + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuFull); + } + + Button* b = AddButton(bd); + return b; +} + +void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, + float b, bool plus, const std::string& s) { + float yoffs = (GetInterfaceType() == UIScale::kSmall) ? 0.0f : -7.0f; + + float width = type == 1 ? 80.0f : 110.0f; + // bar + { + ButtonDef bd; + bd.h_align = h_align; + bd.v_align = VAlign::kTop; + bd.width = width; + bd.height = 36.0f; + bd.x = x; + bd.y = -36.0f + 10.0f + yoffs; + bd.img = "uiAtlas2"; + bd.model_transparent = "currencyMeter"; + bd.selectable = false; + bd.color_r = 0.32f; + bd.color_g = 0.30f; + bd.color_b = 0.4f; + if (GetInterfaceType() != UIScale::kSmall) { + bd.color_r *= TOOLBAR_COLOR_R; + bd.color_g *= TOOLBAR_COLOR_G; + bd.color_b *= TOOLBAR_COLOR_B; + } + bd.depth_min = 0.3f; + bd.call = Python::ObjID::kEmptyCall; + bd.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + + // show in currency mode + if (type == 2 || type == 3) { + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuCurrency); + } + Button* btn = AddButton(bd); + + // Bar value text. + { + TextDef td; + td.button = btn; + td.width = bd.width * 0.7f; + td.text = s; + td.scale = 0.8f; + td.flatness = 1.0f; + td.shadow = 1.0f; + td.depth_min = 0.3f; + AddText(td); + } + } + // Icon on left. + { + ButtonDef bd; + bd.h_align = h_align; + bd.v_align = VAlign::kTop; + bd.width = bd.height = 50.0f; + if (type == 0 || type == 1) { + bd.x = x - width * 0.5f - 10.0f; + } else { + bd.x = x + width * 0.5f + 10.0f; + } + bd.y = -32.0f + 7.0f + yoffs; + bd.color_r = r; + bd.color_g = g; + bd.color_b = b; + bd.depth_min = 0.3f; + switch (type) { + case 0: + bd.img = "levelIcon"; + bd.call = Python::ObjID::kLevelIconPressCall; + break; + case 1: + bd.img = "trophy"; + bd.call = Python::ObjID::kTrophyIconPressCall; + break; + case 2: + bd.img = "coin"; + bd.call = Python::ObjID::kCoinIconPressCall; + break; + case 3: + bd.img = "tickets"; + bd.call = Python::ObjID::kTicketIconPressCall; + break; + default: + break; + } + bd.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + // show in currency mode + if (type == 2 || type == 3) { + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuCurrency); + } + Button* btn = AddButton(bd); + switch (type) { // NOLINT + case 3: + tickets_info_button_ = btn; + break; + default: + break; + } + + // Level num. + if (type == 0) { + TextDef td; + td.button = btn; + td.width = bd.width * 0.8f; + td.text = "12"; + td.x = -1.6f; + td.y = 0.8f; + td.scale = 0.9f; + td.flatness = 1.0f; + td.shadow = 1.0f; + td.depth_min = 0.3f; + td.color_r = 1.0f; + td.color_g = 1.0f; + td.color_b = 1.0f; + AddText(td); + } + } + // plus button + if (plus) { + ButtonDef bd; + bd.h_align = h_align; + bd.v_align = VAlign::kTop; + bd.width = bd.height = 45.0f; + // bd.x = x + 72; + bd.x = x - 68; + bd.y = -36.0f + 11.0f + yoffs; + bd.img = "uiAtlas2"; + bd.model_transparent = "currencyPlusButton"; + bd.color_r = 0.35f; + bd.color_g = 0.35f; + bd.color_b = 0.55f; + if (GetInterfaceType() != UIScale::kSmall) { + bd.color_r *= TOOLBAR_COLOR_R; + bd.color_g *= TOOLBAR_COLOR_G; + bd.color_b *= TOOLBAR_COLOR_B; + } + bd.depth_min = 0.3f; + bd.call = Python::ObjID::kEmptyCall; + bd.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + + // Show in currency mode. + if (type == 2 || type == 3) { + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuCurrency); + } + Button* btn = AddButton(bd); + if (type == 3) { + tickets_plus_button_ = btn; + } + } +} + +void RootWidget::Setup() { +#if BA_TOOLBAR_TEST + + // back button + { + ButtonDef bd; + bd.h_align = 0.0f; + bd.v_align = VAlign::kTop; + bd.width = bd.height = 140.0f; + bd.color_r = 0.7f; + bd.color_g = 0.4f; + bd.color_b = 0.35f; + + bd.x = 40.0f; + bd.y = -40.0f; + bd.img = "nub"; + bd.call = Python::ObjID::kBackButtonPressCall; + bd.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuMinimal) + | static_cast(Widget::ToolbarVisibility::kMenuFull)); + Button* b = back_button_ = AddButton(bd); + + // clan + { + TextDef td; + td.button = b; + td.x = 5.0f; + td.y = 3.0f; + td.width = bd.width * 0.9f; + td.text = g_game->CharStr(SpecialChar::kBack); + td.color_a = 1.0f; + td.scale = 2.0f; + td.flatness = 0.0f; + td.shadow = 0.5f; + AddText(td); + } + } + + // widen this a bit in small mode so it just covers most of the top + // - that looks funny in medium/large mode though + // if (GetInterfaceType() == UIScale::kSmall) { + // AddCover(0.5f, VAlign::kTop, 0.0f, 320.0f, + // GetInterfaceType() == UIScale::kSmall ? 1000.0f : + // 1000.0f, 800.0f, 0.4f); + // } + // if (c) { + // c->visibility_mask |= + // static_cast(Widget::ToolbarVisibility::kMenuCurrency); + // } + + // top bar backing (currency only) + if (false) { + ButtonDef bd; + bd.h_align = 0.5f; + bd.v_align = VAlign::kTop; + bd.width = 370.0f; + // if (GetInterfaceType() != UIScale::kSmall) { + // bd.width = 950.0f; + // } + bd.height = 90.0f; + bd.x = 256.0f; + bd.y = -20.0f; + bd.img = "uiAtlas2"; + // if (GetInterfaceType() != UIScale::kSmall) { + // bd.model_transparent = "toolbarBackingTop"; + // } else { + bd.model_transparent = "toolbarBackingTop2"; + // } + bd.selectable = false; + bd.color_r = 0.44f; + bd.color_g = 0.41f; + bd.color_b = 0.56f; + bd.opacity = 1.0f; + // if (GetInterfaceType() != UIScale::kSmall) { + // bd.color_r *= TOOLBAR_COLOR_R; + // bd.color_g *= TOOLBAR_COLOR_G; + // bd.color_b *= TOOLBAR_COLOR_B; + // bd.opacity *= TOOLBAR_OPACITY; + // } else { + // bd.opacity *= TOOLBAR_OPACITY_2; + // } + bd.depth_min = 0.2f; + // bd.call = ""; + bd.call = Python::ObjID::kEmptyCall; + + // bd.visibility_mask = + // static_cast(Widget::ToolbarVisibility::kMenuFullRoot); + // bd.visibility_mask |= + // static_cast(Widget::ToolbarVisibility::kMenuFull); + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuCurrency); + AddButton(bd); + } + + // top bar backing + if (false) { + ButtonDef bd; + bd.h_align = 0.5f; + bd.v_align = VAlign::kTop; + bd.width = 850.0f; + if (GetInterfaceType() != UIScale::kSmall) { + bd.width = 850.0f; + } + bd.height = 90.0f; + bd.x = 0.0f; + bd.y = -20.0f; + bd.img = "uiAtlas2"; + if (GetInterfaceType() != UIScale::kSmall) { + bd.model_transparent = "toolbarBackingTop2"; + } else { + bd.model_transparent = "toolbarBackingTop2"; + } + bd.selectable = false; + bd.color_r = 0.44f; + bd.color_g = 0.41f; + bd.color_b = 0.56f; + bd.opacity = 1.0f; + if (GetInterfaceType() != UIScale::kSmall) { + bd.color_r *= TOOLBAR_COLOR_R * TOOLBAR_BACK_COLOR_R; + bd.color_g *= TOOLBAR_COLOR_G * TOOLBAR_BACK_COLOR_G; + bd.color_b *= TOOLBAR_COLOR_B * TOOLBAR_BACK_COLOR_B; + bd.opacity *= TOOLBAR_OPACITY; + } else { + bd.opacity *= TOOLBAR_OPACITY_2; + } + bd.depth_min = 0.2f; + // bd.call = ""; + bd.call = Python::ObjID::kEmptyCall; + bd.visibility_mask = + static_cast(Widget::ToolbarVisibility::kMenuFullRoot); + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuFull); + // bd.visibility_mask |= + // static_cast(Widget::ToolbarVisibility::kMenuCurrency); + AddButton(bd); + } + + float yoffs = (GetInterfaceType() == UIScale::kSmall) ? 0.0f : -10.0f; + + // account button + { + ButtonDef bd; + bd.h_align = 0.1f; + bd.v_align = VAlign::kTop; + bd.width = 160.0f; + bd.height = 60.0f; + bd.depth_min = 0.3f; + bd.x = (GetInterfaceType() == UIScale::kSmall) ? 100.0f : -50.0f; + bd.y = -24.0f + yoffs; + bd.color_r = 0.56f; + bd.color_g = 0.5f; + bd.color_b = 0.73f; + if (GetInterfaceType() != UIScale::kSmall) { + bd.color_r *= TOOLBAR_COLOR_R; + bd.color_g *= TOOLBAR_COLOR_G; + bd.color_b *= TOOLBAR_COLOR_B; + } + // bd.call = ""; + bd.call = Python::ObjID::kEmptyCall; + bd.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + + // on desktop, stick this in the top left corner + // if (GetInterfaceType() == UIScale::kLarge) { + // bd.h_align = 0.0f; + // bd.x = 120.0f; + // } + + Button* b = account_button_ = AddButton(bd); + + // player name + { + TextDef td; + td.button = b; + td.y = 9.0f; + td.width = bd.width * 0.9f; + td.text = "Player Name"; + td.scale = 1.2f; + td.depth_min = 0.3f; + td.color_r = 0.5f; + td.color_g = 0.8f; + td.color_b = 0.8f; + td.shadow = 1.0f; + AddText(td); + } + // clan + { + TextDef td; + td.button = b; + td.y = -12.0f; + td.width = bd.width * 0.9f; + td.depth_min = 0.3f; + td.text = "Clan Name"; + td.color_a = 0.6f; + td.scale = 0.6f; + td.flatness = 1.0f; + td.shadow = 0.0f; + AddText(td); + } + } + + float anchorx = (GetInterfaceType() == UIScale::kSmall) ? 0.3f : 0.25f; + + AddMeter(anchorx, 200.0f - 148.0f, 0, 1.0f, 1.0f, 1.0f, false, "456/1000"); + AddMeter(anchorx, 200.0f, 1, 1.0f, 1.0f, 1.0f, false, "123"); + + AddMeter(0.7f, -100.0f, 2, 1.0f, 1.0f, 1.0f, true, "12343"); + AddMeter(0.7f, -100.0f + 188.0f, 3, 1.0f, 1.0f, 1.0f, true, "123"); + + // party button + { + ButtonDef b; + b.h_align = 1.0f; + b.v_align = VAlign::kTop; + b.width = b.height = 70.0f; + b.x = -110.0f; + b.y = b.height * -0.41f; + b.img = "usersButton"; + b.call = Python::ObjID::kFriendsButtonPressCall; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kInGame) + | static_cast(Widget::ToolbarVisibility::kMenuMinimal) + | static_cast(Widget::ToolbarVisibility::kMenuMinimalNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuCurrency) + | static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + party_button_ = AddButton(b); + } + + // menu button (only shows up when we're not in a menu) + // FIXME - this should never be visible on TV or VR UI modes + { + ButtonDef b; + b.h_align = 1.0f; + b.v_align = VAlign::kTop; + b.width = b.height = 65.0f; + b.x = -36.0f; + b.y = b.height * -0.48f; + b.img = "menuButton"; + b.call = Python::ObjID::kBackButtonPressCall; + b.color_r = 0.3f; + b.color_g = 0.5f; + b.color_b = 0.2f; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kInGame) + | static_cast(Widget::ToolbarVisibility::kMenuMinimal) + | static_cast(Widget::ToolbarVisibility::kMenuMinimalNoBack) + | static_cast(Widget::ToolbarVisibility::kMenuCurrency) + | static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + menu_button_ = AddButton(b); + } + + // bot-left cover + // AddCover(0.0f, VAlign::kBottom, 0.0f, -210.0f, 600.0f, 600.0f, 0.25f); + + float bx = 45.0f; + + // log button + { + ButtonDef b; + b.h_align = 0.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 50.0f; + b.x = bx; + b.y = b.height * 0.5f + 5; + b.color_r = BOT_LEFT_COLOR_R; + b.color_g = BOT_LEFT_COLOR_G; + b.color_b = BOT_LEFT_COLOR_B; + b.img = "logIcon"; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + AddButton(b); + } + + bx += 70.0f; + + // achievements button + { + ButtonDef b; + b.h_align = 0.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 50.0f; + b.x = bx; + b.y = b.height * 0.5f + 5; + b.color_r = BOT_LEFT_COLOR_R; + b.color_g = BOT_LEFT_COLOR_G; + b.color_b = BOT_LEFT_COLOR_B; + b.img = "achievementsIcon"; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + AddButton(b); + } + + bx += 70.0f; + + // leaderboards button + { + ButtonDef b; + b.h_align = 0.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 50.0f; + b.x = bx; + b.y = b.height * 0.5f + 5; + b.color_r = BOT_LEFT_COLOR_R; + b.color_g = BOT_LEFT_COLOR_G; + b.color_b = BOT_LEFT_COLOR_B; + b.img = "leaderboardsIcon"; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + AddButton(b); + } + + bx += 70.0f; + + // settings button + { + ButtonDef b; + b.h_align = 0.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 50.0f; + b.x = bx; + b.y = b.height * 0.58f; + b.color_r = BOT_LEFT_COLOR_R; + b.color_g = BOT_LEFT_COLOR_G; + b.color_b = BOT_LEFT_COLOR_B; + b.img = "settingsIcon"; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + settings_button_ = AddButton(b); + } + + // chests + { + // AddCover(0.5f, VAlign::kBottom, 0.0f, -180.0f, 600.0f, 550.0f, 0.35f); + + float backingR = 0.44f; + float backingG = 0.41f; + float backingB = 0.56f; + float backingCoverR = backingR; + float backingCoverG = backingG; + float backingCoverB = backingB; + float backingA = 1.0f; + if (GetInterfaceType() != UIScale::kSmall) { + backingR *= TOOLBAR_COLOR_R * TOOLBAR_BACK_COLOR_R; + backingG *= TOOLBAR_COLOR_G * TOOLBAR_BACK_COLOR_G; + backingB *= TOOLBAR_COLOR_B * TOOLBAR_BACK_COLOR_B; + backingCoverR *= TOOLBAR_COLOR_R; + backingCoverG *= TOOLBAR_COLOR_G; + backingCoverB *= TOOLBAR_COLOR_B; + backingA *= TOOLBAR_OPACITY; + } else { + backingR *= 1.1f; + backingG *= 1.1f; + backingB *= 1.1f; + backingCoverR *= 1.1f; + backingCoverG *= 1.1f; + backingCoverB *= 1.1f; + backingA *= TOOLBAR_OPACITY_2; + } + + // bar backing + { + ButtonDef bd; + bd.h_align = 0.5f; + bd.v_align = VAlign::kBottom; + bd.width = 550.0f; + bd.height = 110.0f; + bd.x = 0.0f; + bd.y = 41.0f; + bd.img = "uiAtlas2"; + if (GetInterfaceType() != UIScale::kSmall) { + bd.model_transparent = "toolbarBackingBottom2"; + } else { + bd.model_transparent = "toolbarBackingBottom2"; + } + bd.selectable = false; + bd.color_r = backingR; + bd.color_g = backingG; + bd.color_b = backingB; + bd.opacity = backingA; + + bd.depth_min = 0.2f; + // bd.call = ""; + bd.call = Python::ObjID::kEmptyCall; + bd.visibility_mask = + static_cast(Widget::ToolbarVisibility::kMenuFullRoot); + bd.visibility_mask |= + static_cast(Widget::ToolbarVisibility::kMenuFull); + + AddButton(bd); + } + + ButtonDef b; + b.h_align = 0.5f; + b.v_align = VAlign::kBottom; + b.width = b.height = 110.0f; + b.x = 0.0f; + b.y = b.height * 0.4f; + b.img = "chestIcon"; + b.depth_min = 0.3f; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + float spacing = 110.0f; + b.x = -2.0f * spacing; + AddButton(b); + + b.x = -1.0f * spacing; + b.img = "chestOpenIcon"; + b.y = b.height * 0.5f; + AddButton(b); + + // test - empty icons + b.y = b.height * 0.4f; + b.x = 0.0f; + b.img = "chestIconEmpty"; + b.width = b.height = 80.0f; + b.color_r = backingCoverR; + b.color_g = backingCoverG; + b.color_b = backingCoverB; + b.opacity = 1.0f; + AddButton(b); + b.x = 1.0f * spacing; + AddButton(b); + b.x = 2.0f * spacing; + + // test - multi-icon tile + b.img = "chestIconMulti"; + AddButton(b); + } + + // bot-right cover + // AddCover(1.0f, VAlign::kBottom, 0.0f, -210.0f, 600.0f, 600.0f, 0.25f); + + // // settings button + // { + // ButtonDef b; + // b.h_align = 1.0f; + // b.v_align = VAlign::kBottom; + // b.width = b.height = 50.0f; + // b.x = -225.0f; + // b.y = b.height * 0.5f + 10; + // b.img = "settingsIcon"; + // b.visibility_mask = + // (static_cast(Widget::ToolbarVisibility::kMenuFull) + // | + // static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + // AddButton(b); + // } + + // store button + { + ButtonDef b; + b.h_align = 1.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 85.0f; + b.x = -206.0f; + b.y = b.height * 0.5f; + b.img = "storeIcon"; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + AddButton(b); + } + + // inventory button + { + ButtonDef b; + b.h_align = 1.0f; + b.v_align = VAlign::kBottom; + b.width = b.height = 135.0f; + b.x = -80.0f; + b.y = b.height * 0.45f; + b.img = "inventoryIcon"; + b.visibility_mask = + (static_cast(Widget::ToolbarVisibility::kMenuFull) + | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); + AddButton(b); + } + +#endif // BA_TOOLBAR_TEST + + UpdateForFocusedWindow(nullptr); +} + +void RootWidget::Draw(RenderPass* pass, bool transparent) { + // Opaque pass gets drawn first; use that as an opportunity to step up our + // motion. + if (!transparent) { + millisecs_t current_time = pass->frame_def()->base_time(); + float time_diff = std::min(millisecs_t{100}, current_time - update_time_); + StepPositions(time_diff); + update_time_ = current_time; + } + ContainerWidget::Draw(pass, transparent); +} + +auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { + ScopedSetContext cp(g_game->GetUIContextTarget()); + buttons_.emplace_back(); + Button& b(buttons_.back()); + b.x = b.x_smoothed = b.x_target = def.x; + b.y = b.y_smoothed = b.y_target = def.y; + b.visibility_mask = def.visibility_mask; + b.scale = def.scale; + b.width = def.width; + b.height = def.height; + b.h_align = def.h_align; + b.v_align = def.v_align; + b.selectable = def.selectable; + b.widget = Object::New(); + b.widget->SetColor(def.color_r, def.color_g, def.color_b); + b.widget->set_opacity(def.opacity); + b.widget->set_auto_select(true); + b.widget->SetText(def.label); + b.widget->set_enabled(def.selectable); + b.widget->set_selectable(def.selectable); + b.widget->SetDepthRange(def.depth_min, def.depth_max); + + // make sure up/down moves focus into the main stack + assert(screen_stack_widget_ != nullptr); + assert(b.v_align != VAlign::kCenter); + if (b.v_align == VAlign::kTop) { + b.widget->set_down_widget(screen_stack_widget_); + } else { + b.widget->set_up_widget(screen_stack_widget_); + } + // we wanna prevent anyone from redirecting these to point to outside widgets + // since we'll probably outlive those outside widgets + b.widget->set_neighbors_locked(true); + + if (!def.img.empty()) { + b.widget->SetTexture(g_ui->GetTexture(def.img).get()); + } + if (!def.model_transparent.empty()) { + b.widget->SetModelTransparent(g_ui->GetModel(def.model_transparent).get()); + } + if (!def.model_opaque.empty()) { + b.widget->SetModelOpaque(g_ui->GetModel(def.model_opaque).get()); + } + if (Python::ObjID::kEmptyCall != def.call) { + b.widget->set_on_activate_call(g_python->obj(def.call).get()); + } + AddWidget(b.widget.get()); + return &b; +} + +auto RootWidget::AddText(const TextDef& def) -> RootWidget::Text* { + ScopedSetContext cp(g_game->GetUIContextTarget()); + texts_.emplace_back(); + Text& t(texts_.back()); + t.button = def.button; + t.widget = Object::New(); + t.widget->SetWidth(0.0f); + t.widget->SetHeight(0.0f); + t.widget->set_halign(TextWidget::HAlign::kCenter); + t.widget->set_valign(TextWidget::VAlign::kCenter); + t.widget->SetText(def.text); + t.widget->set_max_width(def.width); + t.widget->set_center_scale(def.scale); + t.widget->set_color(def.color_r, def.color_g, def.color_b, def.color_a); + t.widget->set_shadow(def.shadow); + t.widget->set_flatness(def.flatness); + t.widget->SetDepthRange(def.depth_min, def.depth_max); + assert(def.button->widget.exists()); + t.widget->set_draw_control_parent(def.button->widget.get()); + t.x = def.x; + t.y = def.y; + AddWidget(t.widget.get()); + return &t; +} + +void RootWidget::UpdateForFocusedWindow() { + UpdateForFocusedWindow( + screen_stack_widget_ != nullptr + ? screen_stack_widget_->GetTopmostToolbarInfluencingWidget() + : nullptr); +} + +void RootWidget::UpdateForFocusedWindow(Widget* widget) { + // Take note if the current session is the main menu; we do a few things + // differently there. + HostSession* s = g_game->GetForegroundContext().GetHostSession(); + in_main_menu_ = (s ? s->is_main_menu() : false); + + if (widget == nullptr) { + toolbar_visibility_ = ToolbarVisibility::kInGame; + } else { + toolbar_visibility_ = widget->toolbar_visibility(); + } + MarkForUpdate(); +} + +void RootWidget::StepPositions(float dt) { + if (!positions_dirty_) { + return; + } + + // Go through our buttons updating their target points and smooth values. + // If everything has arrived at its target point, mark us as not dirty. + bool have_dirty = false; + for (Button& b : buttons_) { + // Update our target position. + b.x_target = b.x; + b.y_target = b.y; + float disable_offset = + 110.0f * ((b.v_align == VAlign::kTop) ? 1.0f : -1.0f); + // float top_right_offset = 100.0f; + + // Can turn this down to debug visibility. + if (explicit_bool(false)) { + disable_offset *= 0.5f; + // top_right_offset *= 0.5f; + } + bool enable_button = + static_cast(static_cast(toolbar_visibility_) + & static_cast(b.visibility_mask)); + + // when we're in the main menu, always disable the menu button + // and shift the party button a bit to the right + if (in_main_menu_) { + if (&b == menu_button_) { + enable_button = false; + } + if (&b == party_button_) { + b.x_target += 70.0f; + } + } + if (&b == back_button_) { + // back button is always disabled in medium/large UI + if (GetInterfaceType() != UIScale::kSmall) { + enable_button = false; + } + + // whenever back button is enabled, left on account button should go to + // it; otherwise it goes nowhere. + Widget* ab = account_button_->widget.get(); + ab->set_neighbors_locked(false); + ab->set_left_widget(enable_button ? back_button_->widget.get() : ab); + account_button_->widget->set_neighbors_locked(true); + } + + if (!enable_button) { + b.y_target += disable_offset; + } + + // special case: we shift buttons on the top right to the right if the menu + // button is hidden (and also if the button is hidden; otherwise things come + // in diagonally) + // if (b.h_align == HAlign::kRight and b.v_align == VAlign::kTop + // if (b.h_align >= 1.0f and b.v_align == VAlign::kTop + // and (toolbar_visibility_ != ToolbarVisibility::kInGame or not + // enable_button)) { + // b.x_target += top_right_offset; + // } + + // Now push our smooth value towards our target value... + b.x_smoothed += (b.x_target - b.x_smoothed) * 0.015f * dt; + b.y_smoothed += (b.y_target - b.y_smoothed) * 0.015f * dt; + + // Snap in place once we reach the target; otherwise note + // that we need to keep going. + if (std::abs(b.x_target - b.x_smoothed) < 0.1f + && std::abs(b.y_target - b.y_smoothed) < 0.1f) { + b.x_smoothed = b.x_target; + b.y_smoothed = b.y_target; + + // Also flip off visibility if we're moving offscreen and have reached our + // target. + if (!enable_button) { + b.widget->set_visible_in_container(false); + } + } else { + have_dirty = true; + // Always remain visible while still moving. + b.widget->set_visible_in_container(true); + } + + // Now calc final abs x and y based on screen size, smoothed positions, etc. + float x, y; + x = width() * b.h_align + + base_scale_ * (b.x_smoothed - b.width * b.scale * 0.5f); + switch (b.v_align) { + case VAlign::kTop: + y = height() + base_scale_ * (b.y_smoothed - b.height * b.scale * 0.5f); + break; + case VAlign::kCenter: + y = height() * 0.5f + + base_scale_ * (b.y_smoothed - b.height * b.scale * 0.5f); + break; + case VAlign::kBottom: + y = base_scale_ * (b.y_smoothed - b.height * b.scale * 0.5f); + break; + } + b.widget->set_selectable(enable_button && b.selectable); + b.widget->set_enabled(enable_button && b.selectable); + b.widget->set_translate(x, y); + b.widget->set_width(b.width); + b.widget->set_height(b.height); + b.widget->set_scale(b.scale * base_scale_); + } + + for (Text& t : texts_) { + // Move the text widget to wherever its target button is (plus offset). + Button* b = t.button; + float x = + b->widget->tx() + base_scale_ * b->scale * (b->width * 0.5f + t.x); + float y = + b->widget->ty() + base_scale_ * b->scale * (b->height * 0.5f + t.y); + t.widget->set_translate(x, y); + t.widget->set_scale(base_scale_ * b->scale); + } + + positions_dirty_ = have_dirty; +} + +void RootWidget::UpdateLayout() { + // Now actually put things in place. + base_scale_ = 1.0f; + switch (GetInterfaceType()) { + case UIScale::kLarge: + base_scale_ = 0.6f; + break; + case UIScale::kMedium: + base_scale_ = 0.8f; + break; + default: + base_scale_ = 1.0f; + break; + } + + // TEST - cycle through our scales +#if 0 + { + int foo = time(nullptr) % 3; + if (foo == 0) { + base_scale_ = 1.0f; + } else if (foo == 1) { + base_scale_ = 0.75f; + } else { + base_scale_ = 0.5f; + } + } +#endif + + // Update the window stack. + BA_DEBUG_UI_READ_LOCK; + if (screen_stack_widget_ != nullptr) { + screen_stack_widget_->set_translate(0, 0); + screen_stack_widget_->SetWidth(width()); + screen_stack_widget_->SetHeight(height()); + } + if (overlay_stack_widget_ != nullptr) { + overlay_stack_widget_->set_translate(0, 0); + overlay_stack_widget_->SetWidth(width()); + overlay_stack_widget_->SetHeight(height()); + } + positions_dirty_ = true; + + // Run an immediate step to update things; (avoids jumpy positions if + // resizing game window)) + StepPositions(0.0f); +} + +auto RootWidget::HandleMessage(const WidgetMessage& m) -> bool { + // If a cancel message comes through and our back button is active, fire our + // back button. + // ..in all other cases just do the default. + if (m.type == WidgetMessage::Type::kCancel && back_button_ != nullptr + && back_button_->widget->enabled() + && !overlay_stack_widget_->HasChildren()) { + back_button_->widget->Activate(); + return true; + } else { + return ContainerWidget::HandleMessage(m); + } +} + +void RootWidget::SetScreenWidget(StackWidget* w) { + // this needs to happen before any buttons get added.. + assert(buttons_.empty()); + AddWidget(w); + screen_stack_widget_ = w; +} + +void RootWidget::SetOverlayWidget(StackWidget* w) { + // this needs to happen after our buttons and things get added.. +#if BA_TOOLBAR_TEST + assert(!buttons_.empty()); +#endif // BA_TOOLBAR_TEST + AddWidget(w); + overlay_stack_widget_ = w; +} + +void RootWidget::OnCancelCustom() { + // if we've got any toolbar buttons selected and hit cancel, flip back to the + // main screen stack SelectWidget(screen_stack_widget_); cout << "ROOT CUSTOM + // CANCEL" << endl; + g_input->HandleBackPress(true); +} + +auto RootWidget::GetSpecialWidget(const std::string& s) const -> Widget* { + if (s == "party_button") { + return party_button_ ? party_button_->widget.get() : nullptr; + } else if (s == "tickets_plus_button") { + return tickets_plus_button_ ? tickets_plus_button_->widget.get() : nullptr; + } else if (s == "back_button") { + return back_button_ ? back_button_->widget.get() : nullptr; + } else if (s == "account_button") { + return account_button_ ? account_button_->widget.get() : nullptr; + } else if (s == "settings_button") { + return settings_button_ ? settings_button_->widget.get() : nullptr; + } else if (s == "tickets_info_button") { + return tickets_info_button_ ? tickets_info_button_->widget.get() : nullptr; + } else if (s == "overlay_stack") { + return overlay_stack_widget_; + } + return nullptr; +} + +} // namespace ballistica diff --git a/src/ballistica/ui/widget/root_widget.h b/src/ballistica/ui/widget/root_widget.h new file mode 100644 index 00000000..3949f373 --- /dev/null +++ b/src/ballistica/ui/widget/root_widget.h @@ -0,0 +1,68 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_WIDGET_ROOT_WIDGET_H_ +#define BALLISTICA_UI_WIDGET_ROOT_WIDGET_H_ + +#include +#include + +#include "ballistica/ui/widget/container_widget.h" + +namespace ballistica { + +// Root-level widget; contains a top-bar, screen-stack, bottom-bar, menu-button, +// etc. This is intended to replace RootUI. +class RootWidget : public ContainerWidget { + public: + RootWidget(); + ~RootWidget() override; + auto GetWidgetTypeName() -> std::string override { return "root"; } + void SetScreenWidget(StackWidget* w); + void SetOverlayWidget(StackWidget* w); + void UpdateForFocusedWindow(); + void Setup(); + auto HandleMessage(const WidgetMessage& m) -> bool override; + void Draw(RenderPass* pass, bool transparent) override; + auto GetSpecialWidget(const std::string& s) const -> Widget*; + auto base_scale() const -> float { return base_scale_; } + auto overlay_window_stack() const -> StackWidget* { + return overlay_stack_widget_; + } + + private: + struct ButtonDef; + struct Button; + struct TextDef; + struct Text; + enum class VAlign { kTop, kCenter, kBottom }; + void UpdateForFocusedWindow(Widget* widget); + void OnCancelCustom() override; + void UpdateLayout() override; + auto AddButton(const ButtonDef& def) -> Button*; + auto AddText(const TextDef& def) -> Text*; + void StepPositions(float dt); + void AddMeter(float h_align, float x, int type, float r, float g, float b, + bool plus, const std::string& s); + auto AddCover(float h_align, VAlign v_align, float x, float y, float w, + float h, float o) -> Button*; + StackWidget* screen_stack_widget_{}; + StackWidget* overlay_stack_widget_{}; + float base_scale_{1.0f}; + std::list