init
This commit is contained in:
commit
87bff164f1
416 files changed
+279512
No files matched your search
+20
@@ -0,0 +1,20 @@
|
|||||||
|
build/
|
||||||
|
build-cpu/
|
||||||
|
__pycache__/
|
||||||
|
analysis/atlas/*.sqlite
|
||||||
|
analysis/atlas/*.jsonl
|
||||||
|
analysis/*.log
|
||||||
|
third_party/luajit/src/*.lib
|
||||||
|
third_party/luajit/src/*.exe
|
||||||
|
third_party/luajit/src/*.exp
|
||||||
|
third_party/luajit/src/*.obj
|
||||||
|
third_party/luajit/src/*.manifest
|
||||||
|
third_party/luajit/src/buildvm_*.h
|
||||||
|
third_party/luajit/src/jit/vmdef.lua
|
||||||
|
demo/world_save.bin
|
||||||
|
demo/world_save.bin
|
||||||
|
clips/
|
||||||
|
build_audio32/
|
||||||
|
|
||||||
|
tools/spell_lab_auto/jobs/all/_todo_*
|
||||||
|
demo
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
[submodule "third_party/c-blosc2"]
|
||||||
|
path = third_party/c-blosc2
|
||||||
|
url = https://github.com/Blosc/c-blosc2.git
|
||||||
|
[submodule "third_party/lz4"]
|
||||||
|
path = third_party/lz4
|
||||||
|
url = https://github.com/lz4/lz4.git
|
||||||
|
[submodule "third_party/libtess2"]
|
||||||
|
path = third_party/libtess2
|
||||||
|
url = https://github.com/memononen/libtess2.git
|
||||||
|
[submodule "third_party/zstd"]
|
||||||
|
path = third_party/zstd
|
||||||
|
url = https://github.com/facebook/zstd.git
|
||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(noita_gpu LANGUAGES CXX)
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
if(MSVC) # Release keeps its optimisations and writes a PDB (the crash handler of noita_gpu prints a symbolised stack)
|
||||||
|
add_compile_options($<$<CONFIG:Release>:/Zi>)
|
||||||
|
add_link_options($<$<CONFIG:Release>:/DEBUG:FULL> $<$<CONFIG:Release>:/OPT:REF> $<$<CONFIG:Release>:/OPT:ICF>)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(ngcore STATIC
|
||||||
|
src/core/wak.cpp
|
||||||
|
src/core/xml.cpp
|
||||||
|
src/core/materials.cpp)
|
||||||
|
target_include_directories(ngcore PUBLIC src)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(ngcore PRIVATE /W4 /permissive-)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---- GPU app (needs the Vulkan SDK: set VULKAN_SDK or -DVULKAN_SDK=...) ----
|
||||||
|
if(NOT VULKAN_SDK AND DEFINED ENV{VULKAN_SDK})
|
||||||
|
set(VULKAN_SDK $ENV{VULKAN_SDK})
|
||||||
|
endif()
|
||||||
|
set(ENV{VULKAN_SDK} "${VULKAN_SDK}")
|
||||||
|
find_package(Vulkan REQUIRED)
|
||||||
|
find_program(GLSLC glslc HINTS ${VULKAN_SDK}/Bin REQUIRED)
|
||||||
|
|
||||||
|
# Runtime Lua -> GLSL -> SPIR-V. Embed engine declarations, not generated Lua.
|
||||||
|
file(READ ${CMAKE_SOURCE_DIR}/shaders/common.glsl NG_LUA_COMMON)
|
||||||
|
string(REGEX MATCH "layout\\(push_constant\\)[^}]*} pc;" NG_LUA_PC "${NG_LUA_COMMON}")
|
||||||
|
file(READ ${CMAKE_SOURCE_DIR}/shaders/fx_common.glsl NG_LUA_FX)
|
||||||
|
string(REGEX MATCH "struct Ent [^{]*\\{[^}]*};" NG_LUA_ENT "${NG_LUA_FX}")
|
||||||
|
file(READ ${CMAKE_SOURCE_DIR}/shaders/fx_state.glsl NG_LUA_STATE)
|
||||||
|
string(REGEX MATCH "struct FProj [^{]*\\{[^}]*};" NG_LUA_PROJ "${NG_LUA_STATE}")
|
||||||
|
file(READ ${CMAKE_SOURCE_DIR}/shaders/lua_native_runtime.glsl NG_LUA_RUNTIME)
|
||||||
|
file(READ ${CMAKE_SOURCE_DIR}/shaders/lua_native_terrain.glsl NG_LUA_TERRAIN)
|
||||||
|
file(READ ${CMAKE_SOURCE_DIR}/shaders/proj_lua_dispatch.glsl NG_LUA_DISPATCH)
|
||||||
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||||
|
shaders/common.glsl shaders/fx_common.glsl shaders/fx_state.glsl
|
||||||
|
shaders/lua_native_runtime.glsl shaders/lua_native_terrain.glsl shaders/proj_lua_dispatch.glsl)
|
||||||
|
if(NOT NG_LUA_PC OR NOT NG_LUA_ENT OR NOT NG_LUA_PROJ)
|
||||||
|
message(FATAL_ERROR "Cannot extract the engine ABI for runtime Lua shaders")
|
||||||
|
endif()
|
||||||
|
configure_file(src/lua/gpu_lua_sources.h.in generated/gpu_lua_sources.h @ONLY)
|
||||||
|
find_path(SHADERC_INCLUDE_DIR shaderc/shaderc.h HINTS ${VULKAN_SDK}/Include REQUIRED)
|
||||||
|
find_file(SHADERC_RUNTIME shaderc_shared.dll HINTS ${VULKAN_SDK}/Bin REQUIRED)
|
||||||
|
add_library(nggpulua STATIC src/lua/gpu_lua.cpp src/lua/gpu_lua_proto.c)
|
||||||
|
target_include_directories(nggpulua PUBLIC src PRIVATE ${CMAKE_BINARY_DIR}/generated ${SHADERC_INCLUDE_DIR})
|
||||||
|
target_link_libraries(nggpulua PUBLIC luajit_static)
|
||||||
|
|
||||||
|
# ---- Blosc2 (world save format: byte-shuffled, LZ4/Zstd-compressed tiles); lz4/zstd sources are local checkouts ----
|
||||||
|
set(BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
set(BUILD_STATIC ON CACHE BOOL "" FORCE)
|
||||||
|
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(BUILD_FUZZERS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
set(BUILD_PLUGINS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(DEACTIVATE_ZLIB ON CACHE BOOL "" FORCE)
|
||||||
|
set(BLOSC_ENABLE_ZFP OFF CACHE STRING "" FORCE)
|
||||||
|
set(BLOSC_LZ4_SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/lz4 CACHE PATH "" FORCE)
|
||||||
|
set(BLOSC_ZSTD_SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/zstd CACHE PATH "" FORCE)
|
||||||
|
set(BLOSC_MERGE_BUNDLED_STATIC OFF CACHE BOOL "" FORCE) # (the in-place merge of lz4 / zstd into libblosc2 ran on every build: blosc/CMakeLists.txt)
|
||||||
|
add_subdirectory(third_party/c-blosc2 EXCLUDE_FROM_ALL)
|
||||||
|
add_library(ngblosc INTERFACE)
|
||||||
|
target_link_libraries(ngblosc INTERFACE blosc2_static)
|
||||||
|
target_include_directories(ngblosc INTERFACE ${CMAKE_SOURCE_DIR}/third_party/c-blosc2/include)
|
||||||
|
|
||||||
|
add_executable(noita_gpu src/app/main.cpp src/audio/fmod_audio.cpp)
|
||||||
|
target_link_libraries(noita_gpu PRIVATE ngspawn ngblosc nggpulua Vulkan::Vulkan)
|
||||||
|
# the short git hash of the sources at configure time (the GIF metadata's "build", docs/GIF_METADATA.md)
|
||||||
|
execute_process(COMMAND git rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NG_GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||||
|
if(NOT NG_GIT_HASH)
|
||||||
|
set(NG_GIT_HASH "dev")
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(noita_gpu PRIVATE NG_BUILD="${NG_GIT_HASH}")
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(noita_gpu PRIVATE /W4 /permissive- /bigobj)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(SHADERS intent apply paint render terrain sprites entities projectiles edits react burn rigid_solve rigid_commit rigid_frame rigid_events proj_spawn proj_step fx_particles fx_anims ents_prev fx_hand displace rigid_raster explode_rays rigid_burn vines sky_cols glow fog particle_req light_cells clip_half)
|
||||||
|
# shaders are rebuilt when their source or any included .glsl changes, then copied next to the executable
|
||||||
|
file(GLOB SHADER_INCLUDES ${CMAKE_SOURCE_DIR}/shaders/*.glsl ${CMAKE_SOURCE_DIR}/shaders/generated/*.glsl)
|
||||||
|
set(SPV_FILES "")
|
||||||
|
# Offline reference artifacts only; the game uses native C++ startup translation.
|
||||||
|
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||||
|
if(Python3_Interpreter_FOUND)
|
||||||
|
add_custom_target(lua_gpu_generate
|
||||||
|
COMMAND ${Python3_EXECUTABLE} -m tools.lua_gpu.engine
|
||||||
|
--data-root ${CMAKE_SOURCE_DIR}/..
|
||||||
|
--out-dir ${CMAKE_SOURCE_DIR}/shaders/generated
|
||||||
|
--header ${CMAKE_SOURCE_DIR}/src/lua/gpu_program_generated.h
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Generating offline Lua GPU reference artifacts"
|
||||||
|
VERBATIM)
|
||||||
|
endif()
|
||||||
|
foreach(s ${SHADERS})
|
||||||
|
add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/spv/${s}.spv
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/spv
|
||||||
|
COMMAND ${GLSLC} --target-env=vulkan1.1 -O -I ${CMAKE_SOURCE_DIR}/shaders
|
||||||
|
${CMAKE_SOURCE_DIR}/shaders/${s}.comp -o ${CMAKE_BINARY_DIR}/spv/${s}.spv
|
||||||
|
DEPENDS ${CMAKE_SOURCE_DIR}/shaders/${s}.comp ${SHADER_INCLUDES}
|
||||||
|
COMMENT "glslc ${s}.comp")
|
||||||
|
list(APPEND SPV_FILES ${CMAKE_BINARY_DIR}/spv/${s}.spv)
|
||||||
|
add_custom_command(TARGET noita_gpu POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv $<TARGET_FILE_DIR:noita_gpu>/${s}.spv)
|
||||||
|
endforeach()
|
||||||
|
add_custom_target(shaders ALL DEPENDS ${SPV_FILES})
|
||||||
|
# building only the shaders (e.g. the `shaders` project in Visual Studio) must also refresh the demo folder and the exe folder
|
||||||
|
foreach(s ${SHADERS})
|
||||||
|
add_custom_command(TARGET shaders POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv ${CMAKE_SOURCE_DIR}/demo/${s}.spv
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv ${CMAKE_BINARY_DIR}/$<CONFIG>/${s}.spv)
|
||||||
|
endforeach()
|
||||||
|
# `cmake --install` / the INSTALL project: the demo folder gets the exe and the shaders
|
||||||
|
install(TARGETS noita_gpu RUNTIME DESTINATION ${CMAKE_SOURCE_DIR}/demo)
|
||||||
|
install(FILES ${SPV_FILES} DESTINATION ${CMAKE_SOURCE_DIR}/demo)
|
||||||
|
# keep the demo folder (run_*.bat) in step with the build
|
||||||
|
add_custom_command(TARGET noita_gpu POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:noita_gpu> ${CMAKE_SOURCE_DIR}/demo/noita_gpu.exe)
|
||||||
|
foreach(s ${SHADERS})
|
||||||
|
add_custom_command(TARGET noita_gpu POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv ${CMAKE_SOURCE_DIR}/demo/${s}.spv)
|
||||||
|
endforeach()
|
||||||
|
add_dependencies(noita_gpu shaders)
|
||||||
|
|
||||||
|
# noita_spelltest.exe: the same sources and engine, built to run only the spell test room (NG_SPELLTEST_ONLY in main.cpp); it uses the same shaders
|
||||||
|
add_executable(noita_spelltest src/app/main.cpp src/audio/fmod_audio.cpp)
|
||||||
|
target_link_libraries(noita_spelltest PRIVATE ngspawn ngblosc nggpulua Vulkan::Vulkan)
|
||||||
|
target_compile_definitions(noita_spelltest PRIVATE NG_BUILD="${NG_GIT_HASH}" NG_SPELLTEST_ONLY=1)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(noita_spelltest PRIVATE /W4 /permissive- /bigobj)
|
||||||
|
endif()
|
||||||
|
add_dependencies(noita_spelltest shaders)
|
||||||
|
foreach(app noita_gpu noita_spelltest)
|
||||||
|
add_custom_command(TARGET ${app} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SHADERC_RUNTIME} $<TARGET_FILE_DIR:${app}>/shaderc_shared.dll
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SHADERC_RUNTIME} ${CMAKE_SOURCE_DIR}/demo/shaderc_shared.dll)
|
||||||
|
endforeach()
|
||||||
|
install(FILES ${SHADERC_RUNTIME} DESTINATION ${CMAKE_SOURCE_DIR}/demo)
|
||||||
|
add_custom_command(TARGET noita_spelltest POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:noita_spelltest> ${CMAKE_SOURCE_DIR}/demo/noita_spelltest.exe)
|
||||||
|
|
||||||
|
# pack_spelltest (not part of ALL): build/pack/noita_spelltest/ and build/pack/noita_spelltest_<hash>.zip, to run with the Steam Noita (packaging/spelltest/README.txt):
|
||||||
|
# the exe, the shaders, audio_host.exe (the 32-bit sound helper, built here in build_audio32) and the C++ runtime DLLs; never data.wak or FMOD
|
||||||
|
# cmake --build build --config Release --target pack_spelltest
|
||||||
|
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
|
||||||
|
set(CMAKE_INSTALL_UCRT_LIBRARIES FALSE)
|
||||||
|
include(InstallRequiredSystemLibraries) # (only to find the redistributable DLLs: CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS)
|
||||||
|
string(REPLACE ";" "|" NG_PACK_SHADERS "${SHADERS}")
|
||||||
|
string(REPLACE ";" "|" NG_PACK_CRT "${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}")
|
||||||
|
set(NG_AUDIO32_DIR ${CMAKE_SOURCE_DIR}/build_audio32)
|
||||||
|
add_custom_target(pack_spelltest
|
||||||
|
COMMAND ${CMAKE_COMMAND} -S ${CMAKE_SOURCE_DIR}/tools/audio_host -B ${NG_AUDIO32_DIR} -G "${CMAKE_GENERATOR}" -A Win32
|
||||||
|
COMMAND ${CMAKE_COMMAND} --build ${NG_AUDIO32_DIR} --config Release
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DEXE=$<TARGET_FILE:noita_spelltest> -DSPV_DIR=${CMAKE_BINARY_DIR}/spv "-DSHADERS=${NG_PACK_SHADERS}"
|
||||||
|
-DAUDIO=${NG_AUDIO32_DIR}/Release/audio_host.exe -DREADME=${CMAKE_SOURCE_DIR}/packaging/spelltest/README.txt "-DCRT=${NG_PACK_CRT}"
|
||||||
|
-DSHADERC=${SHADERC_RUNTIME}
|
||||||
|
-DOUT=${CMAKE_BINARY_DIR}/pack -DVERSION=${NG_GIT_HASH} -P ${CMAKE_SOURCE_DIR}/packaging/spelltest/pack.cmake
|
||||||
|
COMMENT "packing the spell room"
|
||||||
|
VERBATIM)
|
||||||
|
add_dependencies(pack_spelltest noita_spelltest shaders)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
|
||||||
|
# ---- world generation (vendored noita_random, MIT) ----
|
||||||
|
add_library(ngworld STATIC src/world/wang_gen.cpp src/world/biomes.cpp src/world/petri.cpp src/world/noise.cpp src/world/biome_materials.cpp src/world/cave_bitmap.cpp src/world/proc_biome.cpp src/world/terrain.cpp src/world/reactions.cpp)
|
||||||
|
target_link_libraries(ngworld PUBLIC ngcore)
|
||||||
|
target_include_directories(ngworld PUBLIC src PRIVATE third_party/noita_random)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(ngworld PRIVATE /W0 /bigobj /permissive)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(ngspawn STATIC src/ai/pathfind.cpp src/world/chunk_spawn.cpp src/world/area_setup.cpp src/world/area_store.cpp src/world/biome_modifiers.cpp src/entity/entity.cpp src/entity/runtime.cpp src/entity/runtime_natives.cpp src/entity/electricity.cpp src/entity/status.cpp src/gun/gun.cpp src/gun/projectiles.cpp src/gun/effects.cpp src/gun/gpu_fx.cpp src/physics/rigid.cpp src/physics/gpu_bodies.cpp)
|
||||||
|
target_link_libraries(ngspawn PUBLIC ngworld nglua)
|
||||||
|
|
||||||
|
# ---- Lua host (LuaJIT built from third_party/luajit with MSVC) ----
|
||||||
|
set(LUAJIT_DIR ${CMAKE_SOURCE_DIR}/third_party/luajit)
|
||||||
|
if(NOT EXISTS ${LUAJIT_DIR}/src/lua51.lib)
|
||||||
|
message(STATUS "Building LuaJIT (static, x64)...")
|
||||||
|
execute_process(COMMAND cmd /c ${LUAJIT_DIR}/build_luajit.bat OUTPUT_QUIET ERROR_QUIET RESULT_VARIABLE LJ_RC)
|
||||||
|
if(NOT LJ_RC EQUAL 0)
|
||||||
|
message(FATAL_ERROR "LuaJIT build failed; run third_party/luajit/build_luajit.bat to see why")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
add_library(luajit_static STATIC IMPORTED)
|
||||||
|
set_target_properties(luajit_static PROPERTIES IMPORTED_LOCATION ${LUAJIT_DIR}/src/lua51.lib
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES ${LUAJIT_DIR}/src)
|
||||||
|
add_library(nglua STATIC src/lua/lua_host.cpp src/lua/entity_natives.cpp src/lua/host_natives.cpp src/lua/mod_loader.cpp src/entity/herds.cpp)
|
||||||
|
target_link_libraries(nglua PUBLIC ngcore luajit_static)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(nglua PRIVATE /W4 /permissive-)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# CPU rigid-body engine: the recovered Box2D of the original, used by the game through src/phys.
|
||||||
|
set(NG_CPU_PHYS_SOURCES src/phys/entity_physics.cpp src/phys/rigid_dynamics.cpp src/phys/rigid_collision.cpp src/phys/convex_distance.cpp src/phys/rigid_world_toi.cpp src/phys/dynamic_tree.cpp
|
||||||
|
src/phys/contact_solver.cpp src/phys/rigid_contact_world.cpp src/phys/rigid_world_step.cpp src/phys/joint_solver.cpp src/phys/shape_queries.cpp src/phys/recovered_units.cpp src/phys/world.cpp src/phys/rigid_fracture.cpp src/phys/terrain_outline.cpp src/phys/terrain_world.cpp src/phys/poly_decompose.cpp src/phys/phys_system.cpp)
|
||||||
|
file(GLOB NG_TESS_SOURCES third_party/libtess2/Source/*.c) # libtess2 (SGI free software licence B), the tessellator of the original
|
||||||
|
add_library(ngphys STATIC ${NG_CPU_PHYS_SOURCES} ${NG_TESS_SOURCES})
|
||||||
|
target_link_libraries(ngspawn PUBLIC ngphys)
|
||||||
|
target_include_directories(ngphys PUBLIC cpu src third_party/libtess2/Include)
|
||||||
|
target_include_directories(ngphys PRIVATE third_party/libtess2/Source)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# noita_gpu documentation
|
||||||
|
|
||||||
|
A GPU (Vulkan compute) reimplementation of Noita's engine in C++, loading the original game's `data.wak`.
|
||||||
|
Goal: rule-faithful (not bit-exact) falling-sand simulation on the GPU, with the original world generation
|
||||||
|
and the original Lua scripts driving content.
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
Requirements: Visual Studio 2022 (MSVC), CMake 3.20+, Vulkan SDK.
|
||||||
|
The Vulkan SDK location is passed as `-DVULKAN_SDK=...` (or the `VULKAN_SDK` environment variable). LuaJIT is
|
||||||
|
built automatically on first configure from `third_party/luajit`.
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DVULKAN_SDK=F:/workspace/tools/VulkanSDK
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
`demo/` holds a ready-to-run build of `noita_gpu` (exe + compiled shaders) and two launchers: `run_world.bat` (real Mines terrain, optional seed argument) and `run_sandbox.bat`. They read `data.wak` from two folders up (`..\..\data.wak`). Refresh it after a rebuild by copying `build/Release/noita_gpu.exe` and `*.spv`.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Performs the moves that won claim arbitration. A mover wins only if it holds the highest
|
||||||
|
// priority on both its source and its target cell, so the winners' cell pairs are disjoint and
|
||||||
|
// no atomics or ordering are needed here. The displaced occupant takes the mover's old slot
|
||||||
|
// (the original's swap-based move primitive, FUN_0070cc00).
|
||||||
|
void main() {
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
uint i = uint(p.y * pc.gw + p.x);
|
||||||
|
uvec4 it = intent[i];
|
||||||
|
if (it.x == NONE) return;
|
||||||
|
uint t = it.x & 0x7FFFFFFFu;
|
||||||
|
if (claims[i] != it.y || claims[t] != it.y) return; // lost arbitration, retry next pass
|
||||||
|
|
||||||
|
uint cs = cells[i], ct = cells[t];
|
||||||
|
vec2 et = ext[t];
|
||||||
|
if ((it.x >> 31) != 0u) cs ^= DIR_BIT; // gas bounced: flip horizontal direction
|
||||||
|
cells[t] = cs;
|
||||||
|
ext[t] = vec2(uintBitsToFloat(it.z), uintBitsToFloat(it.w));
|
||||||
|
cells[i] = ct;
|
||||||
|
ext[i] = et;
|
||||||
|
const uint bt = birth[t]; birth[t] = birth[i]; birth[i] = bt; // the age moves with the cell
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// The loaded biome areas (host: composeRegion in main.cpp). One record of 16 words per area after the 8 word header, then the tile map (biome-map tile -> area + 1).
|
||||||
|
// header (8 words): [0] area count, [1] tile map offset, [2] tile map width, [3] tile map height, [4] offset of the per-tile topology record offsets, [5] world seed, [6] x offset of the gradient noise (float)
|
||||||
|
// record: [0] origin x, [1] origin y (world cell at the centre of wang pixel (0,0)), [2] wang width, [3] wang height, [4] offset in the solidity plane, [5] offset in the material plane,
|
||||||
|
// [6] scene x0, [7] scene y0, [8] scene width, [9] scene height, [10] offset of the chunk table of the scene planes (materials, visuals, backgrounds; chunks of 64 x 64 cells, absolute index of the chunk or 0xFFFFFFFF), [11] offset of the background tile,
|
||||||
|
// [12] background width, [13] background height, [14] offset of the biome's <Materials> list, [15] its component count
|
||||||
|
layout(std430, set = 0, binding = 43) readonly buffer AreaTab { uint atab[]; };
|
||||||
|
|
||||||
|
int arec(int a, int k) { return int(atab[8 + 16 * a + k]); }
|
||||||
|
|
||||||
|
// The area of the biome-map tile that holds the world cell, -1 when none is loaded.
|
||||||
|
int area_of(ivec2 w) {
|
||||||
|
int tx = (w.x >= 0 ? w.x / 512 : -((-w.x + 511) / 512)) + 35;
|
||||||
|
int ty = (w.y >= 0 ? w.y / 512 : -((-w.y + 511) / 512)) + 14;
|
||||||
|
if (tx < 0 || ty < 0 || tx >= int(atab[2]) || ty >= int(atab[3])) return -1;
|
||||||
|
return int(atab[atab[1] + uint(ty) * atab[2] + uint(tx)]) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index of a world cell in the scene planes of area a, -1 when the cell is outside that area's planes.
|
||||||
|
int scene_index(int a, ivec2 w) {
|
||||||
|
ivec2 sc = w - ivec2(arec(a, 6), arec(a, 7));
|
||||||
|
if (sc.x < 0 || sc.y < 0 || sc.x >= arec(a, 8) || sc.y >= arec(a, 9)) return -1;
|
||||||
|
int cw = (arec(a, 8) + 63) >> 6;
|
||||||
|
uint ct = atab[uint(arec(a, 10) + (sc.y >> 6) * cw + (sc.x >> 6))]; // scene planes are stored in chunks of 64 x 64 cells
|
||||||
|
if (ct == 0xFFFFFFFFu) return -1;
|
||||||
|
return int(ct) + ((sc.y & 63) << 6) + (sc.x & 63);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// The biome modifiers (data/scripts/biome_modifiers.lua, BiomeModifiers at biome + 0x170; src/world/biome_modifiers.h) of the biome-map tile under a world point,
|
||||||
|
// and the cold / warm conversions of the materials. Needs the Tables buffer as `tbl` (main.cpp fills both after the fog of war hole images):
|
||||||
|
// per tile: projectile_drag_coeff, entity_gravity_y_multiplier (float bits), bytes (fog_of_war_delta, fire_extinguish_chance, reaction_freeze_chance, reaction_unfreeze_chance), dust_amount (float bits)
|
||||||
|
// per material: cold_freezes_to_material + 1 | cold_freezes_chance_rev << 16, warmth_melts_to_material + 1 | warmth_melts_chance_rev << 16
|
||||||
|
uint bm_fog_base() { const uint n = mats[mats.length() - 1u].w; return 1024u + 9u * n + 132u + n; } // the biome map's width, height, then the fog type per tile
|
||||||
|
uint bm_mods_base() { return bm_fog_base() + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u; }
|
||||||
|
int bm_tile(vec2 w) { // the tile index, -1 outside the map (the world default: drag 1, gravity 1, no chances)
|
||||||
|
const uint fb = bm_fog_base();
|
||||||
|
const int bw = int(tbl[fb]), bh = int(tbl[fb + 1u]);
|
||||||
|
const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14);
|
||||||
|
if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return -1;
|
||||||
|
return t.y * bw + t.x;
|
||||||
|
}
|
||||||
|
float bm_drag(vec2 w) { const int t = bm_tile(w); return t < 0 ? 1.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t)]); }
|
||||||
|
float bm_gravity(vec2 w) { const int t = bm_tile(w); return t < 0 ? 1.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t) + 1u]); }
|
||||||
|
uint bm_bytes(vec2 w) { const int t = bm_tile(w); return t < 0 ? 0u : tbl[bm_mods_base() + 4u * uint(t) + 2u]; } // fog delta | fire extinguish << 8 | freeze << 16 | unfreeze << 24
|
||||||
|
float bm_dust(vec2 w) { const int t = bm_tile(w); return t < 0 ? 0.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t) + 3u]); } // the biome modifier dust_amount (post_final.frag: tex_fog .g)
|
||||||
|
uvec2 bm_conversions(uint m) { const uint cb = bm_mods_base() + 8192u * 4u + 2u * m; return uvec2(tbl[cb], tbl[cb + 1u]); }
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "fx_state.glsl"
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // 1024 noise words, then 4 per material: burning explosion (type + 1, power, probability, damage), then 2 per material: flame and smoke material + 1 (0 = the default)
|
||||||
|
#include "biome_mods.glsl"
|
||||||
|
|
||||||
|
// Fire, following docs/DISPLACEMENT_AND_FIRE.md (functions 0070f5e0, 0070f9e0, 0070fb10, 0070c6e0, 0070e060, 00709fc0), in a data-parallel form.
|
||||||
|
//
|
||||||
|
// * State of a cell: burning (bit 27) and the engine's signed fade byte (cell + 0x11), in the top byte of the birth word. A flame cell (fire material) carries its fade
|
||||||
|
// (the "intensity" the emitter gave it), a burning cell the fade of the cell that ignited it.
|
||||||
|
// * Ordinary ignition: a burnable, not burning cell ignites iff r % 101 < 100 - autoignition_temperature, and takes the source's fade.
|
||||||
|
// * Spread: every source (burning cell, fire source material, flame) first passes the gate r % 101 <= its fade, then picks ONE of its four neighbours; seen from
|
||||||
|
// the target each neighbour is therefore a source with probability gate x 1/4 (flames spread twice per update), then the ignition test applies.
|
||||||
|
// * A burning cell emits flames (generates_flames percent per update, intensity fade x (0.75 .. 1.29), into (x-1..x+1, y-1) if that cell is empty) and smoke
|
||||||
|
// (generates_smoke); a material that requires_oxygen restores its fade from temperature_of_fire when the flame was accepted and otherwise loses fade, burning
|
||||||
|
// stops when the fade is below 1.
|
||||||
|
// * Liquids (and powders, gases) burn out with a chance 1/(fire_hp+1) per update into a flame (fire_hp 0: at once); solids age (a counter in ext.x) and are
|
||||||
|
// removed on every 32nd frame once fire_hp < age (fire_hp -1 never).
|
||||||
|
// * Water does not put out a neighbour (0070c6e0 / 0070e060): it smothers a covered cell through requires_oxygen.
|
||||||
|
// * Hot smoke (0070f800 + CGasCell update 0070a670): smoke made by a burning cell carries an intensity trunc(fade x (0.75 .. 1.29)) (birth top byte); on an update that
|
||||||
|
// reaches it ((gas_speed + 1) / 101) the intensity drops by rand % 3 and, as the signed fade, spreads fire (rand % 101 < fade, then 0070f9e0): fire climbs through its smoke.
|
||||||
|
// * A burnable gas reached by a spread turns into its flame material at once (0070aa80: whatever the ignition draw; cell type fire excluded).
|
||||||
|
// The engine's random streams are shared and ordered; here every cell draws its own hash, the probabilities are the engine's.
|
||||||
|
const ivec2 D4[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
|
||||||
|
|
||||||
|
uint cell_at(ivec2 p) {
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return NONE;
|
||||||
|
return cells[p.y * pc.gw + p.x];
|
||||||
|
}
|
||||||
|
|
||||||
|
float rnd(uint seed, uint k) { return float(hash3(seed, k, 0x51u) & 0xFFFFu) / 65536.0; }
|
||||||
|
uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; } // 0070f8c0: the CellData flame material (+0xe0) of the burning cell
|
||||||
|
uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; } // 0070f800: its smoke material (+0xfc)
|
||||||
|
|
||||||
|
// TODO(to-do #30): the flames over burning oil look a little taller / denser than the game's low line
|
||||||
|
void main() {
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
uint c = cells[i];
|
||||||
|
uint m = cell_mat(c);
|
||||||
|
if (m == 0u) return;
|
||||||
|
uvec4 sp = mats[mats.length() - 1u]; // special ids: fire, smoke, steam, material count
|
||||||
|
uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w;
|
||||||
|
uint f = mflags(m);
|
||||||
|
bool burnable = ((mz >> 16) & 1u) != 0u;
|
||||||
|
bool burning = (c & BURN_BIT) != 0u;
|
||||||
|
uint seed = uint(i) * 977u + uint(pc.frame) * 64u + uint(pc.pass_);
|
||||||
|
int ai = int(mw & 255u);
|
||||||
|
int tempFire = int((mw >> 8) & 255u);
|
||||||
|
int smokeP = int((mw >> 16) & 255u), flameP = int((mw >> 24) & 255u);
|
||||||
|
int fireHp = int(mz << 16) >> 16; // fire_hp as the signed 16 bits
|
||||||
|
bool noBurnOut = fireHp == -1; // exactly -1 never burns out (0070e060); other negative values satisfy fire_hp < age
|
||||||
|
|
||||||
|
{ // the biome's reaction_freeze_chance / reaction_unfreeze_chance (0070e8c0; FREEZING, HOT): r % (cold_freezes_chance_rev + 1) < freeze turns the cell into its
|
||||||
|
// cold_freezes_to_material when the random neighbour is free; r % (warmth_melts_chance_rev + 1) < unfreeze turns it into its warmth_melts_to_material when that
|
||||||
|
// neighbour is free or of another material (the cell-type gate +0xa0 and the contact predicate 004ac0f0 of the freeze are not ported)
|
||||||
|
const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y)));
|
||||||
|
const uint fc = (bb >> 16) & 0xFFu, uc = bb >> 24;
|
||||||
|
if ((fc | uc) != 0u && !burning) {
|
||||||
|
const uvec2 cv = bm_conversions(m);
|
||||||
|
const uint nb = cell_at(p + D4[hash3(seed, 41u, 9u) & 3u]);
|
||||||
|
const uint nm = nb == NONE ? 0u : cell_mat(nb);
|
||||||
|
if (fc != 0u && (cv.x & 0xFFFFu) != 0u && (hash3(seed, 42u, 9u) & 0x7FFFu) % ((cv.x >> 16) + 1u) < fc && nm == 0u) { cells[i] = ((cv.x & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; }
|
||||||
|
if (uc != 0u && (cv.y & 0xFFFFu) != 0u && (hash3(seed, 43u, 9u) & 0x7FFFu) % ((cv.y >> 16) + 1u) < uc && nm != m) { cells[i] = ((cv.y & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!burning && (f & F_GAS) != 0u) { // CGasCell update 0070a670: with probability (gas_speed + 1) / 101 (else the update only drifts the gas), intensity -= rand % 3,
|
||||||
|
// fade = (char) intensity; a positive one then spreads (rand % 101 < fade, then 0070f9e0) - the targets below count this smoke as a source
|
||||||
|
const uint raw = (birth[i] >> 24) & 255u;
|
||||||
|
if (raw != 0u && (mats[3u * m + 1u].w & 255u) >= uint(rnd(seed, 30u) * 101.0)) {
|
||||||
|
const int inten = max(int(raw) - int(hash3(seed, 31u, 7u) % 3u), 0);
|
||||||
|
birth[i] = with_fade(birth[i], inten);
|
||||||
|
}
|
||||||
|
if (!burnable) return;
|
||||||
|
}
|
||||||
|
if (!burning) {
|
||||||
|
if (!burnable) return;
|
||||||
|
// ignition from the four neighbours
|
||||||
|
// 0070f9e0: every source attempts: r % 101 <= its fade, one of its four neighbours is chosen, then the ignition draw; a fire cell attempts twice per update (00709fc0). The target
|
||||||
|
// sees each neighbour as a source with that probability, and the first one that succeeds gives the fade it carries (not the strongest neighbour's)
|
||||||
|
int srcFade = 0;
|
||||||
|
bool ignited = false;
|
||||||
|
float pIgnite = max(0.0, float(100 - ai)) / 101.0;
|
||||||
|
for (int d = 0; d < 4 && !ignited; ++d) {
|
||||||
|
ivec2 np = p + D4[d];
|
||||||
|
uint n = cell_at(np);
|
||||||
|
if (n == NONE) continue;
|
||||||
|
uint nm = cell_mat(n);
|
||||||
|
if (nm == 0u) continue;
|
||||||
|
uint nz = mats[3u * nm + 2u].z, nw = mats[3u * nm + 2u].w;
|
||||||
|
bool nBurning = (n & BURN_BIT) != 0u;
|
||||||
|
const bool hotGas = !nBurning && (mflags(nm) & F_GAS) != 0u && ((nz >> 17) & 1u) == 0u;
|
||||||
|
int nf = fade_of(birth[np.y * pc.gw + np.x]);
|
||||||
|
bool nSource = nBurning || ((nz >> 17) & 1u) != 0u || (hotGas && nf > 0);
|
||||||
|
if (!nSource) continue;
|
||||||
|
if (!nBurning && !hotGas && nf == 0) nf = int((nw >> 8) & 255u); // a fire source material without a stored fade: its temperature_of_fire
|
||||||
|
bool flame = (mflags(nm) & F_FIRE) != 0u;
|
||||||
|
float gate = nf < 0 ? 0.0 : min(float(nf) + 1.0, 101.0) / 101.0;
|
||||||
|
// (a hot gas: its update reaches the spread with (gas_speed + 1) / 101, then rand % 101 < fade, then 0070f9e0's own gate)
|
||||||
|
if (hotGas) gate *= float(min(mats[3u * nm + 1u].w & 255u, 100u) + 1u) / 101.0 * float(nf) / 101.0;
|
||||||
|
// 0070aa80: a gas target (not of cell type fire) turns into its flame material whenever a spread reaches it, whatever the ignition draw
|
||||||
|
float pSrc = gate * 0.25 * ((f & F_GAS) != 0u ? 1.0 : pIgnite); // gate, then this neighbour is the one chosen, then the ignition draw
|
||||||
|
if (flame) pSrc = 1.0 - (1.0 - pSrc) * (1.0 - pSrc); // two separate attempts
|
||||||
|
if (rnd(seed, 1u + uint(d)) < pSrc) { ignited = true; srcFade = nf; }
|
||||||
|
}
|
||||||
|
if (ignited && (f & F_GAS) != 0u) { const uint fm = flame_material(m, sp.x); cells[i] = fm | (c & 0xFF0000u); birth[i] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); ext[i] = vec2(0.0); return; }
|
||||||
|
if (ignited) { cells[i] = c | BURN_BIT; birth[i] = with_fade(birth[i], srcFade); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (no "water next to it puts it out": the cell updates 0070c6e0 / 0070e060 extinguish a burning cell only by the world's gate at +0x451 (r % 100 + 1 < byte); water
|
||||||
|
// smothers a fire through requires_oxygen - a covered cell cannot put its flame into a free cell above, its fade runs out - while oil floating on water burns on)
|
||||||
|
// the explosion of a burning cell (0070fb10, tail): p <= (r / 32767) * min(p + 0.5, 100) with p the create_cell_probability (100 by default: only r = 32767, one draw in 32768 per update);
|
||||||
|
// the radius is min(sqrt(power * (0.75 + 0.5 u)), 150); the blast is a projectile of the material's type, spawned here and exploding on its first step
|
||||||
|
{
|
||||||
|
const uint bt = tbl[1024u + 4u * m];
|
||||||
|
if (bt != 0u) {
|
||||||
|
const float pr = uintBitsToFloat(tbl[1024u + 4u * m + 2u]);
|
||||||
|
const float r1 = float(hash3(seed, 12u, 5u) & 0x7FFFu) / 32767.0;
|
||||||
|
if (pr <= r1 * min(pr + 0.5, 100.0)) {
|
||||||
|
const float rad = min(sqrt(uintBitsToFloat(tbl[1024u + 4u * m + 1u]) * (0.75 + 0.5 * float(hash3(seed, 13u, 5u) & 0x7FFFu) / 32767.0)), 150.0);
|
||||||
|
uint slot = atomicAdd(fcnt[0], 1u) % uint(FP_MAX);
|
||||||
|
FProj bp;
|
||||||
|
bp.a = vec4(vec2(p + ivec2(pc.view_x, pc.view_y)) + 0.5, 0.0, 0.0);
|
||||||
|
bp.b = vec4(0.0, 1.0, 0.0, 0.0);
|
||||||
|
bp.c = vec4(rad, uintBitsToFloat(tbl[1024u + 4u * m + 3u]), 0.0, 0.0);
|
||||||
|
bp.d = ivec4(int(bt) - 1, -1, 1 << 8, 1);
|
||||||
|
bp.e = vec4(0.0);
|
||||||
|
fprojs[slot] = bp;
|
||||||
|
cells[i] = 0u; ext[i] = vec2(0.0); // the cell is consumed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int fade = fade_of(birth[i]);
|
||||||
|
bool liquidClass = (f & (F_LIQUID | F_GAS | F_LIQCELL)) != 0u; // (cell_type liquid, static ones too: wood_static, sand_static... are CLiquidCells)
|
||||||
|
|
||||||
|
// flames and smoke into (x-1..x+1, y-1), the materials of the burning cell's CellData
|
||||||
|
bool flameAccepted = false;
|
||||||
|
if (flameP > 0 && int(rnd(seed, 2u) * 101.0) < flameP) {
|
||||||
|
ivec2 t = p + ivec2(int(rnd(seed, 4u) * 3.0) - 1, -1);
|
||||||
|
if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) {
|
||||||
|
int ti = t.y * pc.gw + t.x;
|
||||||
|
if (cell_mat(cells[ti]) == 0u) {
|
||||||
|
const uint fm = flame_material(m, sp.x);
|
||||||
|
// (0070f8c0: the intensity goes to the new gas cell's +0x2c; its fade byte +0x11, the gate of its spread, is what a new cell of the flame material has: its
|
||||||
|
// temperature_of_fire, 0070f450 - fire: 100. A flame of burning oil ignites wood as readily as any other)
|
||||||
|
if (atomicCompSwap(cells[ti], 0u, fm | (hash3(seed, 9u, 1u) & 0xFF0000u)) == 0u) { flameAccepted = true; birth[ti] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (((mz >> 19) & 1u) != 0u) { // requires_oxygen: an accepted flame restores the fade, a failure takes one off the signed byte (wrapping)
|
||||||
|
fade = flameAccepted ? tempFire : ((fade - 1 + 128) & 255) - 128;
|
||||||
|
birth[i] = with_fade(birth[i], fade);
|
||||||
|
if (fade < 1) { cells[i] = c & ~BURN_BIT; return; }
|
||||||
|
}
|
||||||
|
if (smokeP > 0 && int(rnd(seed, 6u) * 101.0) < smokeP) {
|
||||||
|
ivec2 t = p + ivec2(int(rnd(seed, 7u) * 3.0) - 1, -1);
|
||||||
|
if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) {
|
||||||
|
const int ti = t.y * pc.gw + t.x;
|
||||||
|
// 0070f800: a new cell of type gas gets the intensity trunc(fade x (0.75 .. 1.29)) at +0x2c (here the birth word's top byte, unsigned); its gas update (0070a670) spreads fire with it
|
||||||
|
const uint smm = smoke_material(m, sp.y);
|
||||||
|
const int inten = (mflags(smm) & F_GAS) != 0u ? clamp(int(float(fade) * (0.75 + 0.54 * rnd(seed, 8u))), 0, 255) : 0;
|
||||||
|
if (atomicCompSwap(cells[ti], 0u, smm | (hash3(seed, 9u, 2u) & 0xFF0000u)) == 0u) birth[ti] = with_fade(birth_frame(), inten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // the biome's fire_extinguish_chance (0070c6e0 / 0070e060, after the burning tick 0070fb10): the cell goes out when rng % 100 + 1 < it (MOIST: 11)
|
||||||
|
const uint fx = (bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y))) >> 8) & 0xFFu;
|
||||||
|
if (fx != 0u && (hash3(seed, 31u, 7u) & 0x7FFFu) % 100u + 1u < fx) { cells[i] = c & ~BURN_BIT; return; }
|
||||||
|
}
|
||||||
|
if (liquidClass) { // liquids, powders, gases: burn out into a flame with chance 1/(fire_hp+1) (0: at once)
|
||||||
|
bool burnOut = !noBurnOut && (fireHp <= 0 || int(hash3(seed, 8u, 3u) % uint(fireHp + 1)) == 0);
|
||||||
|
if (!burnOut && !noBurnOut && fireHp > 0 && (f & F_STATIC) != 0u && int(hash3(seed, 8u, 4u) % uint(fireHp + 1)) <= 29) {
|
||||||
|
// a passive (static) liquid (0070c6e0): a second draw <= 29 also burns it out when a random cardinal neighbour is empty (00743e80: one of four by two bits of a
|
||||||
|
// draw; outside the window the opposite one is looked at) - the surface of burning wood is eaten away
|
||||||
|
const uint k = hash3(seed, 8u, 5u) & 3u;
|
||||||
|
ivec2 q = p + D4[k];
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) q = p - D4[k];
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cells[q.y * pc.gw + q.x] == 0u) burnOut = true;
|
||||||
|
}
|
||||||
|
if (burnOut) { const uint fm = flame_material(m, sp.x); cells[i] = fm | (c & 0xFF0000u); birth[i] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); ext[i] = vec2(0.0); return; } // (a new cell of the flame material: its own fade)
|
||||||
|
} else if ((f & F_STATIC) != 0u) { // solids: a wrapping 16-bit burn age; removed every 32nd frame once fire_hp < age
|
||||||
|
int age = (int(ext[i].x) + 1) & 0xFFFF;
|
||||||
|
if ((pc.frame & 31) == 0 && !noBurnOut && fireHp < age) { cells[i] = 0u; ext[i] = vec2(0.0); return; }
|
||||||
|
ext[i] = vec2(float(age), ext[i].y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// The frame for the replay recorder at half size (2 x 2 average) when the window is wider than 1600 px: the copy to the host is a quarter
|
||||||
|
// (main.cpp; a 2560 x 1360 frame is 14 MB, copied every simulation tick).
|
||||||
|
layout(std430, set = 0, binding = 58) buffer ClipHalf { uint clipHalf[]; };
|
||||||
|
layout(std430, set = 0, binding = 60) readonly buffer ClipRaw { uint clipraw[]; }; // the window picture without the HUD (render.comp)
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const ivec2 q = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
const int hw = pc.ow / 2, hh = pc.oh / 2;
|
||||||
|
if (q.x >= hw || q.y >= hh) return;
|
||||||
|
const int i = 2 * q.y * pc.ow + 2 * q.x;
|
||||||
|
const uvec4 a = uvec4(clipraw[i], clipraw[i + 1], clipraw[i + pc.ow], clipraw[i + pc.ow + 1]);
|
||||||
|
uint o = 0u;
|
||||||
|
for (int c = 0; c < 32; c += 8) {
|
||||||
|
const uvec4 v = (a >> uint(c)) & 0xFFu;
|
||||||
|
o |= ((v.x + v.y + v.z + v.w + 2u) >> 2) << uint(c);
|
||||||
|
}
|
||||||
|
clipHalf[q.y * hw + q.x] = o;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Shared by intent/apply/paint/render compute shaders.
|
||||||
|
//
|
||||||
|
// Cell word (u32): bits 0-15 material index, 16-23 colour variation, 26 gas/fire horizontal direction.
|
||||||
|
// Per-cell extension (vec2): x = vertical velocity accumulator, y = horizontal velocity.
|
||||||
|
// Material table: 3 uvec4 per material
|
||||||
|
// [0] colour(ARGB), flags, density(float), liquid_gravity(float)
|
||||||
|
// [1] liquid_viscosity(int), liquid_damping(float), liquid_flow_speed(float),
|
||||||
|
// gas speeds packed: byte0 gas_speed, byte1 up, byte2 horizontal, byte3 down
|
||||||
|
// [2] lifetime(int), durability(float), burn word 1 (fire_hp | burnable << 16 | on_fire << 17 | extinguishes << 18), burn word 2 (autoignition | temperature_of_fire << 8 | smoke << 16)
|
||||||
|
// The last entry of the table is special: (fire id, smoke id, steam id, material count).
|
||||||
|
layout(std430, set = 0, binding = 0) buffer Cells { uint cells[]; };
|
||||||
|
layout(std430, set = 0, binding = 1) readonly buffer Mats { uvec4 mats[]; };
|
||||||
|
layout(std430, set = 0, binding = 2) buffer Pixels { uint pixels[]; };
|
||||||
|
layout(std430, set = 0, binding = 3) buffer Ext { vec2 ext[]; };
|
||||||
|
layout(std430, set = 0, binding = 42) buffer Birth { uint birth[]; }; // the frame a cell got its material (cell age = frame - birth, 0070ebf0 req_lifetime)
|
||||||
|
layout(std430, set = 0, binding = 4) buffer Claims { uint claims[]; };
|
||||||
|
// per cell: x = target index | (flip direction << 31), y = priority, z/w = new vy/vx bits
|
||||||
|
layout(std430, set = 0, binding = 5) buffer Intent { uvec4 intent[]; };
|
||||||
|
|
||||||
|
layout(push_constant) uniform PC {
|
||||||
|
int gw, gh; // grid size in cells
|
||||||
|
int ow, oh; // output size in pixels
|
||||||
|
int scale; // pixels per cell
|
||||||
|
int frame;
|
||||||
|
int pass_; // movement pass within the frame
|
||||||
|
int brush_x, brush_y, brush_r;
|
||||||
|
int brush_mat; // -1: none, 0: erase
|
||||||
|
int seed;
|
||||||
|
int view_x, view_y; // world cell at the top-left of the grid
|
||||||
|
int area_ox, area_oy; // world cell at the centre of wang pixel (0,0)
|
||||||
|
int pw, ph; // wang plane size
|
||||||
|
int m_rock, m_soil, m_sand, m_wet; // fill materials
|
||||||
|
int shift_x, shift_y, shift_mode;
|
||||||
|
int scene_x0, scene_y0, scene_w, scene_h; // pixel-scene plane placement (cells)
|
||||||
|
int bg_w, bg_h; // biome background tile size (0 = none)
|
||||||
|
int ent_count; // sprite instances for sprites.comp // terrain.comp: mode 1 = scroll by (shift_x, shift_y), keep the overlap
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
const uint F_STATIC = 1u; // never moves (solid or liquid_static)
|
||||||
|
const uint F_LIQUID = 2u; // liquid or powder
|
||||||
|
const uint F_GAS = 4u;
|
||||||
|
const uint F_SAND = 8u; // powder (liquid_sand)
|
||||||
|
const uint F_AIR = 16u;
|
||||||
|
const uint F_FIRE = 32u;
|
||||||
|
const uint F_LIQCELL = 128u; // cell_type liquid, static or not (the native count / immersion predicate: liquid slot 22 true, powder slot 25 false)
|
||||||
|
const uint F_POWDER = 256u; // liquid_sand, static or not
|
||||||
|
const uint F_STAINS = 512u; // liquid_stains: a body in it takes its stain (vine points: the stored material that slows them)
|
||||||
|
const uint F_NOSTAIN = 2048u; // CellData stainable off (the box2d materials): no explosion burn
|
||||||
|
const uint F_INDESTRUCT = 1024u; // material tag [indestructible]: a CellEaterComponent with ignored_material_tag leaves it
|
||||||
|
const uint F_SLIME = 64u; // liquid_slime: moved by 0070b7c0 (docs/IMPLEMENTATION_BLOCKERS.md)
|
||||||
|
const uint NONE = 0xFFFFFFFFu;
|
||||||
|
const uint DIR_BIT = 1u << 26;
|
||||||
|
const uint BURN_BIT = 1u << 27; // the cell is burning
|
||||||
|
const uint FROZEN_BIT = 1u << 26; // a static cell stained frozen (MagicConvertMaterialComponent stain_frozen; bits 24-25 then hold its damage level, explode_rays.comp)
|
||||||
|
const uint ATTACH_BIT = 1u << 26; // a powder attached to a wall or ceiling (passive: byte 22 of the original cell); bits 24-25 then hold the direction: 1 right (state 5), 2 above (6), 3 left (7)
|
||||||
|
// Fire fade (the engine's signed fade byte, in steps of 4): bits 24-25 and 28-31 of the cell word. Burning cells and flames carry it.
|
||||||
|
uint cell_fade6(uint c) { return ((c >> 28) & 15u) | (((c >> 24) & 3u) << 4); }
|
||||||
|
uint with_fade6(uint c, uint f) { f = min(f, 63u); return (c & ~0xF3000000u) | ((f & 15u) << 28) | ((f >> 4) << 24); }
|
||||||
|
|
||||||
|
// The age of a cell and its fire fade share the birth word: bits 0-23 the frame it got its material (the age is the difference modulo 2^24), bits 24-31 the engine's signed fade byte (cell + 0x11).
|
||||||
|
const uint BIRTH_MASK = 0xFFFFFFu;
|
||||||
|
uint birth_frame() { return uint(pc.frame) & BIRTH_MASK; }
|
||||||
|
uint birth_age(uint b) { return (uint(pc.frame) - b) & BIRTH_MASK; }
|
||||||
|
int fade_of(uint b) { return int(b) >> 24; } // signed byte
|
||||||
|
uint with_fade(uint b, int f) { return (b & BIRTH_MASK) | (uint(f & 255) << 24); }
|
||||||
|
|
||||||
|
uint cell_mat(uint c) { return c & 0xFFFFu; }
|
||||||
|
uint mflags(uint m) { return mats[3u * m].y; }
|
||||||
|
float mdensity(uint m) { return uintBitsToFloat(mats[3u * m].z); }
|
||||||
|
float mgravity(uint m) { return uintBitsToFloat(mats[3u * m].w); }
|
||||||
|
int mviscosity(uint m) { return int(mats[3u * m + 1u].x & 0xFFFFu); }
|
||||||
|
uint mceiling(uint m) { return mats[3u * m + 1u].x >> 16; } // liquid_sticks_to_ceiling (powders)
|
||||||
|
float mdamping(uint m) { return uintBitsToFloat(mats[3u * m + 1u].y); }
|
||||||
|
float mflow(uint m) { return uintBitsToFloat(mats[3u * m + 1u].z); }
|
||||||
|
uint mgasbits(uint m) { return mats[3u * m + 1u].w; }
|
||||||
|
int mlifetime(uint m) { return int(mats[3u * m + 2u].x); }
|
||||||
|
|
||||||
|
uint hash(uint x) {
|
||||||
|
x ^= x >> 16; x *= 0x7feb352du;
|
||||||
|
x ^= x >> 15; x *= 0x846ca68bu;
|
||||||
|
x ^= x >> 16;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
uint hash3(uint a, uint b, uint c) { return hash(a * 0x9E3779B1u ^ hash(b + 0x85EBCA6Bu ^ hash(c))); }
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
|
||||||
|
// Rigid bodies displace liquids: the liquid cells inside their volume are moved to the nearest free cell outside it, away from the centre (preferring upward).
|
||||||
|
// (One workgroup per entity, which return at once, then one per rigid body.)
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; } ctrl;
|
||||||
|
|
||||||
|
// Liquids, and for bodies that do not go through sand (CellData solid_go_through_sand, bit 9 of the body flags) the powders too: a body that sinks into a pile pushes the grains out.
|
||||||
|
bool liquid_cell(uint c, bool powders) {
|
||||||
|
uint m = cell_mat(c);
|
||||||
|
if (m == 0u) return false;
|
||||||
|
uint f = mflags(m);
|
||||||
|
if ((f & F_STATIC) != 0u) return false;
|
||||||
|
return (f & F_LIQUID) != 0u && ((f & F_SAND) == 0u || powders);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moves the cell at world position w out of the volume: the volume is a box (bodyIdx < 0) or a rigid body's mask.
|
||||||
|
void push_out(ivec2 w, vec2 centre, ivec4 box, int bodyIdx) {
|
||||||
|
const bool powders = bodyIdx >= 0 && (bodies[bodyIdx].i.z & 512) == 0;
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
uint c = cells[i];
|
||||||
|
if (!liquid_cell(c, powders)) return;
|
||||||
|
vec2 d = vec2(w) + 0.5 - centre;
|
||||||
|
d.y -= 0.8; // up is preferred
|
||||||
|
d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d);
|
||||||
|
// the target is claimed first (only free cells are claimed), then the source is released; another invocation that already moved the source (bodies that overlap handle the
|
||||||
|
// same cell) makes this one give its target back. Nothing is lost or doubled: before, the source was emptied first and put back when no free cell was found, and a neighbour
|
||||||
|
// could fill the briefly empty source in between (a full pool with rubble in it lost its water)
|
||||||
|
const vec2 e = ext[i];
|
||||||
|
for (int r = 1; r <= 12; ++r) {
|
||||||
|
ivec2 t = w + ivec2(round(d * float(r)));
|
||||||
|
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break;
|
||||||
|
bool inside;
|
||||||
|
if (bodyIdx < 0) inside = t.x >= box.x && t.x < box.z && t.y >= box.y && t.y < box.w;
|
||||||
|
else {
|
||||||
|
Body b = bodies[bodyIdx];
|
||||||
|
int s = b.i.x;
|
||||||
|
vec2 dd = vec2(t) + 0.5 - b.p.xy;
|
||||||
|
float cs = cos(b.p.z), sn = sin(b.p.z);
|
||||||
|
vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6));
|
||||||
|
inside = shape_solid(s, int(floor(im.x)), int(floor(im.y)));
|
||||||
|
}
|
||||||
|
if (inside) continue;
|
||||||
|
int ti = q.y * pc.gw + q.x;
|
||||||
|
if (atomicCompSwap(cells[ti], 0u, c) == 0u) { // a free target, now holding the liquid
|
||||||
|
if (atomicCompSwap(cells[i], c, 0u) == c) { ext[ti] = e; ext[i] = vec2(0.0); }
|
||||||
|
else atomicCompSwap(cells[ti], c, 0u); // the source went elsewhere meanwhile: give the target back
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// nowhere to go: the liquid stays where it is
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int g = int(gl_WorkGroupID.x), t = int(gl_LocalInvocationID.x);
|
||||||
|
int nEnt = ctrl.inp.w;
|
||||||
|
if (g < nEnt) {
|
||||||
|
return; // creatures and the player displace through the particle path of entities.comp (docs/DISPLACEMENT_AND_FIRE.md); only rigid bodies are pushed out here (the engine's body rule is not recovered)
|
||||||
|
Ent e = ents[g];
|
||||||
|
int kind = e.st.x;
|
||||||
|
if ((kind != 1 && kind != 2 && kind != 3) || (e.st.w & 8) != 0) return;
|
||||||
|
ivec2 ip = ivec2(floor(e.a.xy));
|
||||||
|
ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w));
|
||||||
|
ivec2 sz = hi - lo;
|
||||||
|
if (sz.x <= 0 || sz.y <= 0 || sz.x * sz.y > 4096) return;
|
||||||
|
vec2 centre = vec2(lo + hi) * 0.5;
|
||||||
|
for (int k = t; k < sz.x * sz.y; k += 64) push_out(lo + ivec2(k % sz.x, k / sz.x), centre, ivec4(lo, hi), -1);
|
||||||
|
} else {
|
||||||
|
int bi = g - nEnt;
|
||||||
|
if (bi >= bhdr.x) return;
|
||||||
|
Body b = bodies[bi];
|
||||||
|
if ((b.i.z & 2) != 0) return;
|
||||||
|
int s = b.i.x;
|
||||||
|
float rad = shf(s, 9);
|
||||||
|
int n = int(2.0 * rad) + 2;
|
||||||
|
ivec2 lo = ivec2(floor(b.p.xy - vec2(rad)));
|
||||||
|
for (int k = t; k < n * n; k += 64) {
|
||||||
|
ivec2 w = lo + ivec2(k % n, k / n);
|
||||||
|
vec2 dd = vec2(w) + 0.5 - b.p.xy;
|
||||||
|
float cs = cos(b.p.z), sn = sin(b.p.z);
|
||||||
|
vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6));
|
||||||
|
if (shape_solid(s, int(floor(im.x)), int(floor(im.y)))) push_out(w, b.p.xy, ivec4(0), bi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "far.glsl"
|
||||||
|
#include "fx_state.glsl"
|
||||||
|
|
||||||
|
// Explosions and spills: circular edits of the cell grid, two vec4 per edit.
|
||||||
|
// e[2k] = (world x, world y, radius, max durability)
|
||||||
|
// e[2k+1] = (mode, material, probability, 0) mode 0: dig (cells whose durability is at most the limit become air)
|
||||||
|
// mode 1: fill air cells with the material, each with the given probability
|
||||||
|
// mode 4: a flask sucks the liquid: the cell becomes air when it still holds material e[2k+1].y
|
||||||
|
// mode 3: the same with a velocity (a poured flask): e[2k].w = horizontal velocity (cells per frame), e[2k+1].w = vertical velocity
|
||||||
|
// mode 5: CellEaterComponent (00a639e0): every cell inside the radius whose material is not e[2k+1].y (ignored_material) and not [indestructible]
|
||||||
|
// (with ignored_material_tag) is removed with probability e[2k+1].z (eat_probability); e[2k].w = 1: only_stain (nothing removed)
|
||||||
|
// mode 6: MagicConvertMaterialComponent (00bab8c0 / 00baac80): the rings e[2k].z .. e[2k].w (excluded) of the square (e[2k+1].w = 0) or circle (1)
|
||||||
|
// around the point: a cell of material e[2k+1].y (-1: any material) becomes e[2k+1].z
|
||||||
|
// mode 7: a line from (e[2k].x, e[2k].y) to (e[2k].z, e[2k].w): its empty cells become material e[2k+1].y (an ArcComponent of type MATERIAL; the host
|
||||||
|
// cut the line at the first cell of another material)
|
||||||
|
// mode 8: MaterialSeaSpawnerComponent (00bbc060), two records: (left x, top y, width, first row) (8, material, end row, 0), then
|
||||||
|
// (sine_wavelength, sine_amplitude, noise_scale, noise_threshold) (0, 0, 0, 0): the empty cells of the rows
|
||||||
|
// first .. end (excluded) at y = top + row + int(sin(x / wavelength) x amplitude) where 3D simplex noise (00872560:
|
||||||
|
// x scale, y scale, 0) x 0.5 + 0.5 >= threshold become the material
|
||||||
|
// mode 10: every non-air cell not e[2k+1].y / .z becomes e[2k+1].z when static, else .y (ConvertEverythingToGold)
|
||||||
|
// mode 11: the one cell at (x, y) is set on fire with the fade e[2k+1].y: ordinary ignition (0070f5e0: a burnable cell, not burning,
|
||||||
|
// r % 101 < 100 - autoignition_temperature) or, with e[2k+1].z = 1, forced (0070f750: no draw) - TorchComponent, hack_is_set_fire
|
||||||
|
// mode 9: AnimalAI eating (00a232e0): the cells of material e[2k+1].y inside the box (x, y) +- (e[2k].z, e[2k].w) become, each with
|
||||||
|
// probability e[2k+1].z, air (e[2k+1].w = 0) or material e[2k+1].w - 1 (food_particle_effect_material)
|
||||||
|
// Up to 256 edits per frame.
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after 1024 + 8 words per material: the material tag bits (one word per material, bits assigned by the host)
|
||||||
|
layout(std430, set = 0, binding = 27) readonly buffer Edits { ivec4 head; vec4 e[512]; } ed; // filled by the host and by the GPU projectiles
|
||||||
|
|
||||||
|
#include "simplex3.glsl"
|
||||||
|
|
||||||
|
// pass 1: the far blocks (far.glsl) - per edit (z) the cells of a 128 x 128 box around its point outside the window: dig (0), fill (1), the cell eater (5),
|
||||||
|
// magic convert (6: freezing charge's liquids to ice);
|
||||||
|
// the slot is marked dirty (main.cpp copies it back to the store)
|
||||||
|
void far_pass() {
|
||||||
|
const int k = int(gl_GlobalInvocationID.z);
|
||||||
|
if (k >= ed.head.x || k >= 256) return;
|
||||||
|
const vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1];
|
||||||
|
const int mode = int(r.x + 0.5);
|
||||||
|
if (mode != 0 && mode != 1 && mode != 5 && mode != 6 && mode != 12) return;
|
||||||
|
const ivec2 wc = ivec2(floor(q.xy)) - 64 + ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
const ivec2 lp = wc - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (lp.x >= 0 && lp.y >= 0 && lp.x < pc.gw && lp.y < pc.gh) return; // (the window's own pass)
|
||||||
|
const vec2 w = vec2(wc) + 0.5;
|
||||||
|
if (mode != 6 && distance(w, q.xy) > q.z) return;
|
||||||
|
const int s = far_slot(wc);
|
||||||
|
if (s == 0) return;
|
||||||
|
const int fi = far_index(s, wc);
|
||||||
|
const uint c = farcells[fi], m = cell_mat(c);
|
||||||
|
const uint hi = uint(fi) * 2654435761u;
|
||||||
|
if (mode == 12) { // a frozen stain (as the window's)
|
||||||
|
if (m == 0u || (mflags(m) & F_STATIC) == 0u || (mflags(m) & F_FIRE) != 0u || (c & FROZEN_BIT) != 0u) return;
|
||||||
|
farcells[fi] = c | FROZEN_BIT;
|
||||||
|
} else if (mode == 6) { // (as the window's: the rings q.z .. q.w of the circle / square, from r.y (-1: any) to r.z)
|
||||||
|
if (m == 0u) return;
|
||||||
|
const vec2 d = w - q.xy;
|
||||||
|
const int ring = r.w > 0.5 ? int(length(d) + 0.5) : int(max(abs(d.x), abs(d.y)));
|
||||||
|
if (ring < int(q.z) || ring >= int(q.w) || (r.y >= 0.0 && m != uint(r.y)) || m == uint(r.z)) return;
|
||||||
|
farcells[fi] = uint(r.z) | (((hash3(hi, uint(pc.frame), uint(k) + 41u) >> 16) & 0xFFu) << 16);
|
||||||
|
} else if (mode == 5) {
|
||||||
|
if (m == 0u || (r.y >= 0.0 && m == uint(r.y)) || (q.w > 1.5 && (mflags(m) & F_INDESTRUCT) != 0u)) return;
|
||||||
|
if (r.w > 0.5 && ((tbl[1024u + 8u * mats[mats.length() - 1u].w + m] >> uint(r.w - 1.0)) & 1u) != 0u) return;
|
||||||
|
if (float(hash3(hi, uint(pc.frame), uint(k) + 37u) % 101u) >= r.z * 100.0 || q.w == 1.0 || q.w == 3.0) return;
|
||||||
|
farcells[fi] = 0u;
|
||||||
|
} else if (mode == 0) {
|
||||||
|
if (m == 0u || uintBitsToFloat(mats[3u * m + 2u].y) > q.w) return;
|
||||||
|
farcells[fi] = 0u;
|
||||||
|
} else {
|
||||||
|
if (m != 0u) return;
|
||||||
|
const uint h = hash3(hi, uint(pc.frame), uint(k) + 31u);
|
||||||
|
if (float(h & 0xFFFFu) / 65536.0 >= r.z) return;
|
||||||
|
farcells[fi] = uint(r.y) | (((h >> 16) & 0xFFu) << 16);
|
||||||
|
}
|
||||||
|
farmap[kFarDirty + s - 1] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (pc.pass_ == 1) { far_pass(); return; }
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
uint m = cell_mat(cells[i]);
|
||||||
|
vec2 w = vec2(p + ivec2(pc.view_x, pc.view_y)) + 0.5;
|
||||||
|
for (int k = 0; k < ed.head.x && k < 256; ++k) {
|
||||||
|
vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1];
|
||||||
|
if (r.x > 1.5 && r.x < 2.5) continue; // mode 2 (angular explosion) is explode_rays.comp
|
||||||
|
if (r.x > 10.5 && r.x < 11.5) { // ignite one cell
|
||||||
|
if (p + ivec2(pc.view_x, pc.view_y) != ivec2(floor(q.xy)) || m == 0u || ((mats[3u * m + 2u].z >> 16) & 1u) == 0u || (cells[i] & BURN_BIT) != 0u) continue;
|
||||||
|
if (r.z < 0.5 && int(hash3(uint(i), uint(pc.frame), uint(k) + 71u) % 101u) >= 100 - int(mats[3u * m + 2u].w & 255u)) continue;
|
||||||
|
cells[i] |= BURN_BIT; birth[i] = with_fade(birth[i], int(r.y));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 12.5 && r.x < 14.5) { // the player digs out of the ground (main.cpp, 00bfa850): q = the box; 13: its loose cells fly out as real particles (007175c0: not a
|
||||||
|
// liquid_static material - the powders and liquids), r.y = the speed s; 14: every cell of the box goes (stuck in the static ground for long)
|
||||||
|
if (m == 0u || w.x < q.x || w.x > q.z + 1.0 || w.y < q.y || w.y > q.w + 1.0) continue;
|
||||||
|
if (r.x > 13.5) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; continue; }
|
||||||
|
if ((mflags(m) & F_STATIC) != 0u || (mflags(m) & (F_LIQUID | F_SAND)) == 0u) continue;
|
||||||
|
const uint h1 = hash3(uint(i), uint(pc.frame), uint(k) + 81u), h2 = hash3(uint(i), uint(pc.frame), uint(k) + 83u);
|
||||||
|
const float s = r.y, c = mix(-1.0, -0.65, float(h1 & 0xFFFFu) / 65536.0) * s, b = mix(-0.5, 0.5, float(h1 >> 16) / 65536.0) * s;
|
||||||
|
const float a = float(h2 & 0xFFFFu) / 65536.0 - 0.5, mul = 0.65 + 0.4 * float(h2 >> 16) / 65536.0;
|
||||||
|
const vec2 v = vec2(cos(a) * b - sin(a) * c, sin(a) * b + cos(a) * c) * mul / 60.0;
|
||||||
|
if (emit_material_particle(w, v, 600.0, PART_GRAV, mats[3u * m].x, int(m))) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 11.5 && r.x < 12.5) { // a frozen stain (MagicConvertMaterialComponent stain_frozen: freeze_charge.xml): the static cells inside the radius look frozen (FROZEN_BIT on a static cell)
|
||||||
|
if (m != 0u && distance(w, q.xy) <= q.z && (mflags(m) & F_STATIC) != 0u && (mflags(m) & F_FIRE) == 0u) cells[i] |= FROZEN_BIT;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 4.5 && r.x < 5.5) { // cell eater
|
||||||
|
if (m == 0u || distance(w, q.xy) > q.z) continue;
|
||||||
|
if ((r.y >= 0.0 && m == uint(r.y)) || (q.w > 1.5 && (mflags(m) & F_INDESTRUCT) != 0u)) continue;
|
||||||
|
if (r.w > 0.5 && ((tbl[1024u + 8u * mats[mats.length() - 1u].w + m] >> uint(r.w - 1.0)) & 1u) != 0u) continue; // ignored_material_tag
|
||||||
|
if (float(hash3(uint(i), uint(pc.frame), uint(k) + 37u) % 101u) >= r.z * 100.0) continue;
|
||||||
|
if (q.w == 1.0 || q.w == 3.0) continue; // only_stain: the colour darkens in the engine (not drawn here)
|
||||||
|
{ // 00a639e0: the eaten cell leaves a cosmetic particle of its material (00716ce0 force-created at the cell, velocity uniform(-30, 30) px/s on each axis,
|
||||||
|
// lifetime arguments 5 .. 10): the debris in the tunnel of a black hole. TODO(to-do #36): the unit of that lifetime - the real game's debris
|
||||||
|
// (spell_reference/BH_SPEED_PLAIN.gif) is gone after 0.5 .. 1 s, so 30 .. 60 frames here
|
||||||
|
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 39u);
|
||||||
|
const vec2 v = (vec2(float(h & 0xFFFFu), float(h >> 16)) / 65536.0 * 60.0 - 30.0) / 60.0;
|
||||||
|
emit_particle_g(w, v, 6.0 * (5.0 + 5.0 * float(hash3(uint(i), uint(pc.frame), 41u) & 0xFFFFu) / 65536.0), PART_GRAV, mats[3u * m].x, false, false, true);
|
||||||
|
}
|
||||||
|
cells[i] = 0u; ext[i] = vec2(0.0); m = 0u;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 8.5 && r.x < 9.5) { // a creature eats
|
||||||
|
if (m == 0u || m != uint(r.y) || abs(w.x - q.x) > q.z + 0.5 || abs(w.y - q.y) > q.w + 0.5) continue;
|
||||||
|
if (float(hash3(uint(i), uint(pc.frame), uint(k) + 53u) & 0xFFFFu) / 65536.0 >= r.z) continue;
|
||||||
|
if (r.w < 0.5) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; continue; }
|
||||||
|
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 59u);
|
||||||
|
m = uint(r.w - 1.0); cells[i] = m | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 5.5 && r.x < 6.5) { // magic convert
|
||||||
|
if (m == 0u) continue;
|
||||||
|
const vec2 d = w - q.xy;
|
||||||
|
const int ring = r.w > 0.5 ? int(length(d) + 0.5) : int(max(abs(d.x), abs(d.y)));
|
||||||
|
if (ring < int(q.z) || ring >= int(q.w)) continue;
|
||||||
|
if ((r.y >= 0.0 && m != uint(r.y)) || m == uint(r.z)) continue;
|
||||||
|
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 41u);
|
||||||
|
cells[i] = uint(r.z) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.z);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 7.5 && r.x < 8.5) { // a material sea (its parameters are the next record)
|
||||||
|
const vec4 sp = ed.e[2 * k + 2];
|
||||||
|
k += 1;
|
||||||
|
if (m != 0u) continue;
|
||||||
|
const int xi = int(floor(w.x)), yi = int(floor(w.y));
|
||||||
|
if (xi < int(q.x) || xi >= int(q.x) + int(q.z)) continue;
|
||||||
|
const int row = yi - int(q.y) - int(sin(float(xi) / sp.x) * sp.y);
|
||||||
|
if (row < int(q.w) || row >= int(r.z)) continue;
|
||||||
|
if (simplex3(vec3(float(xi) * sp.z, float(yi) * sp.z, 0.0)) * 0.5 + 0.5 < sp.w) continue;
|
||||||
|
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 47u);
|
||||||
|
cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.y);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 9.5 && r.x < 10.5) { // everything to gold (00704f70: every material but air is redefined as gold, the static ones as gold_static; here the cells take it)
|
||||||
|
if (m == 0u || m == uint(r.y) || m == uint(r.z)) continue;
|
||||||
|
const uint g = (mflags(m) & F_STATIC) != 0u ? uint(r.z) : uint(r.y);
|
||||||
|
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 61u);
|
||||||
|
cells[i] = g | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = g;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.x > 6.5 && r.x < 7.5) { // a material line
|
||||||
|
if (m != 0u) continue;
|
||||||
|
const vec2 ab = q.zw - q.xy;
|
||||||
|
const float l2 = dot(ab, ab);
|
||||||
|
const float t = l2 > 0.0 ? clamp(dot(w - q.xy, ab) / l2, 0.0, 1.0) : 0.0;
|
||||||
|
if (distance(w, q.xy + ab * t) > 0.5) continue;
|
||||||
|
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 43u);
|
||||||
|
cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.y);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (distance(w, q.xy) > q.z) continue;
|
||||||
|
const bool moving = r.x > 2.5 && r.x < 3.5;
|
||||||
|
if (r.x > 3.5) { if (m == uint(r.y)) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; } continue; }
|
||||||
|
if (r.x < 0.5) {
|
||||||
|
if (m != 0u && uintBitsToFloat(mats[3u * m + 2u].y) <= q.w) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; }
|
||||||
|
} else if (m == 0u) {
|
||||||
|
uint h = hash3(uint(i), uint(pc.frame), uint(k) + 31u);
|
||||||
|
if (float(h & 0xFFFFu) / 65536.0 < r.z) {
|
||||||
|
cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16);
|
||||||
|
ext[i] = moving ? vec2(r.w, q.w) : vec2(0.0); // x = vertical, y = horizontal velocity
|
||||||
|
birth[i] = birth_frame();
|
||||||
|
m = uint(r.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,722 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "fx_state.glsl"
|
||||||
|
#include "fx_host.glsl"
|
||||||
|
|
||||||
|
// Entity simulation: gravity, walking, jumping, flying and a basic AI, one invocation per entity, once per frame.
|
||||||
|
// Entities collide with the live cell grid (solid = static or powder cells). Entities that are not fully inside the
|
||||||
|
// simulated window are frozen. Kinds: 0 static, 1 walker, 2 flyer, 3 player (index 0, controlled by the host).
|
||||||
|
struct Ent {
|
||||||
|
vec4 a; // position xy, velocity xy (cells, cells per frame)
|
||||||
|
vec4 box; // min_x, max_x, min_y, max_y
|
||||||
|
vec4 mv; // walk speed, max speed x, jump velocity (negative = up), gravity
|
||||||
|
vec4 ai; // detection range x, y, melee distance, melee damage
|
||||||
|
ivec4 st; // kind, facing, timer, flags (1 grounded, 2 can jump, 4 can fly, 8 dead, 16 in liquid)
|
||||||
|
vec4 hp; // hp, attack cooldown, max fall speed, climb-over height
|
||||||
|
ivec4 x; // damage taken this frame (milli-hp), jetpack energy / idle flag, spawn x (player) / ranged cooldown, spawn y
|
||||||
|
ivec4 rg; // ranged attack: projectile type (-1 none), frames between shots, min distance, max distance
|
||||||
|
ivec4 rg2; // projectiles per shot (min, max), muzzle offset y, the attack of the last attacking tick (1 melee, 2 ranged | tick << 4)
|
||||||
|
ivec4 aux; // player: maximum hp (milli-hp); creatures: velocity of the last hit (milli-cells per frame)
|
||||||
|
vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0
|
||||||
|
vec4 ex2; // ranged attack: projectile speed, gravity
|
||||||
|
ivec4 ex3; // blood material + 1, blood colour, damage types taken (1 projectile, 2 explosion, 4 fire, 8 melee), 0
|
||||||
|
vec4 ex4; // air in the lungs (frames), air maximum (0 = no air needed), damage per frame without air, frames left on fire
|
||||||
|
vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag
|
||||||
|
vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y)
|
||||||
|
vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left
|
||||||
|
ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player)
|
||||||
|
struct PType { vec4 a; vec4 b; }; // speed min, max, damage, lifetime / gravity, colour bits, 0, 0
|
||||||
|
layout(std430, set = 0, binding = 24) readonly buffer PTypes { PType ptypes[]; };
|
||||||
|
layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; };
|
||||||
|
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) buffer Ctrl {
|
||||||
|
ivec4 inp; // input x (-1/0/1), jump held, attack pressed this frame, entity count
|
||||||
|
ivec4 inp2; // frame, has player, attack hits (debug), projectile counter
|
||||||
|
ivec4 inp3; // aim x, aim y (window cells), fire, player projectile type
|
||||||
|
vec4 outp; // player: x, y, hp, facing (written by the shader, read by the host)
|
||||||
|
vec4 outp2; // player: levitation energy 0..1, fire cooldown, 0, 0
|
||||||
|
vec4 view; // host: camera shift of the last tick (cells), interpolation alpha, gravity scale
|
||||||
|
vec4 hand0; // host: held wand: aim point (world cells), enabled, mirror
|
||||||
|
ivec4 hand1; // host: held wand: texel offset, w, h
|
||||||
|
vec4 hand2; // host: held wand: pivot
|
||||||
|
ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them)
|
||||||
|
} ctrl;
|
||||||
|
|
||||||
|
const int ST_HOVER = 128; // an item that hovers (a wand in the world): no physics, a small bob (play_hover_animation)
|
||||||
|
const int ST_SWIMMER = 256; // the immersion probe at ex.w (the integer buoyancy_check_offset_y) is tested
|
||||||
|
const int ST_GROUNDED = 1, ST_CANJUMP = 2, ST_CANFLY = 4, ST_DEAD = 8, ST_LIQUID = 16, ST_RIGID = 32; // ST_RIGID: moved by the rigid-body engine (rigid_*.comp), only damage is handled here
|
||||||
|
|
||||||
|
// rigid bodies are solid for creatures and the player: rigid_raster.comp marks the cells their masks cover (read here with a one-tick delay)
|
||||||
|
layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; };
|
||||||
|
layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; }; // per material: gfx_glow in the low 16 bits, the stain statuses it gives in bits 16-21 (WET, OILED, BLOODY, ALCOHOLIC, SLIMY, RADIOACTIVE)
|
||||||
|
bool body_solid(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
const uint b = bodyGrid[p.y * pc.gw + p.x];
|
||||||
|
return b != 0u && (b & 0x80000000u) == 0u; // (bit 31: a body that is no platform - the lanterns and their chains do not block, main.cpp isPlatform)
|
||||||
|
}
|
||||||
|
// 00bb6820: the leak emitter's offset is the local hit point, clamped to the HitboxComponent (a corpse part: +-12 here); without one (ex7.w, a prop) as it is, zero beyond 256 px
|
||||||
|
vec2 leak_offset(Ent e, vec2 rel) { return e.ex7.w > 0.5 ? (dot(rel, rel) > 65536.0 ? vec2(0.0) : rel) : clamp(rel, vec2(-12.0), vec2(12.0)); }
|
||||||
|
bool solid_cell_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; // outside the window there is no ground (such entities are frozen)
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) return true;
|
||||||
|
return body_solid(w);
|
||||||
|
}
|
||||||
|
// The character's cell test 0063ef70: a platform cell counts only when one of its four neighbours (below, left, right, above) is one too - a lone pixel (a crumb of
|
||||||
|
// rock, a single body pixel) does not stop the player. (Its other exception, the bodies held by telekinesis - 00c2dfc0 stamps their b2Body user data +0x48 with
|
||||||
|
// the frame - does not apply: TelekinesisComponent is not done.)
|
||||||
|
bool solid_at(ivec2 w) {
|
||||||
|
if (!solid_cell_at(w)) return false;
|
||||||
|
return solid_cell_at(w + ivec2(0, 1)) || solid_cell_at(w + ivec2(-1, 0)) || solid_cell_at(w + ivec2(1, 0)) || solid_cell_at(w + ivec2(0, -1));
|
||||||
|
}
|
||||||
|
// What a creature or the player takes by damage type (bit 1 projectile, 2 explosion, 4 fire, 8 melee): the multipliers of the game effects, ex9 = projectile, explosion, fire, melee
|
||||||
|
float taken_mult(vec4 m, int types) {
|
||||||
|
if ((types & 2) != 0) return m.y;
|
||||||
|
if ((types & 8) != 0) return m.w;
|
||||||
|
if ((types & 4) != 0) return m.z;
|
||||||
|
return m.x;
|
||||||
|
}
|
||||||
|
// The hit flash (00ab5b00, called by TakeDamage 00ab9a90 for damage > 0 that is not fire alone): every sprite of the entity is drawn with
|
||||||
|
// sprite_damage_highlight (shader 6: white, the sprite's alpha) or, for a critical hit, sprite_damage_critical_hit_highlight (7: red) in its third shader slot;
|
||||||
|
// at most once per 5 frames (frame - DamageModel +0x238 > 4), and the DamageModelSystem update (00ab6d00) clears the slot at +0x238 + 3: 3 frames of flash.
|
||||||
|
// ex19.w counts it down: 5 at the hit, drawn while >= 3 (sprites.comp), a new flash at 0; + 8 for a critical hit. (Worm segments keep their root slot there.)
|
||||||
|
void hit_flash(uint id, bool hit, bool crit) {
|
||||||
|
const int c = int(ents[id].ex19.w + 0.5);
|
||||||
|
int n = max((c & 7) - 1, 0);
|
||||||
|
bool red = (c & 8) != 0;
|
||||||
|
if (hit && n == 0) { n = 5; red = crit; }
|
||||||
|
ents[id].ex19.w = n == 0 ? 0.0 : float(n + (red ? 8 : 0));
|
||||||
|
}
|
||||||
|
// What an entity deals: the DAMAGE_MULTIPLIER effect (the perk glass cannon) etc.; the host stores the factor in ex7.y (0 = none)
|
||||||
|
float dealt_mult(Ent s) { return s.ex7.y > 0.0 ? s.ex7.y : 1.0; }
|
||||||
|
|
||||||
|
bool liquid_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m == 0u) return false;
|
||||||
|
uint f = mflags(m);
|
||||||
|
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
|
||||||
|
}
|
||||||
|
bool line_clear(vec2 a, vec2 b) { // no solid cell between two points (3-cell steps)
|
||||||
|
vec2 d = b - a;
|
||||||
|
int n = int(length(d) / 3.0);
|
||||||
|
for (int i = 1; i < n; ++i) if (solid_cell_at(ivec2(floor(a + d * (float(i) / float(n)))))) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool box_hits(ivec2 o, vec4 box) {
|
||||||
|
ivec2 lo = o + ivec2(int(box.x), int(box.z)), hi = o + ivec2(int(box.y), int(box.w)); // hi exclusive
|
||||||
|
for (int y = lo.y; y < hi.y; ++y)
|
||||||
|
for (int x = lo.x; x < hi.x; ++x)
|
||||||
|
if (solid_at(ivec2(x, y))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blood: real particles of the creature's blood material that fly out along the hit and become cells where they land.
|
||||||
|
void blood_burst(uint seed, vec2 pos, vec2 dirv, int count, float speed, int matp1, uint colour) {
|
||||||
|
if (matp1 <= 0) return;
|
||||||
|
for (int k = 0; k < count && k < 64; ++k) {
|
||||||
|
float r1 = float(hash3(seed, uint(k), 1u) & 0xFFFFu) / 65536.0, r2 = float(hash3(seed, uint(k), 2u) & 0xFFFFu) / 65536.0;
|
||||||
|
vec2 v = dirv * (0.4 + r1 * 1.2) * speed + vec2((r2 - 0.5) * 1.4 * speed, -0.3 - r1 * 0.9 * speed);
|
||||||
|
emit_material_particle(pos + vec2(0.0, -3.0), v, 70.0 + r2 * 40.0, PART_GRAV, colour, matp1 - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liquid of a living hit (00ab9a90): the count does not depend on the damage, N = trunc((u * (40 - 20) + 20) * blood_multiplier + 0.5) (the incoming multiplier and the world
|
||||||
|
// multiplier are 1); the first droplet leaves along the hit, each next one is turned by a random angle (-0.6..0.6 rad) from the previous one, a speed of 0.5..1.25 times
|
||||||
|
// the base, at the hit point +-1.5. The velocity is 20 x s x the packet impulse (px/s): a projectile's velocity x ragdoll_force_multiplier (00c091f0), an explosion's radial force (00657960).
|
||||||
|
void hit_blood(uint seed, vec2 hit, vec2 dirv, float bloodMult, int matp1, uint colour, float speedMul) {
|
||||||
|
if (matp1 <= 0 || bloodMult <= 0.0) return;
|
||||||
|
const float u0 = float(hash3(seed, 0u, 11u) & 0x7FFFu) / 32767.0;
|
||||||
|
int n = int((u0 * 20.0 + 20.0) * bloodMult + 0.5);
|
||||||
|
vec2 v = dirv;
|
||||||
|
for (int k = 0; k < n && k < 64; ++k) {
|
||||||
|
float u1 = float(hash3(seed, uint(k), 12u) & 0x7FFFu) / 32767.0, u2 = float(hash3(seed, uint(k), 13u) & 0x7FFFu) / 32767.0;
|
||||||
|
float u3 = float(hash3(seed, uint(k), 14u) & 0x7FFFu) / 32767.0, u4 = float(hash3(seed, uint(k), 15u) & 0x7FFFu) / 32767.0;
|
||||||
|
float sp = 0.5 + 0.75 * u1, a = -0.6 + 1.2 * u2;
|
||||||
|
v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a));
|
||||||
|
emit_material_particle(vec2(hit.x - 1.5 + 3.0 * u4, hit.y - 1.5 + 3.0 * u3), v * (20.0 * sp / 60.0) * speedMul, 70.0 + u2 * 40.0, PART_GRAV, colour, matp1 - 1); // (20 s v px/s, v the packet impulse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits (see edits.comp): flames falling from a burning creature
|
||||||
|
bool extinguishes_at(ivec2 w) { // water and the like put a burning creature out
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && ((mats[3u * m + 2u].z >> 18) & 1u) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// script_damage_about_to_be_received (the host sets 65536 in ex8.x when the entity or a child has one): the hp a hit takes goes to a running total instead
|
||||||
|
// (ex14.w, milli-hp, written only by the entity's own thread); the host runs the scripts on what was added and changes the hp by their result
|
||||||
|
float take_damage(uint id, float v) {
|
||||||
|
if ((ents[id].ex8.x & 65536) != 0) { ents[id].ex14.w += v * 1000.0; return 0.0; }
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_GlobalInvocationID.x;
|
||||||
|
if (id >= uint(ctrl.inp.w)) return;
|
||||||
|
Ent e = ents[id];
|
||||||
|
int kind = e.st.x;
|
||||||
|
if (kind == 0 || (e.st.w & ST_DEAD) != 0) return;
|
||||||
|
const bool is_prop = kind == 4;
|
||||||
|
if (is_prop && e.ex7.y > 0.0) { // a corpse part with a liquid inventory: the particle emitter of the ragdoll piece (docs/BLOOD_EMITTER_LIFECYCLE.md)
|
||||||
|
// ex = (x_vel_min, x_vel_max, y_vel_min, y_vel_max) in px/s (local), ex2 = (next emit frame, mode 0 off / 1 death leakage (1-3 per emission) / 2 damage leakage (1), 0, mass),
|
||||||
|
// ex7 = (-, reserve, material + 1, -). A damage event (min_damage_to_leak 0.09, leak_on_damage_percent 1 compared with u in [0,1)) rewrites the velocity ranges from the clamped
|
||||||
|
// local hit offset (00bb6820) and switches to one droplet per emission; the interval is 1-2 frames either way, the schedule advances before the spawn, a request larger than the
|
||||||
|
// reserve emits nothing, the reserve only falls by the droplets that were created, and a blocked spawn cell ends the burst (emit_only_if_there_is_space).
|
||||||
|
float reserve = e.ex7.y;
|
||||||
|
vec4 vr = e.ex;
|
||||||
|
float nextf = e.ex2.x, mode = e.ex2.y;
|
||||||
|
const uint fr = uint(ctrl.inp2.x);
|
||||||
|
const int dmg = atomicExchange(ents[id].ex8.z, 0);
|
||||||
|
if (dmg > 90) { // 00bb6820: strictly more than min_damage_to_leak (0.09)
|
||||||
|
const vec2 rel = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001;
|
||||||
|
const uint sd = hash3(id, fr, 53u);
|
||||||
|
float u[8];
|
||||||
|
for (int k = 0; k < 8; ++k) u[k] = float(hash3(sd, uint(k), 54u) & 0x7FFFu) / 32767.0;
|
||||||
|
if (u[7] < 1.0) { // u < leak_on_damage_percent (1): fails only for the draw 32767 / 32767
|
||||||
|
vec2 d = leak_offset(e, rel);
|
||||||
|
const float dl = length(d);
|
||||||
|
d = dl > 1e-4 ? d / dl : vec2(0.0);
|
||||||
|
const float dx = d.x * (0.85 + 0.29999995 * u[0]), dy = d.y * (0.85 + 0.29999995 * u[1]);
|
||||||
|
const float speed = 100.0 * (0.7 + (1.1 - 0.7) * u[2]);
|
||||||
|
const float vx = speed * dx, vy = speed * dy;
|
||||||
|
vr = vec4(vx * (0.6 + 0.39999998 * u[3]), vx * (1.0 + 0.39999998 * u[4]), vy * (0.6 + 0.39999998 * u[5]), vy * (1.0 + 0.39999998 * u[6]));
|
||||||
|
mode = 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mode > 0.5 && float(fr) >= nextf && e.ex7.z > 0.0) {
|
||||||
|
const uint seed = hash3(id, fr, 51u);
|
||||||
|
nextf = float(fr) + ((e.ex2.z > 0.5 && mode < 1.5) ? 3.0 + float(hash3(seed, 3u, 58u) % 3u) : 1.0 + float(seed & 1u)); // the next emission is scheduled before anything is spawned (3-5 frames when the cosmetic spray exists and no damage has rewritten the emitter)
|
||||||
|
const int cnt = mode > 1.5 ? 1 : 1 + int(hash3(seed, 7u, 56u) % 3u);
|
||||||
|
if (reserve >= float(cnt)) {
|
||||||
|
const float ca = cos(e.a.z), sa = sin(e.a.z);
|
||||||
|
int made = 0;
|
||||||
|
for (int k = 0; k < cnt; ++k) {
|
||||||
|
const uint r = hash3(seed, uint(k), 52u);
|
||||||
|
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 57u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 57u) & 0xFFFFu) / 65536.0;
|
||||||
|
const vec2 lo = leak_offset(e, vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001);
|
||||||
|
const vec2 off = mode > 1.5 ? vec2(ca * lo.x - sa * lo.y, sa * lo.x + ca * lo.y) : vec2(0.0); // the damage event's offset (the hit point), turned with the body
|
||||||
|
const vec2 at = e.a.xy + off + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py); // rect (-1,-1) .. (1,1)
|
||||||
|
if (solid_cell_at(ivec2(floor(at)))) break;
|
||||||
|
vec2 v = vec2(mix(vr.x, vr.y, ux), mix(vr.z, vr.w, uy)) * (1.0 / 60.0); // px/s -> cells/frame
|
||||||
|
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y); // the entity's rotation
|
||||||
|
if (emit_material_particle(at, v, 70.0 + float((r >> 24) & 0x3Fu), PART_GRAV, uint(e.ex3.y), int(e.ex7.z) - 1)) ++made; // only the particles that were created cost reserve (00bc75e0)
|
||||||
|
}
|
||||||
|
reserve -= float(made);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.ex2.z > 0.5 && e.ex7.z > 0.0 && float(fr) >= e.ex6.z) { // blood_spray_create_some_cosmetic: a second emitter on the same reserve, 1-3 cosmetic droplets (no cells) every 1-2 frames
|
||||||
|
const uint seed2 = hash3(id, fr, 59u);
|
||||||
|
ents[id].ex6.z = float(fr) + 1.0 + float(seed2 & 1u);
|
||||||
|
const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 3u);
|
||||||
|
if (reserve >= float(cnt2)) {
|
||||||
|
float u2[8];
|
||||||
|
for (int k = 0; k < 8; ++k) u2[k] = float(hash3(seed2, uint(k), 60u) & 0x7FFFu) / 32767.0;
|
||||||
|
const vec2 d = e.ex6.xy * vec2(0.85 + 0.29999995 * u2[0], 0.85 + 0.29999995 * u2[1]); // the same formula as the death emitter, other draws
|
||||||
|
const float base = (360.0 + 0.7 * 1200.0 * u2[2]) * e.ex2.w, speed = 0.7 * base + (1.1 * base - 0.7 * base) * u2[3];
|
||||||
|
const vec2 v0 = speed * d;
|
||||||
|
const vec4 cr = vec4(v0.x * (0.8 + 0.19999999 * u2[4]), v0.x * (1.0 + 0.20000005 * u2[5]), v0.y * (0.8 + 0.19999999 * u2[6]), v0.y * (1.0 + 0.20000005 * u2[7]));
|
||||||
|
const float ca = cos(e.a.z), sa = sin(e.a.z);
|
||||||
|
int made = 0;
|
||||||
|
for (int k = 0; k < cnt2; ++k) {
|
||||||
|
const uint r = hash3(seed2, uint(k), 61u);
|
||||||
|
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 62u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 62u) & 0xFFFFu) / 65536.0;
|
||||||
|
const vec2 at = e.a.xy + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py);
|
||||||
|
if (solid_cell_at(ivec2(floor(at)))) break;
|
||||||
|
vec2 v = vec2(mix(cr.x, cr.y, ux), mix(cr.z, cr.w, uy)) * (1.0 / 60.0);
|
||||||
|
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y);
|
||||||
|
if (emit_particle_hit(at, v, 25.0 + float((r >> 24) & 0x1Fu), 0.1, uint(e.ex3.y), int(e.ex7.z) - 1) != 0xFFFFFFFFu) ++made; // a droplet without a cell: it fades in the air and stains where it lands (it owns no cell: 00716ce0)
|
||||||
|
}
|
||||||
|
reserve -= float(made);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ents[id].ex = vr; ents[id].ex2.x = nextf; ents[id].ex2.y = mode; ents[id].ex7.y = max(reserve, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 pos = e.a.xy, vel = e.a.zw;
|
||||||
|
ivec2 ip = ivec2(floor(pos));
|
||||||
|
// frozen unless the collision box (plus margin) is inside the simulated window
|
||||||
|
ivec2 wp = ip - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (wp.x < 3 || wp.y < 3 || wp.x > pc.gw - 3 || wp.y > pc.gh - 3) return;
|
||||||
|
|
||||||
|
uint rnd = hash3(id, uint(ctrl.inp2.x), 911u);
|
||||||
|
int flags = e.st.w;
|
||||||
|
int types = 0; // damage types taken this tick (creatures)
|
||||||
|
vec2 hitv = vec2(0.0); // direction of the hit
|
||||||
|
float dmgTick = 0.0; // damage taken this tick
|
||||||
|
int dir = e.st.y;
|
||||||
|
int timer = e.st.z;
|
||||||
|
float hp = e.hp.x, cooldown = max(e.hp.y - 1.0, 0.0);
|
||||||
|
int energy = e.x.y;
|
||||||
|
// pushed out of solid cells (sand piles landing on it, an explosion refilling, a wall appearing): find the nearest free spot above or beside
|
||||||
|
const bool rigidBody = is_prop && (flags & ST_RIGID) != 0; // moved by the rigid-body engine: its own mask must not count as an obstacle
|
||||||
|
if (kind != 0 && !rigidBody && box_hits(ip, e.box)) {
|
||||||
|
bool freed = false;
|
||||||
|
for (int k = 1; k <= 12 && !freed; ++k) {
|
||||||
|
const ivec2 cand[5] = ivec2[5](ivec2(0, -k), ivec2(-k, -k), ivec2(k, -k), ivec2(-k, 0), ivec2(k, 0));
|
||||||
|
for (int c = 0; c < 5 && !freed; ++c)
|
||||||
|
if (!box_hits(ip + cand[c], e.box)) { ip += cand[c]; pos = vec2(float(ip.x) + fract(pos.x), float(ip.y)); vel.y = min(vel.y, 0.0); freed = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool grounded = !rigidBody && box_hits(ip + ivec2(0, 1), e.box);
|
||||||
|
const bool wasGrounded = grounded;
|
||||||
|
bool liquid = liquid_at(ip);
|
||||||
|
if (kind == 1 || kind == 2 || kind == 3) { // stains: standing in water, oil, blood... leaves its status on the creature for a while (ex8.z: bits | frame << 8)
|
||||||
|
uint sm = 0u;
|
||||||
|
for (int sy = 0; sy < 2; ++sy) {
|
||||||
|
const ivec2 sp = ip + ivec2(0, -4 * sy) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (sp.x >= 0 && sp.y >= 0 && sp.x < pc.gw && sp.y < pc.gh) sm |= (gtab[cell_mat(cells[sp.y * pc.gw + sp.x])] >> 16) & 63u;
|
||||||
|
}
|
||||||
|
if (sm != 0u) {
|
||||||
|
const uint old = ((uint(ents[id].ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(ents[id].ex8.z) & 63u) : 0u;
|
||||||
|
ents[id].ex8.z = int((sm | old) | (uint(pc.frame & 0xFFFFFF) << 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// swimming (CharacterPlatformingComponent swim_*): with the buoyancy check point (CharacterDataComponent buoyancy_check_offset_y) under liquid a walker or
|
||||||
|
// the player is nearly weightless (swim_idle_buoyancy_coeff), every velocity is damped (swim_drag, swim_extra_horizontal_drag) and the up key swims up
|
||||||
|
const bool swimming = (kind == 1 || kind == 3) && (e.st.w & ST_SWIMMER) != 0 && liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))); // float coordinates truncate toward zero (00a7ea90)
|
||||||
|
float walk = e.mv.x, vmax = e.mv.y;
|
||||||
|
if (e.ex15.z > 0.5 && (int(e.ex15.w) & 32) != 0) walk *= 2.0; // the run button of a creature the host runs (00a25d50: it has a prey or a threat; 00a7ea90 doubles the speed)
|
||||||
|
float target = 0.0;
|
||||||
|
bool want_jump = false, thrust = false;
|
||||||
|
// a flyer's ControlsComponent: mButtonDownFly and mFlyingTargetY (ex18.z), set by the path steer (00a29b20 -> 00a8d340) and kept between them
|
||||||
|
bool flyOn = kind == 2;
|
||||||
|
float flyTY = e.ex18.w < -1.0e8 ? pos.y : e.ex18.z;
|
||||||
|
|
||||||
|
vec4 pl = ents[0].a; // the player (if there is one)
|
||||||
|
bool has_player = ctrl.inp2.y != 0;
|
||||||
|
bool player_alive = has_player && (ents[0].st.w & ST_DEAD) == 0;
|
||||||
|
const bool frozen = (e.ex8.x & 1) != 0; // a game effect with disable_movement (FROZEN, PARALYSIS...): no walking, jumping, attacking
|
||||||
|
|
||||||
|
if (kind == 3) { // ---- the player ----
|
||||||
|
target = frozen ? 0.0 : float(ctrl.inp.x) * walk;
|
||||||
|
vec2 aim_w = vec2(ctrl.inp3.xy) + vec2(float(pc.view_x), float(pc.view_y)); // aim point in world cells
|
||||||
|
if (ctrl.inp3.x >= 0) dir = aim_w.x >= pos.x ? 1 : -1; else if (ctrl.inp.x != 0) dir = ctrl.inp.x; // face the mouse
|
||||||
|
cooldown = max(cooldown - 0.0, 0.0);
|
||||||
|
if (ctrl.inp3.z != 0 && !frozen && cooldown <= 0.0 && ctrl.inp3.w >= 0) { // fire the spark bolt toward the mouse
|
||||||
|
PType t = ptypes[ctrl.inp3.w];
|
||||||
|
vec2 from = pos + vec2(0.0, -3.0);
|
||||||
|
vec2 dv = aim_w - from;
|
||||||
|
vec2 dirv = length(dv) > 0.5 ? normalize(dv) : vec2(float(dir), 0.0);
|
||||||
|
uint r2 = hash3(id, uint(ctrl.inp2.x), 77u);
|
||||||
|
float ang = (float(r2 & 255u) / 255.0 - 0.5) * 0.04;
|
||||||
|
dirv = vec2(dirv.x * cos(ang) - dirv.y * sin(ang), dirv.x * sin(ang) + dirv.y * cos(ang));
|
||||||
|
uint slot = uint(atomicAdd(ctrl.inp2.w, 1)) % 512u;
|
||||||
|
projs[slot].a = vec4(from, dirv * t.a.y);
|
||||||
|
projs[slot].b = vec4(t.a.z, t.a.w, t.b.x, t.b.y);
|
||||||
|
projs[slot].c = vec4(1.0, 0.0, 0.0, 0.0);
|
||||||
|
cooldown = 9.0;
|
||||||
|
}
|
||||||
|
want_jump = ctrl.inp.y != 0 && !frozen;
|
||||||
|
{ // the jetpack (CharacterDataComponent fly_time_max, fly_recharge_spd, fly_recharge_spd_ground): flying burns one frame of fuel per frame, on the ground it refills at 6 seconds per second,
|
||||||
|
// in the air the fuel trickles back at fly_recharge_spd as soon as the key is up (units of 1/64 frame)
|
||||||
|
const float emax = (e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0;
|
||||||
|
float en = float(energy);
|
||||||
|
thrust = want_jump && !grounded && en >= 64.0; // the jetpack works in liquids too (the swim correction comes after it)
|
||||||
|
if (grounded) en = min(emax, en + 6.0 * 64.0);
|
||||||
|
else if (thrust) en -= 64.0;
|
||||||
|
else en = min(emax, en + (e.ex2.y > 0.0 ? e.ex2.y : 0.4) * 64.0);
|
||||||
|
energy = int(en);
|
||||||
|
}
|
||||||
|
// damage taken from enemies this frame
|
||||||
|
int dmg = atomicExchange(ents[id].x.x, 0);
|
||||||
|
const float hpBeforeHit = hp;
|
||||||
|
{ const int pty = atomicExchange(ents[id].ex3.z, 0); hp -= take_damage(id, float(dmg) * 0.001); if (pty != 0) ents[id].ex3.w = (pty & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); hit_flash(id, dmg > 0, (pty & 0x10000) != 0); } // (the accumulator already holds the multiplied damage: every source applies the victim's multiplier of its type) (ex3.w: the types of this tick | tick << 8, for the damage sounds)
|
||||||
|
// TakeDamage (00ab9a90) on a lethal hit: SAVING_GRACE (effect 0x2a) leaves 1 / 25 hp (1 HP shown) when there was more, else an unspent RESPAWN (0x2d, extra life: ex8.x
|
||||||
|
// bit 131072, the host clears it) sets 100 / 25 hp, at most the maximum; the host is told (ctrl burnN.z / w) for the message and the PROTECTION_ALL of 60 / 30 frames
|
||||||
|
if (hp <= 0.0 && (e.ex8.x & 16) != 0 && hpBeforeHit > 0.04) { hp = 0.04; atomicAdd(ctrl.burnN.z, 1); }
|
||||||
|
else if (hp <= 0.0 && (ents[id].ex8.x & 131072) != 0) { hp = min(4.0, e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0); atomicAnd(ents[id].ex8.x, ~131072); atomicAdd(ctrl.burnN.w, 1); }
|
||||||
|
if (hp <= 0.0 && (e.ex8.x & 32768) == 0) { // respawn at the spawn point (a polymorphed body is not: the host ends the polymorph, 32768)
|
||||||
|
pos = vec2(float(e.x.z), float(e.x.w)); vel = vec2(0.0); ip = ivec2(floor(pos));
|
||||||
|
hp = e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0;
|
||||||
|
atomicAdd(ctrl.burnN.y, 1);
|
||||||
|
}
|
||||||
|
} else if (kind == 1 || kind == 2) { // ---- creatures ----
|
||||||
|
// the target of the creature: the player; a charmed creature (game effect CHARM) fights the nearest creature of another herd instead
|
||||||
|
vec4 tp = pl; bool tAlive = player_alive; int tgt = 0;
|
||||||
|
const bool berserk = (e.ex8.x & 4) != 0 && (e.ex8.x & 2) == 0; // BERSERK (00a28e10): it attacks every mortal, the player and its own herd included, unless berserk_dont_attack_friends (bit 3)
|
||||||
|
if ((e.ex8.x & 2) != 0 || berserk) {
|
||||||
|
float best = 1.0e9;
|
||||||
|
tgt = -1;
|
||||||
|
for (int k = berserk ? 0 : 1; k < ctrl.inp.w; ++k) {
|
||||||
|
if (k == int(id) || (ents[k].st.w & ST_DEAD) != 0 || (ents[k].ex8.x & 8192) != 0) continue; // (an invisible one is not seen)
|
||||||
|
if (k == 0 ? (!berserk || ents[0].st.x != 3) : (ents[k].st.x != 1 && ents[k].st.x != 2)) continue;
|
||||||
|
if (berserk ? ((e.ex8.x & 8) != 0 && ents[k].ex8.y == e.ex8.y) : ents[k].ex8.y == e.ex8.y) continue;
|
||||||
|
const float dk = distance(ents[k].a.xy, pos);
|
||||||
|
if (dk < best && abs(ents[k].a.x - pos.x) < e.ai.x && abs(ents[k].a.y - pos.y) < e.ai.y) { best = dk; tgt = k; }
|
||||||
|
}
|
||||||
|
tAlive = tgt >= 0;
|
||||||
|
if (tgt >= 0) tp = ents[tgt].a; else tgt = 0;
|
||||||
|
}
|
||||||
|
float dx = tp.x - pos.x, dy = tp.y - pos.y;
|
||||||
|
// sees the player: within the detection range and, for creatures with a limited field of view (creature_detection_angular_range_deg), inside the cone in front of them
|
||||||
|
vec2 toP = vec2(tp.x, tp.y - 4.0) - vec2(pos.x, pos.y + e.ex.y);
|
||||||
|
bool inCone = e.ex.x <= -0.999 || (length(toP) > 1.0 && dot(normalize(toP), vec2(float(dir), 0.0)) >= e.ex.x);
|
||||||
|
bool sees = tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && abs(dx) < e.ai.x && abs(dy) < e.ai.y && inCone;
|
||||||
|
types = atomicExchange(ents[id].ex3.z, 0); // damage types taken since the last tick
|
||||||
|
timer -= 1;
|
||||||
|
if (timer <= 0) { // pick a new idle/wander action
|
||||||
|
uint r = rnd;
|
||||||
|
if ((r % 3u) == 0u) timer = 20 + int((r >> 4) % 50u);
|
||||||
|
else { dir = ((r >> 8) & 1u) == 0u ? -1 : 1; timer = 60 + int((r >> 4) % 120u); }
|
||||||
|
e.x.y = (r % 3u) == 0u ? 1 : 0; // 1 = idling for this action
|
||||||
|
}
|
||||||
|
bool idle = e.x.y == 1;
|
||||||
|
if (sees) { dir = dx >= 0.0 ? 1 : -1; idle = false; }
|
||||||
|
if (e.ex15.z > 0.5) {
|
||||||
|
// the host's AnimalAI (src/ai, AnimalAISystem 00a22540 + PathFindingSystem 00bca030): mode 1 heads for the next node of its path (a flyer in both axes), mode 2 stands;
|
||||||
|
// the attacks below stay the GPU's
|
||||||
|
target = 0.0;
|
||||||
|
if ((int(e.ex15.w) & 2) != 0 || (int(e.ex15.w) & 8) != 0) target = vel.x; // (from the launch on, also on the ground: a lob down off a ledge starts into the floor and slides off the edge at its speed) // a jump of the path: no control in the air (a dash: for its whole flight time)
|
||||||
|
else if (e.ex15.z < 1.5) {
|
||||||
|
const float sdx = e.ex15.x - pos.x;
|
||||||
|
if (abs(sdx) > 1.0) { dir = sdx > 0.0 ? 1 : -1; target = float(dir) * walk; }
|
||||||
|
// a flyer (00a29b20): the fly key with the node's y as mFlyingTargetY; a can_walk flyer with ground within 10 px below and the node not above it lets go
|
||||||
|
// of the key and walks (the host's flag 16)
|
||||||
|
if (kind == 2) { flyOn = (int(e.ex15.w) & 16) == 0; if (flyOn) flyTY = e.ex15.y; }
|
||||||
|
// (a fish swims up and down by its buoyancy: the swim correction below)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!idle) target = float(dir) * walk;
|
||||||
|
if (kind == 1) {
|
||||||
|
// do not walk off ledges while wandering; turn around at walls (handled below), jump after the player
|
||||||
|
if (!sees && grounded && !idle) {
|
||||||
|
ivec2 ahead = ip + ivec2(dir * 5, 0);
|
||||||
|
if (!box_hits(ahead + ivec2(0, 4), e.box) && !box_hits(ahead + ivec2(0, 8), e.box)) { dir = -dir; target = float(dir) * walk; }
|
||||||
|
}
|
||||||
|
} else if (sees) flyTY = tp.y - 8.0; // flyer without the host's AI: the fly key, toward the player's height when it sees one (else it holds the last height)
|
||||||
|
}
|
||||||
|
{ // hits: projectiles and explosions (damage accumulator), the blood sprays out along the hit
|
||||||
|
int dmg = atomicExchange(ents[id].x.x, 0);
|
||||||
|
hp -= take_damage(id, float(dmg) * 0.001); // (already multiplied at the source by the victim's multiplier of the damage type)
|
||||||
|
dmgTick += float(dmg) * 0.001;
|
||||||
|
hit_flash(id, dmg > 0, (types & 0x10000) != 0);
|
||||||
|
if (dmg > 0) {
|
||||||
|
hitv = vec2(float(atomicExchange(ents[id].aux.x, 0)), float(atomicExchange(ents[id].aux.y, 0))) * 0.001;
|
||||||
|
hit_blood(hash3(id, uint(ctrl.inp2.x), 31u), pos + vec2(0.0, 0.5 * (e.box.z + e.box.w)), hitv, e.ex7.x, e.ex3.x, uint(e.ex3.y), 1.0); // (hitv: the packet impulse, px/s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// melee attack
|
||||||
|
if (tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && e.ex.z > 0.5 && abs(dx) < e.ai.z && abs(dy) < e.ai.z + 4.0 && cooldown <= 0.0) {
|
||||||
|
atomicAdd(ents[tgt].x.x, int(e.ai.w * dealt_mult(e) * ents[tgt].ex9.w * 1000.0)); // the target's melee multiplier
|
||||||
|
if (e.ai.w > 0.0 && e.ai.w * dealt_mult(e) * ents[tgt].ex9.w <= 0.0 && ents[tgt].st.x >= 1 && ents[tgt].st.x <= 3) ents[tgt].ex11.w = float(pc.frame); // (a blocked hit: BLOCK on the host)
|
||||||
|
ents[id].rg2.w = 1 | ((ctrl.inp2.x & 0xFFFFFF) << 4);
|
||||||
|
atomicOr(ents[tgt].ex3.z, 8);
|
||||||
|
ents[tgt].ex8.w = int(id + 1) | ((pc.frame & 0xFFFFF) << 12); // the target was hit by this creature
|
||||||
|
cooldown = 50.0;
|
||||||
|
}
|
||||||
|
// ranged attack: cast the creature's projectile (a node of the GPU projectile system, team 2) at the player when in range with a clear line
|
||||||
|
int rcd = max(e.x.z - 1, 0);
|
||||||
|
if (e.rg.x >= 0 && tAlive && !frozen) {
|
||||||
|
vec2 from = pos + vec2(0.0, float(e.rg2.z)), to = tp.xy + vec2(0.0, -4.0);
|
||||||
|
float dist = length(to - from);
|
||||||
|
if (sees && rcd == 0 && dist >= float(e.rg.z) && dist <= float(e.rg.w) && line_clear(from, to)) {
|
||||||
|
int count = e.rg2.x + int(rnd % uint(e.rg2.y - e.rg2.x + 1));
|
||||||
|
float speed = max(e.ex2.x, 0.1), tt = dist / speed;
|
||||||
|
vec2 aimv = (to - from) / tt + vec2(0.0, -0.5 * e.ex2.y * tt); // launch velocity that compensates the gravity over the flight time
|
||||||
|
for (int k = 0; k < count; ++k) {
|
||||||
|
uint r2 = hash3(id, uint(ctrl.inp2.x), uint(k) + 5u);
|
||||||
|
float ang = (float((r2 >> 8) & 255u) / 255.0 - 0.5) * (0.08 + 0.05 * float(count - 1));
|
||||||
|
vec2 d0 = normalize(aimv);
|
||||||
|
vec2 dirv = vec2(d0.x * cos(ang) - d0.y * sin(ang), d0.x * sin(ang) + d0.y * cos(ang));
|
||||||
|
int list = pc.frame & 1;
|
||||||
|
int slot = list == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1);
|
||||||
|
if (slot < 256) {
|
||||||
|
hcast[(list * 256 + slot) * 2] = vec4(from, dirv);
|
||||||
|
hcast[(list * 256 + slot) * 2 + 1] = vec4(float(e.rg.x), float(2 + (e.ex8.y << 3)), float(r2 & 0xFFFFFFu), float(1 + (id + 1) * 2)); // r1.w: muzzle flash flag, the shooter's slot + 1 in the higher bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rcd = e.rg.y;
|
||||||
|
ents[id].rg2.w = 2 | ((ctrl.inp2.x & 0xFFFFFF) << 4); // the attack of the tick (1 melee, 2 ranged | tick << 4): the host plays its sprite events (attack sounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.x.z = rcd;
|
||||||
|
// (the player's kick on creatures is the host's: KickSystem 00bf2040, 18 frames after the press - its damage goes through the damage accumulator)
|
||||||
|
if (hp <= 0.0) flags |= ST_DEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frozen) { target = 0.0; want_jump = false; thrust = false; }
|
||||||
|
// soft separation: creatures and the player do not stack on top of each other
|
||||||
|
// (characters do not push each other: the engine has no creature-creature collision - CharacterCollisionComponent only collides with the world)
|
||||||
|
// ---- damage: projectiles and explosions (the atomic accumulator), contact with harmful materials, fire ----
|
||||||
|
if (is_prop && e.hp.x < 1.0e8) {
|
||||||
|
int dmg = atomicExchange(ents[id].x.x, 0);
|
||||||
|
hp -= float(dmg) * 0.001; // (the material damage of 00ab6010 / 00ab7410 comes through this accumulator from the host)
|
||||||
|
hit_flash(id, dmg > 0, (atomicExchange(ents[id].ex3.z, 0) & 0x10000) != 0);
|
||||||
|
if (hp <= 0.0) flags |= ST_DEAD;
|
||||||
|
}
|
||||||
|
if (kind == 1 || kind == 3 || kind == 2) { // fire hurts: cells of fire sources or burning cells overlapping the box
|
||||||
|
int burn = 0;
|
||||||
|
ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w));
|
||||||
|
for (int y = lo.y; y < hi.y; ++y)
|
||||||
|
for (int x = lo.x; x < hi.x; ++x) {
|
||||||
|
ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
uint cw = cells[q.y * pc.gw + q.x];
|
||||||
|
uint cm = cell_mat(cw);
|
||||||
|
if (cm != 0u && (((mats[3u * cm + 2u].z >> 17) & 1u) != 0u || (cw & BURN_BIT) != 0u)) ++burn;
|
||||||
|
}
|
||||||
|
if (burn > 0) types |= 4; // (touching fire does no damage of its own: only the materials of materials_that_damage and the burning do)
|
||||||
|
// ---- burning (DamageModelSystem): ignition in the cell scan 00ab6010, the burning update 00ab7a70 ----
|
||||||
|
// ignition (fire_probability_of_ignition != 0): the cells under the body that burn or are of an on_fire material (on_fire counts 2, not the creature's own sparks)
|
||||||
|
// add up; above 4, rand(0, 100) < the strength (fade) of such a cell sets mFireFramesLeft to max(left, seconds x 60): 12 s, 0 with a status that protects from
|
||||||
|
// fire (WET, OILED, BLOODY, SLIMY, RADIOACTIVE), 20 s ALCOHOLIC, 30 s OILED (in that order, the last one wins)
|
||||||
|
const uint stains = ((uint(e.ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(e.ex8.z) & 63u) : 0u;
|
||||||
|
const bool fireProt = (stains & (1u | 2u | 4u | 16u | 32u)) != 0u;
|
||||||
|
float fireLeft = e.ex4.w, fireProb = e.ex16.z;
|
||||||
|
if (e.ex2.z > 0.0) {
|
||||||
|
int cnt = 0, fade = 0;
|
||||||
|
ivec2 lo2 = ip + ivec2(int(e.box.x), int(e.box.z)), hi2 = ip + ivec2(int(e.box.y), int(e.box.w));
|
||||||
|
for (int y = lo2.y; y < hi2.y; ++y)
|
||||||
|
for (int x = lo2.x; x < hi2.x; ++x) {
|
||||||
|
ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
uint cw = cells[q.y * pc.gw + q.x];
|
||||||
|
uint cm = cell_mat(cw);
|
||||||
|
if (cm == 0u) continue;
|
||||||
|
const bool onFireMat = ((mats[3u * cm + 2u].z >> 17) & 1u) != 0u;
|
||||||
|
if (!onFireMat && (cw & BURN_BIT) == 0u) continue;
|
||||||
|
cnt += onFireMat ? 2 : 1;
|
||||||
|
int f = int(birth[q.y * pc.gw + q.x]) >> 24; // (the cell's fade, its strength: a fire cell without one has its temperature_of_fire)
|
||||||
|
if (f == 0 && (cw & BURN_BIT) == 0u) f = int((mats[3u * cm + 2u].w >> 8) & 255u);
|
||||||
|
fade = f;
|
||||||
|
}
|
||||||
|
if (cnt > 4 && int(hash3(id, uint(ctrl.inp2.x), 61u) % 101u) < fade) {
|
||||||
|
float secs = 12.0;
|
||||||
|
if (fireProt) secs = 0.0;
|
||||||
|
if ((stains & 8u) != 0u) secs = 20.0;
|
||||||
|
if ((stains & 2u) != 0u) secs = 30.0;
|
||||||
|
fireLeft = max(fireLeft, floor(secs * 60.0 + 0.5));
|
||||||
|
if (e.ex4.w <= 0.0 && fireLeft > 0.0) fireProb = 50.0; // (mIsOnFire changes: mFireProbability 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fireLeft > 0.0) {
|
||||||
|
// 00ab7a70: fire_how_much_fire_generates times a random point of the hitbox: an empty cell gets a spark (the creature's own: it does not ignite it), a burning cell
|
||||||
|
// keeps the fire going (mFireProbability + 1, at most 100), each liquid cell met counts 3 against it when more than half of the tries met liquid; out when
|
||||||
|
// mFireProbability or mFireFramesLeft (-1 a frame, -2 with a fire-protecting status, +1 OILED) reaches 0. The damage while it burns: max(0.8, max hp) x 0.02
|
||||||
|
// per second, twice that for anything but the player (fire_damage_amount only switches it on; the engine delivers the sum every 9 - 12 frames, here each frame)
|
||||||
|
const int nTry = max(int(e.ex2.z) - 1, 0);
|
||||||
|
int lit = 0, wet = 0;
|
||||||
|
const uint spark = mats[mats.length() - 2u].x;
|
||||||
|
for (int k = 0; k < nTry && k < 10; ++k) {
|
||||||
|
const float rx = float(hash3(id, uint(ctrl.inp2.x), 70u + uint(k)) & 0xFFFFu) / 65536.0, ry = float(hash3(id, uint(ctrl.inp2.x), 90u + uint(k)) & 0xFFFFu) / 65536.0;
|
||||||
|
const ivec2 w = ivec2(floor(pos + vec2(e.box.x + (e.box.y - e.box.x) * rx, e.box.z + (e.box.w - e.box.z) * ry)));
|
||||||
|
const ivec2 q = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
const int qi = q.y * pc.gw + q.x;
|
||||||
|
uint cw = cells[qi];
|
||||||
|
if (cell_mat(cw) == 0u && spark != 0u && atomicCompSwap(cells[qi], 0u, spark | (hash3(id, uint(k), 65u) & 0xFF0000u)) == 0u) { birth[qi] = birth_frame(); lit = 1; continue; }
|
||||||
|
cw = cells[qi];
|
||||||
|
const uint cm = cell_mat(cw);
|
||||||
|
if (cm != 0u && ((cw & BURN_BIT) != 0u || (mflags(cm) & F_FIRE) != 0u)) lit = 1;
|
||||||
|
else if (cm != 0u && (mflags(cm) & F_LIQUID) != 0u) wet += 3;
|
||||||
|
}
|
||||||
|
fireProb = min(fireProb + float(lit), 100.0);
|
||||||
|
if (nTry / 2 < wet) fireProb -= float(wet);
|
||||||
|
fireLeft -= 1.0; // (the extra -1 of a fire-protecting status and the +1 of OILED: the host, from the status system's stain amounts)
|
||||||
|
if (lit == 1 && e.ex2.w != 0.0) {
|
||||||
|
const float maxhp = kind == 3 ? float(e.aux.x) * 0.001 : float(e.x.w) * 0.001;
|
||||||
|
hp -= take_damage(id, max(0.8, maxhp) * 0.02 / 60.0 * (kind == 3 ? 1.0 : 2.0) * e.ex9.z);
|
||||||
|
}
|
||||||
|
types |= 4;
|
||||||
|
if (fireProb < 1.0 || fireLeft < 1.0) { fireLeft = 0.0; fireProb = 0.0; }
|
||||||
|
}
|
||||||
|
ents[id].ex16.z = fireProb;
|
||||||
|
ents[id].ex4.w = fireLeft;
|
||||||
|
// air (00ab81b0, frames here): with the head in liquid air_in_lungs runs down 1 per frame; out of it (ex16.w counts the frames) it refills 1 per frame, 5 after
|
||||||
|
// 20 frames, up to air_in_lungs_max; at 0.2 s or less of air (also while it refills) air_lack_of_damage x dt is lost each frame
|
||||||
|
if (e.ex4.y > 0.0) {
|
||||||
|
float air = e.ex4.x, outF = e.ex16.w;
|
||||||
|
if (liquid_at(ip + ivec2(0, int(e.box.z)))) { outF = 0.0; air -= 1.0; }
|
||||||
|
else { outF = min(outF + 1.0, 1.0e6); if (air < e.ex4.y) air = min(e.ex4.y, air + (outF > 20.0 ? 5.0 : 1.0)); }
|
||||||
|
if (air <= 12.0) { air = max(air, 0.0); hp -= take_damage(id, e.ex4.z); types |= 32; }
|
||||||
|
ents[id].ex4.x = air; ents[id].ex16.w = outF;
|
||||||
|
}
|
||||||
|
if (kind != 3 && hp <= 0.0) flags |= ST_DEAD;
|
||||||
|
if ((types & (1 | 2 | 8)) != 0) ents[id].ex3.w = (types & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); // the damage types of this tick | tick << 8 (the host's damage sounds, 00ab9a90)
|
||||||
|
}
|
||||||
|
if ((kind == 1 || kind == 2) && (flags & ST_DEAD) != 0 && (e.st.w & ST_DEAD) == 0) { // it dies now: blood bursts, the host spawns the corpse and runs the death scripts
|
||||||
|
float maxhp = float(e.x.w) * 0.001;
|
||||||
|
// KillMe (00abb7d0): the RAGDOLL_FX of the killing hit (bits 12-15; the packet's default NORMAL), a NORMAL projectile or explosion kill turns into BLOOD_SPRAY when
|
||||||
|
// rand % 100 + 1 <= 20 (01153024); BLOOD_EXPLOSION (2) makes the parts without joints: the gib (ragdoll_fx_forced is the host's)
|
||||||
|
int rfx = (types >> 12) & 15;
|
||||||
|
if (rfx == 0 && (types & 3) != 0) rfx = 1;
|
||||||
|
if (rfx == 1 && (types & 3) != 0 && int(hash3(id, uint(ctrl.inp2.x), 77u) % 100u) + 1 <= 20) rfx = 3;
|
||||||
|
bool gib = rfx == 2;
|
||||||
|
float hl = length(hitv);
|
||||||
|
vec2 hd = hl > 0.01 ? hitv / hl : vec2(0.0, -1.0);
|
||||||
|
int n = 14 + min(int(maxhp * 30.0), 24);
|
||||||
|
// no burst of its own: the lethal hit already made its liquid, the corpse parts carry the rest of the blood (finite reserves, see the part branch above)
|
||||||
|
ents[id].ex3.w = (types & 0xEF) | (gib ? 16 : 0) | ((ctrl.inp2.x & 0xFFFFF) << 8) | (rfx << 28); // (bits 28-31: the RAGDOLL_FX of the death)
|
||||||
|
ents[id].aux.z = int(hitv.x * 1000.0); ents[id].aux.w = int(hitv.y * 1000.0);
|
||||||
|
}
|
||||||
|
if (is_prop && (flags & ST_HOVER) != 0) {
|
||||||
|
const vec2 home = e.ex6.zw;
|
||||||
|
ents[id].a = vec4(home.x, home.y + 2.0 * sin(3.0 * float(pc.frame) / 60.0), 0.0, 0.0); // 00b83980 (ItemSystem, play_hover_animation): home y + 2 sin(3 t), t in seconds; the sprite turned by -pi/2
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (is_prop && (flags & ST_RIGID) != 0) {
|
||||||
|
ents[id].st = ivec4(kind, dir, timer, flags);
|
||||||
|
ents[id].hp = vec4(hp, cooldown, e.hp.z, e.hp.w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- physics ----
|
||||||
|
bool fly = kind == 2 && flyOn;
|
||||||
|
if (kind == 2) ents[id].ex18.zw = vec2(flyTY, fly ? (e.ex18.w < -1.0e8 ? flyTY : (e.ex18.y > 0.5 ? e.ex18.w + (flyTY - e.ex18.w) * 0.05 : flyTY)) : -1.0e9);
|
||||||
|
if (fly && (int(e.ex15.w) & 8) == 0) { // (a flyer's dash: the host's velocity for its flight)
|
||||||
|
// 00a7ea90, fly_model_player 0: mSmoothedFlyingTargetY eases to mFlyingTargetY by 0.05 a frame (fly_smooth_y; set at once when the key goes down); the vertical speed
|
||||||
|
// goes toward clamp(fly_speed_mult x (smoothed - y) - g dt, -fly_speed_max_up, fly_speed_max_down) by fly_speed_change_spd (snapped at the target), then the gravity
|
||||||
|
const float sY = ents[id].ex18.w;
|
||||||
|
const float tv = clamp(e.ex17.x * (sY - pos.y) / 60.0 - e.mv.w, -e.ex17.z, e.ex17.w);
|
||||||
|
const float nv = vel.y + e.ex17.y * (tv - vel.y);
|
||||||
|
vel.y = (sign(tv - nv) != sign(tv - vel.y)) ? tv : nv;
|
||||||
|
vel.y += e.mv.w;
|
||||||
|
} else if (!fly) {
|
||||||
|
vel.y += e.mv.w * ctrl.view.w; // gravity; the swim correction (docs/WATER_PHYSICS.md) follows the input staging below
|
||||||
|
if (kind == 4) vel *= 1.0 - e.ex5.x; // 00c370c0: air friction of the item's VelocityComponent (per frame) after gravity, then the liquid drag, then the terminal speed (vector)
|
||||||
|
else vel.y = min(vel.y, e.hp.z);
|
||||||
|
if (grounded && vel.y > 0.0) vel.y = 0.0; // resting on the ground: no gravity build-up (it made the sub-cell height creep 0.97 and snap back every 5 ticks)
|
||||||
|
}
|
||||||
|
// The knockback state of the platforming update (00a7ea90 lines 384-433): after a hit with knockback the horizontal velocity stays the kick's x (+0x148) and the controls are off; it ends
|
||||||
|
// after at least 3 frames when the body is on the ground (is_on_ground), stood in liquid at the buoyancy probe (x, y + buoyancy_check_offset_y) or collided horizontally
|
||||||
|
// the frame before (mCollidedHorizontally), and for the player after 60 frames (+0x14c, set to 60 by the kick 00ab9910; precision_jumping_max_duration_frames is
|
||||||
|
// -1 = no limit in every file)
|
||||||
|
vec4 kst = e.ex10; // the knockback state: x = the kick's horizontal velocity, y = frames since, z = 1 in the state (2 when it also hit a wall the frame before)
|
||||||
|
if ((kind == 1 || kind == 3) && (e.ex11.y != 0.0 || e.ex11.z != 0.0)) { // a queued kick (00a7ea90 lines 384-395): added to the staged velocity, CONFUSION inverts its x, it becomes the state's horizontal velocity
|
||||||
|
vec2 kk = e.ex11.yz;
|
||||||
|
if ((e.ex8.x & 128) != 0) kk.x = -kk.x;
|
||||||
|
vel += kk;
|
||||||
|
kst = vec4(kk.x, 0.0, 1.0, e.ex10.w);
|
||||||
|
ents[id].ex11.yz = vec2(0.0);
|
||||||
|
}
|
||||||
|
bool knocked = (kind == 1 || kind == 3) && kst.z > 0.5;
|
||||||
|
if (knocked) {
|
||||||
|
const float cnt = kst.y + 1.0;
|
||||||
|
if ((cnt >= 3.0 && (grounded || kst.z > 1.5 || liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))))) || (kind == 3 && cnt >= 60.0)) { ents[id].ex10.xyz = vec3(0.0); knocked = false; }
|
||||||
|
else { ents[id].ex10.xyz = vec3(kst.x, cnt, 1.0); vel.x = kst.x; }
|
||||||
|
}
|
||||||
|
if (!knocked) {
|
||||||
|
if (kind == 3 && thrust) { const float lf = (e.ex8.x & 16384) != 0 ? 1.75 : 1.0; vel.y = max(vel.y - 0.3 * lf, -1.45 * lf); } // levitation (FASTER_LEVITATION 0x4b: the two flying speeds of CharacterPlatformingComponent x 1.75)
|
||||||
|
else if (want_jump && grounded && kind != 2 && (kind == 3 || (flags & ST_CANJUMP) != 0)) vel.y = e.mv.z; // jump
|
||||||
|
if (kind == 2 && fly) target = sign(target) * e.ex18.x; // (flying: fly_velocity_x instead of run_velocity, 00a7ea90)
|
||||||
|
if (kind == 1 || kind == 2 || kind == 3) { // 00a7ea90: v += accel_x x (target - v) each frame (accel_x_air while flying in the air), the target when it would pass it
|
||||||
|
const float f = ((kind == 3 && thrust) || fly) && !grounded ? e.ex16.y : e.ex16.x;
|
||||||
|
const float nv = vel.x + f * (target - vel.x);
|
||||||
|
vel.x = (sign(target - nv) != sign(target - vel.x)) ? target : nv;
|
||||||
|
} else vel.x += clamp(target - vel.x, -0.12, 0.12);
|
||||||
|
// (the lob of a path jump keeps its speed from the launch until the host ends the jump: on the launch tick the creature still stands on the ground - capped
|
||||||
|
// there, a hop flew at walking speed and fell back where it started, and a lob down off a ledge never left it: zombies jumping in place)
|
||||||
|
if (!(((kind == 1) && (int(e.ex15.w) & 2) != 0) || ((kind == 1 || kind == 2) && (int(e.ex15.w) & 8) != 0))) vel.x = clamp(vel.x, -vmax, vmax); // (a dash keeps its speed for its flight, a flyer's too) // (the lob of a path jump keeps its speed)
|
||||||
|
}
|
||||||
|
// player splash (00a7ea90 / 00a809f0), queued BEFORE the swim correction below (the speed it tests and throws is the one before buoyancy and drag): the first swimming frame after more than 15 frames out of the liquid, faster than 40 px/s, falling, not on the ground: the 11x11 square
|
||||||
|
// around the position throws its liquid forward, v = s Rot(a) v_entity, s = 0.1 + 0.7 u1, a = 0.4 u2 - 0.2 (the original draws two numbers for every one of the 121 places)
|
||||||
|
if (kind == 3) {
|
||||||
|
float fns = e.ex6.y;
|
||||||
|
if (swimming) {
|
||||||
|
if (fns > 15.0 && length(vel) * 60.0 > 40.0 && vel.y > 0.0 && !grounded)
|
||||||
|
for (int dy = -5; dy <= 5; ++dy) for (int dx = -5; dx <= 5; ++dx) {
|
||||||
|
const uint rs = hash3(uint(id) * 131u + uint(dx + 5 + 11 * (dy + 5)), uint(pc.frame), 73u);
|
||||||
|
const float s1 = 0.1 + 0.7 * float(rs & 0x7FFFu) / 32767.0, a = 0.4 * float((rs >> 15) & 0x7FFFu) / 32767.0 - 0.2;
|
||||||
|
displace_cell(ivec2(pos) + ivec2(dx, dy), s1 * (mat2(cos(a), sin(a), -sin(a), cos(a)) * vel));
|
||||||
|
}
|
||||||
|
fns = 0.0;
|
||||||
|
} else fns += 1.0;
|
||||||
|
ents[id].ex6.y = fns;
|
||||||
|
}
|
||||||
|
if (swimming && !fly) {
|
||||||
|
// 00a7ea90: after gravity, input, jetpack: b = down ? swim_down : fly key ? swim_up : swim_idle; vx = extra * (drag * vx); vy = drag * (vy - b * G), G = pixel_gravity * dt
|
||||||
|
const float G = e.mv.w * ctrl.view.w;
|
||||||
|
// Down has priority over Fly (the down key is ControlsComponent.mButtonDownDown, the up one mButtonDownFly): down gives swim_down_buoyancy_coeff, fly swim_up_, nothing swim_idle_
|
||||||
|
const bool downKey = kind == 3 && ctrl.inp3.z != 0 && !frozen;
|
||||||
|
const float fishDy = (int(e.ex15.w) & 4) != 0 && e.ex15.z > 0.5 && e.ex15.z < 1.5 ? e.ex15.y - pos.y : 0.0; // a fish's up / down buttons (AdvancedFishAI's ControlsComponent)
|
||||||
|
const float b = kind == 3 ? (downKey ? e.ex6.x : (want_jump ? e.ex5.y : e.ex5.x)) : (fishDy > 2.0 ? e.ex6.x : (fishDy < -2.0 ? e.ex5.y : (want_jump && !grounded ? e.ex5.y : e.ex5.x)));
|
||||||
|
vel.x = e.ex5.w * (e.ex5.z * vel.x);
|
||||||
|
vel.y = e.ex5.z * (vel.y - b * G);
|
||||||
|
}
|
||||||
|
// 00c370c0 counters: only when the integer position changed, 3x3 count of liquid that is not powder; displacement 75 % of the time when on the ground
|
||||||
|
{
|
||||||
|
ivec2 last = ivec2(e.ex6.zw);
|
||||||
|
const ivec2 tp = ivec2(pos); // truncation toward zero (00c370c0)
|
||||||
|
if (tp != last) {
|
||||||
|
int n = 0;
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(tp + ivec2(dx, dy))) ++n;
|
||||||
|
ents[id].ex6.z = float(tp.x); ents[id].ex6.w = float(tp.y);
|
||||||
|
if (n > 0 && kind == 4) vel *= 1.0 - e.ex5.y * (1.0 / 60.0) * float(n); // 00c370c0: an item with a VelocityComponent loses liquid_drag * dt * N of its velocity (unclamped)
|
||||||
|
if (n > 0 && (kind == 1 || kind == 2) && !swimming && e.ex10.w > 0.0) vel *= 1.0 - e.ex10.w * (1.0 / 60.0) * float(n); // 00c370c0 liquid drag of creatures that do not swim (liquid_drag of VelocityComponent, default 1)
|
||||||
|
if (n > 0 && grounded) { // every qualifying cell: r % 101 < 75, then it leaves as a particle with -Rot(a) * 0.1 v, a = 0.6 u - 0.3 (the velocity of a cell per frame is the velocity per second / 60 for both)
|
||||||
|
for (int dy = 1; dy >= -1; --dy) for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
const uint rs = hash3(uint(id) * 9u + uint(dx + 1 + 3 * (dy + 1)), uint(pc.frame), 71u);
|
||||||
|
if (rs % 101u >= 75u) continue;
|
||||||
|
const float ang = 0.6 * float((rs >> 8) & 0x7FFFu) / 32767.0 - 0.3;
|
||||||
|
displace_cell(tp + ivec2(dx, dy), -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind == 4) { const float sp = length(vel); if (sp > e.hp.z) vel *= e.hp.z / sp; } // 00c370c0: the terminal speed limits the vector length and rescales both axes
|
||||||
|
|
||||||
|
// horizontal move in one-cell steps, climbing small steps when standing
|
||||||
|
float nx = pos.x + vel.x;
|
||||||
|
int steps = int(floor(nx)) - ip.x;
|
||||||
|
int sx = steps > 0 ? 1 : -1;
|
||||||
|
bool blocked = false;
|
||||||
|
for (int s = 0; s < abs(steps); ++s) {
|
||||||
|
ivec2 np = ip + ivec2(sx, 0);
|
||||||
|
if (!box_hits(np, e.box)) { ip = np; continue; }
|
||||||
|
bool ok = false;
|
||||||
|
if (grounded) { // (a flyer skimming the floor climbs its small steps too: it stopped dead at a plank's edge)
|
||||||
|
for (int k = 1; k <= int(e.hp.w) && !ok; ++k) {
|
||||||
|
ivec2 cp = np - ivec2(0, k);
|
||||||
|
if (!box_hits(cp, e.box)) { ip = cp; ok = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) { blocked = true; break; }
|
||||||
|
}
|
||||||
|
pos.x = blocked ? float(ip.x) + fract(pos.x) : nx;
|
||||||
|
if (blocked && !(kind == 1 && (int(e.ex15.w) & 2) != 0)) vel.x = 0.0;
|
||||||
|
if (knocked) ents[id].ex10.z = blocked ? 2.0 : 1.0; // (mCollidedHorizontally for the knockback end) // (the lob of a path jump keeps its speed against a wall: it rises past a steep slope's edge and goes on, not straight up and back down where it started)
|
||||||
|
// vertical move
|
||||||
|
pos.y = float(ip.y) + fract(pos.y);
|
||||||
|
float ny = pos.y + vel.y;
|
||||||
|
int vsteps = int(floor(ny)) - ip.y;
|
||||||
|
int sy = vsteps > 0 ? 1 : -1;
|
||||||
|
bool hit = false;
|
||||||
|
for (int s = 0; s < abs(vsteps); ++s) {
|
||||||
|
ivec2 np = ip + ivec2(0, sy);
|
||||||
|
if (!box_hits(np, e.box)) { ip = np; continue; }
|
||||||
|
hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (hit) { pos.y = float(ip.y); vel.y = 0.0; } else pos.y = ny;
|
||||||
|
grounded = box_hits(ip + ivec2(0, 1), e.box);
|
||||||
|
if (wasGrounded && !grounded && (kind == 1 || kind == 3) && vel.y > -0.05 && vel.y < 0.6 && !swimming) { // walking down a slope: the body follows the ground (up to the climb height) instead of hopping from step to step
|
||||||
|
for (int k = 1; k <= int(max(e.hp.w, 4.0)); ++k)
|
||||||
|
if (!box_hits(ip + ivec2(0, k), e.box) && box_hits(ip + ivec2(0, k + 1), e.box)) { ip.y += k; pos.y = float(ip.y); vel.y = 0.0; grounded = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// creatures: turn around at walls, hop over obstacles when they can jump - only the GPU's own fallback walkers: a creature the host runs (ex15.z) moves by its
|
||||||
|
// path alone (its jumps are the AnimalAI's prepared lobs; this random 3-in-4 hop at -jump_velocity_y was a second controller, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-04)
|
||||||
|
if (kind == 1 && blocked && e.ex15.z <= 0.5) {
|
||||||
|
if (grounded && (flags & ST_CANJUMP) != 0 && (e.x.y == 0) && (rnd & 3u) != 0u) vel.y = e.mv.z;
|
||||||
|
else dir = -dir;
|
||||||
|
}
|
||||||
|
flags = (flags & ~(ST_GROUNDED | ST_LIQUID)) | (grounded ? ST_GROUNDED : 0) | (liquid ? ST_LIQUID : 0);
|
||||||
|
|
||||||
|
if ((e.st.w & 1024) != 0) { pos = e.a.xy; vel = e.a.zw; } // moved by the host (a worm: WormSystem): only its damage and death are handled here
|
||||||
|
ents[id].a = vec4(pos, vel);
|
||||||
|
ents[id].st = ivec4(kind, dir, timer, flags);
|
||||||
|
ents[id].hp = vec4(min(hp, kind == 3 && e.aux.x > 0 ? float(e.aux.x) * 0.001 : 1e9), cooldown, e.hp.z, e.hp.w);
|
||||||
|
ents[id].x.y = kind == 3 ? energy : e.x.y;
|
||||||
|
if (kind != 3) ents[id].x.z = e.x.z;
|
||||||
|
if (kind == 3) { ctrl.outp = vec4(pos, hp, float(dir)); ctrl.outp2 = vec4(float(energy) / ((e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0), cooldown, e.ex4.y > 0.0 ? ents[id].ex4.x / e.ex4.y : 1.0, ents[id].ex4.w > 0.0 ? 1.0 : 0.0); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Remembers the entity positions before a tick: sprites, projectiles and particles are drawn between the last two ticks.
|
||||||
|
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
|
||||||
|
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; } ctrl;
|
||||||
|
layout(std430, set = 0, binding = 39) buffer EntPrevW { vec4 prevw[]; };
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint i = gl_GlobalInvocationID.x;
|
||||||
|
if (i >= uint(ctrl.inp.w)) return;
|
||||||
|
prevw[i] = vec4(ents[i].a.xy, 0.0, 0.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 256) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Terrain part of an explosion with angular attenuation (docs/DAMAGE_CONTACTS_AND_EXPLOSIONS.md, 00657960 / 00656b10): 360 bins; each bin is a ray from the centre that
|
||||||
|
// walks the cells, spends min(remaining energy, cell hp left) on every solid / powder cell, and ends at a cell whose durability is above the maximum or when the energy
|
||||||
|
// is gone. A cell within the reach of its bin loses the bin's damage from its hp and goes below 1 (the damage stays on static cells: two weak blasts break what
|
||||||
|
// one cannot, as the engine's per-cell hp). One workgroup per edit
|
||||||
|
// of mode 2 (e[2k] = (x, y, radius <= 64, max durability), e[2k+1] = (2, ray energy, 0, 0)); the rays read the grid before the workgroup destroys anything.
|
||||||
|
layout(std430, set = 0, binding = 27) readonly buffer Edits { ivec4 head; vec4 e[512]; } ed;
|
||||||
|
|
||||||
|
shared float reach2[360]; // the squared reach of each bin
|
||||||
|
shared float binDmg[360]; // the largest hp a ray spent on one cell (00656b10 *param_5)
|
||||||
|
|
||||||
|
float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; }
|
||||||
|
uint cell_hp(uint m) { return 50u * max(1u, (mats[3u * m + 2u].z >> 20) & 0xFFFu); }
|
||||||
|
// the rays and the hole spend the hp of solid and powder cells only (cell vtable slot 23: solid, slot 25: CellData liquid_sand +0x160); liquids and gases are free
|
||||||
|
bool ray_cell(uint m) { const uint f = mflags(m); return (f & F_POWDER) != 0u || ((f & F_STATIC) != 0u && (f & F_LIQCELL) == 0u); }
|
||||||
|
// the hp a cell has left: the material hp (007048c0: cell+4 = CellData hp +0x158) minus the explosion damage it took. A static cell keeps that damage in bits 24-26 of its word
|
||||||
|
// (free there: the movement passes skip static cells), in eighths of its hp, so it is saved with the cell when the window moves
|
||||||
|
float cell_hp_left(int i, uint m) { return float(cell_hp(m)) * ((mflags(m) & F_STATIC) != 0u ? 1.0 - float((cells[i] >> 24) & 3u) / 4.0 : 1.0); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int k = int(gl_WorkGroupID.x);
|
||||||
|
if (k >= ed.head.x || k >= 256) return;
|
||||||
|
vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1];
|
||||||
|
if (r.x < 1.5 || r.x > 2.5) return;
|
||||||
|
const vec2 centre = q.xy;
|
||||||
|
const float R = min(q.z, 64.0);
|
||||||
|
if (!(R > 0.0)) return; // (a radius that is not a positive number - NaN, -inf: no rays; the rays' stop tests never hold for them)
|
||||||
|
const float maxDur = mod(q.w, 4096.0);
|
||||||
|
const float stainCode = floor(q.w / 4096.0); // 0: stains_enabled off, else 1 + stains_radius (gpu_fx.cpp)
|
||||||
|
// the burnt ring (00657960 + 0070e760): out to the radius + stains_radius (0: + 0.4 x the radius)
|
||||||
|
const float Rs = stainCode > 0.0 ? R + (stainCode > 1.0 ? stainCode - 1.0 : 0.4 * R) : R;
|
||||||
|
const uint rot = hash3(uint(k), uint(pc.frame), 41u) % 181u; // the random rotation of the roughness table
|
||||||
|
// 00656b10: a ray from the centre to the edge, unit steps, a cell sampled when the integer coordinates change; on a solid / powder cell spend = min(energy, its hp
|
||||||
|
// left), damage = max(damage, spend); it stops at a cell whose durability is above the maximum or when the energy is out: the reach is then the previous sample's
|
||||||
|
// squared distance (+1 on an even LCG draw), else the full radius
|
||||||
|
for (int bin = int(gl_LocalInvocationID.x); bin < 360; bin += 256) {
|
||||||
|
const float a = float(bin) * 6.2831853 / 360.0;
|
||||||
|
const vec2 dir = vec2(cos(a), sin(a));
|
||||||
|
float energy = r.y >= 0.0 ? r.y : 20000.0; // ray_energy of the data, else the constructor default 20000 (004b2780)
|
||||||
|
ivec2 last = ivec2(floor(centre));
|
||||||
|
float rr = R * R, dmg = 0.0, prevD2 = 0.0;
|
||||||
|
for (int s = 1; s <= 2048; ++s) {
|
||||||
|
const float d = float(s);
|
||||||
|
if (d * d >= R * R) break;
|
||||||
|
const ivec2 w = ivec2(floor(centre + dir * d));
|
||||||
|
if (w == last) continue;
|
||||||
|
last = w;
|
||||||
|
const ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) { rr = prevD2; break; }
|
||||||
|
const int i = p.y * pc.gw + p.x;
|
||||||
|
const uint m = cell_mat(cells[i]);
|
||||||
|
if (m != 0u && ray_cell(m)) {
|
||||||
|
const float spend = min(energy, max(cell_hp_left(i, m), 0.0));
|
||||||
|
energy -= spend;
|
||||||
|
dmg = max(dmg, spend);
|
||||||
|
if (uintBitsToFloat(mats[3u * m + 2u].y) > maxDur || energy <= 0.0) {
|
||||||
|
rr = prevD2 + ((hash3(uint(bin), uint(k), uint(pc.frame) * 13u) & 1u) == 0u ? 1.0 : 0.0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevD2 = d * d;
|
||||||
|
}
|
||||||
|
reach2[bin] = rr;
|
||||||
|
binDmg[bin] = dmg;
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
// the hole (00657960, hole_enabled): every cell at integer offsets inside the radius and within the reach of its bin; a solid / powder cell with a durability up to
|
||||||
|
// the maximum loses the bin's damage and goes below 1 hp (static cells keep the damage they took); other cells (liquids) go as before
|
||||||
|
const int hr = int(ceil(Rs)), n = 2 * hr + 1;
|
||||||
|
const ivec2 c0 = ivec2(floor(centre));
|
||||||
|
for (int j = int(gl_LocalInvocationID.x); j < n * n; j += 256) {
|
||||||
|
const ivec2 o = ivec2(j % n, j / n) - ivec2(hr);
|
||||||
|
const ivec2 w = c0 + o;
|
||||||
|
const float dist2 = float(o.x * o.x + o.y * o.y);
|
||||||
|
if (dist2 > Rs * Rs) continue;
|
||||||
|
const int bin = (o.x == 0 && o.y == 0) ? 0 : int(mod(atan(float(o.y), float(o.x)) + 6.2831853, 6.2831853) * 360.0 / 6.2831853) % 360;
|
||||||
|
const ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
|
||||||
|
const int i = p.y * pc.gw + p.x;
|
||||||
|
if (dist2 >= R * R || dist2 > reach2[bin]) {
|
||||||
|
// not reached: a stainable static solid / powder cell is burnt by s = (uniform(0.5, 0.7)) x roughness[bin + rotation] x falloff^2, the falloff 1 - (d^2 - reach^2) /
|
||||||
|
// (Rs^2 - R^2) clamped to 0..1; the roughness of a bin: uniform(0, 0.5) + (sin(bin / 90) + 1) x 0.25. Each channel x (1 - s): kept as the product in ext.x (render.comp)
|
||||||
|
if (stainCode <= 0.0 || Rs <= R) continue;
|
||||||
|
const uint sm = cell_mat(cells[i]);
|
||||||
|
if (sm == 0u || !ray_cell(sm) || (mflags(sm) & (F_STATIC | F_NOSTAIN)) != F_STATIC || (cells[i] & BURN_BIT) != 0u) continue;
|
||||||
|
const float fall = clamp(1.0 - (dist2 - reach2[bin]) / (Rs * Rs - R * R), 0.0, 1.0);
|
||||||
|
const uint rb = (uint(bin) + rot) % 360u;
|
||||||
|
const float rough = rnd01(hash3(rb, 0u, 43u)) * 0.5 + (sin(float(rb) / 90.0) + 1.0) * 0.25; // (00657960 fills DAT_012059f0 once, at the first explosion of the run: fixed per bin)
|
||||||
|
const float sv = (0.5 + 0.2 * rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 44u))) * rough * fall * fall;
|
||||||
|
if (sv <= 0.0) continue;
|
||||||
|
// the burn level lives in bits 28-30 of the cell word (free on a static cell that does not burn; it is saved with the cell when the window moves): keep = 1 - L / 7,
|
||||||
|
// the new level rounded at random so that weak burns add up
|
||||||
|
const uint cw = cells[i];
|
||||||
|
const float keep = (1.0 - float((cw >> 28) & 7u) / 7.0) * (1.0 - sv);
|
||||||
|
const float lv = (1.0 - keep) * 7.0;
|
||||||
|
const uint L = min(7u, uint(floor(lv + rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 45u)))));
|
||||||
|
cells[i] = (cw & ~(7u << 28)) | (L << 28);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint m = cell_mat(cells[i]);
|
||||||
|
if (m != 0u && uintBitsToFloat(mats[3u * m + 2u].y) > maxDur) continue;
|
||||||
|
if (m != 0u && ray_cell(m)) {
|
||||||
|
const float left = cell_hp_left(i, m) - binDmg[bin];
|
||||||
|
if (left >= 1.0) { // damaged, still there: the level of the damage rounded at random (weak hits add up), at most 3 quarters (bits 24-25; bit 26 is the frozen stain)
|
||||||
|
if ((mflags(m) & F_STATIC) != 0u) {
|
||||||
|
const float lv = (1.0 - left / float(cell_hp(m))) * 4.0;
|
||||||
|
const uint L = min(3u, uint(floor(lv + rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 46u)))));
|
||||||
|
const uint cw = cells[i];
|
||||||
|
cells[i] = (cw & ~(3u << 24)) | (L << 24);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cells[i] = 0u; ext[i] = vec2(0.0);
|
||||||
|
} else if (m != 0u) { cells[i] = 0u; ext[i] = vec2(0.0); }
|
||||||
|
// create_cell_material / create_cell_probability (00657960): every cell the blast reaches, destroyed or empty, becomes the material with that probability in percent (draw % 101 < p)
|
||||||
|
if (r.z > 0.5 && int(hash3(uint(w.x), uint(w.y), uint(pc.frame) * 7u + uint(k)) % 101u) < int(r.w)) {
|
||||||
|
cells[i] = uint(r.z) | (((hash3(uint(w.x), uint(w.y), 97u) >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Far blocks: 64 x 64 cell blocks of the world outside the simulated window, kept on the GPU (not simulated) for the projectiles that leave it - a black hole
|
||||||
|
// with accelerating shots eats on through them instead of meeting a wall at the window's edge. main.cpp makes them resident ahead of such projectiles (from the
|
||||||
|
// persistent store, the rest generated by terrain.comp shift_mode 3), edits.comp pass 1 applies the edits there, the dirty ones go back to the store.
|
||||||
|
// FarMap: (origin block x, y, map width, height), then a slot + 1 per block of the map (0 = not resident), then a dirty flag per slot, then the generation
|
||||||
|
// jobs: a count, then (slot, block x, block y, store-backed) each.
|
||||||
|
const int kFarMapW = 48, kFarMapH = 48, kFarSlots = 512, kFarJobs = 64;
|
||||||
|
const int kFarDirty = 4 + kFarMapW * kFarMapH, kFarJobCount = kFarDirty + kFarSlots, kFarJobList = kFarJobCount + 4;
|
||||||
|
layout(std430, set = 0, binding = 61) buffer FarCells { uint farcells[]; };
|
||||||
|
layout(std430, set = 0, binding = 62) buffer FarMap { int farmap[]; };
|
||||||
|
|
||||||
|
// the slot + 1 of the block of world cell w (0: none)
|
||||||
|
int far_slot(ivec2 w) {
|
||||||
|
const ivec2 b = (w >> 6) - ivec2(farmap[0], farmap[1]);
|
||||||
|
if (b.x < 0 || b.y < 0 || b.x >= farmap[2] || b.y >= farmap[3]) return 0;
|
||||||
|
return farmap[4 + b.y * farmap[2] + b.x];
|
||||||
|
}
|
||||||
|
bool far_in_map(ivec2 w) { const ivec2 b = (w >> 6) - ivec2(farmap[0], farmap[1]); return b.x >= 0 && b.y >= 0 && b.x < farmap[2] && b.y < farmap[3]; }
|
||||||
|
int far_index(int slot1, ivec2 w) { return (slot1 - 1) * 4096 + (w.y & 63) * 64 + (w.x & 63); }
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Fog of war, as FogOfWarRadiusSystem (lambda 00d8f000 -> 00aeef70): a persistent map of the world in 32 x 32 pixel texels (the engine's fog byte, 255 = fog, 0 = clear;
|
||||||
|
// kept here as its complement, the "clear" amount, so that the zeroed buffer is all fog). It only clears: what was seen stays seen. Every frame, for the player
|
||||||
|
// (FogOfWarRadiusComponent radius 200, data/entities/misc/fogofwar_radius.xml; the position lifted by 4 x uniform(0, 3) pixels):
|
||||||
|
// * the 3 x 3 texels around it: fog = min(fog, max(fog - 3 d, 255 d)), d = the distance from the texel centre in texels;
|
||||||
|
// * 36 rays (10 degrees apart) of the radius, stopped by the terrain: at 0, 1/4, 2/4 and 3/4 of the free part of each ray the texel loses 5 of fog
|
||||||
|
// (a texel crossed by several rays loses more; 51 frames clear one entirely).
|
||||||
|
// render.comp reads the map bilinearly (the texel centres at 32 n + 16) as post_final.frag reads tex_fog.
|
||||||
|
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
|
||||||
|
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 49) buffer Fog { uint fog[]; }; // per texel: 255 - the fog byte (the clear amount)
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after the fire palettes: the biome map's width, height and fog_of_war_type per tile (main.cpp)
|
||||||
|
#include "biome_mods.glsl"
|
||||||
|
uint fog_type_at(vec2 w) { // 0 default, 1 HEAVY_CLEAR_AT_PLAYER, 2 HEAVY_CLEAR_WITH_MAGIC, 3 HEAVY_NO_CLEAR; outside the map: the default
|
||||||
|
const uint n = mats[mats.length() - 1u].w;
|
||||||
|
const uint fb = 1024u + 9u * n + 132u + n;
|
||||||
|
const int bw = int(tbl[fb]), bh = int(tbl[fb + 1u]);
|
||||||
|
const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14);
|
||||||
|
if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return 0u;
|
||||||
|
return tbl[fb + 2u + uint(t.y * bw + t.x)];
|
||||||
|
}
|
||||||
|
const int FOG_TW = 1152, FOG_TH = 896, FOG_OX = 18432, FOG_OY = 14336; // texels of 32 pixels from the world origin (-18432, -14336)
|
||||||
|
const float RADIUS = 256.0; // the player's (component_documentation: "256 is the default player has"; the X-ray card's fogofwar_radius.xml has 200)
|
||||||
|
|
||||||
|
bool solid_at(ivec2 w) { // (world cell; outside the window: open)
|
||||||
|
const ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
const uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
|
||||||
|
}
|
||||||
|
int texel_index(vec2 w) {
|
||||||
|
const ivec2 t = ivec2(floor((w + vec2(FOG_OX, FOG_OY)) / 32.0));
|
||||||
|
if (t.x < 0 || t.y < 0 || t.x >= FOG_TW || t.y >= FOG_TH) return -1;
|
||||||
|
return t.y * FOG_TW + t.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the solid share of the 64 x 64 pixel region of a point, 0..255 (the coarse map of the world the 3 x 3 rule reads; outside the window: 0)
|
||||||
|
float coarse_at(vec2 w) {
|
||||||
|
const ivec2 r0 = ivec2(floor(w / 64.0)) * 64;
|
||||||
|
int n = 0, tot = 0;
|
||||||
|
for (int y = 2; y < 64; y += 4) for (int x = 2; x < 64; x += 4) {
|
||||||
|
const ivec2 p = r0 + ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
|
||||||
|
++tot;
|
||||||
|
const uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) ++n;
|
||||||
|
}
|
||||||
|
return tot > 0 ? 255.0 * float(n) / float(tot) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// one workgroup per source: 0 the player (radius 256), then the other FogOfWarRadiusComponents (main.cpp, after the holes in the tables)
|
||||||
|
const uint t = gl_LocalInvocationIndex;
|
||||||
|
if (pc.pass_ != 0) return;
|
||||||
|
if (t >= 45u) { // 00c42d30: the biome's fog_of_war_delta (FOG_OF_WAR_REAPPEARS: 2) - every frame the fog bytes of the 17 x 17 texels around the player (ceil(427 / 32 x 0.5) + 1 = 8 each way) gain it
|
||||||
|
if (gl_WorkGroupID.x != 0u || (ents[0].st.w & 8) != 0) return;
|
||||||
|
const vec2 P0 = ents[0].a.xy;
|
||||||
|
const int delta = int(bitfieldExtract(int(bm_bytes(P0)), 0, 8)); // (a signed byte)
|
||||||
|
if (delta == 0) return;
|
||||||
|
for (uint k = t - 45u; k < 289u; k += 19u) {
|
||||||
|
const int ti = texel_index(P0 + vec2(float(int(k % 17u) - 8), float(int(k / 17u) - 8)) * 32.0);
|
||||||
|
if (ti < 0) continue;
|
||||||
|
uint old = fog[ti];
|
||||||
|
for (int tries = 0; tries < 4; ++tries) { const uint nv = uint(clamp(int(min(old, 255u)) - delta, 0, 255)); const uint got = atomicCompSwap(fog[ti], old, nv); if (got == old) break; old = got; } // (fog[] holds 255 - the fog byte)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint wg = gl_WorkGroupID.x;
|
||||||
|
vec2 P;
|
||||||
|
float R = RADIUS;
|
||||||
|
if (wg == 0u) {
|
||||||
|
if ((ents[0].st.w & 8) != 0) return; // (dead)
|
||||||
|
P = ents[0].a.xy;
|
||||||
|
} else {
|
||||||
|
const uint n = mats[mats.length() - 1u].w;
|
||||||
|
const uint sb = 1024u + 9u * n + 132u + n + 2u + 8192u + 1u + 64u * 6u;
|
||||||
|
if (int(wg) > int(uintBitsToFloat(tbl[sb]))) return;
|
||||||
|
const uint o = sb + 1u + (wg - 1u) * 3u;
|
||||||
|
P = vec2(uintBitsToFloat(tbl[o]), uintBitsToFloat(tbl[o + 1u]));
|
||||||
|
R = uintBitsToFloat(tbl[o + 2u]);
|
||||||
|
}
|
||||||
|
P -= vec2(0.0, 4.0 * float(hash3(uint(pc.frame) + wg * 131u, 61u, 7u) & 3u));
|
||||||
|
if (t < 36u) { // a ray
|
||||||
|
const float a = float(t) * 0.17453294;
|
||||||
|
const vec2 dir = vec2(cos(a), sin(a));
|
||||||
|
float len = R;
|
||||||
|
for (int s = 1; s < int(R); ++s) if (solid_at(ivec2(floor(P + dir * float(s))))) { len = float(s); break; }
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const vec2 sw = P + dir * (len * 0.25 * float(k));
|
||||||
|
if (fog_type_at(sw) != 0u) continue; // only the default fog of war clears along the rays
|
||||||
|
const int i = texel_index(sw);
|
||||||
|
if (i >= 0 && fog[i] < 255u) atomicAdd(fog[i], 5u);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// the 3 x 3 texels around the player
|
||||||
|
const int k = int(t) - 36;
|
||||||
|
const vec2 tc = (floor((P + vec2(FOG_OX, FOG_OY)) / 32.0) + vec2(float(k % 3 - 1), float(k / 3 - 1))) * 32.0 + 16.0 - vec2(FOG_OX, FOG_OY);
|
||||||
|
const int i = texel_index(tc);
|
||||||
|
if (i < 0 || fog_type_at(tc) > 1u) return; // the default and HEAVY_CLEAR_AT_PLAYER clear around the player
|
||||||
|
const float d = distance(P, tc) / 32.0;
|
||||||
|
const float old = 255.0 - float(min(fog[i], 255u));
|
||||||
|
float target = 255.0 * d;
|
||||||
|
if (k % 3 != 1 && k / 3 != 1) target = max(target, (coarse_at(tc) * 16.0 / 1024.0 - d) * 255.0); // the four diagonal texels: a solid region stays fogged (the coarse byte x 16 / 1024)
|
||||||
|
const float nf = min(old, max(old - 3.0 * d, clamp(target, 0.0, 255.0)));
|
||||||
|
atomicMax(fog[i], uint(255.0 - nf));
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
|
||||||
|
// Explosion sprites and flashes: advance, draw the animated sprite (centred) and add a fading light.
|
||||||
|
// One workgroup per animation record (the dispatch is FANIM_ALL groups): its 64 threads share the sprite's pixels. (One thread per record drew a big
|
||||||
|
// explosion sprite alone - 100 000 pixels and more - and three bombs took 15 ms of GPU time.)
|
||||||
|
void main() {
|
||||||
|
const uint id = gl_WorkGroupID.x, lane = gl_LocalInvocationID.x;
|
||||||
|
const bool drawOnly = pc.pass_ == 1; // between ticks nothing advances
|
||||||
|
if (gl_GlobalInvocationID.x == 0u && !drawOnly) lpad[0] = lpad[0] * 5u / 6u; // camera shake decays
|
||||||
|
if (id >= uint(FANIM_ALL)) return;
|
||||||
|
FAnim a = fanims[id];
|
||||||
|
barrier(); // (every lane has read the record before lane 0 advances it)
|
||||||
|
if (a.b.z <= 0) return;
|
||||||
|
int age = drawOnly ? a.b.y : a.b.y + 1;
|
||||||
|
if (age >= a.b.z) { if (!drawOnly && lane == 0u) fanims[id].b.z = 0; return; }
|
||||||
|
float k = 1.0 - float(age) / float(a.b.z);
|
||||||
|
if (!drawOnly && lane == 0u) {
|
||||||
|
fanims[id].b.y = age;
|
||||||
|
const float lk = a.c.y > 0.0 ? max(0.0, 1.0 - float(age) / a.c.y) : k; // (c.y: the light's own fade frames)
|
||||||
|
if (a.a.w > 0.0 && lk > 0.0) add_light(a.a.xy, a.a.z, a.a.w * lk, rgb_of(uint(a.b.w)));
|
||||||
|
}
|
||||||
|
if (a.b.x < 0) return;
|
||||||
|
int b = BOOM_BASE + a.b.x * BOOM_STRIDE;
|
||||||
|
ivec4 b0 = fti(b), b1 = fti(b + 1);
|
||||||
|
vec4 b3 = ftf(b + 3);
|
||||||
|
if (b0.y <= 0 || b0.z <= 0) return;
|
||||||
|
int frame = age / max(b1.x, 1);
|
||||||
|
if (frame >= b0.w) return;
|
||||||
|
// the sprite is placed with its pivot at the effect position and rotated by the angle (muzzle flashes point along the shot)
|
||||||
|
// c.z: a scale (0: 1; negative: drawn behind - only where no cell and no sprite is, the render_back sprite particles of the host, main.cpp); c.w: a tint (rgb) and alpha
|
||||||
|
// (bits 24-31) as bits (0: none); a host record (tinted) has its own y scale in c.y (the GPU's records keep their light's fade there)
|
||||||
|
const uint tw = floatBitsToUint(a.c.w);
|
||||||
|
const bool tinted = tw != 0u;
|
||||||
|
const float sx0 = a.c.z == 0.0 ? 1.0 : abs(a.c.z);
|
||||||
|
const vec2 sclv = max(vec2(sx0, tinted ? a.c.y : sx0), vec2(0.01));
|
||||||
|
const float scl = max(sclv.x, sclv.y);
|
||||||
|
const bool behind = a.c.z < 0.0;
|
||||||
|
const vec3 tint = vec3(float((tw >> 16) & 255u), float((tw >> 8) & 255u), float(tw & 255u)) / 255.0;
|
||||||
|
const float alpha = tinted ? float(tw >> 24) / 255.0 : 1.0;
|
||||||
|
vec2 pivot = b3.zw, size = vec2(b0.yz);
|
||||||
|
float ca = cos(a.c.x), sa = sin(a.c.x);
|
||||||
|
float r = (length(max(pivot, size - pivot)) + 1.5) * scl;
|
||||||
|
int n = int(2.0 * r) + 2;
|
||||||
|
ivec2 lo = ivec2(floor(a.a.xy - vec2(r)));
|
||||||
|
bool additive = (b1.w & 8) != 0;
|
||||||
|
for (int j = int(lane); j < n * n; j += 64) {
|
||||||
|
ivec2 w = lo + ivec2(j % n, j / n);
|
||||||
|
vec2 d = vec2(w) + 0.5 - a.a.xy;
|
||||||
|
ivec2 sp = ivec2(floor(pivot + vec2(ca * d.x + sa * d.y, -sa * d.x + ca * d.y) / sclv));
|
||||||
|
if (sp.x < 0 || sp.y < 0 || sp.x >= b0.y || sp.y >= b0.z) continue;
|
||||||
|
ivec2 q = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
uint t = fxtex[b0.x + frame * b0.y * b0.z + sp.y * b0.y + sp.x];
|
||||||
|
if ((t >> 24) == 0u) continue;
|
||||||
|
const int qi = q.y * pc.gw + q.x;
|
||||||
|
if (behind && (cell_mat(cells[qi]) != 0u || entlayer[qi] != 0u)) continue; // (behind the terrain and the sprites already drawn)
|
||||||
|
if (tinted) {
|
||||||
|
const vec3 c = vec3(float((t >> 16) & 255u), float((t >> 8) & 255u), float(t & 255u)) / 255.0 * tint;
|
||||||
|
const float al = float(t >> 24) / 255.0 * alpha;
|
||||||
|
if (al < 0.02) continue;
|
||||||
|
if (additive) t = 0x01000000u | packUnorm4x8(vec4(c.b * al, c.g * al, c.r * al, 0.0));
|
||||||
|
else t = (uint(al * 255.0) << 24) | (packUnorm4x8(vec4(c.b, c.g, c.r, 0.0)) & 0xFFFFFFu);
|
||||||
|
entlayer[qi] = t;
|
||||||
|
} else entlayer[qi] = additive ? (0x01000000u | (t & 0xFFFFFFu)) : t;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// Shared declarations of the GPU wand/projectile/effect system (proj_spawn, proj_step, fx_particles, fx_anims, rigid_events).
|
||||||
|
// The CPU only runs the spell script (gun.lua) and uploads its result: tables of projectile types, effect definitions, and the shot tree
|
||||||
|
// (nodes and specs) of every cast. Flight, collisions, triggers, explosions, damage, emitters, particles, lights and animations run here.
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "far.glsl"
|
||||||
|
|
||||||
|
struct Ent {
|
||||||
|
vec4 a, box, mv, ai;
|
||||||
|
ivec4 st;
|
||||||
|
vec4 hp;
|
||||||
|
ivec4 x, rg, rg2, aux;
|
||||||
|
vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0
|
||||||
|
vec4 ex2; // ranged attack: projectile speed, gravity
|
||||||
|
ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0
|
||||||
|
vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire
|
||||||
|
vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag
|
||||||
|
vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y)
|
||||||
|
vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left
|
||||||
|
ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending
|
||||||
|
};
|
||||||
|
layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; };
|
||||||
|
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
|
||||||
|
layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // entity positions before the last tick (for interpolation)
|
||||||
|
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits, two vec4 each (see edits.comp)
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 33) readonly buffer FxTables { uvec4 ft[]; }; // projectile types, explosion effects, emitters (words, see gun/gpu_fx.h)
|
||||||
|
layout(std430, set = 0, binding = 34) readonly buffer FxTex { uint fxtex[]; }; // sprite frames (ARGB) of projectiles and explosions
|
||||||
|
#include "fx_host.glsl"
|
||||||
|
|
||||||
|
#include "fx_state.glsl"
|
||||||
|
// rigid bodies (read only): projectiles stop at them
|
||||||
|
layout(std430, set = 0, binding = 30) readonly buffer RShapes { uint rsh[]; };
|
||||||
|
struct RBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0; vec4 ja1; vec4 ja2; vec4 ja3; };
|
||||||
|
layout(std430, set = 0, binding = 31) readonly buffer RBodies { ivec4 rhdr; RBody rbodies[]; };
|
||||||
|
layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; }; // explosion events for the rigid bodies: x, y, radius, power
|
||||||
|
struct FLight { vec4 a; vec4 b; }; // x, y, radius, strength / rgb
|
||||||
|
layout(std430, set = 0, binding = 38) buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; };
|
||||||
|
|
||||||
|
const int TYPE_STRIDE = 18, BOOM_BASE = 9216, BOOM_STRIDE = 4, EMIT_BASE = 9472, EMIT_STRIDE = 4, LOAD_BASE = 13568; // (512 types: the scripts' variants of a projectile are types of their own)
|
||||||
|
|
||||||
|
float uf(uint u) { return uintBitsToFloat(u); }
|
||||||
|
vec4 ftf(int i) { return uintBitsToFloat(ft[i]); }
|
||||||
|
ivec4 fti(int i) { return ivec4(ft[i]); }
|
||||||
|
float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; }
|
||||||
|
|
||||||
|
bool solid_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) { // outside the window: a resident far block (far.glsl), else a wall
|
||||||
|
const int s = far_slot(w);
|
||||||
|
if (s == 0) return true;
|
||||||
|
const uint fm = cell_mat(farcells[far_index(s, w)]);
|
||||||
|
return fm != 0u && (mflags(fm) & (F_STATIC | F_SAND)) != 0u;
|
||||||
|
}
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void add_light(vec2 pos, float radius, float strength, vec3 rgb) {
|
||||||
|
uint i = atomicAdd(lcount, 1u);
|
||||||
|
if (i < 512u) { lights[i].a = vec4(pos, radius, strength); lights[i].b = vec4(rgb, 0.0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
uint add_anim(vec2 pos, int boom, float radius, float strength, int maxage, uint rgb, float angle) { // the record
|
||||||
|
uint s = atomicAdd(fcnt[2], 1u) % uint(FANIM_MAX);
|
||||||
|
fanims[s].a = vec4(pos, radius, strength);
|
||||||
|
fanims[s].b = ivec4(boom, 0, maxage, int(rgb & 0xFFFFFFu));
|
||||||
|
fanims[s].c = vec4(angle, 0.0, 0.0, 0.0);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_edit(vec4 q, vec4 r) {
|
||||||
|
uint i = uint(atomicAdd(ehead.x, 1));
|
||||||
|
if (i < 256u) { ee[2u * i] = q; ee[2u * i + 1u] = r; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// A world point inside a rigid body's pixel mask? (the grid is filled by rigid_raster.comp)
|
||||||
|
layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; };
|
||||||
|
bool body_hit(vec2 w) {
|
||||||
|
ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && bodyGrid[p.y * pc.gw + p.x] != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entity slot of the rigid body that covers a world point, -1 when none.
|
||||||
|
int body_slot(vec2 w) {
|
||||||
|
ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return -1;
|
||||||
|
return int(bodyGrid[p.y * pc.gw + p.x] & 0x7FFFFFFFu) - 1; // (bit 31: the characters-pass mark of rigid_raster.comp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Who damaged an entity last: the slot + 1 of the attacker (0 = unknown) in the low 12 bits of ex8.w, the frame in the rest. The host reads it when the hp drops (script_damage_received).
|
||||||
|
void note_attacker(int victim, int attacker_plus_1) {
|
||||||
|
if (attacker_plus_1 > 0 && attacker_plus_1 < 4096) ents[victim].ex8.w = attacker_plus_1 | ((pc.frame & 0xFFFFF) << 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hit that the victim's multipliers took to nothing (a protection): the frame in ex11.w of a creature or the player, for the host's BLOCK message (00b071f0)
|
||||||
|
void note_block(int victim, float raw, float dealt) {
|
||||||
|
const int kind = ents[victim].st.x;
|
||||||
|
if (raw > 0.0 && dealt <= 0.0 && (kind == 1 || kind == 2 || kind == 3)) ents[victim].ex11.w = float(pc.frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A damage event on a body with a liquid inventory: the corpse part emits (entities.comp).
|
||||||
|
void body_damage(int slot, vec2 hit, float dmg) {
|
||||||
|
if (slot < 0 || ents[slot].ex7.y <= 0.0) return;
|
||||||
|
atomicAdd(ents[slot].ex8.z, max(1, int(dmg * 1000.0)));
|
||||||
|
const vec2 r = hit - ents[slot].a.xy; const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z);
|
||||||
|
const vec2 rel = vec2(ca * r.x + sa * r.y, -sa * r.x + ca * r.y); // in the body's frame (entities.comp turns the leak back with the rotation)
|
||||||
|
atomicExchange(ents[slot].ex8.x, int(rel.x * 1000.0)); atomicExchange(ents[slot].ex8.y, int(rel.y * 1000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The damage multiplier the perks give a shooter; applied ONCE when the shot is made (Message_Shot, 00b06870: projectile damage, and the explosion damage unless not_scaled_by_gamefx)
|
||||||
|
float dealt_of(int shooter) {
|
||||||
|
if (shooter <= 0) return 1.0;
|
||||||
|
const Ent s = ents[shooter - 1];
|
||||||
|
return (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) && s.ex7.y > 0.0 ? s.ex7.y : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The liquid material at a position (0 when there is none).
|
||||||
|
uint liquid_at_pos(vec2 pos) {
|
||||||
|
ivec2 p = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return 0u;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m == 0u) return 0u;
|
||||||
|
uint f = mflags(m);
|
||||||
|
return ((f & F_LIQUID) != 0u && (f & (F_SAND | F_STATIC)) == 0u) ? m : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A projectile pushes the liquid cells around its position aside (to the nearest free cell away from it).
|
||||||
|
void displace_liquid(vec2 pos) {
|
||||||
|
{ ivec2 p0 = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); if (p0.x < 1 || p0.y < 1 || p0.x >= pc.gw - 1 || p0.y >= pc.gh - 1) return;
|
||||||
|
uint m0 = cell_mat(cells[p0.y * pc.gw + p0.x]); if (m0 == 0u || (mflags(m0) & F_LIQUID) == 0u) return; } // only when the projectile is inside a liquid
|
||||||
|
for (int dy = -1; dy <= 1; ++dy)
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
ivec2 w = ivec2(floor(pos)) + ivec2(dx, dy);
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
uint c = cells[i];
|
||||||
|
uint m = cell_mat(c);
|
||||||
|
if (m == 0u) continue;
|
||||||
|
uint f = mflags(m);
|
||||||
|
if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) continue;
|
||||||
|
vec2 d = vec2(w) + 0.5 - pos;
|
||||||
|
d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d);
|
||||||
|
for (int r = 1; r <= 6; ++r) {
|
||||||
|
ivec2 t = w + ivec2(round(d * float(r)));
|
||||||
|
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break;
|
||||||
|
int ti = q.y * pc.gw + q.x;
|
||||||
|
if (atomicCompSwap(cells[ti], 0u, c) == 0u) { ext[ti] = ext[i]; cells[i] = 0u; ext[i] = vec2(0.0); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 rgb_of(uint c) { return vec3(float((c >> 16) & 255u), float((c >> 8) & 255u), float(c & 255u)) / 255.0; }
|
||||||
|
|
||||||
|
// Explosion visuals: animated sprite, light flash, sparks and material sparks.
|
||||||
|
// radius >= 0: the explosion's own radius (a shot with Explosive Projectile: the file's 2 + 15 per modifier); the light is that x light_radius_coeff (00657960), the table
|
||||||
|
// holds it for the file's radius
|
||||||
|
void boom_fx(int boom, vec2 pos, uint seed, float radius) {
|
||||||
|
int b = BOOM_BASE + boom * BOOM_STRIDE;
|
||||||
|
ivec4 b0 = fti(b), b1 = fti(b + 1), b2 = fti(b + 2);
|
||||||
|
vec4 b3 = ftf(b + 3);
|
||||||
|
float rad = b3.x;
|
||||||
|
atomicMax(lpad[0], uint(b3.y * 1500.0)); // camera shake (milli-cells), decays in fx_anims
|
||||||
|
int frames = max(b0.w, 1), wait = max(b1.x, 1);
|
||||||
|
int maxage = b0.z > 0 ? frames * wait + 2 : 9;
|
||||||
|
// the light (ConfigExplosion light_*): radius explosion_radius x light_radius_coeff, colour light_r/g/b, fading over light_fade_time (b1.w bits 4-15 / 16-27)
|
||||||
|
const bool lightOn = (b1.w & 4) != 0;
|
||||||
|
float lrad = float((uint(b1.w) >> 4) & 0xFFFu);
|
||||||
|
if (radius >= 0.0) lrad = rad > 0.0 ? lrad * radius / rad : radius * 8.0; // (the coefficient default 8: 004b2780)
|
||||||
|
lrad = min(lrad, 4095.0);
|
||||||
|
const int lframes = max(1, int((uint(b1.w) >> 16) & 0xFFFu));
|
||||||
|
const uint an = add_anim(pos, boom, lightOn ? max(lrad, 9.0) : 0.0, lightOn ? 0.6 : 0.0, max(max(maxage, 9), lightOn ? lframes : 0), uint(b1.y), 0.0);
|
||||||
|
if (lightOn) fanims[an].c.y = float(lframes); // (the light fades over its own time, the sprite plays over its own)
|
||||||
|
const int sp0 = int(uint(b2.x) & 0xFFu);
|
||||||
|
const uint sparkRgb = uint(b2.x) >> 8;
|
||||||
|
int ns = (b1.w & 1) != 0 ? sp0 + int(rnd01(hash3(seed, 1u, 7u)) * float(b2.y - sp0 + 1)) : 0;
|
||||||
|
for (int i = 0; i < ns && i < 64; ++i) {
|
||||||
|
float a = rnd01(hash3(seed, uint(i), 11u)) * 6.2832, sp = 0.8 + rnd01(hash3(seed, uint(i), 12u)) * 2.4;
|
||||||
|
emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.6), 18.0 + rnd01(hash3(seed, uint(i), 13u)) * 27.0, 0.1, sparkRgb, true, true);
|
||||||
|
}
|
||||||
|
int nm = (b1.w & 2) != 0 ? b2.z + int(rnd01(hash3(seed, 2u, 8u)) * float(b2.w - b2.z + 1)) : 0;
|
||||||
|
if ((b1.w & 10) == 10 && b0.z == 0) { // an ambient emitter (a portal): material sparks spread over a disc of the radius, drifting away from its centre, no gravity, long-lived, additive
|
||||||
|
for (int i = 0; i < nm && i < 160; ++i) {
|
||||||
|
float a = rnd01(hash3(seed, uint(i), 31u)) * 6.2832, rr = sqrt(rnd01(hash3(seed, uint(i), 32u))) * rad, sp = 0.05 + rnd01(hash3(seed, uint(i), 33u)) * 0.25;
|
||||||
|
vec2 d = vec2(cos(a), sin(a));
|
||||||
|
emit_particle(pos + d * rr, d * sp, 150.0 + rnd01(hash3(seed, uint(i), 34u)) * 60.0, 0.0, uint(b1.z), true, true);
|
||||||
|
}
|
||||||
|
nm = 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < nm && i < 64; ++i) {
|
||||||
|
float a = rnd01(hash3(seed, uint(i), 21u)) * 6.2832, sp = 0.5 + rnd01(hash3(seed, uint(i), 22u)) * 1.5;
|
||||||
|
emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.4), 20.0 + rnd01(hash3(seed, uint(i), 23u)) * 30.0, 0.12, uint(b1.z), true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The damage of a projectile hit on entity k (TakeDamage_Impl 00ab9a90: every damage type goes through the victim's multiplier of that type): the projectile channel, the
|
||||||
|
// damage_by_type channels of the type (table words 12-14: melee, electricity, fire, ice / slice, drill, radioactive, poison / healing, curse, holy, explosion) and the wand's adds (f.x electricity, f.y ice)
|
||||||
|
float hit_damage(int k, float proj, int tb, vec4 f) {
|
||||||
|
const Ent v = ents[k];
|
||||||
|
const vec4 c0 = ftf(tb + 12), c1 = ftf(tb + 13), c2 = ftf(tb + 14);
|
||||||
|
return proj * v.ex9.x
|
||||||
|
+ c0.x * v.ex9.w + (c0.y + f.x) * v.ex12.x + c0.z * v.ex9.z + (c0.w + f.y) * v.ex12.w
|
||||||
|
+ c1.x * v.ex12.z + c1.y * v.ex12.y + c1.z * v.ex13.z + c1.w * v.ex13.w
|
||||||
|
+ c2.x * v.ex13.x + c2.y * v.ex14.y + c2.z * v.ex14.z + c2.w * v.ex9.y;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
|
||||||
|
// The player's right arm and the item in its hand, drawn into the entity layer every frame. The arm (player_arm.xml) hangs at the body's
|
||||||
|
// right_arm_root hotspot (the host follows the stand / walk frames, so it breathes with the body), turns toward the aim point (the cursor, or
|
||||||
|
// ahead of the player) and is mirrored when aiming left; the item is held at the arm's "hand" hotspot, turned the same way about its grip.
|
||||||
|
// The arm header is in the first words of the hand texture region (hand1.x - its offset is absolute): [0] arm on, [1] arm pixels (absolute),
|
||||||
|
// [2..3] arm size, [4..5] arm root relative to the body (floats), [6..7] the arm sprite's pivot (offset_x, offset_y), [8..9] its hand hotspot;
|
||||||
|
// [48] cape on, [49..50] the corner of its 32 x 32 image (words 64..1087, rasterized by the host) relative to the body: drawn behind the body (empty pixels only).
|
||||||
|
const uint HAND_BASE = (1u << 20) - 32768u;
|
||||||
|
|
||||||
|
uint hw(uint i) { return fxtex[HAND_BASE + i]; }
|
||||||
|
|
||||||
|
// draws a w x h image whose pixel `pivot` sits at `org`, turned by (c, s), mirrored vertically when `mirror`
|
||||||
|
void draw_turned(vec2 org, vec2 pivot, float c, float s, bool mirror, int w, int h, uint off) {
|
||||||
|
float r = length(vec2(float(w), float(h))) + 2.0;
|
||||||
|
int n = int(2.0 * r) + 2;
|
||||||
|
ivec2 lo = ivec2(floor(org - vec2(r)));
|
||||||
|
for (int i = int(gl_LocalInvocationID.x); i < n * n; i += 64) {
|
||||||
|
ivec2 dst = lo + ivec2(i % n, i / n);
|
||||||
|
vec2 d = vec2(dst) + 0.5 - org;
|
||||||
|
vec2 l = vec2(c * d.x + s * d.y, -s * d.x + c * d.y); // the image frame, relative to the pivot
|
||||||
|
if (mirror) l.y = -l.y;
|
||||||
|
ivec2 sp = ivec2(floor(pivot + l));
|
||||||
|
if (sp.x < 0 || sp.y < 0 || sp.x >= w || sp.y >= h) continue;
|
||||||
|
ivec2 q = dst - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
uint t = fxtex[off + uint(sp.y * w + sp.x)];
|
||||||
|
const uint ta = t >> 24;
|
||||||
|
if (ta == 0u) continue;
|
||||||
|
const uint di = uint(q.y * pc.gw + q.x);
|
||||||
|
const uint under = entlayer[di];
|
||||||
|
if (ta < 255u && (under >> 24) != 0u) { // a translucent texel over the body: mixed with it (source over), not cut through it
|
||||||
|
uint mixd = max(ta, under >> 24) << 24;
|
||||||
|
for (uint sh = 0u; sh < 24u; sh += 8u) mixd |= (((t >> sh) & 0xFFu) * ta + ((under >> sh) & 0xFFu) * (255u - ta)) / 255u << sh;
|
||||||
|
entlayer[di] = mixd;
|
||||||
|
} else entlayer[di] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The items NPCs hold (ItemPickUpperComponent is_in_npc): at word 32640 of the hand region the count, then 12 words per entry: [0] the slot + 1, [1] the image
|
||||||
|
// (absolute), [2..3] its size, [4..5] the grip, [6..7] the hand relative to the body, [8..9] the aim point (floats); drawn like the player's item, turned toward the aim
|
||||||
|
void draw_npc_items() {
|
||||||
|
const uint n = min(hw(32640u), 10u);
|
||||||
|
for (uint k = 0u; k < n; ++k) {
|
||||||
|
const uint b = 32641u + k * 12u;
|
||||||
|
const int sl = int(hw(b)) - 1;
|
||||||
|
if (sl <= 0 || sl >= int(ents.length()) || (ents[sl].st.w & 8) != 0) continue;
|
||||||
|
vec2 cur = ents[sl].a.xy, prv = prevpos[sl].xy;
|
||||||
|
vec2 body = distance(prv, cur) < 10.0 ? mix(prv, cur, ctrl.view.z) : cur;
|
||||||
|
vec2 hand = body + vec2(uintBitsToFloat(hw(b + 6u)), uintBitsToFloat(hw(b + 7u)));
|
||||||
|
vec2 aim = vec2(uintBitsToFloat(hw(b + 8u)), uintBitsToFloat(hw(b + 9u)));
|
||||||
|
float ang = length(aim - hand) > 0.5 ? atan(aim.y - hand.y, aim.x - hand.x) : 0.0;
|
||||||
|
float c = cos(ang), s = sin(ang);
|
||||||
|
draw_turned(hand, vec2(uintBitsToFloat(hw(b + 4u)), uintBitsToFloat(hw(b + 5u))), c, s, c < 0.0, int(hw(b + 2u)), int(hw(b + 3u)), hw(b + 1u));
|
||||||
|
}
|
||||||
|
memoryBarrierBuffer();
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
draw_npc_items();
|
||||||
|
if (ctrl.inp.w < 1 || ents[0].st.x != 3 || (ents[0].st.w & 8) != 0 || (ents[0].ex8.x & 8192) != 0) return;
|
||||||
|
vec2 cur = ents[0].a.xy, prv = prevpos[0].xy;
|
||||||
|
vec2 body = distance(prv, cur) < 10.0 ? mix(prv, cur, ctrl.view.z) : cur;
|
||||||
|
if (hw(48u) == 1u) { // the cape, behind the body
|
||||||
|
ivec2 corner = ivec2(floor(body + vec2(uintBitsToFloat(hw(49u)), uintBitsToFloat(hw(50u))) + 0.5));
|
||||||
|
for (int i = int(gl_LocalInvocationID.x); i < 1024; i += 64) {
|
||||||
|
uint t = hw(64u + uint(i));
|
||||||
|
if ((t >> 24) == 0u) continue;
|
||||||
|
ivec2 q = corner + ivec2(i % 32, i / 32) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
if ((entlayer[q.y * pc.gw + q.x] >> 24) == 0u) entlayer[q.y * pc.gw + q.x] = t;
|
||||||
|
}
|
||||||
|
memoryBarrierBuffer();
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
if (ctrl.hand0.z < 0.5) return;
|
||||||
|
const bool arm = hw(0u) == 1u;
|
||||||
|
vec2 root = arm ? body + vec2(uintBitsToFloat(hw(4u)), uintBitsToFloat(hw(5u))) : body + vec2(0.0, -5.0);
|
||||||
|
vec2 aim = ctrl.hand0.xy;
|
||||||
|
float ang = length(aim - root) > 0.5 ? atan(aim.y - root.y, aim.x - root.x) : 0.0;
|
||||||
|
float c = cos(ang), s = sin(ang);
|
||||||
|
bool mirror = c < 0.0;
|
||||||
|
vec2 hand = root;
|
||||||
|
if (arm) { // the hand: the arm's hand hotspot relative to its pivot, turned and mirrored with the arm
|
||||||
|
vec2 piv = vec2(uintBitsToFloat(hw(6u)), uintBitsToFloat(hw(7u)));
|
||||||
|
vec2 hl = vec2(uintBitsToFloat(hw(8u)), uintBitsToFloat(hw(9u))) + 0.5 - piv;
|
||||||
|
if (mirror) hl.y = -hl.y;
|
||||||
|
hand = root + vec2(c * hl.x - s * hl.y, s * hl.x + c * hl.y);
|
||||||
|
}
|
||||||
|
{ // the item's recoil (00b51670): hand2.z px back along the aim, turned muzzle-up by hand0.w radians (the arm keeps the aim)
|
||||||
|
const float ia = ang + (mirror ? ctrl.hand0.w : -ctrl.hand0.w);
|
||||||
|
const float ic = cos(ia), is = sin(ia);
|
||||||
|
draw_turned(hand - vec2(c, s) * ctrl.hand2.z, ctrl.hand2.xy, ic, is, mirror, ctrl.hand1.y, ctrl.hand1.z, uint(ctrl.hand1.x));
|
||||||
|
}
|
||||||
|
memoryBarrierBuffer();
|
||||||
|
barrier();
|
||||||
|
if (arm) draw_turned(root, vec2(uintBitsToFloat(hw(6u)), uintBitsToFloat(hw(7u))), c, s, mirror, int(hw(2u)), int(hw(3u)), hw(1u)); // the hand over the grip
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Data the host uploads for the GPU wand / projectile system: cast requests, shot nodes and specs (also appended to by the GPU).
|
||||||
|
layout(std430, set = 0, binding = 35) buffer FxHost { // written by the host: cast requests, shot nodes and specs
|
||||||
|
ivec4 hhead; // x, y: number of requests in list 0 / 1
|
||||||
|
vec4 hcast[1024]; // 2 lists x 256 requests x 2 vec4: (x, y, dir x, dir y), (node or -boom-1, team, seed, flags)
|
||||||
|
vec4 hnodes[8192]; // 2048 nodes x 4 vec4
|
||||||
|
ivec4 hspecs[16384]; // 4096 specs x 4 ivec4
|
||||||
|
};
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
|
||||||
|
layout(std430, set = 0, binding = 56) buffer PartGlow { uint partglow[]; }; // the glowing particles added up per glow texel (r, g, b x 256; glow.comp reads it)
|
||||||
|
#include "simplex3.glsl"
|
||||||
|
|
||||||
|
// Particles: cosmetic ones (emitter trails, sparks) fade and vanish; "real" ones (blood, acid, slime...: max life < 0 encodes the material)
|
||||||
|
// fly like projectiles and turn into a cell of the material where they land. Both draw into the entity layer.
|
||||||
|
|
||||||
|
// Blood on the ground: a red droplet (cosmetic or real) that hits a static cell stains it (bit 31 of the cell word, which static cells do not use); render.comp tints it.
|
||||||
|
void stain_at(ivec2 q, int material, uint seed) {
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||||
|
const vec3 mc = rgb_of(mats[3u * uint(material)].x);
|
||||||
|
if (!(mc.r > 1.5 * mc.g && mc.r > 1.5 * mc.b)) return; // only reddish liquids (blood) stain
|
||||||
|
for (int k = 0; k < 5; ++k) {
|
||||||
|
const ivec2 o = k == 0 ? ivec2(0) : (k == 1 ? ivec2(1, 0) : (k == 2 ? ivec2(-1, 0) : (k == 3 ? ivec2(0, 1) : ivec2(0, -1))));
|
||||||
|
const ivec2 t = q + o;
|
||||||
|
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue;
|
||||||
|
if (k > 0 && (hash3(seed, uint(k), 93u) & 1u) != 0u) continue;
|
||||||
|
const uint m = cell_mat(cells[t.y * pc.gw + t.x]);
|
||||||
|
if (m != 0u && (mflags(m) & F_STATIC) != 0u && (cells[t.y * pc.gw + t.x] & BURN_BIT) == 0u) atomicOr(cells[t.y * pc.gw + t.x], 0x80000000u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 cell_colour(uint cw) { return rgb_of(mats[3u * cell_mat(cw)].x); }
|
||||||
|
|
||||||
|
// A particle of displaced liquid (docs/DISPLACEMENT_AND_FIRE.md, 00713e20 / 00712390 / 0071fba0): it moves by its OLD velocity, accelerates (a quarter of gravity in
|
||||||
|
// mode 1), marches cell by cell (at most 60 steps) and ends where it hits liquid or solid; it then goes back into the grid, keeping its identity.
|
||||||
|
void displaced_particle(uint id, FPart p) {
|
||||||
|
vec2 pos = p.a.xy, vel = p.a.zw;
|
||||||
|
uint kind = p.c.y;
|
||||||
|
uint cw = p.c.x;
|
||||||
|
if (pc.pass_ == 1) { // between ticks: only draw
|
||||||
|
pos -= vel * (1.0 - ctrl.view.z);
|
||||||
|
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
|
||||||
|
uvec3 d8 = uvec3(cell_colour(cw) * 255.0 + 0.5);
|
||||||
|
entlayer[q.y * pc.gw + q.x] = 0xFF000000u | (d8.r << 16) | (d8.g << 8) | d8.b;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint seed = id * 7u + uint(pc.frame);
|
||||||
|
ivec2 here = ivec2(floor(pos));
|
||||||
|
if (kind == 3u) { // waiting for a free place
|
||||||
|
if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
|
||||||
|
// No place: out of the window it is gone (00712390: a particle out of the loaded area dies; it waited there forever and filled the pool - 1000+ after 25 s).
|
||||||
|
// In the window the search of its column ended on a solid (a pocket under an overhang, a pool under ice, a column shifted into the rock): it drifts a cell,
|
||||||
|
// down into a free cell (air, gas), else sideways, and searches again from there; after 10 s it is dropped. (It used to wait where it was: blood and water
|
||||||
|
// pixels frozen in the air near pools.)
|
||||||
|
const ivec2 hq = here - ivec2(pc.view_x, pc.view_y);
|
||||||
|
const uint tries = p.c.w & 0xFFFFFu;
|
||||||
|
if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { fparts[id].b.x = 0.0; return; }
|
||||||
|
ivec2 step = ivec2(0, 1);
|
||||||
|
if (hq.y + 1 >= pc.gh || (mflags(cell_mat(cells[(hq.y + 1) * pc.gw + hq.x])) & (F_STATIC | F_LIQUID)) != 0u) {
|
||||||
|
const int sx = (hash3(seed, tries, 67u) & 1u) != 0u ? 1 : -1;
|
||||||
|
step = ivec2(sx, 0);
|
||||||
|
if (hq.x + sx < 0 || hq.x + sx >= pc.gw || (mflags(cell_mat(cells[hq.y * pc.gw + hq.x + sx])) & F_STATIC) != 0u) step = ivec2(0);
|
||||||
|
}
|
||||||
|
fparts[id].a.xy = pos + vec2(step);
|
||||||
|
fparts[id].c.w = p.c.w + 1u;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float g = 0.1667;
|
||||||
|
vec2 target = pos + vel;
|
||||||
|
vel.y += g * (kind == 1u ? 0.25 : 1.0);
|
||||||
|
if (kind == 1u && vel.y > 0.0) kind = 2u;
|
||||||
|
// 00712390: probes at unit-length increments along the move, at most 60, then the final target is committed
|
||||||
|
const float mlen = length(target - pos);
|
||||||
|
int steps = min(60, int(mlen));
|
||||||
|
vec2 dir = mlen > 1e-6 ? (target - pos) / mlen : vec2(0.0);
|
||||||
|
vec2 cur = pos;
|
||||||
|
uint budget = p.c.z;
|
||||||
|
bool dead = false;
|
||||||
|
float dens = mdensity(cell_mat(cw));
|
||||||
|
for (int s = 0; s < steps && !dead; ++s) {
|
||||||
|
vec2 nx = cur + dir;
|
||||||
|
ivec2 q = ivec2(floor(nx)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { dead = true; break; }
|
||||||
|
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
|
if (m != 0u) {
|
||||||
|
uint f = mflags(m);
|
||||||
|
if ((f & F_STATIC) != 0u) { dead = true; break; }
|
||||||
|
if ((f & F_LIQUID) != 0u) {
|
||||||
|
if (kind == 2u) { dead = true; break; } // mode 0 stops at liquid (and powder)
|
||||||
|
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens; // mode 1: effective density 0 (an upward speed below 50 px/s, or a draw at creation) unless moving up fast; it must be lighter than the target
|
||||||
|
if (!(eff < mdensity(m)) || budget == 0u) { dead = true; break; }
|
||||||
|
budget -= 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = nx;
|
||||||
|
}
|
||||||
|
if (!dead && int(mlen) <= 60) { // the target cell itself
|
||||||
|
ivec2 q = ivec2(floor(target)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) dead = true;
|
||||||
|
else {
|
||||||
|
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
|
if (m != 0u) {
|
||||||
|
uint f = mflags(m);
|
||||||
|
if ((f & F_STATIC) != 0u) dead = true;
|
||||||
|
else if ((f & F_LIQUID) != 0u) {
|
||||||
|
if (kind == 2u) dead = true;
|
||||||
|
else {
|
||||||
|
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens;
|
||||||
|
if (!(eff < mdensity(m)) || budget == 0u) dead = true; else budget -= 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dead) cur = target;
|
||||||
|
}
|
||||||
|
} else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is
|
||||||
|
pos = cur;
|
||||||
|
if (dead) {
|
||||||
|
if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
|
||||||
|
kind = 3u; // no place yet: it keeps trying
|
||||||
|
}
|
||||||
|
fparts[id].a = vec4(pos, vel);
|
||||||
|
fparts[id].c = uvec4(cw, kind, budget, (p.c.w & 0x100000u) | ((p.c.w & 0xFFFFFu) + 1u));
|
||||||
|
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
|
||||||
|
vec3 col = cell_colour(cw);
|
||||||
|
uvec3 c8 = uvec3(col * 255.0 + 0.5);
|
||||||
|
entlayer[q.y * pc.gw + q.x] = 0xFF000000u | (c8.r << 16) | (c8.g << 8) | c8.b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_GlobalInvocationID.x;
|
||||||
|
if (id >= uint(FPART_MAX)) return;
|
||||||
|
FPart p = fparts[id];
|
||||||
|
if (p.b.x <= 0.0) return;
|
||||||
|
if (p.c.y != 0u) { displaced_particle(id, p); return; }
|
||||||
|
const bool real = p.b.y < 0.0;
|
||||||
|
vec2 pos = p.a.xy, vel = p.a.zw;
|
||||||
|
if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions
|
||||||
|
pos -= vel * (1.0 - ctrl.view.z);
|
||||||
|
} else {
|
||||||
|
p.b.x -= 1.0;
|
||||||
|
bool gravDone = false;
|
||||||
|
bool landed = false; vec2 landPos = pos; int landMat = 0;
|
||||||
|
if (real) {
|
||||||
|
const int material = int(-p.b.y) - 1;
|
||||||
|
int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y)))));
|
||||||
|
vec2 prevp = pos;
|
||||||
|
bool done = false; // landed (the cell is left)
|
||||||
|
for (int s = 0; s < steps && p.b.x > 0.0; ++s) {
|
||||||
|
vec2 np = pos + vel / float(steps);
|
||||||
|
ivec2 q = ivec2(floor(np)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { p.b.x = 0.0; break; }
|
||||||
|
if (cell_mat(cells[q.y * pc.gw + q.x]) != 0u) { // landed: the material stays in the last free cell
|
||||||
|
stain_at(q, material, id + uint(pc.frame) * 7u);
|
||||||
|
landed = true; landPos = prevp; landMat = material;
|
||||||
|
p.b.x = 0.0; done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevp = pos;
|
||||||
|
pos = np;
|
||||||
|
}
|
||||||
|
if (p.b.x <= 0.0 && !done) { landed = true; landPos = pos; landMat = material; } // the life ran out in the air: it becomes a cell that falls
|
||||||
|
} else if (p.c.x == 1u) { // a droplet that stops at the first cell it meets
|
||||||
|
const int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y)))));
|
||||||
|
for (int s = 0; s < steps; ++s) {
|
||||||
|
const vec2 np = pos + vel / float(steps);
|
||||||
|
const ivec2 q = ivec2(floor(np)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh || cell_mat(cells[q.y * pc.gw + q.x]) != 0u) {
|
||||||
|
if (p.c.z > 0u && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) stain_at(q, int(p.c.z) - 1, id + uint(pc.frame) * 7u);
|
||||||
|
p.b.x = 0.0; break;
|
||||||
|
}
|
||||||
|
pos = np;
|
||||||
|
}
|
||||||
|
} else if ((p.c.w & 2u) != 0u || p.b.y < 0.0) { // collide_with_grid 0
|
||||||
|
pos += vel;
|
||||||
|
} else {
|
||||||
|
// 00712390, mode 0: probes at unit steps toward the target (at most 60, int(distance + 0.5)); a liquid / powder or solid cell (not gas, fire, air) ends the move
|
||||||
|
// at the last free point. With the bounce flag (6a:20, set at creation) the velocity bounces: vy flips when |dx| <= |dy| else vx; |vx| < 10 px/s gets
|
||||||
|
// + uniform(-10, 10); both are scaled by uniform(0.1, 0.3); a life above 1 s at a speed below 40 px/s becomes uniform(0.5, 1.5) s; below 10 px/s the flag
|
||||||
|
// clears. Without it the particle dies (6a:10). Out of the loaded area: dead. (Velocities here are per frame: px/s / 60, lives in frames.)
|
||||||
|
const vec2 target = pos + vel; // (the old velocity; the forces come before the collision, so the bounce acts on the new one)
|
||||||
|
const float dist = length(vel);
|
||||||
|
const int n = min(60, int(dist + 0.5));
|
||||||
|
const vec2 dir = dist > 0.0 ? vel / dist : vec2(0.0);
|
||||||
|
vel.y += p.b.z; gravDone = true;
|
||||||
|
vec2 cur = pos;
|
||||||
|
bool hit = false;
|
||||||
|
for (int s = 0; s < n; ++s) {
|
||||||
|
const vec2 nx = cur + dir;
|
||||||
|
const ivec2 q = ivec2(floor(nx)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { p.b.x = 0.0; break; }
|
||||||
|
const uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
|
if (m != 0u && (mflags(m) & (F_GAS | F_FIRE | F_AIR)) == 0u) { hit = true; break; }
|
||||||
|
cur = nx;
|
||||||
|
}
|
||||||
|
if (hit) {
|
||||||
|
if ((p.c.w & 4u) != 0u) p.b.x = 0.0;
|
||||||
|
else {
|
||||||
|
const uint sd = id * 13u + uint(pc.frame) * 7u;
|
||||||
|
if (abs(dir.x) <= abs(dir.y)) vel.y = -vel.y; else vel.x = -vel.x;
|
||||||
|
if (abs(vel.x) < 10.0 / 60.0) vel.x += (-10.0 + 20.0 * rnd01(hash3(sd, 1u, 71u))) / 60.0;
|
||||||
|
const float f = 0.1 + 0.2 * rnd01(hash3(sd, 2u, 72u));
|
||||||
|
vel *= f;
|
||||||
|
const float sp2 = dot(vel, vel) * 3600.0;
|
||||||
|
if (p.b.x > 60.0 && sp2 < 1600.0) p.b.x = (0.5 + rnd01(hash3(sd, 3u, 73u))) * 60.0;
|
||||||
|
if (sp2 < 100.0) fparts[id].c.w = p.c.w | 4u;
|
||||||
|
}
|
||||||
|
pos = cur;
|
||||||
|
} else if (p.b.x > 0.0) pos = target;
|
||||||
|
}
|
||||||
|
if (!gravDone) vel += vec2(0.0, p.b.z);
|
||||||
|
if (!real && p.c.x == 0u && ((p.c.z >> 19) & 0xFFu) != 0u) { // attractor_force (00713e20): v += (attractor - pos) x dt x strength, the attractor the emitting projectile while it lives
|
||||||
|
const FProj ap = fprojs[(p.c.z >> 8) & 0x7FFu];
|
||||||
|
if (ap.d.w != 0) vel += (ap.a.xy - pos) * float((p.c.z >> 19) & 0xFFu) / 3600.0;
|
||||||
|
}
|
||||||
|
if (!real && p.c.x == 0u && (p.c.w >> 20) != 0u) { // airflow (00713e20): angle = 3D simplex (00872560) of (target x scale, target y scale, time) x pi; v += (-sin, cos) x airflow_force (px/s, per update, no dt)
|
||||||
|
const float force = float(p.c.w >> 20) / 1000.0, scale = float((p.c.w >> 8) & 0xFFFu) / 4096.0;
|
||||||
|
const float ang = simplex3(vec3(pos * scale, float(pc.frame) / 60.0)) * 3.1415927; // (the time: the particle system's clock, here the frame / 60)
|
||||||
|
vel += vec2(-sin(ang), cos(ang)) * force / 60.0;
|
||||||
|
}
|
||||||
|
fparts[id].a = vec4(pos, vel);
|
||||||
|
fparts[id].b.x = p.b.x;
|
||||||
|
if (landed) { // 00716600 / 0071fba0: the cell goes back into the grid where the particle ended, else the first free (or lighter) cell up to 128 above, drifting
|
||||||
|
// sideways; with no place yet it waits as a returning cell (not lost: a barrel's 750 units of spill all come out)
|
||||||
|
const uint cw = uint(landMat) | (((hash3(id + uint(pc.frame) * 7u, uint(landMat), 91u) >> 16) & 0xFFu) << 16);
|
||||||
|
const uint bv = birth_frame();
|
||||||
|
const ivec2 lq = ivec2(floor(landPos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (lq.x >= 0 && lq.y >= 0 && lq.x < pc.gw && lq.y < pc.gh && !reinsert(cw, ivec2(floor(landPos)), id * 7u + uint(pc.frame), bv)) {
|
||||||
|
fparts[id].a = vec4(landPos, 0.0, 0.0);
|
||||||
|
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
|
||||||
|
fparts[id].c = uvec4(cw, 3u, 0u, 0u);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p.b.x <= 0.0) return;
|
||||||
|
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||||
|
uint bits = floatBitsToUint(p.b.w);
|
||||||
|
float a = (!real && (bits & 0x01000000u) != 0u) ? clamp(p.b.x / p.b.y, 0.0, 1.0) : 1.0;
|
||||||
|
if (!real && p.c.x == 0u && (bits & 0x02000000u) == 0u) a *= 1.0 - float(p.c.z & 0xFFu) / 255.0; // the material's alpha (c.z = 255 - alpha)
|
||||||
|
if ((bits & 0x02000000u) != 0u) { // additive: the colour is scaled by the remaining life, alpha 1 marks it for render.comp
|
||||||
|
vec3 c = rgb_of(bits) * a;
|
||||||
|
uvec3 q8 = uvec3(c * 255.0 + 0.5);
|
||||||
|
entlayer[q.y * pc.gw + q.x] = 0x02000000u | (q8.r << 16) | (q8.g << 8) | q8.b; // (alpha 2: an additive particle; its glow is summed below)
|
||||||
|
// tex_glow_source_particles: every glowing particle adds its light to the glow source (the overlapping particles of a dense trail add up to a white-hot core)
|
||||||
|
const uint gt = uint((q.y >> 1) * ((pc.gw + 1) / 2) + (q.x >> 1)) * 3u;
|
||||||
|
atomicAdd(partglow[gt], uint(c.r * 256.0 + 0.5));
|
||||||
|
atomicAdd(partglow[gt + 1u], uint(c.g * 256.0 + 0.5));
|
||||||
|
atomicAdd(partglow[gt + 2u], uint(c.b * 256.0 + 0.5));
|
||||||
|
} else {
|
||||||
|
entlayer[q.y * pc.gw + q.x] = (uint(a * 255.0) << 24) | (bits & 0xFFFFFFu);
|
||||||
|
}
|
||||||
|
if (!real && p.c.x == 0u && (p.c.z & 0x8000000u) != 0u && (bits & 0x02000000u) != 0u) { // draw_as_long: a streak back along the motion, as long as a frame's travel (at most 8 px), fading
|
||||||
|
const float sl = min(length(vel), 8.0);
|
||||||
|
const vec2 bd = length(vel) > 1e-4 ? -normalize(vel) : vec2(0.0);
|
||||||
|
for (int s = 1; s <= int(sl); ++s) {
|
||||||
|
const ivec2 qs = ivec2(floor(pos + bd * float(s))) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (qs.x < 0 || qs.y < 0 || qs.x >= pc.gw || qs.y >= pc.gh) break;
|
||||||
|
const vec3 c = rgb_of(bits) * a * (1.0 - float(s) / (sl + 1.0));
|
||||||
|
const uvec3 q8 = uvec3(c * 255.0 + 0.5);
|
||||||
|
entlayer[qs.y * pc.gw + qs.x] = 0x02000000u | (q8.r << 16) | (q8.g << 8) | q8.b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
// State of the GPU projectile / effect system (device local, all simulated by proj_*.comp and fx_*.comp).
|
||||||
|
const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512; // (the GPU's ring is 0..255, the host writes 256..511)
|
||||||
|
// FProj.d.z bit 30: a CPU query materialized this instance; automatic Lua yields to its twin.
|
||||||
|
struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive
|
||||||
|
struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades)
|
||||||
|
struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle
|
||||||
|
layout(std430, set = 0, binding = 36) buffer FxState {
|
||||||
|
uint fcnt[4]; // projectile ring, particle ring, animation ring
|
||||||
|
FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj)
|
||||||
|
FPart fparts[16384];
|
||||||
|
FAnim fanims[512];
|
||||||
|
};
|
||||||
|
|
||||||
|
// A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead
|
||||||
|
// record of the next 64; -1 when there is none.
|
||||||
|
int alloc_part() {
|
||||||
|
const uint s0 = atomicAdd(fcnt[1], 1u);
|
||||||
|
for (uint k = 0u; k < 64u; ++k) {
|
||||||
|
const uint s = (s0 + k) % uint(FPART_MAX);
|
||||||
|
if (fparts[s].b.x <= 0.0) return int(s);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent
|
||||||
|
// (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp)
|
||||||
|
int emit_particle_g(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive, bool collide) { // the record, -1 when the pool is full
|
||||||
|
const int si = alloc_part();
|
||||||
|
if (si < 0) return -1;
|
||||||
|
const uint s = uint(si);
|
||||||
|
fparts[s].a = vec4(pos, vel);
|
||||||
|
fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | (fade ? 0x01000000u : 0u) | (additive ? 0x02000000u : 0u)));
|
||||||
|
fparts[s].c = uvec4(0u, 0u, 0u, collide ? 0u : 2u); // c.z: 255 - the material's alpha (drawn opacity of a non-additive cosmetic)
|
||||||
|
return si;
|
||||||
|
}
|
||||||
|
// the cosmetic constructor 00716ce0 without force-create: the particle is made only where the grid slot is empty (and loaded)
|
||||||
|
bool cell_free_at(vec2 w) {
|
||||||
|
const ivec2 q = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u;
|
||||||
|
}
|
||||||
|
void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); }
|
||||||
|
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; with a material (c.z = material + 1) it leaves a cell of it there, so the spray feeds the pools.
|
||||||
|
uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full
|
||||||
|
const int si = alloc_part();
|
||||||
|
if (si < 0) return 0xFFFFFFFFu;
|
||||||
|
const uint s = uint(si);
|
||||||
|
fparts[s].a = vec4(pos, vel);
|
||||||
|
fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x01000000u));
|
||||||
|
fparts[s].c = uvec4(1u, 0u, uint(material + 1), 0u);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// A particle that becomes a cell of the material when it lands (blood, acid... "real" particles): max life is -(material + 1).
|
||||||
|
// the acceleration of a particle record (00712010 copies 0115499c / 011549a0, which the particle manager resets to (0, pixels_per_physics_unit x 10) = (0, 100) px/s^2,
|
||||||
|
// 007128c0 / 00712bc0); the real particles (00716af0 -> 007168c0) keep it: 100 / 3600 cells per frame^2
|
||||||
|
const float PART_GRAV = 100.0 / 3600.0;
|
||||||
|
bool emit_material_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // false when the pool is full
|
||||||
|
const int si = alloc_part();
|
||||||
|
if (si < 0) return false;
|
||||||
|
const uint s = uint(si);
|
||||||
|
fparts[s].a = vec4(pos, vel);
|
||||||
|
fparts[s].b = vec4(life, -float(material + 1), grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x04000000u));
|
||||||
|
fparts[s].c = uvec4(0u); // a flag in the top byte keeps the bits a normal float (a zero top byte is a denormal and may be flushed)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- liquid taken out of the grid by an entity, a projectile or a body (docs/DISPLACEMENT_AND_FIRE.md): the cell becomes a particle that keeps its identity
|
||||||
|
// (the whole cell word) and goes back into the grid when it has landed. c.y: 1 = mode 1 (moving up, passes liquids with a penetration budget c.z), 2 = mode 0
|
||||||
|
// (falls, stops at liquid or solid), 3 = waiting for a free place (retries every tick).
|
||||||
|
void emit_displaced(vec2 pos, vec2 vel, uint cellword, uint kind, uint birthv) { // birthv: the age (birth frame) of the cell travels with it (00717670 / 007168c0 hand over the same cell object)
|
||||||
|
const int si = alloc_part();
|
||||||
|
if (si < 0) return; // the pool is full: the cell is gone, as in the original (the caller cleared the grid slot first)
|
||||||
|
const uint s = uint(si);
|
||||||
|
fparts[s].a = vec4(pos, vel);
|
||||||
|
fparts[s].b = vec4(1.0e6, 1.0, uintBitsToFloat(birthv), 0.0);
|
||||||
|
fparts[s].c = uvec4(cellword, kind, kind == 1u ? 20u : 0u, (hash3(s, cellword, 77u) % 101u) < 50u ? 0x100000u : 0u); // bit 20: effective density zero (an inclusive draw below 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a liquid cell (not a powder) out of the grid and makes it a particle with the velocity v (cells per frame). Returns whether it did.
|
||||||
|
bool displace_cell(ivec2 w, vec2 v) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
uint c = cells[i];
|
||||||
|
uint bv = birth[i];
|
||||||
|
uint m = cell_mat(c);
|
||||||
|
if (m == 0u) return false;
|
||||||
|
uint f = mflags(m);
|
||||||
|
if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) return false;
|
||||||
|
if (atomicCompSwap(cells[i], c, 0u) != c) return false; // the grid slot is cleared before the particle exists
|
||||||
|
ext[i] = vec2(0.0);
|
||||||
|
emit_displaced(vec2(w) + 0.5, v, c, 1u, bv); // mode 1; it turns into mode 0 when it moves down (fx_particles.comp)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the cell at the world position a liquid that counts for the drag (liquid, not a powder)?
|
||||||
|
bool liquid_nonsand(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m == 0u) return false;
|
||||||
|
uint f = mflags(m);
|
||||||
|
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0071fba0: puts a cell back into the grid. Searches from (x, y) upward to y - 128: an empty cell accepts it, a non-solid cell that is lighter is exchanged (the
|
||||||
|
// lighter cell then looks for a place of its own: it becomes a waiting particle); equal density is rejected, a solid cell ends the search; after more than
|
||||||
|
// five steps a rejection may shift the column (20 %, then 50 % right / left).
|
||||||
|
bool reinsert(uint cw, ivec2 w, uint seed, uint birthv) {
|
||||||
|
float dens = mdensity(cell_mat(cw));
|
||||||
|
for (int step = 0; step <= 128; ++step) {
|
||||||
|
ivec2 t = ivec2(w.x, w.y - step);
|
||||||
|
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; // not loaded: no random draw either
|
||||||
|
int ti = q.y * pc.gw + q.x;
|
||||||
|
uint occ = cells[ti];
|
||||||
|
uint om = cell_mat(occ);
|
||||||
|
if (om == 0u) {
|
||||||
|
if (atomicCompSwap(cells[ti], 0u, cw) == 0u) { ext[ti] = vec2(0.0); birth[ti] = birthv; return true; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint of = mflags(om);
|
||||||
|
if ((of & F_STATIC) != 0u) return false; // a solid cell stops the search
|
||||||
|
if (mdensity(om) < dens && atomicCompSwap(cells[ti], occ, cw) == occ) { // the lighter cell is queued, not destroyed
|
||||||
|
ext[ti] = vec2(0.0);
|
||||||
|
const uint obirth = birth[ti];
|
||||||
|
birth[ti] = birthv;
|
||||||
|
emit_displaced(vec2(t) + 0.5, vec2(0.0), occ, 3u, obirth);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (step > 5 && (hash3(seed, uint(step), 61u) % 101u) < 20u) w.x += (hash3(seed, uint(step), 62u) % 101u) < 50u ? 1 : -1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// The glow of the materials (gfx_glow of materials.xml), as the game's data/shaders/post_glow1.frag and post_glow2.frag do it: the glow builds up over frames.
|
||||||
|
// pass 1 (post_glow1): H = 0.1 x the sum of 11 taps of the previous frame's glow, 1.5 texels apart, horizontally
|
||||||
|
// pass 2 (post_glow2): D = the sum of 11 taps of H vertically / 12.2; N = the new source x 4 ("HDR": gfx_glow 255 is the old default brightness, up to 1000);
|
||||||
|
// glow = N x 0.125 + mix(D, N, 0.05)
|
||||||
|
// In a steady area the glow settles near 3 x the source and spreads into a wide soft halo (the white core of a dense fire). Half resolution (a texel = 2 x 2 cells);
|
||||||
|
// the previous frame's glow follows the camera (its view is kept after the texels). render.comp adds it to the lights and blends it like post_final.frag.
|
||||||
|
layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; };
|
||||||
|
layout(std430, set = 0, binding = 47) buffer GlowA { vec4 ga[]; }; // the glow (persistent), then one vec4: the view of the frame that wrote it
|
||||||
|
layout(std430, set = 0, binding = 48) buffer GlowB { vec4 gb[]; }; // the horizontal pass
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
|
||||||
|
layout(std430, set = 0, binding = 21) readonly buffer EntLayer { uint entlayer[]; };
|
||||||
|
layout(std430, set = 0, binding = 56) readonly buffer PartGlow { uint partglow[]; }; // the glowing particles of the frame added up per texel (fx_particles.comp) // the sprites and particles of the frame: the additive particles (alpha 1) are the particle glow
|
||||||
|
#define FPAL_TABLE tbl
|
||||||
|
// ---- the fire palettes of the cell blitting (main.cpp loads them after the tables; 00722490 the lookup, 00724580 the use) ----
|
||||||
|
uint fpal_word(uint k) { return FPAL_TABLE[1024u + 9u * mats[mats.length() - 1u].w + k]; }
|
||||||
|
vec3 fpal_rgb(uint w) { return vec3(float((w >> 16) & 255u), float((w >> 8) & 255u), float(w & 255u)) / 255.0; }
|
||||||
|
vec3 fire_lookup(uint at, uint row, uint w, float t) { // the row of the image at t x width: the first / last pixel outside, else the two neighbours mixed
|
||||||
|
const float x = float(w) * t;
|
||||||
|
const uint r0 = at + row * w;
|
||||||
|
if (x >= float(w)) return fpal_rgb(fpal_word(r0 + w - 1u));
|
||||||
|
if (x <= 0.0) return fpal_rgb(fpal_word(r0));
|
||||||
|
const uint i0 = uint(floor(x));
|
||||||
|
if (float(i0) == x || i0 + 1u >= w) return fpal_rgb(fpal_word(r0 + i0));
|
||||||
|
return mix(fpal_rgb(fpal_word(r0 + i0)), fpal_rgb(fpal_word(r0 + i0 + 1u)), x - float(i0));
|
||||||
|
}
|
||||||
|
uint fire_row(uint m) { return min(fpal_word(132u + m), 5u); } // CellData +0x68 fire_colors_index
|
||||||
|
float fire_age(uint b) { return float(birth_age(b)) + float(hash3(b & BIRTH_MASK, 77u, 5u) % 21u); } // 00709ef0: the flame's frame is the creation frame - uniform(0, 20)
|
||||||
|
vec3 fire_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(0u, fire_row(m), 8u, a * a); } // colors_fire.png at (age / 40)^2
|
||||||
|
vec3 fire_glow_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(48u, fire_row(m), 8u, fire_age(b) < 15.0 ? a : a * a); } // colors_fire_glow.png (young: age / 40)
|
||||||
|
vec3 burning_colour(uint m, uint h) { return fpal_rgb(fpal_word(96u + fire_row(m) * 6u + h % 6u)); } // colors_lava.bmp: a random pixel of the row
|
||||||
|
const float TAP = 1.5; // texels between the taps (post_glow*.frag: one_per_glow_texture_size x 1.5)
|
||||||
|
|
||||||
|
vec3 fetch(bool fromA, vec2 t, int hw, int hh) { // bilinear, zero outside
|
||||||
|
const vec2 q = t - 0.5;
|
||||||
|
const ivec2 q0 = ivec2(floor(q));
|
||||||
|
const vec2 f = q - vec2(q0);
|
||||||
|
vec3 r = vec3(0.0);
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const ivec2 s = q0 + ivec2(k & 1, k >> 1);
|
||||||
|
if (s.x < 0 || s.y < 0 || s.x >= hw || s.y >= hh) continue;
|
||||||
|
const float w = ((k & 1) != 0 ? f.x : 1.0 - f.x) * ((k >> 1) != 0 ? f.y : 1.0 - f.y);
|
||||||
|
r += (fromA ? ga[s.y * hw + s.x].rgb : gb[s.y * hw + s.x].rgb) * w;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const int hw = (pc.gw + 1) / 2, hh = (pc.gh + 1) / 2;
|
||||||
|
const ivec2 q = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (pc.pass_ == 0) return; // (the source is read in pass 2)
|
||||||
|
if (q.x >= hw || q.y >= hh) return;
|
||||||
|
const vec2 t = vec2(q) + 0.5;
|
||||||
|
if (pc.pass_ == 1) {
|
||||||
|
// the previous frame's glow, moved with the camera (the view it was written in; half a texel per cell)
|
||||||
|
const vec4 pv = ga[hw * hh];
|
||||||
|
const vec2 sh = pv.w > 0.5 ? (vec2(float(pc.view_x), float(pc.view_y)) - pv.xy) * 0.5 : vec2(1.0e6);
|
||||||
|
vec3 acc = vec3(0.0);
|
||||||
|
for (int x = -5; x <= 5; ++x) acc += fetch(true, t + sh + vec2(float(x) * TAP, 0.0), hw, hh);
|
||||||
|
gb[q.y * hw + q.x] = vec4(acc * 0.1, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// pass 2
|
||||||
|
vec3 dec = vec3(0.0);
|
||||||
|
for (int y = -5; y <= 5; ++y) dec += fetch(false, t + vec2(0.0, float(y) * TAP), hw, hh);
|
||||||
|
dec *= 1.0 / 12.2;
|
||||||
|
vec3 src = vec3(0.0); // the cells of the texel: colour x gfx_glow / 1000, averaged (the cell blitting's HDR range), x 4
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const ivec2 c = min(q * 2 + ivec2(k & 1, k >> 1), ivec2(pc.gw - 1, pc.gh - 1));
|
||||||
|
const uint cw = cells[c.y * pc.gw + c.x];
|
||||||
|
const uint m = cell_mat(cw);
|
||||||
|
if (m == 0u) continue;
|
||||||
|
const uint g = gtab[m] & 0xFFFFu;
|
||||||
|
if ((mflags(m) & F_FIRE) != 0u) { src += fire_glow_colour(m, birth[c.y * pc.gw + c.x]); continue; } // a flame: colors_fire_glow.png by its age, as it is (the blitter writes the palette colour, not scaled by gfx_glow)
|
||||||
|
if ((cw & BURN_BIT) != 0u) { src += burning_colour(m, hash3(uint(c.x + pc.view_x), uint(c.y + pc.view_y), uint(pc.frame) + 3u)) * 0.25; continue; } // a burning cell: its colour / 4 (00722340 with 0x40)
|
||||||
|
if (g == 0u) continue;
|
||||||
|
const uint mc = mats[3u * m].x;
|
||||||
|
src += vec3(float((mc >> 16) & 0xFFu), float((mc >> 8) & 0xFFu), float(mc & 0xFFu)) / 255.0 * (float(g) / 1000.0);
|
||||||
|
}
|
||||||
|
vec3 part = vec3(0.0); // post_glow2.frag: + tex_glow_source_particles x 2.5 (the glowing particles: here the additive ones of the frame, fx_particles.comp)
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const ivec2 c = min(q * 2 + ivec2(k & 1, k >> 1), ivec2(pc.gw - 1, pc.gh - 1));
|
||||||
|
const uint e = entlayer[c.y * pc.gw + c.x];
|
||||||
|
if ((e >> 24) == 1u) part += vec3(float((e >> 16) & 255u), float((e >> 8) & 255u), float(e & 255u)) / 255.0; // (additive sprites and projectiles)
|
||||||
|
else if ((e >> 24) == 253u) src += vec3(float((e >> 16) & 255u), float((e >> 8) & 255u), float(e & 255u)) / 255.0 * 0.25; // a burning pixel of a body (sprites.comp): its colour / 4, as a burning cell
|
||||||
|
}
|
||||||
|
{ // the particles: all of them, added up (fx_particles.comp)
|
||||||
|
const uint gt = uint(q.y * hw + q.x) * 3u;
|
||||||
|
part += vec3(float(partglow[gt]), float(partglow[gt + 1u]), float(partglow[gt + 2u])) / 256.0;
|
||||||
|
}
|
||||||
|
const vec3 N = src * 0.25 * 4.0 + part * 0.25 * 2.5;
|
||||||
|
ga[q.y * hw + q.x] = vec4(N * 0.125 + mix(dec, N, 0.05), 1.0);
|
||||||
|
if (q.x == 0 && q.y == 0) ga[hw * hh] = vec4(float(pc.view_x), float(pc.view_y), 0.0, 1.0); // (the next frame's pass 1 reads all texels before this pass runs again)
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Movement intent. Every mobile cell decides at most one move per pass following the
|
||||||
|
// original engine's rules (noita.exe CLiquidCell / CGasCell / CFireCell updates, see
|
||||||
|
// docs/ENGINE_ANALYSIS.md). Conflicts are resolved afterwards by claim arbitration, then
|
||||||
|
// apply.comp performs the winning swaps. Several passes per frame stand in for the
|
||||||
|
// original's multi-cell steps.
|
||||||
|
|
||||||
|
uint rnd(uint i, uint salt) { return hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), salt + uint(pc.seed)); }
|
||||||
|
int rand100(uint i, uint salt) { return int(rnd(i, salt) % 101u); } // rand() % 101
|
||||||
|
float rand01(uint i, uint salt) { return float(rnd(i, salt) & 0x7FFFu) / 32767.0; }
|
||||||
|
|
||||||
|
bool in_bounds(ivec2 p) { return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh; }
|
||||||
|
uint idx(ivec2 p) { return uint(p.y * pc.gw + p.x); }
|
||||||
|
bool is_air(uint m) { return (mflags(m) & F_AIR) != 0u; }
|
||||||
|
|
||||||
|
// FUN_0070b200: may a liquid/powder `mm` enter a cell holding material `tm`?
|
||||||
|
bool can_enter_liquid(uint mm, uint tm, bool roll25) {
|
||||||
|
uint tf = mflags(tm);
|
||||||
|
if ((tf & F_AIR) != 0u) return true;
|
||||||
|
if ((tf & (F_GAS | F_FIRE)) != 0u) return true; // gas and fire are always displaced
|
||||||
|
if ((tf & F_STATIC) != 0u) return false; // solids are never entered
|
||||||
|
if (!roll25) return false; // dense-through-light swaps only 25% of the time
|
||||||
|
if ((mflags(mm) & F_SAND) != 0u && (tf & F_SAND) != 0u) return false; // powder never through powder
|
||||||
|
return mdensity(tm) < mdensity(mm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUN_0070a5a0: may a gas `mm` enter a cell holding material `tm`?
|
||||||
|
bool can_rise(uint mm, uint tm, bool roll25) {
|
||||||
|
uint tf = mflags(tm);
|
||||||
|
if ((tf & F_AIR) != 0u) return true;
|
||||||
|
if ((tf & (F_STATIC | F_LIQUID)) != 0u) return false;
|
||||||
|
if (tm == mm) return false;
|
||||||
|
return roll25 && mdensity(tm) > mdensity(mm);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
uint i = idx(p);
|
||||||
|
intent[i] = uvec4(NONE, 0u, 0u, 0u);
|
||||||
|
|
||||||
|
uint c = cells[i];
|
||||||
|
uint m = cell_mat(c);
|
||||||
|
uint f = mflags(m);
|
||||||
|
if ((f & (F_AIR | F_STATIC)) != 0u) return;
|
||||||
|
|
||||||
|
vec2 e = ext[i];
|
||||||
|
float vy = e.x, vx = e.y;
|
||||||
|
float nvy = vy, nvx = vx;
|
||||||
|
uint target = NONE;
|
||||||
|
uint flip = 0u;
|
||||||
|
uint prio = ((hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), 777u + uint(pc.seed)) & 0xFFFu) << 20) | i; // unique per cell (12 random bits, the cell index in 20)
|
||||||
|
bool roll25 = rand100(i, 11u) > 74;
|
||||||
|
const int P = pc.pass_;
|
||||||
|
|
||||||
|
if ((f & F_LIQUID) != 0u) {
|
||||||
|
const bool sand = (f & F_SAND) != 0u;
|
||||||
|
{ // 0070c6e0: an active liquid / powder with a positive lifetime disappears when its age (frames since it got its material) reaches it
|
||||||
|
const int lt = mlifetime(m);
|
||||||
|
if (lt > 0 && int(birth_age(birth[i])) >= lt) { cells[i] = 0u; ext[i] = vec2(0.0); return; }
|
||||||
|
}
|
||||||
|
// Powders with liquid_sticks_to_ceiling (soil, mud, moss...): 0070b2d0 attaches them to the cell on one side / above before they would fall, 0070c6e0 releases them again.
|
||||||
|
const uint ceilP = sand ? mceiling(m) : 0u;
|
||||||
|
if (ceilP > 0u) {
|
||||||
|
const ivec2 W = p + ivec2(pc.view_x, pc.view_y);
|
||||||
|
if ((c & ATTACH_BIT) != 0u) { // passive: never moves by itself
|
||||||
|
if (P == 0) {
|
||||||
|
const uint r1 = rnd(i, 91u) % 101u;
|
||||||
|
if (r1 < 20u) { // ceiling check
|
||||||
|
const uint st = (c >> 24) & 3u;
|
||||||
|
const ivec2 sp2 = p + (st == 1u ? ivec2(1, 0) : st == 3u ? ivec2(-1, 0) : ivec2(0, -1));
|
||||||
|
bool support = false;
|
||||||
|
if (in_bounds(sp2)) {
|
||||||
|
const uint nc = cells[idx(sp2)], nm = cell_mat(nc);
|
||||||
|
support = nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u));
|
||||||
|
}
|
||||||
|
bool release = !support;
|
||||||
|
if (support) {
|
||||||
|
const uint r2 = rnd(i, 92u) % 101u;
|
||||||
|
const ivec2 bp = p + ivec2(0, 1);
|
||||||
|
release = r2 < ceilP && in_bounds(bp) && cell_mat(cells[idx(bp)]) == 0u;
|
||||||
|
}
|
||||||
|
if (release) cells[i] = c & ~(ATTACH_BIT | (3u << 24));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// attachment: one location chosen by a hash of the coordinates (0070acc0): even rows give 7 + u % 3 (7 = left, else above), odd rows 5 - u % 3 (5 = right, else above)
|
||||||
|
{
|
||||||
|
int u = W.x + 0x67577458 + 91 * W.y;
|
||||||
|
u = int((uint((u >> 13) ^ u)) * 214013u + 2531011u);
|
||||||
|
u = ((u >> ((W.x & 15) + 1)) ^ u) & 32767;
|
||||||
|
const int code = (W.y & 1) == 0 ? 7 + u % 3 : 5 - u % 3;
|
||||||
|
const ivec2 ap = p + (code == 5 ? ivec2(1, 0) : code == 7 ? ivec2(-1, 0) : ivec2(0, -1));
|
||||||
|
if (in_bounds(ap)) {
|
||||||
|
const uint nc = cells[idx(ap)], nm = cell_mat(nc);
|
||||||
|
if (nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u))) {
|
||||||
|
cells[i] = (c & ~(3u << 24)) | ATTACH_BIT | ((code == 5 ? 1u : code == 7 ? 3u : 2u) << 24);
|
||||||
|
ext[i] = vec2(0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float g = mgravity(m);
|
||||||
|
const bool falling = vy > 0.0;
|
||||||
|
// powders that are already falling skip a frame with probability (1 - gravity) * 0.5
|
||||||
|
if (sand && falling && rand01(i, 3u) < (1.0 - g) * 0.5) return;
|
||||||
|
|
||||||
|
const int extra = vy > 1.0 ? int(ceil((vy - 1.0) / 2.0)) : 0; // extra cells per frame from accumulated velocity
|
||||||
|
const bool downAllowed = (P == 0) || (P <= extra);
|
||||||
|
|
||||||
|
// drift of extra fall steps: 1/8 left, 1/8 right, else straight; liquids follow vx when it is large
|
||||||
|
int dx = 0;
|
||||||
|
if (P > 0) {
|
||||||
|
if (!sand && abs(vx) >= 1.0) dx = vx >= 0.0 ? 1 : -1;
|
||||||
|
else if (!sand) {
|
||||||
|
// 0070b2d0 / 0070b7c0: the drift is drawn afresh only when (r & 127) < 64 (slime: < 12), otherwise the previous one of this update stays; the extra passes of one frame
|
||||||
|
// replay that chain
|
||||||
|
const uint gateLimit = (f & F_SLIME) != 0u ? 12u : 64u;
|
||||||
|
for (int k = 1; k <= P; ++k) {
|
||||||
|
const uint rg = hash3(i, uint(pc.frame) * 16u + uint(k), 5u + uint(pc.seed));
|
||||||
|
if (((rg >> 16) & 127u) < gateLimit) {
|
||||||
|
const uint r = hash3(i, uint(pc.frame) * 16u + uint(k), 6u + uint(pc.seed)) & 7u;
|
||||||
|
dx = r == 1u ? -1 : (r == 2u ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((rnd(i, 5u) & 1u) == 0u) {
|
||||||
|
uint r = rnd(i, 6u) & 7u;
|
||||||
|
dx = r == 1u ? -1 : (r == 2u ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool belowFree = false;
|
||||||
|
bool belowFalling = false;
|
||||||
|
ivec2 below = p + ivec2(0, 1);
|
||||||
|
if (in_bounds(below)) {
|
||||||
|
const uint bm = cell_mat(cells[idx(below)]);
|
||||||
|
belowFree = can_enter_liquid(m, bm, roll25);
|
||||||
|
// The original updates bottom-up, so a falling cell below has already moved by the time this cell
|
||||||
|
// runs. We decide from a snapshot, so wait for it instead of sliding sideways past it.
|
||||||
|
if ((mflags(bm) & F_LIQUID) != 0u) {
|
||||||
|
// already falling, or about to: it has free space of its own below it
|
||||||
|
belowFalling = ext[idx(below)].x > 0.0;
|
||||||
|
ivec2 below2 = below + ivec2(0, 1);
|
||||||
|
if (!belowFalling && in_bounds(below2))
|
||||||
|
belowFalling = can_enter_liquid(bm, cell_mat(cells[idx(below2)]), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downAllowed) {
|
||||||
|
ivec2 t = p + ivec2(dx, 1);
|
||||||
|
if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) {
|
||||||
|
target = idx(t);
|
||||||
|
if (P == 0) nvy = falling ? vy + g * 0.25 : 1.0e-4; // 0070b2d0: the falling state adds gravity / 4, a newly falling cell resets vy (the tiny value marks the falling state)
|
||||||
|
// 0070bdc0: after the fall the lateral loop still runs (while |vx| >= 1: a cell sideways, vx *= liquid_damping), so the sideways speed of a falling liquid
|
||||||
|
// dies within a frame or two; the GPU makes one move per pass, so the damping is applied here (a drop kept vx = +-1 from a diagonal slide and drifted
|
||||||
|
// diagonally on every extra fall step: the diagonal chains of falling water / oil)
|
||||||
|
nvx = (!sand && (f & F_SLIME) == 0u && P == 0 && abs(vx) >= 1.0) ? vx * mdamping(m) : vx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == NONE && !belowFree && belowFalling) return; // wait for the falling cell below to move on
|
||||||
|
|
||||||
|
if (target == NONE && P == 0) {
|
||||||
|
if (belowFree == false) nvy = 0.0; // blocked below: stop falling
|
||||||
|
if (sand) {
|
||||||
|
// diagonal slide with probability liquid_viscosity %, left first then right
|
||||||
|
int visc = mviscosity(m);
|
||||||
|
if (visc < 100 && rand100(i, 7u) < visc) {
|
||||||
|
ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1);
|
||||||
|
if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l);
|
||||||
|
else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), rand100(i, 12u) > 74)) target = idx(r);
|
||||||
|
}
|
||||||
|
} else if ((f & F_SLIME) != 0u && !belowFree) {
|
||||||
|
// 0070b7c0 (slime): the viscosity roll ends the move (no diagonal fallback), then left-down, right-down in this order, then a lateral step only when exactly one side is
|
||||||
|
// free: vx += 0.9, one move when |vx| >= 1 (vx -= 1); two free sides do nothing
|
||||||
|
int visc = mviscosity(m);
|
||||||
|
if (!(visc < 100 && rand100(i, 7u) < visc)) {
|
||||||
|
ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1);
|
||||||
|
if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l);
|
||||||
|
else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), roll25)) target = idx(r);
|
||||||
|
else {
|
||||||
|
ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0);
|
||||||
|
bool lv = in_bounds(lp), rv = in_bounds(rp);
|
||||||
|
bool lo = lv && cell_mat(cells[idx(lp)]) != 0u, ro = rv && cell_mat(cells[idx(rp)]) != 0u;
|
||||||
|
if (rv && !ro && (!lv || lo)) { nvx = vx + 0.9; if (nvx >= 1.0) { target = idx(rp); nvx -= 1.0; } }
|
||||||
|
else if (lv && !lo && (!rv || ro)) { nvx = vx - 0.9; if (nvx <= -1.0) { target = idx(lp); nvx += 1.0; } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!belowFree) {
|
||||||
|
// lateral flow: accelerate toward the single free side, then move when |vx| reaches 1
|
||||||
|
ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0);
|
||||||
|
bool lf = in_bounds(lp) && can_enter_liquid(m, cell_mat(cells[idx(lp)]), roll25);
|
||||||
|
bool rf = in_bounds(rp) && can_enter_liquid(m, cell_mat(cells[idx(rp)]), rand100(i, 13u) > 74);
|
||||||
|
if (lf != rf) {
|
||||||
|
nvx = vx + (rf ? mflow(m) : -mflow(m));
|
||||||
|
if (rf && nvx >= 1.0) { target = idx(rp); nvx *= mdamping(m); }
|
||||||
|
else if (lf && nvx <= -1.0) { target = idx(lp); nvx *= mdamping(m); }
|
||||||
|
}
|
||||||
|
if (target == NONE) {
|
||||||
|
// random diagonal-down side, then the other
|
||||||
|
int dir = (rnd(i, 8u) & 1u) != 0u ? 1 : -1;
|
||||||
|
ivec2 a = p + ivec2(dir, 1), b = p + ivec2(-dir, 1);
|
||||||
|
if (in_bounds(a) && can_enter_liquid(m, cell_mat(cells[idx(a)]), roll25)) { target = idx(a); nvx = float(dir); }
|
||||||
|
else if (in_bounds(b) && can_enter_liquid(m, cell_mat(cells[idx(b)]), rand100(i, 14u) > 74)) { target = idx(b); nvx = float(-dir); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// damped ballistic lateral travel while |vx| >= 1 (one cell per pass, vx *= damping per cell)
|
||||||
|
if (target == NONE && !sand && (f & F_SLIME) == 0u && !belowFree && abs(nvx) >= 1.0) { // (slime has no inertia loop: 0070b7c0 returns)
|
||||||
|
int s = nvx >= 0.0 ? 1 : -1;
|
||||||
|
ivec2 t = p + ivec2(s, 0);
|
||||||
|
if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) { target = idx(t); nvx *= mdamping(m); }
|
||||||
|
// blocked: the velocity stays (the original keeps it until its state / activity logic changes it)
|
||||||
|
}
|
||||||
|
} else if ((f & F_GAS) != 0u) {
|
||||||
|
if (P != 0) return; // gases act once per frame
|
||||||
|
const int lifetime = mlifetime(m);
|
||||||
|
if (lifetime > 0 && vy >= float(lifetime)) { cells[i] = 0u; ext[i] = vec2(0.0); return; } // 0070a670: the move counter (0070aad0 counts the successful moves) against the lifetime
|
||||||
|
uint sp = mgasbits(m);
|
||||||
|
if (rand100(i, 21u) <= int(sp & 255u)) {
|
||||||
|
if (rand100(i, 22u) < int((sp >> 8) & 255u)) {
|
||||||
|
ivec2 t = p + ivec2(0, -1);
|
||||||
|
if (in_bounds(t) && can_rise(m, cell_mat(cells[idx(t)]), roll25)) target = idx(t);
|
||||||
|
}
|
||||||
|
if (target == NONE && rand100(i, 23u) < int((sp >> 16) & 255u)) {
|
||||||
|
int dir = (c & DIR_BIT) != 0u ? 1 : -1;
|
||||||
|
ivec2 a = p + ivec2(dir, 0), b = p + ivec2(-dir, 0);
|
||||||
|
if (in_bounds(a) && can_rise(m, cell_mat(cells[idx(a)]), roll25)) target = idx(a);
|
||||||
|
else if (in_bounds(b) && can_rise(m, cell_mat(cells[idx(b)]), rand100(i, 25u) > 74)) { target = idx(b); flip = 1u; }
|
||||||
|
}
|
||||||
|
if (target == NONE && rand100(i, 24u) < int((sp >> 24) & 255u)) {
|
||||||
|
ivec2 t = p + ivec2(0, 1);
|
||||||
|
if (in_bounds(t) && is_air(cell_mat(cells[idx(t)]))) target = idx(t); // downward only into empty cells
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((f & F_FIRE) != 0u) {
|
||||||
|
if (P != 0) return;
|
||||||
|
if (rand100(i, 31u) < 35) {
|
||||||
|
ivec2 up = p + ivec2(0, -1);
|
||||||
|
if (in_bounds(up) && is_air(cell_mat(cells[idx(up)]))) target = idx(up);
|
||||||
|
else if (rand100(i, 32u) >= 35) {
|
||||||
|
int s = rand100(i, 33u) < 50 ? -1 : 1;
|
||||||
|
ivec2 a = p + ivec2(s, 0), b = p + ivec2(-s, 0);
|
||||||
|
if (in_bounds(a) && is_air(cell_mat(cells[idx(a)]))) target = idx(a);
|
||||||
|
else if (in_bounds(b) && is_air(cell_mat(cells[idx(b)]))) target = idx(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((f & F_FIRE) != 0u && target == NONE && (rnd(i, 30u) & 15u) == 0u) { cells[i] = 0u; return; } // 00709fc0: the extinction test (1/16) sits at the tail: a successful move and the early returns bypass it
|
||||||
|
if ((f & F_GAS) != 0u && target != NONE) nvy = vy + 1.0; // 0070aad0: the gas age counts its successful moves
|
||||||
|
if (target != NONE) {
|
||||||
|
intent[i] = uvec4(target | (flip << 31), prio, floatBitsToUint(nvy), floatBitsToUint(nvx));
|
||||||
|
atomicMax(claims[i], prio);
|
||||||
|
atomicMax(claims[target], prio);
|
||||||
|
} else {
|
||||||
|
ext[i] = vec2(nvy, nvx); // resting updates (velocity bookkeeping)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// The light of the entities (LightComponent, up to 128 near the window) at every cell of the window, once per frame: render.comp's lighting() reads it.
|
||||||
|
// (render.comp summed every light for every output pixel - some 36 pixels per cell in a large window - and grew slow with the lanterns and fires of a
|
||||||
|
// long session.)
|
||||||
|
layout(std430, set = 0, binding = 45) readonly buffer Lit { uvec4 lhead; vec4 lent[]; }; // count, then (x, y, radius, strength), (r, g, b, 0) per light
|
||||||
|
layout(std430, set = 0, binding = 57) buffer LitCells { uvec2 litc[]; }; // per cell: the summed light, half floats (r g, b)
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const ivec2 cp = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (cp.x >= pc.gw || cp.y >= pc.gh) return;
|
||||||
|
const vec2 w = vec2(cp + ivec2(pc.view_x, pc.view_y)) + 0.5;
|
||||||
|
vec3 L = vec3(0.0);
|
||||||
|
for (uint i = 0u; i < min(lhead.x, 128u); ++i) {
|
||||||
|
const vec4 a = lent[2u * i], b = lent[2u * i + 1u];
|
||||||
|
const float d = distance(w, a.xy) / a.z;
|
||||||
|
if (d < 1.0) L += b.rgb * (1.0 - d) * (1.0 - d) * (1.0 - d) * 1.1 * a.w;
|
||||||
|
}
|
||||||
|
litc[cp.y * pc.gw + cp.x] = uvec2(packHalf2x16(L.rg), packHalf2x16(vec2(L.b, 0.0)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Native C++ Lua bytecode target. Only self/owner handles are visible.
|
||||||
|
struct NgpValue { uint tag; float n; uint s; };
|
||||||
|
struct NgpTuple { NgpValue v[8]; int count; };
|
||||||
|
FProj ngp_proj;
|
||||||
|
vec3 ngp_owner;
|
||||||
|
float ngp_owner_id;
|
||||||
|
bool ngp_failed, ngp_loaded;
|
||||||
|
uint ngp_line;
|
||||||
|
uvec4 ngp_error;
|
||||||
|
vec3 ngp_surface(vec2 start,vec2 end);
|
||||||
|
|
||||||
|
NgpValue ngp_nil() { return NgpValue(0u,0.0,0u); }
|
||||||
|
NgpValue ngp_num(float n) { return NgpValue(1u,n,0u); }
|
||||||
|
NgpValue ngp_bool(bool b) { return NgpValue(2u,b?1.0:0.0,0u); }
|
||||||
|
NgpValue ngp_string(uint s) { return NgpValue(3u,0.0,s); }
|
||||||
|
NgpTuple ngp_empty() { NgpTuple t;t.count=0;for(int i=0;i<8;i++)t.v[i]=ngp_nil();return t; }
|
||||||
|
NgpTuple ngp_one(NgpValue v) { NgpTuple t=ngp_empty();t.count=1;t.v[0]=v;return t; }
|
||||||
|
void ngp_fault(uint code) {if(!ngp_failed)ngp_error=uvec4(code,ngp_line,1u,0u);ngp_failed=true;}
|
||||||
|
bool ngp_truth(NgpValue v) {return v.tag!=0u&&!(v.tag==2u&&v.n==0.0);}
|
||||||
|
bool ngp_equal(NgpValue a,NgpValue b) {return a.tag==b.tag&&(a.tag==0u||(a.tag>=3u?a.s==b.s:a.n==b.n));}
|
||||||
|
float ngp_number(NgpValue v) {
|
||||||
|
float n;if(v.tag==1u)return v.n;
|
||||||
|
if(v.tag==3u&&ngp_parse_number(v.s,n))return n;
|
||||||
|
ngp_fault(1u);return 0.0;
|
||||||
|
}
|
||||||
|
bool ngp_less(NgpValue a,NgpValue b,bool equal) {
|
||||||
|
if(a.tag==3u&&b.tag==3u)return equal?ngp_rank(a.s)<=ngp_rank(b.s):ngp_rank(a.s)<ngp_rank(b.s);
|
||||||
|
if(a.tag!=1u||b.tag!=1u){ngp_fault(1u);return false;}
|
||||||
|
return equal?a.n<=b.n:a.n<b.n;
|
||||||
|
}
|
||||||
|
float ngp_pow(float a,float b) {
|
||||||
|
if(b==0.0)return 1.0;
|
||||||
|
if(a<0.0){if(floor(b)!=b)return uintBitsToFloat(0x7fc00000u);return (mod(abs(b),2.0)==0.0?1.0:-1.0)*pow(-a,b);}
|
||||||
|
return pow(a,b);
|
||||||
|
}
|
||||||
|
NgpValue ngp_arithmetic(int op,NgpValue a,NgpValue b) {
|
||||||
|
float x=ngp_number(a),y=ngp_number(b);
|
||||||
|
if(op==0)return ngp_num(x+y);if(op==1)return ngp_num(x-y);
|
||||||
|
if(op==2)return ngp_num(x*y);if(op==3)return ngp_num(x/y);
|
||||||
|
if(op==4)return ngp_num(x-floor(x/y)*y);return ngp_num(ngp_pow(x,y));
|
||||||
|
}
|
||||||
|
NgpTuple ngp_call(NgpValue f,NgpTuple a) {
|
||||||
|
if(f.tag!=4u){ngp_fault(1u);return ngp_empty();}
|
||||||
|
uint api=f.s;
|
||||||
|
if(api==1u){if(a.count!=0)ngp_fault(2u);return ngp_one(ngp_num(16777216.0));}
|
||||||
|
if(api==2u){
|
||||||
|
if(a.count!=1){ngp_fault(2u);return ngp_empty();}
|
||||||
|
float e=ngp_number(a.v[0]);
|
||||||
|
if(e!=16777216.0&&e!=ngp_owner_id){ngp_fault(3u);return ngp_empty();}
|
||||||
|
vec3 tr=e==16777216.0?vec3(ngp_proj.a.xy,ngp_proj.e.x):ngp_owner;
|
||||||
|
NgpTuple r=ngp_empty();r.count=5;
|
||||||
|
r.v[0]=ngp_num(tr.x);r.v[1]=ngp_num(tr.y);r.v[2]=ngp_num(tr.z);r.v[3]=ngp_num(1.0);r.v[4]=ngp_num(1.0);return r;
|
||||||
|
}
|
||||||
|
if(api==3u){
|
||||||
|
if(a.count!=2){ngp_fault(2u);return ngp_empty();}
|
||||||
|
if(ngp_number(a.v[0])!=16777216.0||a.v[1].tag!=3u){ngp_fault(3u);return ngp_empty();}
|
||||||
|
if(a.v[1].s==NGP_PROJECTILE)return ngp_one(ngp_num(1.0));
|
||||||
|
if(a.v[1].s==NGP_VELOCITY)return ngp_one(ngp_num(2.0));
|
||||||
|
ngp_fault(3u);return ngp_empty();
|
||||||
|
}
|
||||||
|
if(api==4u){
|
||||||
|
if(a.count!=2){ngp_fault(2u);return ngp_empty();}
|
||||||
|
if(ngp_number(a.v[0])==2.0&&a.v[1].tag==3u&&a.v[1].s==NGP_MOTION){
|
||||||
|
NgpTuple r=ngp_empty();r.count=2;
|
||||||
|
r.v[0]=ngp_num(ngp_proj.a.z*60.0);r.v[1]=ngp_num(ngp_proj.a.w*60.0);return r;
|
||||||
|
}
|
||||||
|
if(ngp_number(a.v[0])!=1.0||a.v[1].tag!=3u||a.v[1].s!=NGP_WHO){ngp_fault(3u);return ngp_empty();}
|
||||||
|
return ngp_one(ngp_num(ngp_owner_id));
|
||||||
|
}
|
||||||
|
if(api==5u){
|
||||||
|
if(a.count==4&&ngp_number(a.v[0])==2.0&&a.v[1].tag==3u&&a.v[1].s==NGP_MOTION){
|
||||||
|
vec2 velocity=vec2(ngp_number(a.v[2]),ngp_number(a.v[3]));
|
||||||
|
if(!ngp_failed)ngp_proj.a.zw=velocity/60.0;return ngp_empty();
|
||||||
|
}
|
||||||
|
if(a.count!=3){ngp_fault(2u);return ngp_empty();}
|
||||||
|
if(ngp_number(a.v[0])!=2.0||a.v[1].tag!=3u||a.v[1].s!=NGP_GRAVITY){ngp_fault(3u);return ngp_empty();}
|
||||||
|
float gravity=ngp_number(a.v[2]);if(!ngp_failed)ngp_proj.b.z=gravity/3600.0;
|
||||||
|
return ngp_empty();
|
||||||
|
}
|
||||||
|
if(api==6u||api==7u){
|
||||||
|
if(a.count!=1||a.v[0].tag!=3u||a.v[0].s!=NGP_UTIL){ngp_fault(3u);return ngp_empty();}
|
||||||
|
if(api==7u||!ngp_loaded){ngp_module_init();ngp_loaded=true;}return ngp_empty();
|
||||||
|
}
|
||||||
|
if(api==8u){
|
||||||
|
if(a.count!=4){ngp_fault(2u);return ngp_empty();}
|
||||||
|
vec2 start=vec2(ngp_number(a.v[0]),ngp_number(a.v[1]));
|
||||||
|
vec2 end=vec2(ngp_number(a.v[2]),ngp_number(a.v[3]));
|
||||||
|
if(ngp_failed)return ngp_empty();
|
||||||
|
vec3 hit=ngp_surface(start,end);
|
||||||
|
if(hit.z<0.0){ngp_fault(15u);return ngp_empty();}
|
||||||
|
NgpTuple r=ngp_empty();r.count=3;
|
||||||
|
r.v[0]=ngp_bool(hit.z!=0.0);r.v[1]=ngp_num(hit.x);r.v[2]=ngp_num(hit.y);return r;
|
||||||
|
}
|
||||||
|
if(api==9u){
|
||||||
|
// Reviewed cloud script sets only position, retaining angle and scale.
|
||||||
|
if(a.count!=3){ngp_fault(2u);return ngp_empty();}
|
||||||
|
if(ngp_number(a.v[0])!=16777216.0){ngp_fault(3u);return ngp_empty();}
|
||||||
|
vec2 position=vec2(ngp_number(a.v[1]),ngp_number(a.v[2]));
|
||||||
|
if(!ngp_failed)ngp_proj.a.xy=position;
|
||||||
|
return ngp_empty();
|
||||||
|
}
|
||||||
|
if(api>=10u&&api<=15u){
|
||||||
|
int count=(api==12u||api==15u)?2:1;
|
||||||
|
if(a.count!=count){ngp_fault(2u);return ngp_empty();}
|
||||||
|
float x=ngp_number(a.v[0]), y=count==2?ngp_number(a.v[1]):0.0, value=0.0;
|
||||||
|
if(api==10u)value=abs(x);
|
||||||
|
if(api==11u)value=sqrt(x);
|
||||||
|
// GLSL atan(0,0) is undefined; LuaJIT/libm returns signed zero or pi.
|
||||||
|
if(api==12u){
|
||||||
|
if(x==0.0)value=(floatBitsToUint(y)&0x80000000u)!=0u?3.141592653589793:0.0;
|
||||||
|
else if(y==0.0)value=1.5707963267948966;
|
||||||
|
else value=atan(abs(x),y);
|
||||||
|
if((floatBitsToUint(x)&0x80000000u)!=0u)value=-value;
|
||||||
|
}
|
||||||
|
if(api==13u)value=cos(x);
|
||||||
|
if(api==14u)value=sin(x);
|
||||||
|
if(api==15u)value=max(x,y);
|
||||||
|
return ngp_one(ngp_num(value));
|
||||||
|
}
|
||||||
|
ngp_fault(3u);return ngp_empty();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Same half-cell samples and last-free result as the engine's CPU raytrace.
|
||||||
|
// ngp_solid_at supplies terrain only; rigid bodies are outside this API.
|
||||||
|
bool ngp_solid_at(vec2 position);
|
||||||
|
vec3 ngp_surface(vec2 start, vec2 end) {
|
||||||
|
float distance=length(end-start);
|
||||||
|
if(isnan(distance)||isinf(distance)||distance>512.0) return vec3(start, -1.0);
|
||||||
|
int steps=max(1,int(ceil(distance*2.0)));
|
||||||
|
vec2 previous=start;
|
||||||
|
for(int k=1;k<=steps;++k) {
|
||||||
|
vec2 point=start+(end-start)*float(k)/float(steps);
|
||||||
|
if(ngp_solid_at(point))return vec3(previous,1.0);
|
||||||
|
previous=point;
|
||||||
|
}
|
||||||
|
return vec3(end,0.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh || pc.brush_mat < 0) return;
|
||||||
|
ivec2 d = p - ivec2(pc.brush_x, pc.brush_y);
|
||||||
|
if (d.x * d.x + d.y * d.y > pc.brush_r * pc.brush_r) return;
|
||||||
|
uint h = hash3(uint(p.x), uint(p.y), uint(pc.frame + pc.seed) + uint(pc.pass_) * 131u);
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
if (pc.brush_mat == 0) { cells[i] = 0u; ext[i] = vec2(0.0); return; }
|
||||||
|
// only place into empty space, and thin out fluids so they pour
|
||||||
|
if (cell_mat(cells[i]) != 0u) return;
|
||||||
|
if ((mflags(uint(pc.brush_mat)) & F_STATIC) == 0u && (h & 3u) != 0u) return;
|
||||||
|
// bits 16-23: colour variation, bit 26: initial horizontal direction of gases
|
||||||
|
cells[i] = uint(pc.brush_mat) | (((h >> 8) & 0xFFu) << 16) | (((h >> 20) & 1u) << 26);
|
||||||
|
ext[i] = vec2(0.0);
|
||||||
|
birth[i] = birth_frame();
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
|
||||||
|
// Particles requested by the host (effects of the rigid bodies: the splash of a body that hits something, ...): count, then per request (position, velocity), (life, gravity),
|
||||||
|
// (colour bits, mode, material). Mode 0: a cosmetic droplet that ends at the first cell it meets, stains it and leaves a cell of `material`, mode 2: the same without the cell (stains only), mode 1: a real particle of the material.
|
||||||
|
struct PartReq { vec4 a; vec4 b; uvec4 c; };
|
||||||
|
layout(std430, set = 0, binding = 51) buffer PartReqBuf { uvec4 rhead; PartReq reqs[]; };
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const uint i = gl_GlobalInvocationID.x;
|
||||||
|
if (i >= min(rhead.x, 1024u)) return;
|
||||||
|
const PartReq r = reqs[i];
|
||||||
|
if (r.c.y == 0u) emit_particle_hit(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z));
|
||||||
|
else if (r.c.y == 2u) { const uint ps = emit_particle_hit(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z)); if (ps != 0xFFFFFFFFu) fparts[ps].c.w = 1u; } // stains only, leaves no cell
|
||||||
|
else if (r.c.y == 3u) emit_particle(r.a.xy, r.a.zw, r.b.x, 0.0, r.c.x, true, true); // a light ray (SpriteParticleEmitterComponent, additive): fades, no cell
|
||||||
|
else if (r.c.y == 4u) { if ((r.c.w & 2u) == 0u || cell_free_at(r.a.xy)) {
|
||||||
|
// c.w bit 2: a glowing material (gfx_glow): additive, the colour scaled by the material's alpha; else bits 8-15 = 255 - alpha, the opacity
|
||||||
|
const bool glow = (r.c.w & 4u) != 0u;
|
||||||
|
const float al = 1.0 - float((r.c.w >> 8) & 0xFFu) / 255.0;
|
||||||
|
const uint rgb = glow ? ((uint(float((r.c.x >> 16) & 0xFFu) * al + 0.5) << 16) | (uint(float((r.c.x >> 8) & 0xFFu) * al + 0.5) << 8) | uint(float(r.c.x & 0xFFu) * al + 0.5)) : r.c.x;
|
||||||
|
const int ps = emit_particle_g(r.a.xy, r.a.zw, r.b.x, r.b.y, rgb, (r.c.w & 8u) == 0u, glow, (r.c.w & 1u) == 0u); // c.w bit 3: fade_based_on_lifetime 0
|
||||||
|
if (ps >= 0 && !glow) fparts[ps].c.z = (r.c.w >> 8) & 0xFFu;
|
||||||
|
if (ps >= 0 && r.b.z > 0.0) fparts[ps].c.w |= (min(uint(r.b.z * 1000.0 + 0.5), 4095u) << 20) | (min(uint(r.b.w * 4096.0 + 0.5), 4095u) << 8); // airflow_force (b.z), airflow_scale (b.w)
|
||||||
|
} } // a cosmetic particle (ParticleEmitterComponent emit_cosmetic_particles): fades, no cell; c.w bit 0: collide_with_grid 0, bit 1: cosmetic_force_create 0
|
||||||
|
else if (r.c.y == 5u) { // create_real_particles: the cell itself, in the empty cell (or over a gas / liquid unless only-if-space: c.w bit 1)
|
||||||
|
const ivec2 q = ivec2(floor(r.a.xy)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||||
|
const int ci = q.y * pc.gw + q.x;
|
||||||
|
const uint nc = r.c.z | (((hash3(uint(ci), uint(pc.frame), 63u) >> 16) & 0xFFu) << 16);
|
||||||
|
const uint old = atomicCompSwap(cells[ci], 0u, nc);
|
||||||
|
if (old == 0u) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
||||||
|
else if ((r.c.w & 2u) == 0u && (mflags(cell_mat(old)) & (F_GAS | F_LIQUID)) != 0u && (mflags(cell_mat(old)) & (F_STATIC | F_SAND)) == 0u && atomicCompSwap(cells[ci], old, nc) == old) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
||||||
|
}
|
||||||
|
else if (r.c.y == 6u) emit_displaced(r.a.xy, r.a.zw, r.c.z | (((hash3(i, uint(pc.frame), 64u) >> 16) & 0xFFu) << 16), 2u, birth_frame()); // GameCreateParticle (007b3120 -> 00716af0): a cell of the material handed to the particle system like a displaced one (AddAsParticle 007168c0): it falls and goes back into the grid, waiting for a free place
|
||||||
|
else emit_material_particle(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z));
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// Shared by terrain.comp and render.comp: the 70-scaled simplex noise, the biome-map topology lookup and the gradient of the procedural biomes (FUN_0087e8d0).
|
||||||
|
// Needs: perm (binding 8), ffloor, ps, atab (areas.glsl) declared before the include.
|
||||||
|
vec2 grad12(uint g) {
|
||||||
|
const vec2 G[12] = vec2[12](vec2(1, 1), vec2(-1, 1), vec2(1, -1), vec2(-1, -1), vec2(1, 0), vec2(-1, 0), vec2(1, 0), vec2(-1, 0), vec2(0, 1), vec2(0, -1), vec2(0, 1), vec2(0, -1));
|
||||||
|
return G[g];
|
||||||
|
}
|
||||||
|
float simplex70(float x, float y) {
|
||||||
|
const float F2 = 0.36602540378, G2 = 0.21132486540;
|
||||||
|
float s = (x + y) * F2;
|
||||||
|
int i = ffloor(x + s), j = ffloor(y + s);
|
||||||
|
float t = float(i + j) * G2;
|
||||||
|
float x0 = x - (float(i) - t), y0 = y - (float(j) - t);
|
||||||
|
int i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1;
|
||||||
|
float x1 = x0 - float(i1) + G2, y1 = y0 - float(j1) + G2;
|
||||||
|
float x2 = x0 - 1.0 + 2.0 * G2, y2 = y0 - 1.0 + 2.0 * G2;
|
||||||
|
uint ii = uint(i) & 255u, jj = uint(j) & 255u;
|
||||||
|
float n = 0.0;
|
||||||
|
float t0 = 0.5 - x0 * x0 - y0 * y0;
|
||||||
|
if (t0 >= 0.0) { t0 *= t0; n += t0 * t0 * dot(grad12(ps(ii + ps(jj)) % 12u), vec2(x0, y0)); }
|
||||||
|
float t1 = 0.5 - x1 * x1 - y1 * y1;
|
||||||
|
if (t1 >= 0.0) { t1 *= t1; n += t1 * t1 * dot(grad12(ps(ii + uint(i1) + ps(jj + uint(j1))) % 12u), vec2(x1, y1)); }
|
||||||
|
float t2 = 0.5 - x2 * x2 - y2 * y2;
|
||||||
|
if (t2 >= 0.0) { t2 *= t2; n += t2 * t2 * dot(grad12(ps(ii + 1u + ps(jj + 1u)) % 12u), vec2(x2, y2)); }
|
||||||
|
return 70.0 * n;
|
||||||
|
}
|
||||||
|
float fbm70(float x, float y) {
|
||||||
|
const vec4 M = vec4(0.84147, 0.5403, 0.5403, -0.84147);
|
||||||
|
const float K[3] = float[3](2.02, 2.33, 2.01);
|
||||||
|
float sum = simplex70(x, y) * 0.5;
|
||||||
|
float w = 0.25;
|
||||||
|
for (int o = 0; o < 3; ++o) {
|
||||||
|
float nx = (M.x * x + M.z * y) * K[o], ny = (M.y * x + M.w * y) * K[o];
|
||||||
|
x = nx; y = ny;
|
||||||
|
sum += simplex70(x, y) * w;
|
||||||
|
w *= 0.5;
|
||||||
|
}
|
||||||
|
return sum / 0.9375;
|
||||||
|
}
|
||||||
|
// The topology record (offset in atab) of a biome-map tile, 0xFFFFFFFF when there is none (FUN_0087d870: x wraps, y is clamped).
|
||||||
|
uint tile_rec(int tx, int ty) {
|
||||||
|
int W = int(atab[2]), H = int(atab[3]);
|
||||||
|
tx = ((tx % W) + W) % W;
|
||||||
|
ty = clamp(ty, 0, H - 1);
|
||||||
|
return atab[atab[4] + uint(ty * W + tx)];
|
||||||
|
}
|
||||||
|
// word 20 of the record: biome edge flags (bit 0 noise_biome_edges, bit 1 big_noise_biome_edges, bit 2 fat_biome_edges)
|
||||||
|
bool bedge(uint rec, uint bit) { return (atab[rec + 20u] & bit) != 0u; }
|
||||||
|
// FUN_0087d9a0: the biome-map tile whose biome a world cell belongs to. Within 42 cells of the edge of a tile whose neighbour is another biome, the lookup point is
|
||||||
|
// displaced by noise (wavy biome borders): small (2.5 cells) unless both biomes have big_noise_biome_edges (default on): 11 * simplex + 30 * sin / cos.
|
||||||
|
ivec2 border_tile(ivec2 w) {
|
||||||
|
const int ix = w.x + 17920, iy = w.y + 7168;
|
||||||
|
const int fx = ix & 511, fy = iy & 511;
|
||||||
|
const int W = int(atab[2]), H = int(atab[3]);
|
||||||
|
const int tx = ix >> 9, ty = clamp(iy >> 9, 0, H - 1);
|
||||||
|
const ivec2 here = ivec2(tx, ty);
|
||||||
|
const uint b = tile_rec(tx, ty);
|
||||||
|
if (b == 0xFFFFFFFFu || !bedge(b, 1u) || bedge(b, 4u)) return here;
|
||||||
|
ivec2 n = ivec2(-1000000);
|
||||||
|
const bool l = fx <= 41, r = fx >= 471, t = fy <= 41, bt = fy >= 471;
|
||||||
|
if (l && tile_rec(tx - 1, ty) != b) n = ivec2(tx - 1, ty);
|
||||||
|
else if (t && tile_rec(tx, ty - 1) != b) n = ivec2(tx, ty - 1);
|
||||||
|
else if (r && tile_rec(tx + 1, ty) != b) n = ivec2(tx + 1, ty);
|
||||||
|
else if (bt && tile_rec(tx, ty + 1) != b) n = ivec2(tx, ty + 1);
|
||||||
|
else if (l && t && tile_rec(tx - 1, ty - 1) != b) n = ivec2(tx - 1, ty - 1);
|
||||||
|
else if (l && bt && tile_rec(tx - 1, ty + 1) != b) n = ivec2(tx - 1, ty + 1);
|
||||||
|
else if (r && t && tile_rec(tx + 1, ty - 1) != b) n = ivec2(tx + 1, ty - 1);
|
||||||
|
else if (r && bt && tile_rec(tx + 1, ty + 1) != b) n = ivec2(tx + 1, ty + 1);
|
||||||
|
if (n.x == -1000000) return here;
|
||||||
|
const uint nb = tile_rec(n.x, n.y);
|
||||||
|
if (nb == 0xFFFFFFFFu || !bedge(nb, 1u)) return here;
|
||||||
|
const float xf = float(ix), yf = float(iy);
|
||||||
|
float xx, yy;
|
||||||
|
if (!bedge(b, 2u) || !bedge(nb, 2u)) {
|
||||||
|
const float d = simplex70(xf * 0.05, yf * 0.05) * 2.5;
|
||||||
|
xx = xf + d; yy = yf + d;
|
||||||
|
} else {
|
||||||
|
const float n11 = simplex70(xf * 0.05, yf * 0.05) * 11.0;
|
||||||
|
xx = xf + n11 + 30.0 * sin(yf * 0.005);
|
||||||
|
yy = yf + n11 + 30.0 * cos(xf * 0.005);
|
||||||
|
}
|
||||||
|
const ivec2 t2 = ivec2(int(xx) >> 9, int(yy) >> 9);
|
||||||
|
const uint r2 = tile_rec(t2.x, t2.y);
|
||||||
|
// the displaced tile must have its area loaded: otherwise the cell would be placeholder rock (a column of rock stood in the sky at the edge of the loaded region)
|
||||||
|
if (r2 != 0xFFFFFFFFu && bedge(r2, 1u) && t2.x >= 0 && t2.y >= 0 && t2.x < W && t2.y < H && atab[atab[1] + uint(t2.y) * atab[2] + uint(t2.x)] != 0u) return t2;
|
||||||
|
return here;
|
||||||
|
}
|
||||||
|
uint topo_rec(ivec2 w) { // the topology record of the biome the world cell belongs to
|
||||||
|
const ivec2 t = border_tile(w);
|
||||||
|
return tile_rec(t.x, t.y);
|
||||||
|
}
|
||||||
|
int area_of_cell(ivec2 w) { // the loaded area of that biome, -1 when none
|
||||||
|
const ivec2 t = border_tile(w);
|
||||||
|
if (t.x < 0 || t.y < 0 || t.x >= int(atab[2]) || t.y >= int(atab[3])) return -1;
|
||||||
|
return int(atab[atab[1] + uint(t.y) * atab[2] + uint(t.x)]) - 1;
|
||||||
|
}
|
||||||
|
float tf(uint rec, int k) { return uintBitsToFloat(atab[rec + uint(k)]); }
|
||||||
|
vec2 grad_limits(uint rec, float X) { // FUN_0087eaf0: the start and end of the gradient at X
|
||||||
|
float sY = tf(rec, 11), eY = tf(rec, 12);
|
||||||
|
int mode = int((atab[rec] >> 16) & 15u);
|
||||||
|
if (mode == 0) return vec2(sY, eY);
|
||||||
|
if (mode == 3) { float d = (X - tf(rec, 13)) * tf(rec, 14); return vec2(sY + d, eY + d); }
|
||||||
|
float xp = uintBitsToFloat(atab[6]) + X, sc = tf(rec, 15); // the noise offset of the world (header word 6)
|
||||||
|
float s = (mode == 2 ? fbm70(xp * sc, sY * sc) : float(simplex70(xp * sc, sY * sc))) * tf(rec, 16) + sY;
|
||||||
|
float e = float(simplex70(xp * sc, eY * sc)) * tf(rec, 17) + eY;
|
||||||
|
return vec2(s, e);
|
||||||
|
}
|
||||||
|
float proc_gradient(ivec2 w, uint rec) { // FUN_0087e8d0: 0 above the start, 1 below the end, linear between; blended with the left neighbour within 41 cells of the tile edge
|
||||||
|
float X = float(w.x), Y = float(w.y);
|
||||||
|
vec2 L = grad_limits(rec, X);
|
||||||
|
int fx = (w.x + 17920) & 511;
|
||||||
|
if (Y <= 380.0 && fx <= 41) {
|
||||||
|
ivec2 wl = ivec2(w.x - fx - 1, w.y);
|
||||||
|
uint recL = topo_rec(wl);
|
||||||
|
if (recL != 0xFFFFFFFFu && recL != rec) L = mix(L, grad_limits(recL, X), 1.0 - float(fx) / 41.0);
|
||||||
|
}
|
||||||
|
if (L.x >= Y) return 0.0;
|
||||||
|
if (Y >= L.y || L.y == L.x) return 1.0;
|
||||||
|
return (Y - L.x) / (L.y - L.x);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
#include "generated/proj_lua_generated.glsl"
|
||||||
|
|
||||||
|
// Serial prevents a recycled projectile slot from executing a previous task.
|
||||||
|
// x=cast serial, y=scheduled frame, z=1 pending / 2 complete, w=fault code.
|
||||||
|
layout(std430, set = 0, binding = 64) buffer LuaTasks { uvec4 luaTasks[]; };
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_GlobalInvocationID.x;
|
||||||
|
if (id >= uint(FP_MAX)) return;
|
||||||
|
uvec4 task = luaTasks[id];
|
||||||
|
if (task.z != 1u) return;
|
||||||
|
luaTasks[id].z = 2u;
|
||||||
|
FProj p = fprojs[id];
|
||||||
|
if (task.y != uint(pc.frame) || task.x != uint(p.d.w) || p.d.w == 0 || (p.d.z & (1 << 29)) != 0) { luaTasks[id].w = 13u; return; }
|
||||||
|
int owner = int(p.e.z) - 1;
|
||||||
|
if (owner < 0 || owner >= ctrl.inp.w) { luaTasks[id].w = 3u; return; }
|
||||||
|
uvec4 fault = ngp_execute(p, vec3(ents[owner].a.xy, 0.0), float(owner + 1));
|
||||||
|
luaTasks[id].w = fault.x;
|
||||||
|
// Bindings write only this projectile. No state races between invocations.
|
||||||
|
fprojs[id] = p;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Shared by the runtime-compiled program and the offline reference target.
|
||||||
|
layout(std430,binding=64) buffer LuaTasks { uvec4 luaTasks[]; };
|
||||||
|
void main() {
|
||||||
|
uint id=gl_GlobalInvocationID.x;
|
||||||
|
if(id>=32768u)return;
|
||||||
|
FProj p=fprojs[id];
|
||||||
|
uvec4 task=luaTasks[id];
|
||||||
|
// Gather all additive fields into this target invocation. No invocation
|
||||||
|
// writes another projectile, so overlapping fields cannot race or lose force.
|
||||||
|
if(p.d.w!=0&&(p.d.z&(1<<29))==0&&p.d.x>=0&&p.d.x<512) {
|
||||||
|
uint flags=ft[p.d.x*18+11].y;
|
||||||
|
if((flags&(3u<<29))==(3u<<29)) {
|
||||||
|
uint count=luaTasks[32768].y==uint(pc.frame)?min(luaTasks[32768].x,512u):0u;
|
||||||
|
for(uint field=0u;field<count;++field) {
|
||||||
|
uvec4 data=luaTasks[32769u+field*2u];
|
||||||
|
uvec4 source=luaTasks[32770u+field*2u];
|
||||||
|
if((source.z&1u)!=0u&&source.x==id+1u&&source.y==uint(p.d.w))continue;
|
||||||
|
FProj candidate=p;
|
||||||
|
uvec4 fault=ngp_neighbor_dispatch(data.z,candidate,vec3(uintBitsToFloat(data.xy),0.0));
|
||||||
|
if(fault.x==0u)p=candidate;
|
||||||
|
else {luaTasks[id]=uvec4(uint(p.d.w),uint(pc.frame),2u,fault.x);}
|
||||||
|
}
|
||||||
|
if(count!=0u)fprojs[id]=p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(p.d.w!=0 && p.d.x>=0 && p.d.x<512) {
|
||||||
|
const uint flags=ft[p.d.x*18+11].y;
|
||||||
|
const uint period=(flags>>21)&255u;
|
||||||
|
// bit 30 transfers this instance to a CPU query's materialized twin.
|
||||||
|
if((flags&(1u<<20))!=0u && (p.d.z&(1<<30))==0 && period!=0u) {
|
||||||
|
if((uint(pc.frame)+uint(p.d.w))%period!=0u)return;
|
||||||
|
if(p.a.x<float(pc.view_x-128)||p.a.y<float(pc.view_y-128)||p.a.x>float(pc.view_x+pc.gw+128)||p.a.y>float(pc.view_y+pc.gh+128))return;
|
||||||
|
if(task.x==uint(p.d.w)&&task.y==uint(pc.frame)&&task.z==2u)return;
|
||||||
|
task=uvec4(uint(p.d.w),uint(pc.frame),257u,0u); // program 1, pending
|
||||||
|
luaTasks[id]=task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if((task.z&255u)!=1u)return;
|
||||||
|
uint program=task.z>>8;
|
||||||
|
luaTasks[id].z=2u;
|
||||||
|
if(task.y!=uint(pc.frame)||task.x!=uint(p.d.w)||p.d.w==0||(p.d.z&(1<<29))!=0){luaTasks[id].w=13u;return;}
|
||||||
|
int owner=int(p.e.z)-1;
|
||||||
|
bool known=owner>=0&&owner<ctrl.inp.w;
|
||||||
|
if(known)known=(ents[owner].st.w&8)==0;
|
||||||
|
// The original chunk does nothing when mWhoShot is NULL_ENTITY.
|
||||||
|
vec3 transform=known?vec3(ents[owner].a.xy,0.0):vec3(0.0);
|
||||||
|
uvec4 fault=ngp_dispatch(program,p,transform,known?float(owner+1):0.0);
|
||||||
|
luaTasks[id].w=fault.x;
|
||||||
|
fprojs[id]=p;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
|
||||||
|
// Consumes the cast requests of this frame (written by the host for a trigger pull, or by proj_step for the sub-shots of triggers):
|
||||||
|
// a request names a shot node; every spec of the node becomes a projectile. A negative node is a visual-only explosion (exploding props).
|
||||||
|
shared int reqCount;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int list = pc.frame & 1;
|
||||||
|
if (gl_LocalInvocationID.x == 0u) reqCount = min(list == 0 ? hhead.x : hhead.y, 256);
|
||||||
|
barrier();
|
||||||
|
for (int r = int(gl_LocalInvocationID.x); r < reqCount; r += 64) {
|
||||||
|
vec4 r0 = hcast[(list * 256 + r) * 2], r1 = hcast[(list * 256 + r) * 2 + 1];
|
||||||
|
int node = int(r1.x);
|
||||||
|
uint seed = uint(r1.z) * 2654435761u + uint(r) * 40503u + uint(pc.frame);
|
||||||
|
if (node < 0) { boom_fx(-node - 1, r0.xy, seed, -1.0); continue; }
|
||||||
|
vec4 n0 = hnodes[node * 4], n1 = hnodes[node * 4 + 1], n2 = hnodes[node * 4 + 2], n3 = hnodes[node * 4 + 3];
|
||||||
|
int first = int(n0.x), count = int(n0.y);
|
||||||
|
float base = atan(r0.w, r0.z);
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
ivec4 s0 = hspecs[(first + i) * 4];
|
||||||
|
int type = s0.x;
|
||||||
|
int tb = type * TYPE_STRIDE;
|
||||||
|
vec4 t0 = ftf(tb), t1 = ftf(tb + 1);
|
||||||
|
ivec4 t3 = fti(tb + 3);
|
||||||
|
uvec4 hs = uvec4(hash3(seed, uint(i), 1u), hash3(seed, uint(i), 2u), hash3(seed, uint(i), 3u), hash3(seed, uint(i), 4u));
|
||||||
|
const bool done = ((int(r1.y) >> 30) & 1) != 0; // the host has run the script_shot scripts on this shot: its velocity is final
|
||||||
|
const bool exact = (int(r1.y) & 4) != 0 && (fti(tb + 1).w < 0 || done); // a particle entity the host created (its velocity was drawn by SetStartVelocityComponent on the CPU): exactly that velocity
|
||||||
|
float ang = base + (rnd01(hs.x) - 0.5) * n1.x + (exact ? 0.0 : (rnd01(hs.y) - 0.5) * 2.0 * t1.z);
|
||||||
|
if (count > 1 && n1.y != 0.0) ang += -n1.y + 2.0 * n1.y * float(i) / float(n1.y < 3.1241393 ? count - 1 : count); // pattern_degrees (00c0b440): the aim of the first root projectile is turned by -pattern, then a step of 2 pattern / (N - 1, or N when the pattern is 179 degrees or more) per projectile
|
||||||
|
float speed = (t0.x + (t0.y - t0.x) * rnd01(hs.z)) * n1.z;
|
||||||
|
if ((int(r1.y) & 4) != 0) speed = length(r0.zw); // a script's shot: its own velocity
|
||||||
|
if ((fti(tb + 16).x & 256) != 0) speed = 5400.0 / 60.0; // projectile_type LIGHTNING (00c0d290 case 1): the aim direction x 5400 px/s, whatever the speed range
|
||||||
|
const int shooterSlot = int(r1.w) >> 1;
|
||||||
|
if (shooterSlot > 0 && !done && count == 1 && s0.y == 0 && fti(tb + 1).w >= 0 && (ents[shooterSlot - 1].ex8.x & 256) != 0 && ents[shooterSlot - 1].st.x != 3) {
|
||||||
|
// Message_Shot (00c0d290): the shooter's script_shot scripts see the projectile first; the host makes the runtime copy, lets them change it and shoots it again (event kind -3: two entries)
|
||||||
|
uint xi = uint(atomicAdd(xhead.x, 2));
|
||||||
|
if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -3.0, float(1 + type)); xe[xi + 1u] = vec4(ang, speed, base, float(shooterSlot + ((int(r1.y) & 3) << 20))); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((fti(tb + 16).x & 512) != 0) { // projectile_type MATERIAL_PARTICLE (00c0d290 case 3): on_death_emit_particle_count real particles of on_death_emit_particle_type with the launch
|
||||||
|
// velocity and the VelocityComponent gravity, and no projectile (it is removed at once)
|
||||||
|
const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64);
|
||||||
|
for (int k = 0; k < dn && dmat >= 0; ++k) emit_material_particle(r0.xy, vec2(cos(ang), sin(ang)) * speed, max(t0.w, 60.0), t1.x, mats[3u * uint(dmat)].x, dmat);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint serial = atomicAdd(fcnt[0], 1u); // the cast serial identifies the projectile beyond its ring slot
|
||||||
|
uint slot = serial % uint(FP_MAX);
|
||||||
|
for (uint k = 0u; k < 64u && fprojs[slot].d.w != 0; ++k) slot = (serial + k + 1u) % uint(FP_MAX); // (a live record is not overwritten: the next free one of 64, else the ring's)
|
||||||
|
if (fprojs[slot].d.w != 0) slot = serial % uint(FP_MAX);
|
||||||
|
FProj p;
|
||||||
|
p.a = vec4(r0.xy, cos(ang) * speed, sin(ang) * speed);
|
||||||
|
{ // lifetime: -1 never runs out (the timer of 00c06fc0 skips it), lifetime_randomness: + random(-r, r) (ProjectileComponent +50)
|
||||||
|
const int lr = fti(tb + 17).z;
|
||||||
|
const float lrand = lr > 0 ? float(int(hash3(seed, uint(i), 91u) % uint(2 * lr + 1)) - lr) : 0.0;
|
||||||
|
p.b = vec4(t0.z + n2.x, t0.w < 0.0 ? 1e9 : max(1.0, t0.w + n1.w + lrand), t1.x + n2.y, t1.y);
|
||||||
|
}
|
||||||
|
p.c = vec4(ftf(tb + 2).x + n2.w, ftf(tb + 2).y + n3.x, 0.0, n2.z);
|
||||||
|
p.e = vec4(rnd01(hs.w) * 6.2832, (fti(tb + 1).w & 32) != 0 ? (rnd01(hs.z) - 0.5) * 0.5 : speed, float(int(r1.w) >> 1), float((int(r1.y) >> 3) & 0xFFFFFF)); // e.y: the spin of a thrown body, else the speed at the start (mInitialSpeed, damage_scaled_by_speed) // e.z: the slot + 1 of the shooter (0 = unknown), e.w: its herd + 1
|
||||||
|
const int shotBits = int(n3.w); // bit 0 friendly fire, bits 1-12 trail material + 1, bits 13- its amount
|
||||||
|
p.f = vec4(n0.z, n0.w, n3.z, float(shotBits >> 1)); // the wand's electricity / ice adds, knockback add, trail
|
||||||
|
p.c.w += float(ft[tb + 7].y); // bounces_left of the projectile file
|
||||||
|
{ // the perks scale the shot once, here: the projectile damage and (unless not_scaled_by_gamefx) the explosion damage
|
||||||
|
const float dm = dealt_of(int(p.e.z));
|
||||||
|
p.b.x *= dm;
|
||||||
|
if ((fti(tb + 1).w & 32768) == 0) p.c.y *= dm;
|
||||||
|
}
|
||||||
|
p.d = ivec4(type, first + i, ((int(r1.y) & 3) << 8) | ((shotBits & 1) << 10) | (min(int(n3.y) & 8191, 4095) << 12) | (((int(n3.y) >> 13) & 15) << 24) | (n2.z > 0.0 ? (1 << 28) : 0), int(serial + 1u)); // bits 24-27: lightning_count, bit 28: the wand added bounces (00c0b440 then sets bounce_at_any_angle) // team in bits 8-9, the critical chance (percent) from bit 12 // team in bits 8-9 (1 the player's wand, 2 enemies), fired triggers in the low bits
|
||||||
|
fprojs[slot] = p;
|
||||||
|
if ((fti(tb + 11).y & 1) != 0) { // a projectile with components the GPU does not simulate: the host makes its runtime twin (event kind -4, two entries)
|
||||||
|
uint xi = uint(atomicAdd(xhead.x, 2));
|
||||||
|
if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -4.0, float(slot)); xe[xi + 1u] = vec4(float(serial + 1u), float(type), float(shooterSlot), float(int(r1.y) & 3)); }
|
||||||
|
}
|
||||||
|
if ((int(r1.w) & 1) != 0) { // muzzle flash of the first projectiles of a cast
|
||||||
|
ivec4 t6 = fti(tb + 6);
|
||||||
|
float fr = uf(ft[tb + 6].w);
|
||||||
|
if (fr > 0.0 && i == 0) add_anim(r0.xy, -1, min(fr, 40.0) * 0.5, 0.55, 5, uint(t6.z), 0.0);
|
||||||
|
int mb = int(ft[tb + 7].x);
|
||||||
|
if (mb >= 0 && i == 0) { // the muzzle flash sprite, along the shot
|
||||||
|
ivec4 m0 = fti(BOOM_BASE + mb * BOOM_STRIDE), m1 = fti(BOOM_BASE + mb * BOOM_STRIDE + 1);
|
||||||
|
add_anim(r0.xy, mb, 0.0, 0.0, max(m0.w, 1) * max(m1.x, 1) + 2, uint(m1.y), base);
|
||||||
|
}
|
||||||
|
// (the cast flash is the muzzle_flash_file sprite and the light only: no sparks of a colour of ours - the wall pieces of WALL_SQUARE made gold dust)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
if (gl_LocalInvocationID.x == 0u) { if (list == 0) hhead.x = 0; else hhead.y = 0; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,722 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "fx_common.glsl"
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
|
||||||
|
#include "biome_mods.glsl"
|
||||||
|
|
||||||
|
// Flight of the wand projectiles, one invocation per projectile: gravity, air friction, collisions with the world (die or bounce)
|
||||||
|
// and with creatures, triggers (timer, hit, death) that queue the next shot of the wand, explosions (damage, holes, material, push of rigid
|
||||||
|
// bodies, visuals), particle emitters, light and the sprite.
|
||||||
|
const int TF_COLLISION_DIE = 1, TF_EXPLODE = 2, TF_HOLE = 4, TF_ROTATE = 8, TF_ADDITIVE = 16, TF_PHYSICS = 32, TF_DIE_IN_LIQUID = 64, TF_FRIENDLY = 128, TF_NO_PLAYER = 256, TF_HIT_EFFECTS = 512;
|
||||||
|
|
||||||
|
// The entity a projectile touches: the player or a creature / prop. The shooter's herd is spared (ex8.y of an entity holds its herd + 1) unless the projectile has friendly fire.
|
||||||
|
// What the shooter deals: the DAMAGE_MULTIPLIER game effect etc. (ex7.y of creatures and the player, set by the host)
|
||||||
|
// The critical hit of a hit: the chance (percent) of the shot, +100 with the CRITICAL_HIT_BOOST of the type while the hit entity has the condition, + what the shooter's game effects add (ex7.z);
|
||||||
|
// the damage is multiplied by 5 (DAMAGE_CRITICAL_HIT_MULTIPLIER); a chance above 100 % multiplies the multiplier by the extra, the victim's critical_damage_resistance scales it
|
||||||
|
float crit_mult(int chance, int flags, int shooter, int victim, uint seed) {
|
||||||
|
float c = float(chance);
|
||||||
|
const int cond = (flags >> 10) & 7;
|
||||||
|
if (cond != 0 && victim >= 0) { // the HitEffectComponent conditions are on the HIT entity
|
||||||
|
const Ent s = ents[victim];
|
||||||
|
const int z = s.ex8.z;
|
||||||
|
const bool fresh = ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF);
|
||||||
|
bool ok = false;
|
||||||
|
if (cond == 1) ok = s.ex4.w > 0.0;
|
||||||
|
else if (cond == 2) ok = fresh && (z & 4) != 0;
|
||||||
|
else if (cond == 3) ok = fresh && (z & 2) != 0;
|
||||||
|
else if (cond == 4) ok = fresh && (z & 1) != 0;
|
||||||
|
if (ok) c += 100.0;
|
||||||
|
}
|
||||||
|
if (shooter > 0) { const Ent s = ents[shooter - 1]; if (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) c += s.ex7.z; }
|
||||||
|
if (c <= 0.0) return 1.0;
|
||||||
|
// 00ab9a90: rand % 100 + 1 <= chance; m = the packet's multiplier (5), x chance / 100 above 100 %; the victim's critical_damage_resistance r gives (1 - r) (m - 1) + 1, no crit at r >= 1
|
||||||
|
const float r = victim >= 0 ? ents[victim].ex19.x : 0.0;
|
||||||
|
if (r >= 1.0 || float(hash3(seed, 17u, 99u) % 100u) >= c) return 1.0;
|
||||||
|
const float m = c > 100.0 ? 5.0 * c / 100.0 : 5.0;
|
||||||
|
return (1.0 - r) * (m - 1.0) + 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A critical hit (00ab9a90: the multiplier after the resistance above 1): the host loads data/entities/particles/critical_hit.xml at the point of the message and marks the damage number (event kind -9)
|
||||||
|
void note_crit(vec2 at, int victim) {
|
||||||
|
uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(at, -9.0, float(victim));
|
||||||
|
if (victim >= 0) atomicOr(ents[victim].ex3.z, 0x10000); // a critical hit this tick: the victim's hit flash is red (00ab5b00: shader 6 + crit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DAMAGE_BOOST of a hit effect: the type flags hold the stain it needs on the victim (bits 24-26, 1 + the stain bit) and the factor (bits 27-30)
|
||||||
|
float boost_of(int flags, int victim) {
|
||||||
|
const int need = (flags >> 24) & 7, mult = (flags >> 27) & 15;
|
||||||
|
if (need == 0 || mult == 0) return 1.0;
|
||||||
|
const int z = ents[victim].ex8.z;
|
||||||
|
return (((z >> (need - 1)) & 1) != 0 && ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF)) ? float(mult) : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 gAhead; // the next point of the projectile's path (a body's pixel there is a hit on its entity)
|
||||||
|
// 00c08d10: the shooter (mWhoShot) is hit only when collide_with_shooter_frames is not -1 and has run out (bits 16-23 of the flags: 255 = -1, never); friendly_fire
|
||||||
|
// (00c091f0) only lets the shooter's herd be hit. A shot with the friendly_fire modifier (PIERCING_SHOT...) gets friendly_fire 1 and 6 frames (00c0b440): shot_flags.
|
||||||
|
bool shooter_safe(int flags, int age) { const int f = (flags >> 16) & 255; return f == 255 || age < f; }
|
||||||
|
int shot_flags(int typeFlags, int dz) { return ((dz >> 10) & 1) != 0 ? (typeFlags & ~(255 << 16)) | TF_FRIENDLY | (6 << 16) : typeFlags; }
|
||||||
|
bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) {
|
||||||
|
hit = -1;
|
||||||
|
for (int k = 0; k < ctrl.inp.w; ++k) {
|
||||||
|
const int kind = ents[k].st.x;
|
||||||
|
if (kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5) { // a worm's segment (WormComponent hitbox_radius around every part): the hit goes to the worm
|
||||||
|
const int root = int(ents[k].ex19.w) - 1;
|
||||||
|
if (root <= 0 || root >= ctrl.inp.w || !(ents[root].st.x == 1 || ents[root].st.x == 2) || (ents[root].st.w & 8) != 0) continue;
|
||||||
|
if (root == shooter - 1 && shooter_safe(flags, age)) continue;
|
||||||
|
if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[root].ex8.y == herd) continue;
|
||||||
|
if (distance(pos, ents[k].a.xy) <= ents[k].ex19.z + 1.0) { hit = root; return true; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (k == 0 ? (kind != 3 || (flags & TF_NO_PLAYER) != 0) : !(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8))) continue;
|
||||||
|
if ((ents[k].st.w & (8 | 4096)) != 0) continue; // (4096: its sprites, damage model and hitbox are switched off - Olematon away from an Evil Eye)
|
||||||
|
if (k == shooter - 1 && shooter_safe(flags, age)) continue;
|
||||||
|
if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[k].ex8.y == herd && kind != 4) continue; // (a berserk shooter has no herd: its projectiles hurt everybody)
|
||||||
|
if (kind == 4 && ents[k].ex7.w > 0.5) { if (body_slot(gAhead) == k || body_slot(pos) == k) { hit = k; return true; } continue; } // no HitboxComponent: its pixels (00c073b0)
|
||||||
|
const float r = k == 0 ? 1.0 : 2.0;
|
||||||
|
const vec2 d = pos - ents[k].a.xy;
|
||||||
|
if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool creature_at(vec2 pos, float r, out int hit) {
|
||||||
|
hit = -1;
|
||||||
|
for (int k = 1; k < ctrl.inp.w; ++k) {
|
||||||
|
int kind = ents[k].st.x;
|
||||||
|
if (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & 8) != 0) continue;
|
||||||
|
vec2 d = pos - ents[k].a.xy;
|
||||||
|
if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_shot(int node, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) {
|
||||||
|
float len = length(vel);
|
||||||
|
vec2 dir = len < 1e-6 ? vec2(0.0, 1.0) : vel / len;
|
||||||
|
int next = (pc.frame + 1) & 1;
|
||||||
|
int slot = next == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1);
|
||||||
|
if (slot >= 256) return;
|
||||||
|
hcast[(next * 256 + slot) * 2] = vec4(pos, dir); // at the trigger's position itself (00c0b440 puts the new projectile there)
|
||||||
|
hcast[(next * 256 + slot) * 2 + 1] = vec4(float(node), float(team + (herd << 3)), float(seed & 0xFFFFFFu), float(shooter * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The triggers of a projectile (recorded by the natives 00b283e0 / 00b28510 / 00b28600 into three lists of the ProjectileComponent: timers +0x610, hit-world +0x61c, death +0x628;
|
||||||
|
// kinds here 1 timer, 2 hit world, 3 death). mode 1: the update's countdown (00c06c60): a timer whose delay has run out fires and goes; 2: a hit on the world (00c073b0 -> 00c06250):
|
||||||
|
// the hit-world and the waiting timers fire and go; 3: an impact on a creature (00c091f0): the hit-world and the waiting timers fire and stay (a projectile that pierces casts
|
||||||
|
// them again at its next impact); 4: the lifetime runs out (00c06fc0): the waiting timers fire; 5: the projectile is removed (00c0e3e0): the death triggers fire
|
||||||
|
void fire_triggers(int spec, inout int fired, int mode, int age, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) {
|
||||||
|
ivec4 s0 = hspecs[spec * 4];
|
||||||
|
for (int t = 0; t < min(s0.y, 3); ++t) {
|
||||||
|
ivec4 tr = hspecs[spec * 4 + 1 + t]; // kind, delay, node
|
||||||
|
if ((fired & (1 << t)) != 0 || tr.z < 0) continue;
|
||||||
|
// (mode 1: the counter starts at the delay and fires when the decrement reaches 0: a delay of 0 never does)
|
||||||
|
bool go = mode == 1 ? (tr.x == 1 && tr.y > 0 && age == tr.y) : (mode == 2 || mode == 3) ? (tr.x == 1 || tr.x == 2) : mode == 4 ? tr.x == 1 : tr.x == 3;
|
||||||
|
if (!go) continue;
|
||||||
|
vec2 v = vel;
|
||||||
|
if (mode == 5 && dot(vel, vel) * 3600.0 < 100.0) { // 00c0e3e0: nearly at rest (|v| < 10 px/s): a random direction (and length 20..80 px/s) for each death trigger
|
||||||
|
const float ang = rnd01(hash3(seed, uint(t), 131u)) * 6.2831855;
|
||||||
|
v = vec2(cos(ang), sin(ang)) * (rnd01(hash3(seed, uint(t), 132u)) * 60.0 + 20.0) / 60.0;
|
||||||
|
}
|
||||||
|
if (mode != 3) fired |= 1 << t;
|
||||||
|
queue_shot(mode == 2 ? tr.w : tr.z, pos, v, seed + uint(t), team, shooter, herd); // (.w: the payload with its child_speed_multiplier, 00c06250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void explode(vec2 pos, float radius, float damage, int type, int team, int shooter, int herd, int critChance, uint critSeed) {
|
||||||
|
int tb = type * TYPE_STRIDE;
|
||||||
|
vec4 t2 = ftf(tb + 2);
|
||||||
|
ivec4 t1 = fti(tb + 1);
|
||||||
|
int cellMat = int(ft[tb + 2].w);
|
||||||
|
float cellProb = uf(ft[tb + 3].x);
|
||||||
|
if (radius <= 0.0) return;
|
||||||
|
for (int k = 1; k < ctrl.inp.w; ++k) // corpse parts in range bleed
|
||||||
|
if (ents[k].st.x == 4 && ents[k].ex7.y > 0.0 && distance(ents[k].a.xy, pos) <= radius) body_damage(k, pos, 1.0);
|
||||||
|
// 00657960 (docs/EXPLOSION_KNOCKBACK_AND_VINES.md): every entity whose position is within explosion_radius, except the config's dont_damage_this (the shooter only when the projectile has
|
||||||
|
// explosion_dont_damage_shooter), takes the config's damage in full (only with damage_mortals; the perks scaled it at the Shot) and gets a knockback: scalar = radius * knockback_force *
|
||||||
|
// 10 * (0.5 t + 0.5), t = 1 - distance / radius clamped; its direction is the radial physics force (lerp of physics_explosion_power.min / .max by t: zero = no kick); it is compared with
|
||||||
|
// minimum_knockback_force before the division by the CharacterDataComponent mass, the vertical part is always upward, and a killing hit gives none
|
||||||
|
const vec4 t8 = ftf(tb + 8);
|
||||||
|
const bool mortals = t8.w > 0.5;
|
||||||
|
const vec2 pw = unpackHalf2x16(ft[tb + 7].w); // physics_explosion_power.min / .max
|
||||||
|
if (mortals && damage > 0.0) {
|
||||||
|
for (int k = 0; k < ctrl.inp.w; ++k) {
|
||||||
|
int kind = ents[k].st.x;
|
||||||
|
if (k == 0 ? (ents[0].st.w & 8) != 0 : (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & (8 | 4096)) != 0)) continue;
|
||||||
|
if ((t1.w & 16384) != 0 && k == shooter - 1) continue;
|
||||||
|
const vec2 c = ents[k].a.xy;
|
||||||
|
const float dist = distance(c, pos);
|
||||||
|
if (dist > radius) continue;
|
||||||
|
const float ck = crit_mult(critChance, t1.w, shooter, k, critSeed + uint(k) * 7919u);
|
||||||
|
if (ck > 1.0) note_crit(c, k);
|
||||||
|
const float dk = damage * ents[k].ex9.y * ck; // the victim's explosion multiplier (and protections); the damage message carries the projectile's ConfigDamageCritical (00657960 copies chance / multiplier), TakeDamage rolls it per victim
|
||||||
|
note_block(k, damage, dk);
|
||||||
|
atomicAdd(ents[k].x.x, int(dk * 1000.0));
|
||||||
|
note_attacker(k, shooter);
|
||||||
|
if (type >= 0) ents[k].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF));
|
||||||
|
atomicOr(ents[k].ex3.z, 2);
|
||||||
|
const vec2 away = normalize(c - pos + vec2(0.0, -0.01));
|
||||||
|
{ // 00657960: the packet's impulse = the direction x ((power.max - power.min) x (1 - d / R) + power.min) x physics_multiplier_ragdoll_force x 3600 (the blood of the hit
|
||||||
|
// and the ragdoll take it); RAGDOLL_FX of the explosion packet: the constructor's NORMAL
|
||||||
|
const float a = ((pw.y - pw.x) * clamp(1.0 - dist / radius, 0.0, 1.0) + pw.x) * (float((fti(tb + 11).y >> 8) & 4095) / 256.0) * 3600.0;
|
||||||
|
if (k != 0) { // (not the player: its aux.x is the maximum hp - a blast there made the respawn hp 765)
|
||||||
|
atomicExchange(ents[k].aux.x, int(away.x * a * 1000.0)); atomicExchange(ents[k].aux.y, int(away.y * a * 1000.0));
|
||||||
|
atomicAnd(ents[k].ex3.z, ~0xF000); atomicOr(ents[k].ex3.z, 1 << 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float tt = clamp(1.0 - dist / radius, 0.0, 1.0);
|
||||||
|
const float physF = mix(pw.x, pw.y, tt); // (the multiplier +0x108 defaults to 1)
|
||||||
|
const float sc = radius * t8.z * 10.0 * (0.5 * tt + 0.5);
|
||||||
|
if ((kind == 1 || kind == 3) && t8.z != 0.0 && physF != 0.0 && dist > 0.0 && ents[k].hp.x - dk > 0.0 && abs(sc) >= ents[k].ex11.x && (ents[k].ex8.x & 96) == 0 && ents[k].ex8.y > 0) { // (96: KNOCKBACK or KNOCKBACK_IMMUNITY on it; ex8.y > 0: it has a GenomeDataComponent)
|
||||||
|
const float mass = intBitsToFloat(ents[k].rg2.w);
|
||||||
|
vec2 imp = (c - pos) / dist * sc;
|
||||||
|
if (mass != 0.0) imp /= mass;
|
||||||
|
imp.y = -abs(imp.y);
|
||||||
|
ents[k].ex11.yz = imp / 60.0; // the pending kick (pixels per second -> cells per frame); the entity pass adds it, inverts it when confused and starts the knockback state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((t1.w & TF_HOLE) != 0) {
|
||||||
|
if (radius <= 64.0) add_edit(vec4(pos, radius, t2.z), vec4(2.0, ftf(tb + 7).z, cellMat >= 0 ? float(cellMat) : 0.0, cellMat >= 0 ? cellProb * 100.0 : 0.0)); // angular rays (explode_rays.comp), which also create the cells of create_cell_material
|
||||||
|
else add_edit(vec4(pos, radius * 0.5, t2.z), vec4(0.0));
|
||||||
|
}
|
||||||
|
if (((fti(tb + 9).w >> 12) & 255) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -2.0, float(1 + type)); } // config_explosion.load_this_entity: the host spawns the entities (event kind -2)
|
||||||
|
if (cellMat >= 0 && cellProb > 0.0 && ((t1.w & TF_HOLE) == 0 || radius > 64.0)) add_edit(vec4(pos, radius * 0.6, 0.0), vec4(1.0, float(cellMat), cellProb, 0.0));
|
||||||
|
{ // the force on the rigid bodies (00657960): the event is a pair, (x, y, radius in pixels, -1) then (force min, force max)
|
||||||
|
const uint ph = ft[tb + 7].w;
|
||||||
|
const vec2 fr = unpackHalf2x16(ph);
|
||||||
|
uint xp = uint(atomicAdd(xhead.x, 2));
|
||||||
|
if (xp + 1u < 512u) { xe[xp] = vec4(pos, radius, -1.0); xe[xp + 1u] = vec4(fr, float(1 + type), 0.0); } // (z: the type, for the explosion's sound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sprite of a projectile, rotated along the flight when it is not round.
|
||||||
|
// scale: the entity's transform scale (a script's EntitySetTransform on the twin: summon_portal_vibrate.lua pulses the portal 0.1 .. 0.42); the host keeps it as -scale in e.x of
|
||||||
|
// a projectile that is no thrown body (e.x is the tumble angle of those only)
|
||||||
|
void draw_sprite(vec2 pos, vec2 vel, int age, ivec4 t1, ivec4 t5, ivec4 t6, float spin, float scale) {
|
||||||
|
ivec2 sz = ivec2(t5.y, t5.z);
|
||||||
|
scale = max(scale, 0.01);
|
||||||
|
if (sz.x > 0 && sz.y > 0) {
|
||||||
|
int frame = (age / max(t6.x, 1)) % max(t5.w, 1);
|
||||||
|
float ang = (t1.w & TF_PHYSICS) != 0 ? spin : ((t1.w & TF_ROTATE) != 0 ? atan(vel.y, vel.x) : 0.0); // thrown bodies tumble, long sprites follow the flight
|
||||||
|
float c = cos(ang), s = sin(ang);
|
||||||
|
float r = 0.5 * length(vec2(sz)) * scale + 1.0;
|
||||||
|
ivec2 lo = ivec2(floor(pos - vec2(r)));
|
||||||
|
int n = int(2.0 * r) + 2;
|
||||||
|
for (int j = 0; j < n * n; ++j) {
|
||||||
|
ivec2 w = lo + ivec2(j % n, j / n);
|
||||||
|
vec2 d = vec2(w) + 0.5 - pos;
|
||||||
|
ivec2 sp = ivec2(floor(vec2(sz) * 0.5 + vec2(c * d.x + s * d.y, -s * d.x + c * d.y) / scale));
|
||||||
|
if (sp.x < 0 || sp.y < 0 || sp.x >= sz.x || sp.y >= sz.y) continue;
|
||||||
|
ivec2 q = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
uint t = fxtex[t5.x + frame * sz.x * sz.y + sp.y * sz.x + sp.x];
|
||||||
|
if ((t >> 24) != 0u) entlayer[q.y * pc.gw + q.x] = (t1.w & TF_ADDITIVE) != 0 ? (0x01000000u | (t & 0xFFFFFFu)) : t; // alpha 1 marks additive light
|
||||||
|
}
|
||||||
|
} else if ((t1.w & 8192) == 0) { // (an entity with only emitters has no body to draw)
|
||||||
|
ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
ivec2 q = w + ivec2(x, y);
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = 0xFF000000u | uint(t6.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- VERLET projectiles (the tentacles: projectile_type VERLET, a VerletPhysicsComponent chain in a child entity, a VerletWeaponComponent on the root) ----
|
||||||
|
// 00c0d290 (the shot, case 2): every point of the chain starts at the origin with prev = origin - aim x speed (so it moves speed px a frame), the projectile
|
||||||
|
// itself does not move; each frame the chain integrates (00c37d30: cur + (cur - prev) x velocity_dampening, liquid_damping in a liquid; a solid cell
|
||||||
|
// stops the point halfway or lifts it one cell) and relaxes twice (00c38310, stiffness x 0.5), its first point held at the origin (follow_entity_transform;
|
||||||
|
// the weapon system 00c3fdc0 puts the chain on the shooter's hand every frame); the projectile's position is the last point. 00c3fdc0 hurts with every point:
|
||||||
|
// min(damage_coeff x |v|^2, damage_max), nothing under damage_min_step, the boxes of the creatures within damage_radius (not the shooter; the player at most once in
|
||||||
|
// 20 frames, mPlayerCooldownEnd), times (lifetime left) / fade_duration_frames in the last frames. 17 records a slot: the 16 points (position, previous), then
|
||||||
|
// (the anchor: its offset from the shooter or its position, 1 when it follows the shooter, the player's cooldown end).
|
||||||
|
// TODO(to-do #38): the stains of the cells (stain_cells_probability).
|
||||||
|
layout(std430, set = 0, binding = 63) buffer VerletPts { vec4 vpts[]; };
|
||||||
|
const int VERLET_BASE = 13696, VERLET_STRIDE = 20;
|
||||||
|
|
||||||
|
void verlet_draw(uint id, int vi, int n, float fadeK, uint seed) {
|
||||||
|
const int vb = VERLET_BASE + vi * VERLET_STRIDE;
|
||||||
|
const int nparts = fti(vb + 1).w;
|
||||||
|
if (nparts <= 0) return;
|
||||||
|
const uint base = id * 17u;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const ivec4 s = fti(vb + 4 + min(i, nparts - 1));
|
||||||
|
const vec4 sf = ftf(vb + 4 + min(i, nparts - 1));
|
||||||
|
if (s.x < 0) continue;
|
||||||
|
const int w = s.y & 0xFFFF, h = s.y >> 16;
|
||||||
|
if (w <= 0 || h <= 0) continue;
|
||||||
|
const vec2 a = vpts[base + uint(i)].xy;
|
||||||
|
vec2 d = i + 1 < n ? vpts[base + uint(i) + 1u].xy - a : (i > 0 ? a - vpts[base + uint(i) - 1u].xy : vec2(1.0, 0.0));
|
||||||
|
float len = length(d);
|
||||||
|
const vec2 dir = len > 0.01 ? d / len : vec2(1.0, 0.0), nrm = vec2(-dir.y, dir.x);
|
||||||
|
if (i + 1 >= n || len < float(w)) len = max(len, float(w)); // (the last point's sprite at its own length; scale_sprite_x stretches the others along their link)
|
||||||
|
const vec2 lo = min(min(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), min(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w)));
|
||||||
|
const vec2 hi = max(max(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), max(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w)));
|
||||||
|
const ivec2 c0 = ivec2(floor(lo)) - 1, c1 = ivec2(ceil(hi)) + 1;
|
||||||
|
if (c1.x - c0.x > 64 || c1.y - c0.y > 64) continue;
|
||||||
|
for (int y = c0.y; y <= c1.y; ++y)
|
||||||
|
for (int x = c0.x; x <= c1.x; ++x) {
|
||||||
|
const vec2 rel = vec2(x, y) + 0.5 - a;
|
||||||
|
const float u = dot(rel, dir), v = dot(rel, nrm) + sf.w;
|
||||||
|
if (u < 0.0 || u >= len || v < 0.0 || v >= float(h)) continue;
|
||||||
|
const int sx = min(int(u / len * float(w)), w - 1), sy = int(v);
|
||||||
|
const uint t = fxtex[s.x + sy * w + sx];
|
||||||
|
if ((t >> 24) == 0u) continue;
|
||||||
|
if (fadeK < 1.0 && rnd01(hash3(uint(x * 73 + y * 151), seed, 11u)) > fadeK) continue; // (the fade: pixels drop out)
|
||||||
|
const ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool verlet_blocking(vec2 p) { return solid_at(ivec2(int(floor(p.x)), int(ceil(p.y)))); }
|
||||||
|
|
||||||
|
void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team, int spec, int shooter, int herd, uint seed) {
|
||||||
|
const int vb = VERLET_BASE + vi * VERLET_STRIDE;
|
||||||
|
const ivec4 v0i = fti(vb), v1i = fti(vb + 1);
|
||||||
|
const vec4 v0 = ftf(vb), v1 = ftf(vb + 1), v2 = ftf(vb + 2);
|
||||||
|
const int n = clamp(v0i.x, 2, 16);
|
||||||
|
const float rest = v0.y, stiff = v0.z, damp = v0.w;
|
||||||
|
const uint base = id * 17u;
|
||||||
|
vec2 pts[16], prv[16];
|
||||||
|
const bool shooterOk = shooter > 0 && shooter - 1 < ctrl.inp.w && (ents[shooter - 1].st.w & 8) == 0;
|
||||||
|
if (age <= 1) { // the shot (00c0d290 case 2)
|
||||||
|
const vec2 origin = p.a.xy, v = p.a.zw * 60.0; // (the aim x the speed: speed px a frame)
|
||||||
|
for (int k = 0; k < n; ++k) { pts[k] = origin; prv[k] = origin - v; }
|
||||||
|
const bool follow = shooterOk && distance(origin, ents[shooter - 1].a.xy) < 24.0; // (cast from the hand: it follows the shooter; one a trigger made somewhere stays there)
|
||||||
|
vpts[base + 16u] = vec4(follow ? origin - ents[shooter - 1].a.xy : origin, follow ? 1.0 : 0.0, -1.0);
|
||||||
|
} else for (int k = 0; k < n; ++k) { const vec4 q = vpts[base + uint(k)]; pts[k] = q.xy; prv[k] = q.zw; }
|
||||||
|
vec4 aux = vpts[base + 16u];
|
||||||
|
const vec2 anchor = aux.z > 0.5 ? (shooterOk ? ents[shooter - 1].a.xy + aux.xy : pts[0]) : aux.xy;
|
||||||
|
if (age > 1) {
|
||||||
|
for (int k = 1; k < n; ++k) { // 00c37d30
|
||||||
|
vec2 delta = (pts[k] - prv[k]) * damp, np = pts[k] + delta;
|
||||||
|
if (v1i.x != 0 && verlet_blocking(np)) {
|
||||||
|
const vec2 mid = (pts[k] + np) * 0.5;
|
||||||
|
if (!verlet_blocking(mid)) np = mid;
|
||||||
|
else if (!verlet_blocking(mid - vec2(0.0, 1.0))) np = pts[k] - vec2(0.0, 1.0);
|
||||||
|
else np = pts[k];
|
||||||
|
} else if (liquid_nonsand(ivec2(floor(np)))) np = pts[k] + (pts[k] - prv[k]) * v1.y;
|
||||||
|
prv[k] = pts[k]; pts[k] = np;
|
||||||
|
}
|
||||||
|
for (int it = 0; it < 2; ++it) // 00c38310
|
||||||
|
for (int k = 0; k + 1 < n; ++k) {
|
||||||
|
const vec2 d = pts[k + 1] - pts[k];
|
||||||
|
const float l = length(d);
|
||||||
|
if (l < 1e-5) continue;
|
||||||
|
const float diff = (l - rest) / max(l, 0.01) * stiff * 0.5;
|
||||||
|
pts[k] += d * diff * 0.5; pts[k + 1] -= d * diff * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pts[0] = anchor; prv[0] = anchor;
|
||||||
|
const int lifeLeft = int(p.b.y) - age, fade = max(v1i.z, 1);
|
||||||
|
const float fadeK = lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0;
|
||||||
|
// 00c3fdc0: the damage of every point
|
||||||
|
for (int k = 0; k < n; ++k) {
|
||||||
|
const vec2 v = pts[k] - prv[k];
|
||||||
|
float dmg = min(v2.w * dot(v, v), v2.z);
|
||||||
|
if (dmg < v2.y) continue;
|
||||||
|
dmg *= fadeK;
|
||||||
|
if (dmg <= 0.0) continue;
|
||||||
|
const float r = v2.x;
|
||||||
|
for (int e = 0; e < ctrl.inp.w; ++e) {
|
||||||
|
const int kind = ents[e].st.x;
|
||||||
|
if (!(kind == 1 || kind == 2 || kind == 3) || (ents[e].st.w & (8 | 4096)) != 0 || e == shooter - 1) continue;
|
||||||
|
const vec2 rel = pts[k] - ents[e].a.xy;
|
||||||
|
if (rel.x < ents[e].box.x - r || rel.x > ents[e].box.y + r || rel.y < ents[e].box.z - r || rel.y > ents[e].box.w + r) continue;
|
||||||
|
if (kind == 3) { if (float(pc.frame) < aux.w) continue; aux.w = float(pc.frame + 20); } // mPlayerCooldownEnd
|
||||||
|
if (kind != 3 && herd != 0 && ents[e].ex8.y == herd) continue; // (the shooter's herd is spared)
|
||||||
|
atomicAdd(ents[e].x.x, int(dmg * 1000.0));
|
||||||
|
note_attacker(e, shooter);
|
||||||
|
ents[e].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF));
|
||||||
|
atomicOr(ents[e].ex3.z, 1);
|
||||||
|
if (e != 0) { const vec2 imp = v * 60.0 * ftf(vb + 3).x; atomicExchange(ents[e].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[e].aux.y, int(imp.y * 1000.0)); } // the damage packet's impulse: impulse_coeff x the point's velocity (the blood and the ragdoll of the hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 00c3fdc0: a point going back toward the shooter (its velocity toward the anchor) faster than 10 px a frame takes what is at it along: every entity whose position is within
|
||||||
|
// damage_radius of the point (not the shooter; creatures, the player, items) gets the point's velocity (VelocityComponent mVelocity, a knockback for a character)
|
||||||
|
for (int k = 1; k < n; ++k) {
|
||||||
|
const vec2 v = pts[k] - prv[k];
|
||||||
|
if (dot(anchor - pts[k], v) <= 0.0 || dot(v, v) <= 100.0) continue; // (|v|^2 > 100: faster than 10 px a frame)
|
||||||
|
const float r = v2.x;
|
||||||
|
for (int e = 0; e < ctrl.inp.w; ++e) {
|
||||||
|
const int kind = ents[e].st.x;
|
||||||
|
if (!(kind == 1 || kind == 2 || kind == 3 || kind == 4) || (ents[e].st.w & (8 | 32 | 128 | 4096)) != 0 || e == shooter - 1) continue; // (not dead, not a rigid body, not a hovering wand, not hidden)
|
||||||
|
const vec2 d = ents[e].a.xy - pts[k];
|
||||||
|
if (abs(d.x) > r || abs(d.y) > r) continue;
|
||||||
|
if (kind == 1 || kind == 3) ents[e].ex11.yz = v - ents[e].a.zw; // (00ab9910: a character's knockback; the entity pass adds the kick and holds it in the knockback state)
|
||||||
|
else ents[e].a.zw = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 00c3fdc0: the physics bodies within physics_force_radius of a point (a Box2D query of the box) get their velocity moved toward the point's: the host applies
|
||||||
|
// v += (the point's displacement - v) x physics_impulse_coeff / 6 (event -30000 - the body's entity slot, the displacement as two halves)
|
||||||
|
if (ftf(vb + 3).z != 0.0) for (int k = 1; k < n; ++k) {
|
||||||
|
const float fr = ftf(vb + 3).y;
|
||||||
|
int bs = -1;
|
||||||
|
for (int q = 0; q < 5 && bs < 0; ++q) {
|
||||||
|
const vec2 o = q == 0 ? vec2(0.0) : (q == 1 ? vec2(fr, 0.0) : (q == 2 ? vec2(-fr, 0.0) : (q == 3 ? vec2(0.0, fr) : vec2(0.0, -fr))));
|
||||||
|
if (body_hit(pts[k] + o)) bs = body_slot(pts[k] + o);
|
||||||
|
}
|
||||||
|
if (bs < 0) continue;
|
||||||
|
uint xi = uint(atomicAdd(xhead.x, 1));
|
||||||
|
if (xi < 512u) xe[xi] = vec4(ftf(vb + 3).z, 0.0, -30000.0 - float(bs), uintBitsToFloat(packHalf2x16(pts[k] - prv[k]))); // (x: physics_impulse_coeff)
|
||||||
|
}
|
||||||
|
vpts[base + 16u] = aux;
|
||||||
|
const vec2 tip = pts[n - 1], tipV = pts[n - 1] - prv[n - 1];
|
||||||
|
bool alive = true, expired = false;
|
||||||
|
if (spec >= 0) fire_triggers(spec, fired, 1, age, tip, tipV, seed, team, shooter, herd);
|
||||||
|
{ // the ProjectileSystem's own hit (00c073b0 / 00c091f0) at the root, which 00c3fdc0 puts on the last point: the projectile's damage, and its hit triggers on every hit
|
||||||
|
// (00c091f0 casts the +0x61c and +0x610 lists and removes nothing: a piercing tentacle with a trigger casts its payload at each collision)
|
||||||
|
const int tflags = fti(tb + 11).y, tw = fti(tb + 1).w;
|
||||||
|
int hk;
|
||||||
|
gAhead = tip;
|
||||||
|
if (age > 1 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)
|
||||||
|
&& victim_at(tip, shooter, herd, shot_flags(tw, p.d.z), age, hk)) {
|
||||||
|
const float hitDmg = hit_damage(hk, p.b.x, tb, p.f) * boost_of(tw, hk) * fadeK;
|
||||||
|
if (hitDmg > 0.0) atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0));
|
||||||
|
note_attacker(hk, shooter);
|
||||||
|
ents[hk].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF));
|
||||||
|
atomicOr(ents[hk].ex3.z, 1);
|
||||||
|
if (spec >= 0) fire_triggers(spec, fired, 3, age, tip, tipV, seed, team, shooter, herd);
|
||||||
|
if ((tflags & 4) == 0) alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (age >= int(p.b.y)) { alive = false; expired = true; }
|
||||||
|
if (!alive && spec >= 0) { if (expired) fire_triggers(spec, fired, 4, age, tip, tipV, seed, team, shooter, herd); fire_triggers(spec, fired, 5, age, tip, tipV, seed, team, shooter, herd); }
|
||||||
|
for (int k = 0; k < n; ++k) vpts[base + uint(k)] = vec4(pts[k], prv[k]);
|
||||||
|
if (alive) verlet_draw(id, vi, n, fadeK, seed);
|
||||||
|
p.a = vec4(tip, 0.0, 0.0);
|
||||||
|
p.c.z = float(age);
|
||||||
|
p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF);
|
||||||
|
p.d.w = alive ? p.d.w : 0;
|
||||||
|
fprojs[id] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_GlobalInvocationID.x;
|
||||||
|
if (id >= uint(FP_MAX)) return;
|
||||||
|
FProj p = fprojs[id];
|
||||||
|
if (p.d.w == 0) return;
|
||||||
|
int type = p.d.x, tb = type * TYPE_STRIDE;
|
||||||
|
ivec4 t1 = fti(tb + 1), t3 = fti(tb + 3), t5 = fti(tb + 5), t6 = fti(tb + 6);
|
||||||
|
vec4 t4 = ftf(tb + 4);
|
||||||
|
const int vflag = fti(tb + 16).x;
|
||||||
|
if ((vflag & 4096) != 0 && pc.pass_ == 1) { // a VERLET chain between ticks: drawn as it is
|
||||||
|
const int vi = ((vflag >> 13) & 31) - 1;
|
||||||
|
const int fade = max(fti(VERLET_BASE + vi * VERLET_STRIDE + 1).z, 1), lifeLeft = int(p.b.y) - int(p.c.z);
|
||||||
|
verlet_draw(id, vi, clamp(fti(VERLET_BASE + vi * VERLET_STRIDE).x, 2, 16), lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0, hash3(id, uint(pc.frame), 5u));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions
|
||||||
|
draw_sprite(p.a.xy - p.a.zw * (1.0 - ctrl.view.z), p.a.zw, int(p.c.z), t1, t5, t6, p.e.x + p.e.y * ctrl.view.z, (t1.w & TF_PHYSICS) == 0 && p.e.x < 0.0 ? -p.e.x : 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint seed = hash3(id, uint(pc.frame), 5u);
|
||||||
|
vec2 start = p.a.xy, pos = p.a.xy, vel = p.a.zw;
|
||||||
|
{ // waiting at the edge of what is loaded (far.glsl): its next position outside the window, inside the far map, in a block not resident (a biome-map tile
|
||||||
|
// without its area yet, or a block not generated yet): it stays where it is, its age frozen, and goes on from there once the block is resident
|
||||||
|
const ivec2 nw = ivec2(floor(pos + vel));
|
||||||
|
const ivec2 lq = nw - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if ((lq.x < 0 || lq.y < 0 || lq.x >= pc.gw || lq.y >= pc.gh) && far_in_map(nw) && far_slot(nw) == 0) return;
|
||||||
|
}
|
||||||
|
int age = int(p.c.z) + 1, fired = p.d.z & 255, team = (p.d.z >> 8) & 3, bounces = int(p.c.w), spec = p.d.y, shooter = int(p.e.z), herd = int(p.e.w);
|
||||||
|
if ((vflag & 4096) != 0) { verlet_step(id, p, tb, ((vflag >> 13) & 31) - 1, age, fired, team, spec, shooter, herd, seed); return; } // VERLET: the chain (verlet_step)
|
||||||
|
if ((fti(tb + 11).x & 1) != 0) { // HomingComponent (00b323f0): the nearest entity with the target tag within detect_distance (or the shooter with target_who_shot) pulls the velocity
|
||||||
|
const vec4 h = ftf(tb + 10); // detect_distance, homing_velocity_multiplier, homing_targeting_coeff, max_turn_rate
|
||||||
|
const int hf = fti(tb + 11).x, shooterSlot = int(p.e.z);
|
||||||
|
int target = -1;
|
||||||
|
float bestD = h.x * h.x;
|
||||||
|
// 00b323f0: predefined_target first (alive, not invisible), then the shooter with target_who_shot, then the nearest tagged entity; invisible ones (INVISIBILITY) never
|
||||||
|
const bool shooterAlive = shooterSlot > 0 && (ents[shooterSlot - 1].st.w & 8) == 0 && (ents[shooterSlot - 1].ex8.x & 8192) == 0;
|
||||||
|
const int pre = fti(tb + 11).w;
|
||||||
|
vec2 preAim = vec2(0.0); bool preOk = false;
|
||||||
|
if (pre > 0 && pre - 1 < ctrl.inp.w && (ents[pre - 1].st.w & 8) == 0 && (ents[pre - 1].ex8.x & 8192) == 0) target = pre - 1;
|
||||||
|
else if (pre < 0 && -pre - 1 < FP_MAX && fprojs[-pre - 1].d.w != 0) { preAim = fprojs[-pre - 1].a.xy; preOk = true; } // a projectile (homing_projectile.lua aims at other shots)
|
||||||
|
if (target >= 0 || preOk) {}
|
||||||
|
else if ((hf & 4) != 0 && shooterAlive) target = shooterSlot - 1;
|
||||||
|
else for (int k = 0; k < ctrl.inp.w; ++k) {
|
||||||
|
const int kind = ents[k].st.x;
|
||||||
|
if (!(kind == 1 || kind == 2 || kind == 3) || (ents[k].st.w & 8) != 0 || (ents[k].ex8.x & 8192) != 0 || k == shooterSlot - 1 || (ents[k].ex8.x & ((hf >> 4) & 0x1FFF)) == 0) continue;
|
||||||
|
const vec2 dd = pos - ents[k].a.xy;
|
||||||
|
const float dsq = dot(dd, dd);
|
||||||
|
if (dsq < bestD) { bestD = dsq; target = k; }
|
||||||
|
}
|
||||||
|
if (target >= 0 || preOk) {
|
||||||
|
const vec2 tp = preOk ? preAim : ents[target].a.xy + vec2(ents[target].box.x + ents[target].box.y, ents[target].box.z + ents[target].box.w) * 0.5; // the centre of its hitbox
|
||||||
|
const vec2 delta = tp - pos;
|
||||||
|
const float dist = length(delta);
|
||||||
|
if (dist < h.x) {
|
||||||
|
if ((hf & 2) == 0) { // the velocity keeps homing_velocity_multiplier of itself and gains homing_targeting_coeff x the offset x dt x (1 - distance / detect_distance), in pixels per second
|
||||||
|
const float k = max(1.0 - dist / h.x, 0.0);
|
||||||
|
vel = vel * h.y + h.z * delta * (1.0 / 60.0) * k / 60.0;
|
||||||
|
} else { // just_rotate_towards_target: the velocity turns toward the target by at most max_turn_rate
|
||||||
|
const float sp = length(vel);
|
||||||
|
const float av = sp > 0.0 ? atan(vel.y, vel.x) : 0.0, at = dist > 0.0 ? atan(delta.y, delta.x) : 0.0;
|
||||||
|
const float TAU = 6.2831855;
|
||||||
|
float A = av - at; A = A - TAU * floor(A / TAU);
|
||||||
|
const float turn = min(A, h.w);
|
||||||
|
const float ang = (TAU - A <= A) ? turn : -turn;
|
||||||
|
vel = vec2(cos(ang) * vel.x - sin(ang) * vel.y, cos(ang) * vel.y + sin(ang) * vel.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// VelocityComponent (00c370c0): gravity, then v -= v dt air_friction (a negative friction speeds it up), then the terminal velocity (apply_terminal_velocity, pixels per second)
|
||||||
|
const vec4 t15 = ftf(tb + 15), t16f = ftf(tb + 16), t17 = ftf(tb + 17);
|
||||||
|
const int f16 = fti(tb + 16).x;
|
||||||
|
vel.y += p.b.z * bm_gravity(pos); vel.x += t17.x; // (gravity_y x the biome's entity_gravity_y_multiplier, 00c370c0)
|
||||||
|
vel -= vel * p.b.w;
|
||||||
|
if ((f16 & 64) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 > t16f.z * t16f.z) vel = normalize(vel) * (t16f.z / 60.0);
|
||||||
|
const bool inert = ((p.d.z >> 11) & 1) != 0; // on_collision_remove_projectile: the entity stuck where it hit, without its ProjectileComponent
|
||||||
|
if (inert) vel = vec2(0.0);
|
||||||
|
const bool removed = ((p.d.z >> 29) & 1) != 0; // the host removed it (EntityKill of its twin): the component goes, nothing of the update runs
|
||||||
|
const bool wasStuck = (p.d.z & 0x800) != 0;
|
||||||
|
if (spec >= 0 && !inert && !removed) fire_triggers(spec, fired, 1, age, pos, vel, seed, team, shooter, herd); // the countdown runs before the flight of the update (00c06c60)
|
||||||
|
int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y)))));
|
||||||
|
float spin = p.e.x, omega = p.e.y;
|
||||||
|
bool alive = !removed, hitWorld = false, stuck = false, hitEntity = false;
|
||||||
|
bool touchedWorld = false; vec2 trigVel = vec2(0.0); // a world hit this frame (bounce or stop): the hit-world triggers fire with the velocity reflected about the surface (00c06250)
|
||||||
|
// a thrown physics body (the bomb) rests on its shape, not on its centre: the lowest point of its image counts as well (half the smaller side below the position)
|
||||||
|
const vec2 foot = (t1.w & TF_PHYSICS) != 0 && t5.y > 0 ? vec2(0.0, 0.5 * float(min(t5.y, t5.z))) : vec2(0.0);
|
||||||
|
const int tflags = fti(tb + 11).y; // 2: collide_with_world 0 (goes through terrain), 4: penetrate_entities (hits every damage_every_x_frames frames, does not die), 8: collide_with_entities 0
|
||||||
|
const bool noWorld = (tflags & 2) != 0 || inert;
|
||||||
|
// penetrate_world (00c073b0): no collision with the world; when the path of the frame crosses it, the step is penetrate_world_velocity_coeff of the velocity
|
||||||
|
const bool penWorld = (f16 & 1) != 0 && !noWorld;
|
||||||
|
vec2 stepVel = vel;
|
||||||
|
if (penWorld) {
|
||||||
|
for (int i = 1; i <= steps; ++i) if (solid_at(ivec2(floor(pos + vel * (float(i) / float(steps)))))) { stepVel = vel * t15.x; break; }
|
||||||
|
}
|
||||||
|
const float gpEnergy = (f16 & 1) == 0 ? t15.y : 0.0; // ground_penetration_coeff x VelocityComponent mass
|
||||||
|
for (int i = 0; i < steps && alive; ++i) {
|
||||||
|
vec2 np = pos + stepVel / float(steps);
|
||||||
|
const bool bodyAt = body_hit(np) && (shooter <= 0 || body_slot(np) != shooter - 1); // (not the body of its shooter: a physics creature shoots from inside itself)
|
||||||
|
if (!noWorld && !penWorld && bodyAt && !solid_at(ivec2(floor(np)))) body_damage(body_slot(np), pos, p.b.x); // (the hit point: the last free position)
|
||||||
|
bool blocked = !noWorld && !penWorld && (solid_at(ivec2(floor(np))) || solid_at(ivec2(floor(np + foot))) || bodyAt);
|
||||||
|
if (blocked && gpEnergy > 0.0 && solid_at(ivec2(floor(np))) && !bodyAt) {
|
||||||
|
// ground_penetration (00c08a30, per cell of the ray): the kinetic energy coeff x mass x 0.5 x speed^2 is spent on the cell's hp; a cell that loses all of it is removed and the
|
||||||
|
// projectile flies on, slower; a cell harder than ground_penetration_max_durability_to_destroy or a speed of 10 px/s or less stops it
|
||||||
|
const ivec2 c = ivec2(floor(np));
|
||||||
|
const ivec2 q = c - ivec2(pc.view_x, pc.view_y);
|
||||||
|
const float v2 = dot(vel, vel) * 3600.0;
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && v2 > 100.0) {
|
||||||
|
const int ci = q.y * pc.gw + q.x;
|
||||||
|
const uint m = cells[ci] & 0xFFFFu;
|
||||||
|
const float dur = uintBitsToFloat(mats[3u * m + 2u].y);
|
||||||
|
const int maxd = fti(tb + 15).z;
|
||||||
|
if (!(maxd > 0 && dur > float(maxd))) {
|
||||||
|
const float energy = gpEnergy * 0.5 * v2;
|
||||||
|
const float hp = float((mats[3u * m + 2u].z >> 20) * 50u);
|
||||||
|
vel *= 1.0 - min(hp, energy) / energy;
|
||||||
|
stepVel = vel;
|
||||||
|
if (hp - min(hp, round(energy)) < 1.0) { cells[ci] = 0u; ext[ci] = vec2(0.0); blocked = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (blocked) {
|
||||||
|
if (t1.w < 0 && solid_at(ivec2(floor(np)))) { vel = vec2(0.0); break; } // a particle entity (SimplePhysicsComponent, 00c17fc0): a hit on the terrain keeps the position before it and zeroes the velocity (DieIfSpeedBelow then removes it)
|
||||||
|
if ((t1.w & TF_PHYSICS) != 0) { // a thrown body: bounces with loss, rolls and comes to rest; only the fuse makes it explode
|
||||||
|
const int brk = (fti(tb + 11).z >> 16) & 0xFFFF; // a breakable one (the egg): an impact faster than its break speed kills it, it explodes (projectiles.cpp break_speed)
|
||||||
|
if (brk > 0 && length(vel) * 60.0 > float(brk)) { alive = false; hitWorld = true; break; }
|
||||||
|
bool bx = solid_at(ivec2(floor(vec2(np.x, pos.y)))) || solid_at(ivec2(floor(vec2(np.x, pos.y) + foot))), by = solid_at(ivec2(floor(vec2(pos.x, np.y)))) || solid_at(ivec2(floor(vec2(pos.x, np.y) + foot)));
|
||||||
|
if (bx || !by) { vel.x = -vel.x * 0.4; vel.y *= 0.85; omega = -omega * 0.5 + vel.y * 0.15; }
|
||||||
|
if (by || !bx) { vel.y = -vel.y * 0.4; vel.x *= 0.85; omega = omega * 0.6 + vel.x * 0.2; }
|
||||||
|
if (length(vel) < 0.25) vel = vec2(0.0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (body_hit(np) && t16f.w > 0.0 && body_slot(np) >= 0) { // 00c073b0: the body hit gets ApplyLinearImpulse(the frame's displacement x physics_impulse_coeff x VelocityComponent mass / 10) at the point (event -10000 - the entity slot)
|
||||||
|
uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(np, -10000.0 - float(body_slot(np)), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0)));
|
||||||
|
}
|
||||||
|
// the hit (00c073b0): bounces_left counts down at every hit; with some left, the velocity is reflected about the surface normal (16 rays of 5 px around the last free position,
|
||||||
|
// 0063fd80) and scaled by bounce_energy, unless the reflection points back into the wall (r.v <= 0.75, bounce_at_any_angle allows it); bounce_always then turns it around with a
|
||||||
|
// random turn of +-0.1 rad. Without a bounce the projectile stops there (velocity 0) and the death of 00c08b90 follows: on_collision_die kills it, on_collision_remove_projectile
|
||||||
|
// leaves it stuck, otherwise it stays where it stopped.
|
||||||
|
const int bl = bounces;
|
||||||
|
bounces = max(bl - 1, -1);
|
||||||
|
bool bounced = false;
|
||||||
|
vec2 nsum = vec2(0.0); int nh = 0; // the surface normal: 16 rays of 5 px around the last free position (0063fd80)
|
||||||
|
for (int k = 0; k < 16; ++k) {
|
||||||
|
const float a = float(k) * (6.2831855 / 16.0);
|
||||||
|
const vec2 d = vec2(-sin(a), cos(a)) * 5.0;
|
||||||
|
for (int s = 1; s <= 5; ++s) if (solid_at(ivec2(floor(pos + d * (float(s) / 5.0)))) || body_hit(pos + d * (float(s) / 5.0))) { nsum += d; ++nh; break; }
|
||||||
|
}
|
||||||
|
touchedWorld = true;
|
||||||
|
trigVel = (nh > 0 && dot(nsum, nsum) > 0.0) ? vel - normalize(nsum) * 2.0 * dot(normalize(nsum), vel) : -vel;
|
||||||
|
if (bl > 0) {
|
||||||
|
const vec2 v = vel * 60.0;
|
||||||
|
if (nh > 0 && dot(nsum, nsum) > 0.0) {
|
||||||
|
const vec2 n = normalize(nsum);
|
||||||
|
const vec2 r = v - n * 2.0 * dot(n, v);
|
||||||
|
if (dot(r, v) > 0.75 || (f16 & 4) != 0 || ((p.d.z >> 28) & 1) != 0) { vel = r * t15.w / 60.0; bounced = true; }
|
||||||
|
}
|
||||||
|
if (!bounced && (f16 & 2) != 0) {
|
||||||
|
const float a = rnd01(hash3(seed, uint(i), 97u)) * 0.2 - 0.1;
|
||||||
|
const vec2 rv = -vel * t15.w;
|
||||||
|
vel = vec2(cos(a) * rv.x - sin(a) * rv.y, sin(a) * rv.x + cos(a) * rv.y);
|
||||||
|
bounced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bounced && (f16 & 2048) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -8.0, float(id)); } // bounce_fx_file: the host loads it at the bounce (00c073b0; event -8, the projectile's slot)
|
||||||
|
if (bounced) { pos += vel; break; } // (the position of the frame: the last free one + the new velocity x dt)
|
||||||
|
vel = vec2(0.0);
|
||||||
|
if ((t1.w & TF_COLLISION_DIE) != 0) { alive = false; hitWorld = true; }
|
||||||
|
else if ((f16 & 8) != 0) stuck = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos = np;
|
||||||
|
if (!inert && !stuck && (t1.w & TF_PHYSICS) == 0 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)) {
|
||||||
|
int hk; // the herds decide who is hurt: everything except the herd of the shooter (unless friendly fire); never the player with dont_hit_player
|
||||||
|
gAhead = pos + stepVel / float(steps);
|
||||||
|
if (victim_at(pos, shooter, herd, shot_flags(t1.w, p.d.z), age, hk)) { // (the shot's friendly_fire modifier)
|
||||||
|
float speedScale = 1.0; // damage_scaled_by_speed: speed / mInitialSpeed (or damage_scale_max_speed when > 0)
|
||||||
|
if ((f16 & 128) != 0) { const float s0 = t17.y > 0.0 ? t17.y / 60.0 : p.e.y; speedScale = s0 > 0.0 ? length(vel) / s0 : 1.0; }
|
||||||
|
const float cm = crit_mult((p.d.z >> 12) & 4095, t1.w, shooter, hk, seed);
|
||||||
|
const float hitDmg = hit_damage(hk, p.b.x * speedScale, tb, p.f) * boost_of(t1.w, hk) * cm;
|
||||||
|
if (cm > 1.0) note_crit(pos, hk);
|
||||||
|
note_block(hk, p.b.x, hitDmg);
|
||||||
|
atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0));
|
||||||
|
if (ents[hk].st.x == 4) body_damage(hk, pos, hitDmg); // a prop with a liquid inventory leaks
|
||||||
|
if (ents[hk].st.x == 4 && ents[hk].ex7.w > 0.5 && t16f.w > 0.0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(gAhead, -10000.0 - float(hk), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0))); } // and the impulse on its body
|
||||||
|
{ // 00c091f0: knockback scalar = ProjectileComponent.knockback_force x speed (pixels per second) x the projectile's VelocityComponent mass; the force vector = velocity x ragdoll_force_multiplier;
|
||||||
|
// then the victim's gate (00ab9a90): abs(scalar) against minimum_knockback_force before the division by its mass, upward y, never on a killing hit
|
||||||
|
const vec4 t9 = ftf(tb + 9);
|
||||||
|
const int vk = ents[hk].st.x;
|
||||||
|
const vec2 fv = vel * 60.0 * t9.z;
|
||||||
|
const float kfh = t9.x + p.f.z; // ProjectileComponent knockback_force + the shot's knockback_force modifier
|
||||||
|
const float sc = kfh * length(vel * 60.0) * t9.y;
|
||||||
|
if (kfh != 0.0 && (vk == 1 || vk == 3) && length(fv) > 0.0 && ents[hk].hp.x - hitDmg > 0.0 && abs(sc) >= ents[hk].ex11.x && (ents[hk].ex8.x & 96) == 0 && ents[hk].ex8.y > 0) {
|
||||||
|
const float mass = intBitsToFloat(ents[hk].rg2.w);
|
||||||
|
vec2 imp = normalize(fv) * sc;
|
||||||
|
if (mass != 0.0) imp /= mass;
|
||||||
|
imp.y = -abs(imp.y);
|
||||||
|
ents[hk].ex11.yz = imp / 60.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
note_attacker(hk, shooter);
|
||||||
|
ents[hk].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF)); // the projectile of the hit (script_damage_received's projectile_thats_responsible)
|
||||||
|
atomicOr(ents[hk].ex3.z, 1); // hit by a projectile
|
||||||
|
if ((t1.w & TF_HIT_EFFECTS) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(float(hk), float(shooter), -1.0, float(1 + type)); } // the hit effects of the projectile: the host applies them (victim slot, shooter slot + 1, type + 1)
|
||||||
|
if (hk != 0) { // the packet of 00c091f0: the impulse = velocity (px/s) x ragdoll_force_multiplier (the blood of the hit and the ragdoll take it), its RAGDOLL_FX
|
||||||
|
const vec2 imp = vel * 60.0 * ftf(tb + 9).z;
|
||||||
|
atomicExchange(ents[hk].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[hk].aux.y, int(imp.y * 1000.0));
|
||||||
|
atomicAnd(ents[hk].ex3.z, ~0xF000); atomicOr(ents[hk].ex3.z, ((fti(tb + 11).y >> 4) & 15) << 12);
|
||||||
|
}
|
||||||
|
if (spec >= 0) fire_triggers(spec, fired, 3, age, pos, vel, seed, team, shooter, herd); // the impact (00c091f0)
|
||||||
|
if ((tflags & 4) == 0) { alive = false; hitEntity = true; } // (a penetrating projectile flies on)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alive && t1.w < 0 && length(vel) < float(fti(tb + 9).w >> 20 & 4095) / 256.0) alive = false; // DieIfSpeedBelowComponent of a particle entity
|
||||||
|
// 00c370c0: the count N of liquid cells (not powder) in the 3x3 around the new integer position (float coordinates truncate toward zero), taken only when that position changed.
|
||||||
|
// Every candidate counts, then (displace_liquid on, 75 % of the draws) its cell becomes a particle that flies off against the motion (00717670: rotated, 0.1 x); the velocity keeps
|
||||||
|
// 1 - liquid_drag * dt * N of itself (VelocityComponent liquid_drag, unclamped), and N above liquid_death_threshold kills the projectile (liquid_death_threshold, default 0: off).
|
||||||
|
if (alive && any(notEqual(ivec2(pos), ivec2(start)))) {
|
||||||
|
int n = 0;
|
||||||
|
const ivec2 q = ivec2(pos);
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(q + ivec2(dx, dy))) ++n;
|
||||||
|
if (n > 0 && fti(tb + 8).y != 0) {
|
||||||
|
for (int k = 0; k < 9; ++k) {
|
||||||
|
const ivec2 c = q + ivec2(k % 3 - 1, k / 3 - 1);
|
||||||
|
if (liquid_at_pos(vec2(c) + 0.5) == 0u || (hash3(seed, uint(k), 81u) % 101u) >= 75u) continue;
|
||||||
|
const float ang = 0.6 * float(hash3(seed, uint(k), 82u) & 0x7FFFu) / 32767.0 - 0.3;
|
||||||
|
displace_cell(c, -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vel *= 1.0 - ftf(tb + 8).x * (1.0 / 60.0) * float(n);
|
||||||
|
// (liquid_death_threshold of the VelocityComponent defaults to 0: no death by the count; die_on_liquid_collision is the collision with a liquid cell)
|
||||||
|
if ((t1.w & TF_DIE_IN_LIQUID) != 0 && liquid_nonsand(ivec2(pos))) { alive = false; hitWorld = true; }
|
||||||
|
}
|
||||||
|
if (alive && !inert && (f16 & 16) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 <= t16f.y * t16f.y) { // die_on_low_velocity: the death of a hit (00c08b90)
|
||||||
|
if ((t1.w & TF_COLLISION_DIE) != 0) alive = false; else if ((f16 & 8) != 0) stuck = true;
|
||||||
|
}
|
||||||
|
bool expired = false;
|
||||||
|
if (alive && !inert && !stuck && age >= int(p.b.y)) { alive = false; expired = true; } // lifetime out (the inert entity has no ProjectileComponent, so no lifetime)
|
||||||
|
if (!alive && fti(tb + 17).w != 0) { // on_death_emit_particle: on_death_emit_particle_count real particles of the material
|
||||||
|
const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64);
|
||||||
|
for (int k = 0; k < dn; ++k) {
|
||||||
|
const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 93u)), rnd01(hash3(seed, uint(k), 94u))) - 0.5) * 2.0 / 60.0 * 30.0;
|
||||||
|
emit_material_particle(pos, ev, 30.0, PART_GRAV, mats[3u * uint(dmat)].x, dmat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!alive && (hitWorld || hitEntity) && (f16 & 1024) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -6.0, float(type)); } // on_collision_spawn_entity: the host loads spawn_entity there (00c08b90)
|
||||||
|
// triggers (see fire_triggers): a hit on the world fires the hit-world and waiting timers in the reflected direction (also when the projectile bounces and lives on); the end of the
|
||||||
|
// lifetime fires the waiting timers; the removal of the projectile, whatever ended it, fires the death triggers
|
||||||
|
if (touchedWorld && spec >= 0) fire_triggers(spec, fired, 2, age, pos, trigVel, seed, team, shooter, herd);
|
||||||
|
if (stuck && !wasStuck && spec >= 0) fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd); // on_collision_remove_projectile: the ProjectileComponent goes, its removal (00c0e3e0, slot 15 of the system) casts the death triggers
|
||||||
|
if (!alive) {
|
||||||
|
if (spec >= 0) {
|
||||||
|
if (expired) fire_triggers(spec, fired, 4, age, pos, vel, seed, team, shooter, herd);
|
||||||
|
fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd);
|
||||||
|
}
|
||||||
|
if ((t1.w & TF_EXPLODE) != 0) {
|
||||||
|
explode(pos, p.c.x, p.c.y, type, team, shooter, herd, (p.d.z >> 12) & 4095, seed);
|
||||||
|
if (((p.d.z >> 24) & 15) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -7.0, float((p.d.z >> 24) & 15)); } // lightning_count: that many electricity.xml at the blast (event kind -7)
|
||||||
|
if (t3.y >= 0) boom_fx(t3.y, pos, seed, p.c.x);
|
||||||
|
} // (a projectile that does not explode leaves nothing of its own when it dies: no sparks - wall pieces absorbed by Spells To Power vanish into their red poofs only)
|
||||||
|
}
|
||||||
|
if (alive && p.f.w > 0.5) { // the shot's trail_material: real particles of the material along the path (00c0b440: an emitter with count = amount, velocity +- sqrt(amount) x 0.5)
|
||||||
|
const int tm = int(p.f.w), tmat = (tm & 4095) - 1, amount = tm >> 12;
|
||||||
|
const float tv = sqrt(float(amount)) * 0.5 / 60.0;
|
||||||
|
for (int k = 0; k < min(amount, 8); ++k) {
|
||||||
|
const vec2 ep = mix(start, pos, rnd01(hash3(seed, uint(k), 71u)));
|
||||||
|
const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 72u)), rnd01(hash3(seed, uint(k), 73u))) - 0.5) * 2.0 * tv;
|
||||||
|
emit_material_particle(ep, ev, 20.0, PART_GRAV, mats[3u * uint(tmat)].x, tmat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alive) {
|
||||||
|
// particle emitters (cosmetic)
|
||||||
|
for (int e = 0; e < t3.w; ++e) {
|
||||||
|
int eb = EMIT_BASE + (t3.z + e) * EMIT_STRIDE;
|
||||||
|
ivec4 e0 = fti(eb), e2 = fti(eb + 2);
|
||||||
|
vec4 e0f = ftf(eb), e1 = ftf(eb + 1), e3 = ftf(eb + 3);
|
||||||
|
const int iv2 = max(e2.y & 0xFF, 2); // the mean emission interval in half frames (the min .. max interval of 00bc75e0 is random; 1..2 frames emits 2 times in 3 frames, not every frame)
|
||||||
|
const float away = float(e2.y >> 16) / 60.0; // velocity_always_away_from_center, cells per frame
|
||||||
|
const uint attr = uint(e2.y >> 8) & 0xFFu; // attractor_force
|
||||||
|
{ const int ph = age + int(id) + e * 3; if ((ph * 2) / iv2 == (ph * 2 - 2) / iv2) continue; }
|
||||||
|
if (age < (e2.z >> 16)) continue; // delay_frames (summon_portal.xml's burst comes at frame 118)
|
||||||
|
const int c0 = e2.z & 0xFFFF;
|
||||||
|
int n = c0 + int(rnd01(hash3(seed, uint(e), 51u)) * float(e2.w - c0 + 1));
|
||||||
|
for (int k = 0; k < n && k < 64; ++k) { // (the giga black hole's emitters make 40 - 60 a frame)
|
||||||
|
float tt = (e0.y & 1) != 0 ? rnd01(hash3(seed, uint(e * 64 + k), 52u)) : 1.0;
|
||||||
|
vec2 ep = mix(start, pos, tt);
|
||||||
|
ep += (vec2(rnd01(hash3(seed, uint(e * 64 + k), 59u)), rnd01(hash3(seed, uint(e * 64 + k), 60u))) - 0.5) * 2.0 * e3.zw; // x/y_pos_offset ranges
|
||||||
|
{ // area_circle_radius (00bc75e0): a point of the disc, radius sqrt(u) between min and max (quarter pixels in bits 8-19 / 20-31 of the flags)
|
||||||
|
const float rmax = float((uint(e0.y) >> 8) & 0xFFFu) * 0.25, rmin = float((uint(e0.y) >> 20) & 0xFFFu) * 0.25;
|
||||||
|
if (rmax > 0.0) { const float rr = sqrt(rnd01(hash3(seed, uint(e * 64 + k), 61u))) * (rmax - rmin) + rmin, aa = rnd01(hash3(seed, uint(e * 64 + k), 62u)) * 6.2831853; ep += vec2(cos(aa), sin(aa)) * rr; }
|
||||||
|
}
|
||||||
|
vec2 ev = vec2(mix(e1.x, e1.y, rnd01(hash3(seed, uint(e * 64 + k), 53u))), mix(e1.z, e1.w, rnd01(hash3(seed, uint(e * 64 + k), 54u))));
|
||||||
|
ev += (vec2(rnd01(hash3(seed, uint(k), 55u)), rnd01(hash3(seed, uint(k), 56u))) - 0.5) * 2.0 * e3.x;
|
||||||
|
if (away != 0.0) { const vec2 dd = ep - pos; const float dl = length(dd); if (dl > 1e-4) ev += dd / dl * away; } // (00bc75e0: along the way out from the emitter)
|
||||||
|
float life = max(2.0, mix(e0f.z, e0f.w, rnd01(hash3(seed, uint(e * 64 + k), 57u))));
|
||||||
|
if ((e0.y & 4) != 0) { // create_real_particles (00bc75e0 route +0x60): the material is created in the grid at the position (velocity and lifetime are ignored) through the cell
|
||||||
|
// factory: into an empty cell, or over a gas or liquid one unless emit_only_if_there_is_space (+0x67, flag 64)
|
||||||
|
const ivec2 q = ivec2(floor(ep)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
const int mat = int(ft[eb + 3].y);
|
||||||
|
if (mat > 0 && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
|
||||||
|
const int ci = q.y * pc.gw + q.x;
|
||||||
|
const uint cur = cells[ci], cm = cell_mat(cur);
|
||||||
|
const bool free = cm == 0u || ((e0.y & 64) == 0 && (mflags(cm) & (F_GAS | F_LIQCELL)) != 0u && (mflags(cm) & F_STATIC) == 0u);
|
||||||
|
if (free && atomicCompSwap(cells[ci], cur, uint(mat) | (((hash3(seed, uint(e * 8 + k), 58u) >> 16) & 0xFFu) << 16)) == cur) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((e0.y & 16) == 0 || cell_free_at(ep)) { // (bit 3: collide_with_grid 0, bit 4: cosmetic_force_create 0, bit 5: a material that does not glow - blended with its alpha)
|
||||||
|
const bool blended = (e0.y & 32) != 0;
|
||||||
|
const int ps = emit_particle_g(ep, ev, life, uf(ft[eb + 2].x), uint(e0.x) & 0xFFFFFFu, (e0.y & 2) != 0, !blended, (e0.y & 8) == 0);
|
||||||
|
if (ps >= 0 && blended) fparts[ps].c.z = 255u - ((uint(e0.x) >> 24) & 0xFFu);
|
||||||
|
if (ps >= 0 && (attr != 0u || (e0.y & 128) != 0)) fparts[ps].c.z |= (id & 0x7FFu) << 8 | attr << 19 | ((e0.y & 128) != 0 ? 0x8000000u : 0u); // the attractor (this projectile, its force) and draw_as_long, for fx_particles.comp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t4.w > 0.0) add_light(pos, min(t4.w, 36.0) * 0.55, 0.32, t4.xyz);
|
||||||
|
draw_sprite(pos, vel, age, t1, t5, t6, spin, (t1.w & TF_PHYSICS) == 0 && spin < 0.0 ? -spin : 1.0);
|
||||||
|
}
|
||||||
|
if ((t1.w & TF_PHYSICS) != 0) { if (length(vel) < 0.01) omega *= 0.8; spin += omega; omega *= 0.99; }
|
||||||
|
p.e = vec4(spin, omega, p.e.z, p.e.w); // (the shooter slot + 1 and its herd stay with the projectile)
|
||||||
|
if (alive && !inert) vel *= bm_drag(pos); // 00c06c60: after the move the velocity is multiplied by the biome's projectile_drag_coeff (MOIST 0.965)
|
||||||
|
p.a = vec4(pos, vel);
|
||||||
|
p.c.z = float(age); p.c.w = float(bounces);
|
||||||
|
if (!alive && (fti(tb + 11).y & 1) != 0) { uint xi = uint(atomicAdd(xhead.x, 2)); if (xi + 1u < 512u) { xe[xi] = vec4(pos, -5.0, float(id)); xe[xi + 1u] = vec4(float(p.d.w), 0.0, 0.0, 0.0); } } // the twin's projectile died (event kind -5)
|
||||||
|
p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF) | (p.d.z & 0x400) | (stuck ? 0x800 : 0); // bit 11: stuck without its ProjectileComponent (on_collision_remove_projectile); bits 12-23 crit, 24-27 lightning_count, 28 bounce at any angle stay
|
||||||
|
p.d.w = alive ? p.d.w : 0;
|
||||||
|
fprojs[id] = p;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Projectiles shot by creatures: move in one-cell steps, die in solid cells or when they hit the player, drawn as a 3x3 blob into the entity layer.
|
||||||
|
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
|
||||||
|
struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player)
|
||||||
|
layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; };
|
||||||
|
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; } ctrl;
|
||||||
|
|
||||||
|
bool solid_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return true;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_GlobalInvocationID.x;
|
||||||
|
if (id >= 512u) return;
|
||||||
|
Proj p = projs[id];
|
||||||
|
if (p.b.y <= 0.0) return;
|
||||||
|
if (pc.pass_ == 1) { // between ticks: only draw
|
||||||
|
ivec2 w0 = ivec2(floor(p.a.xy)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
ivec2 q = w0 + ivec2(x, y);
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = floatBitsToUint(p.b.w);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vec2 pos = p.a.xy, vel = p.a.zw;
|
||||||
|
vel.y += p.b.z;
|
||||||
|
int n = max(int(ceil(length(vel))), 1);
|
||||||
|
bool dead = false;
|
||||||
|
vec4 pl = ents[0].a, plbox = ents[0].box;
|
||||||
|
bool player_alive = (ents[0].st.w & 8) == 0 && ents[0].st.x == 3;
|
||||||
|
for (int i = 0; i < n && !dead; ++i) {
|
||||||
|
pos += vel / float(n);
|
||||||
|
if (solid_at(ivec2(floor(pos)))) { dead = true; break; }
|
||||||
|
if (p.c.x > 0.5) { // the player's bolt: hits the first living creature whose box contains it
|
||||||
|
for (int k = 1; k < ctrl.inp.w; ++k) {
|
||||||
|
if (ents[k].st.x == 0 || ents[k].st.x == 3 || (ents[k].st.w & 8) != 0) continue;
|
||||||
|
vec2 dk = pos - ents[k].a.xy;
|
||||||
|
if (dk.x >= ents[k].box.x - 1.0 && dk.x <= ents[k].box.y + 1.0 && dk.y >= ents[k].box.z - 1.0 && dk.y <= ents[k].box.w + 1.0) {
|
||||||
|
atomicAdd(ents[k].x.x, int(p.b.x * ents[k].ex9.x * 1000.0));
|
||||||
|
dead = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dead) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
vec2 d = pos - pl.xy;
|
||||||
|
if (player_alive && d.x >= plbox.x - 1.0 && d.x <= plbox.y + 1.0 && d.y >= plbox.z - 1.0 && d.y <= plbox.w + 1.0) {
|
||||||
|
atomicAdd(ents[0].x.x, int(p.b.x * ents[0].ex9.x * 1000.0));
|
||||||
|
dead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float life = dead ? 0.0 : p.b.y - 1.0;
|
||||||
|
projs[id].a = vec4(pos, vel);
|
||||||
|
projs[id].b = vec4(p.b.x, life, p.b.z, p.b.w);
|
||||||
|
if (life > 0.0) { // draw
|
||||||
|
uint col = floatBitsToUint(p.b.w);
|
||||||
|
ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
ivec2 q = w + ivec2(x, y);
|
||||||
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Material reactions (liquids mixing, acid, water on lava, ...). The grid is cut into disjoint pairs of neighbouring cells; pc.pass_
|
||||||
|
// (0..3) selects the pairing: 0/1 horizontal pairs starting at even/odd columns, 2/3 vertical pairs. Both cells of a pair evaluate the
|
||||||
|
// same reaction (symmetric hash) and each applies its own side, so no cell is written twice. Four passes visit every adjacency once per frame.
|
||||||
|
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; }; // [a * n + b] = (offset << 8) | count
|
||||||
|
layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64]; } ctrl; // reactEv: entities to spawn (x, y, entity index + 1)
|
||||||
|
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; }; // 5 words per entry: outs (a | b << 16, 0xFFFF = same), probability / 65536
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
int ph = pc.pass_ & 3;
|
||||||
|
ivec2 q;
|
||||||
|
if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0);
|
||||||
|
else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||||
|
int i = p.y * pc.gw + p.x, j = q.y * pc.gw + q.x;
|
||||||
|
uint ci = cells[i], cj = cells[j];
|
||||||
|
uint mi = cell_mat(ci), mj = cell_mat(cj);
|
||||||
|
if (mi == 0u && mj == 0u) return;
|
||||||
|
uint n = mats[mats.length() - 1u].w;
|
||||||
|
uint h = rhead[mi * n + mj];
|
||||||
|
uint cnt = h & 255u;
|
||||||
|
if (cnt == 0u) return;
|
||||||
|
uint lo = uint(min(i, j)), hi = uint(max(i, j));
|
||||||
|
uint rr = hash3(lo, hi, uint(pc.frame * 4 + ph) ^ 0x51u);
|
||||||
|
uint k = 0u; // the record of the pair table: R % count with the roll R that also gates the attempt (0070eaf0)
|
||||||
|
// 0070eaf0: every cell that has a reaction threshold (CellData + 0x22c: the largest probability of a reaction it is the first input of; air is no cell and never starts one) tries once per
|
||||||
|
// update with ONE random neighbour of four, rolls R (0..10000) against min(that threshold, the record's probability) and the record must accept the direction: the pair gets the chance of
|
||||||
|
// each of its two cells separately (rec / 4 for a material next to air, not rec / 2).
|
||||||
|
{
|
||||||
|
const bool iLo = i < j;
|
||||||
|
const uint mA = iLo ? mi : mj, mB = iLo ? mj : mi;
|
||||||
|
const ivec2 pA = iLo ? p : q, pB = iLo ? q : p;
|
||||||
|
const uint hA = rhead[mA * n + mB], hB = rhead[mB * n + mA];
|
||||||
|
const uint eA = ((hA >> 8) + k % max(hA & 255u, 1u)) * 5u, eB = ((hB >> 8) + k % max(hB & 255u, 1u)) * 5u;
|
||||||
|
const uint pwA = rrec[eA + 1u], pwB = rrec[eB + 1u];
|
||||||
|
const ivec2 dAB = pB - pA;
|
||||||
|
const uint relA = dAB.y < 0 ? 1u : dAB.y > 0 ? 2u : dAB.x < 0 ? 3u : 4u, relB = relA == 1u ? 2u : relA == 2u ? 1u : relA == 3u ? 4u : 3u;
|
||||||
|
const uint dirA = (pwA >> 16) & 7u, dirB = (pwB >> 16) & 7u;
|
||||||
|
// each cell has its own roll R = next15 % 10001: the material threshold (CellData + 0x22c, positive) and the record's threshold both accept R <= threshold, the record is R % count
|
||||||
|
const uint RA = (hash(rr ^ 0xA2u) & 0x7FFFu) % 10001u, RB = (hash(rr ^ 0xB2u) & 0x7FFFu) % 10001u;
|
||||||
|
const uint kA = RA % max(hA & 255u, 1u), kB = RB % max(hB & 255u, 1u);
|
||||||
|
const uint eA2 = ((hA >> 8) + kA) * 5u, eB2 = ((hB >> 8) + kB) * 5u;
|
||||||
|
const uint matA = rhead[n * n + mA], matB = rhead[n * n + mB];
|
||||||
|
const bool fireA = mA != 0u && (hA & 255u) != 0u && matA > 0u && RA <= matA && RA <= (rrec[eA2 + 1u] & 0xFFFFu) && (((rrec[eA2 + 1u] >> 16) & 7u) == 0u || ((rrec[eA2 + 1u] >> 16) & 7u) == relA) && (hash(rr ^ 0xA1u) & 3u) == 0u;
|
||||||
|
const bool fireB = mB != 0u && (hB & 255u) != 0u && matB > 0u && RB <= matB && RB <= (rrec[eB2 + 1u] & 0xFFFFu) && (((rrec[eB2 + 1u] >> 16) & 7u) == 0u || ((rrec[eB2 + 1u] >> 16) & 7u) == relB) && (hash(rr ^ 0xB1u) & 3u) == 0u;
|
||||||
|
if (!fireA && !fireB) return;
|
||||||
|
k = fireA ? kA : kB;
|
||||||
|
}
|
||||||
|
uint e = ((h >> 8) + k % cnt) * 5u;
|
||||||
|
uint outs = rrec[e];
|
||||||
|
uint third = rrec[e + 2u];
|
||||||
|
uint blob = rrec[e + 3u];
|
||||||
|
uint entityIdx = rrec[e + 4u] & 0x7FFFFFFFu;
|
||||||
|
bool second = (mi == mj) && (i > j); // equal materials: the lower cell takes the first output
|
||||||
|
uint o = second ? (outs >> 16) : (outs & 0xFFFFu);
|
||||||
|
const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u; // (the record is the mirrored one when the first input is the second material of the pair)
|
||||||
|
const uint reqLife = blob >> 20;
|
||||||
|
if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return; // req_lifetime: the first cell must be old enough
|
||||||
|
if (((blob >> 19) & 1u) != 0u && firstIsI0 && !second) { // destroy_horizontally_lonely_pixels: the first output becomes air when both horizontal neighbours are empty (both probed)
|
||||||
|
const bool le = p.x > 0 && cell_mat(cells[i - 1]) == 0u, ri = p.x + 1 < pc.gw && cell_mat(cells[i + 1]) == 0u;
|
||||||
|
if (le && ri) o = 0u;
|
||||||
|
}
|
||||||
|
// three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side)
|
||||||
|
// must hold the third material; the thread of the first cell writes its output
|
||||||
|
if ((third & 0xFFFFu) != 0xFFFFu) {
|
||||||
|
const bool firstIsI = firstIsI0;
|
||||||
|
const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p;
|
||||||
|
const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
|
||||||
|
ivec2 p3 = pf + dirs[(rr >> 20) & 3u];
|
||||||
|
if (p3 == ps) p3 = 2 * pf - ps;
|
||||||
|
if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return;
|
||||||
|
const int k3 = p3.y * pc.gw + p3.x;
|
||||||
|
const uint c3 = cells[k3];
|
||||||
|
if (cell_mat(c3) != (third & 0xFFFFu)) return;
|
||||||
|
const uint o3 = (third >> 16) & 0xFFFFu;
|
||||||
|
if (firstIsI && o3 != 0xFFFFu && o3 != cell_mat(c3)) { atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)); birth[k3] = birth_frame(); }
|
||||||
|
}
|
||||||
|
if (o == 0xFFFFu || o == mi) return; // this side stays as it is
|
||||||
|
cells[i] = o | (ci & 0x00FF0000u); // keeps the colour variation, clears burning and direction bits
|
||||||
|
ext[i] = vec2(0.0);
|
||||||
|
birth[i] = birth_frame();
|
||||||
|
if (entityIdx != 0u && firstIsI0 && !second) { // the reaction loads an entity at the position of the first input (the host spawns it)
|
||||||
|
const int ei = atomicAdd(ctrl.reactN.x, 1);
|
||||||
|
if (ei < 64) ctrl.reactEv[ei] = ivec4(p.x + pc.view_x, p.y + pc.view_y, int(entityIdx), 0);
|
||||||
|
else atomicAdd(ctrl.reactN.x, -1);
|
||||||
|
}
|
||||||
|
// blob_radius with blob_restrict_to_input_material (0071ab50 conditional mode): trunc(r * 2 pi + 0.5) rays of unit steps from the cell; a cell of the input material converts,
|
||||||
|
// one that already holds the output lets the ray go on, anything else ends it (a flood of the connected input material up to the radius)
|
||||||
|
{
|
||||||
|
const uint rad = second ? ((blob >> 8) & 255u) : (blob & 255u);
|
||||||
|
const bool restricted = ((blob >> (second ? 17 : 16)) & 1u) != 0u;
|
||||||
|
if (rad > 0u && restricted) {
|
||||||
|
const int nr = int(float(rad) * 6.2831853 + 0.5);
|
||||||
|
for (int ray = 0; ray < nr; ++ray) {
|
||||||
|
const float a = float(ray) * 6.2831853 / float(nr);
|
||||||
|
const vec2 dir = vec2(cos(a), sin(a));
|
||||||
|
for (int st = 1; st <= int(rad); ++st) {
|
||||||
|
const ivec2 t = p + ivec2(trunc(dir * float(st)));
|
||||||
|
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) break;
|
||||||
|
const int ti = t.y * pc.gw + t.x;
|
||||||
|
const uint tc = cells[ti], tm = cell_mat(tc);
|
||||||
|
if (tm == o) continue;
|
||||||
|
if (tm != mi) break;
|
||||||
|
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } // a converted cell starts afresh (0070d0b0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (rad > 0u) { // direct mode (0071ab50, restriction explicitly off): every loaded cell of the disk dx^2 + dy^2 < r^2 takes the output, whatever it holds
|
||||||
|
const int ri = int(rad);
|
||||||
|
for (int dy = -ri; dy <= ri; ++dy)
|
||||||
|
for (int dx = -ri; dx <= ri; ++dx) {
|
||||||
|
if (dx * dx + dy * dy >= ri * ri) continue;
|
||||||
|
const ivec2 t = p + ivec2(dx, dy);
|
||||||
|
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue;
|
||||||
|
const int ti = t.y * pc.gw + t.x;
|
||||||
|
const uint tc = cells[ti];
|
||||||
|
if (cell_mat(tc) == o) continue;
|
||||||
|
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "areas.glsl"
|
||||||
|
|
||||||
|
vec4 unpack_argb(uint c) {
|
||||||
|
return vec4(float((c >> 16) & 0xFFu), float((c >> 8) & 0xFFu), float(c & 0xFFu), float(c >> 24)) / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 13) readonly buffer TexData { uint texdata[]; }; // ARGB texels of all material textures
|
||||||
|
layout(std430, set = 0, binding = 14) readonly buffer TexInfo { uvec4 texinfo[]; }; // per material: offset, width, height, 0 = none
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 12) readonly buffer SceneMat { uint scene_mat[]; }; // pixel scenes: material + 1
|
||||||
|
layout(std430, set = 0, binding = 15) readonly buffer BgTile { uint bgtile[]; }; // biome background tile (ARGB)
|
||||||
|
layout(std430, set = 0, binding = 16) readonly buffer SceneVis { uint scene_vis[]; }; // scene visual colours (ARGB)
|
||||||
|
layout(std430, set = 0, binding = 17) readonly buffer SceneBg { uint scene_bg[]; }; // scene backgrounds (ARGB)
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint perm[]; }; // [0,512) simplex: the gradient of the procedural biomes decides where the background wall is
|
||||||
|
int ffloor(float v) { return int(floor(v)); }
|
||||||
|
uint ps(uint i) { return perm[i]; }
|
||||||
|
#include "procgrad.glsl"
|
||||||
|
layout(std430, set = 0, binding = 21) readonly buffer EntLayer { uint entlayer[]; };
|
||||||
|
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
|
||||||
|
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // positions before the last tick // entity sprites drawn this frame (ARGB per window cell)
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 26) readonly buffer Overlay { uint overlay[]; }; // CPU-drawn HUD (ARGB, output pixels)
|
||||||
|
struct FLight { vec4 a; vec4 b; }; // x, y, radius (world cells), strength / rgb
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
|
||||||
|
layout(std430, set = 0, binding = 38) readonly buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; // lights of projectiles and explosions this frame
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 44) readonly buffer Sky { uint sky[]; }; // src/world/sky.h
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 47) readonly buffer GlowA { vec4 glowA[]; }; // the blurred glow of the materials at half resolution (glow.comp)
|
||||||
|
layout(std430, set = 0, binding = 49) readonly buffer Fog { uint fog[]; };
|
||||||
|
layout(std430, set = 0, binding = 50) readonly buffer Vis { vec4 fogvis[]; }; // line-of-sight visibility per 4x4 block of the window (fog.comp) // the explored map, one byte per 8x8 world cells (fog.comp)
|
||||||
|
layout(std430, set = 0, binding = 57) readonly buffer LitCells { uvec2 litc[]; }; // the light of the entities per cell (light_cells.comp sums lent once per cell)
|
||||||
|
layout(std430, set = 0, binding = 45) readonly buffer Lit { uvec4 lhead; vec4 lent[]; }; // the lights near the window: count, then (x, y, radius, strength), (r, g, b, 0) per light
|
||||||
|
|
||||||
|
uvec4 sky_img(int i) { return uvec4(sky[4 + 3 * i], sky[5 + 3 * i], sky[6 + 3 * i], 0u); }
|
||||||
|
vec4 sky_texel(int i, int x, int y) { // ARGB texel of image i (wrapping in x, clamped in y; transparent outside)
|
||||||
|
uvec4 h = sky_img(i);
|
||||||
|
if (y < 0 || y >= int(h.z)) return vec4(0.0);
|
||||||
|
x = ((x % int(h.y)) + int(h.y)) % int(h.y);
|
||||||
|
return unpack_argb(sky[h.x + uint(y) * h.y + uint(x)]);
|
||||||
|
}
|
||||||
|
vec3 sky_table(float tod, int row) { // parallax_colors.bmp: the colour of a row over the day (x = time of day)
|
||||||
|
return sky_texel(0, int(fract(tod) * 511.0), row).rgb;
|
||||||
|
}
|
||||||
|
// The sky behind the terrain (the parallax of 00888c80 / 008897e0 / 00889cb0, docs/BACKLOG.md 22): the colour of layer k is row 20 k of parallax_colors.bmp at
|
||||||
|
// x = time of day (lerped); the images are masks tinted by it. Layer 0 (weather_background, white) fills the screen with row 0; 1 (parallax_background x row
|
||||||
|
// 20, alpha gradient_sky_alpha) and 2 (parallax_sunset_line x row 40, alpha the sunset alpha) are repeated from the top of the screen; then in the order 3, 10, 7,
|
||||||
|
// 4, 9, 6, 5, 8 the image of layer k repeats horizontally from x = -(camera x / depth) + rec.x, its top at y = camera y / depth - rec.y - parallax_offset_y
|
||||||
|
// (virtual pixels from the top of the screen, at least screen height - image height), tinted by its row and its alpha. Depths 50, 30, 25, 25, 40, 20, 35, 45
|
||||||
|
// (layers 3..10); rec.x / rec.y / alphas are the host's (sky words 48..: cloud 1 x, cloud 1 y, cloud 2 y, mountains 1 y, mountains 2 y, storm y, storm alpha,
|
||||||
|
// gradient alpha, sunset alpha, parallax_offset_y). (The rain particles, layer 11, are the GameEmitRainParticles of init.lua.)
|
||||||
|
vec4 sky_tex(int i, vec2 t) { // image i at texel t, repeating in x, transparent outside in y
|
||||||
|
const uvec4 h = sky_img(i);
|
||||||
|
if (h.y == 0u || t.y < 0.0 || t.y >= float(h.z)) return vec4(0.0);
|
||||||
|
const int x = ((int(floor(t.x)) % int(h.y)) + int(h.y)) % int(h.y);
|
||||||
|
return unpack_argb(sky[h.x + uint(int(t.y)) * h.y + uint(x)]);
|
||||||
|
}
|
||||||
|
vec4 sky_tex_wrap(int i, vec2 t) { // repeating in both directions
|
||||||
|
const uvec4 h = sky_img(i);
|
||||||
|
if (h.y == 0u || h.z == 0u) return vec4(0.0);
|
||||||
|
return sky_tex(i, vec2(t.x, float(((int(floor(t.y)) % int(h.z)) + int(h.z)) % int(h.z))));
|
||||||
|
}
|
||||||
|
vec3 sky_row(float tod, int row) { // 008897e0: index (w - 1) x time, lerped to the next column
|
||||||
|
const uvec4 h = sky_img(0);
|
||||||
|
const float f = (float(h.y) - 1.0) * fract(tod);
|
||||||
|
const int i0 = int(floor(f));
|
||||||
|
const vec3 a = sky_texel(0, i0, row).rgb, b = sky_texel(0, min(i0 + 1, int(h.y) - 1), row).rgb;
|
||||||
|
return mix(a, b, f - float(i0));
|
||||||
|
}
|
||||||
|
float skp(int k) { return uintBitsToFloat(sky[48u + uint(k)]); }
|
||||||
|
// TODO(to-do #18): layer 0 shader variant, the biome source of gradient / sunset alpha, the camera origin
|
||||||
|
vec3 sky_colour(vec2 w, vec2 cam, float tod, float tsec, vec2 scr) {
|
||||||
|
const vec2 camTL = cam - vec2(213.5, 121.0); // (the virtual screen: 427 x 242 virtual pixels, 1 per world pixel)
|
||||||
|
const vec2 sv = w - camTL;
|
||||||
|
vec3 col = sky_row(tod, 0);
|
||||||
|
{ const float ga = skp(7);
|
||||||
|
if (ga > 0.0) { const vec4 c = sky_tex_wrap(1, sv); col = mix(col, c.rgb * sky_row(tod, 20), c.a * ga); } }
|
||||||
|
{ const float sa = skp(8);
|
||||||
|
if (sa > 0.0) { const vec4 c = sky_tex_wrap(2, sv); col = mix(col, c.rgb * sky_row(tod, 40), c.a * sa); } }
|
||||||
|
const int order[8] = int[8](3, 10, 7, 4, 9, 6, 5, 8);
|
||||||
|
const float depth[11] = float[11](1.0, 1.0, 1.0, 50.0, 30.0, 25.0, 25.0, 40.0, 20.0, 35.0, 45.0);
|
||||||
|
for (int o = 0; o < 8; ++o) {
|
||||||
|
const int k = order[o];
|
||||||
|
float rx = 0.0, ry = 0.0, a = 1.0;
|
||||||
|
if (k == 3) { rx = skp(0); ry = skp(1); }
|
||||||
|
else if (k == 4) ry = skp(2);
|
||||||
|
else if (k == 5 || k == 6) ry = skp(3);
|
||||||
|
else if (k == 7) ry = skp(4);
|
||||||
|
else { ry = skp(5); a = skp(6); }
|
||||||
|
if (a <= 0.0) continue;
|
||||||
|
const uvec4 h = sky_img(k);
|
||||||
|
const float top = max(camTL.y / depth[k] - ry - skp(9), 242.0 - float(h.z));
|
||||||
|
const vec4 c = sky_tex(k, vec2(sv.x - camTL.x / depth[k] + rx, sv.y - top));
|
||||||
|
if (c.a > 0.0) col = mix(col, c.rgb * sky_row(tod, 20 * k), c.a * a);
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lighting of the foreground (cells and sprites), after post_final.frag: the lights of the entities (LightComponent, cubic falloff), the glow of the materials, the light of
|
||||||
|
// the sky (the sky exposure of the cell: open sky, the depth below the first solid cell, the sides of an opening; times the colour of the time of day), gamma, then the
|
||||||
|
// fog of war (unexplored cells are black unless the sky lights them) and the baseline 0.35 - sky ambient of the explored map. The background (sky, cave walls) is not lit.
|
||||||
|
// sky[3]: bit 0 lighting on (F4), bit 2 fog of war off (F6), bit 3 glow off (F7).
|
||||||
|
const int FOG_TW = 1152, FOG_TH = 896, FOG_OX = 18432, FOG_OY = 14336; // the fog map of fog.comp: 32 x 32 pixel texels from the world origin, the clear amount (255 - fog)
|
||||||
|
float fog_clear(ivec2 t) {
|
||||||
|
if (t.x < 0 || t.y < 0 || t.x >= FOG_TW || t.y >= FOG_TH) return 0.0;
|
||||||
|
return float(min(fog[t.y * FOG_TW + t.x], 255u)) / 255.0;
|
||||||
|
}
|
||||||
|
vec2 gCpf = vec2(0.0); // window position of the pixel in cells, at sub-cell precision
|
||||||
|
float bm_dust_r(vec2 w) { // the biome modifier dust_amount of the map tile under w (biome_mods.glsl's table, read through perm)
|
||||||
|
const uint n = mats[mats.length() - 1u].w;
|
||||||
|
const uint fb = 1024u + 9u * n + 132u + n;
|
||||||
|
const int bw = int(perm[fb]), bh = int(perm[fb + 1u]);
|
||||||
|
const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14);
|
||||||
|
if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return 0.0;
|
||||||
|
return uintBitsToFloat(perm[fb + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u + 4u * uint(t.y * bw + t.x) + 3u]);
|
||||||
|
}
|
||||||
|
float perlin_tex(vec2 uv) { // tex_perlin_noise (data/shaders/noise_perlin.png, sky image 11): bilinear, repeating; uv in texture units (1 = the image)
|
||||||
|
const uvec4 h = sky_img(11);
|
||||||
|
if (h.y == 0u || h.z == 0u) return 0.5;
|
||||||
|
const vec2 q = uv * vec2(float(h.y), float(h.z)) - 0.5;
|
||||||
|
const ivec2 e = ivec2(floor(q));
|
||||||
|
const vec2 g = q - vec2(e);
|
||||||
|
const int W = int(h.y), H = int(h.z);
|
||||||
|
float t[4];
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const int x = (((e.x + (k & 1)) % W) + W) % W, y = (((e.y + (k >> 1)) % H) + H) % H;
|
||||||
|
t[k] = float((sky[h.x + uint(y) * h.y + uint(x)] >> 16) & 255u) / 255.0;
|
||||||
|
}
|
||||||
|
return mix(mix(t[0], t[1], g.x), mix(t[2], t[3], g.x), g.y);
|
||||||
|
}
|
||||||
|
float sky_cell(ivec2 b) { // one block of the exposure grid (sky words: [7] = its offset, then the first block column / row, then the values)
|
||||||
|
const uint o = sky[60];
|
||||||
|
const ivec2 b0 = ivec2(int(sky[o]), int(sky[o + 1u]));
|
||||||
|
const ivec2 q = clamp(b - b0, ivec2(0), ivec2(1024 / 64 + 1, 576 / 64 + 1));
|
||||||
|
return uintBitsToFloat(sky[o + 2u + uint(q.y * (1024 / 64 + 2) + q.x)]);
|
||||||
|
}
|
||||||
|
float sky_bilinear(vec2 w) { // texel centres at the block centres
|
||||||
|
const vec2 q = w / 64.0 - 0.5;
|
||||||
|
const ivec2 e = ivec2(floor(q));
|
||||||
|
const vec2 g = q - vec2(e);
|
||||||
|
return mix(mix(sky_cell(e), sky_cell(e + ivec2(1, 0)), g.x), mix(sky_cell(e + ivec2(0, 1)), sky_cell(e + ivec2(1, 1)), g.x), g.y);
|
||||||
|
}
|
||||||
|
float sky_amount(vec2 w) { // post_final.frag HIQ: (centre + 0.25 x the four neighbours) x 0.5
|
||||||
|
return (sky_bilinear(w) + 0.25 * (sky_bilinear(w - vec2(64.0, 0.0)) + sky_bilinear(w + vec2(64.0, 0.0)) + sky_bilinear(w - vec2(0.0, 64.0)) + sky_bilinear(w + vec2(0.0, 64.0)))) * 0.5;
|
||||||
|
}
|
||||||
|
float fog_bilinear(vec2 w) { // the fog map between the texel centres (32 n + 16), as a linear-filtered tex_fog
|
||||||
|
const vec2 q = w / 32.0 - 0.5;
|
||||||
|
const ivec2 e = ivec2(floor(q));
|
||||||
|
const vec2 g = q - vec2(e);
|
||||||
|
return mix(mix(fog_clear(e), fog_clear(e + ivec2(1, 0)), g.x), mix(fog_clear(e + ivec2(0, 1)), fog_clear(e + ivec2(1, 1)), g.x), g.y);
|
||||||
|
}
|
||||||
|
float explored(ivec2 cp, ivec2 wp) { // 0 fog .. 1 clear: the fog map read bilinearly between the texel centres (32 n + 16), as post_final.frag samples tex_fog
|
||||||
|
if ((sky[3] & 4u) != 0u) return 1.0;
|
||||||
|
const vec2 ww = gCpf + vec2(float(pc.view_x), float(pc.view_y));
|
||||||
|
const vec2 w = ww + vec2(FOG_OX, FOG_OY);
|
||||||
|
// post_final.frag HIQ: the centre plus four samples at +-0.25 texel diagonally and four at 0.75 texel on the axes, / 9 (tex_fog: FOG_TEX_SIZE 64 at zoom 1, 32 px texels)
|
||||||
|
const float clr = (fog_bilinear(w) + fog_bilinear(w + vec2(8.0, -8.0)) + fog_bilinear(w + vec2(-8.0, -8.0)) + fog_bilinear(w + vec2(-8.0, 8.0)) + fog_bilinear(w + vec2(8.0, 8.0))
|
||||||
|
+ fog_bilinear(w - vec2(24.0, 0.0)) + fog_bilinear(w + vec2(24.0, 0.0)) + fog_bilinear(w - vec2(0.0, 24.0)) + fog_bilinear(w + vec2(0.0, 24.0))) * 0.1111111;
|
||||||
|
// the holes of the frame (main.cpp: fog_of_war_hole sprites): post_final.frag fog x (1 - the hole alpha); the disc of explosion_flare_fog_of_war_hole.png ~ exp(-4.5 t^2)
|
||||||
|
const uint n = mats[mats.length() - 1u].w;
|
||||||
|
const uint hb = 1024u + 9u * n + 132u + n + 2u + 8192u;
|
||||||
|
const int nh = min(int(uintBitsToFloat(perm[hb])), 64);
|
||||||
|
const uint ib = hb + 1u + 64u * 6u + 1u + 16u * 3u;
|
||||||
|
float hole = 0.0;
|
||||||
|
for (int k = 0; k < nh; ++k) { // the hole sprite's alpha (its animation frame by the age), bilinear (smooth_filtering), scaled about its offset
|
||||||
|
const uint o = hb + 1u + uint(k) * 6u;
|
||||||
|
const vec2 hp = vec2(uintBitsToFloat(perm[o]), uintBitsToFloat(perm[o + 1u]));
|
||||||
|
const float sc = max(uintBitsToFloat(perm[o + 2u]), 0.01);
|
||||||
|
const int img = int(uintBitsToFloat(perm[o + 4u]));
|
||||||
|
float a = 0.0;
|
||||||
|
if (img >= 0 && img < 8) {
|
||||||
|
const uint ia = ib + uint(img) * 3272u;
|
||||||
|
const float iw = uintBitsToFloat(perm[ia]), ih = uintBitsToFloat(perm[ia + 1u]);
|
||||||
|
const vec2 sp = (ww - hp) / sc + vec2(uintBitsToFloat(perm[ia + 2u]), uintBitsToFloat(perm[ia + 3u])) - 0.5;
|
||||||
|
if (sp.x < -1.0 || sp.y < -1.0 || sp.x > iw || sp.y > ih) continue;
|
||||||
|
const uint nf = max(perm[ia + 4u] & 255u, 1u), wt = max(perm[ia + 4u] >> 8, 1u);
|
||||||
|
const uint fr = (uint(uintBitsToFloat(perm[o + 5u])) / wt) % nf;
|
||||||
|
const ivec2 s0 = ivec2(floor(sp));
|
||||||
|
const vec2 f = sp - vec2(s0);
|
||||||
|
for (int q = 0; q < 4; ++q) {
|
||||||
|
const ivec2 s = s0 + ivec2(q & 1, q >> 1);
|
||||||
|
if (s.x < 0 || s.y < 0 || s.x >= int(iw) || s.y >= int(ih)) continue;
|
||||||
|
a += float(perm[ia + 5u + fr * 1089u + uint(s.y * int(iw) + s.x)]) / 255.0 * ((q & 1) != 0 ? f.x : 1.0 - f.x) * ((q >> 1) != 0 ? f.y : 1.0 - f.y);
|
||||||
|
}
|
||||||
|
} else { // (no image: a soft disc of 16 px x the scale)
|
||||||
|
const float t = distance(ww, hp) / (16.0 * sc);
|
||||||
|
a = t < 1.0 ? exp(-4.5 * t * t) : 0.0;
|
||||||
|
}
|
||||||
|
hole = max(hole, uintBitsToFloat(perm[o + 3u]) * a);
|
||||||
|
}
|
||||||
|
return 1.0 - (1.0 - clr) * (1.0 - min(hole, 1.0));
|
||||||
|
}
|
||||||
|
#define FPAL_TABLE perm
|
||||||
|
// ---- the fire palettes of the cell blitting (main.cpp loads them after the tables; 00722490 the lookup, 00724580 the use) ----
|
||||||
|
uint fpal_word(uint k) { return FPAL_TABLE[1024u + 9u * mats[mats.length() - 1u].w + k]; }
|
||||||
|
vec3 fpal_rgb(uint w) { return vec3(float((w >> 16) & 255u), float((w >> 8) & 255u), float(w & 255u)) / 255.0; }
|
||||||
|
vec3 fire_lookup(uint at, uint row, uint w, float t) { // the row of the image at t x width: the first / last pixel outside, else the two neighbours mixed
|
||||||
|
const float x = float(w) * t;
|
||||||
|
const uint r0 = at + row * w;
|
||||||
|
if (x >= float(w)) return fpal_rgb(fpal_word(r0 + w - 1u));
|
||||||
|
if (x <= 0.0) return fpal_rgb(fpal_word(r0));
|
||||||
|
const uint i0 = uint(floor(x));
|
||||||
|
if (float(i0) == x || i0 + 1u >= w) return fpal_rgb(fpal_word(r0 + i0));
|
||||||
|
return mix(fpal_rgb(fpal_word(r0 + i0)), fpal_rgb(fpal_word(r0 + i0 + 1u)), x - float(i0));
|
||||||
|
}
|
||||||
|
uint fire_row(uint m) { return min(fpal_word(132u + m), 5u); } // CellData +0x68 fire_colors_index
|
||||||
|
float fire_age(uint b) { return float(birth_age(b)) + float(hash3(b & BIRTH_MASK, 77u, 5u) % 21u); } // 00709ef0: the flame's frame is the creation frame - uniform(0, 20)
|
||||||
|
vec3 fire_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(0u, fire_row(m), 8u, a * a); } // colors_fire.png at (age / 40)^2
|
||||||
|
vec3 fire_glow_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(48u, fire_row(m), 8u, fire_age(b) < 15.0 ? a : a * a); } // colors_fire_glow.png (young: age / 40)
|
||||||
|
vec3 burning_colour(uint m, uint h) { return fpal_rgb(fpal_word(96u + fire_row(m) * 6u + h % 6u)); } // colors_lava.bmp: a random pixel of the row
|
||||||
|
vec3 glow_at(ivec2 cp) { // bilinear in the half-resolution glow
|
||||||
|
if ((sky[3] & 8u) != 0u) return vec3(0.0);
|
||||||
|
const int hw = (pc.gw + 1) / 2, hh = (pc.gh + 1) / 2;
|
||||||
|
const vec2 q = (vec2(cp) + 0.5) * 0.5 - 0.5;
|
||||||
|
const ivec2 q0 = ivec2(floor(q));
|
||||||
|
const vec2 f = q - vec2(q0);
|
||||||
|
vec3 r = vec3(0.0);
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const ivec2 s = clamp(q0 + ivec2(k & 1, k >> 1), ivec2(0), ivec2(hw - 1, hh - 1));
|
||||||
|
r += glowA[s.y * hw + s.x].rgb * (((k & 1) != 0 ? f.x : 1.0 - f.x) * ((k >> 1) != 0 ? f.y : 1.0 - f.y));
|
||||||
|
}
|
||||||
|
return max(vec3(0.0), r - 0.008); // (post_final.frag)
|
||||||
|
}
|
||||||
|
float gFog = 1.0;
|
||||||
|
float gSky = 0.0, gLum = 0.0; // (the squared sky amount and the luminosity of the lights of the last lighting() call: the dust)
|
||||||
|
vec3 gGlow = vec3(0.0);
|
||||||
|
vec3 lighting(ivec2 cp, ivec2 wp, uint emissive) {
|
||||||
|
gFog = 1.0; gGlow = vec3(0.0); gSky = 1.0; gLum = 1.0;
|
||||||
|
if ((sky[3] & 1u) == 0u) return vec3(1.0);
|
||||||
|
gGlow = glow_at(cp);
|
||||||
|
if (emissive != 0u) { gFog = explored(cp, wp); return vec3(1.0); }
|
||||||
|
const uvec2 lc = litc[cp.y * pc.gw + cp.x]; // (the sum over the lights near the window, light_cells.comp: a cell's light is the same for all its pixels)
|
||||||
|
vec3 L = vec3(unpackHalf2x16(lc.x), unpackHalf2x16(lc.y).x);
|
||||||
|
// post_final.frag: lights = pow(tex_lights x 0.8, 1.5) + glow; the sky: sky_light_color x sky_ambient_amount, the exposure grid (tex_skylight, 64-pixel blocks of the
|
||||||
|
// coarse map, main.cpp) read bilinearly with the HIQ blur (0.5 centre + 0.125 x the four neighbours one block away); lights = max(lights, sky), at most 1, gamma 1 / 2.2;
|
||||||
|
// then the fog of war: s = the sky amount faded out between y 250 and 350, fog = min(1, max(2 x 1.4 (0.6, 0.5, 0.45) max(0, 1 - fog - sqrt(s)), s)),
|
||||||
|
// lights x fog + max(0.35 - s, 0) x fog
|
||||||
|
vec3 lights = pow(max(L * 0.8, vec3(0.0)), vec3(1.5)) + gGlow;
|
||||||
|
float amount = sky_amount(vec2(wp) + 0.5);
|
||||||
|
amount *= amount; // (post_final.frag: sky_ambient_amount *= sky_ambient_amount after the sample)
|
||||||
|
const float tod = uintBitsToFloat(sky[0]);
|
||||||
|
const vec3 topc = sky_table(tod, 4);
|
||||||
|
const float f = smoothstep(0.12, 0.5, dot(topc, vec3(0.299, 0.587, 0.114)));
|
||||||
|
// TODO(to-do #17): sky_light_color from the PostFx keyframes (00890330)
|
||||||
|
const vec3 skyc = mix(vec3(0.12, 0.16, 0.30), vec3(1.0), f); // (sky_light_color: the engine's day keyframes are not traced yet; this follows the sky's brightness)
|
||||||
|
lights = min(max(lights, skyc * amount), vec3(1.0));
|
||||||
|
lights = pow(lights, vec3(1.0 / 2.2));
|
||||||
|
const float vis = explored(cp, wp);
|
||||||
|
const float s = amount * (1.0 - clamp((float(wp.y) - 250.0) / 100.0, 0.0, 1.0));
|
||||||
|
vec3 fog = 1.4 * vec3(0.6, 0.5, 0.45) * max(0.0, 1.0 - (1.0 - vis) - sqrt(s));
|
||||||
|
fog = min(vec3(1.0), max(2.0 * fog, vec3(s)));
|
||||||
|
L = lights * fog + max(0.35 - s, 0.0) * fog;
|
||||||
|
gSky = amount; gLum = sqrt(min(1.0, dot(L, vec3(0.299, 0.587, 0.114))));
|
||||||
|
gFog = max(fog.r, max(fog.g, fog.b));
|
||||||
|
return min(L, vec3(1.35));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The picture is made at the game's own resolution, one pixel per world cell, then scaled up to the window (Noita draws its world at a fixed internal
|
||||||
|
// resolution: the window size changes only the final scaling). pass_ 1, the world pass: one thread per cell of the view, the colour of the cell (background,
|
||||||
|
// material, lights, fog of war, entity layer, projectile lights, grading, glow, blindness) into wcell. pass_ 0, the output pass: one thread per window pixel,
|
||||||
|
// the cell under it (camera shake and the camera between two ticks at sub-cell precision), the player at its sub-cell position, the low-health vignette, the HUD.
|
||||||
|
layout(std430, set = 0, binding = 60) writeonly buffer ClipRaw { uint clipraw[]; }; // the window picture without the HUD (the replay recorder)
|
||||||
|
layout(std430, set = 0, binding = 59) buffer WorldCells { uvec4 wcell[]; }; // per cell: the colour (rgba8), the light Lv (half r g, half b + the fog amount)
|
||||||
|
float gScreenY = 0.0;
|
||||||
|
|
||||||
|
vec3 world_cell(ivec2 cp, out vec3 Lv) {
|
||||||
|
uint c = cells[cp.y * pc.gw + cp.x];
|
||||||
|
|
||||||
|
float t = gScreenY; // (the screen row of the cell centre, for the background gradient)
|
||||||
|
vec3 bg = mix(vec3(0.05, 0.05, 0.09), vec3(0.02, 0.02, 0.04), t);
|
||||||
|
|
||||||
|
ivec2 wp0 = cp + ivec2(pc.view_x, pc.view_y); // world cell
|
||||||
|
const int bai = area_of_cell(wp0);
|
||||||
|
bool bgOk = true, bgDrawn = false;
|
||||||
|
{ // a procedural (surface) biome has no background wall: the sky and the surface caves of the hills show none
|
||||||
|
const uint trec = topo_rec(wp0);
|
||||||
|
if (trec != 0xFFFFFFFFu && (atab[trec] & 1u) != 0u) bgOk = false;
|
||||||
|
}
|
||||||
|
if (bgOk && bai >= 0 && arec(bai, 12) > 0) {
|
||||||
|
// the biome's background wall, tiled in world space
|
||||||
|
const int bw = arec(bai, 12), bh = arec(bai, 13);
|
||||||
|
uint tx = uint(((wp0.x % bw) + bw) % bw), ty = uint(((wp0.y % bh) + bh) % bh);
|
||||||
|
vec4 bc = unpack_argb(bgtile[uint(arec(bai, 11)) + ty * uint(bw) + tx]);
|
||||||
|
bg = mix(bg, bc.rgb, bc.a);
|
||||||
|
}
|
||||||
|
uint sidx = 0xFFFFFFFFu;
|
||||||
|
for (int a = 0; a < int(atab[0]); ++a) { // the first area whose scene has a material here; the first scene background
|
||||||
|
int si = scene_index(a, wp0);
|
||||||
|
if (si < 0) continue;
|
||||||
|
if (sidx == 0xFFFFFFFFu && scene_mat[si] != 0u) sidx = uint(si);
|
||||||
|
uint sb = scene_bg[si];
|
||||||
|
if (sb != 0u) { vec4 bc = unpack_argb(sb); bg = mix(bg, bc.rgb, bc.a); bgDrawn = bgDrawn || bc.a > 0.01; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint trecSky = topo_rec(wp0);
|
||||||
|
bool skyPix = false;
|
||||||
|
if (sky[2] != 0u && trecSky != 0xFFFFFFFFu && (atab[trecSky] & 0x1000000u) != 0u && !bgDrawn && ((atab[trecSky] & 0x2000000u) == 0u || cp.y < int(sky[64 + cp.x])) && cell_mat(c) == 0u) { // (the scene biomes show the sky wherever no scene background is drawn; the procedural ones (hills) have caves with no wall behind: there the column must be open) // open sky: no wall behind, nothing solid above in the window
|
||||||
|
const vec2 cam = vec2(float(pc.view_x) + float(pc.gw) * 0.5, float(pc.view_y) + float(pc.gh) * 0.5);
|
||||||
|
skyPix = true;
|
||||||
|
bg = sky_colour(vec2(wp0), cam, uintBitsToFloat(sky[0]), uintBitsToFloat(sky[1]), vec2(0.0, float(wp0.y - pc.view_y) - float(pc.gh) * 0.5));
|
||||||
|
}
|
||||||
|
if ((sky[3] & 2u) != 0u && cell_mat(c) == 0u) { // F5: the sky mask: green = the sky is drawn, red = a solid cell above in the window column (the row of that cell is yellow), blue = not an outdoor biome, magenta = a scene background is drawn
|
||||||
|
const int top = int(sky[64 + cp.x]);
|
||||||
|
vec3 dbg = vec3(0.0);
|
||||||
|
if (!(trecSky != 0xFFFFFFFFu && (atab[trecSky] & 0x1000000u) != 0u)) dbg = vec3(0.0, 0.0, 0.8);
|
||||||
|
else if (bgDrawn) dbg = vec3(0.8, 0.0, 0.8);
|
||||||
|
else if (wp0.y >= 150) dbg = vec3(0.8, 0.8, 0.0);
|
||||||
|
else if ((atab[trecSky] & 0x2000000u) != 0u && cp.y >= top) dbg = vec3(0.8, 0.0, 0.0);
|
||||||
|
else dbg = vec3(0.0, 0.7, 0.0);
|
||||||
|
bg = mix(bg, dbg, 0.5);
|
||||||
|
}
|
||||||
|
{ // the weather fog (post_final.frag): the background mixes toward FOG_BACKGROUND 0.7 (FOG_BACKGROUND_NIGHT 0.2 by night_amount) by fog_amount_background =
|
||||||
|
// WorldStateComponent.fog (00c42d30 eases it toward max(fog_target, fog_target_extra); init.lua's weather sets fog_target_extra 0.3 .. 0.85 when it rains or snows)
|
||||||
|
const float wfog = uintBitsToFloat(sky[61]);
|
||||||
|
if (wfog > 0.0) {
|
||||||
|
const float tod = uintBitsToFloat(sky[0]);
|
||||||
|
const float night = clamp((0.55 - abs(fract(tod) - 0.57) - 0.4) * 10.0, 0.0, 1.0);
|
||||||
|
bg = mix(bg, vec3(mix(0.7, 0.2, night)), wfog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sky[63] != 0u && !skyPix) bg = vec3(0.022, 0.02, 0.028); // the spell test room: a dark background
|
||||||
|
vec3 rgb = bg;
|
||||||
|
uint m = cell_mat(c);
|
||||||
|
gCpf = vec2(cp) + 0.5;
|
||||||
|
Lv = lighting(cp, wp0, m != 0u && ((mats[3u * m + 2u].z >> 17) & 1u) != 0u ? 1u : 0u); // fire and lava light themselves
|
||||||
|
rgb = skyPix ? bg : bg * gFog; // (the fog of war darkens the background too; the sky lights it)
|
||||||
|
if (m != 0u) {
|
||||||
|
vec4 col = unpack_argb(mats[3u * m].x);
|
||||||
|
float shade = 1.0 + (float((c >> 16) & 0xFFu) / 255.0 - 0.5) * 0.18; // per-cell variation
|
||||||
|
uvec4 ti = texinfo[m];
|
||||||
|
if (ti.y != 0u) { // the game's material texture, tiled in world space (one texel per cell)
|
||||||
|
ivec2 wp = cp + ivec2(pc.view_x, pc.view_y);
|
||||||
|
uint tx = uint(((wp.x % int(ti.y)) + int(ti.y)) % int(ti.y));
|
||||||
|
uint ty = uint(((wp.y % int(ti.z)) + int(ti.z)) % int(ti.z));
|
||||||
|
vec4 tc = unpack_argb(texdata[ti.x + ty * ti.y + tx]);
|
||||||
|
col = vec4(tc.rgb, col.a);
|
||||||
|
shade = 1.0;
|
||||||
|
}
|
||||||
|
if ((mflags(m) & F_FIRE) != 0u) { col = vec4(fire_colour(m, birth[cp.y * pc.gw + cp.x]), 1.0); shade = 1.0; } // a flame by its age (00724580: colors_fire.png, opaque)
|
||||||
|
if (sidx != 0xFFFFFFFFu && scene_mat[sidx] == m + 1u && scene_vis[sidx] != 0u) { // pixel-scene visual image // pixel-scene visual image
|
||||||
|
vec4 vc = unpack_argb(scene_vis[sidx]);
|
||||||
|
col = vec4(vc.rgb, col.a);
|
||||||
|
shade = 1.0;
|
||||||
|
}
|
||||||
|
if ((c & 0x80000000u) != 0u && (mflags(m) & F_STATIC) != 0u) col.rgb = mix(col.rgb, vec3(0.42, 0.03, 0.03), 0.6); // stained with blood (fx_particles.comp)
|
||||||
|
if ((c & (FROZEN_BIT | BURN_BIT)) == FROZEN_BIT && (mflags(m) & F_STATIC) != 0u && (mflags(m) & F_FIRE) == 0u) col.rgb = mix(col.rgb, vec3(0.72, 0.88, 1.0), 0.55); // the frozen stain (edits.comp mode 12; the icy look is an approximation of the engine's stain colour)
|
||||||
|
if ((mflags(m) & (F_STATIC | F_LIQCELL)) == F_STATIC && ext[cp.y * pc.gw + cp.x].x >= 8.0) // a solid that burnt: 0070e060 takes one off every channel per eight of its burn age (burn.comp, ext.x), down to 0
|
||||||
|
col.rgb = max(col.rgb - vec3(floor(ext[cp.y * pc.gw + cp.x].x / 8.0) / 255.0), vec3(0.0));
|
||||||
|
if ((mflags(m) & F_STATIC) != 0u && (c & BURN_BIT) == 0u && ((c >> 28) & 7u) != 0u && max(col.r, max(col.g, col.b)) > 18.5 / 255.0) col.rgb = max(col.rgb * (1.0 - float((c >> 28) & 7u) / 7.0), vec3(19.0 / 255.0)); // burnt by explosions (0070e760: every channel x (1 - s), at least 0x13; the level in bits 28-30, explode_rays.comp)
|
||||||
|
if ((c & BURN_BIT) != 0u) { // a burning cell: a random pixel of colors_lava.bmp every frame (00724580), opaque
|
||||||
|
col = vec4(burning_colour(m, hash3(uint(cp.x + pc.view_x), uint(cp.y + pc.view_y), uint(pc.frame))), 1.0);
|
||||||
|
shade = 1.0;
|
||||||
|
}
|
||||||
|
vec3 fgc = col.rgb * shade * Lv;
|
||||||
|
{ // the dust (post_final.frag): the biome modifier dust_amount (tex_fog .g) hazes the lit foreground toward FOG_FOREGROUND 0.6 (night or underground 0.2):
|
||||||
|
// amount = mix(dust, fog_amount_foreground (the weather fog, sky word 61), sky) x max(sky, dust x luminosity x min(1, 2 perlin^2)); night_amount = clamp((0.55 - |tod - 0.57| - 0.4) x 10, 0, 1)
|
||||||
|
const vec2 wq = vec2(cp + ivec2(pc.view_x, pc.view_y));
|
||||||
|
const float dust = bm_dust_r(wq), wfog = uintBitsToFloat(sky[61]);
|
||||||
|
if (dust > 0.0 || (wfog > 0.0 && gSky > 0.0)) {
|
||||||
|
const float tod = uintBitsToFloat(sky[0]);
|
||||||
|
const float night = clamp((0.55 - abs(fract(tod) - 0.57) - 0.4) * 10.0, 0.0, 1.0);
|
||||||
|
const float pn = perlin_tex(wq * 0.0004 + vec2(0.0, uintBitsToFloat(sky[1]) * 0.005)); // noise_perlin2 = texture2D(tex_perlin_noise, world_pos * 0.0004 + (0, noise_time * 0.005))
|
||||||
|
const float amt = mix(dust, wfog, gSky) * max(gSky, dust * gLum * min(1.0, pn * pn * 2.0));
|
||||||
|
fgc = mix(fgc, vec3(mix(0.6, 0.2, max(night, 1.0 - gSky))), amt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rgb = mix(bg, fgc, col.a);
|
||||||
|
}
|
||||||
|
uint ec = entlayer[cp.y * pc.gw + cp.x];
|
||||||
|
const bool playerPix = (ec >> 24) == 254u; // the player is drawn below, at its sub-cell position (the sprite layer itself has whole-cell resolution)
|
||||||
|
if (playerPix) ec = 0u;
|
||||||
|
if ((ec >> 24) >= 1u && (ec >> 24) <= 3u) rgb += unpack_argb(ec | 0xFF000000u).rgb * gFog; // (the fog of war hides them: post_final multiplies the light buffer they go to by fog_of_war) // additive light (alpha 1: additive projectiles and animations, 2: particles, 3: additive sprites such as glowing eyes)
|
||||||
|
else if ((ec >> 24) != 0u) { vec4 e4 = unpack_argb(ec); rgb = mix(rgb, e4.rgb * Lv, e4.a); }
|
||||||
|
return rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 post_cell(vec3 rgb, ivec2 cp, vec3 Lv) {
|
||||||
|
{ // light of projectiles, flashes and explosions: additive glow
|
||||||
|
vec2 wpos = vec2(cp + ivec2(pc.view_x, pc.view_y)) + 0.5; // (the cell centre: the world is drawn at one pixel per cell)
|
||||||
|
for (uint i = 0u; i < min(lcount, 512u); ++i) {
|
||||||
|
float d = distance(wpos, lights[i].a.xy) / lights[i].a.z;
|
||||||
|
if (d < 1.0) rgb += lights[i].b.rgb * ((1.0 - d) * (1.0 - d) * lights[i].a.w) * gFog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{ // the color grading of the biome modifiers (post_final.frag: mix toward the grey by .a, then times .rgb; main.cpp moves it toward the camera's biome; the last 4 words of Tables)
|
||||||
|
const uint n = mats[mats.length() - 1u].w;
|
||||||
|
const uint gi = 1024u + 9u * n + 132u + n + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u + 8192u * 4u + 2u * n;
|
||||||
|
const vec4 cg = vec4(uintBitsToFloat(perm[gi]), uintBitsToFloat(perm[gi + 1u]), uintBitsToFloat(perm[gi + 2u]), uintBitsToFloat(perm[gi + 3u]));
|
||||||
|
if (cg != vec4(1.0, 1.0, 1.0, 0.0) && cg != vec4(0.0)) { rgb = mix(rgb, vec3((rgb.r + rgb.g + rgb.b) * 0.3333), cg.a); rgb *= cg.rgb; }
|
||||||
|
}
|
||||||
|
if ((sky[3] & 1u) != 0u && (gGlow.r + gGlow.g + gGlow.b) > 0.002) { // the glow, blended like post_final.frag: max(color + glow * 0.6 - 0.6 * lights, screen(color, glow))
|
||||||
|
const vec3 g = gGlow * min(vec3(1.0), 2.8 * vec3(0.6, 0.5, 0.45) * gFog); // (post_final.frag: fog_of_war = min(1, 2 x 1.4 x (0.6, 0.5, 0.45) x (1 - fog)): the glow keeps its strength until the fog is thick)
|
||||||
|
rgb = max(rgb + g * 0.6 - 0.6 * Lv, clamp(rgb + g - rgb * g, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
rgb *= 1.0 - clamp(ctrl.hand2.w, 0.0, 1.0); // BLINDNESS (00b015d0 case 9): a black screen over the world, below the HUD
|
||||||
|
return rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// camera shake of explosions: the whole picture (not the HUD) is offset by a random amount
|
||||||
|
const float scl = float(pc.scale) / 16.0; // output pixels per cell (fixed point, 1/16)
|
||||||
|
float amp = float(lpad[0]) * 0.001 * scl;
|
||||||
|
ivec2 so = amp > 0.5 ? ivec2((vec2(float(hash3(uint(pc.frame), 1u, 77u) & 255u), float(hash3(uint(pc.frame), 2u, 77u) & 255u)) / 255.0 - 0.5) * 2.0 * amp) : ivec2(0);
|
||||||
|
so += ivec2(round(ctrl.view.xy * scl)); // the camera is drawn between its last two ticks, at sub-cell precision (host: view.xy = displayed camera - centre of the window, in cells)
|
||||||
|
const ivec2 vis = ivec2((vec2(pc.gw, pc.gh) * scl - vec2(pc.ow, pc.oh)) * 0.5); // the window is the centre of the simulated grid (zoomed)
|
||||||
|
if (pc.pass_ == 1) { // the world pass: the cells from one before the window's first to past its last (main.cpp dispatches ow / scl + 4 by oh / scl + 4)
|
||||||
|
const ivec2 cp = ivec2(floor(vec2(so + vis) / scl)) - 1 + ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (cp.x < 0 || cp.y < 0 || cp.x >= pc.gw || cp.y >= pc.gh) return;
|
||||||
|
gScreenY = ((float(cp.y) + 0.5) * scl - float(so.y + vis.y)) / float(pc.oh);
|
||||||
|
vec3 Lv;
|
||||||
|
vec3 rgb = world_cell(cp, Lv);
|
||||||
|
rgb = post_cell(rgb, cp, Lv);
|
||||||
|
wcell[cp.y * pc.gw + cp.x] = uvec4(packUnorm4x8(vec4(clamp(rgb, 0.0, 1.0), 1.0)), packHalf2x16(Lv.rg), packHalf2x16(vec2(Lv.b, gFog)), 0u);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.ow || p.y >= pc.oh) return;
|
||||||
|
ivec2 cp = clamp(ivec2(floor(vec2(p + so + vis) / scl)), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1));
|
||||||
|
vec3 rgb = unpackUnorm4x8(wcell[cp.y * pc.gw + cp.x].x).rgb; // (only the colour: the light is read for the player's pixels)
|
||||||
|
if (ctrl.inp2.y != 0 && (ents[0].st.w & 8) == 0) {
|
||||||
|
vec2 P = ents[0].a.xy;
|
||||||
|
const vec2 pv = prevpos[0].xy;
|
||||||
|
if (distance(pv, P) < 10.0) P = mix(pv, P, ctrl.view.z);
|
||||||
|
const ivec2 cq = clamp(ivec2(floor(vec2(p + so + vis) / scl - (P - floor(P)))), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1));
|
||||||
|
const uint pe = entlayer[cq.y * pc.gw + cq.x];
|
||||||
|
if ((pe >> 24) == 254u) { // the player's pixel: lit like the cell under it, then the cell's post effects
|
||||||
|
const uvec4 wc = wcell[cp.y * pc.gw + cp.x];
|
||||||
|
const vec3 Lv = vec3(unpackHalf2x16(wc.y), unpackHalf2x16(wc.z).x);
|
||||||
|
gFog = unpackHalf2x16(wc.z).y; gGlow = glow_at(cp);
|
||||||
|
rgb = post_cell(unpack_argb(pe | 0xFF000000u).rgb * Lv, cp, Lv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(to-do #20): damage_flash_interpolation, additive_overlay_color / overlay_color
|
||||||
|
{ // the low health indicator (post_final.frag): + LOW_HEALTH_INDICATOR_COLOR (0.7, 0.1, 0) x (1.3 x the distance from the centre)^4 x low_health_indicator_alpha (sky word 62)
|
||||||
|
const float lh = uintBitsToFloat(sky[62]);
|
||||||
|
if (lh > 0.0) {
|
||||||
|
float a = length(vec2(p) / vec2(float(pc.ow), float(pc.oh)) - 0.5) * 1.3;
|
||||||
|
a *= a; a *= a;
|
||||||
|
rgb += vec3(0.7, 0.1, 0.0) * a * lh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{ // the picture without the HUD for the replay recorder (the game's GIFs have no UI): clip_half.comp / main.cpp copy it
|
||||||
|
const uvec3 q0 = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5);
|
||||||
|
clipraw[p.y * pc.ow + p.x] = 0xFF000000u | (q0.r << 16) | (q0.g << 8) | q0.b;
|
||||||
|
}
|
||||||
|
const uint ovTile = uint((p.y >> 5) * ((pc.ow + 31) >> 5) + (p.x >> 5)); // the HUD overlay: a mask word per 32 x 32 tile, then 1024 words per tile (main.cpp kOvlTiles)
|
||||||
|
const uint ov = overlay[ovTile] != 0u ? overlay[3600u + ovTile * 1024u + uint((p.y & 31) * 32 + (p.x & 31))] : 0u;
|
||||||
|
if ((ov >> 24) != 0u) { vec4 o4 = unpack_argb(ov); rgb = mix(rgb, o4.rgb, o4.a); }
|
||||||
|
uvec3 q = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5);
|
||||||
|
pixels[p.y * pc.ow + p.x] = 0xFF000000u | (q.r << 16) | (q.g << 8) | q.b; // BGRA8 in memory
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 256) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
|
||||||
|
// Fire inside the rigid bodies of a burnable material (docs/FIRE_FRACTURE_AND_LIQUIDS.md, DISPLACEMENT_AND_FIRE.md, CEILING_ATTACHMENT_AND_CONTACT_GEOMETRY.md).
|
||||||
|
// One workgroup per body, a thread per group of pixels. A pixel has a state word in `burn`: bit 31 burning, bit 30 burnt away (reported to the host, which removes it from
|
||||||
|
// the body and splits it), bits 16-23 fade, bits 0-15 the burn age. A burning pixel does what a burning solid cell does:
|
||||||
|
// * common burning (0070fb10): flames into (x-1..x+1, y-1) with generates_flames percent, smoke, requires_oxygen fade (restored by an accepted flame, else -1 per update,
|
||||||
|
// burning stops below 1), and one spread attempt: r % 101 <= fade, then ONE of the four neighbours, then the ignition draw r % 101 < 100 - autoignition_temperature;
|
||||||
|
// * then the age grows, and on every 32nd frame a pixel with fire_hp != -1 and age > fire_hp is removed.
|
||||||
|
// A pixel that is not burning catches fire from the fire sources in its cell and the four around it with the probabilities of burn.comp. Water puts it out.
|
||||||
|
layout(std430, set = 0, binding = 41) buffer Burn { uint burn[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; } ctrl;
|
||||||
|
|
||||||
|
const uint BURNING = 0x80000000u, BURNT = 0x40000000u;
|
||||||
|
|
||||||
|
float rnd(uint a, uint b, uint c) { return float(hash3(a, b, c) & 0xFFFFu) / 65536.0; }
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // 1024 noise words, 4 per material (burning explosion), then 2 per material: flame and smoke material + 1
|
||||||
|
uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; }
|
||||||
|
uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int bi = int(gl_WorkGroupID.x);
|
||||||
|
if (bi >= bhdr.x) return;
|
||||||
|
Body B = bodies[bi];
|
||||||
|
if ((B.i.z & B_REMOVED) != 0 || B.i.y < 0) return;
|
||||||
|
const uint matp1 = uint(ents[B.i.y].ex5.z + 0.5);
|
||||||
|
if (matp1 == 0u) return;
|
||||||
|
const uint m = matp1 - 1u;
|
||||||
|
const uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w;
|
||||||
|
const int s = B.i.x;
|
||||||
|
const int w = shi(s, 0), h = shi(s, 1);
|
||||||
|
if (w * h > 4096) return;
|
||||||
|
const vec2 com = vec2(shf(s, 5), shf(s, 6));
|
||||||
|
const float cs = cos(B.p.z), sn = sin(B.p.z);
|
||||||
|
const uvec4 sp = mats[mats.length() - 1u]; // fire, smoke, steam, count
|
||||||
|
const int ai = int(mw & 255u), tempFire = int((mw >> 8) & 255u), smokeP = int((mw >> 16) & 255u), flameP = int((mw >> 24) & 255u);
|
||||||
|
const int fireHp = int(mz << 16) >> 16; // signed 16 bits
|
||||||
|
const bool noBurnOut = fireHp == -1;
|
||||||
|
const bool needO2 = ((mz >> 19) & 1u) != 0u;
|
||||||
|
const float pIgnite = max(0.0, float(100 - ai)) / 101.0;
|
||||||
|
const uint base = uint(bi) * 4096u;
|
||||||
|
for (int px = int(gl_LocalInvocationID.x); px < w * h; px += 256) {
|
||||||
|
const ivec2 pp = ivec2(px % w, px / w);
|
||||||
|
if (!shape_solid(s, pp.x, pp.y)) continue;
|
||||||
|
uint st = burn[base + uint(px)];
|
||||||
|
if ((st & BURNT) != 0u) continue;
|
||||||
|
const vec2 wp = B.p.xy + rot2(cs, sn, vec2(pp) + 0.5 - com);
|
||||||
|
const ivec2 wc = ivec2(floor(wp));
|
||||||
|
const uint seed = uint(bi) * 4099u + uint(px);
|
||||||
|
if ((st & BURNING) == 0u) {
|
||||||
|
// fire sources in the pixel's cell and the four around it (cells are free under a body)
|
||||||
|
float keep = 1.0, bestFade = -1.0;
|
||||||
|
bool wet = false;
|
||||||
|
for (int d = 0; d < 5; ++d) {
|
||||||
|
const ivec2 q = wc + ivec2(d == 1 ? 1 : d == 2 ? -1 : 0, d == 3 ? 1 : d == 4 ? -1 : 0) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
const uint cw = cells[q.y * pc.gw + q.x], cm = cell_mat(cw);
|
||||||
|
if (cm == 0u) continue;
|
||||||
|
const uint nz = mats[3u * cm + 2u].z, nw = mats[3u * cm + 2u].w;
|
||||||
|
if (((nz >> 18) & 1u) != 0u) wet = true;
|
||||||
|
const bool nBurning = (cw & BURN_BIT) != 0u;
|
||||||
|
if (!(nBurning || ((nz >> 17) & 1u) != 0u)) continue;
|
||||||
|
int nfi = fade_of(birth[q.y * pc.gw + q.x]);
|
||||||
|
if (!nBurning && nfi == 0) nfi = int((nw >> 8) & 255u);
|
||||||
|
const float nf = float(nfi);
|
||||||
|
const float gate = nfi < 0 ? 0.0 : min(nf + 1.0, 101.0) / 101.0;
|
||||||
|
float pSrc = gate * 0.25 * pIgnite;
|
||||||
|
if ((mflags(cm) & F_FIRE) != 0u) pSrc = 1.0 - (1.0 - pSrc) * (1.0 - pSrc); // a fire cell attempts twice
|
||||||
|
keep *= 1.0 - min(pSrc, 1.0);
|
||||||
|
if (rnd(seed, uint(pc.frame), 20u + uint(d)) < pSrc && bestFade < 0.0) bestFade = nf; // the fade of the source that ignited it
|
||||||
|
}
|
||||||
|
if (!wet && bestFade >= 0.0 && rnd(seed, uint(pc.frame), 1u) > keep) burn[base + uint(px)] = BURNING | (uint(min(bestFade, 255.0)) << 16) | (st & 0xFFFFu);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// burning
|
||||||
|
uint age = st & 0xFFFFu;
|
||||||
|
int fade = int((st >> 16) & 255u);
|
||||||
|
bool wet = false;
|
||||||
|
for (int d = 0; d < 5 && !wet; ++d) {
|
||||||
|
const ivec2 q = wc + ivec2(d == 1 ? 1 : d == 2 ? -1 : 0, d == 3 ? 1 : d == 4 ? -1 : 0) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
const uint cm = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
|
wet = cm != 0u && ((mats[3u * cm + 2u].z >> 18) & 1u) != 0u;
|
||||||
|
}
|
||||||
|
if (wet) { burn[base + uint(px)] = age; continue; }
|
||||||
|
bool flameAccepted = false;
|
||||||
|
if (flameP > 0 && int(rnd(seed, uint(pc.frame), 2u) * 101.0) < flameP) {
|
||||||
|
const ivec2 t = wc + ivec2(int(rnd(seed, uint(pc.frame), 3u) * 3.0) - 1, -1) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) {
|
||||||
|
const int ti = t.y * pc.gw + t.x;
|
||||||
|
const uint cur = cells[ti];
|
||||||
|
if (cell_mat(cur) == 0u) { // an empty place (the word may hold a leftover colour variation): the flame takes it
|
||||||
|
flameAccepted = atomicCompSwap(cells[ti], cur, flame_material(m, sp.x) | (hash3(seed, uint(pc.frame), 4u) & 0xFF0000u)) == cur;
|
||||||
|
if (flameAccepted) birth[ti] = with_fade(birth_frame(), int((mats[3u * flame_material(m, sp.x) + 2u].w >> 8) & 255u)); // (0070f8c0: a new flame has its material's own fade, temperature_of_fire; the intensity is a separate field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needO2) fade = flameAccepted ? tempFire : fade - 1;
|
||||||
|
if (smokeP > 0 && int(rnd(seed, uint(pc.frame), 6u) * 101.0) < smokeP) {
|
||||||
|
const ivec2 t = wc + ivec2(int(rnd(seed, uint(pc.frame), 7u) * 3.0) - 1, -1) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) atomicCompSwap(cells[t.y * pc.gw + t.x], 0u, smoke_material(m, sp.y) | (hash3(seed, uint(pc.frame), 8u) & 0xFF0000u));
|
||||||
|
}
|
||||||
|
if (fade < 1) { burn[base + uint(px)] = age; continue; } // out of oxygen: the pixel stops burning
|
||||||
|
// one spread attempt towards one of the four neighbours inside the body
|
||||||
|
if (int(hash3(seed, uint(pc.frame), 9u) % 101u) <= fade) {
|
||||||
|
const uint dsel = hash3(seed, uint(pc.frame), 10u) & 3u;
|
||||||
|
const ivec2 np = pp + (dsel == 0u ? ivec2(0, -1) : dsel == 1u ? ivec2(-1, 0) : dsel == 2u ? ivec2(1, 0) : ivec2(0, 1));
|
||||||
|
if (shape_solid(s, np.x, np.y) && int(hash3(seed, uint(pc.frame), 11u) % 101u) < 100 - ai) {
|
||||||
|
const uint ni = base + uint(np.y * w + np.x), old = burn[ni];
|
||||||
|
if ((old & (BURNING | BURNT)) == 0u) atomicCompSwap(burn[ni], old, BURNING | (uint(fade) << 16) | (old & 0xFFFFu));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
age = (age + 1u) & 0xFFFFu; // 0070e060: a wrapping 16-bit age
|
||||||
|
if ((pc.frame & 31) == 0 && !noBurnOut && fireHp < int(age)) {
|
||||||
|
const int ei = atomicAdd(ctrl.burnN.x, 1);
|
||||||
|
if (ei < 256) { ctrl.burnEv[ei] = ivec4(bi, px, 0, 0); burn[base + uint(px)] = BURNT | age; continue; }
|
||||||
|
atomicAdd(ctrl.burnN.x, -1);
|
||||||
|
}
|
||||||
|
burn[base + uint(px)] = BURNING | (uint(fade) << 16) | age;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
|
||||||
|
// Copies the sub-step result back into the body state.
|
||||||
|
void main() {
|
||||||
|
uint b = gl_GlobalInvocationID.x;
|
||||||
|
if (int(b) >= bhdr.x) return;
|
||||||
|
bodies[b] = bnext[b];
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Shared declarations of the GPU rigid-body engine (rigid_solve.comp, rigid_commit.comp, rigid_frame.comp).
|
||||||
|
// The engine is the GPU port of src/physics/rigid.cpp: bodies are pixel masks, terrain is the live cell grid, contacts are aggregated
|
||||||
|
// into one manifold per pair and resolved with impulses. Bodies are solved Jacobi-style: every body reads the state of the others
|
||||||
|
// from the previous sub-step (`bodies`) and writes its own result to `bnext`; rigid_commit copies bnext back.
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
struct Ent {
|
||||||
|
vec4 a, box, mv, ai;
|
||||||
|
ivec4 st;
|
||||||
|
vec4 hp;
|
||||||
|
ivec4 x, rg, rg2, aux;
|
||||||
|
vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0
|
||||||
|
vec4 ex2; // ranged attack: projectile speed, gravity
|
||||||
|
ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0
|
||||||
|
vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire
|
||||||
|
vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag
|
||||||
|
vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y)
|
||||||
|
vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left
|
||||||
|
ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending
|
||||||
|
};
|
||||||
|
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
|
||||||
|
|
||||||
|
// shape table: 16 words per shape (w, h, mask offset, sample offset, sample count, com x, com y, mass, inertia, radius as float bits),
|
||||||
|
// followed by the pixel masks (one word per pixel) and the outline samples (two float words each).
|
||||||
|
layout(std430, set = 0, binding = 30) readonly buffer Shapes { uint sh[]; };
|
||||||
|
|
||||||
|
struct Body {
|
||||||
|
vec4 p; // centre of mass x, y, angle, angular velocity
|
||||||
|
vec4 v; // velocity x, y, restitution, friction
|
||||||
|
ivec4 i; // shape, entity slot, flags (1 asleep, 2 removed), quiet frames
|
||||||
|
vec4 q; // anchor x, y (the entity origin in image coordinates), density, probe counter
|
||||||
|
vec4 l; // x, y, angle at the last sleep check
|
||||||
|
vec4 n; // fragile: impact speed (cells per frame) above which it is damaged, 0 = unbreakable / nail point relative to the entity origin (x, y) / radius in which a surface holds the nail; flag B_NAILED
|
||||||
|
vec4 m; // collision group (bodies of one group do not collide), y = 1: passable for creatures and projectiles (corpses)
|
||||||
|
ivec4 jn; // joint partners (body indices, -1 = none)
|
||||||
|
vec4 ja0, ja1, ja2, ja3; // joint anchors (x, y on this body, x, y on the partner), relative to the centres of mass, in the bodies' own frames
|
||||||
|
vec4 jb; // break_force multiplier of each joint slot (0 = the joint does not break from force); l.w is the multiplier of the nail
|
||||||
|
vec4 d; // component values: buoyancy, linear damping, angular damping
|
||||||
|
};
|
||||||
|
layout(std430, set = 0, binding = 31) buffer Bodies { ivec4 bhdr; Body bodies[]; }; // bhdr.x = body count
|
||||||
|
layout(std430, set = 0, binding = 32) buffer BNext { ivec4 nhdr; Body bnext[]; };
|
||||||
|
|
||||||
|
const int B_ASLEEP = 1, B_REMOVED = 2, B_NAILED = 4; // nailed: hangs on the wall until the cells around the nail are gone
|
||||||
|
|
||||||
|
int shi(int s, int k) { return int(sh[s * 16 + k]); }
|
||||||
|
float shf(int s, int k) { return uintBitsToFloat(sh[s * 16 + k]); }
|
||||||
|
bool shape_solid(int s, int x, int y) {
|
||||||
|
int w = shi(s, 0), h = shi(s, 1);
|
||||||
|
return x >= 0 && y >= 0 && x < w && y < h && sh[shi(s, 2) + y * w + x] != 0u;
|
||||||
|
}
|
||||||
|
vec2 sample_of(int s, int k) {
|
||||||
|
int o = shi(s, 3) + 2 * k;
|
||||||
|
return vec2(uintBitsToFloat(sh[o]), uintBitsToFloat(sh[o + 1]));
|
||||||
|
}
|
||||||
|
vec2 rot2(float c, float s, vec2 v) { return vec2(c * v.x - s * v.y, s * v.x + c * v.y); }
|
||||||
|
|
||||||
|
bool solid_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; // no ground outside the window: bodies there are frozen
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m == 0u) return false;
|
||||||
|
return (mflags(m) & (F_STATIC | F_SAND)) != 0u;
|
||||||
|
}
|
||||||
|
bool liquid_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
if (m == 0u) return false;
|
||||||
|
uint f = mflags(m);
|
||||||
|
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
|
||||||
|
// Explosions (from projectiles on the GPU, from props on the host) push the rigid bodies and wake them.
|
||||||
|
layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; };
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint bi = gl_GlobalInvocationID.x;
|
||||||
|
if (int(bi) >= bhdr.x) return;
|
||||||
|
Body B = bodies[bi];
|
||||||
|
if ((B.i.z & B_REMOVED) != 0) return;
|
||||||
|
int s = B.i.x;
|
||||||
|
float mass = shf(s, 7), radius = shf(s, 9);
|
||||||
|
bool touched = false;
|
||||||
|
for (int k = 0; k < min(xhead.x, 512); ++k) {
|
||||||
|
vec4 e = xe[k]; // x, y, radius, power
|
||||||
|
if (e.w < -0.5) { // an explosion with the force of 00657960: this entry and the next one (force min, force max)
|
||||||
|
vec4 f2 = xe[min(k + 1, 511)];
|
||||||
|
++k;
|
||||||
|
const float pixels = float(shi(s, 4) > 0 ? int(shf(s, 7)) : 1);
|
||||||
|
const float massKg = max(pixels * B.q.z * 0.01, 1.0e-4);
|
||||||
|
vec2 O = e.xy, C = B.p.xy;
|
||||||
|
float dist = length(C - O) * 0.1, Ru = e.z * 0.1; // physics units (10 pixels each)
|
||||||
|
if (dist > Ru) continue;
|
||||||
|
// the original needs the angular bin of the explosion to reach this body: approximated by a line of sight through the grid that no solid cell interrupts
|
||||||
|
bool blocked = false;
|
||||||
|
const int steps = int(dist * 10.0);
|
||||||
|
for (int q = 1; q < steps && !blocked; ++q) blocked = solid_at(ivec2(floor(mix(O, C, float(q) / float(steps)))));
|
||||||
|
if (blocked) continue;
|
||||||
|
float t = Ru == 0.0 ? 1.0 : 1.0 - dist / Ru;
|
||||||
|
float A = (f2.y - f2.x) * t + f2.x;
|
||||||
|
vec2 n = dist >= 1.19e-7 ? normalize(C - O) : vec2(0.0);
|
||||||
|
// F = n A (x mass if the body weighs less than 1), x 360 N, dv = F / m / 360 per frame in pixels: = n A min(1, m) / m
|
||||||
|
B.v.xy += n * A * min(1.0, massKg) / massKg;
|
||||||
|
const float inertiaKg = max(shf(s, 8) * B.q.z * 1.0e-4, 1.0e-6);
|
||||||
|
const float tau = float(hash3(bi, uint(xhead.x), 91u) & 0x7FFFu) / 32767.0 * 30.0 - 15.0; // the random torque of the original
|
||||||
|
B.p.w += tau / (3600.0 * inertiaKg);
|
||||||
|
touched = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
vec2 d = B.p.xy - e.xy - vec2(0.0, 2.0);
|
||||||
|
float dist = length(d);
|
||||||
|
if (dist > e.z + radius) continue;
|
||||||
|
float f = e.w * max(0.0, 1.0 - dist / (e.z + radius)) / sqrt(max(1.0, mass * B.q.z) / 20.0);
|
||||||
|
vec2 n = dist > 1e-3 ? d / dist : vec2(0.0, -1.0);
|
||||||
|
B.v.xy += n * f;
|
||||||
|
B.p.w += (n.x > 0.0 ? 1.0 : -1.0) * f * 0.05;
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
if (touched) { B.i.z &= ~B_ASLEEP; B.i.w = 0; bodies[bi] = B; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
|
||||||
|
// Once per frame: sleeping (resting bodies stop being simulated, sleeping ones re-check their support), and the entity's position and angle.
|
||||||
|
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits: the flames of a burning corpse
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint bi = gl_GlobalInvocationID.x;
|
||||||
|
if (int(bi) >= bhdr.x) return;
|
||||||
|
Body B = bodies[bi];
|
||||||
|
if ((B.i.z & B_REMOVED) != 0) return;
|
||||||
|
int s = B.i.x;
|
||||||
|
int nS = shi(s, 4);
|
||||||
|
vec2 com = vec2(shf(s, 5), shf(s, 6));
|
||||||
|
float c = cos(B.p.z), sn = sin(B.p.z);
|
||||||
|
if ((B.i.z & 8) != 0) {
|
||||||
|
// simulated on the CPU (src/phys): the sleep state comes from there
|
||||||
|
} else if ((B.i.z & B_ASLEEP) != 0) {
|
||||||
|
B.q.w += 1.0;
|
||||||
|
if (B.q.w >= 12.0) { // re-check the support now and then
|
||||||
|
B.q.w = 0.0;
|
||||||
|
bool supported = false;
|
||||||
|
for (int k = 0; k < nS && !supported; ++k) {
|
||||||
|
vec2 r = rot2(c, sn, sample_of(s, k));
|
||||||
|
supported = solid_at(ivec2(floor(B.p.x + r.x), floor(B.p.y + r.y + 1.5)));
|
||||||
|
}
|
||||||
|
if (!supported) { B.i.z &= ~B_ASLEEP; B.i.w = 0; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 009be020: the sleep timer counts while the velocity stays under the tolerances (0.01 units per second linear, 2 degrees per second angular; pixels per frame at scale 10); a body
|
||||||
|
// sleeps when it has been quiet for half a second AND every body joined to it has too (the island's minimum timer)
|
||||||
|
const bool quiet = length(B.v.xy) < 0.01 * 10.0 / 60.0 && abs(B.p.w) < 0.034906585 / 60.0;
|
||||||
|
B.i.w = quiet ? B.i.w + 1 : 0;
|
||||||
|
if (B.i.w >= 30) {
|
||||||
|
bool island = true;
|
||||||
|
for (int k = 0; k < 4 && island; ++k) {
|
||||||
|
const int j = B.jn[k];
|
||||||
|
if (j < 0) continue;
|
||||||
|
const Body C = bodies[j];
|
||||||
|
if ((C.i.z & B_REMOVED) != 0 || (C.i.z & B_ASLEEP) != 0) continue;
|
||||||
|
island = C.i.w >= 30;
|
||||||
|
}
|
||||||
|
if (island) { B.i.z |= B_ASLEEP; B.v.xy = vec2(0.0); B.p.w = 0.0; B.i.w = 0; }
|
||||||
|
}
|
||||||
|
B.l = vec4(B.p.xy, B.p.z, 0.0);
|
||||||
|
}
|
||||||
|
if (B.m.z > 0.0) { // a burning corpse part: flames now and then, water puts it out (the pixels of burnable props burn in rigid_burn.comp)
|
||||||
|
B.m.z -= 1.0;
|
||||||
|
ivec2 cc = ivec2(floor(B.p.xy)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (cc.x >= 0 && cc.y >= 0 && cc.x < pc.gw && cc.y < pc.gh) {
|
||||||
|
uint cm = cell_mat(cells[cc.y * pc.gw + cc.x]);
|
||||||
|
if (cm != 0u && ((mats[3u * cm + 2u].z >> 18) & 1u) != 0u) B.m.z = 0.0;
|
||||||
|
}
|
||||||
|
if (B.m.z > 0.0 && (hash3(bi, uint(pc.frame), 17u) & 3u) == 0u) {
|
||||||
|
uint ei = uint(atomicAdd(ehead.x, 1));
|
||||||
|
vec2 fp = B.p.xy + vec2(0.0, -1.0);
|
||||||
|
if (ei < 256u) { ee[2u * ei] = vec4(fp, 1.5, 0.0); ee[2u * ei + 1u] = vec4(1.0, float(mats[mats.length() - 1u].x), 0.45, 0.0); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodies[bi] = B;
|
||||||
|
if (B.i.y >= 0) {
|
||||||
|
vec2 d = rot2(c, sn, B.q.xy - com);
|
||||||
|
ents[B.i.y].a = vec4(B.p.xy + d, B.p.z, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
|
||||||
|
// Marks the cells covered by the pixel masks of the rigid bodies (those that are solid for creatures and projectiles) in a per-cell grid, so that
|
||||||
|
// creatures and projectiles test a cell in one read instead of looping over all bodies. One workgroup per body.
|
||||||
|
layout(std430, set = 0, binding = 40) buffer BodyGrid { uint bodyGrid[]; };
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int bi = int(gl_WorkGroupID.x), t = int(gl_LocalInvocationID.x);
|
||||||
|
if (bi >= bhdr.x) return;
|
||||||
|
Body b = bodies[bi];
|
||||||
|
if ((b.i.z & 2) != 0 || b.m.y > 0.5) return; // removed, or a corpse part (passable)
|
||||||
|
int s = b.i.x;
|
||||||
|
float rad = shf(s, 9);
|
||||||
|
int n = int(2.0 * rad) + 2;
|
||||||
|
ivec2 lo = ivec2(floor(b.p.xy - vec2(rad)));
|
||||||
|
float cs = cos(b.p.z), sn = sin(b.p.z);
|
||||||
|
for (int k = t; k < n * n; k += 64) {
|
||||||
|
ivec2 w = lo + ivec2(k % n, k / n);
|
||||||
|
ivec2 q = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
vec2 dd = vec2(w) + 0.5 - b.p.xy;
|
||||||
|
vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6));
|
||||||
|
if (shape_solid(s, int(floor(im.x)), int(floor(im.y)))) bodyGrid[q.y * pc.gw + q.x] = (b.d.w > 0.5 ? uint(b.d.w + 0.5) : uint(b.i.y) + 1u) | ((b.i.z & 1024) != 0 ? 0x80000000u : 0u); // (bit 31: flag 1024, no platform: characters pass) // the entity slot + 1 (a part body of a PhysicsBody2 entity: the owner's, d.w; the engine's body user data is the entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "rigid_common.glsl"
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after the noise words: per material friction and restitution of a rigid contact (6 words per material precede)
|
||||||
|
|
||||||
|
// One sub-step of the rigid-body engine, one workgroup per body (a thread per outline sample). Port of PhysicsWorld::step.
|
||||||
|
shared vec4 sP, sV; // p, v of the body being solved
|
||||||
|
shared int accN, accPX, accPY, accNX, accNY, accD, accWet, woke;
|
||||||
|
shared float sImpact; // fastest approach speed into a contact during this sub-step (fragile bodies take damage from it)
|
||||||
|
|
||||||
|
void contact_static(inout vec3 u, vec2 r, vec2 n, float e, float mu, float invM, float invI) {
|
||||||
|
vec2 vc = u.xy + u.z * vec2(-r.y, r.x);
|
||||||
|
float vn = dot(vc, n);
|
||||||
|
if (vn >= 0.0) return;
|
||||||
|
float rn = r.x * n.y - r.y * n.x;
|
||||||
|
float k = invM + rn * rn * invI;
|
||||||
|
float rest = -vn < 0.1667 ? 0.0 : e; // 009c1ec0: restitution only for vn < -1 unit/s (10 px/s = 0.1667 cells per frame, pixel scale 10)
|
||||||
|
float j = -(1.0 + rest) * vn / k;
|
||||||
|
u.xy += j * n * invM;
|
||||||
|
u.z += invI * (r.x * (j * n.y) - r.y * (j * n.x));
|
||||||
|
vec2 wc = u.xy + u.z * vec2(-r.y, r.x);
|
||||||
|
vec2 t = vec2(-n.y, n.x);
|
||||||
|
float vt = dot(wc, t);
|
||||||
|
float rt = r.x * t.y - r.y * t.x;
|
||||||
|
float kt = invM + rt * rt * invI;
|
||||||
|
float jt = clamp(-vt / kt, -mu * j, mu * j);
|
||||||
|
u.xy += jt * t * invM;
|
||||||
|
u.z += invI * (r.x * (jt * t.y) - r.y * (jt * t.x));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 snap_axis(vec2 g) {
|
||||||
|
if (abs(g.x) > 2.0 * abs(g.y)) g.y = 0.0; else if (abs(g.y) > 2.0 * abs(g.x)) g.x = 0.0;
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 terrain_normal(ivec2 c) {
|
||||||
|
vec2 g = vec2(0.0);
|
||||||
|
for (int dy = -2; dy <= 2; ++dy)
|
||||||
|
for (int dx = -2; dx <= 2; ++dx)
|
||||||
|
if ((dx != 0 || dy != 0) && solid_at(c + ivec2(dx, dy))) {
|
||||||
|
float d = length(vec2(dx, dy));
|
||||||
|
g -= vec2(dx, dy) / (d * d);
|
||||||
|
}
|
||||||
|
g = snap_axis(g);
|
||||||
|
float l = length(g);
|
||||||
|
return l < 1e-6 ? vec2(0.0, -1.0) : g / l;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A world point inside the mask of body C? Gives the outward normal of C's surface (world, pointing into the point's owner) and the depth.
|
||||||
|
bool mask_contact(vec2 wpnt, vec4 cp, int sc, out vec2 nw, out float depth) {
|
||||||
|
float qc = cos(cp.z), qs = sin(cp.z);
|
||||||
|
vec2 d = wpnt - cp.xy;
|
||||||
|
vec2 l = vec2(qc * d.x + qs * d.y, -qs * d.x + qc * d.y);
|
||||||
|
vec2 im = l + vec2(shf(sc, 5), shf(sc, 6));
|
||||||
|
ivec2 ic = ivec2(floor(im));
|
||||||
|
nw = vec2(0.0, -1.0); depth = 0.5;
|
||||||
|
if (!shape_solid(sc, ic.x, ic.y)) return false;
|
||||||
|
vec2 g = vec2(0.0);
|
||||||
|
for (int dy = -2; dy <= 2; ++dy)
|
||||||
|
for (int dx = -2; dx <= 2; ++dx)
|
||||||
|
if ((dx != 0 || dy != 0) && shape_solid(sc, ic.x + dx, ic.y + dy)) {
|
||||||
|
float dd = length(vec2(dx, dy));
|
||||||
|
g -= vec2(dx, dy) / (dd * dd);
|
||||||
|
}
|
||||||
|
g = snap_axis(g);
|
||||||
|
float gl = length(g);
|
||||||
|
if (gl < 1e-6) { g = l; gl = length(g); if (gl < 1e-6) { g = vec2(0.0, -1.0); gl = 1.0; } }
|
||||||
|
g /= gl;
|
||||||
|
for (float t = 0.5; t <= 6.0; t += 0.5) {
|
||||||
|
depth = t;
|
||||||
|
if (!shape_solid(sc, int(floor(im.x + g.x * t)), int(floor(im.y + g.y * t)))) break;
|
||||||
|
}
|
||||||
|
nw = rot2(qc, qs, g);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_contact(vec2 rel, vec2 n, float depth) { // rel: world point relative to the centre of mass of the body being solved
|
||||||
|
atomicAdd(accN, 1);
|
||||||
|
atomicAdd(accPX, int(round(rel.x * 256.0)));
|
||||||
|
atomicAdd(accPY, int(round(rel.y * 256.0)));
|
||||||
|
atomicAdd(accNX, int(round(n.x * 1024.0)));
|
||||||
|
atomicAdd(accNY, int(round(n.y * 1024.0)));
|
||||||
|
atomicMax(accD, int(depth * 256.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int bi = int(gl_WorkGroupID.x);
|
||||||
|
int t = int(gl_LocalInvocationID.x);
|
||||||
|
if (bi >= bhdr.x) return;
|
||||||
|
Body B = bodies[bi];
|
||||||
|
bool asleep = (B.i.z & B_ASLEEP) != 0;
|
||||||
|
vec2 wp0 = B.p.xy - vec2(pc.view_x, pc.view_y);
|
||||||
|
if ((B.i.z & B_REMOVED) != 0 || wp0.x < 4.0 || wp0.y < 4.0 || wp0.x > float(pc.gw - 4) || wp0.y > float(pc.gh - 4)) { // frozen outside the window
|
||||||
|
if (t == 0) bnext[bi] = B;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((B.i.z & B_NAILED) != 0 && (B.i.z & B_REMOVED) == 0) { // hangs on the wall while there is terrain around the nail
|
||||||
|
vec2 org = B.p.xy + rot2(cos(B.p.z), sin(B.p.z), B.q.xy - vec2(shf(B.i.x, 5), shf(B.i.x, 6)));
|
||||||
|
ivec2 nc = ivec2(floor(org + B.n.yz));
|
||||||
|
bool held = false;
|
||||||
|
int nr = int(B.n.w);
|
||||||
|
for (int dy = -nr; dy <= nr; ++dy)
|
||||||
|
for (int dx = -nr; dx <= nr; ++dx) held = held || (dx * dx + dy * dy <= nr * nr && solid_at(nc + ivec2(dx, dy))); // a surface near the nail point
|
||||||
|
if (held && B.l.w > 0.0) { // the joint breaks when its reaction force is above (mass * 160) * break_force (docs/IMPLEMENTATION_BLOCKERS.md): the nail holds the body still, so the
|
||||||
|
// reaction is the whole acceleration it would have had: gravity plus the velocity change the impulses of this frame gave it (explosions, kicks), in cells per frame^2 (x 360 = m/s^2)
|
||||||
|
const vec2 a = B.v.xy + vec2(0.0, 0.1667);
|
||||||
|
const float lim = 160.0 * B.l.w / 360.0;
|
||||||
|
if (dot(a, a) > lim * lim) held = false;
|
||||||
|
}
|
||||||
|
if (held) { B.v.xy = vec2(0.0); B.p.w = 0.0; if (t == 0) bnext[bi] = B; return; } // (the impulses of the frame went into the wall)
|
||||||
|
B.i.z &= ~(B_NAILED | B_ASLEEP); // the wall is gone or the joint broke: it falls
|
||||||
|
B.i.w = 0;
|
||||||
|
asleep = false;
|
||||||
|
}
|
||||||
|
int s = B.i.x;
|
||||||
|
int nS = shi(s, 4);
|
||||||
|
float mass = shf(s, 7), inertia = shf(s, 8), radius = shf(s, 9);
|
||||||
|
float invM = 1.0 / (mass * B.q.z), invI = 1.0 / (inertia * B.q.z);
|
||||||
|
const float dt = 0.5;
|
||||||
|
if (t == 0) { sP = B.p; sV = B.v; accWet = 0; woke = 0; accN = 0; accPX = 0; accPY = 0; accNX = 0; accNY = 0; accD = 0; sImpact = 0.0; }
|
||||||
|
barrier();
|
||||||
|
|
||||||
|
if (!asleep) {
|
||||||
|
// buoyancy and drag in liquids
|
||||||
|
// 00bda390 / 00bde0f0 (docs/LIQUID_PHYSICS_COMPLETE_GUIDE.md section 8): ONE probe at (int(x), int(y) + 8) decides; in a liquid (not a powder) the force is -b m (v + g gravity_scale) at the centre,
|
||||||
|
// b = the component's buoyancy (PhysicsBody2Component constructor 00672980: 0.7). dv/dt = g (1 - b) - b v, no submerged fraction, no density, no extra rotation damping.
|
||||||
|
float c = cos(sP.z), sn = sin(sP.z);
|
||||||
|
if (t == 0) { // the probe sits at the ENTITY ORIGIN (the image point B.q, not the centre of mass) plus 8 in y, float coordinates truncating toward zero
|
||||||
|
const vec2 org = sP.xy + rot2(c, sn, B.q.xy - vec2(shf(s, 5), shf(s, 6)));
|
||||||
|
if (liquid_at(ivec2(int(org.x), int(org.y) + 8))) accWet = 1;
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
if (t == 0) {
|
||||||
|
const float buoy = B.d.x * float(accWet); // the component's buoyancy
|
||||||
|
vec4 P = sP, V = sV;
|
||||||
|
const float h = dt / 60.0; // 009be020: v = (v + h (g + F / m)) / (1 + h linear_damping), the damping acts on the gravity just added
|
||||||
|
V.y += 0.1667 * dt * (1.0 - buoy);
|
||||||
|
V.xy *= (1.0 - buoy * dt / 60.0) / (1.0 + h * B.d.y);
|
||||||
|
P.w /= 1.0 + h * B.d.z;
|
||||||
|
float sp = length(V.xy);
|
||||||
|
if (sp > 2500.0 / 60.0) V.xy *= (2500.0 / 60.0) / sp; // the initial cap: 250 physics units per second (x 10 pixels)
|
||||||
|
P.w = clamp(P.w, -1.5707963, 1.5707963); // 009be020: |h omega| <= pi / 2
|
||||||
|
vec2 stepv = V.xy * dt;
|
||||||
|
if (length(stepv) > 20.0) stepv *= 20.0 / length(stepv); // after the constraints a translation of at most 2 physics units (20 pixels) per step
|
||||||
|
P.xy += stepv;
|
||||||
|
P.z += P.w * dt;
|
||||||
|
sP = P; sV = V;
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
// terrain contacts: outline samples inside solid cells form one aggregated manifold
|
||||||
|
c = cos(sP.z); sn = sin(sP.z);
|
||||||
|
if (t < nS) {
|
||||||
|
vec2 r = rot2(c, sn, sample_of(s, t));
|
||||||
|
vec2 pnt = sP.xy + r;
|
||||||
|
ivec2 cell = ivec2(floor(pnt));
|
||||||
|
if (solid_at(cell)) {
|
||||||
|
vec2 nm = terrain_normal(cell);
|
||||||
|
float depth = 0.5;
|
||||||
|
for (float tt = 0.5; tt <= 6.0; tt += 0.5) {
|
||||||
|
depth = tt;
|
||||||
|
if (!solid_at(ivec2(floor(pnt + nm * tt)))) break;
|
||||||
|
}
|
||||||
|
add_contact(r, nm, depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
if (t == 0 && accN > 0) {
|
||||||
|
vec2 cp = vec2(float(accPX), float(accPY)) / (256.0 * float(accN));
|
||||||
|
vec2 an = vec2(float(accNX), float(accNY)) / 1024.0;
|
||||||
|
float al = length(an);
|
||||||
|
if (al > 1e-6) {
|
||||||
|
vec3 u = vec3(sV.xy, sP.w);
|
||||||
|
sImpact = max(sImpact, -dot(u.xy + u.z * vec2(-cp.y, cp.x), an / al));
|
||||||
|
// the contacted terrain material: its own solid_friction / solid_restitution (009c18c0: friction sqrt(fA fB), restitution max)
|
||||||
|
float tf = 0.3, tr = 0.2;
|
||||||
|
{
|
||||||
|
const ivec2 cc = ivec2(floor(sP.xy + cp));
|
||||||
|
for (int k = 0; k < 9; ++k) {
|
||||||
|
const ivec2 q = cc + ivec2(k % 3 - 1, k / 3 - 1) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
const uint tm = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
|
if (tm != 0u && (mflags(tm) & F_STATIC) != 0u) {
|
||||||
|
const uint tb = 1024u + 6u * mats[mats.length() - 1u].w + 2u * tm;
|
||||||
|
tf = uintBitsToFloat(tbl[tb]); tr = uintBitsToFloat(tbl[tb + 1u]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int it = 0; it < 2; ++it) contact_static(u, cp, an / al, max(B.v.z, tr), sqrt(B.v.w * tf), invM, invI);
|
||||||
|
sV.xy = u.xy; sP.w = u.z;
|
||||||
|
float push = min(float(accD) / 256.0, 3.0) * 0.7;
|
||||||
|
sP.xy += an / al * push;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
// contacts with other bodies: my outline in their mask and theirs in mine
|
||||||
|
for (int j = 0; j < bhdr.x; ++j) {
|
||||||
|
if (j == bi) continue;
|
||||||
|
Body C = bodies[j];
|
||||||
|
if ((C.i.z & B_REMOVED) != 0) continue;
|
||||||
|
if (B.m.x > 0.5 && B.m.x == C.m.x) continue; // parts of one corpse do not collide with each other
|
||||||
|
bool casleep = (C.i.z & B_ASLEEP) != 0;
|
||||||
|
if (asleep && casleep) continue;
|
||||||
|
int sc = C.i.x;
|
||||||
|
float rc = shf(sc, 9);
|
||||||
|
if (distance(sP.xy, C.p.xy) > radius + rc) continue;
|
||||||
|
if (t == 0) { accN = 0; accPX = 0; accPY = 0; accNX = 0; accNY = 0; accD = 0; }
|
||||||
|
barrier();
|
||||||
|
vec4 myp = vec4(sP.xy, sP.z, 0.0);
|
||||||
|
float c = cos(sP.z), sn = sin(sP.z);
|
||||||
|
if (t < nS) {
|
||||||
|
vec2 w = sP.xy + rot2(c, sn, sample_of(s, t));
|
||||||
|
vec2 nw; float depth;
|
||||||
|
if (mask_contact(w, C.p, sc, nw, depth)) add_contact(w - sP.xy, nw, depth);
|
||||||
|
}
|
||||||
|
int ncs = shi(sc, 4);
|
||||||
|
if (t < ncs) {
|
||||||
|
float cc = cos(C.p.z), cs = sin(C.p.z);
|
||||||
|
vec2 w = C.p.xy + rot2(cc, cs, sample_of(sc, t));
|
||||||
|
vec2 nw; float depth;
|
||||||
|
if (mask_contact(w, myp, s, nw, depth)) add_contact(w - sP.xy, -nw, depth);
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
if (t == 0 && accN > 0) {
|
||||||
|
if (asleep && !casleep && length(C.v.xy) > 0.15) woke = 1; // a moving neighbour wakes a sleeping body; resting contacts do not
|
||||||
|
if (!asleep) {
|
||||||
|
float invMc = 1.0 / (shf(sc, 7) * C.q.z), invIc = 1.0 / (shf(sc, 8) * C.q.z);
|
||||||
|
float e = max(B.v.z, C.v.z), mu = sqrt(B.v.w * C.v.w);
|
||||||
|
vec2 ra = vec2(float(accPX), float(accPY)) / (256.0 * float(accN));
|
||||||
|
vec2 rcv = sP.xy + ra - C.p.xy;
|
||||||
|
vec2 sn_ = vec2(float(accNX), float(accNY)) / 1024.0;
|
||||||
|
float al = length(sn_);
|
||||||
|
if (al > 1e-6) {
|
||||||
|
vec2 n = sn_ / al;
|
||||||
|
vec3 u = vec3(sV.xy, sP.w), uc = vec3(C.v.xy, C.p.w); // uc follows the partner's response virtually, only my side is applied
|
||||||
|
for (int it = 0; it < 2; ++it) {
|
||||||
|
vec2 va = u.xy + u.z * vec2(-ra.y, ra.x), vc = uc.xy + uc.z * vec2(-rcv.y, rcv.x);
|
||||||
|
float vn = dot(va - vc, n);
|
||||||
|
if (vn >= 0.0) break;
|
||||||
|
if (it == 0) sImpact = max(sImpact, -vn);
|
||||||
|
float ran = ra.x * n.y - ra.y * n.x, rcn = rcv.x * n.y - rcv.y * n.x;
|
||||||
|
float kk = invM + invMc + ran * ran * invI + rcn * rcn * invIc;
|
||||||
|
float jn = -(1.0 + (-vn < 0.1667 ? 0.0 : e)) * vn / kk;
|
||||||
|
u.xy += jn * n * invM; u.z += invI * (ra.x * (jn * n.y) - ra.y * (jn * n.x));
|
||||||
|
uc.xy -= jn * n * invMc; uc.z -= invIc * (rcv.x * (jn * n.y) - rcv.y * (jn * n.x));
|
||||||
|
vec2 wa = u.xy + u.z * vec2(-ra.y, ra.x), wc = uc.xy + uc.z * vec2(-rcv.y, rcv.x);
|
||||||
|
vec2 tg = vec2(-n.y, n.x);
|
||||||
|
float vt = dot(wa - wc, tg);
|
||||||
|
float rat = ra.x * tg.y - ra.y * tg.x, rct = rcv.x * tg.y - rcv.y * tg.x;
|
||||||
|
float kt = invM + invMc + rat * rat * invI + rct * rct * invIc;
|
||||||
|
float jt = clamp(-vt / kt, -mu * jn, mu * jn);
|
||||||
|
u.xy += jt * tg * invM; u.z += invI * (ra.x * (jt * tg.y) - ra.y * (jt * tg.x));
|
||||||
|
uc.xy -= jt * tg * invMc; uc.z -= invIc * (rcv.x * (jt * tg.y) - rcv.y * (jt * tg.x));
|
||||||
|
}
|
||||||
|
sV.xy = u.xy; sP.w = u.z;
|
||||||
|
float push = min(float(accD) / 256.0, 3.0) * 0.6;
|
||||||
|
sP.xy += n * push * invM / (invM + invMc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
// pin joints (ragdolls): the two anchors are pulled together; every body applies its own side of the impulse
|
||||||
|
int brokenJoints = 0;
|
||||||
|
if (t == 0) {
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
int j = B.jn[k];
|
||||||
|
if (j < 0) continue;
|
||||||
|
Body C = bodies[j];
|
||||||
|
if ((C.i.z & B_REMOVED) != 0) { brokenJoints |= 1 << k; continue; } // the partner is gone (cut away, burnt): the joint is dropped and the body wakes
|
||||||
|
if (asleep) { if ((C.i.z & B_ASLEEP) == 0 && length(C.v.xy) > 0.15) woke = 1; continue; }
|
||||||
|
vec4 ja = k == 0 ? B.ja0 : (k == 1 ? B.ja1 : (k == 2 ? B.ja2 : B.ja3));
|
||||||
|
int sc = C.i.x;
|
||||||
|
float invMc = 1.0 / (shf(sc, 7) * C.q.z), invIc = 1.0 / (shf(sc, 8) * C.q.z);
|
||||||
|
vec2 ra = rot2(cos(sP.z), sin(sP.z), ja.xy), rb = rot2(cos(C.p.z), sin(C.p.z), ja.zw);
|
||||||
|
vec2 err = (C.p.xy + rb) - (sP.xy + ra);
|
||||||
|
vec2 va = sV.xy + sP.w * vec2(-ra.y, ra.x), vb = C.v.xy + C.p.w * vec2(-rb.y, rb.x);
|
||||||
|
vec2 r = (vb - va) + err * 0.35; // residual relative velocity of the anchors, with a pull toward each other
|
||||||
|
float m = invM + invMc;
|
||||||
|
mat2 K = mat2(m + invI * ra.y * ra.y + invIc * rb.y * rb.y, -invI * ra.x * ra.y - invIc * rb.x * rb.y,
|
||||||
|
-invI * ra.x * ra.y - invIc * rb.x * rb.y, m + invI * ra.x * ra.x + invIc * rb.x * rb.x);
|
||||||
|
vec2 J = inverse(K) * r;
|
||||||
|
const float jbk = k == 0 ? B.jb.x : (k == 1 ? B.jb.y : (k == 2 ? B.jb.z : B.jb.w));
|
||||||
|
if (jbk != 0.0) { // force of the joint: impulse / dt, in newtons (mass 0.01 per pixel, 360 = cells/frame^2 -> m/s^2); limit = (m_a + m_b) * 160 * break_force,
|
||||||
|
// for the ragdoll kinds 0x2b67 / 0x2b68 (stored negative) max((m_a + m_b) * 400, 200) * break_force
|
||||||
|
const float M = (shf(s, 7) * B.q.z + shf(sc, 7) * C.q.z) * 0.01;
|
||||||
|
const float F = length(J) * 0.01 * 360.0 / dt, lim = jbk > 0.0 ? M * 160.0 * jbk : max(M * 400.0, 200.0) * -jbk;
|
||||||
|
if (F * F > lim * lim) brokenJoints |= 1 << k;
|
||||||
|
}
|
||||||
|
sV.xy += J * invM;
|
||||||
|
sP.w += invI * (ra.x * J.y - ra.y * J.x);
|
||||||
|
sP.xy += err * 0.15 * invM / m; // and a little of the error is corrected directly
|
||||||
|
woke = max(woke, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
if (t == 0) {
|
||||||
|
if (B.n.x > 0.0 && B.i.y >= 0 && sImpact > B.n.x) atomicAdd(ents[B.i.y].x.x, int((sImpact - B.n.x) * 750.0)); // fall / impact damage of fragile bodies
|
||||||
|
Body o = B;
|
||||||
|
o.p = sP;
|
||||||
|
o.v.xy = sV.xy;
|
||||||
|
for (int k = 0; k < 4; ++k) if ((brokenJoints & (1 << k)) != 0) { o.jn[k] = -1; woke = 1; }
|
||||||
|
if (woke != 0) { o.i.z &= ~B_ASLEEP; o.i.w = 0; }
|
||||||
|
bnext[bi] = o;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// (needs the Tables buffer `tbl`: its first 512 words are the permutation table)
|
||||||
|
// 00872560: Gustavson's 3D simplex noise (skew 1/3, unskew 1/6, falloff 0.6, x 32) on the permutation table at the start of Tables (00fdf530 holds the same values)
|
||||||
|
const ivec3 G3[12] = ivec3[12](ivec3(1,1,0), ivec3(-1,1,0), ivec3(1,-1,0), ivec3(-1,-1,0), ivec3(1,0,1), ivec3(-1,0,1), ivec3(1,0,-1), ivec3(-1,0,-1), ivec3(0,1,1), ivec3(0,-1,1), ivec3(0,1,-1), ivec3(0,-1,-1));
|
||||||
|
float corner3(int gi, vec3 d) { float t = 0.6 - dot(d, d); if (t < 0.0) return 0.0; t *= t; return t * t * dot(vec3(G3[gi]), d); }
|
||||||
|
float simplex3(vec3 v) {
|
||||||
|
const float F3 = 1.0 / 3.0, G = 1.0 / 6.0;
|
||||||
|
const float s = (v.x + v.y + v.z) * F3;
|
||||||
|
const ivec3 ijk = ivec3(floor(v + s));
|
||||||
|
const float t = float(ijk.x + ijk.y + ijk.z) * G;
|
||||||
|
const vec3 d0 = v - (vec3(ijk) - t);
|
||||||
|
ivec3 o1, o2;
|
||||||
|
if (d0.x >= d0.y) { if (d0.y >= d0.z) { o1 = ivec3(1,0,0); o2 = ivec3(1,1,0); } else if (d0.x >= d0.z) { o1 = ivec3(1,0,0); o2 = ivec3(1,0,1); } else { o1 = ivec3(0,0,1); o2 = ivec3(1,0,1); } }
|
||||||
|
else { if (d0.y < d0.z) { o1 = ivec3(0,0,1); o2 = ivec3(0,1,1); } else if (d0.x < d0.z) { o1 = ivec3(0,1,0); o2 = ivec3(0,1,1); } else { o1 = ivec3(0,1,0); o2 = ivec3(1,1,0); } }
|
||||||
|
const vec3 d1 = d0 - vec3(o1) + G, d2 = d0 - vec3(o2) + 2.0 * G, d3 = d0 - 1.0 + 3.0 * G;
|
||||||
|
const int ii = ijk.x & 255, jj = ijk.y & 255, kk = ijk.z & 255;
|
||||||
|
#define P(n) int(tbl[uint(n)])
|
||||||
|
const int g0 = P(ii + P(jj + P(kk))) % 12;
|
||||||
|
const int g1 = P(ii + o1.x + P(jj + o1.y + P(kk + o1.z))) % 12;
|
||||||
|
const int g2 = P(ii + o2.x + P(jj + o2.y + P(kk + o2.z))) % 12;
|
||||||
|
const int g3 = P(ii + 1 + P(jj + 1 + P(kk + 1))) % 12;
|
||||||
|
#undef P
|
||||||
|
return 32.0 * (corner3(g0, d0) + corner3(g1, d1) + corner3(g2, d2) + corner3(g3, d3));
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// The sky is visible in a window column above its first solid cell (static or powder); this finds that cell for every column (the sky buffer: words 64 ...).
|
||||||
|
layout(std430, set = 0, binding = 44) buffer Sky { uint sky[]; };
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int x = int(gl_GlobalInvocationID.x);
|
||||||
|
if (x >= pc.gw) return;
|
||||||
|
int top = pc.gh;
|
||||||
|
for (int y = 0; y < pc.gh; ++y) {
|
||||||
|
uint m = cell_mat(cells[y * pc.gw + x]);
|
||||||
|
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) { top = y; break; }
|
||||||
|
}
|
||||||
|
sky[64 + x] = uint(top);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Draws entity sprites into the per-cell entity layer. One workgroup per sprite instance.
|
||||||
|
// The sprite is placed at its entity current position (entities.comp moves them), mirrored when the entity faces left, and
|
||||||
|
// animated: mobile entities play their walk animation while moving and the idle one otherwise.
|
||||||
|
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
|
||||||
|
layout(std430, set = 0, binding = 19) readonly buffer EntTexels { uint enttex[]; };
|
||||||
|
layout(std430, set = 0, binding = 20) readonly buffer EntInst { ivec4 entinst[]; }; // 4 per instance, see main.cpp
|
||||||
|
layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; };
|
||||||
|
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
|
||||||
|
layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // positions before the last tick
|
||||||
|
layout(std430, set = 0, binding = 54) buffer EntDepth { uint entdepth[]; };
|
||||||
|
// the rigid bodies (rigid_common.glsl) and the burn state of their pixels (rigid_burn.comp: bits 0-15 the burn age): a burnt pixel is charred
|
||||||
|
struct SBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0, ja1, ja2, ja3; vec4 jb; vec4 d; };
|
||||||
|
layout(std430, set = 0, binding = 31) readonly buffer Bodies { ivec4 bhdr; SBody bodies[]; };
|
||||||
|
layout(std430, set = 0, binding = 41) readonly buffer Burn { uint burnpx[]; };
|
||||||
|
shared int sBody;
|
||||||
|
// the burning-cell palette (main.cpp puts colors_lava.bmp after the tables; render.comp / glow.comp read it the same way)
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
|
||||||
|
uint fpal_word(uint k) { return tbl[1024u + 9u * mats[mats.length() - 1u].w + k]; }
|
||||||
|
uint burning_texel(uint m, uint h) { // 00724580: a burning cell takes a random pixel of colors_lava.bmp (the row of its fire_colors_index) every frame; alpha 253 marks it for glow.comp
|
||||||
|
const uint row = min(fpal_word(132u + m), 5u);
|
||||||
|
return (fpal_word(96u + row * 6u + h % 6u) & 0x00FFFFFFu) | 0xFD000000u;
|
||||||
|
} // per cell: the depth key of the nearest opaque texel (cleared to ~0 every frame)
|
||||||
|
|
||||||
|
// The depth of an instance: SpriteComponent z_index (larger = further back; the instance keeps z x 100), then the later instance in front of an earlier one of the same z
|
||||||
|
uint gKey = 0xFFFFFFFFu;
|
||||||
|
uint gFlash = 0u; // the hit flash (entities.comp hit_flash): 0, or the colour every opaque texel takes (white; red for a critical hit), keeping its alpha
|
||||||
|
float gFade = 0.0; // LifetimeComponent fade_sprites (00b9a010): the sprites' alpha is 1 - this (ex10.w of an entity that is not a creature; the host sets it)
|
||||||
|
|
||||||
|
|
||||||
|
// Two passes: 0 draws the ordinary texels, 1 the additive ones (alpha 3: the glow of emissive sprites). The glow is added to what is already in the cell (the creature under
|
||||||
|
// its eyes keeps its pixels); drawing both kinds in one dispatch let the writers of a cell race, which made the creature vanish behind its glow in some frames.
|
||||||
|
void put_texel(int idx, uint t) {
|
||||||
|
uint a = t >> 24;
|
||||||
|
if (a == 0u) return;
|
||||||
|
if (gFade > 0.0 && a >= 4u && a != 253u && a != 254u) { // (the alpha byte keeps clear of the markers 1-3, 253, 254)
|
||||||
|
a = uint(float(a) * (1.0 - gFade) + 0.5);
|
||||||
|
if (a < 4u) return;
|
||||||
|
a = min(a, 252u);
|
||||||
|
t = (t & 0x00FFFFFFu) | (a << 24);
|
||||||
|
}
|
||||||
|
const bool additive = a == 3u;
|
||||||
|
if (gFlash != 0u && !additive && a >= 4u) t = (t & 0xFF000000u) | gFlash;
|
||||||
|
if (pc.pass_ == 2) { if (!additive) atomicMin(entdepth[idx], gKey); return; } // depth pass
|
||||||
|
if (!additive && entdepth[idx] != gKey) return; // hidden by a nearer texel
|
||||||
|
if (additive != (pc.pass_ == 1)) return;
|
||||||
|
if (!additive) { entlayer[idx] = a == 1u ? (t | 0x02000000u) & 0xFEFFFFFFu : t; return; }
|
||||||
|
const uint cur = entlayer[idx];
|
||||||
|
if (cur == 0u) { entlayer[idx] = t; return; }
|
||||||
|
const uvec3 s = uvec3((cur >> 16) & 255u, (cur >> 8) & 255u, cur & 255u) + uvec3((t >> 16) & 255u, (t >> 8) & 255u, t & 255u);
|
||||||
|
const uvec3 c = min(s, uvec3(255u));
|
||||||
|
entlayer[idx] = (cur & 0xFF000000u) | (c.r << 16) | (c.g << 8) | c.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_WorkGroupID.x;
|
||||||
|
ivec4 h0 = entinst[4u * id], hi = entinst[4u * id + 1u], hw = entinst[4u * id + 2u];
|
||||||
|
Ent e = ents[h0.x];
|
||||||
|
gKey = (uint(clamp(h0.w + 32768, 0, 65535)) << 16) | (65535u - (id & 65535u));
|
||||||
|
if ((e.st.w & (8 | 4096)) != 0) return; // dead, or its sprites switched off (4096: a creature, main.cpp on_components_enabled)
|
||||||
|
if (e.st.x != 4 && (e.ex8.x & 8192) != 0) return;
|
||||||
|
{ // the hit flash: creatures, the player and physics items (a worm segment, kind 0, keeps its root slot in ex19.w)
|
||||||
|
const int fc = int(e.ex19.w + 0.5);
|
||||||
|
if (e.st.x != 0 && (fc & 7) >= 3) gFlash = (fc & 8) != 0 ? 0xFF0000u : 0xFFFFFFu;
|
||||||
|
}
|
||||||
|
gFade = (e.st.x == 1 || e.st.x == 2 || e.st.x == 3) ? 0.0 : clamp(e.ex10.w, 0.0, 1.0); // INVISIBILITY active (a prop's ex8.xy is the hit point of its leak) (00b015d0 case 0x51: its sprites are told not to draw)
|
||||||
|
{ vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); } // drawn between the last two ticks
|
||||||
|
const bool rigid = (e.st.w & (32 | 64)) != 0; // drawn rotated by the body angle in a.z (64: the part of a Verlet chain, angle in ex7.x)
|
||||||
|
bool mobile = e.st.x != 0 && !rigid;
|
||||||
|
bool moving = mobile && abs(e.a.z) > 0.08;
|
||||||
|
ivec4 an = moving ? hw : hi; // tex, w, h, frames | wait << 16
|
||||||
|
int w = an.y, h = an.z, frames = an.w & 0xFFFF, wait = max(an.w >> 16, 1);
|
||||||
|
int frame = (pc.frame / wait + int(h0.x)) % max(frames, 1);
|
||||||
|
{ // an action the host played (the attacks of the AnimalAI): its animation once from its start frame (row 4 of the instance: texture, frames | wait << 16, start frame, w | h << 16)
|
||||||
|
const ivec4 hx = entinst[4u * id + 3u];
|
||||||
|
const bool loopA = hx.z < 0; // (a negative start: start + 1 negated, a movement animation that loops: jump_up, jump_fall, swim_*, fly_*, burn)
|
||||||
|
const int af = max(hx.y & 0xFFFF, 1), aw = max(hx.y >> 16, 1), t = pc.frame - (loopA ? -hx.z - 1 : hx.z);
|
||||||
|
if (hx.x > 0 && t >= 0 && (loopA || t < af * aw) && (hx.w & 0xFFFF) > 0 && (hx.w >> 16) > 0) { w = hx.w & 0xFFFF; h = hx.w >> 16; an = ivec4(hx.x, w, h, hx.y); frames = af; wait = aw; frame = loopA ? (t / aw) % af : t / aw; } // (its own frame size: the zombie attacks in 19 x 20 frames, stands in 18 x 19; the sprite offset is the file's)
|
||||||
|
}
|
||||||
|
if (rigid) {
|
||||||
|
if (gl_LocalInvocationID.x == 0u) sBody = -1;
|
||||||
|
barrier();
|
||||||
|
for (int k = int(gl_LocalInvocationID.x); k < min(bhdr.x, 1024); k += 64) if (bodies[k].i.y == int(h0.x) && (bodies[k].i.z & 2) == 0) sBody = k;
|
||||||
|
barrier();
|
||||||
|
const int body = (e.st.w & 64) == 0 && w * h <= 4096 ? sBody : -1;
|
||||||
|
int rf = (pc.frame / wait + int(h0.x)) % max(frames, 1);
|
||||||
|
const float ang = (e.st.w & 64) != 0 ? e.ex7.x : e.a.z;
|
||||||
|
const float sx = (e.st.w & 64) != 0 && e.ex7.y > 0.0 ? e.ex7.y : 1.0; // a part stretched along its length (the segments of a limb: IKLimbComponent)
|
||||||
|
float c = cos(ang), s = sin(ang);
|
||||||
|
vec2 anchor = vec2(float(h0.y), float(h0.z));
|
||||||
|
float r = length(max(anchor, vec2(float(w), float(h)) - anchor) * vec2(max(sx, 1.0), 1.0)) + 1.5;
|
||||||
|
int n = int(2.0 * r) + 2;
|
||||||
|
ivec2 lo = ivec2(floor(e.a.xy - vec2(r))) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
int rbase = an.x + rf * w * h;
|
||||||
|
for (int i = int(gl_LocalInvocationID.x); i < n * n; i += 64) {
|
||||||
|
ivec2 p = lo + ivec2(i % n, i / n);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
|
||||||
|
vec2 d = vec2(p + ivec2(pc.view_x, pc.view_y)) + vec2(0.5) - e.a.xy;
|
||||||
|
ivec2 sp = ivec2(floor(anchor + vec2((c * d.x + s * d.y) / sx, -s * d.x + c * d.y)));
|
||||||
|
if (sp.x < 0 || sp.y < 0 || sp.x >= w || sp.y >= h) continue;
|
||||||
|
uint t = enttex[uint(rbase + sp.y * w + sp.x)];
|
||||||
|
if (body >= 0 && (t >> 24) > 3u) { // 0070e060: a burnt solid loses one of every channel per eight of its burn age
|
||||||
|
const uint bst = burnpx[uint(body) * 4096u + uint(sp.y * w + sp.x)];
|
||||||
|
const uint bm = uint(e.ex5.z + 0.5); // (the body's material + 1, as rigid_burn.comp reads it)
|
||||||
|
if ((bst & 0x80000000u) != 0u && bm > 0u) { put_texel(p.y * pc.gw + p.x, burning_texel(bm - 1u, hash3(uint(sp.x + sp.y * w), uint(body), uint(pc.frame)))); continue; } // a burning pixel: the fire colours
|
||||||
|
const uint dk = (bst & 0xFFFFu) >> 3;
|
||||||
|
if (dk > 0u) { const uvec3 c3 = uvec3((t >> 16) & 255u, (t >> 8) & 255u, t & 255u); const uvec3 r3 = uvec3(max(ivec3(c3) - int(dk), ivec3(0))); t = (t & 0xFF000000u) | (r3.r << 16) | (r3.g << 8) | r3.b; }
|
||||||
|
}
|
||||||
|
put_texel(p.y * pc.gw + p.x, t); // alpha 1 is the marker of additive light in render.comp: faint pixels (the halo of the portal) take alpha 2
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool flip = mobile && e.st.y < 0;
|
||||||
|
ivec2 pos = ivec2(floor(e.a.xy));
|
||||||
|
int ox = flip ? (w - 1 - h0.y) : h0.y;
|
||||||
|
ivec2 origin = pos - ivec2(ox, h0.z) - ivec2(pc.view_x, pc.view_y); // window position of the sprite top-left
|
||||||
|
if (origin.x >= pc.gw || origin.y >= pc.gh || origin.x + w <= 0 || origin.y + h <= 0) return;
|
||||||
|
int base = an.x + frame * w * h;
|
||||||
|
for (int i = int(gl_LocalInvocationID.x); i < w * h; i += 64) {
|
||||||
|
int sxp = i % w;
|
||||||
|
ivec2 p = origin + ivec2(sxp, i / w);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
|
||||||
|
uint t = enttex[uint(base + (flip ? (i / w) * w + (w - 1 - sxp) : i))];
|
||||||
|
if (h0.x == 0u && (t >> 24) == 255u) t = (t & 0x00FFFFFFu) | 0xFE000000u; // the player's pixels (alpha 254): render.comp draws them at the sub-cell position
|
||||||
|
put_texel(p.y * pc.gw + p.x, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "areas.glsl"
|
||||||
|
#include "far.glsl"
|
||||||
|
|
||||||
|
// Terrain generation on the GPU: GLSL port of the engine's wang-image -> cell path
|
||||||
|
// (docs/ENGINE_ANALYSIS.md section 18). One invocation per grid cell.
|
||||||
|
layout(std430, set = 0, binding = 6) readonly buffer Field { float field[]; }; // wang solidity plane
|
||||||
|
layout(std430, set = 0, binding = 7) readonly buffer MatPlane { uint matplane[]; }; // CellData index + 1, 0 = none
|
||||||
|
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint perm[]; }; // [0,512) simplex, [512,1024) perlin
|
||||||
|
layout(std430, set = 0, binding = 9) readonly buffer MatParams { vec2 mparams[]; }; // per material: warp, threshold
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 10) readonly buffer CellsOld { uint cells_old[]; };
|
||||||
|
layout(std430, set = 0, binding = 11) readonly buffer ExtOld { vec2 ext_old[]; };
|
||||||
|
layout(std430, set = 0, binding = 52) readonly buffer BirthOld { uint birth_old[]; }; // the ages before the scroll
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 12) readonly buffer Scene { uint scene[]; }; // pixel scenes: 0 = none, else material + 1
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 18) readonly buffer Override { uint ovr[]; }; // cells restored from the persistent store (0xFFFFFFFF = none)
|
||||||
|
|
||||||
|
int ffloor(float v) { return int(floor(v)); }
|
||||||
|
float fade(float t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
|
||||||
|
float sstep(float t) { return (3.0 - t * 2.0) * t * t; }
|
||||||
|
|
||||||
|
float sgrad(uint hsh, float x, float y) {
|
||||||
|
uint h = hsh & 7u;
|
||||||
|
float u = h < 4u ? x : y;
|
||||||
|
float v = h < 4u ? y : x;
|
||||||
|
return (((h & 1u) != 0u) ? -u : u) + (((h & 2u) != 0u) ? -2.0 * v : 2.0 * v);
|
||||||
|
}
|
||||||
|
uint ps(uint i) { return perm[i]; }
|
||||||
|
uint pp(uint i) { return perm[512u + i]; }
|
||||||
|
|
||||||
|
float simplex2(float x, float y) {
|
||||||
|
const float F2 = 0.366025404, G2 = 0.211324865;
|
||||||
|
float s = (x + y) * F2;
|
||||||
|
int i = ffloor(x + s), j = ffloor(y + s);
|
||||||
|
float t = float(i + j) * G2;
|
||||||
|
float x0 = x - (float(i) - t), y0 = y - (float(j) - t);
|
||||||
|
int i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1;
|
||||||
|
float x1 = x0 - float(i1) + G2, y1 = y0 - float(j1) + G2;
|
||||||
|
float x2 = x0 - 1.0 + 2.0 * G2, y2 = y0 - 1.0 + 2.0 * G2;
|
||||||
|
uint ii = uint(i) & 255u, jj = uint(j) & 255u;
|
||||||
|
float n0 = 0.0, n1 = 0.0, n2 = 0.0;
|
||||||
|
float t0 = 0.5 - x0 * x0 - y0 * y0;
|
||||||
|
if (t0 >= 0.0) { t0 *= t0; n0 = t0 * t0 * sgrad(ps(ii + ps(jj)), x0, y0); }
|
||||||
|
float t1 = 0.5 - x1 * x1 - y1 * y1;
|
||||||
|
if (t1 >= 0.0) { t1 *= t1; n1 = t1 * t1 * sgrad(ps(ii + uint(i1) + ps(jj + uint(j1))), x1, y1); }
|
||||||
|
float t2 = 0.5 - x2 * x2 - y2 * y2;
|
||||||
|
if (t2 >= 0.0) { t2 *= t2; n2 = t2 * t2 * sgrad(ps(ii + 1u + ps(jj + 1u)), x2, y2); }
|
||||||
|
return 40.0 * (n0 + n1 + n2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8 gradients of FUN_00872a20
|
||||||
|
vec2 pgrad(uint g) {
|
||||||
|
const vec2 G[8] = vec2[8](vec2(-1, 0), vec2(1, 0), vec2(0, -1), vec2(0, 1), vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(1, 1));
|
||||||
|
return G[g];
|
||||||
|
}
|
||||||
|
|
||||||
|
float perlin2(float x, float y) {
|
||||||
|
int ix = ffloor(x), iy = ffloor(y);
|
||||||
|
uint X = uint(ix) & 255u, Y = uint(iy) & 255u, X1 = uint(ix + 1) & 255u, Y1 = uint(iy + 1) & 255u;
|
||||||
|
float fx = x - float(ix), fy = y - float(iy);
|
||||||
|
uint pa = pp(X), pb = pp(X1);
|
||||||
|
float na = dot(pgrad(pp(pa + Y) & 7u), vec2(fx, fy));
|
||||||
|
float nb = dot(pgrad(pp(pb + Y) & 7u), vec2(fx - 1.0, fy));
|
||||||
|
float nc = dot(pgrad(pp(pa + Y1) & 7u), vec2(fx, fy - 1.0));
|
||||||
|
float nd = dot(pgrad(pp(pb + Y1) & 7u), vec2(fx - 1.0, fy - 1.0));
|
||||||
|
float v = fade(fy), u = fade(fx);
|
||||||
|
return mix(mix(na, nc, v), mix(nb, nd, v), u);
|
||||||
|
}
|
||||||
|
|
||||||
|
float value2(float x, float y) {
|
||||||
|
int ix = ffloor(x), iy = ffloor(y);
|
||||||
|
uint X = uint(ix) & 255u, Y = uint(iy) & 255u, X1 = uint(ix + 1) & 255u, Y1 = uint(iy + 1) & 255u;
|
||||||
|
float fx = x - float(ix), fy = y - float(iy);
|
||||||
|
uint pa = pp(X), pb = pp(X1);
|
||||||
|
float v00 = float(pp(pa + Y)) / 255.0, v01 = float(pp(pa + Y1)) / 255.0;
|
||||||
|
float v10 = float(pp(pb + Y)) / 255.0, v11 = float(pp(pb + Y1)) / 255.0;
|
||||||
|
float v = fade(fy), u = fade(fx);
|
||||||
|
return (mix(mix(v00, v01, v), mix(v10, v11, v), u) - 0.5) * 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wrapi(int v, int n) { v = v % n; return v < 0 ? v + n : v; }
|
||||||
|
int gpw, gph, gfo, gmo; // the area of the cell being generated: plane size and offsets
|
||||||
|
float field_at(int x, int y) { return field[gfo + wrapi(y, gph) * gpw + wrapi(x, gpw)]; }
|
||||||
|
|
||||||
|
// FUN_008712d0: nearest wang pixel (smoothstep(f) >= 0.5 rounds up); CellData index or -1
|
||||||
|
int material_pixel(float px, float py) {
|
||||||
|
int ix = ffloor(px), iy = ffloor(py);
|
||||||
|
float fx = px - float(ix), fy = py - float(iy);
|
||||||
|
if (!(0.5 > sstep(fy))) ++iy;
|
||||||
|
if (!(0.5 > sstep(fx))) ++ix;
|
||||||
|
return int(matplane[gmo + wrapi(iy, gph) * gpw + wrapi(ix, gpw)]) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUN_00870e60: smoothstep-weighted bilinear sample of the solidity plane
|
||||||
|
float bilinear(float px, float py) {
|
||||||
|
int ix = ffloor(px), iy = ffloor(py);
|
||||||
|
float sx = sstep(px - float(ix)), sy = sstep(py - float(iy));
|
||||||
|
float top = mix(field_at(ix, iy), field_at(ix + 1, iy), sx);
|
||||||
|
float bot = mix(field_at(ix, iy + 1), field_at(ix + 1, iy + 1), sx);
|
||||||
|
return mix(top, bot, sy);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- biome <Materials> evaluator (FUN_0086d2a0) and its noise: 70-scaled simplex (FUN_00872300), 4-octave fbm (FUN_009296d0), polka dots (FUN_00872140)
|
||||||
|
#include "procgrad.glsl"
|
||||||
|
// FUN_0086fd40 / FUN_0086fe30: the cell hash (one value, and three more from the same product)
|
||||||
|
float cell_u(vec2 c) {
|
||||||
|
float cx = floor(c.x * 0.014084507), cy = floor(c.y * 0.014084507);
|
||||||
|
float a = c.x - cx * 71.0 + 26.0, b = c.y - cy * 71.0 + 161.0;
|
||||||
|
return b * b * (a * a);
|
||||||
|
}
|
||||||
|
float polka(float x, float y, float rlow, float rhigh, bool boxed, float prob) {
|
||||||
|
vec2 c = vec2(floor(x), floor(y));
|
||||||
|
float u = cell_u(c);
|
||||||
|
if (fract(u * 0.0010131986) >= prob) return 0.0;
|
||||||
|
float fx = x - c.x, fy = y - c.y;
|
||||||
|
float o0 = fract(u * 0.0010513747), o1 = fract(u * 0.0015553312), o2 = fract(u * 0.0012450161);
|
||||||
|
float r = (rhigh - rlow) * o2 + rlow;
|
||||||
|
if (r <= 0.0) return 0.0;
|
||||||
|
float inv = 2.0 / r;
|
||||||
|
float dx = fx * inv - (inv - 1.0) + (inv - 2.0) * o0;
|
||||||
|
float dy = fy * inv - (inv - 1.0) + (inv - 2.0) * o1;
|
||||||
|
float d;
|
||||||
|
if (boxed) { d = dy * dy; d *= d; float e = dx * dx; d += e * e; if (d > 1.0) return 0.0; }
|
||||||
|
else { d = min(dx * dx + dy * dy, 1.0); }
|
||||||
|
float q = 1.0 - d;
|
||||||
|
return q * q * q;
|
||||||
|
}
|
||||||
|
// area a's component list: 20 words each (see AreaStore): [0] material, [1] flags, [2..3] min max, [4..5] add_perlin scale, [6..7] limit min max, [8..9] rare scale,
|
||||||
|
// [10..11] rare offset, [12..13] required min max, [14..15] polka radius low high, [16] polka probability
|
||||||
|
uint eval_materials_at(int base, int cnt, float v, ivec2 w) {
|
||||||
|
float fx = float(w.x), fy = float(w.y);
|
||||||
|
for (int k = 0; k < cnt; ++k) {
|
||||||
|
int o = base + 20 * k;
|
||||||
|
uint mat = atab[o];
|
||||||
|
uint fl = atab[o + 1];
|
||||||
|
if (mat == 0xFFFFFFFFu) continue;
|
||||||
|
if ((fl & 1u) != 0u && (fy < uintBitsToFloat(atab[o + 6]) || fy > uintBitsToFloat(atab[o + 7]))) continue;
|
||||||
|
float vv = v;
|
||||||
|
if ((fl & 2u) != 0u) vv += simplex70(fx * uintBitsToFloat(atab[o + 4]), fy * uintBitsToFloat(atab[o + 5]));
|
||||||
|
if (!(vv >= uintBitsToFloat(atab[o + 2]) && vv < uintBitsToFloat(atab[o + 3]))) continue;
|
||||||
|
if ((fl & 4u) == 0u) return mat;
|
||||||
|
float px = uintBitsToFloat(atab[o + 10]) + fx * uintBitsToFloat(atab[o + 8]);
|
||||||
|
float py = uintBitsToFloat(atab[o + 11]) + fy * uintBitsToFloat(atab[o + 9]);
|
||||||
|
float rmin = uintBitsToFloat(atab[o + 12]), rmax = uintBitsToFloat(atab[o + 13]);
|
||||||
|
if ((fl & 8u) != 0u) {
|
||||||
|
float n = (fl & 16u) != 0u ? fbm70(px, py) : simplex70(px, py);
|
||||||
|
if (rmin >= n || n > rmax) continue;
|
||||||
|
}
|
||||||
|
if ((fl & 32u) == 0u) return mat;
|
||||||
|
float n2 = polka(px, py, uintBitsToFloat(atab[o + 14]), uintBitsToFloat(atab[o + 15]), (fl & 64u) != 0u, uintBitsToFloat(atab[o + 16]));
|
||||||
|
if (rmin < n2 && n2 <= rmax) return mat;
|
||||||
|
}
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
uint eval_materials(int a, float v, ivec2 w) { return eval_materials_at(arec(a, 14), arec(a, 15), v, w); }
|
||||||
|
|
||||||
|
// ---- procedural biomes (FUN_0087e110): gradient between the biome's y limits, inside perlin, noise-type blend; the BitmapCaves image is not generated (value 1 everywhere)
|
||||||
|
|
||||||
|
float sin_big(float t) { // sin of a large float: reduce by 2*pi in two steps first (the GPU sin loses its accuracy for large arguments)
|
||||||
|
float k = floor(t * 0.15915494309189535 + 0.5);
|
||||||
|
float r = fma(-k, 6.28125, t);
|
||||||
|
r = fma(-k, 0.0019353071795864769, r);
|
||||||
|
return sin(r);
|
||||||
|
}
|
||||||
|
float iq_hash(float n) { return fract(sin_big(n) * 43758.547); }
|
||||||
|
float iq_mix(float n, float sx, float sy) {
|
||||||
|
float a = iq_hash(n), b = iq_hash(n + 1.0), c = iq_hash(n + 57.0), d = iq_hash(n + 58.0);
|
||||||
|
return mix(mix(a, b, sx), mix(c, d, sx), sy);
|
||||||
|
}
|
||||||
|
float iq_noise(float x, float y) { // FUN_00871630
|
||||||
|
float ix = floor(x), iy = floor(y);
|
||||||
|
float fx = x - ix, fy = y - iy;
|
||||||
|
return iq_mix(iy * 57.0 + ix, (3.0 - 2.0 * fx) * fx * fx, (3.0 - 2.0 * fy) * fy * fy);
|
||||||
|
}
|
||||||
|
float iq_smooth_par(float f, int i) { return (i % 2 == 0) ? (3.0 - 2.0 * f) * f * f : f; }
|
||||||
|
float iq_noise2(float x, float y) { // FUN_00871850: the interpolation weight depends on the parity of the cell
|
||||||
|
float ix = floor(x), iy = floor(y);
|
||||||
|
float fx = x - ix, fy = y - iy;
|
||||||
|
int jx = int(ix), jy = int(iy);
|
||||||
|
float sx0 = iq_smooth_par(fx, jx), sx1 = iq_smooth_par(fx, jx + 1);
|
||||||
|
float sy0 = iq_smooth_par(fy, jy), sy1 = iq_smooth_par(fy, jy + 1);
|
||||||
|
return iq_mix(iy * 57.0 + ix, (sx1 - sx0) * fx + sx0, (sy1 - sy0) * fy + sy0);
|
||||||
|
}
|
||||||
|
float noise_t(int t, float x, float y) { // FUN_00873c00 (types 0-4 are not used by any biome of the game)
|
||||||
|
if (t == 5) return simplex70(x, y);
|
||||||
|
if (t == 6) return perlin2(x, y);
|
||||||
|
if (t == 7) return value2(x, y);
|
||||||
|
if (t == 8) return simplex2(x, y);
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
float fbm_t(int t, float x, float y) { // FUN_00873cc0
|
||||||
|
const vec4 M = vec4(0.84147, 0.5403, 0.5403, -0.84147);
|
||||||
|
const float K[3] = float[3](2.02, 2.33, 2.01);
|
||||||
|
float sum = noise_t(t, x, y) * 0.5, wgt = 0.25;
|
||||||
|
for (int o = 0; o < 3; ++o) {
|
||||||
|
float nx = (M.x * x + M.z * y) * K[o], ny = (M.y * x + M.w * y) * K[o];
|
||||||
|
x = nx; y = ny;
|
||||||
|
sum += noise_t(t, x, y) * wgt;
|
||||||
|
wgt *= 0.5;
|
||||||
|
}
|
||||||
|
return sum / 0.9375;
|
||||||
|
}
|
||||||
|
float inside_perlin(uint rec, float X, float Y) { // FUN_0087e7a0
|
||||||
|
uint fl = atab[rec];
|
||||||
|
float x = X * 0.05243442 * tf(rec, 1) + 0.1 + tf(rec, 5);
|
||||||
|
float y = Y * 0.05243442 * tf(rec, 2) + 0.1 + tf(rec, 6);
|
||||||
|
if ((fl & 64u) != 0u) { float sd = float(atab[5]); x += sd * 1.312e-5; y += sd * 2.331e-5; }
|
||||||
|
int it = int((fl >> 12) & 15u);
|
||||||
|
float n = (fl & 2u) != 0u ? fbm_t(it, x, y) : noise_t(it, x, y);
|
||||||
|
float v = n;
|
||||||
|
if ((fl & 4u) != 0u) v = n * n;
|
||||||
|
float mn = tf(rec, 3), mx = tf(rec, 4);
|
||||||
|
if ((fl & 8u) != 0u) { if (!(mx > v)) v = mx; if (!(v > mn)) v = mn; }
|
||||||
|
if ((fl & 16u) != 0u) v = (v * 0.5 + 0.5) * (mx - mn) + mn;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
float cave_at(uint base, int cw, int ch, int x, int y) { // the cave image: both axes wrap
|
||||||
|
x = ((x % cw) + cw) % cw; y = ((y % ch) + ch) % ch;
|
||||||
|
return field[base + uint(y * cw + x)];
|
||||||
|
}
|
||||||
|
float cave_sample(uint base, int cw, int ch, float xf, float yf) { // FUN_00870e60
|
||||||
|
int ix = int(floor(xf)), iy = int(floor(yf));
|
||||||
|
float fx = xf - float(ix), fy = yf - float(iy);
|
||||||
|
float sx = (3.0 - fx * 2.0) * (fx * fx), sy = (3.0 - fy * 2.0) * (fy * fy);
|
||||||
|
float a = cave_at(base, cw, ch, ix, iy), b = cave_at(base, cw, ch, ix + 1, iy), c = cave_at(base, cw, ch, ix, iy + 1), d = cave_at(base, cw, ch, ix + 1, iy + 1);
|
||||||
|
float top = (b - a) * sx + a, bot = (d - c) * sx + c;
|
||||||
|
return (bot - top) * sy + top;
|
||||||
|
}
|
||||||
|
float proc_value(ivec2 w, uint rec) {
|
||||||
|
float X = float(w.x), Y = float(w.y);
|
||||||
|
float v = 1.0;
|
||||||
|
if (atab[rec + 18u] != 0xFFFFFFFFu) { // the cave image of the biome (see proc_biome.cpp)
|
||||||
|
uint cw = atab[rec + 19u] & 0xFFFFu, chh = atab[rec + 19u] >> 16;
|
||||||
|
float px = (X * 0.49162514 + 2560.0) * 0.1, py = (2560.0 * 6.86035959282328e-07 + Y * 0.49162514) * 0.1;
|
||||||
|
v = cave_sample(atab[rec + 18u], int(cw), int(chh), px, py);
|
||||||
|
if (1.0 > v) v = v + simplex70(px, py) * ((1.0 - v) * 0.495);
|
||||||
|
if (0.0 >= v) return 0.0;
|
||||||
|
}
|
||||||
|
v = proc_gradient(w, rec) * v;
|
||||||
|
float s8 = 0.0, s18 = 0.0, s1c = 0.0;
|
||||||
|
float mulp = tf(rec, 9);
|
||||||
|
uint fl = atab[rec];
|
||||||
|
if (v > 0.51 && mulp != 0.0) { s18 = (v - 0.5) * 2.0; s8 = inside_perlin(rec, X, Y); }
|
||||||
|
if (v > 0.85) {
|
||||||
|
s1c = tf(rec, 7);
|
||||||
|
float nz;
|
||||||
|
if (((fl >> 8) & 15u) == 3u) { // SIN_CAPPED_SIMPLEX
|
||||||
|
float amp = iq_noise(X * 0.02, Y * 0.021) * 0.05 + 0.05;
|
||||||
|
float t1 = sin(Y * 0.0009765625) * 1024.0 * 0.5 * 0.06 * amp, t2 = cos(X * 0.0009765625) * 1024.0 * 0.5 * 0.06 * amp;
|
||||||
|
nz = float(simplex70(t2, t1)) * v;
|
||||||
|
} else { // the default noise type
|
||||||
|
float amp = iq_noise2(X * 0.025, Y * 0.025) * 0.05 + 0.05;
|
||||||
|
nz = simplex2(X * 0.01 * amp, Y * 0.01 * amp) * v;
|
||||||
|
}
|
||||||
|
v = v + (nz - v) * ((v - 0.85) / 0.1);
|
||||||
|
}
|
||||||
|
if ((fl & 32u) != 0u) s8 = ((v * s8) - s8) * (Y * 0.001) + s8;
|
||||||
|
return mulp * s18 * s8 + tf(rec, 8) * v + s1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe mode (shift_mode 2, docs: the teleport destination test): the world cells listed in probe[] get their cell word: from the live window when inside it
|
||||||
|
// (flag 4, flag 2 when the cell is falling: ext.x > 0), else generated as the window would generate them (flag 1 = an area is loaded there).
|
||||||
|
layout(std430, set = 0, binding = 53) buffer Probe { uvec4 phead; ivec4 probe[]; };
|
||||||
|
bool g_unloaded = false;
|
||||||
|
|
||||||
|
uint gen_cell(ivec2 w) {
|
||||||
|
uint hh = hash3(uint(w.x), uint(w.y), 77u);
|
||||||
|
uint var = (hh >> 8) & 0xFFu;
|
||||||
|
// LoadPixelScene images override the terrain (the scenes of every loaded area: a scene may reach into a neighbour)
|
||||||
|
bool forceSolid = false;
|
||||||
|
int whiteTile = -1; // the biome-map tile whose biome the white pixels of the scene use (the scene's origin)
|
||||||
|
for (int a = 0; a < int(atab[0]); ++a) {
|
||||||
|
int si = scene_index(a, w);
|
||||||
|
if (si < 0) continue;
|
||||||
|
uint sv = scene[si];
|
||||||
|
if (sv >= 0xE000u) { forceSolid = true; whiteTile = sv == 0xFFFFu ? -1 : int(sv - 0xE000u); break; }
|
||||||
|
if (sv != 0u) { return (sv == 1u ? 0u : ((sv - 1u) | (var << 16))); }
|
||||||
|
}
|
||||||
|
const int ai = area_of_cell(w);
|
||||||
|
if (ai < 0) { g_unloaded = true; return uint(pc.m_rock) | (var << 16); } // no area loaded here
|
||||||
|
if (forceSolid) { // a white scene pixel: solid, the material from the <Materials> of the biome with the wang value 1
|
||||||
|
float xw = float(w.x), yw = float(w.y);
|
||||||
|
float n3 = value2(xw * 0.035, yw * 0.07) * 15.5;
|
||||||
|
float s = simplex2((xw + n3) * 0.0489275, (yw + n3) * 0.0489275);
|
||||||
|
const float wv = max(0.5, s * (0.0625 * 5.35 * 0.95) + 1.0);
|
||||||
|
uint fm;
|
||||||
|
const uint wrec = whiteTile >= 0 ? tile_rec(whiteTile % int(atab[2]), whiteTile / int(atab[2])) : 0xFFFFFFFFu;
|
||||||
|
if (wrec != 0xFFFFFFFFu && atab[wrec + 22u] != 0u) fm = eval_materials_at(int(atab[wrec + 21u]), int(atab[wrec + 22u]), wv, w); // the biome of the scene origin
|
||||||
|
else fm = eval_materials(ai, wv, w);
|
||||||
|
return fm == 0u ? 0u : (fm | (var << 16));
|
||||||
|
}
|
||||||
|
{ // a procedural biome (no wang template): the value function, then the <Materials> list (FUN_0087d0e0, type 0)
|
||||||
|
const uint trec = topo_rec(w);
|
||||||
|
if (trec != 0xFFFFFFFFu && (atab[trec] & 1u) != 0u) {
|
||||||
|
const uint pm = eval_materials(ai, proc_value(w, trec), w);
|
||||||
|
return pm == 0u ? 0u : (pm | (var << 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gpw = arec(ai, 2); gph = arec(ai, 3); gfo = arec(ai, 4); gmo = arec(ai, 5);
|
||||||
|
ivec2 o = w - ivec2(arec(ai, 0), arec(ai, 1)); // cell relative to the centre of wang pixel (0,0)
|
||||||
|
float px = (float(o.x) + 0.5) * 0.1, py = (float(o.y) + 0.5) * 0.1;
|
||||||
|
|
||||||
|
if (py < -0.5) { return 0u; } // open sky above the area
|
||||||
|
// the last wang pixel is 5 cells short of the right / bottom edge of the area's tiles: those cells continue the edge pixel (a 13 cell wall of rock stood there)
|
||||||
|
px = clamp(px, -0.5, float(gpw) - 0.5001); py = clamp(py, -0.5, float(gph) - 0.5001);
|
||||||
|
|
||||||
|
float warp = 1.0, thr = 0.5;
|
||||||
|
int m0 = material_pixel(px, py);
|
||||||
|
if (m0 > 0) { vec2 mp = mparams[m0]; warp = mp.x; thr = mp.y; }
|
||||||
|
|
||||||
|
float qx = px, qy = py;
|
||||||
|
if (warp > 0.0) {
|
||||||
|
float xf = float(o.x), yf = float(o.y);
|
||||||
|
float n1 = simplex2(xf * 0.13715, yf * 0.13717);
|
||||||
|
float a = n1 * 0.45 + 0.1;
|
||||||
|
float xs = xf * 0.11111111, ys = yf * 0.11111111;
|
||||||
|
float n2 = perlin2(ys, xs);
|
||||||
|
float t = ((1.0 - a) * 0.33 + 0.111) * warp;
|
||||||
|
float n3 = value2(xs, ys);
|
||||||
|
qx = px + n3 * (a * warp);
|
||||||
|
qy = py + n2 * t;
|
||||||
|
}
|
||||||
|
float f = bilinear(qx, qy);
|
||||||
|
if (f < thr) { return 0u; }
|
||||||
|
|
||||||
|
int m1 = material_pixel(qx, qy);
|
||||||
|
uint mat;
|
||||||
|
if (m1 > 0) {
|
||||||
|
mat = uint(m1);
|
||||||
|
} else {
|
||||||
|
// the biome's <Materials>: FUN_0087d010 value, then the first matching component (none = air)
|
||||||
|
float xw = float(w.x), yw = float(w.y);
|
||||||
|
float n = value2(xw * 0.035, yw * 0.07);
|
||||||
|
float n3 = n * 15.5;
|
||||||
|
float h = (f - 0.5) * 0.5;
|
||||||
|
float s = simplex2((xw + n3) * 0.0489275, (yw + n3) * 0.0489275);
|
||||||
|
float v = max(0.5, s * (h * h * 5.35 * 0.95) + f);
|
||||||
|
mat = eval_materials(ai, v, w);
|
||||||
|
}
|
||||||
|
return mat == 0u ? 0u : (mat | (var << 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The world rules of the cells that enter the window: the header in the tables before the 4 colour-grading words (main.cpp kWorldRules): [0] EVERYTHING_TO_GOLD,
|
||||||
|
// [1] gold, [2] gold_static; the cuts in binding 55: [0] count, then from word 4, 12 words per GameCutThroughWorldVertical (x, y_min, y_max, radius, edge, the engine's
|
||||||
|
// loaded region at the call: x0, y0, x1, y1, seed), every cut of the run. A generated cell (one that never was in the window) inside a cut's region takes the cut (007392d0: off = floor(3 sin(0.02 y)
|
||||||
|
// +- 0.5), air for left < x <= right, the static stainable cells within edge darkened by (1 - sqrt(d / edge)) x (0.4 + 0.3 rand), as the burn level of bits 28-30);
|
||||||
|
// with EVERYTHING_TO_GOLD every non-air cell is gold (the static ones gold_static), as the engine's redefined materials (00704f70)
|
||||||
|
const uint kWorldRules = 4u;
|
||||||
|
layout(std430, set = 0, binding = 55) readonly buffer CutRules { uint cuts[]; };
|
||||||
|
uint cut_hash(uint a, uint b) { uint h = a * 0x9E3779B1u ^ (b + 0x7F4A7C15u); h ^= h >> 15; h *= 0x2C1B3C6Du; h ^= h >> 12; h *= 0x297A2D39u; h ^= h >> 15; return h; }
|
||||||
|
uint rules_word(uint c, ivec2 w, bool fresh) {
|
||||||
|
const uint wb = uint(perm.length()) - 4u - kWorldRules;
|
||||||
|
if (fresh) {
|
||||||
|
const uint n = min(cuts[0], (uint(cuts.length()) - 4u) / 12u);
|
||||||
|
for (uint k = 0u; k < n; ++k) {
|
||||||
|
const uint b = 4u + 12u * k;
|
||||||
|
const int x = int(cuts[b]), y0 = int(cuts[b + 1u]), y1 = int(cuts[b + 2u]), r = int(cuts[b + 3u]), edge = int(cuts[b + 4u]);
|
||||||
|
if (w.y < y0 || w.y >= y1 || w.x < x - r - edge || w.x >= x + r + edge) continue;
|
||||||
|
if (w.x < int(cuts[b + 5u]) || w.y < int(cuts[b + 6u]) || w.x >= int(cuts[b + 7u]) || w.y >= int(cuts[b + 8u])) continue;
|
||||||
|
const float v = sin(float(w.y) * 0.02) * 3.0;
|
||||||
|
const int off = int(floor(v + (v < 0.0 ? -0.5 : 0.5)));
|
||||||
|
const int left = x - r + off, right = x + r + off;
|
||||||
|
if (w.x > left && w.x <= right) { c = 0u; continue; }
|
||||||
|
const uint m = cell_mat(c);
|
||||||
|
if (edge <= 0 || m == 0u || (mflags(m) & F_STATIC) == 0u || (mflags(m) & F_NOSTAIN) != 0u || (c & BURN_BIT) != 0u) continue;
|
||||||
|
const float rnd = 0.4 + 0.3 * float(cut_hash(uint(w.y), cuts[b + 9u]) & 0xFFFFu) / 65536.0;
|
||||||
|
const float d = float(w.x <= left ? left - w.x : w.x - right);
|
||||||
|
const float s = max(0.0, (1.0 - sqrt(d / float(edge))) * rnd);
|
||||||
|
const float keep = (1.0 - float((c >> 28) & 7u) / 7.0) * (1.0 - s);
|
||||||
|
const uint L = uint(clamp(int(round((1.0 - keep) * 7.0)), 0, 7));
|
||||||
|
c = (c & ~(7u << 28)) | (L << 28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (perm[wb] != 0u) {
|
||||||
|
const uint m = cell_mat(c);
|
||||||
|
if (m != 0u && m != perm[wb + 1u] && m != perm[wb + 2u]) c = ((mflags(m) & F_STATIC) != 0u ? perm[wb + 2u] : perm[wb + 1u]) | (c & (0xFFu << 16));
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
// The spell test room (main.cpp --spelltest; cut rules words 1..3): every generated cell is rock, but the hall (x0 <= x < x1, y0 <= y < y1) is air inside 16 cells of
|
||||||
|
// temple brick
|
||||||
|
uint spell_room(uint c, ivec2 w) {
|
||||||
|
if (cuts[1] == 0u) return c;
|
||||||
|
const ivec2 a = ivec2(int(cuts[2] << 16) >> 16, int(cuts[2]) >> 16), b = ivec2(int(cuts[3] << 16) >> 16, int(cuts[3]) >> 16);
|
||||||
|
const uint var = (cut_hash(uint(w.x), uint(w.y)) >> 8) & 0xFFu;
|
||||||
|
if (w.x >= a.x && w.x < b.x && w.y >= a.y && w.y < b.y) return 0u;
|
||||||
|
if (w.x >= a.x - 16 && w.x < b.x + 16 && w.y >= a.y - 16 && w.y < b.y + 16) return (cuts[1] >> 16) | (var << 16);
|
||||||
|
return (cuts[1] & 0xFFFFu) | (var << 16);
|
||||||
|
}
|
||||||
|
void world_rules(int i, ivec2 w, bool fresh) { const uint c = rules_word(cells[i], w, fresh); if (c != cells[i]) { cells[i] = c; ext[i] = vec2(0.0); } }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (pc.shift_mode == 3) { // far blocks (far.glsl): job z generates its block's cells into its slot - a store-backed block keeps the cells the store had
|
||||||
|
const int j = int(gl_GlobalInvocationID.z);
|
||||||
|
if (j >= farmap[kFarJobCount]) return;
|
||||||
|
const int slot = farmap[kFarJobList + 4 * j], bx = farmap[kFarJobList + 4 * j + 1], by = farmap[kFarJobList + 4 * j + 2], kept = farmap[kFarJobList + 4 * j + 3];
|
||||||
|
const ivec2 w = ivec2(bx * 64, by * 64) + ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
const int fi = slot * 4096 + (w.y & 63) * 64 + (w.x & 63);
|
||||||
|
if (kept != 0 && farcells[fi] != 0xFFFFFFFFu) return;
|
||||||
|
farcells[fi] = rules_word(spell_room(gen_cell(w), w), w, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pc.shift_mode == 2) {
|
||||||
|
const uint q = gl_GlobalInvocationID.y * gl_NumWorkGroups.x * 8u + gl_GlobalInvocationID.x;
|
||||||
|
if (q >= phead.x) return;
|
||||||
|
const ivec2 w = probe[q].xy;
|
||||||
|
const ivec2 lp = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (lp.x >= 0 && lp.y >= 0 && lp.x < pc.gw && lp.y < pc.gh) {
|
||||||
|
const int li = lp.y * pc.gw + lp.x;
|
||||||
|
probe[q].z = int(cells[li]);
|
||||||
|
probe[q].w = 1 | 4 | (ext[li].x > 0.0 ? 2 : 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_unloaded = false;
|
||||||
|
probe[q].z = int(rules_word(spell_room(gen_cell(w), w), w, true));
|
||||||
|
probe[q].w = g_unloaded ? 0 : 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||||
|
int i = p.y * pc.gw + p.x;
|
||||||
|
if (pc.shift_mode == 1) { // scrolling: reuse the simulated overlap, generate only the exposed strips
|
||||||
|
ivec2 src = p + ivec2(pc.shift_x, pc.shift_y);
|
||||||
|
if (src.x >= 0 && src.y >= 0 && src.x < pc.gw && src.y < pc.gh) {
|
||||||
|
cells[i] = cells_old[src.y * pc.gw + src.x];
|
||||||
|
ext[i] = ext_old[src.y * pc.gw + src.x];
|
||||||
|
birth[i] = birth_old[src.y * pc.gw + src.x]; // a cell that stays keeps its age (0070d1e0: the age belongs to the cell)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
birth[i] = birth_frame();
|
||||||
|
if (ovr[i] != 0xFFFFFFFFu) { // a cell that comes back from the store (only its word is kept): a burning cell or a flame gets the fade a new cell of its material has
|
||||||
|
// (0070f450: the material's temperature_of_fire), so a fire that went out of the window goes on when it comes back instead of dying at its next update
|
||||||
|
const uint oc = ovr[i];
|
||||||
|
cells[i] = oc; ext[i] = vec2(0.0);
|
||||||
|
const uint om = cell_mat(oc);
|
||||||
|
if (om != 0u && ((oc & BURN_BIT) != 0u || (mflags(om) & F_FIRE) != 0u)) birth[i] = with_fade(birth_frame(), int((mats[3u * om + 2u].w >> 8) & 255u));
|
||||||
|
world_rules(i, p + ivec2(pc.view_x, pc.view_y), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ivec2 w = p + ivec2(pc.view_x, pc.view_y); // world cell
|
||||||
|
ext[i] = vec2(0.0);
|
||||||
|
cells[i] = spell_room(gen_cell(w), w);
|
||||||
|
world_rules(i, w, true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// VerletPhysicsComponent chains (the vines and ropes of the caves): one invocation per chain root. The root entity has ex.w = 777, ex.x = number of points, ex.y = the
|
||||||
|
// first part entity (the parts are consecutive entity slots, one per point; the point is the part's position, its previous position is kept in a.zw), ex.z = resting
|
||||||
|
// distance, mv = (velocity dampening, stiffness, gravity (0/1), wind (0/1)). The first point hangs on the surface above the root; the others are simulated with Verlet
|
||||||
|
// integration (the previous position gives the velocity), a distance constraint to the neighbour and the live cell grid as the collision.
|
||||||
|
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
|
||||||
|
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
|
||||||
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
|
||||||
|
|
||||||
|
bool solid_at(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cell_present(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && cell_mat(cells[p.y * pc.gw + p.x]) != 0u;
|
||||||
|
}
|
||||||
|
bool blocking_at(ivec2 w) { // a non-null cell whose type is solid (virtual +2c = 1; CLiquidCell 0070b0b0: a static powder - the rocks are liquid_static liquid_sand cells; CSolidCell)
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & F_STATIC) != 0u && ((mflags(m) & F_POWDER) != 0u || (mflags(m) & F_LIQCELL) == 0u);
|
||||||
|
}
|
||||||
|
bool stain_here(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & F_STAINS) != 0u;
|
||||||
|
}
|
||||||
|
bool liquid_here(ivec2 w) {
|
||||||
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
||||||
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||||
|
return m != 0u && (mflags(m) & F_LIQCELL) != 0u && (mflags(m) & F_POWDER) == 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool in_window(vec2 w, float margin) {
|
||||||
|
vec2 p = w - vec2(pc.view_x, pc.view_y);
|
||||||
|
return p.x > margin && p.y > margin && p.x < float(pc.gw) - margin && p.y < float(pc.gh) - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint id = gl_GlobalInvocationID.x;
|
||||||
|
if (id >= uint(ctrl.inp.w)) return;
|
||||||
|
Ent root = ents[id];
|
||||||
|
if (root.ex.w != 777.0) return;
|
||||||
|
const int n = int(root.ex.x), first = int(root.ex.y);
|
||||||
|
if ((root.st.w & 8) != 0) { // the chain was removed (its area is gone): so are its points
|
||||||
|
for (int k = 0; k < n; ++k) ents[uint(first + k)].st.w |= 8;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float rest = root.ex.z, damp = root.mv.x, stiff = root.mv.y;
|
||||||
|
if (!in_window(root.a.xy, 24.0)) return;
|
||||||
|
// a new chain waits 15 frames in the window before its surface search and its joints' first look at the cells: after a teleport the pixel scenes of the new area
|
||||||
|
// reach the cells a few frames after the terrain (the holy mountain's brick over the generated rock), and the ropes searched the bare rock, or remembered it and broke
|
||||||
|
if (root.hp.y == 0.0 && root.hp.z < 15.0) { ents[id].hp.z = root.hp.z + 1.0; return; }
|
||||||
|
vec2 anchor = root.a.xy;
|
||||||
|
const int jtype = int(root.ex4.x); // MoveToSurfaceOnCreateComponent: 0 the surface above, 1 VERLET_ROPE_ONE_JOINT, 2 VERLET_ROPE_TWO_JOINTS
|
||||||
|
if (root.hp.y == 0.0) {
|
||||||
|
if (jtype == 0 || jtype == 3) { // MoveToSurfaceOnCreate (3: the hair of a limb boss, its first point on the root entity the host moves): the first point goes to the surface above the spawn point (the cell below the first solid one)
|
||||||
|
ivec2 c = ivec2(floor(anchor));
|
||||||
|
if (jtype == 0 && !solid_at(c)) {
|
||||||
|
for (int k = 1; k <= 24; ++k) if (solid_at(c - ivec2(0, k))) { anchor = vec2(anchor.x, float(c.y - k + 1)); break; }
|
||||||
|
}
|
||||||
|
ents[id].a.xy = anchor;
|
||||||
|
for (int k = 0; k < n; ++k) { // hang straight down from the anchor
|
||||||
|
vec2 p = anchor + vec2(0.0, float(k) * rest);
|
||||||
|
ents[uint(first + k)].a = vec4(p, p);
|
||||||
|
}
|
||||||
|
} else if (jtype == 3) { // VerletWorldJointComponent (load_verlet_rope_with_*_joint*): the joints are given (ex2, ex7.z), nothing is searched
|
||||||
|
} else {
|
||||||
|
// 00bc3490 / 00bc2fa0 (called with first-hit and the upward fan): each try casts ray_count rays of length lookup_radius from the spawn point at random angles within
|
||||||
|
// +-1.2566371 (0x3fa0d97c) of straight up, the first ray that hits gives the joint: the hit cell itself (offset_from_surface only moves the type-0 entity).
|
||||||
|
// TWO_JOINTS: both joints are searched again until the second is at least verlet_min_joint_distance from the first; after 11 tries the joints are dropped (the rope is free).
|
||||||
|
const float R = root.ex4.y, mind = root.ex4.w;
|
||||||
|
const int rays = max(int(root.ex6.x), 1);
|
||||||
|
vec2 A = vec2(0.0), B = vec2(0.0);
|
||||||
|
bool gotA = false, gotB = false;
|
||||||
|
for (int attempt = 0; attempt < 11 && !(jtype == 1 ? gotA : gotB); ++attempt) {
|
||||||
|
gotA = false; gotB = false;
|
||||||
|
for (int which = 0; which < 2; ++which) {
|
||||||
|
if (which == 1 && (jtype != 2 || !gotA)) continue;
|
||||||
|
vec2 pt = vec2(0.0);
|
||||||
|
bool hit = false;
|
||||||
|
for (int r = 0; r < rays && !hit; ++r) {
|
||||||
|
const float ang = float(hash3(id, uint(attempt * 8 + which * 4 + r), 4242u) & 0x7FFFu) / 32767.0 * 2.5132742 - 1.2566371;
|
||||||
|
const vec2 dir = vec2(sin(ang), -cos(ang));
|
||||||
|
for (float t = 1.0; t <= R; t += 1.0) {
|
||||||
|
const ivec2 c = ivec2(floor(root.a.xy + dir * t));
|
||||||
|
if (solid_at(c)) { pt = vec2(c); hit = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hit) break;
|
||||||
|
if (which == 0) { A = pt; gotA = true; }
|
||||||
|
else if (distance(pt, A) >= mind) { B = pt; gotB = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jtype == 2 && !gotB) gotA = false; // (no pair: both joints go)
|
||||||
|
if (!gotA) { // 00bc3490: after 11 tries without its joint(s) the components go and the entity is killed (0044df60): no rope falls from the ceiling
|
||||||
|
ents[id].st.w |= 8;
|
||||||
|
for (int k = 0; k < n; ++k) ents[uint(first + k)].st.w |= 8;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float flags0 = 0.0;
|
||||||
|
if (gotA) flags0 += 1.0;
|
||||||
|
if (gotB) flags0 += 2.0;
|
||||||
|
ents[id].ex2 = vec4(A, B);
|
||||||
|
ents[id].ex7.z = flags0;
|
||||||
|
}
|
||||||
|
if (jtype == 1 || jtype == 2) {
|
||||||
|
const int fl = int(ents[id].ex7.z);
|
||||||
|
const bool gotA = (fl & 1) != 0, gotB = (fl & 2) != 0;
|
||||||
|
const vec2 A = ents[id].ex2.xy, B = ents[id].ex2.zw;
|
||||||
|
const vec2 P0 = gotA ? A : (gotB ? B - vec2(0.0, float(n - 1) * rest) : root.a.xy);
|
||||||
|
ents[id].a.xy = P0;
|
||||||
|
const float L = float(n - 1) * rest;
|
||||||
|
for (int k = 0; k < n; ++k) { // 00c39fd0: the points start on a horizontal line from the entity position, resting_distance apart; the joints then move their points (first, last)
|
||||||
|
vec2 p = root.a.xy + vec2(float(k) * rest, 0.0);
|
||||||
|
if (k == 0 && gotA) p = A;
|
||||||
|
if (gotB) { // two joints: started where the rope settles (along the chord with the sag of its length) so that no point begins inside the rock beside the spawn point
|
||||||
|
const float d = max(distance(A, B), 1.0);
|
||||||
|
const float sag = L > d ? sqrt(3.0 * d * (L - d) / 8.0) : 0.0;
|
||||||
|
const float u = float(k) / float(max(n - 1, 1));
|
||||||
|
p = mix(A, B, u) + vec2(0.0, 4.0 * sag * u * (1.0 - u));
|
||||||
|
}
|
||||||
|
ents[uint(first + k)].a = vec4(p, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ents[id].hp.y = 1.0;
|
||||||
|
}
|
||||||
|
// the pins: ex7.z = which joints exist (1 first point, 2 last point; -1 none left); jtype 0 and a type 3 without data (a limb boss's hair) pin the first point
|
||||||
|
int pinFlags = ents[id].ex7.z < 0.0 ? 0 : (jtype == 0 || jtype == 3) && ents[id].ex7.z == 0.0 ? 1 : int(ents[id].ex7.z);
|
||||||
|
if (jtype == 1 || jtype == 2 || (jtype == 3 && ents[id].ex7.z > 0.0 && (pinFlags & 1) != 0)) anchor = ents[id].ex2.xy;
|
||||||
|
{ // VerletWorldJointSystem (00c40b90): a joint remembers the cell at its world position on its first update (none there: it never breaks); when another cell or none is
|
||||||
|
// there later, it breaks: the component goes and the rope's damping becomes 0.97 (and a "joint_break" sound, not played here). ex6.z / ex6.w: the remembered material
|
||||||
|
// + 1 of the first / last point's joint, -1 for air, 0 not yet seen
|
||||||
|
bool broke = false;
|
||||||
|
for (int j = 0; j < 2; ++j) {
|
||||||
|
if ((pinFlags & (1 << j)) == 0) continue;
|
||||||
|
const vec2 jw = j == 0 ? anchor : ents[id].ex2.zw;
|
||||||
|
const ivec2 q = ivec2(floor(jw)) - ivec2(pc.view_x, pc.view_y);
|
||||||
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
|
||||||
|
const uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
|
const float rec = j == 0 ? ents[id].ex6.z : ents[id].ex6.w;
|
||||||
|
if (rec == 0.0) { if (j == 0) ents[id].ex6.z = m == 0u ? -1.0 : float(m + 1u); else ents[id].ex6.w = m == 0u ? -1.0 : float(m + 1u); }
|
||||||
|
else if (rec > 0.0 && float(m + 1u) != rec) { pinFlags &= ~(1 << j); broke = true; }
|
||||||
|
}
|
||||||
|
if (broke) { ents[id].ex7.z = pinFlags == 0 ? -1.0 : float(pinFlags); ents[id].mv.x = 0.97; }
|
||||||
|
}
|
||||||
|
const bool pinA = (pinFlags & 1) != 0, pinB = (pinFlags & 2) != 0;
|
||||||
|
vec2 pts[40], prv[40];
|
||||||
|
for (int k = 0; k < n && k < 40; ++k) { vec4 a = ents[uint(first + k)].a; pts[k] = a.xy; prv[k] = a.zw; }
|
||||||
|
const float t = float(ctrl.inp2.x);
|
||||||
|
float massOf[40]; // the random mass of every point between mass_min and mass_max (00c39fd0); it weights the relaxation, the forces are proportional to it
|
||||||
|
for (int k = 0; k < n && k < 40; ++k) massOf[k] = root.ex5.y + (root.ex5.z - root.ex5.y) * float(hash3(id, uint(k), 77u) & 0xFFFFu) / 65536.0;
|
||||||
|
const float T = root.ex5.x * t / 60.0; // wind_change_speed x time in seconds
|
||||||
|
// wind (docs/EXPLOSION_KNOCKBACK_AND_VINES.md section 3): WorldStateComponent.wind (wind_speed x dt) times the sky exposure of the first point (00746620: the 2048 x 2048 map of
|
||||||
|
// 64-pixel cells, 1 in the open and 0 a few cells under the biomes' ground; the earlier "x num_points" was the return of that lookup misread), set by the host in ex6.y
|
||||||
|
const float W = root.ex6.y;
|
||||||
|
const float Aw = sin(25.0 * T), Bw = sin(0.4 * T), Pw = 0.5 * (sin(T) + 1.0) + 0.5, Gw = 5.0 * abs(W) + 1.0;
|
||||||
|
for (int k = 0; k < n && k < 40; ++k) {
|
||||||
|
// 00c37d30: new = cur + (cur - prev) * damping + 0.5 F / m dt^2 (dt = 1/60); a point whose stored liquid material is set damps by 0.85
|
||||||
|
const bool wet = ents[uint(first + k)].hp.x > 0.5; // the point carries a stored liquid material
|
||||||
|
vec2 v = (pts[k] - prv[k]) * (wet ? 0.85 : damp);
|
||||||
|
vec2 F = vec2(0.0); // force per unit mass
|
||||||
|
if (root.mv.z > 0.5) F.y += 600.0; // 00c379e0: mass x 600
|
||||||
|
if (root.mv.w > 0.5) { // 00c37b10
|
||||||
|
F.x += 360.0 * W * Pw + 100.0 * sin(5.0 * pts[k].x) * Aw;
|
||||||
|
F.y += 20.0 * sin(5.0 * pts[k].y) * Aw + 60.0 * sin(0.005 * pts[k].y) * Bw * Gw;
|
||||||
|
}
|
||||||
|
const vec2 delta = v + F * 0.5 / 3600.0;
|
||||||
|
vec2 cand = pts[k] + delta;
|
||||||
|
// the terrain (00c37d30): probe (truncated x, ceil y); a free cell takes the candidate (a liquid one only liquid_damping of the move); a blocking one halves the move once; if the midpoint still
|
||||||
|
// blocks, the cell above it decides: free = one pixel up, otherwise the point stays and its velocity is gone
|
||||||
|
vec2 np = cand;
|
||||||
|
if (blocking_at(ivec2(int(cand.x), int(ceil(cand.y))))) {
|
||||||
|
const vec2 mid = (pts[k] + cand) * 0.5;
|
||||||
|
if (!blocking_at(ivec2(int(mid.x), int(ceil(mid.y))))) np = mid;
|
||||||
|
else if (!blocking_at(ivec2(int(mid.x), int(ceil(mid.y)) - 1)) && cell_present(ivec2(int(mid.x), int(ceil(mid.y)) - 1))) { np = pts[k] + vec2(0.0, -1.0); prv[k] = np; pts[k] = np; continue; }
|
||||||
|
else { prv[k] = pts[k]; continue; }
|
||||||
|
} else if (liquid_here(ivec2(int(cand.x), int(ceil(cand.y))))) {
|
||||||
|
np = pts[k] + root.ex5.w * delta;
|
||||||
|
// 00c37d30: the liquid's material is sampled into the point on 6 of 10 points, else with a 1 in 30 draw (only liquids that stain)
|
||||||
|
if (stain_here(ivec2(int(cand.x), int(ceil(cand.y)))) && (k % 10 < 6 || hash3(id, uint(k), uint(pc.frame)) % 30u == 0u)) ents[uint(first + k)].hp.x = 1.0;
|
||||||
|
}
|
||||||
|
prv[k] = pts[k];
|
||||||
|
pts[k] = np;
|
||||||
|
}
|
||||||
|
for (int it = 0; it < 2; ++it) // 00c38310 runs twice over the links: the chain is soft, it stretches under its own weight
|
||||||
|
for (int k = 0; k + 1 < n && k + 1 < 40; ++k) { // distance constraints; the joints' points move like the others here and are put back afterwards (00c3a610)
|
||||||
|
vec2 d = pts[k + 1] - pts[k];
|
||||||
|
float l = length(d);
|
||||||
|
if (l < 1e-5) continue;
|
||||||
|
float diff = (l - rest) / max(l, 0.01) * stiff * 0.5; // 00c3a610 passes stiffness x 0.5 (0x3f000000) to 00c38310; links shorter than 0.01 count as 0.01
|
||||||
|
const float wa = 1.0 / massOf[k], wb = 1.0 / massOf[k + 1], sa = wa / (wa + wb); // the share of the correction by the inverse masses
|
||||||
|
pts[k] += d * diff * sa; pts[k + 1] -= d * diff * (1.0 - sa);
|
||||||
|
}
|
||||||
|
if (pinA) pts[0] = anchor; // the world joints put their points back after the relaxation (00c3a610)
|
||||||
|
if (pinB) pts[n - 1] = ents[id].ex2.zw;
|
||||||
|
// the animation (00c385a0, after the relaxation when animation_target_offset is not 0 - the tentacler's tentacles, offset (0, 1)): every link pulls its far point toward
|
||||||
|
// near point + offset + the offset's direction turned by 90 degrees x sin((link + time) x animation_speed) x animation_amount, by animation_energy shared by the inverse masses
|
||||||
|
// (root ex17 = offset, amount, speed; ex18.x = energy; the time in seconds)
|
||||||
|
if (root.ex17.x != 0.0 || root.ex17.y != 0.0) {
|
||||||
|
const vec2 D = root.ex17.xy, u = normalize(D), perp = vec2(-u.y, u.x);
|
||||||
|
for (int k = 0; k + 1 < n && k + 1 < 40; ++k) {
|
||||||
|
const float w = sin((float(k) + t / 60.0) * root.ex17.w) * root.ex17.z;
|
||||||
|
const vec2 delta = pts[k] + D + perp * w - pts[k + 1];
|
||||||
|
const float wa = 1.0 / massOf[k], wb = 1.0 / massOf[k + 1];
|
||||||
|
pts[k + 1] += delta * (root.ex18.x * wb / (wa + wb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int k = 0; k < n && k < 40; ++k) {
|
||||||
|
ents[uint(first + k)].a = vec4(pts[k], prv[k]);
|
||||||
|
const vec2 d = k + 1 < n ? pts[k + 1] - pts[k] : pts[k] - pts[max(k - 1, 0)];
|
||||||
|
ents[uint(first + k)].ex7.x = atan(d.y, d.x); // the horizontal part sprite is turned along the chain
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
#include "ai/pathfind.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
#include "entity/herds.h"
|
||||||
|
|
||||||
|
namespace ng::ai {
|
||||||
|
|
||||||
|
void PathGrid::build(int worldX, int worldY, int pixelsW, int pixelsH, const std::function<uint8_t(int x, int y)>& classify) {
|
||||||
|
const int nox = (int)std::ceil(worldX / 4.0) * 4, noy = (int)std::ceil(worldY / 4.0) * 4;
|
||||||
|
const int nw = std::max(0, (worldX + pixelsW - nox + 3) / 4), nh = std::max(0, (worldY + pixelsH - noy + 3) / 4);
|
||||||
|
// the stuck marks of the nodes that stay
|
||||||
|
std::vector<uint8_t> oldBlocked = std::move(blocked);
|
||||||
|
std::vector<int32_t> oldUntil = std::move(blockedUntil);
|
||||||
|
const int pox = ox, poy = oy, pw = w, ph = h;
|
||||||
|
ox = nox; oy = noy; w = nw; h = nh;
|
||||||
|
flags.assign((size_t)w * h, 0);
|
||||||
|
liquidCells.assign((size_t)w * h, 0);
|
||||||
|
markHerd.assign((size_t)w * h, -1);
|
||||||
|
markEnt.assign((size_t)w * h, 0);
|
||||||
|
playerArea.assign((size_t)w * h, 0);
|
||||||
|
clearance.assign((size_t)w * h, 0);
|
||||||
|
blocked.assign((size_t)w * h, 0);
|
||||||
|
blockedUntil.assign((size_t)w * h, 0);
|
||||||
|
for (int y = 0; y < h; ++y)
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
const int wx = ox + x * 4, wy = oy + y * 4;
|
||||||
|
int nSolid = 0, nLiquid = 0, nDanger[4] = {0, 0, 0, 0};
|
||||||
|
for (int cy = 0; cy < 4; ++cy)
|
||||||
|
for (int cx = 0; cx < 4; ++cx) {
|
||||||
|
const uint8_t c = classify(wx + cx, wy + cy);
|
||||||
|
if (c & kNodeSolid) ++nSolid; else if (c & kNodeLiquid) ++nLiquid;
|
||||||
|
for (int b = 0; b < 4; ++b) if (c & (1 << b)) ++nDanger[b];
|
||||||
|
}
|
||||||
|
uint8_t f = (uint8_t)((nSolid > 0 ? kNodeSolid : 0) | (nLiquid > 10 && nSolid == 0 ? kNodeLiquid : 0) | (nSolid > 14 ? kNodeFullSolid : 0));
|
||||||
|
for (int b = 0; b < 4; ++b) if (nDanger[b] > 2) f |= (uint8_t)(1 << b);
|
||||||
|
flags[idx(x, y)] = f;
|
||||||
|
liquidCells[idx(x, y)] = (uint8_t)nLiquid;
|
||||||
|
const int px = (wx - pox) / 4, py = (wy - poy) / 4;
|
||||||
|
if (!oldBlocked.empty() && px >= 0 && py >= 0 && px < pw && py < ph && (wx - pox) % 4 == 0 && (wy - poy) % 4 == 0) {
|
||||||
|
blocked[idx(x, y)] = oldBlocked[(size_t)py * pw + px];
|
||||||
|
blockedUntil[idx(x, y)] = oldUntil[(size_t)py * pw + px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clearance (00860150): the Chebyshev distance in nodes to the nearest solid node, 0 for a solid one, at most 4
|
||||||
|
for (int y = 0; y < h; ++y)
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
if (flags[idx(x, y)] & kNodeSolid) { clearance[idx(x, y)] = 0; continue; }
|
||||||
|
int d = 4;
|
||||||
|
for (int r = 1; r < 4 && d == 4; ++r) {
|
||||||
|
for (int k = -r; k <= r && d == 4; ++k)
|
||||||
|
if (solid(x + k, y - r) || solid(x + k, y + r) || solid(x - r, y + k) || solid(x + r, y + k)) d = r;
|
||||||
|
}
|
||||||
|
clearance[idx(x, y)] = (uint8_t)d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathGrid::clear_marks() {
|
||||||
|
std::fill(markHerd.begin(), markHerd.end(), (int16_t)-1);
|
||||||
|
std::fill(markEnt.begin(), markEnt.end(), 0);
|
||||||
|
std::fill(playerArea.begin(), playerArea.end(), (uint8_t)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathGrid::mark(float wx, float wy, int herd, int entity) {
|
||||||
|
const int nx = (int)std::floor((wx - ox) / 4.0f), ny = (int)std::floor((wy - oy) / 4.0f);
|
||||||
|
if (!valid(nx, ny)) return;
|
||||||
|
markHerd[(size_t)idx(nx, ny)] = (int16_t)(herd >= 0 && herd < 0x80 ? herd : -1);
|
||||||
|
markEnt[(size_t)idx(nx, ny)] = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathGrid::mark_player_area(float wx, float wy, float radius, float phase) { // 12 rays (every 30 degrees from the phase: (frame % 60) x a step) to the radius, stopped by a solid node
|
||||||
|
if (radius <= 0.0f) return;
|
||||||
|
for (int k = 0; k < 12; ++k) {
|
||||||
|
const float a = phase + (float)k * 0.5235988f;
|
||||||
|
const float ex = wx + std::cos(a) * radius, ey = wy + std::sin(a) * radius;
|
||||||
|
const int n = std::max(1, (int)(radius / 4.0f));
|
||||||
|
for (int i = 0; i <= n; ++i) {
|
||||||
|
const int nx = (int)std::floor((wx + (ex - wx) * i / n - ox) / 4.0f), ny = (int)std::floor((wy + (ey - wy) * i / n - oy) / 4.0f);
|
||||||
|
if (!valid(nx, ny) || solid(nx, ny)) break;
|
||||||
|
playerArea[(size_t)idx(nx, ny)] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathGrid::block(int nx, int ny, uint8_t dirs, int untilFrame) {
|
||||||
|
if (!valid(nx, ny)) return;
|
||||||
|
blocked[idx(nx, ny)] |= dirs;
|
||||||
|
blockedUntil[idx(nx, ny)] = std::max(blockedUntil[idx(nx, ny)], untilFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathGrid::forget_blocks(int frame) {
|
||||||
|
for (size_t i = 0; i < blocked.size(); ++i) if (blocked[i] && blockedUntil[i] <= frame) blocked[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool lob_velocity(float x0, float y0, float x1, float y1, float speed, float gravity, float& vx, float& vy) {
|
||||||
|
const float dx = x1 - x0, dy = y1 - y0;
|
||||||
|
const float dist = std::hypot(dx, dy);
|
||||||
|
if (speed <= 0.0f) return false;
|
||||||
|
const float t = std::max(dist / speed, 0.15f); // the flight time at the jump speed
|
||||||
|
vx = dx / t;
|
||||||
|
vy = (dy - 0.5f * gravity * t * t) / t;
|
||||||
|
return std::hypot(vx, vy) <= speed * 2.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Rec { int node; float g; int depth; int parent; bool jump; float jvx, jvy; };
|
||||||
|
|
||||||
|
// a node a walker can stand on (00860790): free, a solid node below, two free nodes above
|
||||||
|
bool standable(const PathGrid& g, int x, int y) {
|
||||||
|
return !g.solid(x, y) && g.solid(x, y + 1) && !g.solid(x, y - 1) && !g.solid(x, y - 2);
|
||||||
|
}
|
||||||
|
// 00860790 with its strict argument (00860930: the landings of jumps): each side is a wall, or standable at the same level or one node down - a one-node perch
|
||||||
|
// above a drop is no landing (the helper's neighbour order is partly unclear in the decompile: this footprint is the reading of docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-08)
|
||||||
|
bool standable_strict(const PathGrid& g, int x, int y) {
|
||||||
|
if (!standable(g, x, y)) return false;
|
||||||
|
auto side = [&](int d) { return g.solid(x + d, y) || standable(g, x + d, y) || standable(g, x + d, y + 1); };
|
||||||
|
return side(-1) && side(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// one pass of 00862620 with the movement modes of p; budgetOut: it stopped at iterations_max with nodes still open (the engine would resume it later)
|
||||||
|
std::vector<PathNode> search_pass(const PathGrid& g, float sx, float sy, const SearchParams& p, bool& budgetOut) {
|
||||||
|
std::vector<PathNode> out;
|
||||||
|
budgetOut = false;
|
||||||
|
if (g.w <= 0 || g.h <= 0) return out;
|
||||||
|
int snx = (int)std::floor((sx - g.ox) / 4.0f), sny = (int)std::floor((sy - g.oy) / 4.0f);
|
||||||
|
if (!g.valid(snx, sny)) return out;
|
||||||
|
// 00861e00: the start projected like the engine. An embedded start looks left, right, then up, up to 12 pixels (3 nodes), for a free node; an unsupported
|
||||||
|
// walker's start is dropped down a ray up to 12 pixels to the node above the ground. (Only one node up before: a creature wedged under a table top beside its leg
|
||||||
|
// found no route at all, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-06)
|
||||||
|
if (g.solid(snx, sny)) {
|
||||||
|
bool moved = false;
|
||||||
|
for (int d = 1; d <= 3 && !moved; ++d) {
|
||||||
|
if (g.valid(snx - d, sny) && !g.solid(snx - d, sny)) { snx -= d; moved = true; }
|
||||||
|
else if (g.valid(snx + d, sny) && !g.solid(snx + d, sny)) { snx += d; moved = true; }
|
||||||
|
else if (g.valid(snx, sny - d) && !g.solid(snx, sny - d)) { sny -= d; moved = true; }
|
||||||
|
}
|
||||||
|
if (!moved) return out;
|
||||||
|
} else if (!p.can_fly && !g.solid(snx, sny + 1) && !(g.at(snx, sny + 1) & kNodeLiquid) && !(g.at(snx, sny) & kNodeLiquid)) {
|
||||||
|
for (int d = 1; d <= 3; ++d) {
|
||||||
|
if (!g.valid(snx, sny + d) || g.solid(snx, sny + d) || (g.at(snx, sny + d) & kNodeLiquid)) break;
|
||||||
|
sny += 1;
|
||||||
|
if (g.solid(snx, sny + 1)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int N = g.w * g.h;
|
||||||
|
std::vector<float> best((size_t)N, 1e30f);
|
||||||
|
std::vector<Rec> recs;
|
||||||
|
recs.reserve(4096);
|
||||||
|
std::vector<int> recOf((size_t)N, -1);
|
||||||
|
using QE = std::pair<float, int>; // f, rec
|
||||||
|
std::priority_queue<QE, std::vector<QE>, std::greater<QE>> open;
|
||||||
|
const bool directed = p.goal == Goal::Hunt || p.goal == Goal::Goto || p.goal == Goal::MoveNear;
|
||||||
|
const int gnx = (int)std::floor((p.gx - g.ox) / 4.0f), gny = (int)std::floor((p.gy - g.oy) / 4.0f);
|
||||||
|
auto heuristic = [&](int nx, int ny) -> float { // 00862620: (|dx| + |dy|) in pixels x 10
|
||||||
|
if (!directed) return 0.0f;
|
||||||
|
return (float)(std::abs(nx - gnx) * 4 + std::abs(ny - gny) * 4) * 10.0f;
|
||||||
|
};
|
||||||
|
const bool hasMarks = !g.markHerd.empty();
|
||||||
|
auto avoided = [&](int nx, int ny) -> uint8_t { // the stuck directions of this creature at a node (00860c20), plus the shared ones of the grid
|
||||||
|
uint8_t d = g.blocked.empty() ? 0 : g.blocked[g.idx(nx, ny)];
|
||||||
|
if (p.avoid && !p.avoid->empty()) {
|
||||||
|
const int64_t k = world_node_key((g.ox >> 2) + nx, (g.oy >> 2) + ny);
|
||||||
|
for (const auto& [ak, ad] : *p.avoid) if (ak == k) d |= ad;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
// RandomMove, Escaping and EscapingPrey step only into nodes no creature marks (their edge checks 00864a80 / 00864ea0 / 00865090)
|
||||||
|
const bool unmarkedOnly = hasMarks && (p.goal == Goal::RandomMove || p.goal == Goal::Away || p.goal == Goal::Escape);
|
||||||
|
auto isGoal = [&](int nx, int ny) -> bool {
|
||||||
|
// the node's centre; Goto (008649c0) and MoveNear (00864d40) test its corner
|
||||||
|
const bool corner = p.goal == Goal::Goto || p.goal == Goal::MoveNear;
|
||||||
|
const float wx = (float)(g.ox + nx * 4 + (corner ? 0 : 2)), wy = (float)(g.oy + ny * 4 + (corner ? 0 : 2));
|
||||||
|
if (p.home_radius > 0.0f && p.goal != Goal::Goto && p.goal != Goal::MoveNear && p.goal != Goal::Charmed) {
|
||||||
|
const float d2 = (wx - p.home_x) * (wx - p.home_x) + (wy - p.home_y) * (wy - p.home_y);
|
||||||
|
if (d2 > p.home_radius * p.home_radius) return false;
|
||||||
|
if (p.goal == Goal::Escape && d2 > p.home_radius * p.home_radius * 0.2f) return false;
|
||||||
|
}
|
||||||
|
const int id = g.idx(nx, ny);
|
||||||
|
switch (p.goal) {
|
||||||
|
case Goal::Hunt: { // 00864bf0: a node marked by a hostile herd, or by the target entity
|
||||||
|
if (!hasMarks) return std::abs(nx - gnx) <= 1 && std::abs(ny - gny) <= 1;
|
||||||
|
const int hm = g.markHerd[(size_t)id];
|
||||||
|
if (hm >= 0 && hm != p.own_herd && ng::herd_relation(p.own_herd, hm) < p.aggression) return true;
|
||||||
|
return p.target_entity != 0 && g.markEnt[(size_t)id] == p.target_entity;
|
||||||
|
}
|
||||||
|
case Goal::Escape: return (wx - p.gx) * (wx - p.gx) + (wy - p.gy) * (wy - p.gy) >= p.away_distance * p.away_distance && !g.marked(id); // 00864ed0
|
||||||
|
case Goal::Charmed: return !g.playerArea.empty() && g.playerArea[(size_t)id] && (wx - sx) * (wx - sx) + (wy - sy) * (wy - sy) > 256.0f; // 00865480
|
||||||
|
case Goal::Goto: return (wx - p.gx) * (wx - p.gx) + (wy - p.gy) * (wy - p.gy) < 8.0f; // 008649c0 (DAT_01053cc0 = 8: the squared distance, a radius of sqrt 8)
|
||||||
|
case Goal::MoveNear: return std::abs(p.gx - wx) < 8.0f && std::abs(p.gy - wy) < 256.0f; // 00864d40
|
||||||
|
case Goal::Away: return (wx - p.gx) * (wx - p.gx) + (wy - p.gy) * (wy - p.gy) >= p.away_distance * p.away_distance && !g.marked(id); // 008650c0
|
||||||
|
case Goal::Burning: { // 00865280: a liquid node of 16 liquid cells without danger, with the same above it and on both sides (to put the fire out)
|
||||||
|
auto wet = [&](int x, int y) { return g.valid(x, y) && (g.at(x, y) & kNodeLiquid) && !(g.at(x, y) & p.danger_mask & 0x0F) && g.liquidCells[g.idx(x, y)] == 16; };
|
||||||
|
return wet(nx, ny) && wet(nx, ny - 1) && wet(nx - 1, ny) && wet(nx + 1, ny);
|
||||||
|
}
|
||||||
|
case Goal::RandomMove: return std::abs((int)wx - (int)sx) + std::abs((int)wy - (int)sy) > p.random_distance && !(g.at(nx, ny) & kNodeLiquid) && !g.marked(id); // 00864ab0
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto push = [&](int nx, int ny, int parent, float g2, int depth, bool jump, float jvx, float jvy) {
|
||||||
|
const int id = g.idx(nx, ny);
|
||||||
|
if (g2 >= best[id]) return;
|
||||||
|
if (unmarkedOnly && parent >= 0 && g.marked(id)) return;
|
||||||
|
best[id] = g2;
|
||||||
|
recs.push_back({id, g2, depth, parent, jump, jvx, jvy});
|
||||||
|
recOf[id] = (int)recs.size() - 1;
|
||||||
|
open.push({g2 + heuristic(nx, ny), (int)recs.size() - 1});
|
||||||
|
};
|
||||||
|
push(snx, sny, -1, 0.0f, 0, false, 0, 0);
|
||||||
|
bool fanned = false; // (00862620 local_22: the fan of initial jumps only once a search)
|
||||||
|
int iterations = 0, found = -1;
|
||||||
|
// the node closest to the goal, for a directed search that runs out of iterations (the engine keeps the best partial path of a target)
|
||||||
|
int closest = -1; float closestH = 1e30f;
|
||||||
|
while (!open.empty() && iterations < p.iterations_max) {
|
||||||
|
const auto [f, ri] = open.top(); open.pop();
|
||||||
|
const Rec r = recs[ri];
|
||||||
|
if (r.g > best[r.node]) continue;
|
||||||
|
++iterations;
|
||||||
|
if (p.work) ++*p.work;
|
||||||
|
const int cx = r.node % g.w, cy = r.node / g.w;
|
||||||
|
if (ri != 0 && isGoal(cx, cy)) { found = ri; break; }
|
||||||
|
if (directed) { const float h = heuristic(cx, cy); if (h < closestH) { closestH = h; closest = ri; } }
|
||||||
|
if (r.depth >= p.depth_max) continue;
|
||||||
|
const uint8_t cflags = g.at(cx, cy);
|
||||||
|
const bool cLiquid = (cflags & kNodeLiquid) != 0;
|
||||||
|
// jumps (00862620 -> 00860930): from a node on the ground every 5th depth, or at a ledge. The first such node of the search tries a fan of aim points every
|
||||||
|
// 4 pixels within +-initial_jump_max_distance_x / _y (only (0, 0) left out); every later one only the component's jump_trajectories, each mirrored too
|
||||||
|
// (00862620 generates them before the neighbours; on every 5th depth from a node with solid cells below it, or at a ledge: the parent a lateral neighbour and
|
||||||
|
// no ground beyond the node on the far side - not the full standable test, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-14)
|
||||||
|
const int parentNode = r.parent >= 0 ? recs[r.parent].node : -1;
|
||||||
|
const bool lateralParent = parentNode >= 0 && parentNode / g.w == cy && std::abs(parentNode % g.w - cx) == 1;
|
||||||
|
const bool ledge = lateralParent && !g.solid(cx + (cx - parentNode % g.w), cy + 1);
|
||||||
|
if (p.can_jump && !p.can_fly && g.solid(cx, cy + 1) && !g.solid(cx, cy) && (r.depth % 5 == 0 || ledge)) {
|
||||||
|
// 00860930: the lob from the node's corner aimed at (dx, dy) (0063ec90: T = lob x 60 x distance / jump_speed frames, v = (d - g T^2 / 2) / T per frame,
|
||||||
|
// here per second); 0063ee20 samples it at n = (|dx| + |vy t| + |g t^2 / 2|) / 4 points evenly in x up to the aim point (t = dx / vx); a solid or dangerous
|
||||||
|
// node on the way ends it with nothing; the first standable node it enters going down (a node below the one of the sample before) is the landing.
|
||||||
|
// A vertical aim (dx = 0) has no samples: no jump. Cost: the nodes sampled x 10000
|
||||||
|
auto arc = [&](float dx, float dy, float lob) {
|
||||||
|
const float dist = std::hypot(dx, dy);
|
||||||
|
if (dist < 1.1920929e-7f || p.jump_speed <= 0.0f) return;
|
||||||
|
const float T = lob * 60.0f * (dist / p.jump_speed); // frames
|
||||||
|
if (!(T > 0.0f)) return;
|
||||||
|
const float vx = (dx * 60.0f) / T, vy = ((dy - p.gravity / 3600.0f * T * T * 0.5f) * 60.0f) / T; // px/s
|
||||||
|
const float t = vx != 0.0f ? dx / vx : 0.0f;
|
||||||
|
const int n = (int)((std::fabs(dx) + std::fabs(vy * t) + std::fabs(p.gravity * 0.5f * t * t)) * 0.25f);
|
||||||
|
if ((unsigned)(n - 1) >= 0x1ffu) return;
|
||||||
|
const float x0 = (float)(g.ox + cx * 4), y0 = (float)(g.oy + cy * 4);
|
||||||
|
const float sdx = dx / (float)n, sdt = vx != 0.0f ? sdx / vx : 0.0f;
|
||||||
|
int crossed = 0, prevY = cy;
|
||||||
|
for (int s = 1; s <= n; ++s) {
|
||||||
|
const float ts = sdt * (float)s;
|
||||||
|
const int ax = (int)std::floor(((float)(int)(x0 + sdx * (float)s) - (float)g.ox) / 4.0f);
|
||||||
|
const int ay = (int)std::floor(((float)(int)(y0 + vy * ts + p.gravity * 0.5f * ts * ts) - (float)g.oy) / 4.0f);
|
||||||
|
if (!g.valid(ax, ay)) continue; // (no node there: not counted)
|
||||||
|
++crossed;
|
||||||
|
const uint8_t af = g.at(ax, ay);
|
||||||
|
if ((af & kNodeSolid) || (af & p.danger_mask & 0x0F)) return;
|
||||||
|
if (standable_strict(g, ax, ay) && ay > prevY) {
|
||||||
|
// 00862620 hands the landing to the ordinary relaxer 00863af0: its clearance and danger tests too (docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-09)
|
||||||
|
if (g.clearance[g.idx(ax, ay)] < p.clearance_required) return;
|
||||||
|
push(ax, ay, ri, r.g + 10.0f + (float)crossed * 10000.0f, r.depth + 1, true, vx, vy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevY = ay;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!fanned) {
|
||||||
|
fanned = true;
|
||||||
|
for (float dx = -p.jump_max_x; dx <= p.jump_max_x; dx += 4.0f)
|
||||||
|
for (float dy = -p.jump_max_y; dy <= p.jump_max_y; dy += 4.0f)
|
||||||
|
if (dx != 0.0f || dy != 0.0f) arc(dx, dy, p.jump_lob);
|
||||||
|
} else
|
||||||
|
for (const auto& jt : p.jump_trajectories) { arc(jt[0], jt[1], jt[2]); arc(-jt[0], jt[1], jt[2]); }
|
||||||
|
}
|
||||||
|
// the four neighbours (00863af0)
|
||||||
|
static const int dx4[4] = {-1, 0, 0, 1}, dy4[4] = {0, 1, -1, 0};
|
||||||
|
static const uint8_t dirBit[4] = {kBlockLeft, kBlockDown, kBlockUp, kBlockRight};
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const int nx = cx + dx4[k], ny = cy + dy4[k];
|
||||||
|
if (!g.valid(nx, ny)) continue;
|
||||||
|
if (avoided(cx, cy) & dirBit[k]) continue; // a direction this creature got stuck going (00860c20)
|
||||||
|
const uint8_t nf = g.at(nx, ny);
|
||||||
|
if (nf & kNodeSolid) continue;
|
||||||
|
if (nf & p.danger_mask & 0x0F) continue;
|
||||||
|
if (g.clearance[g.idx(nx, ny)] < p.clearance_required) continue;
|
||||||
|
float extra = 0.0f;
|
||||||
|
if (nf & kNodeLiquid) { // swimming on the surface (free node above): +10000, diving: +13000
|
||||||
|
const bool surface = g.valid(nx, ny - 1) && !(g.at(nx, ny - 1) & (kNodeSolid | kNodeLiquid));
|
||||||
|
if (surface && p.can_swim_on_surface) extra = 10000.0f;
|
||||||
|
else if (p.can_dive) extra = 13000.0f;
|
||||||
|
else continue;
|
||||||
|
}
|
||||||
|
else if (!p.can_fly) {
|
||||||
|
// a walker enters a node without ground under it only by falling into it from above (or moving along the surface of a liquid)
|
||||||
|
const bool supported = g.solid(nx, ny + 1) || (g.at(nx, ny + 1) & kNodeLiquid) != 0;
|
||||||
|
// (00863af0 lets a walker whose node holds solid cells - uneven ground, a slope - step sideways into an unsupported node; here the nodes walked
|
||||||
|
// through are free of solid cells, so the same step down a slope is a sideways step whose node below has ground under it)
|
||||||
|
const bool stepDown = dy4[k] == 0 && g.solid(cx, cy + 1) && g.valid(nx, ny + 2) && !g.solid(nx, ny + 1) && g.solid(nx, ny + 2);
|
||||||
|
if (!supported && dy4[k] != 1 && !cLiquid && !stepDown) continue;
|
||||||
|
if (!p.can_walk && supported) continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const bool supported = g.solid(nx, ny + 1);
|
||||||
|
if (!supported) extra = p.cost_of_flying;
|
||||||
|
}
|
||||||
|
push(nx, ny, ri, r.g + 10.0f + extra, r.depth + 1, false, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found < 0) {
|
||||||
|
// 00861e00 publishes a path only on success (+0xa2): a search that ran out of nodes returns none. One stopped by our per-call budget (the engine yields at 1000
|
||||||
|
// units of work and resumes the next frames) keeps the best partial route of a directed search, standing in for the resumed search
|
||||||
|
budgetOut = !open.empty() && iterations >= p.iterations_max;
|
||||||
|
if (!budgetOut || !directed || closest <= 0) return out;
|
||||||
|
found = closest;
|
||||||
|
}
|
||||||
|
std::vector<int> chain;
|
||||||
|
for (int ri = found; ri > 0; ri = recs[ri].parent) chain.push_back(ri);
|
||||||
|
std::reverse(chain.begin(), chain.end());
|
||||||
|
if (!chain.empty() && recs[chain.front()].jump) chain.insert(chain.begin(), 0); // (a jump from the start: the start node is its source entry)
|
||||||
|
for (int ri : chain) {
|
||||||
|
const Rec& r = recs[ri];
|
||||||
|
out.push_back({g.ox + (r.node % g.w) * 4 + 2, g.oy + (r.node / g.w) * 4 + 2, r.jump, r.jvx, r.jvy, false}); // (a node covers its 4 x 4 cells: their centre)
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i + 1 < out.size(); ++i) if (out[i + 1].jump) out[i].jumpFrom = true; // (00861e00's post-pass: the entry before a jump)
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// TODO(to-do #34): source flags of jump entries, strict landings, jump ends through the relaxer, per-creature stuck memory, a resumable search
|
||||||
|
std::vector<PathNode> search(const PathGrid& g, float sx, float sy, const SearchParams& p) {
|
||||||
|
// 00861e00: the movement modes in passes - walking / diving / surface swimming alone first (flight and jumps off); only when that found no route the flying pass,
|
||||||
|
// then the jumping one. A route of an earlier pass wins even when a jump would be cheaper (docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-01)
|
||||||
|
bool budget = false;
|
||||||
|
std::vector<PathNode> partial;
|
||||||
|
if (p.can_walk || p.can_dive || p.can_swim_on_surface) {
|
||||||
|
SearchParams q = p; q.can_fly = false; q.can_jump = false;
|
||||||
|
std::vector<PathNode> r = search_pass(g, sx, sy, q, budget);
|
||||||
|
if (!r.empty() && !budget) return r;
|
||||||
|
if (budget) partial = std::move(r);
|
||||||
|
}
|
||||||
|
if (p.can_fly) {
|
||||||
|
SearchParams q = p; q.can_jump = false;
|
||||||
|
std::vector<PathNode> r = search_pass(g, sx, sy, q, budget);
|
||||||
|
if (!r.empty() && !budget) return r;
|
||||||
|
if (budget && partial.empty()) partial = std::move(r);
|
||||||
|
}
|
||||||
|
if (p.can_jump) {
|
||||||
|
SearchParams q = p; q.can_fly = false;
|
||||||
|
std::vector<PathNode> r = search_pass(g, sx, sy, q, budget);
|
||||||
|
if (!r.empty() && !budget) return r;
|
||||||
|
if (budget && partial.empty()) partial = std::move(r);
|
||||||
|
}
|
||||||
|
return partial;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng::ai
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
// The pathfinding of the creatures (PathFindingSystem 00bca030, search 00862620 / 00863af0, jumps 00860930 / 00860790, the goal logics 00864bf0..00865480).
|
||||||
|
// The grid has a node every 4 world pixels (the engine's chunks hold 128 x 128 nodes of 32 bytes over 512 x 512 pixels, 00635060 / 00635470); here it covers the
|
||||||
|
// simulated window and is rebuilt from the cells. docs/PATHFINDING_AND_ANIMAL_AI.md.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace ng::ai {
|
||||||
|
|
||||||
|
// node flags (byte +0xf of the engine's node, 00864060): counted over the node's 4 x 4 cells, from the pixel bytes of 00863fd0 (the path type of a cell, its vtable
|
||||||
|
// slot 11, e.g. CLiquidCell 0070b0b0: 3 = a liquid that is not liquid_sand, 1 = a solid; the danger bits of the material, a burning cell)
|
||||||
|
enum : uint8_t {
|
||||||
|
kNodeDangerFire = 1, // more than 2 cells burning or of a danger_fire material
|
||||||
|
kNodeDangerPoison = 2, // more than 2 cells danger_poison
|
||||||
|
kNodeDangerRadioactive = 4, // more than 2 cells danger_radioactive
|
||||||
|
kNodeDangerWater = 8, // more than 2 cells danger_water
|
||||||
|
kNodeFullSolid = 0x10, // more than 14 solid cells
|
||||||
|
kNodeLiquid = 0x20, // more than 10 liquid cells and no solid one
|
||||||
|
kNodeSolid = 0x40, // any solid cell
|
||||||
|
};
|
||||||
|
// blocked directions of a node (byte +0x18, 00860bb0 / 00860c20): set when a creature got stuck going that way
|
||||||
|
enum : uint8_t { kBlockLeft = 1, kBlockRight = 2, kBlockUp = 4, kBlockDown = 8 };
|
||||||
|
|
||||||
|
struct PathGrid {
|
||||||
|
int ox = 0, oy = 0; // world pixel of node (0, 0); a multiple of 4
|
||||||
|
int w = 0, h = 0; // nodes
|
||||||
|
std::vector<uint8_t> flags, clearance, blocked;
|
||||||
|
std::vector<uint8_t> liquidCells; // the liquid cells of the node, 0..16 (byte 0 of the engine's 3-byte node record, +0x80000)
|
||||||
|
std::vector<int32_t> blockedUntil; // the frame the blocked directions are forgotten (mNextClearDontApproachListFrame)
|
||||||
|
// PathFindingGridMarkerSystem (00bcdc80, every frame): the node at an entity's position + marker_offset holds its herd (byte +0x10, -1 none) and entity (+0x14);
|
||||||
|
// the player's player_marker_radius marks the nodes along 12 rays from its node (byte +0xe bit 0x10, the Charmed logic's goal)
|
||||||
|
std::vector<int16_t> markHerd;
|
||||||
|
std::vector<int32_t> markEnt;
|
||||||
|
std::vector<uint8_t> playerArea;
|
||||||
|
void clear_marks();
|
||||||
|
void mark(float wx, float wy, int herd, int entity);
|
||||||
|
void mark_player_area(float wx, float wy, float radius, float phase);
|
||||||
|
bool marked(int i) const { return !markHerd.empty() && markHerd[(size_t)i] >= 0; }
|
||||||
|
bool valid(int nx, int ny) const { return nx >= 0 && ny >= 0 && nx < w && ny < h; }
|
||||||
|
int idx(int nx, int ny) const { return ny * w + nx; }
|
||||||
|
uint8_t at(int nx, int ny) const { return valid(nx, ny) ? flags[idx(nx, ny)] : (uint8_t)kNodeSolid; } // (outside the grid: a wall)
|
||||||
|
bool solid(int nx, int ny) const { return (at(nx, ny) & kNodeSolid) != 0; }
|
||||||
|
// Rebuilds the flags from a classifier of world pixels (returns the flags of one cell: kNodeSolid, kNodeLiquid and the danger bits), counted per node, then the clearance (00860150: distance in nodes to the nearest solid, 0..4)
|
||||||
|
void build(int worldX, int worldY, int pixelsW, int pixelsH, const std::function<uint8_t(int x, int y)>& classify);
|
||||||
|
// keeps the blocked directions of the overlapping area when the window moves
|
||||||
|
void block(int nx, int ny, uint8_t dirs, int untilFrame);
|
||||||
|
void forget_blocks(int frame);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hunt: PathFindingLogic_Default 00864bf0; Burning 00865280; Away: EscapingPrey 008650c0; Escape: Escaping 00864ed0; Charmed 00865480
|
||||||
|
enum class Goal { None, Hunt, RandomMove, Goto, MoveNear, Burning, Away, Escape, Charmed };
|
||||||
|
|
||||||
|
struct SearchParams {
|
||||||
|
bool can_fly = false, can_walk = true, can_jump = false, can_dive = false, can_swim_on_surface = false;
|
||||||
|
int clearance_required = 0; // space_required / 4 (00bca030: input +0xcc)
|
||||||
|
float cost_of_flying = 20.0f;
|
||||||
|
int depth_max = 50; // nodes along the path (AnimalAI pathfinding_max_depth_has_target / _no_target)
|
||||||
|
int iterations_max = 1500;
|
||||||
|
float jump_max_x = 100.0f, jump_max_y = 80.0f; // initial_jump_max_distance_x / _y (pixels)
|
||||||
|
float jump_speed = 200.0f; // pixels per second
|
||||||
|
float jump_lob = 1.0f; // initial_jump_lob: the flight takes lob x 60 x distance / jump_speed frames (0063ec90)
|
||||||
|
std::vector<std::array<float, 3>> jump_trajectories; // PathFindingComponent jump_trajectories (dx, dy, lob; each tried mirrored too): the jumps after the first fan
|
||||||
|
float gravity = 350.0f; // pixels per second squared of the jumping body
|
||||||
|
uint8_t danger_mask = kNodeDangerFire | kNodeDangerPoison | kNodeDangerRadioactive;
|
||||||
|
Goal goal = Goal::None;
|
||||||
|
float gx = 0, gy = 0; // the goal point (Hunt: the target's marker, Goto / MoveNear: the point)
|
||||||
|
float away_distance = 100.0f; // Away / Escape: a node at least this far (pixels) from the goal point (input +0x64)
|
||||||
|
float home_x = 0, home_y = 0, home_radius = 0; // mHomePosition, max_distance_to_move_from_home (input +0x68 / +0x6c / +0x70): the goals of Hunt, RandomMove,
|
||||||
|
// Burning, Away and Escape lie within it (Escape: within sqrt(0.2) of it)
|
||||||
|
int own_herd = -1, aggression = 100; // Hunt: a node marked by another herd whose relation is below the aggression (input +0x78 / +0x7c) ...
|
||||||
|
int target_entity = 0; // ... or by this entity (input +0x88 / +0x8c)
|
||||||
|
int random_distance = 100; // RandomMove: a node further than this (manhattan, pixels) from the start (00864ab0)
|
||||||
|
int* work = nullptr; // when set: the nodes the search expanded are added to it (the host's per-frame budget, standing in for the engine's resumable search)
|
||||||
|
// the directions this creature got stuck going, by world node (the search input's own tree, 00860c20 / 00860bf0: not shared with other creatures); key = world_node_key
|
||||||
|
const std::vector<std::pair<int64_t, uint8_t>>* avoid = nullptr;
|
||||||
|
};
|
||||||
|
inline int64_t world_node_key(int wnx, int wny) { return ((int64_t)wnx << 32) ^ (uint32_t)wny; } // (a node of the world: floor(pixel / 4))
|
||||||
|
|
||||||
|
struct PathNode {
|
||||||
|
int x = 0, y = 0; // world pixel of the node's centre (a node covers the cells 4n .. 4n + 3; a world pixel is in node floor(p / 4))
|
||||||
|
bool jump = false; // reached by a jump from the node before
|
||||||
|
float jvx = 0, jvy = 0; // the launch velocity of that jump (pixels per second)
|
||||||
|
bool jumpFrom = false; // the source of the next node's jump (00861e00 propagates a flag onto it; 00bca030 +0x169: reached within 2 px in x, the launch from there)
|
||||||
|
};
|
||||||
|
|
||||||
|
// A* from the node of (sx, sy): the path from the start to the first node the goal accepts (start excluded); empty when none was found
|
||||||
|
std::vector<PathNode> search(const PathGrid& g, float sx, float sy, const SearchParams& p);
|
||||||
|
|
||||||
|
// the launch velocity of a lob from (x0, y0) to (x1, y1) at `speed` under `gravity` (0063ec90); false when out of reach
|
||||||
|
bool lob_velocity(float x0, float y0, float x1, float y1, float speed, float gravity, float& vx, float& vy);
|
||||||
|
|
||||||
|
} // namespace ng::ai
|
||||||
+544
@@ -0,0 +1,544 @@
|
|||||||
|
#pragma once
|
||||||
|
// Replay recorder: keeps the last N presented frames (compressed with Blosc2/Zstd in a worker thread) and writes them to disk on request: every frame as a PNG
|
||||||
|
// (real deflate), an animated GIF, a contact sheet and a text file with the view / player position of every frame, so that a bug that only shows in motion can be
|
||||||
|
// looked at afterwards. Capture: the host copies the final output image into a host-visible buffer each frame and hands it to push() one frame later.
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <deque>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <blosc2.h>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct ClipMeta { int frame = 0, view_x = 0, view_y = 0; double px = 0, py = 0; double time_ms = 0; float scale = 1, psx = 0, psy = 0; }; // scale: output pixels per cell, psx / psy: the player on the screen (output pixels)
|
||||||
|
struct ClipFrame { int w = 0, h = 0; ClipMeta meta; std::vector<uint8_t> comp; }; // one recorded frame: BGRA w*h*4, compressed
|
||||||
|
|
||||||
|
// ---------- PNG with a real (fixed Huffman + LZ77) deflate, thread safe ----------
|
||||||
|
namespace clipimpl {
|
||||||
|
|
||||||
|
inline uint32_t crc32(const uint8_t* p, size_t n, uint32_t crc = 0xFFFFFFFFu) {
|
||||||
|
static const auto table = [] {
|
||||||
|
struct T { uint32_t v[256]; } t;
|
||||||
|
for (uint32_t i = 0; i < 256; ++i) { uint32_t c = i; for (int k = 0; k < 8; ++k) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1; t.v[i] = c; }
|
||||||
|
return t;
|
||||||
|
}();
|
||||||
|
for (size_t i = 0; i < n; ++i) crc = table.v[(crc ^ p[i]) & 0xFF] ^ (crc >> 8);
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BitWriter {
|
||||||
|
std::vector<uint8_t>& out;
|
||||||
|
uint32_t acc = 0; int nb = 0;
|
||||||
|
explicit BitWriter(std::vector<uint8_t>& o) : out(o) {}
|
||||||
|
void bits(uint32_t v, int n) { acc |= v << nb; nb += n; while (nb >= 8) { out.push_back((uint8_t)acc); acc >>= 8; nb -= 8; } }
|
||||||
|
void huff(uint32_t code, int len) { uint32_t r = 0; for (int i = 0; i < len; ++i) r |= ((code >> i) & 1u) << (len - 1 - i); bits(r, len); } // codes go out most significant bit first
|
||||||
|
void flush() { if (nb > 0) { out.push_back((uint8_t)acc); acc = 0; nb = 0; } }
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void put_lit(BitWriter& w, int c) {
|
||||||
|
if (c < 144) w.huff(0x30 + c, 8);
|
||||||
|
else if (c < 256) w.huff(0x190 + c - 144, 9);
|
||||||
|
else if (c < 280) w.huff(c - 256, 7);
|
||||||
|
else w.huff(0xC0 + c - 280, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<uint8_t> deflate_fixed(const std::vector<uint8_t>& in) {
|
||||||
|
static const uint16_t lbase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
|
||||||
|
static const uint8_t lext[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
|
||||||
|
static const uint16_t dbase[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
|
||||||
|
static const uint8_t dext[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
|
||||||
|
std::vector<uint8_t> out;
|
||||||
|
out.reserve(in.size() / 4 + 64);
|
||||||
|
out.push_back(0x78); out.push_back(0x01);
|
||||||
|
BitWriter w(out);
|
||||||
|
w.bits(1, 1); w.bits(1, 2); // final block, fixed Huffman
|
||||||
|
const size_t n = in.size();
|
||||||
|
std::vector<int32_t> head(1 << 15, -1), prev(n, -1);
|
||||||
|
auto hash = [&](size_t i) { return (uint32_t)((in[i] * 506832829u ^ in[i + 1] * 2654435761u ^ in[i + 2] * 40503u) >> 4) & 0x7FFFu; };
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
int bestLen = 0, bestDist = 0;
|
||||||
|
if (i + 3 <= n) {
|
||||||
|
const uint32_t h = hash(i);
|
||||||
|
int32_t cand = head[h];
|
||||||
|
for (int depth = 0; cand >= 0 && depth < 24 && i - (size_t)cand <= 32768; ++depth, cand = prev[(size_t)cand]) {
|
||||||
|
const size_t maxl = std::min<size_t>(258, n - i);
|
||||||
|
size_t l = 0;
|
||||||
|
while (l < maxl && in[(size_t)cand + l] == in[i + l]) ++l;
|
||||||
|
if ((int)l > bestLen) { bestLen = (int)l; bestDist = (int)(i - (size_t)cand); if (l == maxl) break; }
|
||||||
|
}
|
||||||
|
prev[i] = head[h]; head[h] = (int32_t)i;
|
||||||
|
}
|
||||||
|
if (bestLen >= 3) {
|
||||||
|
int lc = 28; while (lbase[lc] > bestLen) --lc;
|
||||||
|
put_lit(w, 257 + lc);
|
||||||
|
if (lext[lc]) w.bits((uint32_t)(bestLen - lbase[lc]), lext[lc]);
|
||||||
|
int dc = 29; while (dbase[dc] > bestDist) --dc;
|
||||||
|
w.huff((uint32_t)dc, 5);
|
||||||
|
if (dext[dc]) w.bits((uint32_t)(bestDist - dbase[dc]), dext[dc]);
|
||||||
|
for (int k = 1; k < bestLen; ++k) { const size_t j = i + (size_t)k; if (j + 3 <= n) { const uint32_t h = hash(j); prev[j] = head[h]; head[h] = (int32_t)j; } }
|
||||||
|
i += (size_t)bestLen;
|
||||||
|
} else { put_lit(w, in[i]); ++i; }
|
||||||
|
}
|
||||||
|
put_lit(w, 256);
|
||||||
|
w.flush();
|
||||||
|
uint32_t a = 1, b = 0;
|
||||||
|
for (uint8_t v : in) { a = (a + v) % 65521; b = (b + a) % 65521; }
|
||||||
|
const uint32_t ad = (b << 16) | a;
|
||||||
|
for (int s = 24; s >= 0; s -= 8) out.push_back((ad >> s) & 0xFF);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool write_png(const std::string& path, const uint8_t* rgb, int w, int h) {
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
raw.reserve((size_t)h * (w * 3 + 1));
|
||||||
|
for (int y = 0; y < h; ++y) { // filter 1 (sub) per row
|
||||||
|
const uint8_t* row = rgb + (size_t)y * w * 3;
|
||||||
|
raw.push_back(1);
|
||||||
|
for (int x = 0; x < w * 3; ++x) raw.push_back((uint8_t)(row[x] - (x >= 3 ? row[x - 3] : 0)));
|
||||||
|
}
|
||||||
|
const std::vector<uint8_t> z = deflate_fixed(raw);
|
||||||
|
FILE* f = nullptr;
|
||||||
|
if (fopen_s(&f, path.c_str(), "wb") != 0 || !f) return false;
|
||||||
|
auto be32 = [](std::vector<uint8_t>& v, uint32_t x) { for (int s = 24; s >= 0; s -= 8) v.push_back((x >> s) & 0xFF); };
|
||||||
|
auto chunk = [&](const char* type, const std::vector<uint8_t>& data) {
|
||||||
|
std::vector<uint8_t> c;
|
||||||
|
be32(c, (uint32_t)data.size());
|
||||||
|
c.insert(c.end(), type, type + 4);
|
||||||
|
c.insert(c.end(), data.begin(), data.end());
|
||||||
|
be32(c, ~crc32(c.data() + 4, c.size() - 4));
|
||||||
|
fwrite(c.data(), 1, c.size(), f);
|
||||||
|
};
|
||||||
|
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
|
||||||
|
fwrite(sig, 1, 8, f);
|
||||||
|
std::vector<uint8_t> ihdr;
|
||||||
|
be32(ihdr, (uint32_t)w); be32(ihdr, (uint32_t)h);
|
||||||
|
ihdr.insert(ihdr.end(), {8, 2, 0, 0, 0});
|
||||||
|
chunk("IHDR", ihdr); chunk("IDAT", z); chunk("IEND", {});
|
||||||
|
return fclose(f) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- GIF (median cut palette, LZW) ----------
|
||||||
|
struct Palette { uint8_t rgb[256][3]; int n = 0; std::vector<uint8_t> lut; }; // lut: 15-bit colour -> index
|
||||||
|
|
||||||
|
inline Palette make_palette(const std::vector<const uint8_t*>& imgs, int w, int h) { // imgs: RGB w*h*3
|
||||||
|
struct Box { std::vector<uint32_t> px; }; // packed 0xRRGGBB
|
||||||
|
std::vector<uint32_t> all;
|
||||||
|
for (const uint8_t* im : imgs) for (size_t i = 0; i < (size_t)w * h; i += 7) all.push_back((uint32_t)im[i * 3] << 16 | (uint32_t)im[i * 3 + 1] << 8 | im[i * 3 + 2]);
|
||||||
|
std::vector<Box> boxes(1);
|
||||||
|
boxes[0].px = std::move(all);
|
||||||
|
while ((int)boxes.size() < 256) {
|
||||||
|
size_t bi = 0, best = 0;
|
||||||
|
for (size_t k = 0; k < boxes.size(); ++k) if (boxes[k].px.size() > best) { best = boxes[k].px.size(); bi = k; }
|
||||||
|
if (best < 2) break;
|
||||||
|
Box& b = boxes[bi];
|
||||||
|
int lo[3] = {255, 255, 255}, hi[3] = {0, 0, 0};
|
||||||
|
for (uint32_t p : b.px) for (int c = 0; c < 3; ++c) { const int v = (p >> (16 - 8 * c)) & 255; lo[c] = std::min(lo[c], v); hi[c] = std::max(hi[c], v); }
|
||||||
|
int axis = 0; for (int c = 1; c < 3; ++c) if (hi[c] - lo[c] > hi[axis] - lo[axis]) axis = c;
|
||||||
|
if (hi[axis] == lo[axis]) { bool any = false; for (size_t k = 0; k < boxes.size(); ++k) if (k != bi && boxes[k].px.size() > 1) any = true; if (!any) break; boxes[bi].px.shrink_to_fit(); Box tmp; tmp.px.assign(1, b.px[0]); std::vector<uint32_t> rest(b.px.begin() + 1, b.px.end()); b.px = std::move(rest); boxes.push_back(std::move(tmp)); continue; }
|
||||||
|
const int sh = 16 - 8 * axis;
|
||||||
|
std::sort(b.px.begin(), b.px.end(), [&](uint32_t x, uint32_t y) { return ((x >> sh) & 255) < ((y >> sh) & 255); });
|
||||||
|
Box nb;
|
||||||
|
nb.px.assign(b.px.begin() + b.px.size() / 2, b.px.end());
|
||||||
|
b.px.resize(b.px.size() / 2);
|
||||||
|
boxes.push_back(std::move(nb));
|
||||||
|
}
|
||||||
|
Palette pal;
|
||||||
|
for (const Box& b : boxes) {
|
||||||
|
if (b.px.empty() || pal.n >= 256) continue;
|
||||||
|
uint64_t s[3] = {0, 0, 0};
|
||||||
|
for (uint32_t p : b.px) for (int c = 0; c < 3; ++c) s[c] += (p >> (16 - 8 * c)) & 255;
|
||||||
|
for (int c = 0; c < 3; ++c) pal.rgb[pal.n][c] = (uint8_t)(s[c] / b.px.size());
|
||||||
|
++pal.n;
|
||||||
|
}
|
||||||
|
pal.lut.assign(32768, 0);
|
||||||
|
for (int k = 0; k < 32768; ++k) {
|
||||||
|
const int r = ((k >> 10) & 31) * 255 / 31, g = ((k >> 5) & 31) * 255 / 31, bl = (k & 31) * 255 / 31;
|
||||||
|
int bestd = 1 << 30, bi = 0;
|
||||||
|
for (int p = 0; p < pal.n; ++p) { const int dr = r - pal.rgb[p][0], dg = g - pal.rgb[p][1], db = bl - pal.rgb[p][2]; const int d = dr * dr + dg * dg + db * db; if (d < bestd) { bestd = d; bi = p; } }
|
||||||
|
pal.lut[(size_t)k] = (uint8_t)bi;
|
||||||
|
}
|
||||||
|
return pal;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void gif_lzw(std::vector<uint8_t>& out, const std::vector<uint8_t>& idx) {
|
||||||
|
out.push_back(8); // minimum code size
|
||||||
|
std::vector<uint8_t> blk;
|
||||||
|
uint32_t acc = 0; int nb = 0;
|
||||||
|
auto emit = [&](uint32_t code, int bits) { acc |= code << nb; nb += bits; while (nb >= 8) { blk.push_back((uint8_t)acc); acc >>= 8; nb -= 8; } };
|
||||||
|
std::unordered_map<uint32_t, uint16_t> dict;
|
||||||
|
int next = 258, bits = 9;
|
||||||
|
emit(256, bits);
|
||||||
|
uint32_t cur = idx.empty() ? 0 : idx[0];
|
||||||
|
for (size_t i = 1; i < idx.size(); ++i) {
|
||||||
|
const uint32_t key = cur << 8 | idx[i];
|
||||||
|
auto it = dict.find(key);
|
||||||
|
if (it != dict.end()) { cur = it->second; continue; }
|
||||||
|
emit(cur, bits);
|
||||||
|
if (next < 4096) { dict[key] = (uint16_t)next++; if (next > (1 << bits) && bits < 12) ++bits; }
|
||||||
|
else { emit(256, bits); dict.clear(); next = 258; bits = 9; }
|
||||||
|
cur = idx[i];
|
||||||
|
}
|
||||||
|
if (!idx.empty()) emit(cur, bits);
|
||||||
|
emit(257, bits);
|
||||||
|
if (nb > 0) blk.push_back((uint8_t)acc);
|
||||||
|
for (size_t p = 0; p < blk.size(); p += 255) { const size_t c = std::min<size_t>(255, blk.size() - p); out.push_back((uint8_t)c); out.insert(out.end(), blk.begin() + p, blk.begin() + p + c); }
|
||||||
|
out.push_back(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::mutex& blosc_mutex() { static std::mutex m; return m; }
|
||||||
|
|
||||||
|
// The BGRA pixels of a recorded frame.
|
||||||
|
inline bool decode_bgra(const ClipFrame& f, std::vector<uint8_t>& bgra) {
|
||||||
|
bgra.resize((size_t)f.w * f.h * 4);
|
||||||
|
std::lock_guard<std::mutex> bl(blosc_mutex());
|
||||||
|
return blosc2_decompress(f.comp.data(), (int32_t)f.comp.size(), bgra.data(), (int32_t)bgra.size()) == (int)bgra.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rectangle (x, y, w, h) of BGRA src (sw wide) to RGB dst of dw x dh, every output pixel the mean of the source pixels it covers.
|
||||||
|
inline void crop_scale_rgb(const uint8_t* src, int sw, int x, int y, int w, int h, int dw, int dh, std::vector<uint8_t>& dst) {
|
||||||
|
dst.resize((size_t)dw * dh * 3);
|
||||||
|
std::vector<int> xs(dw + 1);
|
||||||
|
for (int i = 0; i <= dw; ++i) xs[i] = x + (int)((int64_t)i * w / dw);
|
||||||
|
for (int j = 0; j < dh; ++j) {
|
||||||
|
const int y0 = y + (int)((int64_t)j * h / dh), y1 = std::max(y0 + 1, y + (int)((int64_t)(j + 1) * h / dh));
|
||||||
|
for (int i = 0; i < dw; ++i) {
|
||||||
|
const int x0 = xs[i], x1 = std::max(x0 + 1, xs[i + 1]);
|
||||||
|
uint32_t s[3] = {0, 0, 0};
|
||||||
|
for (int yy = y0; yy < y1; ++yy) { const uint8_t* p = src + ((size_t)yy * sw + x0) * 4; for (int xx = x0; xx < x1; ++xx, p += 4) { s[0] += p[2]; s[1] += p[1]; s[2] += p[0]; } }
|
||||||
|
const uint32_t n = (uint32_t)((y1 - y0) * (x1 - x0));
|
||||||
|
uint8_t* d = &dst[((size_t)j * dw + i) * 3];
|
||||||
|
d[0] = (uint8_t)(s[0] / n); d[1] = (uint8_t)(s[1] / n); d[2] = (uint8_t)(s[2] / n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The palette of one image (rgb, n pixels) for a GIF local colour table: a histogram of all its pixels (6 bits per channel), cut into boxes, always the box with
|
||||||
|
// the largest squared error, at the weighted mean of its widest channel. A small patch of a colour far from the rest (the player's robe, a spark) then gets an
|
||||||
|
// entry of its own instead of being averaged into a large box, as the plain median cut by pixel count does; an image with at most 256 colours keeps them all.
|
||||||
|
// No lut is made (GifWriter finds the nearest entry of the colours it meets).
|
||||||
|
inline Palette make_palette_frame(const uint8_t* rgb, size_t n) {
|
||||||
|
std::vector<uint32_t> hist(1u << 18, 0);
|
||||||
|
for (size_t i = 0; i < n; ++i) ++hist[(uint32_t)(rgb[i * 3] >> 2) << 12 | (uint32_t)(rgb[i * 3 + 1] >> 2) << 6 | (uint32_t)(rgb[i * 3 + 2] >> 2)];
|
||||||
|
struct E { uint32_t c, n; };
|
||||||
|
std::vector<E> es;
|
||||||
|
for (uint32_t c = 0; c < hist.size(); ++c) if (hist[c]) es.push_back({c, hist[c]});
|
||||||
|
auto ch = [](uint32_t c, int k) { const uint32_t v = (c >> (12 - 6 * k)) & 63; return (double)(v << 2 | v >> 4); };
|
||||||
|
struct Box { size_t b, e; double n, s[3], sse; };
|
||||||
|
auto stats = [&](size_t b, size_t e) {
|
||||||
|
Box bx{b, e, 0, {0, 0, 0}, 0};
|
||||||
|
double s2 = 0;
|
||||||
|
for (size_t i = b; i < e; ++i) for (int k = 0; k < 3; ++k) { const double v = ch(es[i].c, k); bx.s[k] += v * es[i].n; s2 += v * v * es[i].n; }
|
||||||
|
for (size_t i = b; i < e; ++i) bx.n += es[i].n;
|
||||||
|
bx.sse = s2 - (bx.s[0] * bx.s[0] + bx.s[1] * bx.s[1] + bx.s[2] * bx.s[2]) / std::max(1.0, bx.n);
|
||||||
|
return bx;
|
||||||
|
};
|
||||||
|
std::vector<Box> boxes;
|
||||||
|
if (!es.empty()) boxes.push_back(stats(0, es.size()));
|
||||||
|
while (boxes.size() < 256) {
|
||||||
|
size_t bi = 0;
|
||||||
|
for (size_t k = 1; k < boxes.size(); ++k) if (boxes[k].sse > boxes[bi].sse) bi = k;
|
||||||
|
const Box bx = boxes[bi];
|
||||||
|
if (bx.sse <= 0.5 || bx.e - bx.b < 2) break;
|
||||||
|
int axis = 0; double best = -1;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const double m = bx.s[k] / bx.n;
|
||||||
|
double var = 0;
|
||||||
|
for (size_t i = bx.b; i < bx.e; ++i) { const double d = ch(es[i].c, k) - m; var += d * d * es[i].n; }
|
||||||
|
if (var > best) { best = var; axis = k; }
|
||||||
|
}
|
||||||
|
const double mean = bx.s[axis] / bx.n;
|
||||||
|
const auto mid = std::partition(es.begin() + (std::ptrdiff_t)bx.b, es.begin() + (std::ptrdiff_t)bx.e, [&](const E& x) { return ch(x.c, axis) <= mean; });
|
||||||
|
const size_t m = (size_t)(mid - es.begin());
|
||||||
|
if (m == bx.b || m == bx.e) break;
|
||||||
|
boxes[bi] = stats(bx.b, m);
|
||||||
|
boxes.push_back(stats(m, bx.e));
|
||||||
|
}
|
||||||
|
Palette pal;
|
||||||
|
for (const Box& bx : boxes) {
|
||||||
|
for (int k = 0; k < 3; ++k) pal.rgb[pal.n][k] = (uint8_t)std::clamp((int)std::lround(bx.s[k] / bx.n), 0, 255);
|
||||||
|
++pal.n;
|
||||||
|
}
|
||||||
|
if (pal.n == 0) { pal.rgb[0][0] = pal.rgb[0][1] = pal.rgb[0][2] = 0; pal.n = 1; }
|
||||||
|
return pal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a looping GIF frame by frame, so that a long clip does not have to be held in memory: with one global palette (made with make_palette, its lut used),
|
||||||
|
// or with a local colour table per frame (open without a palette, add with one from make_palette_frame), optionally with a light ordered (Bayer 4x4) dither.
|
||||||
|
class GifWriter {
|
||||||
|
public:
|
||||||
|
~GifWriter() { if (f_) std::fclose(f_); }
|
||||||
|
bool open(const std::string& path, int w, int h, const Palette* pal) {
|
||||||
|
w_ = w; h_ = h; pal_ = pal; bytes_ = 0;
|
||||||
|
if (fopen_s(&f_, path.c_str(), "wb") != 0 || !f_) { f_ = nullptr; return false; }
|
||||||
|
std::vector<uint8_t> g;
|
||||||
|
const char* hdr = "GIF89a";
|
||||||
|
g.insert(g.end(), hdr, hdr + 6);
|
||||||
|
g.push_back((uint8_t)(w & 255)); g.push_back((uint8_t)(w >> 8)); g.push_back((uint8_t)(h & 255)); g.push_back((uint8_t)(h >> 8));
|
||||||
|
g.push_back(pal ? 0xF7 : 0x70); g.push_back(0); g.push_back(0);
|
||||||
|
if (pal) for (int p = 0; p < 256; ++p) for (int c = 0; c < 3; ++c) g.push_back(p < pal->n ? pal->rgb[p][c] : 0);
|
||||||
|
const uint8_t app[] = {0x21, 0xFF, 0x0B, 'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0', 0x03, 0x01, 0, 0, 0}; // loop forever
|
||||||
|
g.insert(g.end(), app, app + sizeof app);
|
||||||
|
put(g);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// rgb: w*h*3; delay in hundredths of a second; local: the palette of this frame (a local colour table); dither: the span of the ordered dither (0 = none).
|
||||||
|
void add(const uint8_t* rgb, int delay_cs, const Palette* local = nullptr, int dither = 0) {
|
||||||
|
if (!f_ || (!local && !pal_)) return;
|
||||||
|
std::vector<uint8_t> idx((size_t)w_ * h_), g;
|
||||||
|
if (!local) for (size_t i = 0; i < idx.size(); ++i) idx[i] = pal_->lut[(size_t)((rgb[i * 3] >> 3) << 10 | (rgb[i * 3 + 1] >> 3) << 5 | (rgb[i * 3 + 2] >> 3))];
|
||||||
|
else { // the nearest entry of each colour met (6 bits per channel), cached
|
||||||
|
static const int bayer[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
|
||||||
|
std::vector<int16_t> nearest(1u << 18, -1);
|
||||||
|
for (int y = 0; y < h_; ++y) for (int x = 0; x < w_; ++x) {
|
||||||
|
const size_t i = (size_t)y * w_ + x;
|
||||||
|
const int d = dither ? (bayer[y & 3][x & 3] * 2 - 15) * dither / 32 : 0;
|
||||||
|
const int r = std::clamp(rgb[i * 3] + d, 0, 255), gg = std::clamp(rgb[i * 3 + 1] + d, 0, 255), b = std::clamp(rgb[i * 3 + 2] + d, 0, 255);
|
||||||
|
const uint32_t key = (uint32_t)(r >> 2) << 12 | (uint32_t)(gg >> 2) << 6 | (uint32_t)(b >> 2);
|
||||||
|
int16_t& e = nearest[key];
|
||||||
|
if (e < 0) {
|
||||||
|
int bestd = 1 << 30, bi = 0;
|
||||||
|
for (int p = 0; p < local->n; ++p) { const int dr = r - local->rgb[p][0], dg = gg - local->rgb[p][1], db = b - local->rgb[p][2]; const int dd = dr * dr + dg * dg + db * db; if (dd < bestd) { bestd = dd; bi = p; } }
|
||||||
|
e = (int16_t)bi;
|
||||||
|
}
|
||||||
|
idx[i] = (uint8_t)e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const uint8_t gce[] = {0x21, 0xF9, 0x04, 0x00, (uint8_t)(delay_cs & 255), (uint8_t)(delay_cs >> 8), 0, 0};
|
||||||
|
g.insert(g.end(), gce, gce + sizeof gce);
|
||||||
|
g.push_back(0x2C); g.push_back(0); g.push_back(0); g.push_back(0); g.push_back(0);
|
||||||
|
g.push_back((uint8_t)(w_ & 255)); g.push_back((uint8_t)(w_ >> 8)); g.push_back((uint8_t)(h_ & 255)); g.push_back((uint8_t)(h_ >> 8));
|
||||||
|
if (local) { g.push_back(0x87); for (int p = 0; p < 256; ++p) for (int c = 0; c < 3; ++c) g.push_back(p < local->n ? local->rgb[p][c] : 0); } // a local colour table of 256 entries
|
||||||
|
else g.push_back(0);
|
||||||
|
gif_lzw(g, idx);
|
||||||
|
put(g);
|
||||||
|
}
|
||||||
|
// Returns the size of the file, 0 if it could not be written.
|
||||||
|
size_t close() {
|
||||||
|
if (!f_) return 0;
|
||||||
|
const uint8_t end = 0x3B;
|
||||||
|
std::fwrite(&end, 1, 1, f_); ++bytes_;
|
||||||
|
const bool ok = std::fclose(f_) == 0 && !failed_;
|
||||||
|
f_ = nullptr;
|
||||||
|
return ok ? bytes_ : 0;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
void put(const std::vector<uint8_t>& g) { if (std::fwrite(g.data(), 1, g.size(), f_) != g.size()) failed_ = true; bytes_ += g.size(); }
|
||||||
|
FILE* f_ = nullptr;
|
||||||
|
int w_ = 0, h_ = 0;
|
||||||
|
const Palette* pal_ = nullptr;
|
||||||
|
size_t bytes_ = 0;
|
||||||
|
bool failed_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The frames of a GIF from the frame times (ms): every step-th frame of [in, out], each shown until the next one picked, in whole hundredths rounded on the running
|
||||||
|
// clock so that the GIF keeps the real speed. A frame that would last less than 2 cs is left out (browsers show such frames at 10 cs), the one before it lasts longer.
|
||||||
|
struct GifPick { int index = 0; int delay_cs = 2; };
|
||||||
|
inline std::vector<GifPick> gif_pick(const std::vector<double>& t_ms, int in, int out, int step) {
|
||||||
|
std::vector<GifPick> picks;
|
||||||
|
if (t_ms.empty() || in > out) return picks;
|
||||||
|
step = std::max(1, step);
|
||||||
|
const double per = out > in ? (t_ms[(size_t)out] - t_ms[(size_t)in]) / (out - in) : 1000.0 / 60.0; // the mean frame time (the last frame lasts that long times the step)
|
||||||
|
int prevCs = 0;
|
||||||
|
for (int i = in; i <= out; i += step) {
|
||||||
|
const int cs = (int)std::lround((t_ms[(size_t)i] - t_ms[(size_t)in]) / 10.0);
|
||||||
|
if (!picks.empty()) { if (cs - prevCs < 2) continue; picks.back().delay_cs = cs - prevCs; }
|
||||||
|
GifPick p; p.index = i; picks.push_back(p);
|
||||||
|
prevCs = cs;
|
||||||
|
}
|
||||||
|
const int last = picks.back().index;
|
||||||
|
const int endCs = (int)std::lround((t_ms[(size_t)last] - t_ms[(size_t)in] + per * step) / 10.0);
|
||||||
|
picks.back().delay_cs = std::max(2, endCs - prevCs);
|
||||||
|
return picks;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace clipimpl
|
||||||
|
|
||||||
|
class ClipRecorder {
|
||||||
|
public:
|
||||||
|
explicit ClipRecorder(size_t max_frames) : max_(max_frames) { blosc2_init(); worker_ = std::thread([this] { run(); }); }
|
||||||
|
~ClipRecorder() {
|
||||||
|
{ std::lock_guard<std::mutex> l(mu_); stop_ = true; }
|
||||||
|
cv_.notify_all();
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
for (auto& t : writers_) if (t.joinable()) t.join();
|
||||||
|
}
|
||||||
|
size_t frames() { std::lock_guard<std::mutex> l(ringMu_); return ring_.size(); }
|
||||||
|
size_t dropped() const { return dropped_.load(); }
|
||||||
|
size_t bytes() { std::lock_guard<std::mutex> l(ringMu_); size_t b = 0; for (const auto& f : ring_) b += f->comp.size(); return b; } // the compressed size of the ring
|
||||||
|
// The frames of the ring now (oldest first); they stay valid while the ring moves on.
|
||||||
|
std::vector<std::shared_ptr<const ClipFrame>> snapshot() { std::lock_guard<std::mutex> l(ringMu_); return std::vector<std::shared_ptr<const ClipFrame>>(ring_.begin(), ring_.end()); }
|
||||||
|
|
||||||
|
// bgra: w*h*4 bytes of the output image (blue first), read by the worker thread straight from where it is (a mapped buffer): `busy` is true until it is done with it.
|
||||||
|
void push_ref(const uint8_t* bgra, int w, int h, const ClipMeta& m, std::atomic<bool>* busy) {
|
||||||
|
std::lock_guard<std::mutex> l(mu_);
|
||||||
|
if (queue_.size() >= 6) { ++dropped_; busy->store(false); return; }
|
||||||
|
Job j; j.w = w; j.h = h; j.meta = m; j.ref = bgra; j.busy = busy;
|
||||||
|
queue_.push_back(std::move(j));
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the frames of the ring to <base>/clip_<time>/ on a background thread; returns the folder.
|
||||||
|
std::string dump(const std::string& base) {
|
||||||
|
std::vector<std::shared_ptr<Frame>> snap;
|
||||||
|
{ std::lock_guard<std::mutex> l(ringMu_); snap.assign(ring_.begin(), ring_.end()); }
|
||||||
|
const std::time_t t = std::time(nullptr);
|
||||||
|
std::tm tmv{};
|
||||||
|
localtime_s(&tmv, &t);
|
||||||
|
char name[64];
|
||||||
|
std::snprintf(name, sizeof name, "clip_%04d%02d%02d_%02d%02d%02d", tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
|
||||||
|
const std::string dir = (std::filesystem::path(base) / name).string();
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(dir, ec);
|
||||||
|
writers_.emplace_back([this, snap = std::move(snap), dir] { write_all(snap, dir); });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Frame = ClipFrame;
|
||||||
|
struct Job { int w = 0, h = 0; ClipMeta meta; const uint8_t* ref = nullptr; std::atomic<bool>* busy = nullptr; };
|
||||||
|
|
||||||
|
static std::mutex& bloscMutex() { return clipimpl::blosc_mutex(); }
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
for (;;) {
|
||||||
|
Job j;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> l(mu_);
|
||||||
|
cv_.wait(l, [this] { return stop_ || !queue_.empty(); });
|
||||||
|
if (queue_.empty()) return;
|
||||||
|
j = std::move(queue_.front());
|
||||||
|
queue_.pop_front();
|
||||||
|
}
|
||||||
|
auto f = std::make_shared<Frame>();
|
||||||
|
f->w = j.w; f->h = j.h; f->meta = j.meta;
|
||||||
|
// a window wider than 1600 px is recorded at half size (2 x 2 average): at 2560 x 1440 a frame is 14.7 MB raw and explosions compress
|
||||||
|
// badly, so 1200 frames grew to 10 - 17 GB and the compression fell behind (the GIFs of the editor are small anyway)
|
||||||
|
const uint8_t* src = j.ref;
|
||||||
|
if (j.w > 1600 && j.h >= 2) {
|
||||||
|
const int hw = j.w / 2, hh = j.h / 2;
|
||||||
|
half_.resize((size_t)hw * hh * 4);
|
||||||
|
for (int y = 0; y < hh; ++y) {
|
||||||
|
const uint8_t* r0 = j.ref + (size_t)(2 * y) * j.w * 4;
|
||||||
|
const uint8_t* r1 = r0 + (size_t)j.w * 4;
|
||||||
|
uint8_t* d = &half_[(size_t)y * hw * 4];
|
||||||
|
for (int x = 0; x < hw; ++x, d += 4)
|
||||||
|
for (int c = 0; c < 4; ++c) d[c] = (uint8_t)((r0[8 * x + c] + r0[8 * x + 4 + c] + r1[8 * x + c] + r1[8 * x + 4 + c] + 2) >> 2);
|
||||||
|
}
|
||||||
|
f->w = hw; f->h = hh; src = half_.data();
|
||||||
|
f->meta.scale *= 0.5f; f->meta.psx *= 0.5f; f->meta.psy *= 0.5f; // (window pixels per world pixel, the player on the window)
|
||||||
|
j.busy->store(false); // (the window's buffer is free as soon as it is copied)
|
||||||
|
}
|
||||||
|
const size_t rawBytes = (size_t)f->w * f->h * 4;
|
||||||
|
f->comp.resize(rawBytes + BLOSC2_MAX_OVERHEAD);
|
||||||
|
int cs;
|
||||||
|
// Zstd level 1: about 0.2 MB for a 1280x720 frame and 2 ms on this thread (LZ4 level 1: 0.38 MB and 1.3 ms), so the 1200 frames of the ring take about 250 MB
|
||||||
|
{ std::lock_guard<std::mutex> bl(bloscMutex()); blosc1_set_compressor("zstd"); cs = blosc2_compress(1, BLOSC_SHUFFLE, 4, src, (int32_t)rawBytes, f->comp.data(), (int32_t)f->comp.size()); }
|
||||||
|
j.busy->store(false); // the buffer can be written again
|
||||||
|
if (cs <= 0) continue;
|
||||||
|
f->comp.resize((size_t)cs);
|
||||||
|
f->comp.shrink_to_fit(); // (resize keeps the raw-size allocation)
|
||||||
|
std::lock_guard<std::mutex> l(ringMu_);
|
||||||
|
ringBytes_ += f->comp.size();
|
||||||
|
ring_.push_back(std::move(f));
|
||||||
|
// at most max_ frames and budget_ bytes (NG_CLIP_MB, default 1536 MB): the oldest go
|
||||||
|
while (ring_.size() > 1 && (ring_.size() > max_ || ringBytes_ > budget_)) { ringBytes_ -= ring_.front()->comp.size(); ring_.pop_front(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_all(const std::vector<std::shared_ptr<Frame>>& frames, const std::string& dir) {
|
||||||
|
if (frames.empty()) return;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
auto to_rgb = [&](const Frame& f, std::vector<uint8_t>& rgb) {
|
||||||
|
std::vector<uint8_t> bgra((size_t)f.w * f.h * 4);
|
||||||
|
{ std::lock_guard<std::mutex> bl(bloscMutex()); blosc2_decompress(f.comp.data(), (int32_t)f.comp.size(), bgra.data(), (int32_t)bgra.size()); }
|
||||||
|
rgb.resize((size_t)f.w * f.h * 3);
|
||||||
|
for (size_t i = 0; i < (size_t)f.w * f.h; ++i) { rgb[i * 3] = bgra[i * 4 + 2]; rgb[i * 3 + 1] = bgra[i * 4 + 1]; rgb[i * 3 + 2] = bgra[i * 4]; }
|
||||||
|
};
|
||||||
|
// info
|
||||||
|
{
|
||||||
|
FILE* fi = nullptr;
|
||||||
|
fopen_s(&fi, (fs::path(dir) / "info.txt").string().c_str(), "wb");
|
||||||
|
if (fi) {
|
||||||
|
std::fprintf(fi, "frames: %zu (file index, game frame, ms since the first, view x y, player x y)\r\n", frames.size());
|
||||||
|
for (size_t k = 0; k < frames.size(); ++k) std::fprintf(fi, "%04zu frame %d %.1f ms view %d %d player %.1f %.1f\r\n", k, frames[k]->meta.frame, frames[k]->meta.time_ms - frames[0]->meta.time_ms, frames[k]->meta.view_x, frames[k]->meta.view_y, frames[k]->meta.px, frames[k]->meta.py);
|
||||||
|
std::fclose(fi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double span = (frames.back()->meta.time_ms - frames.front()->meta.time_ms) / 1000.0;
|
||||||
|
const double fps = span > 0.01 ? (double)(frames.size() - 1) / span : 60.0;
|
||||||
|
const int step = std::max(1, (int)std::lround(fps / 50.0)); // a GIF frame lasts at least 2 cs
|
||||||
|
// the GIF (half size) and the PNG frames
|
||||||
|
const int gw = frames[0]->w / 2, gh = frames[0]->h / 2;
|
||||||
|
std::vector<std::vector<uint8_t>> gifFrames;
|
||||||
|
std::vector<uint8_t> rgb;
|
||||||
|
std::vector<std::vector<uint8_t>> sample;
|
||||||
|
auto half = [&](const std::vector<uint8_t>& src, int w, int h, std::vector<uint8_t>& dst) {
|
||||||
|
dst.resize((size_t)(w / 2) * (h / 2) * 3);
|
||||||
|
for (int y = 0; y < h / 2; ++y) for (int x = 0; x < w / 2; ++x) for (int c = 0; c < 3; ++c)
|
||||||
|
dst[((size_t)y * (w / 2) + x) * 3 + c] = (uint8_t)((src[((size_t)(2 * y) * w + 2 * x) * 3 + c] + src[((size_t)(2 * y) * w + 2 * x + 1) * 3 + c] + src[((size_t)(2 * y + 1) * w + 2 * x) * 3 + c] + src[((size_t)(2 * y + 1) * w + 2 * x + 1) * 3 + c]) / 4);
|
||||||
|
};
|
||||||
|
std::vector<std::vector<uint8_t>> sheetTiles;
|
||||||
|
const size_t sheetCount = 30;
|
||||||
|
for (size_t k = 0; k < frames.size(); ++k) {
|
||||||
|
const Frame& f = *frames[k];
|
||||||
|
to_rgb(f, rgb);
|
||||||
|
char fn[32];
|
||||||
|
std::snprintf(fn, sizeof fn, "f%04zu.png", k);
|
||||||
|
clipimpl::write_png((fs::path(dir) / fn).string(), rgb.data(), f.w, f.h);
|
||||||
|
if (f.w / 2 == gw && f.h / 2 == gh && k % (size_t)step == 0) { std::vector<uint8_t> hf; half(rgb, f.w, f.h, hf); gifFrames.push_back(std::move(hf)); }
|
||||||
|
if (k % std::max<size_t>(1, frames.size() / sheetCount) == 0 && sheetTiles.size() < sheetCount) { std::vector<uint8_t> hf, qf; half(rgb, f.w, f.h, hf); half(hf, f.w / 2, f.h / 2, qf); sheetTiles.push_back(std::move(qf)); }
|
||||||
|
}
|
||||||
|
// contact sheet: 6 columns of quarter-size frames
|
||||||
|
if (!sheetTiles.empty()) {
|
||||||
|
const int tw = frames[0]->w / 4, th = frames[0]->h / 4, cols = 6, rows = (int)((sheetTiles.size() + cols - 1) / cols);
|
||||||
|
std::vector<uint8_t> sheet((size_t)cols * tw * rows * th * 3, 0);
|
||||||
|
for (size_t t = 0; t < sheetTiles.size(); ++t) {
|
||||||
|
const int cx = (int)(t % cols), cy = (int)(t / cols);
|
||||||
|
for (int y = 0; y < th; ++y) std::memcpy(&sheet[((size_t)(cy * th + y) * cols * tw + (size_t)cx * tw) * 3], &sheetTiles[t][(size_t)y * tw * 3], (size_t)tw * 3);
|
||||||
|
}
|
||||||
|
clipimpl::write_png((fs::path(dir) / "sheet.png").string(), sheet.data(), cols * tw, rows * th);
|
||||||
|
}
|
||||||
|
// GIF
|
||||||
|
if (!gifFrames.empty()) {
|
||||||
|
std::vector<const uint8_t*> ptrs;
|
||||||
|
for (size_t k = 0; k < gifFrames.size(); k += std::max<size_t>(1, gifFrames.size() / 8)) ptrs.push_back(gifFrames[k].data());
|
||||||
|
const clipimpl::Palette pal = clipimpl::make_palette(ptrs, gw, gh);
|
||||||
|
clipimpl::GifWriter gwr;
|
||||||
|
if (gwr.open((fs::path(dir) / "clip.gif").string(), gw, gh, &pal)) {
|
||||||
|
for (const auto& gf : gifFrames) gwr.add(gf.data(), 2); // 2 cs
|
||||||
|
gwr.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::printf("clip written: %s (%zu frames)%c", dir.c_str(), frames.size(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t max_;
|
||||||
|
std::mutex mu_, ringMu_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
std::deque<Job> queue_;
|
||||||
|
std::vector<uint8_t> half_; // the worker's half-size copy
|
||||||
|
size_t ringBytes_ = 0;
|
||||||
|
size_t budget_ = (std::getenv("NG_CLIP_MB") ? (size_t)std::max(64, std::atoi(std::getenv("NG_CLIP_MB"))) : (size_t)1536) << 20;
|
||||||
|
std::deque<std::shared_ptr<Frame>> ring_;
|
||||||
|
std::thread worker_;
|
||||||
|
std::vector<std::thread> writers_;
|
||||||
|
bool stop_ = false;
|
||||||
|
std::atomic<size_t> dropped_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
#pragma once
|
||||||
|
// The replay clip editor (F11), after the game's own: the frames kept by the replay recorder (clip.h) are shown over the dimmed, paused game. A strip of thumbnails
|
||||||
|
// covers the whole recording; left click on it sets the start of the clip, right click the end, and the preview box plays the clip in a loop (or shows the hovered
|
||||||
|
// frame). Enter writes the clip as clips/gif_<time>.gif on a worker thread, either the whole window or a box of 240x240 ... 480x320 world pixels that follows the
|
||||||
|
// player (the player position of every frame is in its ClipMeta), at 100% / 50% / 33% / 25% of the recorded pixels and every 1st, 2nd or 3rd frame.
|
||||||
|
// The host routes the keys and the mouse buttons here while it is open (see Input), pauses the game and stops recording, and draws it over the HUD.
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "app/gif_meta.h"
|
||||||
|
#include "app/clip.h"
|
||||||
|
#include "app/hud.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
class GifEditor {
|
||||||
|
public:
|
||||||
|
struct Input {
|
||||||
|
int mx = -1, my = -1;
|
||||||
|
int ldown = 0, rdown = 0; // presses of the left / right button since the last frame
|
||||||
|
std::vector<int> keys; // virtual key codes pressed since the last frame
|
||||||
|
};
|
||||||
|
enum Result { kNone = 0, kClose = 1, kOpenDir = 2 };
|
||||||
|
static constexpr int kDither = 4; // the span of the ordered dither in 0..255 units: light, so that the flat areas of the pixel art stay flat
|
||||||
|
|
||||||
|
~GifEditor() { stop_thumbs(); if (saver_.joinable()) saver_.join(); }
|
||||||
|
|
||||||
|
bool is_open() const { return open_; }
|
||||||
|
|
||||||
|
void open(std::vector<std::shared_ptr<const ClipFrame>> frames) {
|
||||||
|
stop_thumbs();
|
||||||
|
frames_ = std::move(frames);
|
||||||
|
open_ = !frames_.empty();
|
||||||
|
if (!open_) return;
|
||||||
|
const int n = (int)frames_.size();
|
||||||
|
times_.resize((size_t)n);
|
||||||
|
for (int i = 0; i < n; ++i) times_[(size_t)i] = frames_[(size_t)i]->meta.time_ms;
|
||||||
|
in_ = 0; out_ = n - 1; ph_ = 0; playing_ = true; acc_ = 0; last_ = std::chrono::steady_clock::now();
|
||||||
|
cacheIdx_ = -1; hits_.clear(); stripW_ = 0;
|
||||||
|
thumbs_.assign((size_t)n, std::vector<uint32_t>());
|
||||||
|
ready_.reset(new std::atomic<bool>[(size_t)n]);
|
||||||
|
for (int i = 0; i < n; ++i) ready_[(size_t)i] = false;
|
||||||
|
thumbStop_ = false;
|
||||||
|
thumbT_ = std::thread([this] { make_thumbs(); });
|
||||||
|
}
|
||||||
|
void close() { stop_thumbs(); open_ = false; frames_.clear(); thumbs_.clear(); cache_.clear(); cacheIdx_ = -1; }
|
||||||
|
|
||||||
|
// Settings (the test hook sets them directly).
|
||||||
|
void set_range(int a, int b) { const int n = (int)frames_.size(); in_ = std::clamp(a, 0, n - 1); out_ = std::clamp(b, in_, n - 1); ph_ = in_; acc_ = 0; }
|
||||||
|
void set_size(int k) { size_ = std::clamp(k, 0, kSizes - 1); }
|
||||||
|
void set_scale(int k) { scale_ = std::clamp(k, 0, kScales - 1); }
|
||||||
|
void set_step(int k) { step_ = std::clamp(k, 1, 3); }
|
||||||
|
bool saving() const { return saving_.load(); }
|
||||||
|
void wait_save() { if (saver_.joinable()) saver_.join(); }
|
||||||
|
void set_meta(const GifMeta& m) { meta_ = m; } // the build and the wands written into the next saved GIF (docs/GIF_METADATA.md)
|
||||||
|
void set_test_palette(bool global, int dither) { globalPal_ = global; dither_ = dither; } // (test: the old single palette, the span of the dither)
|
||||||
|
|
||||||
|
// Starts writing the clip to <dir>/gif_<time>.gif on the worker thread (nothing happens while a save is running).
|
||||||
|
void save(const std::string& dir) {
|
||||||
|
if (!open_ || saving_.load()) return;
|
||||||
|
if (saver_.joinable()) saver_.join();
|
||||||
|
std::vector<clipimpl::GifPick> picks = clipimpl::gif_pick(times_, in_, out_, step_);
|
||||||
|
if (picks.empty()) return;
|
||||||
|
int ow = 0, oh = 0;
|
||||||
|
output_size(ow, oh);
|
||||||
|
std::vector<std::shared_ptr<const ClipFrame>> sel;
|
||||||
|
for (const auto& p : picks) sel.push_back(frames_[(size_t)p.index]);
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(dir, ec);
|
||||||
|
const std::time_t t = std::time(nullptr);
|
||||||
|
std::tm tmv{};
|
||||||
|
localtime_s(&tmv, &t);
|
||||||
|
char name[64];
|
||||||
|
std::snprintf(name, sizeof name, "gif_%04d%02d%02d_%02d%02d%02d", tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
|
||||||
|
std::string path = (std::filesystem::path(dir) / (std::string(name) + ".gif")).string();
|
||||||
|
for (int k = 2; std::filesystem::exists(path, ec); ++k) path = (std::filesystem::path(dir) / (std::string(name) + "_" + std::to_string(k) + ".gif")).string();
|
||||||
|
saveDone_ = 0; saveTotal_ = (int)picks.size(); saving_ = true;
|
||||||
|
const int size = size_;
|
||||||
|
saver_ = std::thread([this, sel = std::move(sel), picks = std::move(picks), ow, oh, size, path] { write_gif(sel, picks, ow, oh, size, path); });
|
||||||
|
}
|
||||||
|
|
||||||
|
Result update(const Input& in) {
|
||||||
|
if (!open_) return kNone;
|
||||||
|
const int n = (int)frames_.size();
|
||||||
|
Result res = kNone;
|
||||||
|
for (int k : in.keys) {
|
||||||
|
if (k == VK_ESCAPE) res = kClose;
|
||||||
|
else if (k == VK_RETURN) save("clips");
|
||||||
|
else if (k == VK_SPACE) { playing_ = !playing_; acc_ = 0; }
|
||||||
|
else if (k == VK_LEFT) { playing_ = false; ph_ = std::max(0, ph_ - 1); }
|
||||||
|
else if (k == VK_RIGHT) { playing_ = false; ph_ = std::min(n - 1, ph_ + 1); }
|
||||||
|
else if (k == VK_HOME) { playing_ = false; ph_ = in_; }
|
||||||
|
else if (k == VK_END) { playing_ = false; ph_ = out_; }
|
||||||
|
else if (k == 'I') { in_ = ph_; if (out_ < in_) out_ = n - 1; }
|
||||||
|
else if (k == 'O') { out_ = ph_; if (out_ < in_) in_ = 0; }
|
||||||
|
}
|
||||||
|
hover_ = strip_frame(in.mx, in.my);
|
||||||
|
if (hover_ >= 0) {
|
||||||
|
if (in.ldown) { in_ = hover_; if (out_ < in_) out_ = n - 1; ph_ = in_; acc_ = 0; }
|
||||||
|
if (in.rdown) { out_ = hover_; if (out_ < in_) in_ = 0; ph_ = in_; acc_ = 0; }
|
||||||
|
} else if (in.ldown) {
|
||||||
|
for (const Hit& h : hits_) {
|
||||||
|
if (in.mx < h.x || in.my < h.y || in.mx >= h.x + h.w || in.my >= h.y + h.h) continue;
|
||||||
|
if (h.id == kHitSave) save("clips");
|
||||||
|
else if (h.id == kHitDir) res = kOpenDir;
|
||||||
|
else if (h.id == kHitReturn) res = kClose;
|
||||||
|
else if (h.id == kHitScale) scale_ = (scale_ + 1) % kScales;
|
||||||
|
else if (h.id >= kHitSize && h.id < kHitSize + kSizes) size_ = h.id - kHitSize;
|
||||||
|
else if (h.id >= kHitStep && h.id < kHitStep + 3) step_ = h.id - kHitStep + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// playback of [in, out] at the recorded speed
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
acc_ += std::chrono::duration<double, std::milli>(now - last_).count();
|
||||||
|
last_ = now;
|
||||||
|
if (ph_ < in_ || ph_ > out_) { if (playing_) ph_ = in_; }
|
||||||
|
if (!playing_ || in_ == out_) acc_ = 0;
|
||||||
|
for (int guard = 0; playing_ && guard < 8; ++guard) {
|
||||||
|
const double dt = ph_ < out_ ? times_[(size_t)ph_ + 1] - times_[(size_t)ph_] : 1000.0 / 60.0;
|
||||||
|
if (acc_ < dt) break;
|
||||||
|
acc_ -= dt;
|
||||||
|
ph_ = ph_ < out_ ? ph_ + 1 : in_;
|
||||||
|
}
|
||||||
|
if (acc_ > 250) acc_ = 0;
|
||||||
|
mx_ = in.mx; my_ = in.my;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(hud::Canvas& cv) {
|
||||||
|
if (!open_) return;
|
||||||
|
cv.mark_all(); // (it dims the whole window and writes pixels directly)
|
||||||
|
using hud::rgba;
|
||||||
|
const uint32_t grey = rgba(170, 170, 170), white = rgba(255, 255, 255), yellow = rgba(255, 220, 90), dimText = rgba(120, 120, 120);
|
||||||
|
dim(cv);
|
||||||
|
hits_.clear();
|
||||||
|
const int ts = std::max(1, cv.h / 360);
|
||||||
|
const int lh = ((cv.font && cv.font->ok()) ? cv.font->line_height : 7) * ts + 3 * ts;
|
||||||
|
const int x0 = 8 * ts;
|
||||||
|
int y = 6 * ts;
|
||||||
|
auto over = [&](int x, int yy, int w, int h) { return mx_ >= x && my_ >= yy && mx_ < x + w && my_ < yy + h; };
|
||||||
|
auto item = [&](int x, int yy, const std::string& s, int id, uint32_t c, uint32_t hc) { // clickable text, returns its width
|
||||||
|
const int w = cv.text_width(s, ts);
|
||||||
|
const bool o = over(x, yy, w, lh);
|
||||||
|
cv.text(x, yy, s, o ? hc : c, ts);
|
||||||
|
hits_.push_back({x, yy, w, lh, id});
|
||||||
|
return w;
|
||||||
|
};
|
||||||
|
const int n = (int)frames_.size();
|
||||||
|
cv.text(x0, y, "REPLAY CLIP EDITOR - Right/Left click on frames to set clip start and end points.", grey, ts);
|
||||||
|
y += lh;
|
||||||
|
{
|
||||||
|
const std::string s = saving_.load() ? "Saving gif... " + std::to_string(saveDone_.load()) + "/" + std::to_string(saveTotal_.load()) : std::string("Save as Gif [ENTER]");
|
||||||
|
const int w = item(x0, y, s, kHitSave, white, yellow);
|
||||||
|
std::lock_guard<std::mutex> l(statusMu_);
|
||||||
|
if (!status_.empty()) cv.text(x0 + w + 24 * ts, y, status_, grey, ts);
|
||||||
|
}
|
||||||
|
y += lh;
|
||||||
|
item(x0, y, "Open gif directory", kHitDir, white, yellow);
|
||||||
|
y += lh;
|
||||||
|
{
|
||||||
|
int x = x0;
|
||||||
|
cv.text(x, y, "Size:", grey, ts);
|
||||||
|
x += cv.text_width("Size: ", ts) + 6 * ts;
|
||||||
|
for (int k = 0; k < kSizes; ++k) {
|
||||||
|
const std::string s = k == 0 ? std::string("Full") : std::to_string(kSizeW[k]) + "x" + std::to_string(kSizeH[k]);
|
||||||
|
x += item(x, y, s, kHitSize + k, k == size_ ? yellow : grey, k == size_ ? yellow : white) + 12 * ts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y += lh;
|
||||||
|
item(x0, y, std::string("Output scale: ") + kScaleName[scale_], kHitScale, white, yellow);
|
||||||
|
y += lh;
|
||||||
|
{
|
||||||
|
int x = x0;
|
||||||
|
cv.text(x, y, "Frame step:", grey, ts);
|
||||||
|
x += cv.text_width("Frame step: ", ts) + 6 * ts;
|
||||||
|
for (int k = 1; k <= 3; ++k) x += item(x, y, std::to_string(k), kHitStep + k - 1, k == step_ ? yellow : grey, k == step_ ? yellow : white) + 12 * ts;
|
||||||
|
}
|
||||||
|
y += lh;
|
||||||
|
{
|
||||||
|
const std::vector<clipimpl::GifPick> picks = clipimpl::gif_pick(times_, in_, out_, step_);
|
||||||
|
int cs = 0;
|
||||||
|
for (const auto& p : picks) cs += p.delay_cs;
|
||||||
|
int ow = 0, oh = 0;
|
||||||
|
output_size(ow, oh);
|
||||||
|
std::string s = "Output: " + std::to_string(picks.size()) + " frames - " + hud::fmt("%.1f", cs / 100.0) + "s - " + std::to_string(ow) + "x" + std::to_string(oh) + " px";
|
||||||
|
if (ratio_ > 0) s += " - about " + hud::fmt("%.1f", ratio_ * ow * oh * (double)picks.size() / 1048576.0) + " MB";
|
||||||
|
cv.text(x0, y, s, grey, ts);
|
||||||
|
}
|
||||||
|
y += lh;
|
||||||
|
const int shown = hover_ >= 0 ? hover_ : ph_;
|
||||||
|
cv.text(x0, y, "Left click - start clip on hovered frame. Right click - end clip on hovered frame. Frame = " + std::to_string(shown), grey, ts);
|
||||||
|
y += lh + 2 * ts;
|
||||||
|
// the strip of thumbnails
|
||||||
|
stripX_ = x0; stripY_ = y; stripW_ = cv.w - 2 * x0; stripH_ = 15 * ts;
|
||||||
|
{
|
||||||
|
const int cellsMax = std::max(1, stripW_ / std::max(1, stripH_ * 16 / 9));
|
||||||
|
const int cells = std::min(n, cellsMax);
|
||||||
|
for (int c = 0; c < cells; ++c) {
|
||||||
|
const int cx0 = stripX_ + c * stripW_ / cells, cx1 = stripX_ + (c + 1) * stripW_ / cells;
|
||||||
|
const int g0 = c * n / cells, g1 = std::max(g0 + 1, (c + 1) * n / cells);
|
||||||
|
int tf = -1;
|
||||||
|
for (int f = g0; f < g1 && tf < 0; ++f) if (ready_[(size_t)f].load()) tf = f;
|
||||||
|
const int iw = cx1 - cx0 - 2, ih = stripH_ - 2;
|
||||||
|
if (tf >= 0) {
|
||||||
|
const std::vector<uint32_t>& t = thumbs_[(size_t)tf];
|
||||||
|
for (int j = 0; j < ih; ++j) {
|
||||||
|
const int py = stripY_ + 1 + j;
|
||||||
|
if (py < 0 || py >= cv.h) continue;
|
||||||
|
const int sy = j * kThumbH / ih;
|
||||||
|
for (int i = 0; i < iw; ++i) { const int px = cx0 + 1 + i; if (px >= 0 && px < cv.w) cv.px[(size_t)py * cv.w + px] = t[(size_t)sy * kThumbW + i * kThumbW / iw]; }
|
||||||
|
}
|
||||||
|
} else cv.rect(cx0 + 1, stripY_ + 1, iw, ih, rgba(30, 30, 30));
|
||||||
|
cv.frame(cx0, stripY_, cx1 - cx0, stripH_, rgba(90, 90, 90));
|
||||||
|
}
|
||||||
|
for (int px = stripX_; px < stripX_ + stripW_; ++px) { // the frames outside the clip are darkened
|
||||||
|
const int f = (px - stripX_) * n / stripW_;
|
||||||
|
if (f >= in_ && f <= out_) continue;
|
||||||
|
for (int py = stripY_; py < stripY_ + stripH_; ++py) if (px >= 0 && px < cv.w && py >= 0 && py < cv.h) { uint32_t& d = cv.px[(size_t)py * cv.w + px]; d = (d & 0xFF000000u) | ((d >> 2) & 0x3F3F3Fu); }
|
||||||
|
}
|
||||||
|
auto mark = [&](int f, uint32_t c, int w) { const int px = stripX_ + (int)((f + 0.5) * stripW_ / n); cv.rect(px - w / 2, stripY_ - 2 * ts, w, stripH_ + 4 * ts, c); };
|
||||||
|
mark(in_, rgba(90, 220, 90), 2 * ts);
|
||||||
|
mark(out_, rgba(230, 80, 70), 2 * ts);
|
||||||
|
if (hover_ < 0) mark(ph_, white, ts); // (while the preview shows the hovered frame, only its line is drawn)
|
||||||
|
if (hover_ >= 0) mark(hover_, yellow, ts);
|
||||||
|
}
|
||||||
|
y += stripH_ + 6 * ts;
|
||||||
|
// the preview box: what the GIF shows
|
||||||
|
{
|
||||||
|
const int bottom = cv.h - lh - 6 * ts;
|
||||||
|
int bw = cv.w * 2 / 3, bh = bw * 9 / 16;
|
||||||
|
if (y + bh > bottom) { bh = std::max(16, bottom - y); bw = bh * 16 / 9; }
|
||||||
|
cv.rect(x0, y, bw, bh, rgba(12, 12, 12));
|
||||||
|
const ClipFrame* f = frame_bgra(shown);
|
||||||
|
if (f) {
|
||||||
|
int rx, ry, rw, rh;
|
||||||
|
source_rect(*f, rx, ry, rw, rh);
|
||||||
|
const double s = std::min((double)(bw - 2) / rw, (double)(bh - 2) / rh);
|
||||||
|
const int dw = std::max(1, (int)(rw * s)), dh = std::max(1, (int)(rh * s));
|
||||||
|
const int dx = x0 + (bw - dw) / 2, dy = y + (bh - dh) / 2;
|
||||||
|
const uint32_t* src = reinterpret_cast<const uint32_t*>(cache_.data());
|
||||||
|
xmap_.resize((size_t)dw);
|
||||||
|
for (int i = 0; i < dw; ++i) xmap_[(size_t)i] = rx + std::min(rw - 1, (int)(i / s));
|
||||||
|
for (int j = 0; j < dh; ++j) {
|
||||||
|
const int py = dy + j;
|
||||||
|
if (py < 0 || py >= cv.h) continue;
|
||||||
|
const uint32_t* row = src + (size_t)(ry + std::min(rh - 1, (int)(j / s))) * f->w;
|
||||||
|
uint32_t* dst = cv.px + (size_t)py * cv.w;
|
||||||
|
for (int i = 0; i < dw; ++i) { const int px = dx + i; if (px >= 0 && px < cv.w) dst[px] = 0xFF000000u | (row[xmap_[(size_t)i]] & 0xFFFFFFu); } // (BGRA in memory is ARGB as a number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cv.frame(x0 - 1, y - 1, bw + 2, bh + 2, rgba(200, 200, 200));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const int w = item(x0, cv.h - lh - 2 * ts, "Return to game [ESC]", kHitReturn, white, yellow);
|
||||||
|
const std::string keys = std::string(playing_ ? "Space pause" : "Space play") + " Left/Right step I/O start/end at this frame Home/End go to start/end";
|
||||||
|
cv.text(x0 + w + 30 * ts, cv.h - lh - 2 * ts, keys, dimText, ts);
|
||||||
|
}
|
||||||
|
if (mx_ >= 0) { // the cursor (the HUD's crosshair is under the editor)
|
||||||
|
const uint32_t c = rgba(255, 255, 255, 230);
|
||||||
|
cv.rect(mx_ - 8, my_ - 1, 6, 2, c); cv.rect(mx_ + 3, my_ - 1, 6, 2, c);
|
||||||
|
cv.rect(mx_ - 1, my_ - 8, 2, 6, c); cv.rect(mx_ - 1, my_ + 3, 2, 6, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int kSizes = 6, kScales = 4, kThumbW = 64, kThumbH = 36;
|
||||||
|
static constexpr int kSizeW[kSizes] = {0, 240, 320, 320, 480, 480}, kSizeH[kSizes] = {0, 240, 180, 320, 270, 320}; // world pixels (cells)
|
||||||
|
static constexpr double kScalePct[kScales] = {1.0, 0.5, 1.0 / 3.0, 0.25};
|
||||||
|
static constexpr const char* kScaleName[kScales] = {"100%", "50%", "33%", "25%"};
|
||||||
|
enum { kHitSave = 1, kHitDir, kHitReturn, kHitScale, kHitSize = 10, kHitStep = 20 };
|
||||||
|
struct Hit { int x, y, w, h, id; };
|
||||||
|
|
||||||
|
// The part of a recorded frame that goes into the GIF: the whole frame, or the chosen box of world pixels centred on the player (inside the frame).
|
||||||
|
static void source_rect(const ClipFrame& f, int size, int& x, int& y, int& w, int& h) {
|
||||||
|
if (size <= 0) { x = 0; y = 0; w = f.w; h = f.h; return; }
|
||||||
|
const float s = f.meta.scale > 0 ? f.meta.scale : 1.0f;
|
||||||
|
w = std::clamp((int)std::lround(kSizeW[size] * s), 1, f.w);
|
||||||
|
h = std::clamp((int)std::lround(kSizeH[size] * s), 1, f.h);
|
||||||
|
x = std::clamp((int)std::lround(f.meta.psx - w / 2.0), 0, f.w - w);
|
||||||
|
y = std::clamp((int)std::lround(f.meta.psy - h / 2.0), 0, f.h - h);
|
||||||
|
}
|
||||||
|
void source_rect(const ClipFrame& f, int& x, int& y, int& w, int& h) const { source_rect(f, size_, x, y, w, h); }
|
||||||
|
void output_size(int& ow, int& oh) const {
|
||||||
|
int x, y, w = 1, h = 1;
|
||||||
|
if (!frames_.empty()) source_rect(*frames_[(size_t)in_], x, y, w, h);
|
||||||
|
ow = std::max(1, (int)std::lround(w * kScalePct[scale_]));
|
||||||
|
oh = std::max(1, (int)std::lround(h * kScalePct[scale_]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The frame under the strip at (mx, my), -1 if none.
|
||||||
|
int strip_frame(int mx, int my) const {
|
||||||
|
if (stripW_ <= 0 || frames_.empty() || mx < stripX_ || mx >= stripX_ + stripW_ || my < stripY_ || my >= stripY_ + stripH_) return -1;
|
||||||
|
return std::clamp((mx - stripX_) * (int)frames_.size() / stripW_, 0, (int)frames_.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClipFrame* frame_bgra(int i) {
|
||||||
|
if (i < 0 || i >= (int)frames_.size()) return nullptr;
|
||||||
|
if (cacheIdx_ != i) { if (!clipimpl::decode_bgra(*frames_[(size_t)i], cache_)) return nullptr; cacheIdx_ = i; }
|
||||||
|
return frames_[(size_t)i].get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Darkens what is under the editor (the overlay blends over the frozen game picture).
|
||||||
|
static void dim(hud::Canvas& cv) {
|
||||||
|
const uint32_t a = 160;
|
||||||
|
for (size_t i = 0, n = (size_t)cv.w * cv.h; i < n; ++i) {
|
||||||
|
uint32_t& d = cv.px[i];
|
||||||
|
const uint32_t da = d >> 24;
|
||||||
|
if (da == 0) { d = a << 24; continue; }
|
||||||
|
const uint32_t oa = a + da * (255 - a) / 255;
|
||||||
|
auto ch = [&](int sh) { return ((d >> sh & 255) * da * (255 - a) / 255) / oa; };
|
||||||
|
d = oa << 24 | ch(16) << 16 | ch(8) << 8 | ch(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnails for the strip, coarse first (every 64th frame, then the ones between) so that the strip fills quickly.
|
||||||
|
void make_thumbs() {
|
||||||
|
const int n = (int)frames_.size();
|
||||||
|
std::vector<uint8_t> bgra, rgb;
|
||||||
|
for (int stride = 64; stride >= 1; stride /= 2)
|
||||||
|
for (int i = 0; i < n; i += stride) {
|
||||||
|
if (thumbStop_.load()) return;
|
||||||
|
if (ready_[(size_t)i].load()) continue;
|
||||||
|
const ClipFrame& f = *frames_[(size_t)i];
|
||||||
|
if (!clipimpl::decode_bgra(f, bgra)) continue;
|
||||||
|
clipimpl::crop_scale_rgb(bgra.data(), f.w, 0, 0, f.w, f.h, kThumbW, kThumbH, rgb);
|
||||||
|
std::vector<uint32_t> t((size_t)kThumbW * kThumbH);
|
||||||
|
for (size_t k = 0; k < t.size(); ++k) t[k] = 0xFF000000u | (uint32_t)rgb[k * 3] << 16 | (uint32_t)rgb[k * 3 + 1] << 8 | rgb[k * 3 + 2];
|
||||||
|
thumbs_[(size_t)i] = std::move(t);
|
||||||
|
ready_[(size_t)i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void stop_thumbs() { thumbStop_ = true; if (thumbT_.joinable()) thumbT_.join(); }
|
||||||
|
|
||||||
|
void write_gif(const std::vector<std::shared_ptr<const ClipFrame>>& sel, const std::vector<clipimpl::GifPick>& picks, int ow, int oh, int size, const std::string& path) {
|
||||||
|
std::vector<uint8_t> bgra, rgb;
|
||||||
|
auto out_rgb = [&](const ClipFrame& f, std::vector<uint8_t>& dst) {
|
||||||
|
if (!clipimpl::decode_bgra(f, bgra)) { dst.assign((size_t)ow * oh * 3, 0); return; }
|
||||||
|
int x, y, w, h;
|
||||||
|
source_rect(f, size, x, y, w, h);
|
||||||
|
clipimpl::crop_scale_rgb(bgra.data(), f.w, x, y, w, h, ow, oh, dst);
|
||||||
|
};
|
||||||
|
// every frame has its own palette (a local colour table, clipimpl::make_palette_frame) and a light ordered dither; the test switch globalPal_ makes the
|
||||||
|
// old single palette of 8 frames spread over the clip instead (for comparisons)
|
||||||
|
clipimpl::Palette global;
|
||||||
|
if (globalPal_) {
|
||||||
|
std::vector<std::vector<uint8_t>> sample;
|
||||||
|
for (size_t k = 0; k < sel.size(); k += std::max<size_t>(1, sel.size() / 8)) { sample.emplace_back(); out_rgb(*sel[k], sample.back()); }
|
||||||
|
std::vector<const uint8_t*> ptrs;
|
||||||
|
for (const auto& s : sample) ptrs.push_back(s.data());
|
||||||
|
global = clipimpl::make_palette(ptrs, ow, oh);
|
||||||
|
}
|
||||||
|
clipimpl::GifWriter gw;
|
||||||
|
size_t bytes = 0;
|
||||||
|
if (gw.open(path, ow, oh, globalPal_ ? &global : nullptr)) {
|
||||||
|
for (size_t k = 0; k < sel.size(); ++k) {
|
||||||
|
out_rgb(*sel[k], rgb);
|
||||||
|
if (globalPal_) gw.add(rgb.data(), picks[k].delay_cs);
|
||||||
|
else { const clipimpl::Palette pal = clipimpl::make_palette_frame(rgb.data(), (size_t)ow * oh); gw.add(rgb.data(), picks[k].delay_cs, &pal, dither_); }
|
||||||
|
saveDone_ = (int)k + 1;
|
||||||
|
}
|
||||||
|
bytes = gw.close();
|
||||||
|
if (bytes) { // the signed metadata (NGPUMETA)
|
||||||
|
GifMeta m = meta_;
|
||||||
|
m.frames = (int)sel.size(); m.width = ow; m.height = oh; m.duration_ms = 0;
|
||||||
|
for (const auto& p : picks) m.duration_ms += p.delay_cs * 10;
|
||||||
|
if (gif_embed_meta(path, m)) { std::error_code ec2; bytes = (size_t)std::filesystem::file_size(path, ec2); }
|
||||||
|
else std::printf("gif metadata: could not write %s%c", path.c_str(), 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string msg;
|
||||||
|
if (bytes) {
|
||||||
|
ratio_ = (double)bytes / ((double)ow * oh * sel.size());
|
||||||
|
msg = "saved " + path + " (" + hud::fmt("%.2f", bytes / 1048576.0) + " MB)";
|
||||||
|
std::printf("gif written: %s (%zu frames, %dx%d, %zu bytes)%c", path.c_str(), sel.size(), ow, oh, bytes, 10);
|
||||||
|
} else {
|
||||||
|
msg = "could not write " + path;
|
||||||
|
std::printf("%s%c", msg.c_str(), 10);
|
||||||
|
}
|
||||||
|
{ std::lock_guard<std::mutex> l(statusMu_); status_ = msg; }
|
||||||
|
saving_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool open_ = false;
|
||||||
|
GifMeta meta_;
|
||||||
|
std::vector<std::shared_ptr<const ClipFrame>> frames_;
|
||||||
|
std::vector<double> times_;
|
||||||
|
int in_ = 0, out_ = 0, ph_ = 0, hover_ = -1, mx_ = -1, my_ = -1;
|
||||||
|
int size_ = 0, scale_ = 1, step_ = 1;
|
||||||
|
bool globalPal_ = false;
|
||||||
|
int dither_ = kDither;
|
||||||
|
bool playing_ = true;
|
||||||
|
double acc_ = 0;
|
||||||
|
std::chrono::steady_clock::time_point last_;
|
||||||
|
std::vector<uint8_t> cache_;
|
||||||
|
int cacheIdx_ = -1;
|
||||||
|
std::vector<int> xmap_;
|
||||||
|
std::vector<Hit> hits_;
|
||||||
|
int stripX_ = 0, stripY_ = 0, stripW_ = 0, stripH_ = 0;
|
||||||
|
std::vector<std::vector<uint32_t>> thumbs_;
|
||||||
|
std::unique_ptr<std::atomic<bool>[]> ready_;
|
||||||
|
std::thread thumbT_;
|
||||||
|
std::atomic<bool> thumbStop_{false};
|
||||||
|
std::thread saver_;
|
||||||
|
std::atomic<bool> saving_{false};
|
||||||
|
std::atomic<int> saveDone_{0}, saveTotal_{0};
|
||||||
|
std::atomic<double> ratio_{0};
|
||||||
|
std::mutex statusMu_;
|
||||||
|
std::string status_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#pragma once
|
||||||
|
// The metadata of the GIFs of the clip editor (docs/GIF_METADATA.md): an application extension "NGPUMETA" "1.0" with a JSON object (the build, the seed,
|
||||||
|
// the player's wands) and its signature, HMAC-SHA256 over the canonical JSON and the SHA-256 of the GIF without that block. The upload site checks it.
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
// The shared secret of the signature (the site's NGPU_GIF_KEY). It is inside the executable: the signature shows that a file came out of noita_gpu
|
||||||
|
// unchanged, not who made it.
|
||||||
|
inline const char* kGifMetaKey = "aDILI6RDUCzKjdX-ns7qHHddiyWH5iW0Fq42ktl7fPvS-0YL";
|
||||||
|
|
||||||
|
namespace gifmeta {
|
||||||
|
|
||||||
|
struct Sha256 {
|
||||||
|
uint32_t h[8] = {0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au, 0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u};
|
||||||
|
uint8_t buf[64] = {};
|
||||||
|
size_t n = 0;
|
||||||
|
uint64_t total = 0;
|
||||||
|
static uint32_t rotr(uint32_t x, int k) { return (x >> k) | (x << (32 - k)); }
|
||||||
|
void block(const uint8_t* p) {
|
||||||
|
static const uint32_t K[64] = {
|
||||||
|
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u,
|
||||||
|
0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
|
||||||
|
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u,
|
||||||
|
0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
|
||||||
|
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u,
|
||||||
|
0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u};
|
||||||
|
uint32_t w[64];
|
||||||
|
for (int i = 0; i < 16; ++i) w[i] = (uint32_t)p[i * 4] << 24 | (uint32_t)p[i * 4 + 1] << 16 | (uint32_t)p[i * 4 + 2] << 8 | p[i * 4 + 3];
|
||||||
|
for (int i = 16; i < 64; ++i) {
|
||||||
|
const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3), s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||||
|
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||||||
|
}
|
||||||
|
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
const uint32_t t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
|
||||||
|
const uint32_t t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
|
||||||
|
hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
|
||||||
|
}
|
||||||
|
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
|
||||||
|
}
|
||||||
|
void update(const uint8_t* p, size_t len) {
|
||||||
|
total += len;
|
||||||
|
while (len > 0) {
|
||||||
|
const size_t k = std::min(len, 64 - n);
|
||||||
|
std::memcpy(buf + n, p, k); n += k; p += k; len -= k;
|
||||||
|
if (n == 64) { block(buf); n = 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void update(const std::string& s) { update((const uint8_t*)s.data(), s.size()); }
|
||||||
|
std::array<uint8_t, 32> finish() {
|
||||||
|
const uint64_t bits = total * 8;
|
||||||
|
const uint8_t one = 0x80, zero = 0;
|
||||||
|
update(&one, 1);
|
||||||
|
while (n != 56) update(&zero, 1);
|
||||||
|
uint8_t len[8];
|
||||||
|
for (int i = 0; i < 8; ++i) len[i] = (uint8_t)(bits >> (56 - 8 * i));
|
||||||
|
update(len, 8);
|
||||||
|
std::array<uint8_t, 32> out{};
|
||||||
|
for (int i = 0; i < 8; ++i) for (int k = 0; k < 4; ++k) out[i * 4 + k] = (uint8_t)(h[i] >> (24 - 8 * k));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::string hex(const uint8_t* p, size_t n) { static const char* d = "0123456789abcdef"; std::string s; for (size_t i = 0; i < n; ++i) { s += d[p[i] >> 4]; s += d[p[i] & 15]; } return s; }
|
||||||
|
|
||||||
|
inline std::string hmac_sha256_hex(const std::string& key, const std::string& msg) {
|
||||||
|
uint8_t k[64] = {};
|
||||||
|
if (key.size() > 64) { Sha256 s; s.update(key); const auto d = s.finish(); std::memcpy(k, d.data(), 32); } else std::memcpy(k, key.data(), key.size());
|
||||||
|
uint8_t ip[64], op[64];
|
||||||
|
for (int i = 0; i < 64; ++i) { ip[i] = k[i] ^ 0x36; op[i] = k[i] ^ 0x5c; }
|
||||||
|
Sha256 in; in.update(ip, 64); in.update(msg); const auto ih = in.finish();
|
||||||
|
Sha256 out; out.update(op, 64); out.update(ih.data(), 32); const auto oh = out.finish();
|
||||||
|
return hex(oh.data(), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON as Python's json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=True) writes it (ASCII strings, numbers that round-trip)
|
||||||
|
inline std::string jstr(const std::string& s) {
|
||||||
|
std::string o = "\"";
|
||||||
|
for (unsigned char c : s) {
|
||||||
|
if (c == '"') o += "\\\"";
|
||||||
|
else if (c == '\\') o += "\\\\";
|
||||||
|
else if (c == '\n') o += "\\n";
|
||||||
|
else if (c == '\t') o += "\\t";
|
||||||
|
else if (c == '\r') o += "\\r";
|
||||||
|
else if (c == '\b') o += "\\b";
|
||||||
|
else if (c == '\f') o += "\\f";
|
||||||
|
else if (c < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; }
|
||||||
|
else if (c >= 0x80) o += '?'; // (ASCII only: a byte of a multi-byte character becomes '?')
|
||||||
|
else o += (char)c;
|
||||||
|
}
|
||||||
|
return o + "\"";
|
||||||
|
}
|
||||||
|
inline std::string jnum(double v) { // integers without a point, other numbers with up to 6 decimals and at least one (Python's repr of the parsed value)
|
||||||
|
if (v == (double)(long long)v && v > -1e15 && v < 1e15) return std::to_string((long long)v);
|
||||||
|
char b[64];
|
||||||
|
std::snprintf(b, sizeof b, "%.6f", v);
|
||||||
|
std::string s = b;
|
||||||
|
while (s.size() > 1 && s.back() == '0' && s[s.size() - 2] != '.') s.pop_back();
|
||||||
|
if (s == "-0.0") s = "0.0";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
inline std::string jbool(bool b) { return b ? "true" : "false"; }
|
||||||
|
inline std::string jlist(const std::vector<std::string>& v) { std::string o = "["; for (size_t i = 0; i < v.size(); ++i) { if (i) o += ","; o += jstr(v[i]); } return o + "]"; }
|
||||||
|
|
||||||
|
} // namespace gifmeta
|
||||||
|
|
||||||
|
struct GifMetaWand {
|
||||||
|
int slot = 0;
|
||||||
|
std::string name;
|
||||||
|
bool shuffle = false;
|
||||||
|
int spells_per_cast = 1, cast_delay = 0, recharge_time = 0, capacity = 0;
|
||||||
|
double mana_max = 0, mana_charge_speed = 0, spread_degrees = 0, speed_multiplier = 1;
|
||||||
|
std::vector<std::string> always_cast, deck;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GifMeta {
|
||||||
|
std::string build = "dev", created, mode = "world";
|
||||||
|
long long seed = 0;
|
||||||
|
int frames = 0, width = 0, height = 0, duration_ms = 0, active_wand = -1;
|
||||||
|
std::vector<GifMetaWand> wands;
|
||||||
|
|
||||||
|
std::string canonical() const { // the object without "sig", keys sorted
|
||||||
|
using namespace gifmeta;
|
||||||
|
std::string w = "[";
|
||||||
|
for (size_t i = 0; i < wands.size(); ++i) {
|
||||||
|
const GifMetaWand& a = wands[i];
|
||||||
|
if (i) w += ",";
|
||||||
|
w += "{\"always_cast\":" + jlist(a.always_cast) + ",\"capacity\":" + jnum(a.capacity) + ",\"cast_delay\":" + jnum(a.cast_delay) + ",\"deck\":" + jlist(a.deck) +
|
||||||
|
",\"mana_charge_speed\":" + jnum(a.mana_charge_speed) + ",\"mana_max\":" + jnum(a.mana_max) + ",\"name\":" + jstr(a.name) + ",\"recharge_time\":" + jnum(a.recharge_time) +
|
||||||
|
",\"shuffle\":" + jbool(a.shuffle) + ",\"slot\":" + jnum(a.slot) + ",\"speed_multiplier\":" + jnum(a.speed_multiplier) + ",\"spells_per_cast\":" + jnum(a.spells_per_cast) +
|
||||||
|
",\"spread_degrees\":" + jnum(a.spread_degrees) + "}";
|
||||||
|
}
|
||||||
|
w += "]";
|
||||||
|
return "{\"active_wand\":" + jnum(active_wand) + ",\"build\":" + jstr(build) + ",\"created\":" + jstr(created) + ",\"duration_ms\":" + jnum(duration_ms) +
|
||||||
|
",\"frames\":" + jnum(frames) + ",\"generator\":\"noita_gpu\",\"height\":" + jnum(height) + ",\"mode\":" + jstr(mode) + ",\"seed\":" + jnum((double)seed) +
|
||||||
|
",\"v\":1,\"wands\":" + w + ",\"width\":" + jnum(width) + "}";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Signs the finished GIF at `path` and inserts the NGPUMETA block after its logical screen descriptor and global colour table. False on an I/O error.
|
||||||
|
inline bool gif_embed_meta(const std::string& path, GifMeta meta) {
|
||||||
|
std::vector<uint8_t> f;
|
||||||
|
{
|
||||||
|
FILE* fp = nullptr;
|
||||||
|
if (fopen_s(&fp, path.c_str(), "rb") != 0 || !fp) return false;
|
||||||
|
std::fseek(fp, 0, SEEK_END); const long n = std::ftell(fp); std::fseek(fp, 0, SEEK_SET);
|
||||||
|
f.resize(n > 0 ? (size_t)n : 0);
|
||||||
|
const bool ok = n > 0 && std::fread(f.data(), 1, f.size(), fp) == f.size();
|
||||||
|
std::fclose(fp);
|
||||||
|
if (!ok || f.size() < 13) return false;
|
||||||
|
}
|
||||||
|
if (meta.created.empty()) {
|
||||||
|
const std::time_t t = std::time(nullptr);
|
||||||
|
std::tm g{};
|
||||||
|
gmtime_s(&g, &t);
|
||||||
|
char b[32];
|
||||||
|
std::snprintf(b, sizeof b, "%04d-%02d-%02dT%02d:%02d:%02dZ", g.tm_year + 1900, g.tm_mon + 1, g.tm_mday, g.tm_hour, g.tm_min, g.tm_sec);
|
||||||
|
meta.created = b;
|
||||||
|
}
|
||||||
|
gifmeta::Sha256 fh;
|
||||||
|
fh.update(f.data(), f.size());
|
||||||
|
const auto d = fh.finish();
|
||||||
|
const std::string canon = meta.canonical();
|
||||||
|
const std::string sig = gifmeta::hmac_sha256_hex(kGifMetaKey, canon + "\n" + gifmeta::hex(d.data(), 32));
|
||||||
|
const std::string json = canon.substr(0, canon.size() - 1) + ",\"sig\":\"" + sig + "\"}";
|
||||||
|
std::vector<uint8_t> blk = {0x21, 0xFF, 0x0B, 'N', 'G', 'P', 'U', 'M', 'E', 'T', 'A', '1', '.', '0'};
|
||||||
|
for (size_t p = 0; p < json.size(); p += 255) {
|
||||||
|
const size_t c = std::min<size_t>(255, json.size() - p);
|
||||||
|
blk.push_back((uint8_t)c);
|
||||||
|
blk.insert(blk.end(), json.begin() + (std::ptrdiff_t)p, json.begin() + (std::ptrdiff_t)(p + c));
|
||||||
|
}
|
||||||
|
blk.push_back(0);
|
||||||
|
size_t at = 13;
|
||||||
|
if (f[10] & 0x80) at += 3u * (2u << (f[10] & 7));
|
||||||
|
if (at > f.size()) return false;
|
||||||
|
f.insert(f.begin() + (std::ptrdiff_t)at, blk.begin(), blk.end());
|
||||||
|
FILE* fp = nullptr;
|
||||||
|
if (fopen_s(&fp, path.c_str(), "wb") != 0 || !fp) return false;
|
||||||
|
const bool ok = std::fwrite(f.data(), 1, f.size(), fp) == f.size();
|
||||||
|
std::fclose(fp);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
+652
@@ -0,0 +1,652 @@
|
|||||||
|
#pragma once
|
||||||
|
// Heads-up display drawn on the CPU into an ARGB overlay (1280x720, output pixels) that render.comp blends over the frame.
|
||||||
|
// Text uses an embedded 5x7 bitmap font; icons and bar colours are the game's own HUD images from data.wak.
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/png.h"
|
||||||
|
#include "core/wak.h"
|
||||||
|
#include "world/wang_gen.h" // Image, decode_png
|
||||||
|
|
||||||
|
namespace hud {
|
||||||
|
|
||||||
|
// 5x7 font, ASCII 32..126, one byte per column (bit 0 = top row).
|
||||||
|
inline const uint8_t kFont[95][5] = {
|
||||||
|
{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14},
|
||||||
|
{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x56,0x20,0x50},{0x00,0x08,0x07,0x03,0x00},
|
||||||
|
{0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x2A,0x1C,0x7F,0x1C,0x2A},{0x08,0x08,0x3E,0x08,0x08},
|
||||||
|
{0x00,0x80,0x70,0x30,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x00,0x60,0x60,0x00},{0x20,0x10,0x08,0x04,0x02},
|
||||||
|
{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x72,0x49,0x49,0x49,0x46},{0x21,0x41,0x49,0x4D,0x33},
|
||||||
|
{0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x31},{0x41,0x21,0x11,0x09,0x07},
|
||||||
|
{0x36,0x49,0x49,0x49,0x36},{0x46,0x49,0x49,0x29,0x1E},{0x00,0x00,0x14,0x00,0x00},{0x00,0x40,0x34,0x00,0x00},
|
||||||
|
{0x00,0x08,0x14,0x22,0x41},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x59,0x09,0x06},
|
||||||
|
{0x3E,0x41,0x5D,0x59,0x4E},{0x7C,0x12,0x11,0x12,0x7C},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
|
||||||
|
{0x7F,0x41,0x41,0x41,0x3E},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},{0x3E,0x41,0x41,0x51,0x73},
|
||||||
|
{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},
|
||||||
|
{0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x1C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
|
||||||
|
{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x26,0x49,0x49,0x49,0x32},
|
||||||
|
{0x03,0x01,0x7F,0x01,0x03},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},
|
||||||
|
{0x63,0x14,0x08,0x14,0x63},{0x03,0x04,0x78,0x04,0x03},{0x61,0x59,0x49,0x4D,0x43},{0x00,0x7F,0x41,0x41,0x41},
|
||||||
|
{0x02,0x04,0x08,0x10,0x20},{0x41,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40},
|
||||||
|
{0x00,0x03,0x07,0x08,0x00},{0x20,0x54,0x54,0x78,0x40},{0x7F,0x28,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x28},
|
||||||
|
{0x38,0x44,0x44,0x28,0x7F},{0x38,0x54,0x54,0x54,0x18},{0x00,0x08,0x7E,0x09,0x02},{0x18,0xA4,0xA4,0x9C,0x78},
|
||||||
|
{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x40,0x3D,0x00},{0x7F,0x10,0x28,0x44,0x00},
|
||||||
|
{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x78,0x04,0x78},{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},
|
||||||
|
{0xFC,0x18,0x24,0x24,0x18},{0x18,0x24,0x24,0x18,0xFC},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x24},
|
||||||
|
{0x04,0x04,0x3F,0x44,0x24},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},{0x3C,0x40,0x30,0x40,0x3C},
|
||||||
|
{0x44,0x28,0x10,0x28,0x44},{0x4C,0x90,0x90,0x90,0x7C},{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},
|
||||||
|
{0x00,0x00,0x77,0x00,0x00},{0x00,0x41,0x36,0x08,0x00},{0x02,0x01,0x02,0x04,0x02},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Icon {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
std::vector<uint32_t> px; // ARGB
|
||||||
|
bool ok() const { return w > 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
inline Icon load_icon(const ng::Wak& wak, const std::string& path) {
|
||||||
|
Icon ic;
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
ng::Image im;
|
||||||
|
if (!wak.read(path, raw) || !ng::decode_png(raw.data(), raw.size(), im)) return ic;
|
||||||
|
ic.w = im.width; ic.h = im.height;
|
||||||
|
ic.px.resize((size_t)ic.w * ic.h);
|
||||||
|
for (size_t i = 0; i < ic.px.size(); ++i) {
|
||||||
|
const uint8_t* p = &im.rgba[i * 4];
|
||||||
|
ic.px[i] = (uint32_t)p[3] << 24 | (uint32_t)p[0] << 16 | (uint32_t)p[1] << 8 | p[2];
|
||||||
|
}
|
||||||
|
return ic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The game's bitmap fonts (data/fonts/*.xml: FontData with a texture and a QuadChar per character): the UI and the text sprites of the world (shop prices) use font_pixel_white.
|
||||||
|
struct GameFont {
|
||||||
|
struct Glyph { int rx = 0, ry = 0, rw = 0, rh = 0, ox = 0, oy = 0, width = 0; bool ok = false; };
|
||||||
|
Icon tex;
|
||||||
|
Glyph g[256];
|
||||||
|
int line_height = 7, char_space = 0, word_space = 6;
|
||||||
|
bool ok() const { return tex.ok(); }
|
||||||
|
};
|
||||||
|
inline GameFont load_font(const ng::Wak& wak, const std::string& xml_path) {
|
||||||
|
GameFont f;
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (!wak.read(xml_path, raw)) return f;
|
||||||
|
const std::string s(raw.begin(), raw.end());
|
||||||
|
auto tagText = [&](const char* tag) { const std::string o = std::string("<") + tag + ">", c = std::string("</") + tag + ">"; const size_t a = s.find(o), b = s.find(c); if (a == std::string::npos || b == std::string::npos) return std::string(); std::string t = s.substr(a + o.size(), b - a - o.size()); const size_t i0 = t.find_first_not_of(" \t\r\n"), i1 = t.find_last_not_of(" \t\r\n"); return i0 == std::string::npos ? std::string() : t.substr(i0, i1 - i0 + 1); };
|
||||||
|
const std::string texPath = tagText("Texture");
|
||||||
|
if (!tagText("LineHeight").empty()) f.line_height = std::atoi(tagText("LineHeight").c_str());
|
||||||
|
if (!tagText("CharSpace").empty()) f.char_space = std::atoi(tagText("CharSpace").c_str());
|
||||||
|
if (!tagText("WordSpace").empty()) f.word_space = std::atoi(tagText("WordSpace").c_str());
|
||||||
|
for (size_t pos = s.find("<QuadChar"); pos != std::string::npos; pos = s.find("<QuadChar", pos + 1)) {
|
||||||
|
const size_t end = s.find('>', pos);
|
||||||
|
const std::string q = s.substr(pos, end - pos);
|
||||||
|
auto at = [&](const char* k) { const std::string key = std::string(" ") + k + "=\""; const size_t a = q.find(key); return a == std::string::npos ? 0 : std::atoi(q.c_str() + a + key.size()); };
|
||||||
|
const int id = at("id");
|
||||||
|
if (id < 0 || id > 255) continue;
|
||||||
|
GameFont::Glyph& gl = f.g[id];
|
||||||
|
gl.rx = at("rect_x"); gl.ry = at("rect_y"); gl.rw = at("rect_w"); gl.rh = at("rect_h"); gl.ox = at("offset_x"); gl.oy = at("offset_y"); gl.width = at("width"); gl.ok = true;
|
||||||
|
}
|
||||||
|
if (!texPath.empty()) f.tex = load_icon(wak, texPath);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t rgba(int r, int g, int b, int a = 255) { return (uint32_t)a << 24 | (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b; }
|
||||||
|
|
||||||
|
// Draws into a caller-owned ARGB buffer (premultiplication is not used: the shader does a plain alpha mix).
|
||||||
|
struct Canvas {
|
||||||
|
uint32_t* px = nullptr;
|
||||||
|
int w = 0, h = 0;
|
||||||
|
const GameFont* font = nullptr; // the game's font when it is loaded (else the embedded 5x7)
|
||||||
|
|
||||||
|
// The 32 x 32 tiles drawn into since the last clear: clear() zeroes only those, main.cpp uploads only those and render.comp reads the overlay only
|
||||||
|
// there (the HUD covers a few percent of the window; the whole 14 MB buffer was cleared, copied to mapped memory and read over PCIe every frame).
|
||||||
|
std::vector<uint8_t> tiles;
|
||||||
|
int tw = 0, th = 0;
|
||||||
|
void mark(int x, int y) { tiles[(size_t)(y >> 5) * tw + (x >> 5)] = 1; }
|
||||||
|
void mark_all() { std::fill(tiles.begin(), tiles.end(), (uint8_t)1); } // (a drawer that writes px directly over the whole window: the replay editor)
|
||||||
|
void clear() {
|
||||||
|
const int ntw = (w + 31) >> 5, nth = (h + 31) >> 5;
|
||||||
|
if (ntw != tw || nth != th) { tw = ntw; th = nth; tiles.assign((size_t)tw * th, 0); std::memset(px, 0, (size_t)w * h * 4); return; } // (a new size: a new row stride)
|
||||||
|
for (int ty = 0; ty < th; ++ty) for (int tx = 0; tx < tw; ++tx) {
|
||||||
|
uint8_t& m = tiles[(size_t)ty * tw + tx];
|
||||||
|
if (!m) continue;
|
||||||
|
m = 0;
|
||||||
|
const int x0 = tx * 32, n = std::min(32, w - x0);
|
||||||
|
for (int y = ty * 32; y < std::min(h, ty * 32 + 32); ++y) std::memset(px + (size_t)y * w + x0, 0, (size_t)n * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(int x, int y, uint32_t c) {
|
||||||
|
if (x < 0 || y < 0 || x >= w || y >= h) return;
|
||||||
|
const uint32_t a = c >> 24;
|
||||||
|
if (a == 0) return;
|
||||||
|
mark(x, y);
|
||||||
|
uint32_t& d = px[(size_t)y * w + x];
|
||||||
|
if (a == 255 || (d >> 24) == 0) { d = c; return; }
|
||||||
|
// blend over an existing overlay pixel (keep the larger alpha)
|
||||||
|
const uint32_t da = d >> 24;
|
||||||
|
const uint32_t oa = a + da * (255 - a) / 255;
|
||||||
|
auto mix = [&](int sh) { return ((c >> sh & 255) * a + (d >> sh & 255) * da * (255 - a) / 255) / std::max<uint32_t>(oa, 1); };
|
||||||
|
d = oa << 24 | mix(16) << 16 | mix(8) << 8 | mix(0);
|
||||||
|
}
|
||||||
|
void rect(int x, int y, int rw, int rh, uint32_t c) {
|
||||||
|
for (int j = 0; j < rh; ++j) for (int i = 0; i < rw; ++i) put(x + i, y + j, c);
|
||||||
|
}
|
||||||
|
void frame(int x, int y, int rw, int rh, uint32_t c) {
|
||||||
|
rect(x, y, rw, 1, c); rect(x, y + rh - 1, rw, 1, c); rect(x, y, 1, rh, c); rect(x + rw - 1, y, 1, rh, c);
|
||||||
|
}
|
||||||
|
int text_width(const std::string& s, int scale) const {
|
||||||
|
if (font && font->ok()) { int w = 0; for (unsigned char ch : s) { const GameFont::Glyph& gl = font->g[ch]; w += (gl.ok ? gl.width : font->word_space) + font->char_space; } return w * scale; }
|
||||||
|
return (int)s.size() * 6 * scale - scale;
|
||||||
|
}
|
||||||
|
void text(int x, int y, const std::string& s, uint32_t c, int scale = 2, bool shadow = true) {
|
||||||
|
if (font && font->ok()) { // the game's font: its texture holds the glyph (white) and its shadow (black): the white pixels take the colour, the black ones stay black
|
||||||
|
const Icon& t = font->tex;
|
||||||
|
const uint32_t shade = rgba(0, 0, 0, (int)std::min<uint32_t>(220u, c >> 24)); // (the shadow fades with the text; without `shadow` it is left out)
|
||||||
|
for (int pass = 1; pass < 2; ++pass) {
|
||||||
|
const int off = 0;
|
||||||
|
int cx = x + off;
|
||||||
|
for (unsigned char ch : s) {
|
||||||
|
const GameFont::Glyph& gl = font->g[ch];
|
||||||
|
if (gl.ok) for (int j = 0; j < gl.rh; ++j) for (int i = 0; i < gl.rw; ++i) {
|
||||||
|
const int sx = gl.rx + i, sy = gl.ry + j;
|
||||||
|
if (sx < 0 || sy < 0 || sx >= t.w || sy >= t.h) continue;
|
||||||
|
const uint32_t tp = t.px[(size_t)sy * t.w + sx];
|
||||||
|
if ((tp >> 24) < 128) continue;
|
||||||
|
const bool dark = (tp & 0xFFFFFFu) < 0x404040u;
|
||||||
|
if (dark && !shadow) continue;
|
||||||
|
rect(cx + (gl.ox + i) * scale, y + off + (gl.oy + j) * scale, scale, scale, dark ? shade : c);
|
||||||
|
}
|
||||||
|
cx += ((gl.ok ? gl.width : font->word_space) + font->char_space) * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int pass = shadow ? 0 : 1; pass < 2; ++pass) {
|
||||||
|
const uint32_t col = pass == 0 ? rgba(0, 0, 0, (int)std::min<uint32_t>(200u, c >> 24)) : c; // (the shadow fades with the text)
|
||||||
|
const int off = pass == 0 ? scale : 0;
|
||||||
|
int cx = x + off;
|
||||||
|
for (char ch : s) {
|
||||||
|
if (ch >= 32 && ch < 127) {
|
||||||
|
const uint8_t* g = kFont[ch - 32];
|
||||||
|
for (int col_i = 0; col_i < 5; ++col_i)
|
||||||
|
for (int row = 0; row < 8; ++row)
|
||||||
|
if (g[col_i] >> row & 1) rect(cx + col_i * scale, y + off + row * scale, scale, scale, col);
|
||||||
|
}
|
||||||
|
cx += 6 * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void icon(const Icon& ic, int x, int y, int scale) {
|
||||||
|
for (int j = 0; j < ic.h; ++j)
|
||||||
|
for (int i = 0; i < ic.w; ++i) {
|
||||||
|
const uint32_t c = ic.px[(size_t)j * ic.w + i];
|
||||||
|
if (c >> 24) rect(x + i * scale, y + j * scale, scale, scale, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Draws an icon stretched to w x h output pixels (nearest texel: the slots of the information boxes, 0.6 of the UI scale)
|
||||||
|
void icon_fit(const Icon& ic, int x, int y, int w, int h) {
|
||||||
|
if (ic.w <= 0 || ic.h <= 0 || w <= 0 || h <= 0) return;
|
||||||
|
for (int j = 0; j < ic.h; ++j) {
|
||||||
|
const int y0 = y + j * h / ic.h, y1 = y + (j + 1) * h / ic.h;
|
||||||
|
for (int i = 0; i < ic.w; ++i) {
|
||||||
|
const uint32_t c = ic.px[(size_t)j * ic.w + i];
|
||||||
|
if (!(c >> 24)) continue;
|
||||||
|
const int x0 = x + i * w / ic.w, x1 = x + (i + 1) * w / ic.w;
|
||||||
|
if (x1 > x0 && y1 > y0) rect(x0, y0, x1 - x0, y1 - y0, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Draws texels (ARGB, w x h) centred on (cx, cy).
|
||||||
|
void blit_centered(const uint32_t* tex, int tw, int th, int cx, int cy, int scale) {
|
||||||
|
for (int j = 0; j < th; ++j)
|
||||||
|
for (int i = 0; i < tw; ++i) {
|
||||||
|
const uint32_t c = tex[(size_t)j * tw + i];
|
||||||
|
if (c >> 24) rect(cx + (i - tw / 2) * scale, cy + (j - th / 2) * scale, scale, scale, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Draws texels rotated by `angle` (radians) around the pivot pixel (px, py) of the image, placed at (x, y); mirror flips the image vertically.
|
||||||
|
void blit_rotated(const uint32_t* tex, int tw, int th, int px, int py, double x, double y, double angle, int scale, bool mirror) {
|
||||||
|
const double ca = std::cos(angle), sa = std::sin(angle);
|
||||||
|
const int rad = (int)std::ceil(std::hypot((double)tw, (double)th)) * scale + 2;
|
||||||
|
for (int dy = -rad; dy <= rad; ++dy)
|
||||||
|
for (int dx = -rad; dx <= rad; ++dx) {
|
||||||
|
// inverse rotation of the destination offset into image space
|
||||||
|
const double ix = (dx * ca + dy * sa) / scale + px, iy = (-dx * sa + dy * ca) / scale + py;
|
||||||
|
int sx = (int)std::floor(ix), sy = (int)std::floor(iy);
|
||||||
|
if (mirror) sy = th - 1 - sy + 0;
|
||||||
|
if (sx < 0 || sy < 0 || sx >= tw || sy >= th) continue;
|
||||||
|
const uint32_t c = tex[(size_t)sy * tw + sx];
|
||||||
|
if (c >> 24) put((int)std::lround(x) + dx, (int)std::lround(y) + dy, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Horizontal bar: dark frame, background, filled part.
|
||||||
|
void bar(int x, int y, int bw, int bh, float frac, uint32_t fill, uint32_t back) {
|
||||||
|
frac = std::clamp(frac, 0.0f, 1.0f);
|
||||||
|
rect(x - 2, y - 2, bw + 4, bh + 4, rgba(0, 0, 0, 220));
|
||||||
|
rect(x, y, bw, bh, back);
|
||||||
|
rect(x, y, (int)(bw * frac + 0.5f), bh, fill);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Enemy { int sx, sy; float hp, maxhp; };
|
||||||
|
struct BossBar { float hp, maxhp; }; // a BossHealthBarComponent bar on the screen (00a58380)
|
||||||
|
|
||||||
|
// Everything the HUD shows for one frame.
|
||||||
|
struct State {
|
||||||
|
bool world = false, has_player = false, paused = false, brush_mode = false, free_cam = false, debug = false, help = false;
|
||||||
|
float hp = 4, maxhp = 4, energy = 1; // hp in game units (x25 on screen), energy 0..1
|
||||||
|
double px = 0, py = 0;
|
||||||
|
int view_x = 0, view_y = 0;
|
||||||
|
int mouse_x = 0, mouse_y = 0; // output pixels
|
||||||
|
int brush_select = 0, brush_radius = 6;
|
||||||
|
const char* const* palette_names = nullptr;
|
||||||
|
const uint32_t* palette_colors = nullptr; // ARGB swatches of the 8 brush materials
|
||||||
|
double fps = 0;
|
||||||
|
double gpu_ms = 0, frame_ms = 0; // GPU time of the last frame (timestamp queries), wall time between frames
|
||||||
|
int sim_speed = 1; // cell simulation ticks per frame
|
||||||
|
bool show_timing = false; // always-on timing line (sandbox)
|
||||||
|
int entities = 0, stored_tiles = 0, frame = 0;
|
||||||
|
int death_timer = 0; // frames left of the "you died" message
|
||||||
|
std::vector<Enemy> enemies;
|
||||||
|
std::vector<BossBar> boss_bars; // (main.cpp: the bosses within gui_max_distance_visible)
|
||||||
|
int ui_frame = 0; // (the blink of a low boss bar)
|
||||||
|
// the mod GUI (Gui* of the Lua API) in GUI units: gui_scale window pixels each; kind 0 text, 1 image, 2 nine-piece
|
||||||
|
struct GuiDraw { int kind = 0; float x = 0, y = 0, w = 0, h = 0, scale = 1, alpha = 1; uint32_t color = 0xFFFFFFFFu; std::string text; const Icon* icon = nullptr; };
|
||||||
|
std::vector<GuiDraw> gui;
|
||||||
|
int gui_scale = 2;
|
||||||
|
struct WorldText { int x = 0, y = 0; std::string text; uint32_t color = 0xFFFFFFFFu; int scale = 2; bool dmg_font = false; }; // damage numbers (font_damage, centred on x and y, no shadow) and BLOCK (centred on x)
|
||||||
|
std::vector<WorldText> damage_texts;
|
||||||
|
int money = 0;
|
||||||
|
struct WandInfo { const Icon* icon = nullptr; std::string name; int cards = 0, capacity = 0, uses = -1; }; // uses: the uses left of its first limited spell (-1 none)
|
||||||
|
std::vector<WandInfo> wands; // inventory slots
|
||||||
|
int active_wand = -1;
|
||||||
|
struct ItemInfo { const Icon* icon = nullptr; float frac = 1.0f; std::string name; bool empty = false; }; // an item of the quick inventory (flask): icon, how full it is
|
||||||
|
std::vector<ItemInfo> items; // slots 5-8
|
||||||
|
int active_slot = -1; // highlighted slot (0-7) when it is an item
|
||||||
|
std::vector<const Icon*> spell_icons; // spells of the active wand, in deck order
|
||||||
|
std::vector<int> spell_uses; // their uses left (-1: unlimited), drawn in the corner like in the inventory
|
||||||
|
float mana = 0, mana_max = 0;
|
||||||
|
float air = 1.0f; // breath left (1 = full)
|
||||||
|
bool burning = false;
|
||||||
|
int reload_left = 0, cooldown_left = 0;
|
||||||
|
struct ProjDraw { int sx = 0, sy = 0; const uint32_t* tex = nullptr; int w = 0, h = 0; uint32_t color = 0xFFFFD040u; };
|
||||||
|
std::vector<ProjDraw> projectiles;
|
||||||
|
// the wand held in the hand: sprite, pivot in the sprite, hand position and aim angle (output pixels / radians)
|
||||||
|
const uint32_t* hand_tex = nullptr; int hand_w = 0, hand_h = 0, hand_px = 0, hand_py = 0, hand_x = 0, hand_y = 0; double hand_angle = 0; bool hand_mirror = false;
|
||||||
|
std::vector<std::string> log_lines; // recent game messages, newest last
|
||||||
|
std::vector<std::string> inventory; // names of picked up items
|
||||||
|
std::string hint; // e.g. "E Open chest" next to the player
|
||||||
|
int hint_x = -1, hint_y = -1; // output pixels
|
||||||
|
uint32_t hint_color = 0xFFFFFFFFu;
|
||||||
|
const char* seed_text = "";
|
||||||
|
// ---- the open inventory (wand editing): cells are laid out and hit-tested by the host, drawn here ----
|
||||||
|
struct InvCell { int x = 0, y = 0, w = 0, h = 0; bool draw = true; int bg = -1; const Icon* icon = nullptr; float fill = -1.0f; bool hover = false, selected = false, locked = false, ghost = false; bool frame = true; std::string label; };
|
||||||
|
struct InvPanel { int x = 0, y = 0, w = 0, h = 0; int style = 0; bool active = false; }; // style 0: the brown frame of a bar, 1: the dark box of a wand
|
||||||
|
struct InvText { int x = 0, y = 0; std::string text; uint32_t color = 0xFFFFFFFFu; int scale = 1; };
|
||||||
|
struct TipLine { std::string text; uint32_t color = 0xFFFFFFFFu; const Icon* icon = nullptr; std::string value; };
|
||||||
|
struct StatusIcon { const Icon* icon = nullptr; std::string name, desc; float alpha = 1.0f; };
|
||||||
|
std::vector<StatusIcon> statuses; // the active statuses of the player (stains, ingestion, effects with a UIIconComponent display_in_hud): icons left of the bars, the text on hover
|
||||||
|
struct HeadIcon { const Icon* icon = nullptr; int x = 0, y = 0, scale = 1; };
|
||||||
|
std::vector<HeadIcon> head_icons; // the status icons over the player's head (UIIconComponent display_above_head, the stains), top-left corner in output pixels
|
||||||
|
bool inv_open = false;
|
||||||
|
int hover_slot = -1; // quick slot (0-7) under the mouse
|
||||||
|
std::vector<InvCell> inv_cells;
|
||||||
|
std::vector<InvPanel> inv_panels;
|
||||||
|
bool tip_fixed = false; int tip_w = 0; const Icon* tip_icon = nullptr; // the information box at a fixed place (width in output pixels, a big icon on its right)
|
||||||
|
std::vector<InvText> inv_texts;
|
||||||
|
bool drag_on = false; const Icon* drag_icon = nullptr; int drag_bg = -1; int drag_x = 0, drag_y = 0; // the item on the cursor
|
||||||
|
std::vector<TipLine> tip; // hover information
|
||||||
|
int tip_x = 0, tip_y = 0;
|
||||||
|
// ---- the text sprites of the world (shop prices: SpriteComponent is_text_sprite, font_pixel_white), top-left corner in output pixels, scale = output pixels per world pixel ----
|
||||||
|
std::vector<InvText> world_texts;
|
||||||
|
// ---- overlays drawn over everything (the information box of a wand in the world, the new-wand choice): style 2 the dark information box, 3 the orange banner ----
|
||||||
|
struct Layer { bool dim = false; int dim_alpha = 120; std::vector<InvPanel> panels; std::vector<InvCell> cells; std::vector<InvText> texts; std::vector<InvPanel> shades; }; // (shades: darkened over everything of the layer - the slot under the mouse in the new-wand choice)
|
||||||
|
std::vector<Layer> layers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Assets {
|
||||||
|
Icon heart, jetpack, box, boxHi, mana, money, itemBg[8], inactive, boxQuickHi, importantMsg;
|
||||||
|
GameFont font, dmgFont;
|
||||||
|
bool loaded = false;
|
||||||
|
void load(const ng::Wak& wak) {
|
||||||
|
font = load_font(wak, "data/fonts/font_pixel_white.xml");
|
||||||
|
dmgFont = load_font(wak, "data/fonts/font_damage.xml"); // REPORT_DAMAGE_FONT: red digits and "-" only
|
||||||
|
if (std::getenv("NG_FONTLOG")) { int n = 0; for (const auto& gl : dmgFont.g) n += gl.ok; std::printf("font_damage: texture %dx%d, %d glyphs%c", dmgFont.tex.w, dmgFont.tex.h, n, 10); }
|
||||||
|
heart = load_icon(wak, "data/ui_gfx/hud/health.png");
|
||||||
|
importantMsg = load_icon(wak, "data/ui_gfx/decorations/3piece_important_msg.png"); // (the banner of the new-wand choice: 3 pieces of 24, the middle stretched)
|
||||||
|
jetpack = load_icon(wak, "data/ui_gfx/hud/jetpack.png");
|
||||||
|
box = load_icon(wak, "data/ui_gfx/inventory/full_inventory_box.png");
|
||||||
|
money = load_icon(wak, "data/ui_gfx/hud/money.png");
|
||||||
|
boxHi = load_icon(wak, "data/ui_gfx/inventory/full_inventory_box_highlight.png");
|
||||||
|
mana = load_icon(wak, "data/ui_gfx/hud/mana.png");
|
||||||
|
static const char* const bgs[8] = {"projectile", "static_projectile", "modifier", "draw_many", "material", "other", "utility", "passive"}; // by spell type
|
||||||
|
for (int i = 0; i < 8; ++i) itemBg[i] = load_icon(wak, std::string("data/ui_gfx/inventory/item_bg_") + bgs[i] + ".png");
|
||||||
|
inactive = load_icon(wak, "data/ui_gfx/inventory/inventory_box_inactive_overlay.png");
|
||||||
|
boxQuickHi = load_icon(wak, "data/ui_gfx/inventory/quick_inventory_box_hovered.png");
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::string fmt(const char* f, double a) { char b[64]; std::snprintf(b, sizeof b, f, a); return b; }
|
||||||
|
|
||||||
|
// Draws the whole HUD. Coordinates are output pixels (1280x720).
|
||||||
|
inline void draw(Canvas& cv, const Assets& as, const State& s) {
|
||||||
|
cv.clear();
|
||||||
|
cv.font = as.font.ok() ? &as.font : nullptr;
|
||||||
|
for (const State::InvText& t : s.world_texts) cv.text(t.x, t.y, t.text, t.color, t.scale, false);
|
||||||
|
const int uiS = std::max(1, (int)std::lround(cv.w * 2.8 / 2000.0)), tsS = std::max(1, uiS * 2 / 3);
|
||||||
|
auto drawCell = [&](const State::InvCell& c, int ui, int ts) {
|
||||||
|
if (!c.draw) return;
|
||||||
|
const Icon& frame = c.hover ? as.boxHi : as.box;
|
||||||
|
const Icon* fr = c.frame ? ((c.bg >= 0 && c.bg < 8 && as.itemBg[c.bg].ok()) ? &as.itemBg[c.bg] : &frame) : nullptr;
|
||||||
|
const bool exact = fr && fr->w * ui == c.w; // (a slot at the UI scale: the art as it is; a smaller one - the information boxes, 0.6 - stretched to the cell, its icon to 16 / 20 of it)
|
||||||
|
if (fr) { if (exact) cv.icon(*fr, c.x, c.y, ui); else cv.icon_fit(*fr, c.x, c.y, c.w, c.h); }
|
||||||
|
if (c.icon && c.icon->ok()) {
|
||||||
|
if (c.frame && !exact) { const int iw = c.w * c.icon->w / 20, ih = c.h * c.icon->h / 20; cv.icon_fit(*c.icon, c.x + (c.w - iw) / 2, c.y + (c.h - ih) / 2, iw, ih); }
|
||||||
|
else cv.blit_centered(c.icon->px.data(), c.icon->w, c.icon->h, c.x + c.w / 2, c.y + c.h / 2, c.frame ? std::max(1, std::min(ui, (c.w * 17 / 20) / std::max(c.icon->w, 1))) : std::max(1, c.w / std::max(c.icon->w, 1)));
|
||||||
|
}
|
||||||
|
if (c.hover && c.bg >= 0) cv.frame(c.x, c.y, c.w, c.h, rgba(255, 255, 255, 220));
|
||||||
|
if (c.selected) cv.frame(c.x - ui, c.y - ui, c.w + 2 * ui, c.h + 2 * ui, rgba(255, 220, 120, 230));
|
||||||
|
if (c.locked && as.inactive.ok()) cv.icon(as.inactive, c.x, c.y, ui);
|
||||||
|
if (c.ghost) cv.rect(c.x, c.y, c.w, c.h, rgba(0, 0, 0, 150));
|
||||||
|
if (!c.label.empty()) cv.text(c.x + 2 * ui, c.y + 2 * ui, c.label, rgba(255, 255, 255), std::max(1, ts));
|
||||||
|
};
|
||||||
|
const uint32_t white = rgba(255, 255, 255), grey = rgba(190, 190, 200), red = rgba(210, 40, 40), dark = rgba(40, 20, 20, 230);
|
||||||
|
if (s.inv_open && s.has_player) {
|
||||||
|
cv.rect(0, 0, cv.w, cv.h, rgba(0, 0, 0, 120)); // the inventory dims the world
|
||||||
|
for (const State::InvPanel& p : s.inv_panels) {
|
||||||
|
if (p.style == 0) { cv.rect(p.x, p.y, p.w, p.h, rgba(60, 47, 42, 255)); cv.frame(p.x, p.y, p.w, p.h, rgba(79, 50, 45, 255)); } // (the game's colours, sampled at 2560 x 1440)
|
||||||
|
else { cv.rect(p.x, p.y, p.w, p.h, rgba(4, 3, 4, 245)); cv.frame(p.x, p.y, p.w, p.h, p.active ? rgba(255, 160, 40, 255) : rgba(200, 190, 170, 255)); if (p.active) cv.frame(p.x + 1, p.y + 1, p.w - 2, p.h - 2, rgba(255, 160, 40, 255)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---- the status icons over the player's head ----
|
||||||
|
for (const State::HeadIcon& hi : s.head_icons) if (hi.icon && hi.icon->ok()) cv.icon(*hi.icon, hi.x, hi.y, hi.scale);
|
||||||
|
// ---- the damage numbers / BLOCK rising over the entities (00ab9a90 / 00b071f0) ----
|
||||||
|
for (const State::WorldText& wt : s.damage_texts) {
|
||||||
|
if (wt.dmg_font && as.dmgFont.ok()) {
|
||||||
|
const GameFont* keep = cv.font;
|
||||||
|
cv.font = &as.dmgFont;
|
||||||
|
cv.text(wt.x - cv.text_width(wt.text, wt.scale) / 2, wt.y - as.dmgFont.line_height * wt.scale / 2, wt.text, wt.color, wt.scale, false);
|
||||||
|
cv.font = keep;
|
||||||
|
} else cv.text(wt.x - cv.text_width(wt.text, wt.scale) / 2, wt.y, wt.text, wt.color, wt.scale);
|
||||||
|
}
|
||||||
|
// TODO(to-do #5): the bar widget details (damage chunk, fade, name), the final boss variant, in_world slider scaling
|
||||||
|
// ---- boss bars (BossHealthBarComponent, 00a58380 / 00b7cbf0): centred, at 0.8 of the screen height in virtual pixels and 24 higher for each further boss; the
|
||||||
|
// width 20 log10(2.5 max hp); the player's bar art (frame, back, fill), the heart and the hp; below 0.25 hp the fill blinks every 30 frames in the damage colour
|
||||||
|
{
|
||||||
|
const int ui = std::max(1, (int)std::lround(cv.w * 2.8 / 2000.0));
|
||||||
|
const int ts = std::max(1, ui * 2 / 3);
|
||||||
|
for (size_t k = 0; k < s.boss_bars.size(); ++k) {
|
||||||
|
const BossBar& b = s.boss_bars[k];
|
||||||
|
const float wv = std::clamp(20.0f * std::log10(std::max(2.5f * b.maxhp, 1.0f)), 8.0f, 200.0f);
|
||||||
|
const int bw = (int)(wv * ui), bh = 6 * ui;
|
||||||
|
const int cy = (int)((cv.h / (float)ui * 0.8f - 24.0f * (float)k) * ui), bx = cv.w / 2 - bw / 2, top = cy - bh / 2;
|
||||||
|
const bool blink = b.hp < 0.25f && (s.ui_frame / 30) % 2 == 1;
|
||||||
|
cv.rect(bx, top, bw, bh, rgba(121, 71, 56));
|
||||||
|
cv.rect(bx + ui, top + ui, bw - 2 * ui, bh - 2 * ui, b.hp < 0.25f && !blink ? rgba(110, 30, 30, 235) : rgba(58, 36, 31, 235));
|
||||||
|
cv.rect(bx + ui, top + ui, (int)((bw - 2 * ui) * std::clamp(b.hp / std::max(b.maxhp, 0.001f), 0.0f, 1.0f)), bh - 2 * ui, blink ? rgba(255, 255, 255) : rgba(135, 191, 28));
|
||||||
|
if (as.heart.ok()) cv.icon(as.heart, bx - 10 * ui, cy - 4 * ui, ui);
|
||||||
|
const std::string hp = std::to_string((int)std::lround(std::max(b.hp, 0.0f) * 25.0f));
|
||||||
|
cv.text(bx + bw + 3 * ui, cy - 3 * ts, hp, rgba(255, 255, 255), ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---- health bars (HealthBarComponent: the bosses) ----
|
||||||
|
for (const Enemy& e : s.enemies) {
|
||||||
|
const int bw = 28;
|
||||||
|
cv.rect(e.sx - bw / 2 - 1, e.sy - 1, bw + 2, 5, rgba(0, 0, 0, 200));
|
||||||
|
cv.rect(e.sx - bw / 2, e.sy, bw, 3, dark);
|
||||||
|
cv.rect(e.sx - bw / 2, e.sy, (int)(bw * std::clamp(e.hp / std::max(e.maxhp, 0.001f), 0.0f, 1.0f)), 3, red);
|
||||||
|
}
|
||||||
|
if (s.has_player) {
|
||||||
|
// The layout of the game (measured on a 2000x1088 screenshot, where the HUD is drawn at 2.8 pixels per asset pixel): the quick inventory top left,
|
||||||
|
// the health / flight / mana bars with their icons top right, the gold under them.
|
||||||
|
const int ui = std::max(1, (int)std::lround(cv.w * 2.8 / 2000.0)); // output pixels per asset pixel
|
||||||
|
const int ts = std::max(1, ui * 2 / 3); // text scale
|
||||||
|
const int slot = 20 * ui, pitch = (int)(22.5 * ui), x0 = 23 * ui, y0 = 23 * ui;
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
const int x = x0 + i * pitch;
|
||||||
|
cv.icon(((s.active_slot >= 0 ? i == s.active_slot : i == s.active_wand) || i == s.hover_slot) ? as.boxHi : as.box, x, y0, ui);
|
||||||
|
if (i >= 4 && i - 4 < (int)s.items.size() && !s.items[i - 4].empty) {
|
||||||
|
const State::ItemInfo& it = s.items[i - 4];
|
||||||
|
if (it.icon && it.icon->ok()) cv.blit_centered(it.icon->px.data(), it.icon->w, it.icon->h, x + slot / 2, y0 + slot / 2, std::max(1, std::min(ui, (slot * 3 / 4) / std::max(it.icon->w, 1))));
|
||||||
|
cv.rect(x + 2 * ui, y0 + slot - 3 * ui, slot - 4 * ui, 2 * ui, rgba(0, 0, 0, 170)); // how full the flask is
|
||||||
|
cv.rect(x + 2 * ui, y0 + slot - 3 * ui, (int)((slot - 4 * ui) * std::clamp(it.frac, 0.0f, 1.0f)), 2 * ui, rgba(110, 170, 230));
|
||||||
|
}
|
||||||
|
if (i < 4 && i < (int)s.wands.size() && s.wands[i].icon && s.wands[i].icon->ok()) {
|
||||||
|
const Icon& ic = *s.wands[i].icon;
|
||||||
|
cv.blit_centered(ic.px.data(), ic.w, ic.h, x + slot / 2, y0 + slot / 2, std::max(1, std::min(ui, (slot * 3 / 4) / std::max(ic.w, 1))));
|
||||||
|
if (s.wands[i].uses >= 0) cv.text(x + 2 * ui, y0 + 2 * ui, std::to_string(s.wands[i].uses), rgba(255, 255, 255), std::max(1, ts)); // the first limited spell's uses left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// spells of the active wand, under the quick inventory
|
||||||
|
const int sy = y0 + slot + 3 * ui;
|
||||||
|
for (size_t i = 0; i < s.spell_icons.size() && !s.inv_open; ++i) {
|
||||||
|
const int x = x0 + (int)i * 18 * ui;
|
||||||
|
cv.rect(x, sy, 17 * ui, 17 * ui, rgba(0, 0, 0, 150));
|
||||||
|
if (s.spell_icons[i] && s.spell_icons[i]->ok()) cv.blit_centered(s.spell_icons[i]->px.data(), s.spell_icons[i]->w, s.spell_icons[i]->h, x + 17 * ui / 2, sy + 17 * ui / 2, ui);
|
||||||
|
if (i < s.spell_uses.size() && s.spell_uses[i] >= 0) cv.text(x + 2 * ui, sy + 2 * ui, std::to_string(s.spell_uses[i]), rgba(255, 255, 255), std::max(1, ts));
|
||||||
|
}
|
||||||
|
if (s.inv_open) {
|
||||||
|
} else if (s.active_slot >= 4 && s.active_slot - 4 < (int)s.items.size()) {
|
||||||
|
cv.text(x0, sy + (s.spell_icons.empty() ? 0 : 19 * ui), s.items[s.active_slot - 4].name, white, ts);
|
||||||
|
} else if (s.active_wand >= 0 && s.active_wand < (int)s.wands.size()) {
|
||||||
|
const auto& w = s.wands[s.active_wand];
|
||||||
|
cv.text(x0, sy + (s.spell_icons.empty() ? 0 : 19 * ui), w.name.empty() ? "Wand" : w.name, white, ts);
|
||||||
|
}
|
||||||
|
if (s.inv_open) { // the wand rows, the full inventory and what is on the cursor
|
||||||
|
for (const State::InvText& t : s.inv_texts) cv.text(t.x, t.y, t.text, t.color, t.scale);
|
||||||
|
for (const State::InvCell& c : s.inv_cells) drawCell(c, ui, ts);
|
||||||
|
if (!s.tip.empty()) { // hover information: a dark box with the name, the description and the numbers
|
||||||
|
const int ts = std::max(2, ui * 3 / 4); // (the information box has bigger text than the rest)
|
||||||
|
int tw = 0;
|
||||||
|
for (const State::TipLine& l : s.tip) tw = std::max(tw, cv.text_width(l.text, ts) + (l.value.empty() ? 0 : cv.text_width(l.value, ts) + 10 * ts) + (l.icon ? 9 * ts : 0));
|
||||||
|
const int lh = 9 * ts, bw = s.tip_fixed ? s.tip_w : tw + 8 * ts, bh = (int)s.tip.size() * lh + 6 * ts;
|
||||||
|
const int bx = s.tip_fixed ? s.tip_x : std::clamp(s.tip_x, 4, std::max(4, cv.w - bw - 4)), by = s.tip_fixed ? s.tip_y : std::clamp(s.tip_y, 4, std::max(4, cv.h - bh - 4));
|
||||||
|
cv.rect(bx, by, bw, bh, rgba(12, 10, 12, 235));
|
||||||
|
cv.frame(bx, by, bw, bh, rgba(150, 130, 110, 255));
|
||||||
|
if (s.tip_icon && s.tip_icon->ok()) cv.blit_centered(s.tip_icon->px.data(), s.tip_icon->w, s.tip_icon->h, bx + bw - 28 * ui, by + bh / 2, ui);
|
||||||
|
int ty = by + 3 * ts;
|
||||||
|
for (const State::TipLine& l : s.tip) {
|
||||||
|
int tx = bx + 4 * ts;
|
||||||
|
if (l.icon && l.icon->ok()) { cv.blit_centered(l.icon->px.data(), l.icon->w, l.icon->h, tx + 3 * ts, ty + 3 * ts, std::max(1, ts)); tx += 9 * ts; }
|
||||||
|
cv.text(tx, ty, l.text, l.color, ts);
|
||||||
|
if (!l.value.empty()) cv.text(bx + bw - 4 * ts - cv.text_width(l.value, ts), ty, l.value, rgba(255, 255, 255), ts);
|
||||||
|
ty += lh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.drag_on && (s.drag_icon || s.drag_bg >= 0)) {
|
||||||
|
if (s.drag_bg >= 0 && s.drag_bg < 8 && as.itemBg[s.drag_bg].ok()) cv.icon(as.itemBg[s.drag_bg], s.drag_x - 10 * ui, s.drag_y - 10 * ui, ui);
|
||||||
|
if (s.drag_icon && s.drag_icon->ok()) cv.blit_centered(s.drag_icon->px.data(), s.drag_icon->w, s.drag_icon->h, s.drag_x, s.drag_y, std::max(1, std::min(ui, (17 * ui) / std::max(s.drag_icon->w, 1))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// bars: a brown frame, a dark back, the fill in the colour of the game's colour strips
|
||||||
|
const int right = cv.w - 45 * ui;
|
||||||
|
auto bar = [&](float cy, int h, float frac, uint32_t fill) {
|
||||||
|
const int top = (int)((cy - h * 0.5f) * ui), bx = right - 46 * ui, bw = 46 * ui;
|
||||||
|
cv.rect(bx, top, bw, h * ui, rgba(121, 71, 56));
|
||||||
|
cv.rect(bx + ui, top + ui, bw - 2 * ui, (h - 2) * ui, rgba(58, 36, 31, 235));
|
||||||
|
cv.rect(bx + ui, top + ui, (int)((bw - 2 * ui) * std::clamp(frac, 0.0f, 1.0f)), (h - 2) * ui, fill);
|
||||||
|
};
|
||||||
|
auto small_icon = [&](const Icon& ic, float cy) { cv.icon(ic, right + 3 * ui, (int)((cy - 4.0f) * ui), ui); };
|
||||||
|
const bool low = s.hp < 0.3f * s.maxhp;
|
||||||
|
bar(25.7f, 6, s.hp / std::max(s.maxhp, 0.001f), low ? rgba(214, 90, 56) : rgba(135, 191, 28));
|
||||||
|
small_icon(as.heart, 25.7f);
|
||||||
|
cv.text(right + 14 * ui, (int)(25.7f * ui) - 3 * ts, std::to_string((int)std::lround(std::max(s.hp, 0.0f) * 25.0f)), white, ts);
|
||||||
|
bar(35.7f, 5, s.energy, rgba(255, 170, 64));
|
||||||
|
small_icon(as.jetpack, 35.7f);
|
||||||
|
if (s.mana_max > 0) {
|
||||||
|
bar(44.6f, 5, s.mana / s.mana_max, rgba(66, 168, 226));
|
||||||
|
small_icon(as.mana, 44.6f);
|
||||||
|
if (s.reload_left > 0) cv.rect(right - 46 * ui, (int)(47.6f * ui), std::min(46 * ui, s.reload_left * ui / 2), ui, rgba(255, 150, 60));
|
||||||
|
else if (s.cooldown_left > 0) cv.rect(right - 46 * ui, (int)(47.6f * ui), std::min(46 * ui, s.cooldown_left * ui / 2), ui, rgba(255, 255, 255, 160));
|
||||||
|
}
|
||||||
|
small_icon(as.money, 52.5f);
|
||||||
|
{ // the statuses: a column of icons under the bars (from the gold row down, at the bars' left edge); the mouse over one shows its name and description
|
||||||
|
const int colX = right - 46 * ui;
|
||||||
|
int sy2 = (int)(s.air < 0.999f ? 67.0f * ui : 60.0f * ui), hover = -1, hy = 0;
|
||||||
|
for (size_t k = 0; k < s.statuses.size(); ++k) {
|
||||||
|
const State::StatusIcon& si = s.statuses[k];
|
||||||
|
const int w = si.icon && si.icon->ok() ? si.icon->w * ui : 8 * ui, h = si.icon && si.icon->ok() ? si.icon->h * ui : 8 * ui;
|
||||||
|
if (si.icon && si.icon->ok()) cv.icon(*si.icon, colX + (10 * ui - w) / 2, sy2, ui);
|
||||||
|
else cv.rect(colX + ui, sy2, 8 * ui, 8 * ui, rgba(200, 200, 200, 160));
|
||||||
|
if (s.mouse_x >= colX - ui && s.mouse_x < colX + 11 * ui && s.mouse_y >= sy2 && s.mouse_y < sy2 + h) { hover = (int)k; hy = sy2; }
|
||||||
|
sy2 += h + 2 * ui;
|
||||||
|
}
|
||||||
|
if (hover >= 0) {
|
||||||
|
const State::StatusIcon& si = s.statuses[(size_t)hover];
|
||||||
|
const int lh = 9 * ts, tw = std::max(cv.text_width(si.name, ts), si.desc.empty() ? 0 : cv.text_width(si.desc, ts)) + 8 * ts;
|
||||||
|
const int bh = (si.desc.empty() ? 1 : 2) * lh + 6 * ts, bx = std::max(4, colX - 3 * ui - tw);
|
||||||
|
cv.rect(bx, hy, tw, bh, rgba(22, 20, 20, 235)); cv.frame(bx, hy, tw, bh, rgba(196, 178, 140));
|
||||||
|
cv.text(bx + 4 * ts, hy + 3 * ts, si.name, white, ts);
|
||||||
|
if (!si.desc.empty()) cv.text(bx + 4 * ts, hy + 3 * ts + lh, si.desc, rgba(190, 190, 200), ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cv.text(right + 14 * ui, (int)(52.5f * ui) - 3 * ts, std::to_string(s.money), white, ts);
|
||||||
|
if (s.air < 0.999f) { // breath under liquid
|
||||||
|
bar(61.0f, 5, s.air, s.air < 0.25f ? rgba(255, 80, 60) : rgba(120, 220, 255));
|
||||||
|
cv.text(right + 14 * ui, (int)(61.0f * ui) - 3 * ts, "AIR", rgba(150, 230, 255), ts);
|
||||||
|
}
|
||||||
|
if (s.burning) cv.text(right - 46 * ui, (int)(66.0f * ui), "ON FIRE", rgba(255, 150, 40), ts);
|
||||||
|
}
|
||||||
|
// ---- brush palette ----
|
||||||
|
if (!s.world || s.brush_mode) {
|
||||||
|
const int x0 = 14, y0 = s.world ? 120 : 14;
|
||||||
|
cv.text(x0, y0, s.world ? "BRUSH MODE (B to leave)" : "SANDBOX", rgba(255, 220, 120), 2);
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
const int x = x0 + i * 74, y = y0 + 24;
|
||||||
|
cv.rect(x - 2, y - 2, 70, 34, i == s.brush_select ? rgba(255, 255, 255, 230) : rgba(0, 0, 0, 160));
|
||||||
|
cv.rect(x, y, 66, 20, s.palette_colors ? s.palette_colors[i] : white);
|
||||||
|
cv.rect(x, y + 20, 66, 10, rgba(0, 0, 0, 180));
|
||||||
|
cv.text(x + 2, y + 21, std::to_string(i + 1) + " " + std::string(s.palette_names ? s.palette_names[i] : "").substr(0, 8), white, 1, false);
|
||||||
|
}
|
||||||
|
cv.text(x0, y0 + 64, "LMB paint RMB erase wheel = size " + std::to_string(s.brush_radius), grey, 1);
|
||||||
|
}
|
||||||
|
// ---- messages and the interaction hint ----
|
||||||
|
{
|
||||||
|
int y = 110;
|
||||||
|
for (size_t i = 0; i < s.log_lines.size(); ++i) {
|
||||||
|
const std::string& l = s.log_lines[i];
|
||||||
|
cv.text(cv.w / 2 - cv.text_width(l, 2) / 2, 20 + (int)i * 24, l, rgba(255, 240, 160), 2);
|
||||||
|
(void)y;
|
||||||
|
}
|
||||||
|
if (!s.hint.empty() && s.hint_x >= 0) cv.text(s.hint_x - cv.text_width(s.hint, 2) / 2, s.hint_y, s.hint, s.hint_color, 2);
|
||||||
|
}
|
||||||
|
// ---- wand in hand and projectiles ----
|
||||||
|
if (s.hand_tex) cv.blit_rotated(s.hand_tex, s.hand_w, s.hand_h, s.hand_px, s.hand_py, s.hand_x, s.hand_y, s.hand_angle, 2, s.hand_mirror);
|
||||||
|
for (const auto& p : s.projectiles) {
|
||||||
|
if (p.tex && p.w > 0) cv.blit_centered(p.tex, p.w, p.h, p.sx, p.sy, 2);
|
||||||
|
else cv.rect(p.sx - 3, p.sy - 3, 6, 6, p.color);
|
||||||
|
}
|
||||||
|
// ---- overlays ----
|
||||||
|
for (const State::Layer& L : s.layers) {
|
||||||
|
if (L.dim) cv.rect(0, 0, cv.w, cv.h, rgba(0, 0, 0, L.dim_alpha));
|
||||||
|
for (const State::InvPanel& p : L.panels) {
|
||||||
|
if (p.style == 3 && as.importantMsg.ok() && as.importantMsg.w == 72) { // the banner of the new-wand choice: 3piece_important_msg.png, its ends as they are, its middle stretched
|
||||||
|
const Icon& im = as.importantMsg;
|
||||||
|
const int pw = 24, ph = im.h;
|
||||||
|
auto piece = [&](int k) { Icon c; c.w = pw; c.h = ph; c.px.resize((size_t)pw * ph); for (int j = 0; j < ph; ++j) for (int i = 0; i < pw; ++i) c.px[(size_t)j * pw + i] = im.px[(size_t)j * im.w + k * pw + i]; return c; };
|
||||||
|
static Icon left, mid, right; static bool cut = false;
|
||||||
|
if (!cut) { left = piece(0); mid = piece(1); right = piece(2); cut = true; }
|
||||||
|
const int ew = pw * uiS, eh = p.h;
|
||||||
|
cv.icon_fit(left, p.x, p.y, ew, eh);
|
||||||
|
cv.icon_fit(mid, p.x + ew, p.y, std::max(1, p.w - 2 * ew), eh);
|
||||||
|
cv.icon_fit(right, p.x + p.w - ew, p.y, ew, eh);
|
||||||
|
} else if (p.style == 3) { // (without the art: an orange band with a golden frame and rounded ends)
|
||||||
|
for (int j = 0; j < p.h; ++j) { const float t = (float)j / std::max(1, p.h - 1); const int r = 220 - (int)(60 * std::fabs(t - 0.4f)), gg = 112 - (int)(40 * std::fabs(t - 0.4f)); cv.rect(p.x, p.y + j, p.w, 1, rgba(r, gg, 24, 245)); }
|
||||||
|
cv.frame(p.x, p.y, p.w, p.h, rgba(255, 196, 70)); cv.frame(p.x + uiS, p.y + uiS, p.w - 2 * uiS, p.h - 2 * uiS, rgba(120, 50, 10));
|
||||||
|
cv.rect(p.x - 3 * uiS, p.y + p.h / 2 - 3 * uiS, 3 * uiS, 6 * uiS, rgba(255, 196, 70)); cv.rect(p.x + p.w, p.y + p.h / 2 - 3 * uiS, 3 * uiS, 6 * uiS, rgba(255, 196, 70));
|
||||||
|
} else if (p.style == 2) { cv.rect(p.x, p.y, p.w, p.h, rgba(15, 14, 13, 225)); cv.frame(p.x, p.y, p.w, p.h, rgba(148, 128, 100)); cv.frame(p.x + 1, p.y + 1, p.w - 2, p.h - 2, rgba(148, 128, 100)); if (p.active) cv.frame(p.x - uiS, p.y - uiS, p.w + 2 * uiS, p.h + 2 * uiS, rgba(255, 160, 40)); } // (the information box: the game's fill and 2 px frame, sampled at 2560 x 1440)
|
||||||
|
else { cv.rect(p.x, p.y, p.w, p.h, rgba(30, 22, 22, 235)); cv.frame(p.x, p.y, p.w, p.h, p.active ? rgba(255, 160, 40, 255) : rgba(200, 190, 170, 255)); }
|
||||||
|
}
|
||||||
|
for (const State::InvCell& c : L.cells) drawCell(c, uiS, tsS);
|
||||||
|
for (const State::InvText& t : L.texts) cv.text(t.x, t.y, t.text, t.color, t.scale);
|
||||||
|
for (const State::InvPanel& p : L.shades) cv.rect(p.x, p.y, p.w, p.h, rgba(0, 0, 0, 110));
|
||||||
|
}
|
||||||
|
// ---- messages ----
|
||||||
|
if (s.death_timer > 0) {
|
||||||
|
const std::string m = "YOU DIED";
|
||||||
|
cv.rect(0, cv.h / 2 - 60, cv.w, 90, rgba(0, 0, 0, std::min(200, s.death_timer * 4)));
|
||||||
|
cv.text(cv.w / 2 - cv.text_width(m, 6) / 2, cv.h / 2 - 40, m, rgba(220, 40, 40), 6);
|
||||||
|
const std::string m2 = "respawning at the start";
|
||||||
|
cv.text(cv.w / 2 - cv.text_width(m2, 2) / 2, cv.h / 2 + 6, m2, grey, 2);
|
||||||
|
}
|
||||||
|
if (s.paused) {
|
||||||
|
cv.rect(0, 0, cv.w, cv.h, rgba(0, 0, 0, 90));
|
||||||
|
const std::string m = "PAUSED";
|
||||||
|
cv.text(cv.w / 2 - cv.text_width(m, 6) / 2, cv.h / 2 - 40, m, white, 6);
|
||||||
|
}
|
||||||
|
if (s.help) {
|
||||||
|
const char* lines[] = {
|
||||||
|
"A / D, arrows walk", "W, Up, Space jump (hold in the air to levitate)", "mouse aim, the camera leans toward it",
|
||||||
|
"LMB fire the spark bolt", "E pick up / open the item next to you", "F kick (melee swing, throws crates and barrels)", "B brush mode: paint materials with the mouse (keys 1-8)",
|
||||||
|
"5-8 flasks (LMB pour, RMB throw)", "Tab, I inventory: drag spells and wands (spells only in a workshop)", "RMB throw the item in the hand, Q drop it", "F2 free camera: WASD, [ ] speed, T teleport, G 10000 gold, no fog", "F8 debug mode: wand editing everywhere, K test wand, C spell panel (type to filter, click to spawn)",
|
||||||
|
"F9 save the last 20 s (clips folder), F11 clip editor: cut and save a GIF", "F10 performance report of the last 20 s (perf folder)", "P pause", "F3 debug info, entity ids", "F4 lighting on / off", "F5 sky debug view", "F1 this help", "Esc quit",
|
||||||
|
};
|
||||||
|
cv.rect(300, 120, 680, 470, rgba(0, 0, 0, 215));
|
||||||
|
cv.frame(300, 120, 680, 470, rgba(255, 255, 255, 160));
|
||||||
|
cv.text(320, 134, "CONTROLS", rgba(255, 220, 120), 3);
|
||||||
|
int y = 172;
|
||||||
|
for (const char* l : lines) { cv.text(320, y, l, white, 2); y += 24; }
|
||||||
|
} else if (s.has_player || !s.world) {
|
||||||
|
cv.text(cv.w - 14 - cv.text_width("F1 help", 1), cv.h - 18, "F1 help", grey, 1);
|
||||||
|
}
|
||||||
|
if (s.debug || s.show_timing) { // speed measurement: GPU time of a whole frame, per simulation tick (upper bound), frame pacing
|
||||||
|
const std::string t = "sim x" + std::to_string(s.sim_speed) + " GPU " + fmt("%.2f", s.gpu_ms) + " ms (" + fmt("%.3f", s.gpu_ms / std::max(1, s.sim_speed)) +
|
||||||
|
"/tick) " + fmt("%.1f", s.frame_ms) + " ms/frame " + fmt("%.0f", s.fps) + " fps";
|
||||||
|
cv.text(cv.w - 14 - cv.text_width(t, 2), s.debug ? 90 : 14, t, rgba(255, 220, 120), 2);
|
||||||
|
}
|
||||||
|
if (s.debug) {
|
||||||
|
const std::string l1 = fmt("%.0f fps", s.fps) + " frame " + std::to_string(s.frame);
|
||||||
|
const std::string l2 = "player " + fmt("%.0f", s.px) + ", " + fmt("%.0f", s.py) + " view " + std::to_string(s.view_x) + ", " + std::to_string(s.view_y);
|
||||||
|
const std::string l3 = "entities " + std::to_string(s.entities) + " stored tiles " + std::to_string(s.stored_tiles) + " " + s.seed_text;
|
||||||
|
int y = 14;
|
||||||
|
for (const std::string& l : {l1, l2, l3}) {
|
||||||
|
cv.text(cv.w - 14 - cv.text_width(l, 2), y, l, rgba(160, 255, 160), 2);
|
||||||
|
y += 22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---- the mod GUI ----
|
||||||
|
for (const State::GuiDraw& d : s.gui) {
|
||||||
|
const float k = (float)s.gui_scale;
|
||||||
|
const uint32_t a = (uint32_t)std::clamp(d.alpha * (float)(d.color >> 24), 0.0f, 255.0f);
|
||||||
|
if (d.kind == 0) { cv.text((int)std::lround(d.x * k), (int)std::lround(d.y * k), d.text, (d.color & 0xFFFFFFu) | a << 24, std::max(1, (int)std::lround(k * d.scale)), true); continue; }
|
||||||
|
if (!d.icon || !d.icon->ok()) continue;
|
||||||
|
const Icon& ic = *d.icon;
|
||||||
|
auto blit = [&](int sx0, int sy0, int sw, int sh, float dx, float dy, float dw, float dh) { // a part of the image stretched over the rectangle, alpha blended
|
||||||
|
const int X0 = (int)std::lround(dx * k), Y0 = (int)std::lround(dy * k), X1 = (int)std::lround((dx + dw) * k), Y1 = (int)std::lround((dy + dh) * k);
|
||||||
|
if (sw <= 0 || sh <= 0) return;
|
||||||
|
for (int Y = std::max(0, Y0); Y < std::min(cv.h, Y1); ++Y) for (int X = std::max(0, X0); X < std::min(cv.w, X1); ++X) {
|
||||||
|
const int ix = sx0 + std::min(sw - 1, (int)((X - X0) * (float)sw / std::max(1, X1 - X0))), iy = sy0 + std::min(sh - 1, (int)((Y - Y0) * (float)sh / std::max(1, Y1 - Y0)));
|
||||||
|
const uint32_t c = ic.px[(size_t)iy * ic.w + ix];
|
||||||
|
const uint32_t ca = (uint32_t)((c >> 24) * a / 255);
|
||||||
|
if (ca < 8) continue;
|
||||||
|
const uint32_t tint = d.color & 0xFFFFFFu;
|
||||||
|
const uint32_t r = ((c >> 16) & 255) * ((tint >> 16) & 255) / 255, g2 = ((c >> 8) & 255) * ((tint >> 8) & 255) / 255, b = (c & 255) * (tint & 255) / 255;
|
||||||
|
uint32_t& o = cv.px[(size_t)Y * cv.w + X];
|
||||||
|
cv.mark(X, Y);
|
||||||
|
const uint32_t oa = o >> 24;
|
||||||
|
auto mix = [&](uint32_t s0, uint32_t d0) { return (s0 * ca + d0 * (255 - ca)) / 255; };
|
||||||
|
o = std::max(oa, ca) << 24 | mix(r, (o >> 16) & 255) << 16 | mix(g2, (o >> 8) & 255) << 8 | mix(b, o & 255);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (d.kind == 1) { blit(0, 0, ic.w, ic.h, d.x, d.y, d.w, d.h); continue; }
|
||||||
|
const int cw = std::max(1, ic.w / 3), ch = std::max(1, ic.h / 3), mw = ic.w - 2 * cw, mh = ic.h - 2 * ch; // nine pieces: the corners keep their size, the edges and the middle stretch
|
||||||
|
const float W = std::max(d.w, (float)(2 * cw)), H = std::max(d.h, (float)(2 * ch));
|
||||||
|
const float xs[3] = {d.x, d.x + cw, d.x + W - cw}, ws[3] = {(float)cw, W - 2 * cw, (float)cw}, ys[3] = {d.y, d.y + ch, d.y + H - ch}, hs3[3] = {(float)ch, H - 2 * ch, (float)ch};
|
||||||
|
const int sxs[3] = {0, cw, cw + mw}, sws[3] = {cw, mw, cw}, sys[3] = {0, ch, ch + mh}, shs[3] = {ch, mh, ch};
|
||||||
|
for (int j = 0; j < 3; ++j) for (int i = 0; i < 3; ++i) blit(sxs[i], sys[j], sws[i], shs[j], xs[i], ys[j], ws[i], hs3[j]);
|
||||||
|
}
|
||||||
|
// ---- crosshair (last: over the overlays and the menus) ----
|
||||||
|
if (s.mouse_x >= 0 && !s.paused && !s.drag_on) { // (an item on the cursor replaces the crosshair)
|
||||||
|
const uint32_t c = s.world && !s.brush_mode ? rgba(255, 255, 255, 230) : rgba(255, 220, 120, 230);
|
||||||
|
cv.rect(s.mouse_x - 8, s.mouse_y - 1, 6, 2, c); cv.rect(s.mouse_x + 3, s.mouse_y - 1, 6, 2, c);
|
||||||
|
cv.rect(s.mouse_x - 1, s.mouse_y - 8, 2, 6, c); cv.rect(s.mouse_x - 1, s.mouse_y + 3, 2, 6, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace hud
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
#pragma once
|
||||||
|
// The inventory of the player: the quick inventory (4 wand slots, 4 item slots), the full inventory (16 spell slots) and the spell slots of the wands,
|
||||||
|
// with the rules of the original engine for what may be dropped where (recovered from noita.exe: FUN_00b5ee40 and the predicates it calls, see
|
||||||
|
// docs/INVENTORY_UI.md). Items are runtime entities; where they are is kept in `ItemComponent.inventory_slot.x / .y` like in the game, the owner of a spell is
|
||||||
|
// its parent (the wand); the gun reads a wand's spells by slot.
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entity/runtime.h"
|
||||||
|
#include "gun/gun.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
class Inventory {
|
||||||
|
public:
|
||||||
|
static constexpr int kWands = 4, kItems = 4, kFull = 16;
|
||||||
|
enum Kind { None = 0, QuickWand, QuickItem, Full, WandSpell };
|
||||||
|
struct Loc {
|
||||||
|
Kind kind = None;
|
||||||
|
int wand = 0; // WandSpell: the wand entity
|
||||||
|
int index = 0; // slot number inside the container
|
||||||
|
bool operator==(const Loc& o) const { return kind == o.kind && wand == o.wand && index == o.index; }
|
||||||
|
};
|
||||||
|
|
||||||
|
Inventory(EntityRuntime& rt, GunRuntime& gun) : rt_(rt), gun_(gun) {}
|
||||||
|
|
||||||
|
std::array<int, kWands> wands{}; // entity ids, 0 = empty slot
|
||||||
|
std::array<int, kItems> items{};
|
||||||
|
std::array<int, kFull> full{};
|
||||||
|
bool editing = false; // the wands may be edited (inside a workshop, or the perk EDIT_WANDS_EVERYWHERE): DAT_0122250a of the engine
|
||||||
|
|
||||||
|
// ---- predicates of the engine (FUN_00b5db10 and friends) ----
|
||||||
|
bool has(int e, const char* comp) { return e && !rt_.components(e, comp, "", true).empty(); }
|
||||||
|
bool is_wand(int e) { return has(e, "AbilityComponent") && attr(e, "AbilityComponent", "use_gun_script") == "1"; } // FUN_00b5db10: AbilityComponent.use_gun_script
|
||||||
|
bool is_spell(int e) { return has(e, "ItemActionComponent"); } // FUN_00b5dbf0
|
||||||
|
bool is_pure_spell(int e) { return is_spell(e) && !has(e, "AbilityComponent"); } // FUN_00b5dd60
|
||||||
|
bool is_flask(int e) { return has(e, "MaterialInventoryComponent"); } // FUN_00b5e0d0
|
||||||
|
std::string preferred(int e) { return attr(e, "ItemComponent", "preferred_inventory"); } // ItemComponent.preferred_inventory: DEFAULT / QUICK / FULL
|
||||||
|
|
||||||
|
int capacity(int wand) { // FUN_00b5d7d0: the slots of a wand = its deck capacity
|
||||||
|
return (int)std::atof(attr(wand, "AbilityComponent", "gun_config.deck_capacity").c_str());
|
||||||
|
}
|
||||||
|
int slot_x(int item) { return (int)std::atof(attr(item, "ItemComponent", "inventory_slot.x").c_str()); }
|
||||||
|
void set_slot(int item, int x, int y = 0) {
|
||||||
|
for (int cid : rt_.components(item, "ItemComponent", "", true)) {
|
||||||
|
rt_.comp(cid)->attrs["inventory_slot.x"] = std::to_string(x);
|
||||||
|
rt_.comp(cid)->attrs["inventory_slot.y"] = std::to_string(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The spells of a wand by slot (0 = empty), `capacity` entries; the always-cast ones are not in slots.
|
||||||
|
std::vector<int> spells(int wand) {
|
||||||
|
std::vector<int> r(std::max(capacity(wand), 0), 0);
|
||||||
|
for (int k : rt_.children(wand)) {
|
||||||
|
if (!is_spell(k) || permanent(k)) continue;
|
||||||
|
const int x = slot_x(k);
|
||||||
|
if (x >= 0 && x < (int)r.size() && !r[x]) r[x] = k;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
std::vector<int> permanent_spells(int wand) {
|
||||||
|
std::vector<int> r;
|
||||||
|
for (int k : rt_.children(wand)) if (is_spell(k) && permanent(k)) r.push_back(k);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
bool permanent(int spell) { // (AddGunActionPermanent sets the flag on the ItemComponent, the data files on the ItemActionComponent)
|
||||||
|
for (int cid : rt_.components(spell, "ItemActionComponent", "", true)) if (rt_.comp(cid)->attrs["permanently_attached"] == "1") return true;
|
||||||
|
for (int cid : rt_.components(spell, "ItemComponent", "", true)) if (rt_.comp(cid)->attrs["permanently_attached"] == "1") return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loc where(int item) {
|
||||||
|
for (int i = 0; i < kWands; ++i) if (wands[i] == item) return {QuickWand, 0, i};
|
||||||
|
for (int i = 0; i < kItems; ++i) if (items[i] == item) return {QuickItem, 0, i};
|
||||||
|
for (int i = 0; i < kFull; ++i) if (full[i] == item) return {Full, 0, i};
|
||||||
|
if (RtEntity* e = rt_.ent(item)) if (e->parent && isWandLike(e->parent)) {
|
||||||
|
const int x = slot_x(item);
|
||||||
|
return {WandSpell, e->parent, x};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
int at(const Loc& l) {
|
||||||
|
switch (l.kind) {
|
||||||
|
case QuickWand: return l.index >= 0 && l.index < kWands ? wands[l.index] : 0;
|
||||||
|
case QuickItem: return l.index >= 0 && l.index < kItems ? items[l.index] : 0;
|
||||||
|
case Full: return l.index >= 0 && l.index < kFull ? full[l.index] : 0;
|
||||||
|
case WandSpell: { auto s = spells(l.wand); return l.index >= 0 && l.index < (int)s.size() ? s[l.index] : 0; }
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the rules: may `item` be dropped into the container of `target`? (FUN_00b5ee40) ----
|
||||||
|
bool can_drop(int item, const Loc& target) {
|
||||||
|
if (!item || target.kind == None) return false;
|
||||||
|
const bool to_quick = target.kind == QuickWand || target.kind == QuickItem, to_full = target.kind == Full, to_wand = target.kind == WandSpell;
|
||||||
|
if (is_wand(item) && !to_quick) return false;
|
||||||
|
if (target.kind == QuickWand && !is_wand(item)) return false; // the wands row takes wands, the items row everything else of the quick inventory
|
||||||
|
if (target.kind == QuickItem && is_wand(item)) return false; // wands only live in the quick inventory
|
||||||
|
const Loc cur = where(item);
|
||||||
|
if (same_container(cur, target)) return true; // moving inside a container is always allowed
|
||||||
|
if (to_wand && target.wand == item) return false;
|
||||||
|
if (to_wand && !editing) return false; // a wand takes spells only while the wands may be edited
|
||||||
|
if (preferred(item) == "FULL" && to_full && !is_spell(item)) return true; // (the sampo and other items that live in the full inventory)
|
||||||
|
if (to_full && !editing) return false; // spells only enter the full inventory while editing
|
||||||
|
if (is_spell(item) && to_wand) return !is_inside(item, target.wand); // a wand that is not yet holding it (the container is a wand here)
|
||||||
|
if (is_spell(item) && to_quick) return false; // spells never go into the quick inventory
|
||||||
|
if (is_flask(item) && to_wand) return false;
|
||||||
|
if (!is_spell(item)) { if (to_full) return false; return to_quick; } // other items: the quick inventory only (unless they prefer the full one)
|
||||||
|
return to_full; // spells: the full inventory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free place for an item that is picked up (the engine's pick-up goes to the preferred container of the item): returns the location or {}.
|
||||||
|
Loc free_place(int item) {
|
||||||
|
if (is_spell(item) || preferred(item) == "FULL") { for (int i = 0; i < kFull; ++i) if (!full[i]) return {Full, 0, i}; return {}; }
|
||||||
|
if (is_wand(item)) { for (int i = 0; i < kWands; ++i) if (!wands[i]) return {QuickWand, 0, i}; return {}; }
|
||||||
|
for (int i = 0; i < kItems; ++i) if (!items[i]) return {QuickItem, 0, i};
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
bool can_pick(int item) { return free_place(item).kind != None; }
|
||||||
|
|
||||||
|
// The item enters the inventory at its preferred place (picking it up never needs the editing rights).
|
||||||
|
bool add(int item) {
|
||||||
|
const Loc l = free_place(item);
|
||||||
|
if (l.kind == None) return false;
|
||||||
|
put(item, l);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moves an item to the slot `target`: into an empty slot when the rules allow, onto an occupied slot as a swap when both may go to the other's container.
|
||||||
|
// Returns true when something changed.
|
||||||
|
bool drop_on(int item, const Loc& target) {
|
||||||
|
const Loc from = where(item);
|
||||||
|
if (from.kind == None || target.kind == None || from == target) return false;
|
||||||
|
const int other = at(target);
|
||||||
|
if (!can_drop(item, target)) return false;
|
||||||
|
if (!other) {
|
||||||
|
take(item);
|
||||||
|
put(item, target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (other == item) return false;
|
||||||
|
if (!can_drop(other, from)) return false; // a swap: the other one must be allowed to go where the item came from
|
||||||
|
take(item); take(other);
|
||||||
|
put(item, target);
|
||||||
|
put(other, from);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The item leaves the inventory (thrown, dropped, consumed).
|
||||||
|
void remove(int item) {
|
||||||
|
const Loc l = where(item);
|
||||||
|
take(item);
|
||||||
|
(void)l;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First free spell slot of a wand (-1: full).
|
||||||
|
int free_spell_slot(int wand) {
|
||||||
|
auto s = spells(wand);
|
||||||
|
for (int i = 0; i < (int)s.size(); ++i) if (!s[i]) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Shift-click: a spell of the full inventory goes to the first free slot of `wand` (editing only), a spell of a wand to the first free full slot.
|
||||||
|
bool quick_move(int item, int wand) {
|
||||||
|
const Loc from = where(item);
|
||||||
|
if (from.kind == Full && wand) { const int s = free_spell_slot(wand); return s >= 0 && drop_on(item, {WandSpell, wand, s}); }
|
||||||
|
if (from.kind == WandSpell) { for (int i = 0; i < kFull; ++i) if (!full[i]) return drop_on(item, {Full, 0, i}); }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// after any change of the spells of a wand the gun has to read them again
|
||||||
|
std::function<void(int wand)> on_wand_changed;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string attr(int e, const char* comp, const char* key) {
|
||||||
|
for (int cid : rt_.components(e, comp, "", true)) { RtComponent* c = rt_.comp(cid); auto it = c->attrs.find(key); if (it != c->attrs.end()) return it->second; }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
bool isWandLike(int e) { return is_wand(e); }
|
||||||
|
bool is_inside(int item, int wand) { RtEntity* e = rt_.ent(item); return e && e->parent == wand; }
|
||||||
|
static bool same_container(const Loc& a, const Loc& b) {
|
||||||
|
if (a.kind == None || b.kind == None) return false;
|
||||||
|
const bool qa = a.kind == QuickWand || a.kind == QuickItem, qb = b.kind == QuickWand || b.kind == QuickItem;
|
||||||
|
if (qa && qb) return true;
|
||||||
|
if (a.kind == Full && b.kind == Full) return true;
|
||||||
|
if (a.kind == WandSpell && b.kind == WandSpell) return a.wand == b.wand;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void take(int item) {
|
||||||
|
for (auto& v : wands) if (v == item) v = 0;
|
||||||
|
for (auto& v : items) if (v == item) v = 0;
|
||||||
|
for (auto& v : full) if (v == item) v = 0;
|
||||||
|
if (RtEntity* e = rt_.ent(item)) if (e->parent && is_wand(e->parent) && is_spell(item)) {
|
||||||
|
const int w = e->parent;
|
||||||
|
e->parent = rt_.player();
|
||||||
|
if (on_wand_changed) on_wand_changed(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void put(int item, const Loc& l) {
|
||||||
|
RtEntity* e = rt_.ent(item);
|
||||||
|
switch (l.kind) {
|
||||||
|
case QuickWand: wands[l.index] = item; if (e) e->parent = rt_.player(); set_slot(item, l.index); break;
|
||||||
|
case QuickItem: items[l.index] = item; if (e) e->parent = rt_.player(); set_slot(item, kWands + l.index); break;
|
||||||
|
case Full: full[l.index] = item; if (e) e->parent = rt_.player(); set_slot(item, l.index); break;
|
||||||
|
case WandSpell: if (e) e->parent = l.wand; set_slot(item, l.index); if (on_wand_changed) on_wand_changed(l.wand); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityRuntime& rt_;
|
||||||
|
GunRuntime& gun_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
+13847
File diff suppressed because it is too large.
Load diff
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#pragma once
|
||||||
|
// Always-on frame recorder for slowdowns: the time of every frame split into named CPU sections (cheap clock marks in the frame loop), the GPU
|
||||||
|
// passes (timestamps), and counts of what the world holds; the last 1200 frames are kept. F10 (or NG_PERF_AUTO=ms: any frame slower than that,
|
||||||
|
// at most every 10 s) writes perf/perf_<time>.txt (summary, averages, the slowest frames in full) and perf_<time>.csv (every frame).
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
class PerfRecorder {
|
||||||
|
public:
|
||||||
|
static constexpr int kMaxSections = 32, kGpu = 20, kCounts = 12;
|
||||||
|
struct Frame {
|
||||||
|
int frame = 0;
|
||||||
|
float total = 0, wait = 0, gpu = 0; // whole frame (wall time from one frame to the next), waiting for the GPU / the swap chain, the GPU's own time
|
||||||
|
float sect[kMaxSections] = {};
|
||||||
|
float gpuPass[kGpu] = {};
|
||||||
|
int count[kCounts] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
PerfRecorder() : ring_(1200) {}
|
||||||
|
|
||||||
|
// a section name per id (registered once, in order of first use)
|
||||||
|
int section(const char* name) {
|
||||||
|
for (int i = 0; i < (int)names_.size(); ++i) if (names_[i] == name) return i;
|
||||||
|
if ((int)names_.size() >= kMaxSections) return kMaxSections - 1;
|
||||||
|
names_.push_back(name);
|
||||||
|
return (int)names_.size() - 1;
|
||||||
|
}
|
||||||
|
void set_count_names(std::vector<std::string> n) { countNames_ = std::move(n); }
|
||||||
|
void set_gpu_names(std::vector<std::string> n) { gpuNames_ = std::move(n); }
|
||||||
|
|
||||||
|
// the start of a frame: the previous one is closed
|
||||||
|
void begin(int frame) {
|
||||||
|
const auto now = clk::now();
|
||||||
|
if (open_) {
|
||||||
|
cur_.total = ms(frameT_, now);
|
||||||
|
ring_[head_ % ring_.size()] = cur_;
|
||||||
|
++head_;
|
||||||
|
if (autoMs_ > 0 && cur_.total > autoMs_ && std::chrono::duration<double>(now - lastAuto_).count() > 10.0) { pendingAuto_ = true; lastAuto_ = now; }
|
||||||
|
}
|
||||||
|
cur_ = Frame();
|
||||||
|
cur_.frame = frame;
|
||||||
|
frameT_ = now; markT_ = now; curSect_ = -1; open_ = true;
|
||||||
|
}
|
||||||
|
// from here on the time goes to section `id` (until the next mark)
|
||||||
|
void mark(int id) {
|
||||||
|
const auto now = clk::now();
|
||||||
|
if (curSect_ >= 0 && curSect_ < kMaxSections) cur_.sect[curSect_] += ms(markT_, now);
|
||||||
|
curSect_ = id; markT_ = now;
|
||||||
|
}
|
||||||
|
void wait_ms(float w) { cur_.wait += w; }
|
||||||
|
void gpu_ms(float g) { cur_.gpu = g; }
|
||||||
|
void gpu_pass(int k, float v) { if (k >= 0 && k < kGpu) cur_.gpuPass[k] = v; }
|
||||||
|
void count(int k, int v) { if (k >= 0 && k < kCounts) cur_.count[k] = v; }
|
||||||
|
void set_auto(double msThreshold) { autoMs_ = msThreshold; }
|
||||||
|
bool take_auto() { const bool a = pendingAuto_; pendingAuto_ = false; return a; }
|
||||||
|
|
||||||
|
// the report: returns the path of the text file
|
||||||
|
std::string dump(const std::string& dir, const std::string& context, const char* why) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(dir, ec);
|
||||||
|
std::time_t t = std::time(nullptr);
|
||||||
|
std::tm tm{};
|
||||||
|
localtime_s(&tm, &t);
|
||||||
|
char stamp[32];
|
||||||
|
std::strftime(stamp, sizeof stamp, "%Y%m%d_%H%M%S", &tm);
|
||||||
|
const std::string base = (fs::path(dir) / ("perf_" + std::string(stamp))).string();
|
||||||
|
std::vector<Frame> fr;
|
||||||
|
const size_t n = std::min(head_, ring_.size());
|
||||||
|
for (size_t k = head_ - n; k < head_; ++k) fr.push_back(ring_[k % ring_.size()]);
|
||||||
|
if (FILE* f = std::fopen((base + ".csv").c_str(), "wb")) {
|
||||||
|
std::fprintf(f, "frame,total_ms,wait_ms,gpu_ms");
|
||||||
|
for (const auto& s : names_) std::fprintf(f, ",\"%s\"", s.c_str()); // (quoted: some names hold commas)
|
||||||
|
for (const auto& s : gpuNames_) std::fprintf(f, ",\"gpu %s\"", s.c_str());
|
||||||
|
for (const auto& s : countNames_) std::fprintf(f, ",\"%s\"", s.c_str());
|
||||||
|
std::fprintf(f, "\r\n");
|
||||||
|
for (const Frame& x : fr) {
|
||||||
|
std::fprintf(f, "%d,%.3f,%.3f,%.3f", x.frame, x.total, x.wait, x.gpu);
|
||||||
|
for (size_t s = 0; s < names_.size(); ++s) std::fprintf(f, ",%.3f", x.sect[s]);
|
||||||
|
for (size_t s = 0; s < gpuNames_.size(); ++s) std::fprintf(f, ",%.3f", x.gpuPass[s]);
|
||||||
|
for (size_t s = 0; s < countNames_.size(); ++s) std::fprintf(f, ",%d", x.count[s]);
|
||||||
|
std::fprintf(f, "\r\n");
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
FILE* f = std::fopen((base + ".txt").c_str(), "wb");
|
||||||
|
if (!f) return "";
|
||||||
|
auto line = [&](const Frame& x) {
|
||||||
|
std::fprintf(f, "frame %6d %6.1f ms (waiting for the GPU %.1f, GPU %.1f)\r\n", x.frame, x.total, x.wait, x.gpu);
|
||||||
|
std::vector<std::pair<float, std::string>> v;
|
||||||
|
for (size_t s = 0; s < names_.size(); ++s) if (x.sect[s] >= 0.05f) v.push_back({x.sect[s], names_[s]});
|
||||||
|
std::sort(v.rbegin(), v.rend());
|
||||||
|
std::fprintf(f, " CPU:");
|
||||||
|
for (auto& [m, s] : v) std::fprintf(f, " %s %.2f", s.c_str(), m);
|
||||||
|
std::fprintf(f, "\r\n GPU:");
|
||||||
|
for (size_t s = 0; s < gpuNames_.size(); ++s) if (x.gpuPass[s] >= 0.05f) std::fprintf(f, " %s %.2f", gpuNames_[s].c_str(), x.gpuPass[s]);
|
||||||
|
std::fprintf(f, "\r\n counts:");
|
||||||
|
for (size_t s = 0; s < countNames_.size(); ++s) std::fprintf(f, " %s %d", countNames_[s].c_str(), x.count[s]);
|
||||||
|
std::fprintf(f, "\r\n");
|
||||||
|
};
|
||||||
|
std::fprintf(f, "noita_gpu performance report (%s), %s\r\n%s\r\n\r\n", why, stamp, context.c_str());
|
||||||
|
if (!fr.empty()) {
|
||||||
|
double avg = 0, worst = 0;
|
||||||
|
Frame mean;
|
||||||
|
for (const Frame& x : fr) {
|
||||||
|
avg += x.total; worst = std::max(worst, (double)x.total);
|
||||||
|
mean.wait += x.wait; mean.gpu += x.gpu;
|
||||||
|
for (int s = 0; s < kMaxSections; ++s) mean.sect[s] += x.sect[s];
|
||||||
|
for (int s = 0; s < kGpu; ++s) mean.gpuPass[s] += x.gpuPass[s];
|
||||||
|
}
|
||||||
|
const float k = 1.0f / (float)fr.size();
|
||||||
|
mean.total = (float)(avg * k); mean.wait *= k; mean.gpu *= k;
|
||||||
|
for (float& s : mean.sect) s *= k;
|
||||||
|
for (float& s : mean.gpuPass) s *= k;
|
||||||
|
mean.frame = -1;
|
||||||
|
for (int s = 0; s < kCounts; ++s) mean.count[s] = fr.back().count[s];
|
||||||
|
std::fprintf(f, "%zu frames (%.1f s of frames): average %.1f ms, worst %.1f ms\r\n\r\nAVERAGE (counts: the last frame)\r\n", fr.size(), avg / 1000.0, avg * k, worst);
|
||||||
|
line(mean);
|
||||||
|
std::vector<const Frame*> sorted;
|
||||||
|
for (const Frame& x : fr) sorted.push_back(&x);
|
||||||
|
std::sort(sorted.begin(), sorted.end(), [](const Frame* a, const Frame* b) { return a->total > b->total; });
|
||||||
|
std::fprintf(f, "\r\nTHE 25 SLOWEST FRAMES\r\n");
|
||||||
|
for (size_t q = 0; q < std::min<size_t>(25, sorted.size()); ++q) line(*sorted[q]);
|
||||||
|
std::fprintf(f, "\r\nTHE LAST 60 FRAMES\r\n");
|
||||||
|
for (size_t q = fr.size() > 60 ? fr.size() - 60 : 0; q < fr.size(); ++q) line(fr[q]);
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
return base + ".txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using clk = std::chrono::steady_clock;
|
||||||
|
static float ms(clk::time_point a, clk::time_point b) { return std::chrono::duration<float, std::milli>(b - a).count(); }
|
||||||
|
std::vector<Frame> ring_;
|
||||||
|
size_t head_ = 0;
|
||||||
|
Frame cur_;
|
||||||
|
bool open_ = false, pendingAuto_ = false;
|
||||||
|
int curSect_ = -1;
|
||||||
|
double autoMs_ = 0;
|
||||||
|
clk::time_point frameT_ = clk::now(), markT_ = clk::now(), lastAuto_ = clk::now() - std::chrono::hours(1);
|
||||||
|
std::vector<std::string> names_, gpuNames_, countNames_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
// The command ring between the game (64-bit) and tools/audio_host (32-bit, it loads the game's own FMOD Studio 2.1.5: the Noita DLLs are x86).
|
||||||
|
// One named shared memory block: the game writes commands at `head`, the host reads from `tail`; the host writes back the state of the kept instances.
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace ng::audio_ipc {
|
||||||
|
|
||||||
|
constexpr int kRing = 1024; // commands
|
||||||
|
constexpr int kHandles = 4096; // kept instances (handle % kHandles)
|
||||||
|
constexpr int kName = 120;
|
||||||
|
|
||||||
|
enum Op : int32_t { None = 0, PlayOneShot, Start, Move, SetParam, Stop, StopNow, Listener, SetVolume, Quit, Release };
|
||||||
|
|
||||||
|
struct Cmd {
|
||||||
|
int32_t op = None;
|
||||||
|
int32_t handle = 0;
|
||||||
|
float x = 0, y = 0, z = 0, value = 0;
|
||||||
|
char name[kName] = {}; // event path ("event:/...") or parameter name
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Shared {
|
||||||
|
uint32_t magic; // 'NGAU'
|
||||||
|
volatile int32_t ready; // 1: the host loaded FMOD and the banks, -1: it failed
|
||||||
|
volatile uint32_t head; // written by the game
|
||||||
|
volatile uint32_t tail; // written by the host
|
||||||
|
volatile uint8_t playing[kHandles]; // 1 while the instance of the handle plays (host)
|
||||||
|
char error[256];
|
||||||
|
char events[131072]; // every event path of the loaded banks, one per line (host)
|
||||||
|
Cmd ring[kRing];
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr uint32_t kMagic = 0x5541474Eu;
|
||||||
|
|
||||||
|
} // namespace ng::audio_ipc
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#include "audio/fmod_audio.h"
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#define NOMINMAX
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "audio/audio_ipc.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
using namespace audio_ipc;
|
||||||
|
|
||||||
|
FmodAudio::~FmodAudio() {
|
||||||
|
if (sh_) { push(Quit, 0, 0, 0, 0, ""); UnmapViewOfFile(sh_); }
|
||||||
|
if (map_) CloseHandle(map_);
|
||||||
|
if (process_) CloseHandle(process_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FmodAudio::init(const std::string& game_dir, const std::string& host_exe) {
|
||||||
|
if (GetFileAttributesA(host_exe.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("audio: %s not found, no sound%c", host_exe.c_str(), 10); return false; }
|
||||||
|
char name[64];
|
||||||
|
std::snprintf(name, sizeof name, "ng_audio_%lu", GetCurrentProcessId());
|
||||||
|
map_ = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(Shared), name);
|
||||||
|
if (!map_) return false;
|
||||||
|
sh_ = (Shared*)MapViewOfFile(map_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(Shared));
|
||||||
|
if (!sh_) return false;
|
||||||
|
std::memset((void*)sh_, 0, sizeof(Shared));
|
||||||
|
sh_->magic = kMagic;
|
||||||
|
std::string cmd = "\"" + host_exe + "\" \"" + game_dir + "\" " + name + " " + std::to_string(GetCurrentProcessId());
|
||||||
|
STARTUPINFOA si{}; si.cb = sizeof si;
|
||||||
|
PROCESS_INFORMATION pi{};
|
||||||
|
std::vector<char> buf(cmd.begin(), cmd.end()); buf.push_back(0);
|
||||||
|
if (!CreateProcessA(nullptr, buf.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { std::printf("audio: the host did not start, no sound%c", 10); return false; }
|
||||||
|
process_ = pi.hProcess; CloseHandle(pi.hThread);
|
||||||
|
for (int i = 0; i < 1000 && sh_->ready == 0; ++i) { // (loading the banks' metadata takes a moment)
|
||||||
|
if (WaitForSingleObject(process_, 10) == WAIT_OBJECT_0) break;
|
||||||
|
}
|
||||||
|
if (sh_->ready != 1) { std::printf("audio: no sound (%s)%c", sh_->error[0] ? sh_->error : "the host gave up", 10); return false; }
|
||||||
|
std::printf("audio: the game's FMOD Studio is up (Made with FMOD Studio by Firelight Technologies Pty Ltd.)%c", 10);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FmodAudio::ok() const { return sh_ && sh_->ready == 1; }
|
||||||
|
|
||||||
|
bool FmodAudio::push(int op, int handle, float x, float y, float value, const std::string& n, float z) {
|
||||||
|
if (!ok()) return false;
|
||||||
|
if (sh_->head - sh_->tail >= (uint32_t)kRing) return false; // full: dropped
|
||||||
|
Cmd& c = sh_->ring[sh_->head % kRing];
|
||||||
|
c.op = op; c.handle = handle; c.x = x; c.y = y; c.z = z; c.value = value;
|
||||||
|
std::string path = n;
|
||||||
|
if ((op == PlayOneShot || op == Start) && path.rfind("event:/", 0) != 0) path = "event:/" + path;
|
||||||
|
std::snprintf(c.name, sizeof c.name, "%s", path.c_str());
|
||||||
|
MemoryBarrier();
|
||||||
|
sh_->head = sh_->head + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FmodAudio::play(const std::string& event, float x, float y, float volume) { if (!event.empty()) push(PlayOneShot, 0, x, y, volume, event); }
|
||||||
|
|
||||||
|
int FmodAudio::start(const std::string& event, float x, float y, float volume) {
|
||||||
|
if (event.empty() || !ok()) return 0;
|
||||||
|
const int h = next_handle_++;
|
||||||
|
if (next_handle_ >= kHandles * 1000) next_handle_ = 1;
|
||||||
|
if (!push(Start, h, x, y, volume, event)) return 0;
|
||||||
|
sh_->playing[h % kHandles] = 1;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FmodAudio::move(int handle, float x, float y) { if (handle > 0) push(Move, handle, x, y, 0, ""); }
|
||||||
|
void FmodAudio::set_parameter(int handle, const std::string& name, float value) { if (handle > 0) push(SetParam, handle, 0, 0, value, name); }
|
||||||
|
void FmodAudio::set_volume(int handle, float volume) { if (handle > 0) push(SetVolume, handle, 0, 0, volume, ""); }
|
||||||
|
void FmodAudio::release(int handle) { if (handle > 0) push(Release, handle, 0, 0, 0, ""); }
|
||||||
|
void FmodAudio::stop(int handle, bool immediate) { if (handle > 0) push(immediate ? StopNow : Stop, handle, 0, 0, 0, ""); }
|
||||||
|
bool FmodAudio::playing(int handle) const { return handle > 0 && ok() && sh_->playing[handle % kHandles] != 0; }
|
||||||
|
std::vector<std::string> FmodAudio::events() const {
|
||||||
|
std::vector<std::string> r;
|
||||||
|
if (!ok()) return r;
|
||||||
|
const char* p = sh_->events;
|
||||||
|
while (*p) { const char* e = std::strchr(p, 10); if (!e) break; r.emplace_back(p, e - p); p = e + 1; }
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
void FmodAudio::set_listener(float x, float y, float z) { push(Listener, 0, x, y, 0, "", z); }
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
// Sound through the game's own FMOD Studio (fmodstudio.dll / fmod.dll 2.1.5 of the Noita install). Those DLLs are 32-bit, so they run in audio_host.exe
|
||||||
|
// (tools/audio_host), which this class starts and drives through shared memory (audio_ipc.h). The DLLs are loaded from the Noita folder, never shipped.
|
||||||
|
// Events by the names the engine and the data use: "event:/" + path. Without the host, the DLLs or the banks everything here is silent.
|
||||||
|
// "Made with FMOD Studio by Firelight Technologies Pty Ltd."
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace audio_ipc { struct Shared; }
|
||||||
|
|
||||||
|
class FmodAudio {
|
||||||
|
public:
|
||||||
|
~FmodAudio();
|
||||||
|
// game_dir: the Noita folder (fmodstudio.dll, fmod.dll, data/audio/Desktop/*.bank); host_exe: audio_host.exe
|
||||||
|
bool init(const std::string& game_dir, const std::string& host_exe);
|
||||||
|
bool ok() const; // the host is up and has loaded the banks
|
||||||
|
// a one-shot event at a world position (pixels): "game_effect/teleport/tick" or "event:/game_effect/teleport/tick"
|
||||||
|
void play(const std::string& event, float x, float y, float volume = 1.0f);
|
||||||
|
// a kept instance (loops, sounds that follow an entity): handle > 0
|
||||||
|
int start(const std::string& event, float x, float y, float volume = 1.0f);
|
||||||
|
void move(int handle, float x, float y);
|
||||||
|
void set_parameter(int handle, const std::string& name, float value);
|
||||||
|
void set_volume(int handle, float volume);
|
||||||
|
void stop(int handle, bool immediate = false);
|
||||||
|
void release(int handle); // the instance plays to its end on its own; the handle is free
|
||||||
|
bool playing(int handle) const;
|
||||||
|
void set_listener(float x, float y, float z = 0.0f);
|
||||||
|
std::vector<std::string> events() const; // every event path of the banks
|
||||||
|
private:
|
||||||
|
bool push(int op, int handle, float x, float y, float value, const std::string& name, float z = 0.0f);
|
||||||
|
void* map_ = nullptr;
|
||||||
|
void* process_ = nullptr;
|
||||||
|
audio_ipc::Shared* sh_ = nullptr;
|
||||||
|
int next_handle_ = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
// FastLZ (level 1 and 2) block decompressor, as used by Noita for chunk files and stream info.
|
||||||
|
// The level is in the top 3 bits of the first byte (0 = level 1, 1 = level 2).
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
// Returns false on malformed input or if the output would exceed max_out.
|
||||||
|
inline bool fastlz_decompress(const uint8_t* in, size_t in_len, std::vector<uint8_t>& out, size_t max_out) {
|
||||||
|
out.clear();
|
||||||
|
if (in_len == 0) return true;
|
||||||
|
const int level = (in[0] >> 5) + 1;
|
||||||
|
if (level != 1 && level != 2) return false;
|
||||||
|
size_t ip = 0;
|
||||||
|
uint32_t ctrl = in[ip++] & 31;
|
||||||
|
for (;;) {
|
||||||
|
if (ctrl >= 32) {
|
||||||
|
uint32_t len = (ctrl >> 5) - 1;
|
||||||
|
uint32_t ofs = (ctrl & 31) << 8;
|
||||||
|
if (len == 6) {
|
||||||
|
uint8_t code;
|
||||||
|
do {
|
||||||
|
if (ip >= in_len) return false;
|
||||||
|
code = in[ip++];
|
||||||
|
len += code;
|
||||||
|
} while (code == 255 && level == 2);
|
||||||
|
if (level == 1 && false) return false;
|
||||||
|
}
|
||||||
|
if (ip >= in_len) return false;
|
||||||
|
const uint8_t code = in[ip++];
|
||||||
|
ofs += code;
|
||||||
|
size_t back;
|
||||||
|
if (level == 2 && code == 255 && (ctrl & 31) == 31) {
|
||||||
|
if (ip + 1 >= in_len) return false;
|
||||||
|
ofs = ((uint32_t)in[ip] << 8) + in[ip + 1];
|
||||||
|
ip += 2;
|
||||||
|
back = (size_t)ofs + 8191 + 1; // 16-bit distance: MAX_L2_DISTANCE + 1 (the +1 is easy to miss)
|
||||||
|
} else {
|
||||||
|
back = (size_t)ofs + 1;
|
||||||
|
}
|
||||||
|
if (back > out.size()) return false;
|
||||||
|
len += 3;
|
||||||
|
if (out.size() + len > max_out) return false;
|
||||||
|
size_t ref = out.size() - back;
|
||||||
|
for (uint32_t k = 0; k < len; ++k) out.push_back(out[ref + k]);
|
||||||
|
} else {
|
||||||
|
const size_t n = (size_t)ctrl + 1;
|
||||||
|
if (ip + n > in_len || out.size() + n > max_out) return false;
|
||||||
|
out.insert(out.end(), in + ip, in + ip + n);
|
||||||
|
ip += n;
|
||||||
|
}
|
||||||
|
if (ip >= in_len) break;
|
||||||
|
ctrl = in[ip++];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#include "core/materials.h"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "core/xml.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
uint32_t parse_hex_color(const std::string& s) {
|
||||||
|
return (uint32_t)std::strtoul(s.c_str(), nullptr, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
CellType parse_type(const std::string& s) {
|
||||||
|
if (s == "liquid") return CellType::Liquid;
|
||||||
|
if (s == "gas") return CellType::Gas;
|
||||||
|
if (s == "solid") return CellType::Solid;
|
||||||
|
if (s == "fire") return CellType::Fire;
|
||||||
|
return CellType::Air;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "[fire],[hot]" -> {"fire","hot"}
|
||||||
|
std::vector<std::string> parse_tags(const std::string& s) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string cur;
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '[') cur.clear();
|
||||||
|
else if (c == ']') out.push_back(cur);
|
||||||
|
else cur += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_attrs(Material& m, const XmlNode& n) {
|
||||||
|
std::set<std::string> seen; // a duplicated attribute keeps its first occurrence (00dde120)
|
||||||
|
for (auto& [k, v] : n.attrs) {
|
||||||
|
if (k == "name" || k == "_parent" || k == "_inherit_reactions") continue;
|
||||||
|
if (!seen.insert(k).second) continue;
|
||||||
|
m.attrs[k] = v;
|
||||||
|
}
|
||||||
|
if (auto* v = n.attr("cell_type")) m.type = parse_type(*v);
|
||||||
|
if (auto* v = n.attr("wang_color")) m.wang_color = parse_hex_color(*v);
|
||||||
|
if (auto* v = n.attr("tags")) m.tags = parse_tags(*v);
|
||||||
|
if (auto* g = n.child("Graphics"))
|
||||||
|
if (auto* c = g->attr("color")) m.graphics_color = parse_hex_color(*c);
|
||||||
|
if (auto* g = n.child("Graphics"))
|
||||||
|
if (auto* t = g->attr("texture_file")) m.graphics_texture = *t;
|
||||||
|
if (auto* se = n.child("StatusEffects")) { // <Ingestion><StatusEffect type amount/>: kept as status.ingestion = "TYPE:amount;TYPE:amount" (the stains come from the status_effects attribute and liquid_stains)
|
||||||
|
for (auto& ch : se->children) {
|
||||||
|
if (ch->name != "Ingestion" && ch->name != "Stains") continue;
|
||||||
|
std::string list;
|
||||||
|
for (auto& e : ch->children) if (e->name == "StatusEffect") list += e->attr_or("type") + ":" + e->attr_or("amount", "1") + ";";
|
||||||
|
m.attrs[ch->name == "Ingestion" ? "status.ingestion" : "status.stains"] = list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto* x = n.child("ExplosionConfig")) // the explosion of a burning cell or a solid body of this material: kept as explosion.<attribute>
|
||||||
|
for (auto& [k, v] : x->attrs) m.attrs["explosion." + k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void MaterialDb::load(std::string_view xml_text) {
|
||||||
|
auto root = parse_xml(xml_text);
|
||||||
|
if (root->name != "Materials") throw std::runtime_error("materials: root is not <Materials>");
|
||||||
|
|
||||||
|
// Pass 1: index materials in file order (children may precede their parent).
|
||||||
|
std::vector<const XmlNode*> nodes;
|
||||||
|
for (auto& c : root->children) {
|
||||||
|
if (c->name != "CellData" && c->name != "CellDataChild") continue;
|
||||||
|
std::string name = c->attr_or("name");
|
||||||
|
if (name.empty()) continue;
|
||||||
|
if (auto dup = by_name_.find(name); dup != by_name_.end()) { nodes[dup->second] = c.get(); continue; } // (a mod's file defines it again: the later definition wins)
|
||||||
|
Material m;
|
||||||
|
m.name = std::move(name);
|
||||||
|
m.index = (int)mats_.size();
|
||||||
|
by_name_[m.name] = m.index;
|
||||||
|
mats_.push_back(std::move(m));
|
||||||
|
nodes.push_back(c.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: resolve inheritance depth-first.
|
||||||
|
enum : uint8_t { Todo, Busy, Done };
|
||||||
|
std::vector<uint8_t> state(mats_.size(), Todo);
|
||||||
|
auto resolve = [&](auto&& self, int idx) -> void {
|
||||||
|
if (state[idx] == Done) return;
|
||||||
|
if (state[idx] == Busy) throw std::runtime_error("materials: parent cycle at " + mats_[idx].name);
|
||||||
|
state[idx] = Busy;
|
||||||
|
Material& m = mats_[idx];
|
||||||
|
const XmlNode& n = *nodes[idx];
|
||||||
|
if (n.name == "CellDataChild") {
|
||||||
|
m.parent = n.attr_or("_parent");
|
||||||
|
auto it = by_name_.find(m.parent);
|
||||||
|
if (it == by_name_.end())
|
||||||
|
throw std::runtime_error("materials: " + m.name + " has unknown parent " + m.parent);
|
||||||
|
self(self, it->second);
|
||||||
|
const Material& p = mats_[it->second];
|
||||||
|
m.type = p.type;
|
||||||
|
m.wang_color = p.wang_color;
|
||||||
|
m.graphics_color = p.graphics_color;
|
||||||
|
m.graphics_texture = p.graphics_texture;
|
||||||
|
m.tags = p.tags;
|
||||||
|
m.attrs = p.attrs;
|
||||||
|
}
|
||||||
|
apply_attrs(m, n);
|
||||||
|
state[idx] = Done;
|
||||||
|
};
|
||||||
|
for (int i = 0; i < (int)mats_.size(); ++i) resolve(resolve, i);
|
||||||
|
for (auto& m : mats_)
|
||||||
|
if (m.wang_color) by_wang_.emplace(m.wang_color, m.index);
|
||||||
|
|
||||||
|
for (auto& c : root->children) {
|
||||||
|
if (c->name == "Reaction") {
|
||||||
|
Reaction r;
|
||||||
|
r.probability = (float)std::atof(c->attr_or("probability", "0").c_str());
|
||||||
|
std::set<std::string> seenKeys;
|
||||||
|
for (auto& [k, v] : c->attrs) {
|
||||||
|
if (!seenKeys.insert(k).second) continue; // first occurrence wins
|
||||||
|
if (k.size() == 11 && k.compare(0, 10, "input_cell") == 0 && k[10] >= '1' && k[10] <= '3')
|
||||||
|
r.in[k[10] - '1'] = v;
|
||||||
|
else if (k.size() == 12 && k.compare(0, 11, "output_cell") == 0 && k[11] >= '1' && k[11] <= '3')
|
||||||
|
r.out[k[11] - '1'] = v;
|
||||||
|
else if (k != "probability")
|
||||||
|
r.attrs[k] = v;
|
||||||
|
}
|
||||||
|
reactions_.push_back(std::move(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialDb::add_loose_twins() {
|
||||||
|
const size_t n = mats_.size();
|
||||||
|
loose_twin_.assign(n, -1);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
if (mats_[i].type != CellType::Liquid || attr_int(mats_[i], "liquid_static", 0) != 1 || attr_int(mats_[i], "liquid_sand", 0) != 1) continue;
|
||||||
|
Material t = mats_[i];
|
||||||
|
t.name += "#loose";
|
||||||
|
t.attrs["liquid_static"] = "0";
|
||||||
|
t.index = (int)mats_.size();
|
||||||
|
by_name_[t.name] = t.index;
|
||||||
|
loose_twin_[i] = t.index;
|
||||||
|
mats_.push_back(std::move(t));
|
||||||
|
}
|
||||||
|
loose_twin_.resize(mats_.size(), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Material* MaterialDb::find(std::string_view name) const {
|
||||||
|
auto it = by_name_.find(std::string(name));
|
||||||
|
return it == by_name_.end() ? nullptr : &mats_[it->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Material* MaterialDb::by_wang_color(uint32_t argb) const {
|
||||||
|
auto it = by_wang_.find(argb);
|
||||||
|
return it == by_wang_.end() ? nullptr : &mats_[it->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
float attr_float(const Material& m, const char* key, float def) {
|
||||||
|
auto it = m.attrs.find(key);
|
||||||
|
return it == m.attrs.end() ? def : (float)std::atof(it->second.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int attr_int(const Material& m, const char* key, int def) {
|
||||||
|
auto it = m.attrs.find(key);
|
||||||
|
return it == m.attrs.end() ? def : std::atoi(it->second.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
SimParams sim_params(const Material& m) {
|
||||||
|
SimParams p; // member initialisers are the engine defaults
|
||||||
|
p.density = attr_float(m, "density", p.density);
|
||||||
|
p.gravity = attr_float(m, "liquid_gravity", p.gravity);
|
||||||
|
p.viscosity = attr_int(m, "liquid_viscosity", p.viscosity);
|
||||||
|
p.damping = attr_float(m, "liquid_damping", p.damping);
|
||||||
|
p.flow_speed = attr_float(m, "liquid_flow_speed", p.flow_speed);
|
||||||
|
p.gas_speed = attr_int(m, "gas_speed", p.gas_speed);
|
||||||
|
p.gas_up = attr_int(m, "gas_upwards_speed", p.gas_up);
|
||||||
|
p.gas_horizontal = attr_int(m, "gas_horizontal_speed", p.gas_horizontal);
|
||||||
|
p.gas_down = attr_int(m, "gas_downwards_speed", p.gas_down);
|
||||||
|
p.lifetime = attr_int(m, "lifetime", p.lifetime);
|
||||||
|
p.sand = attr_int(m, "liquid_sand", 0) != 0;
|
||||||
|
p.is_static = m.type == CellType::Solid || attr_int(m, "liquid_static", 0) == 1;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
enum class CellType : uint8_t { Air, Liquid, Gas, Solid, Fire };
|
||||||
|
|
||||||
|
struct Reaction {
|
||||||
|
float probability = 0; // 0..100
|
||||||
|
std::string in[3]; // input_cell1..3 (third optional)
|
||||||
|
std::string out[3]; // output_cell1..3
|
||||||
|
std::map<std::string, std::string> attrs; // everything else, verbatim
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Material {
|
||||||
|
std::string name;
|
||||||
|
std::string parent; // empty if root
|
||||||
|
CellType type = CellType::Air;
|
||||||
|
uint32_t wang_color = 0; // ARGB, key used by world-gen tiles
|
||||||
|
uint32_t graphics_color = 0; // ARGB from <Graphics color>
|
||||||
|
std::string graphics_texture; // <Graphics texture_file>, empty if none
|
||||||
|
std::vector<std::string> tags;
|
||||||
|
std::map<std::string, std::string> attrs; // merged along the parent chain
|
||||||
|
int index = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MaterialDb {
|
||||||
|
public:
|
||||||
|
// Parses data/materials.xml text. Throws on malformed input.
|
||||||
|
void load(std::string_view xml_text);
|
||||||
|
|
||||||
|
const std::vector<Material>& materials() const { return mats_; }
|
||||||
|
const std::vector<Reaction>& reactions() const { return reactions_; }
|
||||||
|
const Material* find(std::string_view name) const;
|
||||||
|
const Material* by_wang_color(uint32_t argb) const;
|
||||||
|
// LooseGroundComponent (00ba5f20) clears the static flag of single cells of a static powder (rock: cell_type liquid, liquid_static, liquid_sand), which then fall
|
||||||
|
// like sand. The cells here have no flag of their own, so every such material gets a twin ("<name>#loose", liquid_static 0, not in the wang colours) for them.
|
||||||
|
void add_loose_twins();
|
||||||
|
int loose_twin(int index) const { return index >= 0 && index < (int)loose_twin_.size() ? loose_twin_[index] : -1; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Material> mats_;
|
||||||
|
std::vector<Reaction> reactions_;
|
||||||
|
std::vector<int> loose_twin_;
|
||||||
|
std::unordered_map<std::string, int> by_name_;
|
||||||
|
std::unordered_map<uint32_t, int> by_wang_;
|
||||||
|
};
|
||||||
|
|
||||||
|
float attr_float(const Material& m, const char* key, float def = 0);
|
||||||
|
int attr_int(const Material& m, const char* key, int def = 0);
|
||||||
|
|
||||||
|
// Simulation parameters of a material with the engine's default values filled in
|
||||||
|
// (defaults from the CellData constructor, noita.exe FUN_004ac7e0; see ENGINE_ANALYSIS.md).
|
||||||
|
struct SimParams {
|
||||||
|
float density = 1.0f;
|
||||||
|
float gravity = 0.5f; // liquid_gravity
|
||||||
|
int viscosity = 50; // liquid_viscosity, 0..100: chance (%) of a diagonal slide when blocked
|
||||||
|
float damping = 0.8f; // liquid_damping
|
||||||
|
float flow_speed = 0.9f; // liquid_flow_speed
|
||||||
|
int gas_speed = 50; // chance (%) a gas acts in a frame (CellData constructor 004ac7e0: the word 0x5a646432 at +0x194 = 50, 100, 100, 90 for these four)
|
||||||
|
int gas_up = 100; // gas_upwards_speed
|
||||||
|
int gas_horizontal = 100; // gas_horizontal_speed
|
||||||
|
int gas_down = 90; // gas_downwards_speed
|
||||||
|
int lifetime = 0; // frames, 0 = unlimited
|
||||||
|
bool sand = false; // liquid_sand: powder
|
||||||
|
bool is_static = false; // solid cell type or liquid_static
|
||||||
|
};
|
||||||
|
SimParams sim_params(const Material& m);
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
// parallel_for with a process-wide limit on the helper threads (areas are generated on several threads, each of them may split its own work).
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
inline std::atomic<int>& parallel_helpers() { static std::atomic<int> n{0}; return n; }
|
||||||
|
|
||||||
|
// Runs fn(i) for i in [0, n); the calling thread takes part, helpers join while fewer than hardware_concurrency are busy.
|
||||||
|
template <class F>
|
||||||
|
void parallel_for(size_t n, F&& fn) {
|
||||||
|
if (n == 0) return;
|
||||||
|
const int limit = (int)std::max(2u, std::thread::hardware_concurrency()) - 1;
|
||||||
|
std::atomic<size_t> next{0};
|
||||||
|
auto work = [&] { for (size_t i; (i = next++) < n;) fn(i); };
|
||||||
|
std::vector<std::thread> th;
|
||||||
|
const size_t want = std::min<size_t>(n, 16) - 1;
|
||||||
|
for (size_t k = 0; k < want; ++k) {
|
||||||
|
if (parallel_helpers().fetch_add(1) >= limit) { parallel_helpers().fetch_sub(1); break; }
|
||||||
|
th.emplace_back([&] { work(); parallel_helpers().fetch_sub(1); });
|
||||||
|
}
|
||||||
|
work();
|
||||||
|
for (auto& t : th) t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
// .plz images (pixel scenes of the spliced scenes): six little-endian words (version 1, width, height, bytes per pixel 4, compressed size, raw size),
|
||||||
|
// then one FastLZ stream with the RGBA8 pixels (rows top to bottom).
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/fastlz.h"
|
||||||
|
#include "world/wang_gen.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
// Reads only the size of a .plz image.
|
||||||
|
inline bool plz_size(const uint8_t* d, size_t n, int& w, int& h) {
|
||||||
|
if (n < 24) return false;
|
||||||
|
uint32_t v[6];
|
||||||
|
std::memcpy(v, d, 24);
|
||||||
|
if (v[0] != 1 || v[3] != 4 || v[1] == 0 || v[2] == 0 || v[1] > 8192 || v[2] > 8192) return false;
|
||||||
|
w = (int)v[1]; h = (int)v[2];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool decode_plz(const uint8_t* d, size_t n, Image& out) {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
if (!plz_size(d, n, w, h)) return false;
|
||||||
|
uint32_t v[6];
|
||||||
|
std::memcpy(v, d, 24);
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (!fastlz_decompress(d + 24, std::min<size_t>(n - 24, v[4]), raw, v[5])) return false;
|
||||||
|
raw.resize((size_t)w * h * 4, 0); // a stream that stops early leaves the rest transparent
|
||||||
|
out.width = w; out.height = h;
|
||||||
|
out.rgba = std::move(raw);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
// Tiny PNG writer (stored/uncompressed deflate) for debug screenshots.
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
inline uint32_t png_crc(const uint8_t* p, size_t n, uint32_t crc = 0xFFFFFFFFu) {
|
||||||
|
static uint32_t table[256];
|
||||||
|
static bool init = false;
|
||||||
|
if (!init) {
|
||||||
|
for (uint32_t i = 0; i < 256; ++i) {
|
||||||
|
uint32_t c = i;
|
||||||
|
for (int k = 0; k < 8; ++k) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
|
||||||
|
table[i] = c;
|
||||||
|
}
|
||||||
|
init = true;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < n; ++i) crc = table[(crc ^ p[i]) & 0xFF] ^ (crc >> 8);
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rgb: w*h*3 bytes, top row first. Returns false on I/O failure.
|
||||||
|
inline bool write_png_rgb(const std::string& path, const uint8_t* rgb, int w, int h) {
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
raw.reserve((size_t)h * (w * 3 + 1));
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
raw.push_back(0); // filter: none
|
||||||
|
raw.insert(raw.end(), rgb + (size_t)y * w * 3, rgb + (size_t)(y + 1) * w * 3);
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> z = {0x78, 0x01};
|
||||||
|
for (size_t pos = 0; pos < raw.size() || pos == 0;) {
|
||||||
|
size_t n = std::min<size_t>(65535, raw.size() - pos);
|
||||||
|
bool last = pos + n >= raw.size();
|
||||||
|
z.push_back(last ? 1 : 0);
|
||||||
|
z.push_back((uint8_t)(n & 0xFF)); z.push_back((uint8_t)(n >> 8));
|
||||||
|
z.push_back((uint8_t)(~n & 0xFF)); z.push_back((uint8_t)((~n >> 8) & 0xFF));
|
||||||
|
z.insert(z.end(), raw.begin() + pos, raw.begin() + pos + n);
|
||||||
|
pos += n;
|
||||||
|
if (last) break;
|
||||||
|
}
|
||||||
|
uint32_t a = 1, b = 0;
|
||||||
|
for (uint8_t v : raw) { a = (a + v) % 65521; b = (b + a) % 65521; }
|
||||||
|
uint32_t ad = (b << 16) | a;
|
||||||
|
for (int s = 24; s >= 0; s -= 8) z.push_back((ad >> s) & 0xFF);
|
||||||
|
|
||||||
|
FILE* f = nullptr;
|
||||||
|
if (fopen_s(&f, path.c_str(), "wb") != 0 || !f) return false;
|
||||||
|
auto be32 = [](std::vector<uint8_t>& v, uint32_t x) { for (int s = 24; s >= 0; s -= 8) v.push_back((x >> s) & 0xFF); };
|
||||||
|
auto chunk = [&](const char* type, const std::vector<uint8_t>& data) {
|
||||||
|
std::vector<uint8_t> c;
|
||||||
|
be32(c, (uint32_t)data.size());
|
||||||
|
c.insert(c.end(), type, type + 4);
|
||||||
|
c.insert(c.end(), data.begin(), data.end());
|
||||||
|
uint32_t crc = ~png_crc(c.data() + 4, c.size() - 4);
|
||||||
|
be32(c, crc);
|
||||||
|
fwrite(c.data(), 1, c.size(), f);
|
||||||
|
};
|
||||||
|
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
|
||||||
|
fwrite(sig, 1, 8, f);
|
||||||
|
std::vector<uint8_t> ihdr;
|
||||||
|
be32(ihdr, w); be32(ihdr, h);
|
||||||
|
ihdr.insert(ihdr.end(), {8, 2, 0, 0, 0});
|
||||||
|
chunk("IHDR", ihdr);
|
||||||
|
chunk("IDAT", z);
|
||||||
|
chunk("IEND", {});
|
||||||
|
return fclose(f) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#pragma once
|
||||||
|
// Noita's PRNG (Lehmer / MINSTD variant stored in a double) and the seeding
|
||||||
|
// scheme behind SetRandomSeed/Random. Ported from the reverse-engineered
|
||||||
|
// implementation in TwoAbove/noita-tools (MIT), src/services/SeedInfo/wasm/core/nolla_prng.zig.
|
||||||
|
// To be cross-checked against noita.exe.
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
constexpr uint32_t kHalf = 0x80000000u;
|
||||||
|
constexpr int32_t kModulus = 0x7fffffff;
|
||||||
|
constexpr int32_t kMultiplier = 0x41a7;
|
||||||
|
constexpr int32_t kDivisor = 0x1f31d;
|
||||||
|
|
||||||
|
inline uint32_t trunc_double_to_u32(double v) { return (uint32_t)(uint64_t)(int64_t)v; }
|
||||||
|
|
||||||
|
inline uint32_t seed_helper(double v) {
|
||||||
|
const bool finite = std::isfinite(v);
|
||||||
|
if (!finite || !(-9.223372036854776e18 <= v && v < 9.223372036854776e18)) return 0;
|
||||||
|
return trunc_double_to_u32(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline uint32_t seed_hash(uint32_t a, uint32_t b, uint32_t ws) {
|
||||||
|
uint32_t v2 = ((a - b) - ws) ^ (ws >> 0xd);
|
||||||
|
uint32_t v1 = ((b - v2) - ws) ^ (v2 << 8);
|
||||||
|
uint32_t v3 = ((ws - v2) - v1) ^ (v1 >> 0xd);
|
||||||
|
v2 = ((v2 - v1) - v3) ^ (v3 >> 0xc);
|
||||||
|
v1 = ((v1 - v2) - v3) ^ (v2 << 0x10);
|
||||||
|
v3 = ((v3 - v2) - v1) ^ (v1 >> 5);
|
||||||
|
v2 = ((v2 - v1) - v3) ^ (v3 >> 3);
|
||||||
|
v1 = ((v1 - v2) - v3) ^ (v2 << 10);
|
||||||
|
return ((v3 - v2) - v1) ^ (v1 >> 0xf);
|
||||||
|
}
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
// Banker's rounding on f32, as the game's Random(min,max) bindings do.
|
||||||
|
inline float round_half_even_f32(float v) {
|
||||||
|
const float fl = std::floor(v);
|
||||||
|
const float diff = v - fl;
|
||||||
|
if (diff < 0.5f) return fl;
|
||||||
|
if (diff > 0.5f) return fl + 1.0f;
|
||||||
|
return (((int64_t)fl) % 2 == 0) ? fl : fl + 1.0f;
|
||||||
|
}
|
||||||
|
inline int32_t round_half_even_i32(double v) { return (int32_t)round_half_even_f32((float)v); }
|
||||||
|
|
||||||
|
class NollaPrng {
|
||||||
|
public:
|
||||||
|
double seed = 0;
|
||||||
|
|
||||||
|
NollaPrng() = default;
|
||||||
|
// Matches NewNollaPrng: stores the seed, then advances once.
|
||||||
|
explicit NollaPrng(double s) : seed(s) { next(); }
|
||||||
|
|
||||||
|
double next() {
|
||||||
|
using namespace detail;
|
||||||
|
const int32_t si = (int32_t)seed;
|
||||||
|
// wrapping 32-bit arithmetic, as in the original
|
||||||
|
int32_t v = (int32_t)((uint32_t)kMultiplier * (uint32_t)si) -
|
||||||
|
(int32_t)((uint32_t)kModulus * (uint32_t)(si / kDivisor));
|
||||||
|
if (v <= 0) v += kModulus;
|
||||||
|
seed = (double)v;
|
||||||
|
return seed / (double)kModulus;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t next_u() {
|
||||||
|
next();
|
||||||
|
return (uint32_t)(seed * 4.656612875e-10 * 2147483645.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_from_world_seed(uint32_t s) {
|
||||||
|
seed = (double)s;
|
||||||
|
if (2147483647.0 <= seed) seed = (double)s * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRandomSeed(x, y) for a given world seed.
|
||||||
|
void set_random_seed(uint32_t ws, double x, double y) {
|
||||||
|
using namespace detail;
|
||||||
|
const uint32_t a = ws ^ 0x93262e6fu;
|
||||||
|
const uint32_t b = a & 0xfff;
|
||||||
|
const uint32_t c = (a >> 0xc) & 0xfff;
|
||||||
|
|
||||||
|
const double xa = x + (double)b;
|
||||||
|
double ya = y + (double)c;
|
||||||
|
|
||||||
|
double material = xa * 134217727.0;
|
||||||
|
const uint32_t e = seed_helper(material);
|
||||||
|
|
||||||
|
if (std::fabs(ya) >= 102400.0 || std::fabs(xa) <= 1.0) {
|
||||||
|
material = ya * 134217727.0;
|
||||||
|
} else {
|
||||||
|
double yw = ya * 3483.328;
|
||||||
|
yw += (double)e;
|
||||||
|
ya *= yw;
|
||||||
|
material = ya;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t f = seed_helper(material);
|
||||||
|
const uint32_t g = seed_hash(e, f, ws);
|
||||||
|
|
||||||
|
static const uint32_t diddle[17] = {0, 4, 6, 25, 12, 39, 52, 9, 21, 64, 78, 92, 104, 118, 18, 32, 44};
|
||||||
|
constexpr uint32_t magic = 252645135u;
|
||||||
|
|
||||||
|
uint32_t t = g;
|
||||||
|
t = t + (uint32_t)(g < kHalf) + (uint32_t)(g == 0);
|
||||||
|
t -= g / magic;
|
||||||
|
t += (uint32_t)((g % magic < diddle[g / magic]) && (g < 0xc3c3c3c3u + 4 || g >= 0xc3c3c3c3u + 62));
|
||||||
|
t = (t + (uint32_t)(g > kHalf)) >> 1;
|
||||||
|
t += (uint32_t)(g == 0xffffffffu);
|
||||||
|
|
||||||
|
seed = (double)t;
|
||||||
|
next();
|
||||||
|
for (uint32_t h = ws & 3; h != 0; --h) next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random(min, max): inclusive integer range.
|
||||||
|
int32_t random(int32_t lo, int32_t hi) {
|
||||||
|
const double range = (double)(hi + 1 - lo);
|
||||||
|
return lo + (int32_t)(range * next());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global-state wrapper mirroring the Lua API (SetWorldSeed / SetRandomSeed / Random*).
|
||||||
|
class GameRandom {
|
||||||
|
public:
|
||||||
|
void set_world_seed(uint32_t s) { world_seed_ = s; }
|
||||||
|
uint32_t world_seed() const { return world_seed_; }
|
||||||
|
void set_random_seed(double x, double y) { rng_.set_random_seed(world_seed_, x, y); }
|
||||||
|
int32_t random(int32_t lo, int32_t hi) { return rng_.random(lo, hi); }
|
||||||
|
float randomf() { return (float)rng_.next(); }
|
||||||
|
float procedural_randomf(double x, double y, double lo, double hi) {
|
||||||
|
rng_.set_random_seed(world_seed_, x, y);
|
||||||
|
return (float)(lo + (hi - lo) * rng_.next());
|
||||||
|
}
|
||||||
|
int32_t procedural_randomi(double x, double y, double lo, double hi) {
|
||||||
|
rng_.set_random_seed(world_seed_, x, y);
|
||||||
|
return rng_.random(round_half_even_i32(lo), round_half_even_i32(hi));
|
||||||
|
}
|
||||||
|
NollaPrng& prng() { return rng_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t world_seed_ = 0;
|
||||||
|
NollaPrng rng_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#include <cctype>
|
||||||
|
#include "core/wak.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct FileCloser { void operator()(FILE* f) const { if (f) std::fclose(f); } };
|
||||||
|
using FilePtr = std::unique_ptr<FILE, FileCloser>;
|
||||||
|
|
||||||
|
FilePtr open_file(const std::filesystem::path& p) {
|
||||||
|
FILE* f = nullptr;
|
||||||
|
#ifdef _WIN32
|
||||||
|
_wfopen_s(&f, p.c_str(), L"rb");
|
||||||
|
#else
|
||||||
|
f = std::fopen(p.c_str(), "rb");
|
||||||
|
#endif
|
||||||
|
if (!f) throw std::runtime_error("cannot open " + p.string());
|
||||||
|
return FilePtr(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t kMaxPath = 4096;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Wak::Wak(const std::filesystem::path& file) : file_(file) {
|
||||||
|
FilePtr f = open_file(file);
|
||||||
|
auto rd32 = [&]() {
|
||||||
|
uint32_t v;
|
||||||
|
if (std::fread(&v, 4, 1, f.get()) != 1) throw std::runtime_error("wak: truncated header/TOC");
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
rd32(); // version
|
||||||
|
const uint32_t count = rd32();
|
||||||
|
rd32(); // toc_size
|
||||||
|
rd32(); // reserved
|
||||||
|
|
||||||
|
entries_.reserve(count);
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
WakEntry e;
|
||||||
|
e.offset = rd32();
|
||||||
|
e.length = rd32();
|
||||||
|
const uint32_t n = rd32();
|
||||||
|
if (n > kMaxPath) throw std::runtime_error("wak: corrupt path length");
|
||||||
|
e.path.resize(n);
|
||||||
|
if (n && std::fread(e.path.data(), 1, n, f.get()) != n) throw std::runtime_error("wak: truncated path");
|
||||||
|
index_.emplace(e.path, entries_.size());
|
||||||
|
{ std::string l = e.path; for (char& c : l) c = (char)std::tolower((unsigned char)c); lower_.emplace(std::move(l), entries_.size()); }
|
||||||
|
entries_.push_back(std::move(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WakEntry* Wak::find(std::string_view path) const {
|
||||||
|
auto it = index_.find(std::string(path));
|
||||||
|
if (it != index_.end()) return &entries_[it->second];
|
||||||
|
std::string l(path);
|
||||||
|
for (char& c : l) c = (char)std::tolower((unsigned char)c);
|
||||||
|
it = lower_.find(l);
|
||||||
|
return it == lower_.end() ? nullptr : &entries_[it->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> Wak::read(const WakEntry& e) const {
|
||||||
|
FilePtr f = open_file(file_);
|
||||||
|
std::vector<uint8_t> buf(e.length);
|
||||||
|
if (_fseeki64(f.get(), e.offset, SEEK_SET) != 0 ||
|
||||||
|
(e.length && std::fread(buf.data(), 1, e.length, f.get()) != e.length))
|
||||||
|
throw std::runtime_error("wak: failed reading " + e.path);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string lower_path(std::string_view p) { std::string l(p); for (char& c : l) c = (char)std::tolower((unsigned char)c); return l; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wak::set_override(std::string_view path, std::vector<uint8_t> bytes) const { overrides_[lower_path(path)] = std::move(bytes); }
|
||||||
|
bool Wak::overridden(std::string_view path) const { return overrides_.count(lower_path(path)) != 0; }
|
||||||
|
|
||||||
|
// Loose files beside the archive: the mods (mods/<id>/...) and the files of data/ the archive does not hold (the install ships data/fonts/font_damage.xml,
|
||||||
|
// the translations... next to data/data.wak). The root is the archive's folder; for data/ also its parent when the archive itself sits in data/.
|
||||||
|
static bool loose_candidate(const std::filesystem::path& root, std::string_view path, std::filesystem::path& out) {
|
||||||
|
if (root.empty()) return false;
|
||||||
|
const std::string lp = lower_path(path);
|
||||||
|
const bool mods = lp.rfind("mods/", 0) == 0, data = lp.rfind("data/", 0) == 0;
|
||||||
|
if (!mods && !data) return false;
|
||||||
|
std::error_code ec;
|
||||||
|
out = root / std::filesystem::path(std::string(path));
|
||||||
|
if (std::filesystem::is_regular_file(out, ec)) return true;
|
||||||
|
if (data && root.filename() == "data") { out = root.parent_path() / std::filesystem::path(std::string(path)); return std::filesystem::is_regular_file(out, ec); }
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Wak::read_loose(std::string_view path, std::vector<uint8_t>& out) const {
|
||||||
|
std::filesystem::path p;
|
||||||
|
if (!loose_candidate(loose_root_, path, p)) return false;
|
||||||
|
FILE* f = nullptr;
|
||||||
|
if (_wfopen_s(&f, p.wstring().c_str(), L"rb") != 0 || !f) return false;
|
||||||
|
std::fseek(f, 0, SEEK_END); const long n = std::ftell(f); std::fseek(f, 0, SEEK_SET);
|
||||||
|
out.resize(n > 0 ? (size_t)n : 0);
|
||||||
|
const bool ok = n <= 0 || std::fread(out.data(), 1, (size_t)n, f) == (size_t)n;
|
||||||
|
std::fclose(f);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Wak::exists(std::string_view path) const {
|
||||||
|
if (overrides_.count(lower_path(path)) || find(path)) return true;
|
||||||
|
std::filesystem::path p;
|
||||||
|
std::error_code ec;
|
||||||
|
return loose_candidate(loose_root_, path, p) && std::filesystem::is_regular_file(p, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Wak::read(std::string_view path, std::vector<uint8_t>& out) const {
|
||||||
|
if (!overrides_.empty()) { auto it = overrides_.find(lower_path(path)); if (it != overrides_.end()) { out = it->second; return true; } }
|
||||||
|
const WakEntry* e = find(path);
|
||||||
|
if (!e) return read_loose(path, out);
|
||||||
|
out = read(*e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
// Reader for Noita's data.wak archive (uncompressed, flat TOC).
|
||||||
|
// header: u32 version, u32 num_files, u32 toc_size, u32 reserved
|
||||||
|
// entry : u32 offset, u32 length, u32 path_len, char path[path_len]
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct WakEntry {
|
||||||
|
uint32_t offset = 0;
|
||||||
|
uint32_t length = 0;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Wak {
|
||||||
|
public:
|
||||||
|
// Throws std::runtime_error on missing/corrupt archive.
|
||||||
|
explicit Wak(const std::filesystem::path& file);
|
||||||
|
|
||||||
|
const std::vector<WakEntry>& entries() const { return entries_; }
|
||||||
|
const WakEntry* find(std::string_view path) const;
|
||||||
|
std::vector<uint8_t> read(const WakEntry& e) const;
|
||||||
|
// Empty optional-style: returns false if path is absent.
|
||||||
|
bool read(std::string_view path, std::vector<uint8_t>& out) const;
|
||||||
|
// Mods: ModTextFileSetContent / ModImageMakeEditable replace a file for every later read; paths under mods/ are loose files below the game folder.
|
||||||
|
void set_override(std::string_view path, std::vector<uint8_t> bytes) const;
|
||||||
|
bool overridden(std::string_view path) const;
|
||||||
|
void set_loose_root(const std::filesystem::path& root) const { loose_root_ = root; }
|
||||||
|
const std::filesystem::path& loose_root() const { return loose_root_; }
|
||||||
|
bool exists(std::string_view path) const; // in the archive, overridden, or a loose mods/ file
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path file_;
|
||||||
|
std::vector<WakEntry> entries_;
|
||||||
|
std::unordered_map<std::string, size_t> index_;
|
||||||
|
mutable std::unordered_map<std::string, std::vector<uint8_t>> overrides_; // by lower-case path
|
||||||
|
mutable std::filesystem::path loose_root_;
|
||||||
|
bool read_loose(std::string_view path, std::vector<uint8_t>& out) const;
|
||||||
|
std::unordered_map<std::string, size_t> lower_; // the same paths in lower case: the files are looked up without regard to case, as on the game's file system (boss_limbs.xml asks for limb1_A.png, the archive has limb1_a.png)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#include "core/xml.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Parser {
|
||||||
|
std::string_view s;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
[[noreturn]] void fail(const std::string& msg) const {
|
||||||
|
throw std::runtime_error("xml: " + msg + " at offset " + std::to_string(i));
|
||||||
|
}
|
||||||
|
bool eof() const { return i >= s.size(); }
|
||||||
|
bool starts(std::string_view p) const { return s.substr(i, p.size()) == p; }
|
||||||
|
void ws() { while (!eof() && std::isspace((unsigned char)s[i])) ++i; }
|
||||||
|
|
||||||
|
void skip_until(std::string_view end) {
|
||||||
|
size_t p = s.find(end, i);
|
||||||
|
if (p == std::string_view::npos) fail("unterminated construct");
|
||||||
|
i = p + end.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skips text, comments, <?..?>, <!DOCTYPE..>; stops at the '<' of a real tag.
|
||||||
|
void skip_misc() {
|
||||||
|
for (;;) {
|
||||||
|
while (!eof() && s[i] != '<') ++i;
|
||||||
|
if (starts("<!--")) skip_until("-->");
|
||||||
|
else if (starts("<?")) skip_until("?>");
|
||||||
|
else if (starts("<![CDATA[")) skip_until("]]>");
|
||||||
|
else if (starts("<!")) skip_until(">");
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool name_char(char c) {
|
||||||
|
return std::isalnum((unsigned char)c) || c == '_' || c == '-' || c == ':' || c == '.';
|
||||||
|
}
|
||||||
|
std::string name() {
|
||||||
|
size_t b = i;
|
||||||
|
while (!eof() && name_char(s[i])) ++i;
|
||||||
|
if (b == i) fail("expected name");
|
||||||
|
return std::string(s.substr(b, i - b));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void unescape(std::string& v) {
|
||||||
|
if (v.find('&') == std::string::npos) return;
|
||||||
|
static const std::pair<std::string_view, char> ents[] = {
|
||||||
|
{"&", '&'}, {"<", '<'}, {">", '>'}, {""", '"'}, {"'", '\''}};
|
||||||
|
std::string out;
|
||||||
|
for (size_t k = 0; k < v.size();) {
|
||||||
|
bool hit = false;
|
||||||
|
if (v[k] == '&')
|
||||||
|
for (auto& [e, c] : ents)
|
||||||
|
if (std::string_view(v).substr(k, e.size()) == e) { out += c; k += e.size(); hit = true; break; }
|
||||||
|
if (!hit) out += v[k++];
|
||||||
|
}
|
||||||
|
v = std::move(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<XmlNode> element() {
|
||||||
|
if (eof() || s[i] != '<') fail("expected '<'");
|
||||||
|
++i;
|
||||||
|
auto n = std::make_unique<XmlNode>();
|
||||||
|
n->name = name();
|
||||||
|
for (;;) {
|
||||||
|
ws();
|
||||||
|
if (eof()) fail("unexpected end in tag");
|
||||||
|
if (s[i] == '/') { // self-closing
|
||||||
|
++i;
|
||||||
|
if (eof() || s[i] != '>') fail("expected '>'");
|
||||||
|
++i;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
if (s[i] == '>') { ++i; break; }
|
||||||
|
std::string key = name();
|
||||||
|
ws();
|
||||||
|
std::string val;
|
||||||
|
if (!eof() && s[i] == '=') {
|
||||||
|
++i; ws();
|
||||||
|
if (eof()) fail("expected value");
|
||||||
|
if (s[i] == '"' || s[i] == '\'') {
|
||||||
|
char q = s[i++];
|
||||||
|
size_t e = s.find(q, i);
|
||||||
|
if (e == std::string_view::npos) fail("unterminated attribute");
|
||||||
|
val = std::string(s.substr(i, e - i));
|
||||||
|
i = e + 1;
|
||||||
|
} else { // unquoted: tolerate
|
||||||
|
size_t b = i;
|
||||||
|
while (!eof() && !std::isspace((unsigned char)s[i]) && s[i] != '>' && s[i] != '/') ++i;
|
||||||
|
val = std::string(s.substr(b, i - b));
|
||||||
|
}
|
||||||
|
unescape(val);
|
||||||
|
}
|
||||||
|
n->attrs.emplace_back(std::move(key), std::move(val));
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
skip_misc();
|
||||||
|
if (eof()) fail("unclosed element " + n->name);
|
||||||
|
if (starts("</")) {
|
||||||
|
i += 2;
|
||||||
|
std::string close = name();
|
||||||
|
if (close != n->name) fail("mismatched closing tag " + close + " for " + n->name);
|
||||||
|
ws();
|
||||||
|
if (eof() || s[i] != '>') fail("expected '>'");
|
||||||
|
++i;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
n->children.push_back(element());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<XmlNode> parse_xml(std::string_view text) {
|
||||||
|
Parser p{text};
|
||||||
|
if (text.size() >= 3 && (unsigned char)text[0] == 0xEF && (unsigned char)text[1] == 0xBB) p.i = 3; // UTF-8 BOM
|
||||||
|
p.skip_misc();
|
||||||
|
if (p.eof()) throw std::runtime_error("xml: no root element");
|
||||||
|
return p.element();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
// Minimal tolerant XML DOM, sufficient for Noita's data files
|
||||||
|
// (elements and attributes; text content is ignored).
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct XmlNode {
|
||||||
|
std::string name;
|
||||||
|
std::vector<std::pair<std::string, std::string>> attrs;
|
||||||
|
std::vector<std::unique_ptr<XmlNode>> children;
|
||||||
|
|
||||||
|
const std::string* attr(std::string_view key) const {
|
||||||
|
for (auto& a : attrs) if (a.first == key) return &a.second;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::string attr_or(std::string_view key, std::string_view def = {}) const {
|
||||||
|
auto* v = attr(key);
|
||||||
|
return v ? *v : std::string(def);
|
||||||
|
}
|
||||||
|
const XmlNode* child(std::string_view n) const {
|
||||||
|
for (auto& c : children) if (c->name == n) return c.get();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the root element. Throws std::runtime_error on malformed input.
|
||||||
|
std::unique_ptr<XmlNode> parse_xml(std::string_view text);
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,152 @@
|
|||||||
|
#include "entity/electricity.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
bool ElectricitySystem::electrified(int x, int y, int frame) const {
|
||||||
|
auto it = stamp_.find(key(x, y));
|
||||||
|
return it != stamp_.end() && frame - it->second < 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
double numa(EntityRuntime& rt, int comp, const char* k, double d) {
|
||||||
|
RtComponent* c = rt.comp(comp);
|
||||||
|
if (!c) return d;
|
||||||
|
auto it = c->attrs.find(k);
|
||||||
|
return it == c->attrs.end() || it->second.empty() ? d : std::atof(it->second.c_str());
|
||||||
|
}
|
||||||
|
void setn(EntityRuntime& rt, int comp, const char* k, double v) { if (RtComponent* c = rt.comp(comp)) c->attrs[k] = std::to_string(v); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void ElectricitySystem::step(EntityRuntime& rt, int frame, const ElectricityGrid& g) {
|
||||||
|
if (stamp_.size() > 200000) { for (auto it = stamp_.begin(); it != stamp_.end();) it = frame - it->second > 60 ? stamp_.erase(it) : std::next(it); }
|
||||||
|
// ---- ElectricitySourceComponent (00ad8920): radius +48, emission_interval_frames +4c, next emission +50; a walker at the first cell of every row of the disc that conducts and is not electrified ----
|
||||||
|
for (int id : rt.with_component("ElectricitySourceComponent")) {
|
||||||
|
RtEntity* e = rt.ent(id);
|
||||||
|
if (!e || !e->alive) continue;
|
||||||
|
for (int sc : rt.components(id, "ElectricitySourceComponent", "", false)) {
|
||||||
|
const int radius = (int)numa(rt, sc, "radius", 0), interval = (int)numa(rt, sc, "emission_interval_frames", 0), next = (int)numa(rt, sc, "mNextEmission", 0);
|
||||||
|
if (next > frame) continue;
|
||||||
|
const int x0 = (int)e->x, y0 = (int)e->y;
|
||||||
|
if (std::getenv("NG_ELECLOG") && frame % 60 == 0) { int cc = 0, ee = 0; for (int dy = -radius; dy <= radius; ++dy) for (int dx = -radius; dx <= radius; ++dx) { ee += g.exists(x0 + dx, y0 + dy); cc += g.exists(x0 + dx, y0 + dy) && g.conductive(x0 + dx, y0 + dy); } std::printf("frame %d: source at (%d, %d) radius %d interval %d: %d cells, %d conductive%c", frame, x0, y0, radius, interval, ee, cc, 10); }
|
||||||
|
for (int dy = -radius; dy <= radius; ++dy) {
|
||||||
|
for (int dx = -radius; dx <= radius; ++dx) {
|
||||||
|
if (std::sqrt((float)(dx * dx + dy * dy)) > (float)radius) continue;
|
||||||
|
const int x = x0 + dx, y = y0 + dy;
|
||||||
|
if (!g.exists(x, y) || !g.conductive(x, y) || electrified(x, y, frame)) continue;
|
||||||
|
rt.spawn("data/entities/misc/electricity.xml", x, y, 0, false);
|
||||||
|
setn(rt, sc, "mNextEmission", frame + interval);
|
||||||
|
break; // (the engine leaves the row loop only: one walker per row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (std::getenv("NG_ELECLOG") && frame % 60 == 0) std::printf("frame %d: %zu sources %zu receivers %zu electricity walkers, %zu marked cells%c", frame, rt.with_component("ElectricitySourceComponent").size(), rt.with_component("ElectricityReceiverComponent").size(), rt.with_component("ElectricityComponent").size(), stamp_.size(), 10);
|
||||||
|
(void)0;
|
||||||
|
// ---- ElectricityComponent walkers (00ad0de0, the plain electricity mode): defaults 00650ed0: energy 1000, probability_to_heat 0, speed 32 steps per frame ----
|
||||||
|
for (int id : rt.with_component("ElectricityComponent")) {
|
||||||
|
RtEntity* e = rt.ent(id);
|
||||||
|
if (!e || !e->alive) continue;
|
||||||
|
const auto ecs = rt.components(id, "ElectricityComponent", "", false);
|
||||||
|
if (ecs.empty()) continue;
|
||||||
|
const int ec = ecs.front();
|
||||||
|
if (numa(rt, ec, "hack_is_material_crack", 0) != 0 || numa(rt, ec, "hack_is_set_fire", 0) != 0) continue; // the crack and fire modes are other workers
|
||||||
|
const double heat = numa(rt, ec, "probability_to_heat", 0);
|
||||||
|
const int steps = (int)numa(rt, ec, "speed", 32);
|
||||||
|
float dirx = (float)numa(rt, ec, "mAvgDir.x", 0), diry = (float)numa(rt, ec, "mAvgDir.y", 0);
|
||||||
|
double px = e->x, py = e->y;
|
||||||
|
bool kill = false;
|
||||||
|
for (int s = 0; s < steps && !kill; ++s) {
|
||||||
|
const int ix = (int)std::floor(px), iy = (int)std::floor(py);
|
||||||
|
const bool onConductor = g.exists(ix, iy) && g.conductive(ix, iy);
|
||||||
|
const float len = std::sqrt(dirx * dirx + diry * diry);
|
||||||
|
const float nx = len > 0 ? dirx / len : 0.0f, ny = len > 0 ? diry / len : 0.0f;
|
||||||
|
if (!onConductor) {
|
||||||
|
// search the squares around for the conductive cell that lies best in the direction (score = dot - 0.5 distance, from -3), nearest square first
|
||||||
|
int bx = 0, by = 0;
|
||||||
|
bool found = false;
|
||||||
|
float best = -3.0f;
|
||||||
|
for (int lo = -1; lo > -5 && !found; --lo) {
|
||||||
|
const int hi = -lo;
|
||||||
|
for (int dy = lo; dy <= hi; ++dy)
|
||||||
|
for (int dx = lo; dx <= hi; ++dx) {
|
||||||
|
const int x = ix + dx, y = iy + dy;
|
||||||
|
if (!g.exists(x, y)) continue;
|
||||||
|
if (!g.conductive(x, y)) {
|
||||||
|
if (draw() < heat && g.ignitable(x, y)) g.ignite(x, y, (int)std::clamp(heat * 100.0, 0.0, 100.0));
|
||||||
|
} else if (!electrified(x, y, frame)) {
|
||||||
|
const float dl = std::sqrt((float)(dx * dx + dy * dy));
|
||||||
|
const float ox = dl > 0 ? dx / dl : 0.0f, oy = dl > 0 ? dy / dl : 0.0f;
|
||||||
|
const float score = ox * nx + oy * ny - dl * 0.5f;
|
||||||
|
if (score > best) { best = score; bx = x; by = y; found = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) { kill = true; break; }
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) { // the 3 x 3 around the old position
|
||||||
|
const int x = ix + dx, y = iy + dy;
|
||||||
|
if (g.exists(x, y) && g.conductive(x, y) && !electrified(x, y, frame)) mark(x, y, frame);
|
||||||
|
}
|
||||||
|
px = bx; py = by;
|
||||||
|
dirx = (float)(bx - ix); diry = (float)(by - iy);
|
||||||
|
} else {
|
||||||
|
// a step of two cells along the direction turned by an angle in [-3pi/4, 3pi/4], up to 16 tries for a cell that is not electrified yet
|
||||||
|
int ox = 0, oy = 0;
|
||||||
|
float stepx = 0, stepy = 0;
|
||||||
|
bool ok = false;
|
||||||
|
for (int tr = 0; tr < 17; ++tr) {
|
||||||
|
const float a = draw() * 4.712389f - 2.3561945f;
|
||||||
|
stepx = (nx * std::cos(a) - ny * std::sin(a)) * 2.0f;
|
||||||
|
stepy = (ny * std::cos(a) + std::sin(a) * nx) * 2.0f;
|
||||||
|
ox = (int)stepx; oy = (int)stepy;
|
||||||
|
if (g.exists(ix + ox, iy + oy) && (ox != 0 || oy != 0) && !electrified(ix + ox, iy + oy, frame)) { ok = true; break; }
|
||||||
|
}
|
||||||
|
if (!ok && !(g.exists(ix + ox, iy + oy) && g.conductive(ix + ox, iy + oy))) { kill = true; break; }
|
||||||
|
mark(ix + ox, iy + oy, frame);
|
||||||
|
px += ox; py += oy;
|
||||||
|
dirx += stepx; diry += stepy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kill) { rt.kill(id); continue; }
|
||||||
|
e->x = px; e->y = py;
|
||||||
|
setn(rt, ec, "mAvgDir.x", dirx); setn(rt, ec, "mAvgDir.y", diry);
|
||||||
|
const int energy = (int)numa(rt, ec, "energy", 1000) - 1;
|
||||||
|
setn(rt, ec, "energy", energy);
|
||||||
|
if (energy <= 0) rt.kill(id);
|
||||||
|
}
|
||||||
|
// ---- ElectricityReceiverComponent (00ad6240): defaults 00ad2680: radius 1, active_time_frames 1, electrified_msg_interval -1, last electrified -1000 ----
|
||||||
|
for (int id : rt.with_component("ElectricityReceiverComponent")) {
|
||||||
|
RtEntity* e = rt.ent(id);
|
||||||
|
if (!e || !e->alive) continue;
|
||||||
|
for (int rc : rt.components(id, "ElectricityReceiverComponent", "", false)) {
|
||||||
|
const int ox = (int)numa(rt, rc, "offset_x", 0), oy = (int)numa(rt, rc, "offset_y", 0), radius = (int)numa(rt, rc, "radius", 1);
|
||||||
|
const int active = (int)numa(rt, rc, "active_time_frames", 1), switchCd = (int)numa(rt, rc, "switch_on_msg_interval_frames", 0), elecInt = (int)numa(rt, rc, "electrified_msg_interval_frames", -1);
|
||||||
|
int last = (int)numa(rt, rc, "mLastFrameElectrified", -1000);
|
||||||
|
const int prevLast = last;
|
||||||
|
const int cx = (int)e->x + ox, cy = (int)e->y + oy;
|
||||||
|
for (int dy = -radius; dy <= radius; ++dy) // (the engine leaves the inner loop at a hit and goes on with the next row)
|
||||||
|
for (int dx = -radius; dx <= radius; ++dx)
|
||||||
|
if (g.exists(cx + dx, cy + dy) && electrified(cx + dx, cy + dy, frame)) { last = frame; break; }
|
||||||
|
const bool on = frame - last < active;
|
||||||
|
if (on && elecInt >= 0 && (int)numa(rt, rc, "mNextElectrifiedMsgFrame", 0) <= frame) {
|
||||||
|
setn(rt, rc, "mNextElectrifiedMsgFrame", elecInt + frame);
|
||||||
|
rt.send_event(id, "script_electricity_receiver_electrified", "electricity_receiver_electrified", {});
|
||||||
|
}
|
||||||
|
setn(rt, rc, "mLastFrameElectrified", last);
|
||||||
|
if (on != (frame - prevLast - 1 < active)) {
|
||||||
|
if (on && switchCd > 0) {
|
||||||
|
if (frame < (int)numa(rt, rc, "mNextSwitchOnMsgFrame", 0)) continue;
|
||||||
|
setn(rt, rc, "mNextSwitchOnMsgFrame", switchCd + frame);
|
||||||
|
}
|
||||||
|
if (std::getenv("NG_ELECLOG")) std::printf("frame %d: receiver of entity %d (%s) switched %s%c", frame, id, e->file.c_str(), on ? "on" : "off", 10);
|
||||||
|
rt.send_event(id, "script_electricity_receiver_switched", "electricity_receiver_switched", {LVal::boolean(on)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
// Electricity: ElectricitySourceComponent (00ad8920) emits walkers, ElectricityComponent (00ad0de0, data/entities/misc/electricity.xml) walks through conductive cells and marks them,
|
||||||
|
// ElectricityReceiverComponent (00ad6240) reacts to marked cells nearby and sends Message_ElectricityReceiverSwitched / Electrified (script_electricity_receiver_*).
|
||||||
|
// The cell timestamp of the engine (cell +8, the frame a cell was last electrified) lives here in a map; the cells themselves are read through the callbacks.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "entity/runtime.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct ElectricityGrid {
|
||||||
|
std::function<bool(int x, int y)> exists; // a cell is there
|
||||||
|
std::function<bool(int x, int y)> conductive; // CellData.electrical_conductivity of its material (+0x10c)
|
||||||
|
std::function<bool(int x, int y)> ignitable; // it can burn and does not (cell vtable +0x70, +0x74)
|
||||||
|
std::function<void(int x, int y, int percent)> ignite; // cell vtable +0x78 (world, chance in percent)
|
||||||
|
};
|
||||||
|
|
||||||
|
class ElectricitySystem {
|
||||||
|
public:
|
||||||
|
// One update of every source, walker and receiver of the runtime on `frame`.
|
||||||
|
void step(EntityRuntime& rt, int frame, const ElectricityGrid& grid);
|
||||||
|
bool electrified(int x, int y, int frame) const; // marked within the last 9 frames
|
||||||
|
private:
|
||||||
|
void mark(int x, int y, int frame) { stamp_[key(x, y)] = frame; }
|
||||||
|
static uint64_t key(int x, int y) { return ((uint64_t)(uint32_t)x << 32) | (uint32_t)y; }
|
||||||
|
std::unordered_map<uint64_t, int> stamp_;
|
||||||
|
uint32_t lcg_ = 0x7a3c1d5u; // the engine's shared LCG (0120741c): state * 0x343fd + 0x269ec3, draw = (state >> 16 & 0x7fff) / 32767
|
||||||
|
float draw() { lcg_ = lcg_ * 0x343fdu + 0x269ec3u; return (float)((lcg_ >> 16) & 0x7fffu) / 32767.0f; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,857 @@
|
|||||||
|
#include "entity/entity.h"
|
||||||
|
#include "entity/herds.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "core/xml.h"
|
||||||
|
#include "world/wang_gen.h" // Image, decode_png
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Child elements of a component are objects (gun_config, config_explosion, damage_by_type, ...): their attributes become
|
||||||
|
// "object.attribute" entries, which is how ComponentObjectGetValue2 addresses them.
|
||||||
|
void flatten(const XmlNode& n, const std::string& prefix, std::map<std::string, std::string>& out) {
|
||||||
|
for (auto& a : n.attrs) out.emplace(prefix + a.first, a.second); // a duplicated attribute keeps its first occurrence (00dde120 / 00449560 discard the later one)
|
||||||
|
for (auto& ch : n.children) {
|
||||||
|
if (ch->name == "jump_trajectories") { // (PathFindingComponent's VECTOR_JUMPPARAMS: a list of JumpTrajectory x / y / lob, kept as "x,y,lob;...")
|
||||||
|
std::string list;
|
||||||
|
for (auto& j : ch->children) if (j->name == "JumpTrajectory") list += j->attr_or("x", "0") + "," + j->attr_or("y", "0") + "," + j->attr_or("lob", "1") + ";";
|
||||||
|
out.emplace(prefix + "jump_trajectories", list);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
flatten(*ch, prefix + ch->name + ".", out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityComponent to_component(const XmlNode& n) {
|
||||||
|
EntityComponent c;
|
||||||
|
c.type = n.name;
|
||||||
|
flatten(n, "", c.attrs);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void merge_into(EntityComponent& dst, const XmlNode& src) { // the override's values (first occurrence of each) replace the inherited ones
|
||||||
|
std::map<std::string, std::string> over;
|
||||||
|
flatten(src, "", over);
|
||||||
|
for (auto& [k, v] : over) dst.attrs[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<const EntityComponent*> EntityDef::all(const char* type) const {
|
||||||
|
std::vector<const EntityComponent*> r;
|
||||||
|
for (auto& c : comps) if (c.type == type) r.push_back(&c);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EntityDb::load(const std::string& path, EntityDef& out, int depth) {
|
||||||
|
if (depth > 8) return false;
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (!wak_.read(path, raw)) return false;
|
||||||
|
std::unique_ptr<XmlNode> root;
|
||||||
|
try { root = parse_xml(std::string_view((const char*)raw.data(), raw.size())); } catch (...) { return false; }
|
||||||
|
if (!root || root->name != "Entity") return false;
|
||||||
|
return parse(*root, path, out, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EntityDb::parse(const XmlNode& rootNode, const std::string& path, EntityDef& out, int depth) {
|
||||||
|
const XmlNode* root = &rootNode;
|
||||||
|
out.path = path;
|
||||||
|
if (auto* n = root->attr("name")) out.name = *n;
|
||||||
|
if (auto* t = root->attr("tags")) out.tags = *t;
|
||||||
|
for (auto& ch : root->children) {
|
||||||
|
if (ch->name == "Base") {
|
||||||
|
EntityDef base;
|
||||||
|
const size_t seg0 = out.comps.size(); // the components this Base brings in
|
||||||
|
if (auto* f = ch->attr("file")) {
|
||||||
|
load(*f, base, depth + 1);
|
||||||
|
if (ch->attr_or("include_children") == "1") out.children.insert(out.children.end(), base.children.begin(), base.children.end());
|
||||||
|
if (out.name.empty()) out.name = base.name;
|
||||||
|
if (!base.tags.empty()) out.tags = out.tags.empty() ? base.tags : base.tags + "," + out.tags;
|
||||||
|
out.comps.insert(out.comps.end(), base.comps.begin(), base.comps.end());
|
||||||
|
}
|
||||||
|
// overrides of the base's components (00575270): the n-th override of a type belongs to the n-th component of that type of the referenced template; an occurrence that does not
|
||||||
|
// exist is skipped (no component is made), _remove_from_base="1" drops the inherited component
|
||||||
|
std::map<std::string, int> ordinal;
|
||||||
|
std::set<size_t> removed;
|
||||||
|
for (auto& over : ch->children) {
|
||||||
|
const int k = ordinal[over->name]++;
|
||||||
|
int seen = 0;
|
||||||
|
size_t target = (size_t)-1;
|
||||||
|
for (size_t ci = seg0; ci < out.comps.size(); ++ci)
|
||||||
|
if (out.comps[ci].type == over->name && seen++ == k) { target = ci; break; }
|
||||||
|
if (target == (size_t)-1) continue;
|
||||||
|
if (over->attr_or("_remove_from_base") == "1") removed.insert(target);
|
||||||
|
else merge_into(out.comps[target], *over);
|
||||||
|
}
|
||||||
|
if (!removed.empty()) {
|
||||||
|
std::vector<EntityComponent> kept;
|
||||||
|
for (size_t ci = 0; ci < out.comps.size(); ++ci) if (!removed.count(ci)) kept.push_back(std::move(out.comps[ci]));
|
||||||
|
out.comps = std::move(kept);
|
||||||
|
}
|
||||||
|
} else if (ch->name == "Entity") { // an inline child: a definition of its own
|
||||||
|
EntityDef::Child cr;
|
||||||
|
cr.path = path + "#c" + std::to_string(out.children.size());
|
||||||
|
cr.name = ch->attr_or("name");
|
||||||
|
for (auto& t : ch->children) if (t->name == "_Transform") { cr.x = std::atof(t->attr_or("position.x", "0").c_str()); cr.y = std::atof(t->attr_or("position.y", "0").c_str()); }
|
||||||
|
auto cd = std::make_unique<EntityDef>();
|
||||||
|
if (depth < 8 && parse(*ch, cr.path, *cd, depth + 1)) { cache_[cr.path] = std::move(cd); out.children.push_back(cr); }
|
||||||
|
} else if (ch->name == "_Transform") { // (the position of an inline child is read by its parent)
|
||||||
|
out.sx = std::atof(ch->attr_or("scale.x", "1").c_str()); out.sy = std::atof(ch->attr_or("scale.y", "1").c_str());
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
out.comps.push_back(to_component(*ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityDef* EntityDb::get(const std::string& path) {
|
||||||
|
auto it = cache_.find(path);
|
||||||
|
if (it != cache_.end()) return it->second.get();
|
||||||
|
if (const size_t hc = path.find("#c"); hc != std::string::npos) { // an inline child: its file defines it
|
||||||
|
get(path.substr(0, hc));
|
||||||
|
it = cache_.find(path);
|
||||||
|
return it != cache_.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
auto def = std::make_unique<EntityDef>();
|
||||||
|
const bool ok = load(path, *def, 0);
|
||||||
|
const EntityDef* r = ok ? def.get() : nullptr;
|
||||||
|
cache_[path] = ok ? std::move(def) : nullptr;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Anim {
|
||||||
|
std::vector<uint32_t> texels; // frames * w * h, ARGB, frame after frame
|
||||||
|
int w = 0, h = 0, frames = 1, wait = 6; // wait = frames (1/60 s) per animation frame
|
||||||
|
float off_x = 0, off_y = 0; // origin offset declared by the sprite file
|
||||||
|
bool ok = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t pack(const uint8_t* p) { return (uint32_t)p[3] << 24 | (uint32_t)p[0] << 16 | (uint32_t)p[1] << 8 | p[2]; }
|
||||||
|
|
||||||
|
// file is either a .png or a sprite .xml (<Sprite filename ...><RectAnimation name frame_count .../>).
|
||||||
|
// animation: wanted RectAnimation name; empty = the file's default. strict: fail if that name does not exist.
|
||||||
|
Anim load_anim(const Wak& wak, const std::string& file, const std::string& animation, bool strict) {
|
||||||
|
Anim a;
|
||||||
|
std::string png = file;
|
||||||
|
int rx = 0, ry = 0, rw = 0, rh = 0, count = 1, per_row = 1;
|
||||||
|
float wait_s = 0.1f;
|
||||||
|
bool rect = false;
|
||||||
|
if (file.size() > 4 && file.compare(file.size() - 4, 4, ".xml") == 0) {
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (!wak.read(file, raw)) return a;
|
||||||
|
std::unique_ptr<XmlNode> root;
|
||||||
|
try { root = parse_xml(std::string_view((const char*)raw.data(), raw.size())); } catch (...) { return a; }
|
||||||
|
if (!root || root->name != "Sprite") return a;
|
||||||
|
png = root->attr_or("filename");
|
||||||
|
a.off_x = (float)std::atof(root->attr_or("offset_x", "0").c_str());
|
||||||
|
a.off_y = (float)std::atof(root->attr_or("offset_y", "0").c_str());
|
||||||
|
const std::string want = animation.empty() ? root->attr_or("default_animation") : animation;
|
||||||
|
const XmlNode* pick = nullptr;
|
||||||
|
const XmlNode* first = nullptr;
|
||||||
|
for (auto& c : root->children)
|
||||||
|
if (c->name == "RectAnimation") {
|
||||||
|
if (!first) first = c.get();
|
||||||
|
if (c->attr_or("name") == want) { pick = c.get(); break; }
|
||||||
|
}
|
||||||
|
if (!pick) { if (strict) return a; pick = first; }
|
||||||
|
if (pick) {
|
||||||
|
rx = std::atoi(pick->attr_or("pos_x", "0").c_str());
|
||||||
|
ry = std::atoi(pick->attr_or("pos_y", "0").c_str());
|
||||||
|
rw = std::atoi(pick->attr_or("frame_width", "0").c_str());
|
||||||
|
rh = std::atoi(pick->attr_or("frame_height", "0").c_str());
|
||||||
|
count = std::max(1, std::atoi(pick->attr_or("frame_count", "1").c_str()));
|
||||||
|
per_row = std::max(1, std::atoi(pick->attr_or("frames_per_row", std::to_string(count)).c_str()));
|
||||||
|
wait_s = (float)std::atof(pick->attr_or("frame_wait", "0.1").c_str());
|
||||||
|
rect = rw > 0 && rh > 0;
|
||||||
|
}
|
||||||
|
} else if (strict) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
Image im;
|
||||||
|
if (png.empty() || !wak.read(png, raw) || !decode_png(raw.data(), raw.size(), im)) return a;
|
||||||
|
if (!rect) { rx = ry = 0; rw = im.width; rh = im.height; count = 1; }
|
||||||
|
if (rw <= 0 || rh <= 0 || rw > 512 || rh > 512) return a;
|
||||||
|
a.w = rw; a.h = rh;
|
||||||
|
a.frames = std::min(count, 64);
|
||||||
|
a.wait = std::max(1, (int)std::lround(wait_s * 60.0f));
|
||||||
|
a.texels.assign((size_t)rw * rh * a.frames, 0u);
|
||||||
|
for (int f = 0; f < a.frames; ++f) {
|
||||||
|
const int fx = rx + (f % per_row) * rw, fy = ry + (f / per_row) * rh;
|
||||||
|
for (int y = 0; y < rh; ++y)
|
||||||
|
for (int x = 0; x < rw; ++x) {
|
||||||
|
if (fx + x >= im.width || fy + y >= im.height) continue;
|
||||||
|
a.texels[(size_t)f * rw * rh + (size_t)y * rw + x] = pack(&im.rgba[((size_t)(fy + y) * im.width + fx + x) * 4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.ok = true;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t marker_color(const std::string& path) {
|
||||||
|
if (path.find("/animals/") != std::string::npos) return 0xFFE04040u; // creatures: red
|
||||||
|
if (path.find("/chest") != std::string::npos) return 0xFFE0A030u; // chests: orange
|
||||||
|
if (path.find("/items/") != std::string::npos || path.find("/pickup") != std::string::npos) return 0xFFE0E040u; // items: yellow
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float comp_f(const EntityComponent* c, const char* key, float def) {
|
||||||
|
if (!c) return def;
|
||||||
|
auto it = c->attrs.find(key);
|
||||||
|
return it == c->attrs.end() ? def : (float)std::atof(it->second.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityComponent* first(const EntityDef& d, const char* type) {
|
||||||
|
auto v = d.all(type);
|
||||||
|
return v.empty() ? nullptr : v.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average colour of the opaque pixels of the entity's first sprite frame (used to draw projectiles).
|
||||||
|
uint32_t average_color(const Wak& wak, const EntityDef& def) {
|
||||||
|
for (const EntityComponent* c : def.all("SpriteComponent")) {
|
||||||
|
const Anim a = load_anim(wak, c->get("image_file"), c->get("rect_animation"), false);
|
||||||
|
if (!a.ok) continue;
|
||||||
|
uint64_t r = 0, g = 0, b = 0, n = 0;
|
||||||
|
for (int i = 0; i < a.w * a.h; ++i) {
|
||||||
|
const uint32_t t = a.texels[i];
|
||||||
|
if ((t >> 24) < 128) continue;
|
||||||
|
r += (t >> 16) & 255; g += (t >> 8) & 255; b += t & 255; ++n;
|
||||||
|
}
|
||||||
|
if (n) return 0xFF000000u | (uint32_t)(r / n) << 16 | (uint32_t)(g / n) << 8 | (uint32_t)(b / n);
|
||||||
|
}
|
||||||
|
return 0xFFFFD040u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projectile type (per second values converted to cells per frame) for an entity file; id into EntityLayer::projectiles.
|
||||||
|
int projectile_type(EntityDb& db, const Wak& wak, EntityLayer& L, std::map<std::string, int>& ids, const std::string& file) {
|
||||||
|
auto it = ids.find(file);
|
||||||
|
if (it != ids.end()) return it->second;
|
||||||
|
int id = -1;
|
||||||
|
if (const EntityDef* d = db.get(file)) {
|
||||||
|
const EntityComponent* pc = first(*d, "ProjectileComponent");
|
||||||
|
const EntityComponent* vc = first(*d, "VelocityComponent");
|
||||||
|
ProjectileType t;
|
||||||
|
t.speed_min = comp_f(pc, "speed_min", 300) / 60.0f;
|
||||||
|
t.speed_max = comp_f(pc, "speed_max", 300) / 60.0f;
|
||||||
|
t.damage = comp_f(pc, "damage", 0.3f);
|
||||||
|
t.lifetime = comp_f(pc, "lifetime", 120);
|
||||||
|
t.gravity = comp_f(vc, "gravity_y", 0) / 3600.0f;
|
||||||
|
t.color = average_color(wak, *d);
|
||||||
|
t.file = file;
|
||||||
|
id = (int)L.projectiles.size();
|
||||||
|
L.projectiles.push_back(t);
|
||||||
|
}
|
||||||
|
ids[file] = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Physics and AI parameters from the entity's components (values in the files are per second, the simulation runs per frame).
|
||||||
|
EntityState make_state(EntityDb& db, const Wak& wak, EntityLayer& L, std::map<std::string, int>& proj_ids, const EntityDef& def,
|
||||||
|
const EntitySpawn& s, bool is_player, const std::function<int(const std::string&)>& mat_lookup,
|
||||||
|
const std::function<uint32_t(const std::string&)>& mat_color) {
|
||||||
|
EntityState e;
|
||||||
|
e.pos[0] = (float)s.x; e.pos[1] = (float)s.y;
|
||||||
|
const EntityComponent* cd = first(def, "CharacterDataComponent");
|
||||||
|
const EntityComponent* cp = first(def, "CharacterPlatformingComponent");
|
||||||
|
const EntityComponent* ai = first(def, "AnimalAIComponent");
|
||||||
|
const EntityComponent* dm = first(def, "DamageModelComponent");
|
||||||
|
const EntityComponent* pf = first(def, "PathFindingComponent");
|
||||||
|
const EntityComponent* hb = first(def, "HitboxComponent");
|
||||||
|
if (cd) {
|
||||||
|
e.box[0] = comp_f(cd, "collision_aabb_min_x", -2); e.box[1] = comp_f(cd, "collision_aabb_max_x", 2);
|
||||||
|
e.box[2] = comp_f(cd, "collision_aabb_min_y", -3); e.box[3] = comp_f(cd, "collision_aabb_max_y", 3);
|
||||||
|
e.hp[3] = comp_f(cd, "climb_over_y", 4);
|
||||||
|
} else if (hb) {
|
||||||
|
e.box[0] = comp_f(hb, "aabb_min_x", -2); e.box[1] = comp_f(hb, "aabb_max_x", 2);
|
||||||
|
e.box[2] = comp_f(hb, "aabb_min_y", -3); e.box[3] = comp_f(hb, "aabb_max_y", 3);
|
||||||
|
}
|
||||||
|
// keep the box at least one cell in each direction
|
||||||
|
if (e.box[1] - e.box[0] < 2) { e.box[0] = -1; e.box[1] = 1; }
|
||||||
|
if (e.box[3] - e.box[2] < 2) { e.box[2] = -1; e.box[3] = 1; }
|
||||||
|
const float run = comp_f(cp, "run_velocity", 28), vmax = comp_f(cp, "velocity_max_x", 50);
|
||||||
|
e.mv[0] = std::min(run, vmax) / 60.0f;
|
||||||
|
e.mv[1] = vmax / 60.0f;
|
||||||
|
e.mv[2] = comp_f(cp, "jump_velocity_y", -175) / 60.0f;
|
||||||
|
e.ex16[0] = comp_f(cp, "accel_x", 1.0f); e.ex16[1] = comp_f(cp, "accel_x_air", 0.1f);
|
||||||
|
e.ex17[0] = comp_f(cp, "fly_speed_mult", 0.0f); e.ex17[1] = comp_f(cp, "fly_speed_change_spd", 5.0f);
|
||||||
|
e.ex17[2] = comp_f(cp, "fly_speed_max_up", 0.0f) / 60.0f; e.ex17[3] = comp_f(cp, "fly_speed_max_down", 0.0f) / 60.0f;
|
||||||
|
e.ex18[0] = std::max(std::min(comp_f(cp, "fly_velocity_x", 30.0f), vmax), comp_f(cp, "velocity_min_x", -vmax)) / 60.0f; // 00a7ea90: min(fly_velocity_x, velocity_max_x), then max(., velocity_min_x)
|
||||||
|
e.ex18[1] = comp_f(cp, "fly_smooth_y", 1.0f);
|
||||||
|
e.mv[3] = comp_f(cp, "pixel_gravity", 600) / 3600.0f;
|
||||||
|
e.hp[2] = comp_f(cp, "velocity_max_y", 350) / 60.0f;
|
||||||
|
e.ai[0] = std::min(comp_f(ai, "creature_detection_range_x", 100), 220.0f);
|
||||||
|
e.ai[1] = std::min(comp_f(ai, "creature_detection_range_y", 100), 140.0f);
|
||||||
|
e.ai[2] = comp_f(ai, "attack_melee_max_distance", 12);
|
||||||
|
e.ai[3] = 0.5f * (comp_f(ai, "attack_melee_damage_min", 0.1f) + comp_f(ai, "attack_melee_damage_max", 0.3f));
|
||||||
|
e.hp[0] = comp_f(dm, "hp", is_player ? 4.0f : 1.0f);
|
||||||
|
int flags = 0;
|
||||||
|
if (pf && comp_f(pf, "can_jump", 0) != 0) flags |= kEntCanJump;
|
||||||
|
if (ai && comp_f(ai, "can_fly", 0) != 0) flags |= kEntCanFly;
|
||||||
|
if (is_player) { // the jetpack: fuel in 1/64 frames, fly_time_max seconds of it (CharacterDataComponent), fly_recharge_spd seconds per second in the air, 6 on the ground
|
||||||
|
e.st[0] = 3;
|
||||||
|
const float fly_max = comp_f(cd, "fly_time_max", 3.0f) * 60.0f;
|
||||||
|
e.x[1] = (int32_t)std::lround(fly_max * 64.0f);
|
||||||
|
e.ex2[0] = fly_max; e.ex2[1] = comp_f(cd, "fly_recharge_spd", 0.4f);
|
||||||
|
}
|
||||||
|
else if (first(def, "WormComponent") || first(def, "BossDragonComponent") || first(def, "LimbBossComponent") || first(def, "GhostComponent")
|
||||||
|
|| (first(def, "PhysicsAIComponent") && first(def, "PhysicsBodyComponent") && !first(def, "PhysicsImageShapeComponent") && !ai) // (with an AnimalAI - the wand ghosts, Summon Taikasauva - it is a flying creature of the generic AI: it hunts the other herds and uses the wand it holds)
|
||||||
|
|| (!ai && dm && first(def, "ProjectileComponent") && first(def, "VelocityComponent") && !first(def, "CharacterDataComponent"))) { // (a projectile creature: boss_fish, moved by its VelocityComponent, which its script sets) // (a PhysicsAI body without an image: the ghost boss) // a worm (and the dragon boss, the limb boss): moved by the host (WormSystem 00c44480 / WormAISystem 00c47210, PhysicsAISystem 00bce0a0), no generic AI; the GPU keeps its damage and death
|
||||||
|
e.st[0] = 2;
|
||||||
|
flags |= 1024;
|
||||||
|
e.ai[0] = e.ai[1] = e.ai[2] = e.ai[3] = 0.0f;
|
||||||
|
}
|
||||||
|
else if (first(def, "PhysicsAIComponent") && first(def, "PhysicsImageShapeComponent")) { // a physics creature (the crystals, drones, statues, the book...): a rigid body
|
||||||
|
e.st[0] = 4; // that the host's PhysicsAI (00bce0a0) flies; hit and killed as a body with hp
|
||||||
|
}
|
||||||
|
else if (ai || (dm && s.path.find("/animals/") != std::string::npos)) { // every AnimalAIComponent (AnimalAISystem runs on all of them: the traps, the drone clone, the homunculus...), and the creatures (spiders and the like have no AnimalAIComponent: their PathFindingComponent says whether they fly)
|
||||||
|
if (!ai && pf && comp_f(pf, "can_fly", 0) != 0) flags |= kEntCanFly;
|
||||||
|
e.st[0] = (flags & kEntCanFly) ? 2 : 1;
|
||||||
|
if (first(def, "PhysicsAIComponent") && first(def, "PhysicsBodyComponent") && !first(def, "PhysicsImageShapeComponent")) flags |= 1024; // (a PhysicsAI body with an AnimalAI - the wand ghost: host-moved by PhysicsAI, main.cpp limb boss pass; the GPU's own wandering stood in and drove it 0.4 px a frame on top)
|
||||||
|
}
|
||||||
|
else if (first(def, "ItemComponent") || first(def, "PhysicsBodyComponent") || first(def, "PhysicsBody2Component")) e.st[0] = 4; // items, chests, barrels: gravity and collision only
|
||||||
|
else if (dm && first(def, "HitboxComponent") && !first(def, "ProjectileComponent") && !is_player) { // a hittable thing without AI or body (the dummy target, the nests, the crystals,
|
||||||
|
e.st[0] = 2; // the candles): the projectiles meet its hitbox, it takes damage and dies; it stays where it is
|
||||||
|
flags |= 1024; // (host-moved: nothing moves it)
|
||||||
|
e.ai[0] = e.ai[1] = e.ai[2] = e.ai[3] = 0.0f;
|
||||||
|
}
|
||||||
|
else e.st[0] = 0;
|
||||||
|
if (e.st[0] == 4) { e.ex5[0] = 0.0f; e.ex5[1] = 0.0f; } // (no VelocityComponent: no air friction, no liquid drag)
|
||||||
|
if (e.st[0] == 4) if (const EntityComponent* vc = first(def, "VelocityComponent")) { // an item falls by its VelocityComponent (constructor 00776280: gravity_y 400, air_friction 0.55, terminal_velocity 1000, liquid_drag 1.0 per liquid cell of the 3x3)
|
||||||
|
e.mv[3] = comp_f(vc, "gravity_y", 400.0f) / 3600.0f;
|
||||||
|
e.hp[2] = vc->get("apply_terminal_velocity", "1") == "0" ? 1.0e9f : comp_f(vc, "terminal_velocity", 1000.0f) / 60.0f; // (apply_terminal_velocity off: no cap)
|
||||||
|
e.ex5[0] = comp_f(vc, "air_friction", 0.55f) / 60.0f;
|
||||||
|
e.ex5[1] = comp_f(vc, "liquid_drag", 1.0f);
|
||||||
|
}
|
||||||
|
if (e.st[0] == 4 && first(def, "ItemComponent")) { // wands in the world: SimplePhysicsComponent _enabled="0" and ItemComponent play_hover_animation: they hover where they are, standing up, bobbing
|
||||||
|
const EntityComponent* sp = first(def, "SimplePhysicsComponent");
|
||||||
|
const EntityComponent* ic = first(def, "ItemComponent");
|
||||||
|
if (sp && sp->get("_enabled", "1") == "0" && ic && ic->get("play_hover_animation", "0") == "1") { flags |= 128 | 64; e.ex7[0] = -1.5707963f; e.ex6[2] = e.pos[0]; e.ex6[3] = e.pos[1]; }
|
||||||
|
}
|
||||||
|
e.st[3] = flags;
|
||||||
|
if (e.st[0] == 1 || e.st[0] == 2 || e.st[0] == 3) { // the herd (GenomeDataComponent.herd_id) decides which projectiles hurt it
|
||||||
|
const EntityComponent* gc = first(def, "GenomeDataComponent");
|
||||||
|
const int h = herd_of(gc ? gc->get("herd_id", "-1") : std::string("-1"));
|
||||||
|
e.ex8[1] = h + 1;
|
||||||
|
}
|
||||||
|
if (e.st[0] == 4) { // physics items and props: only those with a DamageModelComponent can be destroyed
|
||||||
|
e.hp[0] = dm ? comp_f(dm, "hp", 1.0f) : 1e9f;
|
||||||
|
e.x[3] = dm ? (int32_t)std::lround(e.hp[0] * 1000.0f) : 0;
|
||||||
|
// (materials_that_damage: the host pass of 00ab6010 / 00ab7410)
|
||||||
|
}
|
||||||
|
if (e.st[0] == 1 || e.st[0] == 2) if (const EntityComponent* vc2 = first(def, "VelocityComponent")) e.ex10[3] = comp_f(vc2, "liquid_drag", 1.0f); // the liquid drag of the VelocityComponent (constructor 00776280: 1.0); none without the component
|
||||||
|
if (cd && (e.st[0] == 1 || e.st[0] == 2 || e.st[0] == 3)) { // the mass that divides a knockback (CharacterDataComponent.mass, constructor 0069afc0: 1.0); the DamageModelComponent minimum_knockback_force (default 0) goes to ex11
|
||||||
|
const float mass = comp_f(cd, "mass", 1.0f);
|
||||||
|
std::memcpy(&e.rg2[3], &mass, 4);
|
||||||
|
e.ex11[0] = dm ? comp_f(dm, "minimum_knockback_force", 0.0f) : 0.0f;
|
||||||
|
e.ex19[0] = dm ? comp_f(dm, "critical_damage_resistance", 0.0f) : 0.0f; // 00ab9a90: the crit multiplier becomes (1 - this) x (m - 1) + 1, none at 1 or more // compared with abs(the raw scalar) before the mass divides it
|
||||||
|
}
|
||||||
|
if (is_player) e.aux[0] = (int32_t)std::lround(e.hp[0] * 1000.0f);
|
||||||
|
if (is_player) { e.x[2] = (int32_t)s.x; e.x[3] = (int32_t)s.y; } // respawn point
|
||||||
|
else e.x[3] = (int32_t)std::lround(e.hp[0] * 1000.0f); // maximum hp (milli-hp), for the HUD's health bars
|
||||||
|
// ranged attack
|
||||||
|
if (ai && e.st[0] != 0 && comp_f(ai, "attack_ranged_enabled", 0) != 0) {
|
||||||
|
const std::string file = ai->get("attack_ranged_entity_file");
|
||||||
|
if (!file.empty()) {
|
||||||
|
e.rg[0] = projectile_type(db, wak, L, proj_ids, file);
|
||||||
|
e.rg[1] = std::max(20, (int)comp_f(ai, "attack_ranged_frames_between", 100));
|
||||||
|
e.rg[2] = (int)comp_f(ai, "attack_ranged_min_distance", 0);
|
||||||
|
e.rg[3] = (int)std::min(comp_f(ai, "attack_ranged_max_distance", 150), 260.0f);
|
||||||
|
e.rg2[0] = std::max(1, (int)comp_f(ai, "attack_ranged_entity_count_min", 1));
|
||||||
|
e.rg2[1] = std::max(e.rg2[0], (int)comp_f(ai, "attack_ranged_entity_count_max", 1));
|
||||||
|
e.rg2[2] = (int)comp_f(ai, "attack_ranged_offset_y", -4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((e.st[0] == 1 || e.st[0] == 3) && cd) { // where the body is tested for swimming; a fraction of .5 marks creatures that swim on the surface
|
||||||
|
e.ex[3] = (float)(int)comp_f(cd, "buoyancy_check_offset_y", -6.0f); // the integer offset of the immersion probe from the position (00a7ea90), whatever its sign
|
||||||
|
e.st[3] |= 256; // ST_SWIMMER: this body tests the probe
|
||||||
|
if (cp) { // CharacterPlatformingComponent swim_*: the data values, else the constructor defaults of EntityState (009e91f0)
|
||||||
|
e.ex5[0] = comp_f(cp, "swim_idle_buoyancy_coeff", e.ex5[0]);
|
||||||
|
e.ex5[1] = comp_f(cp, "swim_up_buoyancy_coeff", e.ex5[1]);
|
||||||
|
e.ex5[2] = comp_f(cp, "swim_drag", e.ex5[2]);
|
||||||
|
e.ex5[3] = comp_f(cp, "swim_extra_horizontal_drag", e.ex5[3]);
|
||||||
|
e.ex6[0] = comp_f(cp, "swim_down_buoyancy_coeff", e.ex6[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ai && e.st[0] != 0 && e.st[0] != 3) { // what the creature sees and does: field of view, eye height, melee
|
||||||
|
const float deg = comp_f(ai, "creature_detection_angular_range_deg", 360.0f);
|
||||||
|
e.ex[0] = deg >= 359.0f ? -1.0f : std::cos(deg * 0.5f * 3.14159265f / 180.0f);
|
||||||
|
e.ex[1] = comp_f(ai, "eye_offset_y", -8.0f);
|
||||||
|
e.ex[2] = comp_f(ai, "attack_melee_enabled", 1.0f) != 0 ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
if (dm && e.st[0] != 0 && e.st[0] != 4) { // fire and air (DamageModelComponent): ignition chance per fire cell (percent per frame), burning damage per second, breath
|
||||||
|
// fire (00ab6010 / 00ab7a70): fire_probability_of_ignition only switches ignition on (!= 0); ex2.z = 1 + fire_how_much_fire_generates when it can ignite, else 0;
|
||||||
|
// fire_damage_amount only switches the fire damage on; is_on_fire: 12 s (mFireFramesLeft 720), mFireProbability (ex16.z) starts at 50 when the burning starts
|
||||||
|
e.ex2[2] = comp_f(dm, "fire_probability_of_ignition", 0.5f) != 0.0f ? 1.0f + std::max(0.0f, comp_f(dm, "fire_how_much_fire_generates", 4.0f)) : 0.0f;
|
||||||
|
e.ex2[3] = comp_f(dm, "fire_damage_amount", 0.2f);
|
||||||
|
if (comp_f(dm, "is_on_fire", 0) != 0) { e.ex4[3] = 720.0f; e.ex16[2] = 50.0f; }
|
||||||
|
if (comp_f(dm, "air_needed", 1) != 0) {
|
||||||
|
e.ex4[1] = comp_f(dm, "air_in_lungs_max", 7.0f) * 60.0f;
|
||||||
|
e.ex4[0] = std::min(e.ex4[1], comp_f(dm, "air_in_lungs", 7.0f) * 60.0f);
|
||||||
|
e.ex4[2] = comp_f(dm, "air_lack_of_damage", 0.4f) / 60.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dm && (e.st[0] == 1 || e.st[0] == 2) && mat_lookup) { // the blood: blood_material (the liquid a hit makes), else blood_spray_material, else "blood" (00ab9a90 picks the spray only for a recent entity)
|
||||||
|
std::string bm = dm->get("blood_material");
|
||||||
|
if (bm.empty()) bm = dm->get("blood_spray_material");
|
||||||
|
if (bm.empty()) bm = "blood";
|
||||||
|
const int id = bm.empty() ? -1 : mat_lookup(bm);
|
||||||
|
e.ex7[0] = comp_f(dm, "blood_multiplier", 1.0f);
|
||||||
|
if (id >= 0) { e.ex3[0] = id + 1; e.ex3[1] = (int32_t)(mat_color ? mat_color(bm) : 0xFFA01010u); }
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool tint_key_image(uint32_t* t, size_t n, uint32_t mc) {
|
||||||
|
int opaque = 0;
|
||||||
|
std::set<uint32_t> colours;
|
||||||
|
for (size_t i = 0; i < n; ++i) { // key images: a few colours with no blue at all (ordinary art uses blue)
|
||||||
|
if ((t[i] >> 24) < 128) continue;
|
||||||
|
++opaque;
|
||||||
|
if ((t[i] & 255) != 0) return false;
|
||||||
|
colours.insert(t[i]);
|
||||||
|
}
|
||||||
|
if (opaque == 0) return false;
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
if ((t[i] >> 24) < 128) continue;
|
||||||
|
// the red and green channels are a normal map (a surface normal per pixel); lit from the upper left
|
||||||
|
const float nx = ((t[i] >> 16) & 255) / 127.5f - 1.0f, ny = ((t[i] >> 8) & 255) / 127.5f - 1.0f;
|
||||||
|
const float f = std::clamp(0.85f - 0.3f * nx - 0.25f * ny, 0.55f, 1.1f);
|
||||||
|
auto mix = [&](int sh) { return (uint32_t)std::min(255.0f, (float)((mc >> sh) & 255) * f); };
|
||||||
|
t[i] = 0xFF000000u | mix(16) << 16 | mix(8) << 8 | mix(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tint_key_image(std::vector<uint32_t>& texels, uint32_t mc) { return tint_key_image(texels.data(), texels.size(), mc); }
|
||||||
|
|
||||||
|
bool load_sprite_frames(const Wak& wak, const std::string& file, const std::string& animation, SpriteFrames& out) {
|
||||||
|
Anim a = load_anim(wak, file, animation, false);
|
||||||
|
if (!a.ok) return false;
|
||||||
|
out.texels = std::move(a.texels); out.w = a.w; out.h = a.h; out.frames = a.frames; out.wait = a.wait; out.off_x = a.off_x; out.off_y = a.off_y;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool load_first_frame(const Wak& wak, const std::string& file, const std::string& animation, std::vector<uint32_t>& texels, int& w, int& h) {
|
||||||
|
Anim a = load_anim(wak, file, animation, false);
|
||||||
|
if (!a.ok) return false;
|
||||||
|
w = a.w; h = a.h;
|
||||||
|
texels.assign(a.texels.begin(), a.texels.begin() + (size_t)a.w * a.h);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::pair<int, EntityLayerBuilder::AnimEntry>* EntityLayerBuilder::get_anim(const std::string& file, const std::string& name, bool strict, bool additive) {
|
||||||
|
const std::string key = file + "|" + name + (strict ? "!" : "") + (additive ? "+" : "");
|
||||||
|
auto it = cache_.find(key);
|
||||||
|
if (it == cache_.end()) {
|
||||||
|
AnimEntry ae;
|
||||||
|
Anim a = load_anim(wak_, file, name, strict);
|
||||||
|
int off = 0;
|
||||||
|
if (a.ok && additive) { // an additive sprite (SpriteComponent additive = 1, the glowing eyes of the zombies): the colour times the alpha is added to the picture, black adds nothing
|
||||||
|
for (uint32_t& t : a.texels) {
|
||||||
|
const uint32_t al = t >> 24;
|
||||||
|
const uint32_t r = ((t >> 16) & 255) * al / 255, g = ((t >> 8) & 255) * al / 255, b = (t & 255) * al / 255;
|
||||||
|
t = (r | g | b) == 0 ? 0u : (0x03000000u | r << 16 | g << 8 | b); // alpha 3 marks the additive light of a sprite for render.comp (alpha 1 is taken by the particles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (a.ok) { off = (int)L_.texels.size(); L_.texels.insert(L_.texels.end(), a.texels.begin(), a.texels.end()); }
|
||||||
|
ae.w = a.w; ae.h = a.h; ae.frames = a.frames; ae.wait = a.wait; ae.off_x = a.off_x; ae.off_y = a.off_y; ae.ok = a.ok;
|
||||||
|
it = cache_.emplace(key, std::make_pair(off, ae)).first;
|
||||||
|
}
|
||||||
|
return it->second.second.ok ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EntityLayerBuilder::add_texels(const std::string& key, const std::vector<uint32_t>& px) {
|
||||||
|
auto it = raw_tex_.find(key);
|
||||||
|
if (it != raw_tex_.end()) return it->second;
|
||||||
|
const int off = (int)L_.texels.size();
|
||||||
|
L_.texels.insert(L_.texels.end(), px.begin(), px.end());
|
||||||
|
raw_tex_[key] = off;
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<int, const std::pair<int, EntityLayerBuilder::AnimEntry>*>> EntityLayerBuilder::part_animations(int slot, const std::string& name) {
|
||||||
|
std::vector<std::pair<int, const std::pair<int, AnimEntry>*>> r;
|
||||||
|
for (size_t i = 0; i < L_.inst.size(); ++i)
|
||||||
|
if (L_.inst[i].ent == slot && !L_.inst[i].file.empty()) r.push_back({(int)i, get_anim(L_.inst[i].file, name, true, L_.inst[i].additive)});
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EntityLayerBuilder::first_instance(int slot) const {
|
||||||
|
for (size_t i = 0; i < L_.inst.size(); ++i) if (L_.inst[i].ent == slot) return (int)i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EntityLayerBuilder::set_sprite(int slot, const std::string& image_file, const std::string& animation, int ox, int oy) {
|
||||||
|
const int idx = first_instance(slot);
|
||||||
|
if (idx < 0) return -1;
|
||||||
|
const auto* a = get_anim(image_file, animation, false);
|
||||||
|
if (!a) return -1;
|
||||||
|
SpriteInstance& si = L_.inst[idx];
|
||||||
|
si.idle.tex = (uint32_t)a->first; si.idle.w = a->second.w; si.idle.h = a->second.h; si.idle.frames = a->second.frames; si.idle.wait = a->second.wait;
|
||||||
|
si.walk = si.idle;
|
||||||
|
si.ox = ox; si.oy = oy;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EntityLayerBuilder::add_body_entity(int tex, int w, int h, int ox, int oy, double x, double y, int reuse, int* inst) {
|
||||||
|
EntityState st;
|
||||||
|
st.pos[0] = (float)x; st.pos[1] = (float)y;
|
||||||
|
st.st[0] = 4;
|
||||||
|
st.hp[0] = 1.0e9f;
|
||||||
|
for (int i = 0; i < 4; ++i) st.ex5[i] = st.ex6[i] = 0.0f; // ex5.z = material of a burnable body + 1 (rigid_burn.comp): bodies are not swimmers
|
||||||
|
st.box[0] = -1; st.box[1] = 1; st.box[2] = -1; st.box[3] = 1;
|
||||||
|
SpriteInstance si;
|
||||||
|
si.x = (int)std::floor(x); si.y = (int)std::floor(y);
|
||||||
|
si.idle.tex = (uint32_t)tex; si.idle.w = w; si.idle.h = h; si.idle.frames = 1; si.idle.wait = 6;
|
||||||
|
si.walk = si.idle;
|
||||||
|
si.ox = ox; si.oy = oy;
|
||||||
|
if (reuse >= 0 && reuse < (int)L_.ents.size()) {
|
||||||
|
const int idx = first_instance(reuse);
|
||||||
|
if (idx >= 0) {
|
||||||
|
L_.ents[reuse] = st;
|
||||||
|
si.ent = reuse;
|
||||||
|
L_.inst[idx] = si;
|
||||||
|
*inst = idx;
|
||||||
|
return reuse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int slot = (int)L_.ents.size();
|
||||||
|
si.ent = slot;
|
||||||
|
L_.ents.push_back(st);
|
||||||
|
L_.inst.push_back(si);
|
||||||
|
*inst = (int)L_.inst.size() - 1;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EntityLayerBuilder::add_projectile_type(const ProjectileType& t) {
|
||||||
|
L_.projectiles.push_back(t);
|
||||||
|
return (int)L_.projectiles.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EntityLayerBuilder::add(const EntitySpawn& s0, bool is_player, bool only_visible) {
|
||||||
|
EntitySpawn s = s0;
|
||||||
|
if (const EntityDef* d0 = db_.get(s0.path)) { double dx = 0, dy = 0; if (body2_spawn_shift(*d0, s0.x, s0.y, dx, dy)) { s.x += dx; s.y += dy; } } // (a PhysicsBody2Component entity: its origin is the body's root point)
|
||||||
|
auto to_ref = [](const std::pair<int, AnimEntry>& p) {
|
||||||
|
AnimRef r;
|
||||||
|
r.tex = (uint32_t)p.first; r.w = p.second.w; r.h = p.second.h; r.frames = p.second.frames; r.wait = p.second.wait;
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
if (!is_player) { ++L_.entities; ++L_.by_path[s.path]; }
|
||||||
|
if (std::getenv("NG_SPAWNLOG")) std::fprintf(stderr, "spawn %s at (%.0f, %.0f)%c", s.path.c_str(), s.x, s.y, 10);
|
||||||
|
const EntityDef* def = db_.get(s.path);
|
||||||
|
EntityState st;
|
||||||
|
if (def) st = make_state(db_, wak_, L_, proj_ids_, *def, s, is_player, material_lookup, material_color);
|
||||||
|
else { st.pos[0] = (float)s.x; st.pos[1] = (float)s.y; }
|
||||||
|
if (!def) {
|
||||||
|
++L_.missing_defs;
|
||||||
|
if (only_visible) return -1;
|
||||||
|
L_.ents.push_back(st);
|
||||||
|
return (int)L_.ents.size() - 1;
|
||||||
|
}
|
||||||
|
const int slot = (int)L_.ents.size();
|
||||||
|
const bool mobile = st.st[0] != 0 && st.st[0] != 4;
|
||||||
|
for (const EntityComponent* lc : def->all("LightComponent")) {
|
||||||
|
if (lc->get("_enabled", "1") == "0") continue;
|
||||||
|
const float radius = (float)std::atof(lc->get("radius", "0").c_str());
|
||||||
|
if (radius <= 0.0f) continue;
|
||||||
|
L_.lights.push_back({slot, radius, (float)std::atof(lc->get("r", "255").c_str()) / 255.0f, (float)std::atof(lc->get("g", "255").c_str()) / 255.0f, (float)std::atof(lc->get("b", "255").c_str()) / 255.0f});
|
||||||
|
}
|
||||||
|
std::vector<SpriteInstance> parts;
|
||||||
|
auto add_part = [&](const std::pair<int, AnimEntry>* idle, const std::pair<int, AnimEntry>* walk, int ox, int oy, float z, const std::string& pfile = std::string(), bool padd = false) {
|
||||||
|
SpriteInstance si;
|
||||||
|
si.ent = slot;
|
||||||
|
si.x = (int)std::floor(s.x);
|
||||||
|
si.y = (int)std::floor(s.y);
|
||||||
|
si.idle = to_ref(*idle);
|
||||||
|
si.walk = (walk && walk->second.w == idle->second.w && walk->second.h == idle->second.h) ? to_ref(*walk) : si.idle;
|
||||||
|
si.ox = ox; si.oy = oy; si.z = z;
|
||||||
|
si.file = pfile; si.additive = padd;
|
||||||
|
parts.push_back(si);
|
||||||
|
};
|
||||||
|
std::vector<EntityComponent> extra_sprites; // the sprites a world-generation script added (the perks' icons)
|
||||||
|
for (const SpawnComp& sc : s.comps) if (sc.type == "SpriteComponent") extra_sprites.push_back({sc.type, sc.attrs});
|
||||||
|
std::vector<const EntityComponent*> sprite_comps = def->all("SpriteComponent");
|
||||||
|
std::vector<EntityComponent> replaced; // the sprites a script changed (a generated wand: SetWandSprite sets the image and the grip offset) replace the file's
|
||||||
|
for (const SpawnComp& sc : s.comps) if (sc.type == "SpriteReplace") replaced.push_back({"SpriteComponent", sc.attrs});
|
||||||
|
if (!replaced.empty()) { sprite_comps.clear(); for (const EntityComponent& ec : replaced) sprite_comps.push_back(&ec); }
|
||||||
|
for (const EntityComponent& ec : extra_sprites) sprite_comps.push_back(&ec);
|
||||||
|
for (const EntityComponent* c : sprite_comps) {
|
||||||
|
if (c->get("_enabled", "1") == "0" || c->get("visible", "1") == "0") continue;
|
||||||
|
if (c->get("_tags").find("aiming_reticle") != std::string::npos) continue; // the gamepad aiming cursor of the player, not a visible part of it
|
||||||
|
const std::string file = c->get("image_file");
|
||||||
|
if (file.empty() || file.find('$') != std::string::npos) continue;
|
||||||
|
const std::string anim = c->get("rect_animation");
|
||||||
|
// idle animation: "stand" when the sprite has one, otherwise the component's / the file's default
|
||||||
|
const bool add = c->get("additive", "0") == "1";
|
||||||
|
const auto* idle = get_anim(file, "stand", true, add);
|
||||||
|
if (!idle) idle = get_anim(file, anim, false, add);
|
||||||
|
if (!idle) continue;
|
||||||
|
const auto* walk = mobile ? get_anim(file, "walk", true, add) : nullptr;
|
||||||
|
// the sprite file's origin offset and the component's offset add up (measured against creatures in real chunks:
|
||||||
|
// sprite pixels inside solid cells drop from 70-100 % to ~0-7 % for most creatures with this rule)
|
||||||
|
// a non-zero component offset is used as is, otherwise the sprite file's (the player has the same offset in both, so they must not add up)
|
||||||
|
const double cx = std::atof(c->get("offset_x", "0").c_str()), cy = std::atof(c->get("offset_y", "0").c_str());
|
||||||
|
add_part(idle, walk, (int)std::lround(cx != 0 ? cx : idle->second.off_x), (int)std::lround(cy != 0 ? cy : idle->second.off_y),
|
||||||
|
(float)std::atof(c->get("z_index", "1").c_str()), file, add); // (the component's default z_index is 1)
|
||||||
|
}
|
||||||
|
// physics bodies (chests, gold, potions, boxes) draw their image shape, centred on the position
|
||||||
|
struct Secondary { SpriteInstance si; BodyPart bp; };
|
||||||
|
std::vector<Secondary> secondary; // a body joined to the first by a PhysicsJointComponent (the wheel of a cart) is a body of its own
|
||||||
|
std::string first_body_id;
|
||||||
|
// the shapes and PhysicsJoint2Components a script added before PhysicsBody2InitFromComponents (temple_lantern.lua: the chain) come with the spawn
|
||||||
|
std::vector<EntityComponent> script_bodies, script_joints;
|
||||||
|
for (const SpawnComp& sc : s.comps) { if (sc.type == "PhysicsImageShapeComponent") script_bodies.push_back({sc.type, sc.attrs}); else if (sc.type == "PhysicsJoint2Component") script_joints.push_back({sc.type, sc.attrs}); }
|
||||||
|
std::vector<const EntityComponent*> shape_comps = def->all("PhysicsImageShapeComponent"), joint2_comps = def->all("PhysicsJoint2Component");
|
||||||
|
for (const EntityComponent& ec : script_bodies) shape_comps.push_back(&ec);
|
||||||
|
for (const EntityComponent& ec : script_joints) joint2_comps.push_back(&ec);
|
||||||
|
const size_t sprite_parts = parts.size();
|
||||||
|
for (const EntityComponent* c : shape_comps) {
|
||||||
|
const std::string file = c->get("image_file");
|
||||||
|
if (file.empty() || file.find('$') != std::string::npos) continue;
|
||||||
|
const auto* img = get_anim(file, "", false);
|
||||||
|
if (!img) continue;
|
||||||
|
const std::string bid = c->get("body_id");
|
||||||
|
if (first_body_id.empty()) first_body_id = bid;
|
||||||
|
const EntityComponent* joint = nullptr;
|
||||||
|
const EntityComponent* joint2 = nullptr;
|
||||||
|
if (!bid.empty() && bid != first_body_id) {
|
||||||
|
for (const EntityComponent* jc : def->all("PhysicsJointComponent")) if (jc->get("body2_id") == bid && jc->get("nail_to_wall", "0") != "1") { joint = jc; break; }
|
||||||
|
if (!joint) for (const EntityComponent* jc : joint2_comps) if (jc->get("type", "REVOLUTE_JOINT") == "REVOLUTE_JOINT" && jc->get("body2_id") == bid) { joint2 = jc; break; }
|
||||||
|
}
|
||||||
|
if (material_color && !c->get("material").empty() && tinted_.insert(img->first).second) // the gold nugget image only gives the shape: the material gives the colour
|
||||||
|
{ const bool tinted = tint_key_image(L_.texels.data() + img->first, (size_t)img->second.w * img->second.h * img->second.frames, material_color(c->get("material")));
|
||||||
|
if (std::getenv("NG_DEBUGTINT")) std::fprintf(stderr, "tint %s material %s -> %d colour %08x%c", file.c_str(), c->get("material").c_str(), (int)tinted, material_color(c->get("material")), 10); }
|
||||||
|
const bool centered = c->get("centered", "0") != "0";
|
||||||
|
if (std::getenv("NG_SHAPELOG")) std::fprintf(stderr, "image shape %s %dx%d centered %d%c", file.c_str(), img->second.w, img->second.h, (int)centered, 10);
|
||||||
|
// PhysicsBody2Component: the shapes' and joints' offsets share one space whose point (root offset + root_offset) is the entity origin (body2_root_point; the origin was moved there
|
||||||
|
// from the spawn point, body2_spawn_shift), so a point P of it is P - that point
|
||||||
|
double rox = 0, roy = 0;
|
||||||
|
body2_root_point(*def, rox, roy);
|
||||||
|
const int pox = (int)std::lround((centered ? img->second.w * 0.5 : 0.0) - std::atof(c->get("offset_x", "0").c_str()) + rox);
|
||||||
|
const int poy = (int)std::lround((centered ? img->second.h * 0.5 : 0.0) - std::atof(c->get("offset_y", "0").c_str()) + roy);
|
||||||
|
if (joint || joint2) {
|
||||||
|
Secondary sc;
|
||||||
|
sc.si.x = (int)std::floor(s.x); sc.si.y = (int)std::floor(s.y);
|
||||||
|
sc.si.idle = to_ref(*img); sc.si.walk = sc.si.idle; sc.si.ox = pox; sc.si.oy = poy; sc.si.z = 50.0f + (float)std::atof(c->get("z", "0").c_str());
|
||||||
|
if (joint) { sc.bp.joint_x = std::atof(joint->get("pos_x", "0").c_str()); sc.bp.joint_y = std::atof(joint->get("pos_y", "0").c_str()); }
|
||||||
|
else {
|
||||||
|
sc.bp.local = true; sc.bp.body_id = bid; sc.bp.parent_id = joint2->get("body1_id", "0");
|
||||||
|
sc.bp.joint_x = std::atof(joint2->get("offset_x", "0").c_str()) - rox; sc.bp.joint_y = std::atof(joint2->get("offset_y", "0").c_str()) - roy;
|
||||||
|
sc.bp.joint_bf = std::atof(joint2->get("break_force", "1.3").c_str());
|
||||||
|
sc.bp.joint_id = std::atoi(joint2->get("joint_id", "0").c_str()); // (PhysicsJoint2MutatorComponent drives it by this id)
|
||||||
|
for (const EntityComponent* jc : joint2_comps)
|
||||||
|
if (jc->get("type") == "REVOLUTE_JOINT_ATTACH_TO_NEARBY_SURFACE" && jc->get("body1_id", "0") == bid) {
|
||||||
|
sc.bp.nail = true; sc.bp.nail_x = std::atof(jc->get("offset_x", "0").c_str()) - rox; sc.bp.nail_y = std::atof(jc->get("offset_y", "0").c_str()) - roy; sc.bp.nail_bf = std::atof(jc->get("break_force", "1.3").c_str()); sc.bp.nail_rx = std::atof(jc->get("ray_x", "0").c_str()); sc.bp.nail_ry = std::atof(jc->get("ray_y", "-10").c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sc.bp.material = c->get("material");
|
||||||
|
for (const EntityComponent* pb : def->all("PhysicsBodyComponent")) if (pb->get("uid") == bid) sc.bp.angular_damping = std::atof(pb->get("angular_damping", "0").c_str());
|
||||||
|
secondary.push_back(sc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
add_part(img, nullptr, pox, poy, 50.0f); // a physics body is part of the world: behind the sprites (the player passes in front of the lanterns)
|
||||||
|
}
|
||||||
|
// the body's image comes first: the rigid body is made from the entity's first sprite (the statue's emissive overlay must not be taken for its shape)
|
||||||
|
if (parts.size() > sprite_parts && sprite_parts > 0) std::rotate(parts.begin(), parts.begin() + (std::ptrdiff_t)sprite_parts, parts.end());
|
||||||
|
if ((first(*def, "WormComponent") || first(*def, "BossDragonComponent")) && !parts.empty()) { // the worm's sprites (head, body, tail) are parts of their own, placed and turned by the host (st[3] 64: drawn along ex7.x)
|
||||||
|
const int n = std::min((int)parts.size(), 20);
|
||||||
|
L_.ents.push_back(st);
|
||||||
|
for (int k = 0; k < n; ++k) {
|
||||||
|
EntityState ps;
|
||||||
|
ps.pos[0] = st.pos[0]; ps.pos[1] = st.pos[1];
|
||||||
|
ps.st[0] = 0; ps.st[3] = 64;
|
||||||
|
{ // the segments take the projectiles (WormComponent / BossDragonComponent hitbox_radius around each part): ex19.w = the root slot + 1, ex19.z = the radius
|
||||||
|
const EntityComponent* wc = first(*def, "WormComponent");
|
||||||
|
if (!wc) wc = first(*def, "BossDragonComponent");
|
||||||
|
ps.ex19[3] = (float)(slot + 1); ps.ex19[2] = comp_f(wc, "hitbox_radius", 1.0f);
|
||||||
|
}
|
||||||
|
L_.ents.push_back(ps);
|
||||||
|
parts[k].ent = slot + 1 + k;
|
||||||
|
L_.inst.push_back(parts[k]);
|
||||||
|
}
|
||||||
|
L_.worm_parts[slot] = {slot + 1, n};
|
||||||
|
++L_.with_sprite;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
if (const EntityComponent* vc = first(*def, "VerletPhysicsComponent")) {
|
||||||
|
// A Verlet chain (vines, ropes): the root entity keeps the parameters, every point is an entity of its own (consecutive slots) that carries the sprite of its part;
|
||||||
|
// shaders/vines.comp simulates the points.
|
||||||
|
const int n = std::clamp(std::atoi(vc->get("num_points", "2").c_str()), 2, 40); // the constructor 00645e50: num_points 2, resting_distance 2, mass 0.8..1, stiffness 1, velocity_dampening 0.99, liquid_damping 0.7, gravity and wind on, wind_change_speed 1
|
||||||
|
const float rest = (float)std::atof(vc->get("resting_distance", "2").c_str());
|
||||||
|
st.st[0] = 0;
|
||||||
|
st.hp[1] = 0.0f; // not yet moved to its surface
|
||||||
|
st.ex[0] = (float)n; st.ex[1] = (float)(slot + 1); st.ex[2] = rest; st.ex[3] = 777.0f;
|
||||||
|
st.mv[0] = (float)std::atof(vc->get("velocity_dampening", "0.99").c_str());
|
||||||
|
st.mv[1] = (float)std::atof(vc->get("stiffness", "1.0").c_str());
|
||||||
|
st.mv[2] = vc->get("simulate_gravity", "1") == "1" ? 1.0f : 0.0f;
|
||||||
|
st.mv[3] = vc->get("simulate_wind", "1") == "1" ? 1.0f : 0.0f;
|
||||||
|
st.ex5[0] = comp_f(vc, "wind_change_speed", 1.0f); st.ex5[1] = comp_f(vc, "mass_min", 0.8f); st.ex5[2] = comp_f(vc, "mass_max", 1.0f); st.ex5[3] = comp_f(vc, "liquid_damping", 0.7f);
|
||||||
|
st.ex17[0] = comp_f(vc, "animation_target_offset.x", 0.0f); st.ex17[1] = comp_f(vc, "animation_target_offset.y", 0.0f); // the animation (vines.comp, 00c385a0): off while the offset is 0
|
||||||
|
st.ex17[2] = comp_f(vc, "animation_amount", 2.0f); st.ex17[3] = comp_f(vc, "animation_speed", 5.0f); st.ex18[0] = comp_f(vc, "animation_energy", 0.6f);
|
||||||
|
if (const EntityComponent* ms = first(*def, "MoveToSurfaceOnCreateComponent")) { // the constructor 00d3da80 defaults: lookup_radius 64, offset_from_surface 2, ray_count 4, verlet_min_joint_distance 32
|
||||||
|
const std::string ty = ms->get("type");
|
||||||
|
st.ex4[0] = ty == "VERLET_ROPE_TWO_JOINTS" ? 2.0f : (ty == "VERLET_ROPE_ONE_JOINT" ? 1.0f : 0.0f);
|
||||||
|
st.ex4[1] = comp_f(ms, "lookup_radius", 64.0f); st.ex4[2] = comp_f(ms, "offset_from_surface", 2.0f); st.ex4[3] = comp_f(ms, "verlet_min_joint_distance", 32.0f);
|
||||||
|
st.ex6[0] = comp_f(ms, "ray_count", 4.0f);
|
||||||
|
}
|
||||||
|
{ // VerletWorldJointComponent (load_verlet_rope_with_one_joint / two_joints): the point verlet_point_index is held at world_position; joint type 3 in vines.comp,
|
||||||
|
// ex2.xy the joint of the first point, ex2.zw that of the last, ex7.z which of them exist (1, 2)
|
||||||
|
int pins = 0;
|
||||||
|
for (const SpawnComp& sc : s.comps) {
|
||||||
|
if (sc.type != "VerletWorldJointComponent") continue;
|
||||||
|
auto num = [&](const char* k, double d) { auto it = sc.attrs.find(k); return it == sc.attrs.end() ? d : std::atof(it->second.c_str()); };
|
||||||
|
const int idx = (int)num("verlet_point_index", 0);
|
||||||
|
const float wx = (float)num("world_position.x", s.x), wy = (float)num("world_position.y", s.y);
|
||||||
|
if (idx <= 0) { st.ex2[0] = wx; st.ex2[1] = wy; pins |= 1; } else { st.ex2[2] = wx; st.ex2[3] = wy; pins |= 2; }
|
||||||
|
}
|
||||||
|
if (pins) { st.ex4[0] = 3.0f; st.ex7[2] = (float)pins; }
|
||||||
|
st.ex6[2] = 0.0f; st.ex6[3] = 0.0f; // the cells under the world joints, remembered by vines.comp (00c40b90): not seen yet
|
||||||
|
st.ex6[1] = verlet_wind_at ? verlet_wind_at(pins & 1 ? st.ex2[0] : s.x, pins & 1 ? st.ex2[1] : s.y) : 0.0f; // the wind of the job (ex6.y), taken at the first point
|
||||||
|
if (std::getenv("NG_VERLETLOG")) std::printf("verlet %s at (%.0f, %.0f) pins %d (%.0f,%.0f) (%.0f,%.0f) surface type %.0f wind %.4f%c", s.path.c_str(), s.x, s.y, pins, st.ex2[0], st.ex2[1], st.ex2[2], st.ex2[3], st.ex4[0], st.ex6[1], 10);
|
||||||
|
}
|
||||||
|
L_.ents.push_back(st);
|
||||||
|
for (int k = 0; k < n; ++k) {
|
||||||
|
EntityState ps;
|
||||||
|
ps.pos[0] = ps.vel[0] = st.pos[0];
|
||||||
|
ps.pos[1] = ps.vel[1] = st.pos[1] + k * rest;
|
||||||
|
ps.st[0] = 0;
|
||||||
|
ps.st[3] = 64; // drawn rotated along the chain
|
||||||
|
ps.hp[0] = 0.0f; // hp.x = 1 once the point has taken a liquid's material (it then damps by 0.85) (vines.comp stores the angle in ex7.x)
|
||||||
|
L_.ents.push_back(ps);
|
||||||
|
}
|
||||||
|
for (size_t k = 0; k < parts.size() && k < (size_t)n; ++k) { parts[k].ent = slot + 1 + (int)k; L_.inst.push_back(parts[k]); }
|
||||||
|
++L_.with_sprite;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
if (first(*def, "LimbBossComponent") || (first(*def, "PhysicsAIComponent") && first(*def, "PhysicsBodyComponent") && !first(*def, "PhysicsImageShapeComponent"))) { // the limb boss (and a PhysicsAI body without an image): the body is drawn as usual, every sprite of its limbs (IKLimbComponent children) is a part entity the host moves
|
||||||
|
std::stable_sort(parts.begin(), parts.end(), [](const SpriteInstance& a, const SpriteInstance& b) { return a.z > b.z; });
|
||||||
|
L_.ents.push_back(st);
|
||||||
|
std::vector<SpriteInstance> limb_inst;
|
||||||
|
std::vector<EntityLayer::LimbParts> limbs;
|
||||||
|
for (const EntityDef::Child& ch : def->children) {
|
||||||
|
const EntityDef* cd = db_.get(ch.path);
|
||||||
|
if (!cd || !first(*cd, "IKLimbComponent")) continue;
|
||||||
|
EntityLayer::LimbParts lp;
|
||||||
|
lp.child = ch.path; lp.first = (int)L_.ents.size(); lp.attacker = first(*cd, "IKLimbAttackerComponent") != nullptr;
|
||||||
|
for (const EntityComponent* c : cd->all("SpriteComponent")) {
|
||||||
|
const std::string file = c->get("image_file");
|
||||||
|
const auto* img = file.empty() ? nullptr : get_anim(file, "", false);
|
||||||
|
if (!img) continue;
|
||||||
|
EntityState ps;
|
||||||
|
ps.pos[0] = st.pos[0]; ps.pos[1] = st.pos[1];
|
||||||
|
ps.st[0] = 0; ps.st[3] = 64; ps.ex7[1] = 1.0f;
|
||||||
|
L_.ents.push_back(ps);
|
||||||
|
SpriteInstance si;
|
||||||
|
si.ent = lp.first + lp.n; si.x = (int)std::floor(s.x); si.y = (int)std::floor(s.y);
|
||||||
|
si.idle = si.walk = to_ref(*img);
|
||||||
|
si.ox = (int)std::floor(std::atof(c->get("offset_x", "0").c_str())); si.oy = (int)std::floor(std::atof(c->get("offset_y", "0").c_str()));
|
||||||
|
si.z = (float)std::atof(c->get("z_index", "1").c_str());
|
||||||
|
limb_inst.push_back(si);
|
||||||
|
lp.w.push_back(img->second.w);
|
||||||
|
++lp.n;
|
||||||
|
}
|
||||||
|
if (lp.n > 0) limbs.push_back(lp);
|
||||||
|
}
|
||||||
|
for (auto& p : limb_inst) L_.inst.push_back(p); // (behind the body: z_index 10)
|
||||||
|
for (auto& p : parts) L_.inst.push_back(p);
|
||||||
|
L_.limb_parts[slot] = limbs;
|
||||||
|
std::vector<EntityLayer::Hair> hair;
|
||||||
|
for (const EntityDef::Child& ch : def->children) { // the hair: Verlet chains hanging from InheritTransformComponent's offset, moved with the body by the host
|
||||||
|
const EntityDef* cd = db_.get(ch.path);
|
||||||
|
if (!cd || !first(*cd, "VerletPhysicsComponent")) continue;
|
||||||
|
float ox = (float)ch.x, oy = (float)ch.y;
|
||||||
|
if (const EntityComponent* it = first(*cd, "InheritTransformComponent")) { const std::string tx = it->get("Transform.position.x", ""), ty = it->get("Transform.position.y", ""); if (!tx.empty()) ox = (float)std::atof(tx.c_str()); if (!ty.empty()) oy = (float)std::atof(ty.c_str()); }
|
||||||
|
const int hs = add(EntitySpawn{ch.path, s.x + ox, s.y + oy, {}}, false, false);
|
||||||
|
if (hs < 0 || hs >= (int)L_.ents.size() || L_.ents[hs].ex[3] != 777.0f) continue;
|
||||||
|
L_.ents[hs].ex4[0] = 3.0f;
|
||||||
|
hair.push_back({hs, ox, oy});
|
||||||
|
}
|
||||||
|
if (!hair.empty()) L_.limb_hair[slot] = hair;
|
||||||
|
if (std::getenv("NG_WORMLOG")) { std::fprintf(stderr, "limb boss %s: %d children, %d limbs%c", s.path.c_str(), (int)def->children.size(), (int)limbs.size(), 10); for (const auto& lp : limbs) std::fprintf(stderr, " limb %s: %d sprites%c", lp.child.c_str(), lp.n, 10); }
|
||||||
|
++L_.with_sprite;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
std::stable_sort(parts.begin(), parts.end(), [](const SpriteInstance& a, const SpriteInstance& b) { return a.z > b.z; }); // far to near
|
||||||
|
bool drawn = !parts.empty();
|
||||||
|
if (!drawn && only_visible && st.st[0] == 0) return -1; // effects and scripts-only entities need no GPU slot
|
||||||
|
// items without a hitbox fall as a box the size of their image
|
||||||
|
if (st.st[0] == 4 && !first(*def, "HitboxComponent") && !parts.empty()) {
|
||||||
|
const SpriteInstance& p = parts.front();
|
||||||
|
st.box[0] = (float)-p.ox; st.box[1] = (float)(p.idle.w - p.ox);
|
||||||
|
st.box[2] = (float)-p.oy; st.box[3] = (float)(p.idle.h - p.oy);
|
||||||
|
if (st.box[1] - st.box[0] < 2) { st.box[0] = -1; st.box[1] = 1; }
|
||||||
|
if (st.box[3] - st.box[2] < 2) { st.box[2] = -1; st.box[3] = 1; }
|
||||||
|
}
|
||||||
|
L_.ents.push_back(st);
|
||||||
|
for (auto& p : parts) L_.inst.push_back(p);
|
||||||
|
for (Secondary& sc : secondary) {
|
||||||
|
EntityState ws = st;
|
||||||
|
const int ns = (int)L_.ents.size();
|
||||||
|
sc.si.ent = ns; sc.bp.slot = ns;
|
||||||
|
ws.box[0] = (float)-sc.si.ox; ws.box[1] = (float)(sc.si.idle.w - sc.si.ox); ws.box[2] = (float)-sc.si.oy; ws.box[3] = (float)(sc.si.idle.h - sc.si.oy);
|
||||||
|
L_.ents.push_back(ws);
|
||||||
|
L_.inst.push_back(sc.si);
|
||||||
|
L_.body_parts[slot].push_back(sc.bp);
|
||||||
|
L_.body_part_slots.insert(ns);
|
||||||
|
}
|
||||||
|
if (drawn) { ++L_.with_sprite; return slot; }
|
||||||
|
if (std::getenv("NG_PLACEHOLDERS")) std::fprintf(stderr, "placeholder %s at (%.0f, %.0f)%c", s.path.c_str(), s.x, s.y, 10);
|
||||||
|
if (const uint32_t col = marker_color(s.path)) { // placeholder 5x5 square so creatures/items can be located
|
||||||
|
const int off = (int)L_.texels.size();
|
||||||
|
L_.texels.insert(L_.texels.end(), 25, col);
|
||||||
|
SpriteInstance si;
|
||||||
|
si.ent = slot;
|
||||||
|
si.x = (int)std::floor(s.x); si.y = (int)std::floor(s.y);
|
||||||
|
si.idle.tex = si.walk.tex = (uint32_t)off;
|
||||||
|
si.idle.w = si.idle.h = si.walk.w = si.walk.h = 5;
|
||||||
|
si.ox = si.oy = 2;
|
||||||
|
L_.inst.push_back(si);
|
||||||
|
++L_.placeholders;
|
||||||
|
}
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityLayer build_entity_layer(const Wak& wak, const std::vector<EntitySpawn>& spawns, const EntitySpawn* player) {
|
||||||
|
EntityLayerBuilder b(wak);
|
||||||
|
if (player) b.add(*player, true);
|
||||||
|
for (const EntitySpawn& s : spawns) b.add(s, false);
|
||||||
|
return b.layer();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool body2_root_point(const EntityDef& d, double& rx, double& ry) {
|
||||||
|
rx = ry = 0;
|
||||||
|
const EntityComponent* pb = first(d, "PhysicsBody2Component");
|
||||||
|
if (!pb) return false;
|
||||||
|
const auto shapes = d.all("PhysicsImageShapeComponent");
|
||||||
|
const EntityComponent* root = shapes.empty() ? nullptr : shapes.front();
|
||||||
|
for (const EntityComponent* c : shapes) if (c->get("is_root", "0") == "1") { root = c; break; }
|
||||||
|
rx = std::atof(pb->get("root_offset_x", "0").c_str()) + (root ? std::atof(root->get("offset_x", "0").c_str()) : 0.0);
|
||||||
|
ry = std::atof(pb->get("root_offset_y", "0").c_str()) + (root ? std::atof(root->get("offset_y", "0").c_str()) : 0.0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool body2_spawn_shift(const EntityDef& d, double x, double y, double& dx, double& dy) {
|
||||||
|
// 00bdfa40 puts every image shape at the spawn position - init_offset (+1 on an axis where the position is negative; component +0x7c / +0x80, setter 0067bd90) + its own offset;
|
||||||
|
// the entity then follows the body at the root image's point root_offset (update_entity_transform). The entity's origin therefore moves by (root offset + root_offset) - init_offset.
|
||||||
|
dx = dy = 0;
|
||||||
|
const EntityComponent* pb = first(d, "PhysicsBody2Component");
|
||||||
|
double rx = 0, ry = 0;
|
||||||
|
if (!pb || !body2_root_point(d, rx, ry)) return false;
|
||||||
|
dx = rx - (std::atof(pb->get("init_offset_x", "0").c_str()) + (x < 0 ? 1.0 : 0.0));
|
||||||
|
dy = ry - (std::atof(pb->get("init_offset_y", "0").c_str()) + (y < 0 ? 1.0 : 0.0));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#pragma once
|
||||||
|
// Entity definitions (data/entities/**/*.xml) and a first drawable layer built from them.
|
||||||
|
//
|
||||||
|
// An entity file is an <Entity> with components as children. `<Base file="X">` loads X first; the children of the
|
||||||
|
// <Base> element are merged into the base's component of the same type (attributes override), components listed
|
||||||
|
// after it are added. Only what is needed to place and draw entities is interpreted (SpriteComponent); behaviour
|
||||||
|
// components are kept as raw attribute maps for later.
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/wak.h"
|
||||||
|
#include "entity/spawn_comp.h"
|
||||||
|
#include "core/xml.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct EntityComponent {
|
||||||
|
std::string type; // e.g. "SpriteComponent"
|
||||||
|
std::map<std::string, std::string> attrs;
|
||||||
|
std::string get(const char* k, const char* def = "") const {
|
||||||
|
auto it = attrs.find(k);
|
||||||
|
return it == attrs.end() ? def : it->second;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EntityDef {
|
||||||
|
std::string path, name, tags;
|
||||||
|
std::vector<EntityComponent> comps;
|
||||||
|
// inline child <Entity> elements (and those of Base files with include_children="1"): each is a definition of its own, cached under "<file>#c<k>",
|
||||||
|
// placed at its _Transform position relative to the parent
|
||||||
|
struct Child { std::string path, name; double x = 0, y = 0; };
|
||||||
|
std::vector<Child> children;
|
||||||
|
double sx = 1, sy = 1; // the scale of its own _Transform (the explosion flares: 5, 10)
|
||||||
|
std::vector<const EntityComponent*> all(const char* type) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EntityDb {
|
||||||
|
public:
|
||||||
|
explicit EntityDb(const Wak& wak) : wak_(wak) {}
|
||||||
|
// nullptr if the file is missing or malformed
|
||||||
|
const EntityDef* get(const std::string& path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool load(const std::string& path, EntityDef& out, int depth);
|
||||||
|
bool parse(const XmlNode& root, const std::string& path, EntityDef& out, int depth);
|
||||||
|
const Wak& wak_;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<EntityDef>> cache_;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EntitySpawn {
|
||||||
|
std::string path;
|
||||||
|
double x = 0, y = 0;
|
||||||
|
std::vector<SpawnComp> comps; // added by the script that loaded it
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulation state of one entity, uploaded as is to the GPU (std430: seven 16-byte rows, see shaders/entities.comp).
|
||||||
|
struct EntityState {
|
||||||
|
float pos[2] = {0, 0}, vel[2] = {0, 0}; // world cells, cells per frame
|
||||||
|
float box[4] = {-2, 2, -3, 3}; // collision box: min_x, max_x, min_y, max_y relative to the position
|
||||||
|
float mv[4] = {0.47f, 0.8f, -2.f, 0.1667f}; // walk speed, max speed x, jump velocity (negative = up), gravity (cells/frame)
|
||||||
|
float ai[4] = {100, 100, 12, 0.2f}; // detection range x, y, melee distance, melee damage
|
||||||
|
int32_t st[4] = {0, 1, 0, 0}; // kind (0 static, 1 walker, 2 flyer, 3 player, 4 physics item), facing (+1/-1), ai timer, flags
|
||||||
|
float hp[4] = {1, 0, 5.8f, 4}; // hp, attack cooldown (frames), max fall speed, climb-over height
|
||||||
|
int32_t x[4] = {0, 0, 0, 0}; // damage taken this frame (milli-hp, atomic), jetpack energy / idle flag, spawn x (player) / ranged cooldown, spawn y (player) / max hp in milli-hp (creatures)
|
||||||
|
int32_t rg[4] = {-1, 100, 0, 180}; // ranged attack: projectile type (-1 none), frames between shots, min distance, max distance
|
||||||
|
int32_t rg2[4] = {1, 1, -4, 0}; // projectiles per shot (min, max), muzzle offset y, reserved
|
||||||
|
int32_t aux[4] = {0, 0, 0, 0}; // player: maximum hp (milli-hp); creatures: velocity of the last hit, written by the projectiles
|
||||||
|
float ex[4] = {-1, -8, 1, 0}; // detection cone (cos of the half angle, -1 = all around), eye offset y, melee attack enabled, reserved
|
||||||
|
float ex2[4] = {0, 0, 0, 0}; // ranged attack: speed and gravity of the projectile (cells per frame, per frame squared), for aiming
|
||||||
|
int32_t ex3[4] = {0, 0, 0, 0}; // creatures: blood material + 1, blood colour, damage types taken (written by the GPU), types of the killing blow
|
||||||
|
float ex4[4] = {0, 0, 0, 0}; // air in the lungs (frames), air maximum (0 = no air needed), damage per frame without air, frames left on fire
|
||||||
|
float ex5[4] = {1.2f, 0.9f, 0.95f, 0.9f}; // swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag (CharacterPlatformingComponent constructor 009e91f0: 1.2, 0.9, 0.95, 0.9)
|
||||||
|
float ex6[4] = {0.7f, 0, 1e9f, 1e9f}; // swim_down_buoyancy_coeff (default 0.7), frames not swimming, pixel of the last liquid count (x, y)
|
||||||
|
float ex7[4] = {1.0f, 0, 0, 0}; // blood_multiplier; liquid inventory of a corpse part: reserve, material + 1, frames of emission left
|
||||||
|
int32_t ex8[4] = {0, 0, 0, 0}; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending. Creatures and the player: x = game effect bits (1 movement disabled, 2 charmed), y = herd + 1, z = stains | frame << 8, w = last attacker (slot + 1 | frame << 12)
|
||||||
|
float ex9[4] = {1, 1, 1, 1}; // creatures and the player: damage taken multipliers (projectile, explosion, fire, melee); the host sets them from the game effects (protections)
|
||||||
|
float ex10[4] = {0, 0, 0, 0}; // creatures and the player: the knockback state of CharacterDataComponent / CharacterPlatformingComponent: x = horizontal velocity of the kick (cells per frame), y = frames since it, z = 1 while in the state, w = VelocityComponent liquid_drag
|
||||||
|
float ex11[4] = {0, 0, 0, 0}; // creatures and the player: x = DamageModelComponent.minimum_knockback_force (default 0), y z = the pending kick, w = the frame of the last hit a protection took to nothing (the GPU; BLOCK on the host)
|
||||||
|
// the damage multipliers of the DamageModelComponent times the protections (TakeDamage_Impl 00ab9a90: the multiplier of the damage type, then 00b06d20 zeroes protected types):
|
||||||
|
float ex12[4] = {1, 1, 1, 1}; // electricity (0x80), drill (0x400), slice (0x800), ice (0x1000); ex9 holds projectile (2), explosion (4), fire (0x10), melee (1)
|
||||||
|
float ex13[4] = {1, 1, 1, 1}; // healing (0x2000), physics_hit (0x4000), radioactive (0x8000), poison (0x10000)
|
||||||
|
float ex14[4] = {1, 1, 1, 0}; // overeating (0x40000), curse (0x80000), holy (0x100000), reserved
|
||||||
|
float ex15[4] = {0, 0, 0, 0}; // creatures: the host's AnimalAI steering (src/ai): x, y = the next path node (world pixels), z = mode (0 the GPU's own AI, 1 go to the node, 2 stand), w = flags (1 flying)
|
||||||
|
float ex16[4] = {1, 0.1f, 0, 0}; // walkers and the player: CharacterPlatformingComponent accel_x, accel_x_air (constructor 009e91f0: 1, 0.1; per-frame lerp factors, 00a7ea90); mFireProbability; frames out of liquid (air refill, 00ab81b0)
|
||||||
|
float ex17[4] = {0, 5, 0, 0}; // flyers: CharacterPlatformingComponent fly_speed_mult, fly_speed_change_spd (constructor 009e91f0: 0, 5), fly_speed_max_up, fly_speed_max_down (cells per frame)
|
||||||
|
float ex18[4] = {0.5f, 1, 0, -1.0e9f}; // flyers: fly_velocity_x clamped to velocity_min_x / max_x (cells per frame; default 30 px/s), fly_smooth_y, mFlyingTargetY, mSmoothedFlyingTargetY (-1e9: not yet set)
|
||||||
|
float ex19[4] = {0, 0, 0, 0}; // creatures and the player: x DamageModelComponent critical_damage_resistance (+0xa8 in TakeDamage 00ab9a90), yz the type + 1 and frame of the last projectile hit (GPU); worm parts: z the hitbox radius, w the root slot + 1
|
||||||
|
};
|
||||||
|
enum EntityFlags : int32_t { kEntGrounded = 1, kEntCanJump = 2, kEntCanFly = 4, kEntDead = 8, kEntInLiquid = 16 };
|
||||||
|
|
||||||
|
// One animation: `frames` frames of w x h texels (ARGB) starting at texel offset `tex`, each shown for `wait` game frames.
|
||||||
|
struct AnimRef {
|
||||||
|
uint32_t tex = 0;
|
||||||
|
int w = 0, h = 0, frames = 1, wait = 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One drawn sprite: placed with its top-left at (entity position - origin offset). Position comes from the entity state.
|
||||||
|
// Mobile entities show `walk` while moving and `idle` otherwise.
|
||||||
|
struct SpriteInstance {
|
||||||
|
int ent = 0; // index into EntityLayer::ents
|
||||||
|
int x = 0, y = 0; // spawn position (world cells), informational
|
||||||
|
AnimRef idle, walk;
|
||||||
|
int ox = 0, oy = 0;
|
||||||
|
float z = 0;
|
||||||
|
std::string file; // the sprite xml of a SpriteComponent part (empty: a body image), its additive flag: the host gives every part the animation it plays
|
||||||
|
bool additive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A projectile kind used by creatures' ranged attacks (cells per frame).
|
||||||
|
struct ProjectileType {
|
||||||
|
std::string file; // the projectile entity file
|
||||||
|
float speed_min = 5, speed_max = 5, damage = 0.3f, lifetime = 120, gravity = 0;
|
||||||
|
uint32_t color = 0xFFFFD040u;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EntityLight { int slot = 0; float radius = 0, r = 1, g = 1, b = 1; }; // a LightComponent of an entity (the position is the entity's)
|
||||||
|
// A second body of a prop made of several bodies joined by a PhysicsJointComponent (the cart: the box and a wheel, images on one canvas): the entity slot of its own, the joint point in image coordinates
|
||||||
|
// A PhysicsJoint2Component body (the chain of a temple lantern, made by its script): body_id / parent_id name the bodies, the joint point is relative to the entity origin (local),
|
||||||
|
// nail: a REVOLUTE_JOINT_ATTACH_TO_NEARBY_SURFACE on this body (nail_x / nail_y relative to the origin, nail_bf its break_force)
|
||||||
|
// PhysicsBody2Component: the point of the shared shape/joint space that is the entity origin once the body exists (root shape offset + root_offset); false without the component
|
||||||
|
bool body2_root_point(const EntityDef& d, double& rx, double& ry);
|
||||||
|
// ... and how far that origin is from the spawn point (the shapes were placed by init_offset: 00bdfa40)
|
||||||
|
bool body2_spawn_shift(const EntityDef& d, double x, double y, double& dx, double& dy);
|
||||||
|
// VerletPhysicsComponent wind: the host's WorldStateComponent.wind times the sky exposure at a world point (00c3a610 / 00746620); unset = no wind
|
||||||
|
inline std::function<float(double x, double y)> verlet_wind_at;
|
||||||
|
struct BodyPart { int slot = -1; double joint_x = 0, joint_y = 0; std::string material; double angular_damping = 0;
|
||||||
|
std::string body_id, parent_id; bool local = false; double joint_bf = 0; int joint_id = 0; bool nail = false; double nail_x = 0, nail_y = 0, nail_bf = 0, nail_rx = 0, nail_ry = -10; }; // (nail_rx / ry: the joint's ray_x / ray_y, the surface search)
|
||||||
|
|
||||||
|
struct EntityLayer {
|
||||||
|
std::map<int, std::vector<BodyPart>> body_parts; // main slot -> its other bodies
|
||||||
|
std::set<int> body_part_slots;
|
||||||
|
// a limb boss (LimbBossComponent): root slot -> its limbs, the child entities with an IKLimbComponent; each sprite of a limb (the two segments, the knee) is a part
|
||||||
|
// entity of its own placed, turned and stretched by the host (st[3] 64: drawn along ex7.x, ex7.y times as long)
|
||||||
|
struct LimbParts { std::string child; int first = 0, n = 0; bool attacker = false; std::vector<int> w; };
|
||||||
|
std::map<int, std::vector<LimbParts>> limb_parts;
|
||||||
|
struct Hair { int slot = -1; float ox = 0, oy = 0; }; // its Verlet children (the hair): the chain's root slot (joint type 3: its first point on the root) and its offset
|
||||||
|
std::map<int, std::vector<Hair>> limb_hair;
|
||||||
|
std::map<int, std::pair<int, int>> worm_parts; // a worm (WormComponent): root slot -> (first part slot, number of parts); one part per sprite, moved by the host
|
||||||
|
std::vector<EntityLight> lights;
|
||||||
|
std::vector<uint32_t> texels;
|
||||||
|
std::vector<SpriteInstance> inst;
|
||||||
|
std::vector<EntityState> ents; // one per spawn (index 0 is the player when requested)
|
||||||
|
std::vector<ProjectileType> projectiles;
|
||||||
|
int entities = 0, with_sprite = 0, placeholders = 0, missing_defs = 0;
|
||||||
|
std::map<std::string, int> by_path; // spawn counts, for reports
|
||||||
|
};
|
||||||
|
|
||||||
|
// First frame of a sprite (png or sprite xml + animation name) as ARGB texels; false if it cannot be loaded.
|
||||||
|
bool load_first_frame(const Wak& wak, const std::string& file, const std::string& animation, std::vector<uint32_t>& texels, int& w, int& h);
|
||||||
|
|
||||||
|
// Physics images such as the gold nuggets are drawn in key colours (a handful of multiples of 32); the body shows its material instead:
|
||||||
|
// recolours such an image with the material colour (opaque ARGB), keeping a little of the shading. Returns false for ordinary art.
|
||||||
|
bool tint_key_image(std::vector<uint32_t>& texels, uint32_t material_color);
|
||||||
|
bool tint_key_image(uint32_t* texels, size_t count, uint32_t material_color);
|
||||||
|
|
||||||
|
// A whole animation: frames of w x h texels (ARGB) one after the other, `wait` game frames per frame, origin offset of the sprite file.
|
||||||
|
struct SpriteFrames {
|
||||||
|
std::vector<uint32_t> texels;
|
||||||
|
int w = 0, h = 0, frames = 1, wait = 6;
|
||||||
|
float off_x = 0, off_y = 0;
|
||||||
|
bool ok() const { return w > 0 && h > 0 && !texels.empty(); }
|
||||||
|
const uint32_t* frame(int i) const { return texels.data() + (size_t)(((i % frames) + frames) % frames) * w * h; }
|
||||||
|
};
|
||||||
|
bool load_sprite_frames(const Wak& wak, const std::string& file, const std::string& animation, SpriteFrames& out);
|
||||||
|
|
||||||
|
class EntityLayerBuilder;
|
||||||
|
|
||||||
|
// Draws the first frame of each entity's default animation. Entities without a drawable sprite that look like
|
||||||
|
// creatures or items get a small coloured marker so they are visible.
|
||||||
|
// player: optional controllable character (data/entities/player.xml), placed at index 0.
|
||||||
|
EntityLayer build_entity_layer(const Wak& wak, const std::vector<EntitySpawn>& spawns, const EntitySpawn* player = nullptr);
|
||||||
|
|
||||||
|
// Incremental version: entities can be added while the game runs (loot, drops); the host uploads what was appended.
|
||||||
|
class EntityLayerBuilder {
|
||||||
|
public:
|
||||||
|
explicit EntityLayerBuilder(const Wak& wak) : wak_(wak), db_(wak) { L_.texels.assign(1, 0u); }
|
||||||
|
// Adds an entity; returns its slot in layer().ents, or -1 when only_visible is set and it has nothing to show
|
||||||
|
// (and is not a creature/item). Slots equal indices of EntityLayer::ents.
|
||||||
|
int add(const EntitySpawn& s, bool is_player, bool only_visible = false);
|
||||||
|
int add_projectile_type(const ProjectileType& t);
|
||||||
|
EntityLayer& layer() { return L_; }
|
||||||
|
// A texture made of the given texels (cached by key); returns its texel offset.
|
||||||
|
int add_texels(const std::string& key, const std::vector<uint32_t>& px);
|
||||||
|
// An entity that is only a sprite and a body (ragdoll parts): kind 4, cannot be damaged. `reuse` >= 0 recycles that released slot.
|
||||||
|
// Returns the slot; *inst receives the index of its sprite instance.
|
||||||
|
int add_body_entity(int tex, int w, int h, int ox, int oy, double x, double y, int reuse, int* inst);
|
||||||
|
int first_instance(int slot) const;
|
||||||
|
// an animation of a sprite file, its texels added to the layer when new (the host plays the attack animations with it)
|
||||||
|
auto animation(const std::string& file, const std::string& name) { return get_anim(file, name, true); } // index of the first sprite instance of an entity, -1 when it has none
|
||||||
|
// Gives the first sprite instance of an entity another image and origin (a generated wand: SetWandSprite changes the image and the grip offset after the entity exists); returns the instance index, -1 on failure.
|
||||||
|
int set_sprite(int slot, const std::string& image_file, const std::string& animation, int ox, int oy);
|
||||||
|
EntityDb& db() { return db_; }
|
||||||
|
// material name -> index (set by the host); destructible props list the materials that hurt them
|
||||||
|
std::function<int(const std::string&)> material_lookup;
|
||||||
|
std::function<uint32_t(const std::string&)> material_color; // opaque colour of a material (blood of creatures)
|
||||||
|
|
||||||
|
struct AnimEntry { int w = 0, h = 0, frames = 1, wait = 6; float off_x = 0, off_y = 0; bool ok = false; };
|
||||||
|
// every sprite part of an entity made from a SpriteComponent: (instance index, the animation of that part's file, or null when it has none)
|
||||||
|
std::vector<std::pair<int, const std::pair<int, AnimEntry>*>> part_animations(int slot, const std::string& name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::pair<int, AnimEntry>* get_anim(const std::string& file, const std::string& name, bool strict, bool additive = false);
|
||||||
|
std::map<std::string, int> raw_tex_;
|
||||||
|
std::set<int> tinted_;
|
||||||
|
const Wak& wak_;
|
||||||
|
EntityDb db_;
|
||||||
|
EntityLayer L_;
|
||||||
|
std::map<std::string, std::pair<int, AnimEntry>> cache_;
|
||||||
|
std::map<std::string, int> proj_ids_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
// The engine services behind the Lua API functions that reach past the entity/component model (cells, camera, input, music, inventory,
|
||||||
|
// statuses, rigid bodies, the GUI): the host (src/app/main.cpp) fills the hooks; an unset hook makes the function return its "nothing
|
||||||
|
// there" answer (nil, false, 0) as the engine does when the thing it asks about does not exist. src/entity/runtime_natives.cpp uses them.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct GameServices {
|
||||||
|
// ---- cells (world coordinates; material index, -1 = not loaded) ----
|
||||||
|
std::function<int(int x, int y)> cell_at;
|
||||||
|
std::function<void(int x, int y, int material)> set_cell; // material 0 = air
|
||||||
|
std::function<bool(int x0, int y0, int x1, int y1)> world_loaded; // DoesWorldExistAt
|
||||||
|
std::function<void(int from, int to)> convert_everywhere; // (ConvertEverythingToGold uses it per material)
|
||||||
|
std::function<std::vector<int>()> materials_present; // the materials in the loaded window
|
||||||
|
std::function<double(double x, double y)> sky_visibility; // 0..1
|
||||||
|
std::function<int(double x, double y)> fog_get; // 0..255, -1 outside
|
||||||
|
std::function<bool(double x, double y, int v)> fog_set;
|
||||||
|
std::function<void(int x, int y_min, int y_max, double radius, double edge)> cut_vertical; // GameCutThroughWorldVertical
|
||||||
|
// ---- particles and sprites ----
|
||||||
|
std::function<void(const std::string& material, double x, double y, int count, double vx, double vy, bool just_visual, bool draw_as_long, bool randomize_velocity)> create_particles;
|
||||||
|
std::function<void(const std::string& material, double x, double y, int count, double vx, double vy, uint32_t color, double life_min, double life_max,
|
||||||
|
bool force_create, bool collide, bool randomize_velocity, double gx, double gy)> create_cosmetic_particles; // (force_create 0: only where the cell is empty, 00716ce0)
|
||||||
|
std::function<void(const std::string& file, double x, double y, bool centered, double ox, double oy, int frames, bool emissive)> sprite_for_frames;
|
||||||
|
std::function<void(int count, double width_outside, const std::string& material, double vmin, double vmax, double gravity, bool bounce, bool draw_long)> rain;
|
||||||
|
std::function<void(const std::string& file, double x, double y, double z, bool check_corners)> load_background;
|
||||||
|
std::function<bool(const std::string& file, double x, double y)> remove_background;
|
||||||
|
std::function<void(double x0, double y0, double x1, double y1)> remove_backgrounds;
|
||||||
|
std::function<int(double x, double y, const std::string& image, int max_durability)> loose_chunk; // LooseChunk: a body from the cells under the image
|
||||||
|
std::function<void(int entity, const std::string& material, bool use_stain_colors, bool kill, bool use_sprite_colors, double offset_x, double offset_y)> entity_to_material;
|
||||||
|
// ---- camera, mouse, keyboard ----
|
||||||
|
std::function<void(double x, double y)> camera_set;
|
||||||
|
std::function<void(bool free)> camera_free;
|
||||||
|
std::function<void(double& x, double& y)> mouse_world;
|
||||||
|
std::function<void(double& x, double& y)> mouse_screen;
|
||||||
|
std::function<bool(int key, int mode)> key; // mode 0 down, 1 went down this frame, 2 went up this frame (SDL scancodes, data/scripts/debug/keycodes.lua)
|
||||||
|
std::function<bool(int button, int mode)> mouse_button; // Mouse_left 1, Mouse_right 2, Mouse_middle 3, wheel up 4, wheel down 5
|
||||||
|
std::function<void(double x, double y, const std::string& text, double r, double g, double b)> debug_mark;
|
||||||
|
// ---- sound and music ----
|
||||||
|
std::function<void(const std::string& event, bool can_be_faded, double x, double y)> music_event;
|
||||||
|
std::function<void(const std::string& cue)> music_cue;
|
||||||
|
std::function<void(double seconds)> music_fade_out;
|
||||||
|
// ---- inventory ----
|
||||||
|
std::function<std::vector<int>(int owner)> inventory_items;
|
||||||
|
std::function<void(int owner, int item)> kill_inventory_item;
|
||||||
|
std::function<void(int who, int item, bool effects)> pick_up_item;
|
||||||
|
std::function<void(int owner, int mode)> drop_items; // 0 GameDropAllItems, 1 GameDropPlayerInventoryItems, 2 GameDestroyInventoryItems
|
||||||
|
std::function<bool()> inventory_open;
|
||||||
|
// ---- stains and ingestion (src/entity/status) ----
|
||||||
|
std::function<void(int entity, int material, double amount)> ingest;
|
||||||
|
std::function<void(int entity, const std::string& status)> remove_ingestion;
|
||||||
|
std::function<void(int entity, const std::string& status, int cooldown)> remove_stain;
|
||||||
|
std::function<void(int entity, int material, double amount)> add_stains;
|
||||||
|
// ---- rigid bodies (body ids are 1-based: the host's body index + 1) ----
|
||||||
|
std::function<std::vector<int>(int entity)> body_ids;
|
||||||
|
std::function<std::vector<int>(double x0, double y0, double x1, double y1, bool include_static)> bodies_in_box;
|
||||||
|
std::function<bool(int body, double* out6)> body_get; // x, y (box2d units), angle, vx, vy (box2d), angular velocity
|
||||||
|
std::function<void(int body, const double* in6, int n)> body_set; // the first n of the six
|
||||||
|
std::function<void(int body, double fx, double fy, double wx, double wy, bool at_point, bool impulse)> body_force;
|
||||||
|
std::function<void(int body, double torque)> body_torque;
|
||||||
|
std::function<bool(int body, double& linear, double& angular)> body_damping_get;
|
||||||
|
std::function<void(int body, double linear, double angular, bool set_angular)> body_damping_set;
|
||||||
|
std::function<bool(int body, double& g)> body_gravity_get;
|
||||||
|
std::function<void(int body, double g)> body_gravity_set;
|
||||||
|
std::function<bool(int body, double* aabb4)> body_aabb;
|
||||||
|
std::function<void(int entity, bool is_static)> set_static;
|
||||||
|
std::function<int(int entity, const std::string& image, const std::string& material, double ox, double oy, bool centered, bool circle, int w, int h)> add_body; // image empty: a box w x h
|
||||||
|
std::function<int(int entity, int body0, int body1, double ox, double oy, const std::string& type)> add_joint;
|
||||||
|
std::function<void(double x, double y, double radius, double force)> verlet_circular;
|
||||||
|
std::function<void(int entity, int comp, double fx, double fy)> verlet_directional;
|
||||||
|
// ---- game flow ----
|
||||||
|
std::function<void(const std::string& map_file, const std::string& pixel_scenes)> biome_map_load; // BiomeMapLoad_KeepPlayer: the world is made again from that map, the player stays
|
||||||
|
std::function<void()> game_over;
|
||||||
|
std::function<void(int ending)> ending; // 1 GameOnCompleted, 2 GameDoEnding2
|
||||||
|
std::function<void(double x, double y)> set_spawn_location;
|
||||||
|
std::function<int(const std::string& file, double x, double y, double impulse_x, double impulse_y, int entity)> ragdoll; // LoadRagdoll
|
||||||
|
std::function<void(int entity, const std::string& name, int priority, const std::string& followup, int followup_priority)> play_animation;
|
||||||
|
std::function<void(int entity, int comp)> refresh_sprite;
|
||||||
|
std::function<bool(int entity, const std::string& tag, double& x, double& y)> hotspot;
|
||||||
|
// ---- GUI (data/scripts/lib/... and mods): an immediate-mode draw list the HUD renders every frame ----
|
||||||
|
struct GuiItem { int kind = 0; double x = 0, y = 0, w = 0, h = 0, scale = 1, alpha = 1, z = 0, r = 1, g = 1, b = 1; std::string text, image; }; // kind 0 text, 1 image, 2 nine-piece
|
||||||
|
std::function<void(const GuiItem&)> gui_draw;
|
||||||
|
std::function<void(double& w, double& h)> gui_screen; // the GUI's virtual screen
|
||||||
|
std::function<void(const std::string& text, double scale, double& w, double& h)> gui_text_size;
|
||||||
|
std::function<bool(const std::string& image, double& w, double& h)> gui_image_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<GameServices::GuiItem> gui_draw_list(); // every GUI's widgets of its last frame, back to front (entity/runtime_natives.cpp)
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#include "entity/herds.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
namespace {
|
||||||
|
std::map<std::string, int> g_herds;
|
||||||
|
std::vector<std::vector<int>> g_rel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void herds_load(const Wak& wak) {
|
||||||
|
if (!g_herds.empty()) return;
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (!wak.read("data/genome_relations.csv", raw)) return;
|
||||||
|
std::istringstream in(std::string((const char*)raw.data(), raw.size()));
|
||||||
|
std::string line;
|
||||||
|
if (!std::getline(in, line)) return;
|
||||||
|
std::stringstream hs(line);
|
||||||
|
std::string cell;
|
||||||
|
std::getline(hs, cell, ','); // "HERD"
|
||||||
|
int i = 0;
|
||||||
|
while (std::getline(hs, cell, ',')) {
|
||||||
|
while (!cell.empty() && (cell.back() == '\r' || cell.back() == ' ')) cell.pop_back();
|
||||||
|
g_herds[cell] = i++;
|
||||||
|
}
|
||||||
|
while (std::getline(in, line)) { // the rows: a name, then the relation to every herd
|
||||||
|
std::stringstream rs(line);
|
||||||
|
std::getline(rs, cell, ',');
|
||||||
|
std::vector<int> row;
|
||||||
|
while (std::getline(rs, cell, ',')) row.push_back(std::atoi(cell.c_str()));
|
||||||
|
g_rel.push_back(std::move(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int herd_of(const std::string& name) {
|
||||||
|
auto it = g_herds.find(name);
|
||||||
|
return it == g_herds.end() ? -1 : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int herd_count() { return (int)g_herds.size(); }
|
||||||
|
|
||||||
|
std::string herd_name(int herd) { for (const auto& [n, i] : g_herds) if (i == herd) return n; return ""; }
|
||||||
|
|
||||||
|
int herd_relation(int a, int b) { return a >= 0 && b >= 0 && a < (int)g_rel.size() && b < (int)g_rel[a].size() ? g_rel[a][b] : 0; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
// The herds of the creatures (data/genome_relations.csv): GenomeDataComponent.herd_id names one of them. A projectile does not hurt creatures of the herd of its shooter
|
||||||
|
// (mShooterHerdId) unless it has friendly_fire; everything else it touches is hurt.
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/wak.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
void herds_load(const Wak& wak); // reads the table once
|
||||||
|
int herd_of(const std::string& name); // index in the table, -1 when the name is unknown ("-1" is a herd of its own, the default of creatures)
|
||||||
|
int herd_count();
|
||||||
|
std::string herd_name(int herd); // the name of a herd index ("" when unknown)
|
||||||
|
int herd_relation(int a, int b); // data/genome_relations.csv: 0..100 (the first herd's row, the second herd's column), 0 for unknown herds
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
// The contents of a flask or barrel: MaterialInventoryComponent holds how much of each material it has (count_per_material_type). A flask keeps its materials
|
||||||
|
// apart (no reactions inside: do_reactions is 0 for potions), they react when they are poured out or spilled into the world.
|
||||||
|
// Representation on the component: count_per_material_type.mix = "water:700;radioactive_liquid:300", plus the main material (the largest part) and the total count
|
||||||
|
// under count_per_material_type.Material.material / .count (what scripts and the HUD read).
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "lua/entity_api.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct MixPart { std::string mat; double n = 0; };
|
||||||
|
|
||||||
|
inline std::vector<MixPart> mix_get(IEntityWorld& w, int comp) {
|
||||||
|
std::vector<MixPart> parts;
|
||||||
|
const LVal mix = w.get_value(comp, "count_per_material_type.mix", false);
|
||||||
|
if (mix.t == LVal::T::Str && !mix.s.empty()) {
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < mix.s.size()) {
|
||||||
|
size_t end = mix.s.find(';', pos);
|
||||||
|
if (end == std::string::npos) end = mix.s.size();
|
||||||
|
const std::string item = mix.s.substr(pos, end - pos);
|
||||||
|
const size_t c = item.rfind(':');
|
||||||
|
if (c != std::string::npos) parts.push_back({item.substr(0, c), std::atof(item.c_str() + c + 1)});
|
||||||
|
pos = end + 1;
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
const LVal m = w.get_value(comp, "count_per_material_type.Material.material", false), n = w.get_value(comp, "count_per_material_type.Material.count", false);
|
||||||
|
if (m.t == LVal::T::Str && !m.s.empty() && n.t == LVal::T::Str && std::atof(n.s.c_str()) > 0) parts.push_back({m.s, std::atof(n.s.c_str())});
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline double mix_total(const std::vector<MixPart>& p) { double t = 0; for (const MixPart& x : p) t += x.n; return t; }
|
||||||
|
|
||||||
|
inline void mix_set(IEntityWorld& w, int comp, std::vector<MixPart> parts) {
|
||||||
|
parts.erase(std::remove_if(parts.begin(), parts.end(), [](const MixPart& x) { return x.n < 0.5; }), parts.end());
|
||||||
|
std::stable_sort(parts.begin(), parts.end(), [](const MixPart& a, const MixPart& b) { return a.n > b.n; });
|
||||||
|
std::string s;
|
||||||
|
for (const MixPart& x : parts) s += (s.empty() ? "" : ";") + x.mat + ":" + std::to_string((long long)std::llround(x.n));
|
||||||
|
w.set_value(comp, "count_per_material_type.mix", LVal::string(s));
|
||||||
|
w.set_value(comp, "count_per_material_type.Material.material", LVal::string(parts.empty() ? std::string() : parts.front().mat));
|
||||||
|
w.set_value(comp, "count_per_material_type.Material.count", LVal::string(std::to_string((long long)std::llround(mix_total(parts)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void mix_add(std::vector<MixPart>& parts, const std::string& mat, double n) {
|
||||||
|
for (MixPart& x : parts) if (x.mat == mat) { x.n += n; return; }
|
||||||
|
parts.push_back({mat, n});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,250 @@
|
|||||||
|
#pragma once
|
||||||
|
// CPU-side game entities with Noita's component model: what Lua scripts see through EntityGetFirstComponent,
|
||||||
|
// ComponentGetValue2, EntityLoad, EntityKill, ... (lua/entity_api.h). Positions of entities that also live on the GPU are
|
||||||
|
// synchronised by the host; item pickup, chest opening and loot spawning run here, through the original scripts.
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entity/entity.h"
|
||||||
|
#include "entity/game_services.h"
|
||||||
|
#include "lua/lua_host.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct RtComponent {
|
||||||
|
int id = 0, entity = 0;
|
||||||
|
std::string type;
|
||||||
|
std::map<std::string, std::string> attrs;
|
||||||
|
bool enabled = true;
|
||||||
|
std::set<std::string> tags;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RtEntity {
|
||||||
|
int id = 0;
|
||||||
|
std::string file, name;
|
||||||
|
std::set<std::string> tags;
|
||||||
|
double x = 0, y = 0, rot = 0, sx = 1, sy = 1;
|
||||||
|
bool alive = true;
|
||||||
|
std::vector<int> comps;
|
||||||
|
int parent = 0;
|
||||||
|
int born = 0;
|
||||||
|
int died = -1; // the frame of kill(); the record and its components are erased a few frames later (tick)
|
||||||
|
bool parked = false; // in a part of the world far from the player (main.cpp parkEntity): kept as it is but not alive (no scripts, no queries, no slot simulated) until it wakes
|
||||||
|
bool follow = false; double lx = 0, ly = 0;
|
||||||
|
bool inline_child = false; // made from an inline <Entity> of its parent's file (no GPU slot of its own yet) // a child added to a parent keeps its offset from it (the parent's position is synced from the GPU)
|
||||||
|
std::vector<SpawnComp> extra; // components a world-generation script added (perks)
|
||||||
|
bool cpu_motion = false; // moved by the runtime itself (VelocityComponent + SimplePhysicsComponent + DieIfSpeedBelowComponent, as the engine does): entities with scripts that keep running
|
||||||
|
int gpu = -1; // GPU entity slot, -1 = none (effects, markers, scripts-only entities)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GameMessage {
|
||||||
|
std::string title, text;
|
||||||
|
bool important = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EntityRuntime : public IEntityWorld {
|
||||||
|
public:
|
||||||
|
EntityRuntime(const Wak& wak, LuaHost& host);
|
||||||
|
|
||||||
|
// ---- the engine services behind the rest of the Lua API (src/entity/runtime_natives.cpp; the host fills the hooks) ----
|
||||||
|
GameServices svc;
|
||||||
|
int max_entity_id() const { return next_ent_ - 1; }
|
||||||
|
const std::map<int, RtEntity>& entities() const { return ents_; }
|
||||||
|
struct Timeout { int frame = 0, entity = 0, comp = 0; std::string file, fn; };
|
||||||
|
std::vector<Timeout> timeouts; // SetTimeOut: run at that frame (tick)
|
||||||
|
std::set<int> orbs_run, orbs_all; // OrbComponent orb_id of the orbs picked this run / ever (the host may load the latter)
|
||||||
|
std::map<std::string, std::vector<float>> postfx; // GameSetPostFxParameter: name -> x, y, z, w (the renderer reads them)
|
||||||
|
std::map<std::string, std::string> postfx_textures;
|
||||||
|
void run_callback(int entity, int comp, const std::string& file, const std::string& fn, const std::vector<LVal>& args) { run_script_callback(entity, comp, file, fn, args); }
|
||||||
|
|
||||||
|
// Creates an entity. forced_id > 0 assigns that id (initial entities use id = GPU slot + 1).
|
||||||
|
// notify = false: the new entity never gets a GPU slot (cards inside wands).
|
||||||
|
int spawn(const std::string& path, double x, double y, int forced_id = 0, bool notify = true, const std::vector<SpawnComp>* extra = nullptr);
|
||||||
|
RtEntity* ent(int id);
|
||||||
|
RtComponent* comp(int id);
|
||||||
|
|
||||||
|
// ---- host side ----
|
||||||
|
// Return true only when a compiled GPU backend has accepted this exact call.
|
||||||
|
std::function<bool(int entity, int component, const std::string& file, const std::string& function, const std::vector<LVal>& args)> on_gpu_script;
|
||||||
|
void set_frame(int f) { frame_ = f; }
|
||||||
|
std::vector<int>* spawn_log = nullptr; // when set, every entity created by spawn() (including the ones LoadEntitiesComponents load) is appended
|
||||||
|
std::function<int(int x0, int y0, int x1, int y1)> cells_in_rect; // the cells (not air, not gas) in the rectangle x0..x1, y0..y1 inclusive: PressurePlateComponent (00c01860)
|
||||||
|
std::function<void(int id, double fade)> on_sprite_fade; // LifetimeComponent fade_sprites (00b9a010): every frame the sprites' alpha is 1 - fade, fade = age / lifetime
|
||||||
|
std::function<void(int id)> on_spawned; // after a script-created entity exists (host may give it a GPU slot)
|
||||||
|
std::function<void(int id)> on_killed;
|
||||||
|
std::function<void(int effect_entity, int comp)> on_game_effect_end; // a GameEffectComponent whose frames run out (00b015d0: the counter reaches 0), before its entity goes
|
||||||
|
std::function<void(int id)> on_physics_init; // PhysicsBody2InitFromComponents: the host makes the bodies again from the components
|
||||||
|
std::function<void(double x, double y)> on_set_fire; // an ElectricityComponent with hack_is_set_fire (fire.xml): the host lights the cells there
|
||||||
|
std::function<void(int id, int comp)> on_component_added; // a script added a component (EntityAddComponent): perk_spawn gives a loaded perk.xml its sprite
|
||||||
|
std::function<void(int id)> on_components_enabled; // a component of the entity was switched on or off
|
||||||
|
std::function<std::string(int item)> display_name; // the name the game shows for an item (spells: the translated action name, wands: their own name); empty = the ItemComponent's
|
||||||
|
std::function<bool(int item)> allow_pick; // Message_ItemAllowedToPickUp: the inventory has a free place for the item (and is closed); false = it stays where it is
|
||||||
|
std::function<void(int id)> on_hidden; // picked up into the inventory: still alive, no longer in the world
|
||||||
|
std::function<void(int id, const std::string& type, const std::string& key)> on_value_changed; // component value written by a script
|
||||||
|
std::vector<GameMessage> take_messages();
|
||||||
|
const std::vector<std::string>& script_errors() const { return errors_; }
|
||||||
|
void clear_script_errors() { errors_.clear(); }
|
||||||
|
|
||||||
|
// Gameplay: called once per frame with the player's position. interact = the E key went down this frame.
|
||||||
|
// Returns the ids of items picked up (their pickup scripts have run).
|
||||||
|
std::vector<int> pickup_step(double player_x, double player_y, bool interact);
|
||||||
|
// Applies lifetimes (LifetimeComponent) and similar per-frame upkeep.
|
||||||
|
void tick();
|
||||||
|
void load_entities(int id, int lc, bool notify); // a LoadEntitiesComponent loads its entities (at spawn, or when its timeout_frames timer comes)
|
||||||
|
long script_runs() const { return script_runs_; } // periodic script executions so far (debug)
|
||||||
|
// The area whose entities run their scripts (the loaded world around the camera): LuaComponents with execute_every_n_frame, CollisionTriggerComponents.
|
||||||
|
void set_active_area(double cx, double cy, double hw, double hh) { act_cx_ = cx; act_cy_ = cy; act_hw_ = hw; act_hh_ = hh; scripts_on_ = true; }
|
||||||
|
// Runs the script_death scripts of an entity (death(damage types, message, killer, drop_items)): the gold drop, loot, ... The caller kills the entity afterwards.
|
||||||
|
void run_death_scripts(int id, int damage_types, int killer);
|
||||||
|
// A message of the engine to an entity (Message_Kick, Message_PortalTeleportUsed, Message_WandFired...): every enabled LuaComponent of the entity that has the script_* field set
|
||||||
|
// (`field`) runs its function `func` with the arguments. Returns how many scripts ran.
|
||||||
|
bool has_event_script(int id, const std::string& field); // the entity or one of its children has an enabled LuaComponent with that script_* field
|
||||||
|
int send_event(int id, const std::string& field, const std::string& func, const std::vector<LVal>& args);
|
||||||
|
// Message_Kick (0083d970): the entities with a script_kick within `radius` of the point get kick( who_kicked )
|
||||||
|
void kick_entities(int kicker, double x, double y, double radius);
|
||||||
|
// InteractableComponent (00b4c810, defaults 00b49b70: radius 10): when the player presses the interact key within `radius` of an interactable the first one gets Message_Interaction:
|
||||||
|
// script_interacting -> interacting( entity_who_interacted, entity_interacted, interactable_name ) (the name is the component's ui_text)
|
||||||
|
void interact_step(int who, double x, double y, bool pressed);
|
||||||
|
// script_physics_body_modified of the LuaComponents: physics_body_modified( is_destroyed ) (a lantern drops fire when its body is damaged)
|
||||||
|
void run_body_modified(int id, bool is_destroyed);
|
||||||
|
std::vector<int> game_effect_components(int entity, const std::string& effect) override;
|
||||||
|
int load_game_effect(int entity, const std::string& effect, bool always_new, int& effect_entity) override;
|
||||||
|
int load_game_effect_file(int entity, const std::string& file) override;
|
||||||
|
struct GameEffectRef { int owner = 0, comp = 0; std::string effect; bool disable_movement = false; int caster_herd = 0; }; // caster_herd: herd + 1 of the one that loaded the effect (mCasterHerdId), 0 = unknown
|
||||||
|
std::vector<GameEffectRef> all_game_effects(); // every enabled GameEffectComponent with the entity it affects (the parent of its entity, or the entity itself)
|
||||||
|
bool has_game_effect(int entity, const std::string& effect) { return !game_effect_components(entity, effect).empty(); }
|
||||||
|
// script_damage_received of the LuaComponents: damage_received( damage, message, entity_thats_responsible, is_fatal, projectile_thats_responsible ). The DamageModelComponent still
|
||||||
|
// holds the hp from before the hit while the script runs (the scripts compute `health - damage`); `hp_after` is stored when it returns.
|
||||||
|
void run_damage_received(int id, double damage, const std::string& message, int responsible, bool is_fatal, double hp_before, double hp_after, int projectile = 0);
|
||||||
|
bool quiet_spawn = false; // spawn() runs no execute_on_added scripts (a stand-in entity: the projectile of a GPU hit handed to script_damage_received)
|
||||||
|
bool has_damage_script(int id);
|
||||||
|
// script_damage_about_to_be_received (Message_DamageAboutToBeApplied at the start of TakeDamage_Impl 00ab9a90, sent to the entity and every descendant by 00d17ad0;
|
||||||
|
// LuaComponent handler 00ba80d0): each script gets ( damage, x, y, entity_thats_responsible, critical_hit_chance ) and its two results replace them. Returns the damage.
|
||||||
|
double run_damage_about_to_be_received(int id, double damage, double x, double y, int responsible, int& crit);
|
||||||
|
std::set<int> damage_about_entities(); // the entities that have such a script on themselves or a descendant (one pass over the components)
|
||||||
|
|
||||||
|
// MaterialAreaCheckerComponent: every update_every_x_frame frames the host tells whether one of the two materials is in the area
|
||||||
|
// (x0, y0, x1, y1 in world cells): -1 = the area is not loaded (nothing happens), 0 = no, 1 = yes. A checker that looks for failure calls
|
||||||
|
// script_material_area_checker_failed of the entity's LuaComponent when the answer is 0, the other one the _success script when it is 1.
|
||||||
|
// query: the cells of the box that are m1 or m2 (*total: all its cells), -1 when the box is not loaded
|
||||||
|
void material_area_step(const std::function<int(double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total)>& query);
|
||||||
|
// TeleportComponent (with an enabled HitboxComponent): true when the player point is inside the hitbox of an enabled teleporter; gives the destination
|
||||||
|
// (target.x / target.y, absolute or relative to the teleporter) and the entity.
|
||||||
|
bool teleport_step(double player_x, double player_y, double& target_x, double& target_y, int* teleporter = nullptr);
|
||||||
|
// Kills every entity of the world except the player and what the player carries (the area was left).
|
||||||
|
void clear_world_entities();
|
||||||
|
std::vector<int> with_file(const std::string& part);
|
||||||
|
std::vector<int> with_component(const std::string& type); // living entities with a component of the type // living entities whose file name contains the text
|
||||||
|
|
||||||
|
int player() const { return player_id_; }
|
||||||
|
void set_player(int id) { player_id_ = id; }
|
||||||
|
std::vector<int> inventory; // picked up items without a pickup script (wands, potions, spells)
|
||||||
|
int money();
|
||||||
|
void spend_money(int amount); // WalletComponent: money -= amount, money_spent += amount (a purchase)
|
||||||
|
int pickup_candidate(double px, double py);
|
||||||
|
void pick(int item, int who); // the item goes into the inventory of `who` (its script_item_picked_up first) // the item E would pick up now (0: none), the rules of pickup_step
|
||||||
|
// Text for the nearest item that needs the E key ("E Open chest"), empty when none is in reach.
|
||||||
|
std::string interact_hint(double player_x, double player_y); // player's WalletComponent
|
||||||
|
|
||||||
|
// ---- IEntityWorld ----
|
||||||
|
int load(const std::string& path, double x, double y) override { return spawn(path, x, y); }
|
||||||
|
int load_with(const std::string& path, double x, double y, const std::vector<SpawnComp>& extra) override { return spawn(path, x, y, 0, true, &extra); }
|
||||||
|
void kill(int id) override;
|
||||||
|
void physics_init(int id) override { if (on_physics_init) on_physics_init(id); }
|
||||||
|
bool alive(int id) override;
|
||||||
|
bool get_transform(int id, double& x, double& y, double& rot, double& sx, double& sy) override;
|
||||||
|
void set_transform(int id, double x, double y, double rot, double sx, double sy) override;
|
||||||
|
std::string get_name(int id) override;
|
||||||
|
void set_name(int id, const std::string& name) override;
|
||||||
|
std::string get_filename(int id) override;
|
||||||
|
bool has_tag(int id, const std::string& tag) override;
|
||||||
|
void add_tag(int id, const std::string& tag) override;
|
||||||
|
void remove_tag(int id, const std::string& tag) override;
|
||||||
|
std::string get_tags(int id) override;
|
||||||
|
std::vector<int> with_tag(const std::string& tag) override;
|
||||||
|
std::vector<int> in_radius(double x, double y, double r, const std::string& tag) override;
|
||||||
|
void script_query(bool on) override { script_query_ = on; }
|
||||||
|
bool script_query_ = false; // (before_tag_query runs only for a script's query: the host's own enumerations must not make a twin of every GPU projectile)
|
||||||
|
int parent(int id) override;
|
||||||
|
std::vector<int> children(int id) override;
|
||||||
|
void add_child(int parent, int child) override;
|
||||||
|
void assign_card_slot(int wand, int card); // a spell card that becomes a child of a wand takes the first free slot of the wand
|
||||||
|
int add_gun_action(int wand, const std::string& action_id, bool permanent) override;
|
||||||
|
std::vector<int> components(int entity, const std::string& type, const std::string& tag, bool include_disabled) override;
|
||||||
|
int add_component(int entity, const std::string& type, const std::map<std::string, std::string>& attrs) override;
|
||||||
|
void remove_component(int comp) override;
|
||||||
|
void set_component_enabled(int comp, bool enabled) override;
|
||||||
|
bool component_enabled(int comp) override;
|
||||||
|
std::string component_type(int comp) override;
|
||||||
|
int component_entity(int comp) override;
|
||||||
|
bool comp_has_tag(int comp, const std::string& tag) override;
|
||||||
|
LVal get_value(int comp, const std::string& key, bool typed) override;
|
||||||
|
void set_value(int comp, const std::string& key, const LVal& v) override;
|
||||||
|
int updated_entity() override { return updated_entity_; }
|
||||||
|
int updated_component() override { return updated_comp_; }
|
||||||
|
int frame() override { return frame_; }
|
||||||
|
std::function<void(int shooter, double x, double y, double tx, double ty, int entity, bool send_message)> on_shoot; // GameShootProjectile: the host turns the entity into a GPU projectile
|
||||||
|
void shoot_projectile(int shooter, double x, double y, double tx, double ty, int entity, bool send_message = true) override { if (on_shoot) on_shoot(shooter, x, y, tx, ty, entity, send_message); }
|
||||||
|
std::function<void(const std::string& bank, const std::string& event, double x, double y)> on_play_sound; // the host's sound (src/audio)
|
||||||
|
std::function<void(int entity, const std::string& name)> on_entity_sound;
|
||||||
|
std::function<void(int entity, const std::string& tag, double intensity, double intensity2)> on_entity_sound_loop;
|
||||||
|
void play_sound(const std::string& bank, const std::string& event, double x, double y) override { if (on_play_sound) on_play_sound(bank, event, x, y); }
|
||||||
|
std::function<void(double, double, double, double)> on_remove_joints; // PhysicsRemoveJoints: the host's bodies
|
||||||
|
std::function<void(double)> on_screenshake; // GameScreenshake
|
||||||
|
void physics_remove_joints(double x0, double y0, double x1, double y1) override { if (on_remove_joints) on_remove_joints(x0, y0, x1, y1); }
|
||||||
|
void screenshake(double amount, double, double) override { if (on_screenshake) on_screenshake(amount); }
|
||||||
|
std::function<void(int, int)> on_convert_material; // ConvertMaterialEverywhere
|
||||||
|
std::function<bool(const std::string& image, int& w, int& h)> image_size; // the size of an image file (the centered physics image shapes)
|
||||||
|
void convert_material_everywhere(int from, int to) override { if (on_convert_material) on_convert_material(from, to); }
|
||||||
|
void entity_play_sound(int entity, const std::string& name) override { if (on_entity_sound) on_entity_sound(entity, name); }
|
||||||
|
void entity_play_sound_loop(int entity, const std::string& tag, double intensity, double intensity2) override { if (on_entity_sound_loop) on_entity_sound_loop(entity, tag, intensity, intensity2); }
|
||||||
|
std::function<void(int entity, double amount, const std::string& type, const std::string& description, const std::string& ragdoll_fx, double ix, double iy, int responsible, double wx, double wy, double knockback)> on_inflict_damage; // the host applies it to the GPU entity
|
||||||
|
void inflict_damage(int entity, double amount, const std::string& type, const std::string& description, const std::string& ragdoll_fx, double ix, double iy, int responsible, double wx, double wy, double knockback) override { if (on_inflict_damage) on_inflict_damage(entity, amount, type, description, ragdoll_fx, ix, iy, responsible, wx, wy, knockback); }
|
||||||
|
std::function<std::string(double, double)> biome_fn; // the host knows the biome map
|
||||||
|
std::string biome_name(double x, double y) override { return biome_fn ? biome_fn(x, y) : std::string("_EMPTY_"); }
|
||||||
|
int world_state_entity() override { return world_state_; }
|
||||||
|
int create_entity(const std::string& name) override;
|
||||||
|
void regen_item_actions(int player) override;
|
||||||
|
void remove_from_parent(int id) override { if (RtEntity* e = ent(id)) { e->parent = 0; e->follow = false; } }
|
||||||
|
std::function<void(double&, double&, double&, double&)> camera_fn; // the host knows the camera (x, y, width, height in world cells)
|
||||||
|
bool camera_bounds(double& x, double& y, double& w, double& h) override { if (!camera_fn) return false; camera_fn(x, y, w, h); return true; }
|
||||||
|
void comp_add_tag(int comp, const std::string& tag) override;
|
||||||
|
int herd_relation(int a, int b) override;
|
||||||
|
int wand_capacity(int wand) override;
|
||||||
|
void print(const std::string& title, const std::string& text, bool important) override;
|
||||||
|
void physics_force(int entity, double fx, double fy) override { if (on_physics_force) on_physics_force(entity, fx, fy); }
|
||||||
|
std::function<void(int id, double fx, double fy)> on_physics_force;
|
||||||
|
std::vector<BodyInfo> bodies_in_area(double x0, double y0, double x1, double y1) override { return bodies_in_area_fn ? bodies_in_area_fn(x0, y0, x1, y1) : std::vector<BodyInfo>(); }
|
||||||
|
void physics_area_force(int id, double fx, double fy) override { if (on_physics_area_force) on_physics_area_force(id, fx, fy); }
|
||||||
|
std::function<std::vector<BodyInfo>(double, double, double, double)> bodies_in_area_fn;
|
||||||
|
std::function<void(int id, double fx, double fy)> on_physics_area_force; // PhysicsApplyForce: the host pushes the rigid body of the entity
|
||||||
|
// a tag query of a script (EntityGetWithTag, EntityGetInRadiusWithTag, EntityGetInRadius...): the host makes the entities the query must see that exist only on the GPU (the plain
|
||||||
|
// projectiles: lazy twins); radius < 0 = the whole world. Empty tag = every entity
|
||||||
|
std::function<void(const std::string& tag, double x, double y, double radius)> before_tag_query;
|
||||||
|
void load_to_entity(const std::string& path, int entity) override;
|
||||||
|
std::string file_tags(const std::string& file) { const EntityDef* d = db_.get(file); return d ? d->tags : std::string(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int add_comp_internal(int entity, const std::string& type, std::map<std::string, std::string> attrs);
|
||||||
|
bool run_script_callback(int entity, int comp, const std::string& file, const std::string& function, const std::vector<LVal>& args);
|
||||||
|
|
||||||
|
const Wak& wak_;
|
||||||
|
LuaHost& host_;
|
||||||
|
EntityDb db_;
|
||||||
|
std::map<int, RtEntity> ents_;
|
||||||
|
std::map<int, RtComponent> comps_;
|
||||||
|
double act_cx_ = 0, act_cy_ = 0, act_hw_ = 0, act_hh_ = 0;
|
||||||
|
bool scripts_on_ = false;
|
||||||
|
long script_runs_ = 0;
|
||||||
|
static constexpr int kDynamicIdBase = 1 << 16; // ids below are reserved for forced ids (the initial world entities: GPU slot + 1, the player 1)
|
||||||
|
int next_ent_ = kDynamicIdBase, next_comp_ = 1, frame_ = 0, player_id_ = 0, world_state_ = 0;
|
||||||
|
int updated_entity_ = 0, updated_comp_ = 0;
|
||||||
|
std::vector<GameMessage> messages_;
|
||||||
|
std::vector<std::string> errors_;
|
||||||
|
std::vector<int> pending_scripts_; // entities whose on-added scripts still have to run
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,945 @@
|
|||||||
|
// The Lua API functions of the game VM that need the world: entity and component queries the base natives (lua/entity_natives.cpp)
|
||||||
|
// leave out, timers, orbs, inventory, stains and ingestion, cells, particles, the camera and input, music, rigid bodies and the mod GUI.
|
||||||
|
// Each forwards to EntityRuntime or to the host's GameServices hooks (entity/game_services.h); an unset hook gives the engine's
|
||||||
|
// "nothing found" answer.
|
||||||
|
#include <lua.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <random>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entity/herds.h"
|
||||||
|
#include "entity/material_mix.h"
|
||||||
|
#include "entity/runtime.h"
|
||||||
|
#include "lua/lua_host.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr double kPx = 10.0; // pixels per box2d unit (00718740)
|
||||||
|
|
||||||
|
double nn(lua_State* L, int i, double d = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : d; }
|
||||||
|
int ni(lua_State* L, int i, int d = 0) { return lua_isnumber(L, i) ? (int)lua_tonumber(L, i) : d; }
|
||||||
|
std::string ss(lua_State* L, int i, const char* d = "") { return lua_isstring(L, i) ? lua_tostring(L, i) : d; }
|
||||||
|
bool bb(lua_State* L, int i, bool d) { return lua_isnoneornil(L, i) ? d : lua_toboolean(L, i) != 0; }
|
||||||
|
EntityRuntime* R(LuaHost& h) { return static_cast<EntityRuntime*>(h.world); }
|
||||||
|
GameServices* S(LuaHost& h) { return h.world ? &R(h)->svc : nullptr; }
|
||||||
|
void push_ints(lua_State* L, const std::vector<int>& v) { lua_createtable(L, (int)v.size(), 0); for (size_t i = 0; i < v.size(); ++i) { lua_pushnumber(L, v[i]); lua_rawseti(L, -2, (int)i + 1); } }
|
||||||
|
void push_strs(lua_State* L, const std::vector<std::string>& v) { lua_createtable(L, (int)v.size(), 0); for (size_t i = 0; i < v.size(); ++i) { lua_pushstring(L, v[i].c_str()); lua_rawseti(L, -2, (int)i + 1); } }
|
||||||
|
std::vector<std::string> split(const std::string& s, char c) { std::vector<std::string> r; size_t p = 0; while (p <= s.size()) { size_t e = s.find(c, p); if (e == std::string::npos) e = s.size(); if (e > p) r.push_back(s.substr(p, e - p)); p = e + 1; } return r; }
|
||||||
|
int nret(lua_State* L) { lua_pushnil(L); return 1; }
|
||||||
|
|
||||||
|
// ---- entities ----
|
||||||
|
int n_EntityGetClosest(LuaHost& h, lua_State* L) {
|
||||||
|
if (!h.world) return nret(L);
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2);
|
||||||
|
int best = 0; double bd = 1e300;
|
||||||
|
for (const auto& [id, e] : R(h)->entities()) {
|
||||||
|
if (!e.alive || id == R(h)->world_state_entity()) continue;
|
||||||
|
const double d = (e.x - x) * (e.x - x) + (e.y - y) * (e.y - y);
|
||||||
|
if (d < bd) { bd = d; best = id; }
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, best);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityGetWithName(LuaHost& h, lua_State* L) {
|
||||||
|
const std::string n = ss(L, 1);
|
||||||
|
if (h.world) for (const auto& [id, e] : R(h)->entities()) if (e.alive && e.name == n) { lua_pushnumber(L, id); return 1; }
|
||||||
|
lua_pushnumber(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntitiesGetMaxID(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.world ? R(h)->max_entity_id() : 0); return 1; }
|
||||||
|
int n_IsInvisible(LuaHost& h, lua_State* L) { lua_pushboolean(L, h.world && R(h)->has_game_effect(ni(L, 1), "INVISIBILITY")); return 1; }
|
||||||
|
int n_EntityGetHerdRelationSafe(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.world ? R(h)->herd_relation(ni(L, 1), ni(L, 2)) : 0); return 1; }
|
||||||
|
int n_GenomeSetHerdId(LuaHost& h, lua_State* L) {
|
||||||
|
if (!h.world) return 0;
|
||||||
|
for (int c : R(h)->components(ni(L, 1), "GenomeDataComponent", "", true)) R(h)->set_value(c, "herd_id", LVal::string(ss(L, 2)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_EntityGetHotspot(LuaHost& h, lua_State* L) { // (entity, tag, transformed, include_disabled) -> x, y
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const int e = ni(L, 1);
|
||||||
|
double x = 0, y = 0;
|
||||||
|
for (int c : R(h)->components(e, "HotspotComponent", ss(L, 2), bb(L, 4, false))) {
|
||||||
|
auto& a = R(h)->comp(c)->attrs;
|
||||||
|
x = std::atof(a["offset.x"].c_str()); y = std::atof(a["offset.y"].c_str());
|
||||||
|
if (bb(L, 3, false)) { double ex, ey, r, sx, sy; R(h)->get_transform(e, ex, ey, r, sx, sy); const double c0 = std::cos(r), s0 = std::sin(r); const double tx = ex + (x * sx) * c0 - (y * sy) * s0, ty = ey + (x * sx) * s0 + (y * sy) * c0; x = tx; y = ty; }
|
||||||
|
lua_pushnumber(L, x); lua_pushnumber(L, y);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_EntityGetClosestWorm(LuaHost& h, lua_State* L, const char* type, bool radius) { // -> entity, x, y (, radius)
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2);
|
||||||
|
int best = 0; double bd = 1e300, bx = 0, by = 0, br = 0;
|
||||||
|
for (int id : R(h)->with_component(type)) {
|
||||||
|
const RtEntity* e = R(h)->ent(id);
|
||||||
|
const double d = (e->x - x) * (e->x - x) + (e->y - y) * (e->y - y);
|
||||||
|
if (d < bd) { bd = d; best = id; bx = e->x; by = e->y; for (int c : R(h)->components(id, type, "", false)) br = std::atof(R(h)->comp(c)->attrs["radius"].c_str()); }
|
||||||
|
}
|
||||||
|
if (!best) return 0;
|
||||||
|
lua_pushnumber(L, best); lua_pushnumber(L, bx); lua_pushnumber(L, by);
|
||||||
|
if (radius) { lua_pushnumber(L, br); return 4; }
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
int n_EntityGetClosestWormAttractor(LuaHost& h, lua_State* L) { return n_EntityGetClosestWorm(h, L, "WormAttractorComponent", false); }
|
||||||
|
int n_EntityGetClosestWormDetractor(LuaHost& h, lua_State* L) { return n_EntityGetClosestWorm(h, L, "WormAttractorComponent", true); }
|
||||||
|
|
||||||
|
// ---- components ----
|
||||||
|
int n_ComponentSetValueValueRange(LuaHost& h, lua_State* L) {
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const std::string k = ss(L, 2);
|
||||||
|
R(h)->set_value(ni(L, 1), k + ".min", LVal::number(nn(L, 3)));
|
||||||
|
R(h)->set_value(ni(L, 1), k + ".max", LVal::number(nn(L, 4)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_ComponentRemoveTag(LuaHost& h, lua_State* L) {
|
||||||
|
if (!h.world) return 0;
|
||||||
|
RtComponent* c = R(h)->comp(ni(L, 1));
|
||||||
|
if (!c) return 0;
|
||||||
|
const std::string t = ss(L, 2);
|
||||||
|
c->tags.erase(t);
|
||||||
|
std::string joined;
|
||||||
|
for (const std::string& x : c->tags) joined += (joined.empty() ? "" : ",") + x;
|
||||||
|
c->attrs["_tags"] = joined;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_ComponentGetTags(LuaHost& h, lua_State* L) {
|
||||||
|
RtComponent* c = h.world ? R(h)->comp(ni(L, 1)) : nullptr;
|
||||||
|
if (!c) return nret(L);
|
||||||
|
std::string joined;
|
||||||
|
for (const std::string& x : c->tags) joined += (joined.empty() ? "" : ",") + x;
|
||||||
|
lua_pushstring(L, joined.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::vector<std::string> vec_of(LuaHost& h, lua_State* L, bool& ok) { // a vector member: stored as its comma separated text
|
||||||
|
RtComponent* c = h.world ? R(h)->comp(ni(L, 1)) : nullptr;
|
||||||
|
ok = c != nullptr;
|
||||||
|
if (!c) return {};
|
||||||
|
const auto it = c->attrs.find(ss(L, 2));
|
||||||
|
ok = it != c->attrs.end();
|
||||||
|
return ok ? split(it->second, ',') : std::vector<std::string>();
|
||||||
|
}
|
||||||
|
void push_typed(lua_State* L, const std::string& v, const std::string& type) { if (type == "string") lua_pushstring(L, v.c_str()); else lua_pushnumber(L, type == "int" ? (double)std::atoll(v.c_str()) : std::atof(v.c_str())); }
|
||||||
|
int n_ComponentGetVectorSize(LuaHost& h, lua_State* L) { bool ok; const auto v = vec_of(h, L, ok); lua_pushnumber(L, (double)v.size()); return 1; }
|
||||||
|
int n_ComponentGetVectorValue(LuaHost& h, lua_State* L) {
|
||||||
|
bool ok; const auto v = vec_of(h, L, ok);
|
||||||
|
const int i = ni(L, 4);
|
||||||
|
if (i < 0 || i >= (int)v.size()) return nret(L);
|
||||||
|
push_typed(L, v[i], ss(L, 3));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentGetVector(LuaHost& h, lua_State* L) {
|
||||||
|
bool ok; const auto v = vec_of(h, L, ok);
|
||||||
|
if (!ok) return nret(L);
|
||||||
|
const std::string t = ss(L, 3);
|
||||||
|
lua_createtable(L, (int)v.size(), 0);
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) { push_typed(L, v[i], t); lua_rawseti(L, -2, (int)i + 1); }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentGetMembers(LuaHost& h, lua_State* L) {
|
||||||
|
RtComponent* c = h.world ? R(h)->comp(ni(L, 1)) : nullptr;
|
||||||
|
if (!c) return nret(L);
|
||||||
|
lua_newtable(L);
|
||||||
|
for (const auto& [k, v] : c->attrs) {
|
||||||
|
if (k.empty() || k[0] == '_' || k.find('.') != std::string::npos) continue; // (objects' fields: ComponentObjectGetMembers)
|
||||||
|
lua_pushstring(L, v.c_str()); lua_setfield(L, -2, k.c_str());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentObjectGetMembers(LuaHost& h, lua_State* L) {
|
||||||
|
RtComponent* c = h.world ? R(h)->comp(ni(L, 1)) : nullptr;
|
||||||
|
if (!c) return nret(L);
|
||||||
|
const std::string pre = ss(L, 2) + ".";
|
||||||
|
bool any = false;
|
||||||
|
lua_newtable(L);
|
||||||
|
for (const auto& [k, v] : c->attrs) if (k.rfind(pre, 0) == 0) { any = true; lua_pushstring(L, v.c_str()); lua_setfield(L, -2, k.substr(pre.size()).c_str()); }
|
||||||
|
if (!any) { lua_pop(L, 1); return nret(L); }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntitySetDamageFromMaterial(LuaHost& h, lua_State* L) { // the material's entry in materials_that_damage / materials_how_much_damage (added when missing)
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const std::string m = ss(L, 2);
|
||||||
|
for (int c : R(h)->components(ni(L, 1), "DamageModelComponent", "", true)) {
|
||||||
|
auto& a = R(h)->comp(c)->attrs;
|
||||||
|
auto names = split(a["materials_that_damage"], ','), amts = split(a["materials_how_much_damage"], ',');
|
||||||
|
amts.resize(names.size(), "0");
|
||||||
|
auto it = std::find(names.begin(), names.end(), m);
|
||||||
|
char buf[64]; std::snprintf(buf, sizeof buf, "%g", nn(L, 3));
|
||||||
|
if (it == names.end()) { names.push_back(m); amts.push_back(buf); } else amts[it - names.begin()] = buf;
|
||||||
|
std::string jn, ja;
|
||||||
|
for (size_t i = 0; i < names.size(); ++i) { jn += (i ? "," : "") + names[i]; ja += (i ? "," : "") + amts[i]; }
|
||||||
|
R(h)->set_value(c, "materials_that_damage", LVal::string(jn));
|
||||||
|
R(h)->set_value(c, "materials_how_much_damage", LVal::string(ja));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_RemoveMaterialInventoryMaterial(LuaHost& h, lua_State* L) { // (entity, material = all)
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const std::string m = ss(L, 2);
|
||||||
|
for (int c : R(h)->components(ni(L, 1), "MaterialInventoryComponent", "", true)) {
|
||||||
|
auto& a = R(h)->comp(c)->attrs;
|
||||||
|
if (m.empty()) { for (auto& [k, v] : a) if (k.rfind("count_per_material_type", 0) == 0) v = "0"; a["count_per_material_type"] = ""; }
|
||||||
|
else { // the counts are kept as material=count pairs (material_mix.h)
|
||||||
|
auto parts = split(a["count_per_material_type"], ',');
|
||||||
|
std::string out;
|
||||||
|
for (const std::string& p : parts) if (p.rfind(m + "=", 0) != 0) out += (out.empty() ? "" : ",") + p;
|
||||||
|
a["count_per_material_type"] = out;
|
||||||
|
}
|
||||||
|
R(h)->set_value(c, "count_per_material_type", LVal::string(a["count_per_material_type"]));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- timers, orbs, stats, flow ----
|
||||||
|
int n_SetTimeOut(LuaHost& h, lua_State* L) { // (seconds, file, function = nil): runs in the context of the entity of the calling script
|
||||||
|
if (!h.world) return 0;
|
||||||
|
EntityRuntime::Timeout t;
|
||||||
|
t.frame = R(h)->frame() + std::max(1, (int)std::lround(nn(L, 1) * 60.0));
|
||||||
|
t.entity = R(h)->updated_entity(); t.comp = R(h)->updated_component();
|
||||||
|
t.file = ss(L, 2); t.fn = ss(L, 3);
|
||||||
|
R(h)->timeouts.push_back(t);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GameGetOrbCountThisRun(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.world ? (double)R(h)->orbs_run.size() : 0); return 1; }
|
||||||
|
int n_GameGetOrbCountAllTime(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.world ? (double)R(h)->orbs_all.size() : 0); return 1; }
|
||||||
|
int n_GameGetOrbCollectedThisRun(LuaHost& h, lua_State* L) { lua_pushboolean(L, h.world && R(h)->orbs_run.count(ni(L, 1))); return 1; }
|
||||||
|
int n_GameGetOrbCollectedAllTime(LuaHost& h, lua_State* L) { lua_pushboolean(L, h.world && R(h)->orbs_all.count(ni(L, 1))); return 1; }
|
||||||
|
int n_GameClearOrbsFoundThisRun(LuaHost& h, lua_State*) { if (h.world) R(h)->orbs_run.clear(); return 0; }
|
||||||
|
int n_GameGetOrbCountTotal(LuaHost& h, lua_State* L) { // 007abb70: the magic number NUM_ORBS_TOTAL (default 10; data/magic_numbers.xml sets 12)
|
||||||
|
const std::string v = LuaHost::magic_value(h.wak(), "NUM_ORBS_TOTAL");
|
||||||
|
lua_pushnumber(L, v.empty() ? 10 : std::atoi(v.c_str()));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_GameGetPlayerStatsEntity(LuaHost& h, lua_State* L) { lua_pushnumber(L, h.world ? R(h)->player() : 0); return 1; }
|
||||||
|
int n_StatsBiomeReset(LuaHost& h, lua_State*) { h.biome_stats_base = h.stats; return 0; }
|
||||||
|
int n_StatsLogPlayerKill(LuaHost& h, lua_State* L) { // 007c5c10: no argument: enemies_killed + 1; an entity: only one with a GameStatsComponent counts,
|
||||||
|
// and (00b1a1c0) unless the run flag no_progress_flags_animal is set, its kill count by name (the first kill is the bestiary progress)
|
||||||
|
if (lua_gettop(L) < 1) { h.stats["enemies_killed"] += 1; return 0; }
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const int id = ni(L, 1);
|
||||||
|
const RtEntity* e = R(h)->ent(id);
|
||||||
|
if (!e || R(h)->components(id, "GameStatsComponent", "", true).empty()) return 0;
|
||||||
|
h.stats["enemies_killed"] += 1;
|
||||||
|
if (!h.run_flags.count("no_progress_flags_animal")) h.stats["kill_" + (e->name.empty() ? e->file : e->name)] += 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GameTriggerGameOver(LuaHost& h, lua_State*) { if (S(h) && S(h)->game_over) S(h)->game_over(); return 0; }
|
||||||
|
int n_GameOnCompleted(LuaHost& h, lua_State*) { if (S(h) && S(h)->ending) S(h)->ending(1); return 0; }
|
||||||
|
int n_GameDoEnding2(LuaHost& h, lua_State*) { if (S(h) && S(h)->ending) S(h)->ending(2); return 0; }
|
||||||
|
int n_SetPlayerSpawnLocation(LuaHost& h, lua_State* L) { if (S(h) && S(h)->set_spawn_location) S(h)->set_spawn_location(nn(L, 1), nn(L, 2)); return 0; }
|
||||||
|
|
||||||
|
// ---- values a script keeps between its runs (SetValueNumber...: per LuaComponent) ----
|
||||||
|
std::map<int, std::map<std::string, double>>& values() { static std::map<int, std::map<std::string, double>> m; return m; }
|
||||||
|
int value_key(LuaHost& h) { return h.world ? R(h)->updated_component() : 0; }
|
||||||
|
int n_SetValue(LuaHost& h, lua_State* L) { values()[value_key(h)][ss(L, 1)] = lua_isboolean(L, 2) ? (lua_toboolean(L, 2) ? 1.0 : 0.0) : nn(L, 2); return 0; }
|
||||||
|
double get_value(LuaHost& h, lua_State* L) { const auto& m = values()[value_key(h)]; const auto it = m.find(ss(L, 1)); return it == m.end() ? (lua_isboolean(L, 2) ? (lua_toboolean(L, 2) ? 1.0 : 0.0) : nn(L, 2)) : it->second; }
|
||||||
|
int n_GetValueNumber(LuaHost& h, lua_State* L) { lua_pushnumber(L, get_value(h, L)); return 1; }
|
||||||
|
int n_GetValueInteger(LuaHost& h, lua_State* L) { lua_pushnumber(L, (double)(int64_t)get_value(h, L)); return 1; }
|
||||||
|
int n_GetValueBool(LuaHost& h, lua_State* L) { lua_pushboolean(L, get_value(h, L) != 0.0); return 1; }
|
||||||
|
int n_BiomeMapLoad_KeepPlayer(LuaHost& h, lua_State* L) { if (S(h) && S(h)->biome_map_load) S(h)->biome_map_load(ss(L, 1), lua_isstring(L, 2) ? ss(L, 2) : "data/biome/_pixel_scenes.xml"); return 0; }
|
||||||
|
|
||||||
|
// ---- spawning ----
|
||||||
|
int min_bones(LuaHost& h) { const std::string v = LuaHost::magic_value(h.wak(), "APPARITION_MIN_BONES_REQUIRED"); return v.empty() ? 7 : std::atoi(v.c_str()); }
|
||||||
|
int n_SpawnActionItem(LuaHost& h, lua_State* L) { // a random spell card of the level at x, y (GetRandomAction with the world seed and the position)
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2);
|
||||||
|
const int level = ni(L, 3);
|
||||||
|
std::string code = "local a = GetRandomAction(" + std::to_string((int)x) + "," + std::to_string((int)y) + "," + std::to_string(level) + ",0) if a and a ~= '' then CreateItemActionEntity(a," + std::to_string(x) + "," + std::to_string(y) + ") end";
|
||||||
|
std::string err;
|
||||||
|
h.run_string(code, "SpawnActionItem", &err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_SpawnStash(LuaHost& h, lua_State* L) { // a chest (the stash) with action_count random spells of the level inside
|
||||||
|
if (!h.world) { lua_pushnumber(L, 0); return 1; }
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2);
|
||||||
|
// 007a38b0 loads "data/entities/items/stash.xml", a file data.wak doesn't have: an empty entity at x, y; the cards are its children
|
||||||
|
const int stash = R(h)->create_entity("");
|
||||||
|
R(h)->set_transform(stash, x, y, 0, 1, 1);
|
||||||
|
const int level = ni(L, 3), n = ni(L, 4);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
std::string code = "local a = GetRandomAction(" + std::to_string((int)x + i) + "," + std::to_string((int)y) + "," + std::to_string(level) + "," + std::to_string(i) + ") if a and a ~= '' then local c = CreateItemActionEntity(a," + std::to_string(x) + "," + std::to_string(y) + ") if c then EntityAddChild(" + std::to_string(stash) + ", c) EntitySetComponentsWithTagEnabled(c, 'enabled_in_world', false) end end";
|
||||||
|
std::string err;
|
||||||
|
h.run_string(code, "SpawnStash", &err);
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, stash);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_LoadEntityToStash(LuaHost& h, lua_State* L) { // the entity is made and kept inside the stash (hidden, a child)
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const int stash = ni(L, 2);
|
||||||
|
double x = 0, y = 0, r, sx, sy;
|
||||||
|
R(h)->get_transform(stash, x, y, r, sx, sy);
|
||||||
|
const int e = R(h)->load(ss(L, 1), x, y);
|
||||||
|
if (e) { R(h)->add_child(stash, e); for (int c : R(h)->components(e, "", "enabled_in_world", true)) R(h)->set_component_enabled(c, false); }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_SpawnApparition(LuaHost& h, lua_State* L) { // 007a4160: (x, y, level, spawn_now) -> spawn state, entity; the grave of a dead player (biome_scripts.lua spawn_apparition)
|
||||||
|
static std::map<int, int> state; // per level (the game keeps it in its world state, +0xb4)
|
||||||
|
static std::mt19937 rng{std::random_device{}()};
|
||||||
|
auto rnd = [](int lo, int hi) { return std::uniform_int_distribution<int>(lo, hi)(rng); };
|
||||||
|
auto ghost = [&](bool need_bones) { // 007a3df0: playerghost.xml at x, y holding a random wand of save00/persistent/bones_new (none: the ghost alone)
|
||||||
|
const auto& bones = LuaHost::profile_bones;
|
||||||
|
if (!h.world || (need_bones && (int)bones.size() < min_bones(h))) return 0;
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2);
|
||||||
|
const int g = R(h)->load("data/entities/animals/apparition/playerghost.xml", x, y);
|
||||||
|
if (!g || bones.empty()) return g;
|
||||||
|
const std::string& file = bones[bones.size() < 2 ? 0 : (size_t)rnd(0, (int)bones.size() - 1)];
|
||||||
|
std::vector<uint8_t> bytes;
|
||||||
|
if (FILE* f = nullptr; _wfopen_s(&f, std::filesystem::path(file).wstring().c_str(), L"rb") == 0 && f) { // (read only)
|
||||||
|
std::fseek(f, 0, SEEK_END); const long n = std::ftell(f); std::fseek(f, 0, SEEK_SET);
|
||||||
|
bytes.resize(n > 0 ? (size_t)n : 0);
|
||||||
|
if (n > 0 && std::fread(bytes.data(), 1, (size_t)n, f) != (size_t)n) bytes.clear();
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
if (bytes.empty()) return g;
|
||||||
|
const std::string vpath = "??SAV/persistent/bones_new/" + std::filesystem::path(file).filename().string(); // (the game's save-folder prefix)
|
||||||
|
h.wak().set_override(vpath, std::move(bytes));
|
||||||
|
const int w = R(h)->load(vpath, x, y);
|
||||||
|
if (!w) return g;
|
||||||
|
for (int c : R(h)->components(w, "AbilityComponent", "", true)) { // (0091c360: the old run's use counters are cleared)
|
||||||
|
auto& a = R(h)->comp(c)->attrs;
|
||||||
|
a["stat_times_player_has_shot"] = "0"; a["stat_times_player_has_edited"] = "0";
|
||||||
|
}
|
||||||
|
R(h)->pick(w, g); // (00b52cb0: the ghost picks the wand up)
|
||||||
|
return g;
|
||||||
|
};
|
||||||
|
if (lua_gettop(L) >= 4 && lua_toboolean(L, 4)) { const int e = ghost(false); lua_pushinteger(L, 3); lua_pushinteger(L, e); return 2; }
|
||||||
|
int& s = state[ni(L, 3)];
|
||||||
|
int ret = 0, e = 0;
|
||||||
|
switch (s) {
|
||||||
|
case 0: // enough bones (APPARITION_MIN_BONES_REQUIRED) and a 1 in BIOME_APPARITION_CHANCE: the candles (state 1) come first
|
||||||
|
if ((int)LuaHost::profile_bones.size() >= min_bones(h)) {
|
||||||
|
const std::string c = LuaHost::magic_value(h.wak(), "BIOME_APPARITION_CHANCE");
|
||||||
|
if (rnd(1, std::max(1, c.empty() ? 20 : std::atoi(c.c_str()))) == 1) { ++s; ret = s++; }
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: e = ghost(true); [[fallthrough]];
|
||||||
|
case 1: case 2: ret = s++; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, ret);
|
||||||
|
lua_pushinteger(L, e);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_LoadRagdoll(LuaHost& h, lua_State* L) { // (filename, x, y, material, scale_x, impulse_x, impulse_y)
|
||||||
|
if (!(S(h) && S(h)->ragdoll)) return 0;
|
||||||
|
S(h)->ragdoll(ss(L, 1), nn(L, 2), nn(L, 3), nn(L, 6), nn(L, 7), 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- inventory ----
|
||||||
|
int n_GameRegenItemAction(LuaHost& h, lua_State* L) { if (!h.world) return 0; for (int c : R(h)->components(ni(L, 1), "ItemComponent", "", true)) R(h)->comp(c)->attrs.erase("uses_remaining"); return 0; }
|
||||||
|
int n_GameRegenItemActionsInContainer(LuaHost& h, lua_State* L) {
|
||||||
|
if (!h.world) return 0;
|
||||||
|
std::vector<int> st = R(h)->children(ni(L, 1));
|
||||||
|
while (!st.empty()) { const int c = st.back(); st.pop_back(); for (int ic : R(h)->components(c, "ItemComponent", "", true)) if (!R(h)->components(c, "ItemActionComponent", "", true).empty()) R(h)->comp(ic)->attrs.erase("uses_remaining"); for (int k : R(h)->children(c)) st.push_back(k); }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GameKillInventoryItem(LuaHost& h, lua_State* L) { if (S(h) && S(h)->kill_inventory_item) S(h)->kill_inventory_item(ni(L, 1), ni(L, 2)); else if (h.world) R(h)->kill(ni(L, 2)); return 0; }
|
||||||
|
int n_GamePickUpInventoryItem(LuaHost& h, lua_State* L) { if (S(h) && S(h)->pick_up_item) S(h)->pick_up_item(ni(L, 1), ni(L, 2), bb(L, 3, true)); else if (h.world) R(h)->pick(ni(L, 2), ni(L, 1)); return 0; }
|
||||||
|
int n_GameGetAllInventoryItems(LuaHost& h, lua_State* L) {
|
||||||
|
if (!(S(h) && S(h)->inventory_items)) return nret(L);
|
||||||
|
push_ints(L, S(h)->inventory_items(ni(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_GameDropAllItems(LuaHost& h, lua_State* L) { if (S(h) && S(h)->drop_items) S(h)->drop_items(ni(L, 1), 0); return 0; }
|
||||||
|
int n_GameDropPlayerInventoryItems(LuaHost& h, lua_State* L) { if (S(h) && S(h)->drop_items) S(h)->drop_items(ni(L, 1), 1); return 0; }
|
||||||
|
int n_GameDestroyInventoryItems(LuaHost& h, lua_State* L) { if (S(h) && S(h)->drop_items) S(h)->drop_items(ni(L, 1), 2); return 0; }
|
||||||
|
int n_GameIsInventoryOpen(LuaHost& h, lua_State* L) { lua_pushboolean(L, S(h) && S(h)->inventory_open && S(h)->inventory_open()); return 1; }
|
||||||
|
|
||||||
|
// ---- stains, ingestion, sprites ----
|
||||||
|
int n_EntityIngestMaterial(LuaHost& h, lua_State* L) { if (S(h) && S(h)->ingest) S(h)->ingest(ni(L, 1), ni(L, 2), nn(L, 3)); return 0; }
|
||||||
|
int n_EntityRemoveIngestionStatusEffect(LuaHost& h, lua_State* L) { if (S(h) && S(h)->remove_ingestion) S(h)->remove_ingestion(ni(L, 1), ss(L, 2)); return 0; }
|
||||||
|
int n_EntityRemoveStainStatusEffect(LuaHost& h, lua_State* L) { if (S(h) && S(h)->remove_stain) S(h)->remove_stain(ni(L, 1), ss(L, 2), ni(L, 3)); return 0; }
|
||||||
|
int n_EntityAddRandomStains(LuaHost& h, lua_State* L) { if (S(h) && S(h)->add_stains) S(h)->add_stains(ni(L, 1), ni(L, 2), nn(L, 3)); return 0; }
|
||||||
|
int n_EntityRefreshSprite(LuaHost& h, lua_State* L) { if (S(h) && S(h)->refresh_sprite) S(h)->refresh_sprite(ni(L, 1), ni(L, 2)); return 0; }
|
||||||
|
int n_GamePlayAnimation(LuaHost& h, lua_State* L) { // (entity, name, priority, followup, followup_priority): the SpriteComponents play it (00ab2f50 picks the higher priority)
|
||||||
|
if (S(h) && S(h)->play_animation) { S(h)->play_animation(ni(L, 1), ss(L, 2), ni(L, 3), ss(L, 4), ni(L, 5)); return 0; }
|
||||||
|
if (h.world) for (int c : R(h)->components(ni(L, 1), "SpriteComponent", "", false)) R(h)->set_value(c, "rect_animation", LVal::string(ss(L, 2)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GameCreateSpriteForXFrames(LuaHost& h, lua_State* L) { // 007b36c0: the game reads sprite_offset_y into the x offset too (the y offset stays 0)
|
||||||
|
const int n = lua_gettop(L);
|
||||||
|
const double ox = n >= 6 ? nn(L, 6) : n >= 5 ? nn(L, 5) : 0.0;
|
||||||
|
if (S(h) && S(h)->sprite_for_frames) S(h)->sprite_for_frames(ss(L, 1), nn(L, 2), nn(L, 3), bb(L, 4, true), ox, 0.0, ni(L, 7, 1), bb(L, 8, false));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- cells ----
|
||||||
|
bool liquiform(GameServices* s, int x, int y, const LuaHost& h) { // not air, not gas, not fire
|
||||||
|
if (!s->cell_at) return false;
|
||||||
|
const int m = s->cell_at(x, y);
|
||||||
|
if (m <= 0) return false;
|
||||||
|
static std::vector<int> kind; // (by material: 2 gas, 3 fire)
|
||||||
|
if (kind.empty() && h.material_list) for (const auto& b : const_cast<LuaHost&>(h).material_list()) kind.push_back(b.kind);
|
||||||
|
return m >= (int)kind.size() || (kind[m] != 2 && kind[m] != 3);
|
||||||
|
}
|
||||||
|
int n_RaytraceSurfacesAndLiquiform(LuaHost& h, lua_State* L) { // -> hit, x, y: the first cell that is not gas or fire
|
||||||
|
GameServices* s = S(h);
|
||||||
|
const double x1 = nn(L, 1), y1 = nn(L, 2), x2 = nn(L, 3), y2 = nn(L, 4);
|
||||||
|
const int steps = std::max(1, (int)std::ceil(std::hypot(x2 - x1, y2 - y1)));
|
||||||
|
if (s) for (int k = 0; k <= steps; ++k) {
|
||||||
|
const double px = x1 + (x2 - x1) * k / steps, py = y1 + (y2 - y1) * k / steps;
|
||||||
|
if (liquiform(s, (int)std::floor(px), (int)std::floor(py), h)) { lua_pushboolean(L, 1); lua_pushnumber(L, px); lua_pushnumber(L, py); return 3; }
|
||||||
|
}
|
||||||
|
lua_pushboolean(L, 0); lua_pushnumber(L, x2); lua_pushnumber(L, y2);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
int n_GetSurfaceNormal(LuaHost& h, lua_State* L) { // (x, y, ray_length, ray_count) -> found, nx, ny, distance
|
||||||
|
// 007bb030 -> 0063fd80: ray_count rays of ray_length at angles i x 2 pi / ray_count. From outside the solid the rays that stay clear (0063f680) are summed, from inside
|
||||||
|
// (the start cell is a solid) the rays that stay inside (0063fa50) are subtracted; the normal is the sum normalized (it points away from the surface), the distance is
|
||||||
|
// |sum| / count (negative inside); no ray counted: found false
|
||||||
|
GameServices* s = S(h);
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2), len = nn(L, 3);
|
||||||
|
const int rays = ni(L, 4);
|
||||||
|
auto solidAt = [&](double px, double py) { return s && s->cell_at && liquiform(s, (int)std::floor(px), (int)std::floor(py), h); };
|
||||||
|
const bool inside = solidAt(x, y);
|
||||||
|
double sx = 0, sy = 0; int n = 0;
|
||||||
|
for (int r = 0; r < rays; ++r) {
|
||||||
|
const double a2 = 6.283185307179586 * r / std::max(1, rays), vx = -std::sin(a2) * len, vy = std::cos(a2) * len; // ((0, len) rotated)
|
||||||
|
bool keeps = true;
|
||||||
|
const int steps = std::max(1, (int)std::ceil(len));
|
||||||
|
for (int k = 1; k <= steps && keeps; ++k) { const bool sol = solidAt(x + vx * k / steps, y + vy * k / steps); keeps = inside ? sol : !sol; }
|
||||||
|
if (!keeps) continue;
|
||||||
|
++n;
|
||||||
|
if (inside) { sx -= vx; sy -= vy; } else { sx += vx; sy += vy; }
|
||||||
|
}
|
||||||
|
if (n < 1) { lua_pushboolean(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 4; }
|
||||||
|
const double l = std::hypot(sx, sy);
|
||||||
|
lua_pushboolean(L, 1); lua_pushnumber(L, l > 0 ? sx / l : 0); lua_pushnumber(L, l > 0 ? sy / l : 0); lua_pushnumber(L, (inside ? -1.0 : 1.0) * l / n);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
int n_FindFreePositionForBody(LuaHost& h, lua_State* L) { // (x, y, vx, vy, radius) -> x, y
|
||||||
|
// 007bacb0 -> 00bdd560: the direction of the velocity d; a cast from p + 2 d back to p - 20 d (0063fa50): when it starts in solid cells and comes out of them, the
|
||||||
|
// position is the exit point - d x radius x 0.5; otherwise the position stays
|
||||||
|
GameServices* s = S(h);
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2), vx = nn(L, 3), vy = nn(L, 4), r = nn(L, 5);
|
||||||
|
const double vl = std::hypot(vx, vy);
|
||||||
|
double ox = x, oy = y;
|
||||||
|
if (s && s->cell_at && vl > 0.0) {
|
||||||
|
const double dx = vx / vl, dy = vy / vl, ax = x + dx * 2.0, ay = y + dy * 2.0, bx = x - dx * 20.0, by = y - dy * 20.0;
|
||||||
|
auto solidAt = [&](double px, double py) { return liquiform(s, (int)std::floor(px), (int)std::floor(py), h); };
|
||||||
|
if (solidAt(ax, ay)) {
|
||||||
|
const int steps = 22;
|
||||||
|
for (int k = 1; k <= steps; ++k) {
|
||||||
|
const double px = ax + (bx - ax) * k / steps, py = ay + (by - ay) * k / steps;
|
||||||
|
if (!solidAt(px, py)) { ox = px - dx * r * 0.5; oy = py - dy * r * 0.5; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, ox); lua_pushnumber(L, oy);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_DoesWorldExistAt(LuaHost& h, lua_State* L) { lua_pushboolean(L, S(h) && S(h)->world_loaded && S(h)->world_loaded(ni(L, 1), ni(L, 2), ni(L, 3), ni(L, 4))); return 1; }
|
||||||
|
int n_GameGetSkyVisibility(LuaHost& h, lua_State* L) { lua_pushnumber(L, S(h) && S(h)->sky_visibility ? S(h)->sky_visibility(nn(L, 1), nn(L, 2)) : 0.0); return 1; }
|
||||||
|
int n_GameGetFogOfWar(LuaHost& h, lua_State* L) { lua_pushnumber(L, S(h) && S(h)->fog_get ? S(h)->fog_get(nn(L, 1), nn(L, 2)) : -1); return 1; }
|
||||||
|
int n_GameGetFogOfWarBilinear(LuaHost& h, lua_State* L) {
|
||||||
|
if (!(S(h) && S(h)->fog_get)) { lua_pushnumber(L, -1); return 1; }
|
||||||
|
const double x = nn(L, 1), y = nn(L, 2), x0 = std::floor(x), y0 = std::floor(y), fx = x - x0, fy = y - y0;
|
||||||
|
const int a = S(h)->fog_get(x0, y0), b = S(h)->fog_get(x0 + 1, y0), c = S(h)->fog_get(x0, y0 + 1), d = S(h)->fog_get(x0 + 1, y0 + 1);
|
||||||
|
if (a < 0 || b < 0 || c < 0 || d < 0) { lua_pushnumber(L, a); return 1; }
|
||||||
|
lua_pushnumber(L, std::lround((a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_GameSetFogOfWar(LuaHost& h, lua_State* L) { lua_pushboolean(L, S(h) && S(h)->fog_set && S(h)->fog_set(nn(L, 1), nn(L, 2), (uint8_t)ni(L, 3))); return 1; }
|
||||||
|
int n_ConvertMaterialOnAreaInstantly(LuaHost& h, lua_State* L) { // (x, y, w, h, from, to, trim_box2d, update_edge_graphics)
|
||||||
|
GameServices* s = S(h);
|
||||||
|
if (!s || !s->cell_at || !s->set_cell) return 0;
|
||||||
|
const int x0 = ni(L, 1), y0 = ni(L, 2), w = ni(L, 3), hh = ni(L, 4), from = ni(L, 5), to = ni(L, 6);
|
||||||
|
for (int y = y0; y < y0 + hh; ++y) for (int x = x0; x < x0 + w; ++x) if (s->cell_at(x, y) == from) s->set_cell(x, y, to);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_ConvertEverythingToGold(LuaHost& h, lua_State* L) { // 007e53f0 -> 00baa540: (material_dynamic = "", material_static = ""): non-empty names of materials replace
|
||||||
|
// WorldStateComponent material_everything_to_gold(_static) (an unknown name is an error); then EVERYTHING_TO_GOLD (+0x172) is set once: every material but air
|
||||||
|
// (gases and fires too) becomes the gold one, the static ones (liquid_static or solid_static_type, 00704f70) the static gold, the cells there now and later
|
||||||
|
// (main.cpp: edit mode 10 each frame) and the flasks' contents
|
||||||
|
if (!h.world) return 0;
|
||||||
|
const int ws = R(h)->world_state_entity();
|
||||||
|
if (!ws) return 0;
|
||||||
|
for (int wc : R(h)->components(ws, "WorldStateComponent", "", true)) {
|
||||||
|
auto& wa = R(h)->comp(wc)->attrs;
|
||||||
|
for (int k = 1; k <= 2; ++k) {
|
||||||
|
if (lua_gettop(L) < 2 || !lua_isstring(L, k)) continue;
|
||||||
|
const std::string n = ss(L, k);
|
||||||
|
if (!h.material_type || h.material_type(n) < 1) { luaL_error(L, "ConvertEverythingToGold - couldn't find material: %s", n.c_str()); return 0; }
|
||||||
|
wa[k == 1 ? "material_everything_to_gold" : "material_everything_to_gold_static"] = n;
|
||||||
|
}
|
||||||
|
if (wa["EVERYTHING_TO_GOLD"] == "1") return 0;
|
||||||
|
wa["EVERYTHING_TO_GOLD"] = "1";
|
||||||
|
const std::string gold = wa.count("material_everything_to_gold") ? wa["material_everything_to_gold"] : "gold"; // the flasks: their materials are gold now too
|
||||||
|
for (int e : R(h)->with_component("MaterialInventoryComponent"))
|
||||||
|
for (int ic : R(h)->components(e, "MaterialInventoryComponent", "", true)) {
|
||||||
|
auto parts = mix_get(*R(h), ic);
|
||||||
|
double n = 0; bool other = false;
|
||||||
|
for (const auto& pt : parts) { n += pt.n; other = other || pt.mat != gold; }
|
||||||
|
if (other) mix_set(*R(h), ic, {MixPart{gold, n}});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_EntityConvertToMaterial(LuaHost& h, lua_State* L) { if (S(h) && S(h)->entity_to_material) S(h)->entity_to_material(ni(L, 1), ss(L, 2), bb(L, 3, true), bb(L, 4, true), bb(L, 5, true), nn(L, 6), nn(L, 7)); return 0; }
|
||||||
|
int n_GameCutThroughWorldVertical(LuaHost& h, lua_State* L) { if (S(h) && S(h)->cut_vertical) S(h)->cut_vertical(ni(L, 1), ni(L, 2), ni(L, 3), nn(L, 4), nn(L, 5)); return 0; }
|
||||||
|
int n_LooseChunk(LuaHost& h, lua_State* L) { lua_pushnumber(L, S(h) && S(h)->loose_chunk ? S(h)->loose_chunk(nn(L, 1), nn(L, 2), ss(L, 3), ni(L, 4, 2147483647)) : 0); return 1; }
|
||||||
|
|
||||||
|
// ---- particles, backgrounds ----
|
||||||
|
int n_GameCreateParticle(LuaHost& h, lua_State* L) { if (S(h) && S(h)->create_particles) S(h)->create_particles(ss(L, 1), nn(L, 2), nn(L, 3), ni(L, 4), nn(L, 5), nn(L, 6), bb(L, 7, false), bb(L, 8, false), bb(L, 9, true)); return 0; }
|
||||||
|
int n_GameCreateCosmeticParticle(LuaHost& h, lua_State* L) {
|
||||||
|
if (S(h) && S(h)->create_cosmetic_particles)
|
||||||
|
S(h)->create_cosmetic_particles(ss(L, 1), nn(L, 2), nn(L, 3), ni(L, 4), nn(L, 5), nn(L, 6), (uint32_t)(int64_t)nn(L, 7), nn(L, 8, 5), nn(L, 9, 10), bb(L, 10, true), bb(L, 12, true), bb(L, 13, true), nn(L, 14), nn(L, 15, 100)); // (007b2a50: gravity_y defaults to 100)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GameEmitRainParticles(LuaHost& h, lua_State* L) { if (S(h) && S(h)->rain) S(h)->rain(ni(L, 1), nn(L, 2), ss(L, 3), nn(L, 4), nn(L, 5), nn(L, 6), bb(L, 7, false), bb(L, 8, false)); return 0; }
|
||||||
|
int n_LoadBackgroundSprite(LuaHost& h, lua_State* L) { if (S(h) && S(h)->load_background) S(h)->load_background(ss(L, 1), nn(L, 2), nn(L, 3), nn(L, 4, 40.0), bb(L, 5, false)); return 0; }
|
||||||
|
int n_RemovePixelSceneBackgroundSprite(LuaHost& h, lua_State* L) { lua_pushboolean(L, S(h) && S(h)->remove_background && S(h)->remove_background(ss(L, 1), nn(L, 2), nn(L, 3))); return 1; }
|
||||||
|
int n_RemovePixelSceneBackgroundSprites(LuaHost& h, lua_State* L) { if (S(h) && S(h)->remove_backgrounds) S(h)->remove_backgrounds(nn(L, 1), nn(L, 2), nn(L, 3), nn(L, 4)); return 0; }
|
||||||
|
|
||||||
|
// ---- camera, debug, input ----
|
||||||
|
int n_GameSetCameraPos(LuaHost& h, lua_State* L) { if (S(h) && S(h)->camera_set) S(h)->camera_set(nn(L, 1), nn(L, 2)); return 0; }
|
||||||
|
int n_GameSetCameraFree(LuaHost& h, lua_State* L) { if (S(h) && S(h)->camera_free) S(h)->camera_free(bb(L, 1, false)); return 0; }
|
||||||
|
int n_DEBUG_GetMouseWorld(LuaHost& h, lua_State* L) { double x = 0, y = 0; if (S(h) && S(h)->mouse_world) S(h)->mouse_world(x, y); lua_pushnumber(L, x); lua_pushnumber(L, y); return 2; }
|
||||||
|
int n_DEBUG_MARK(LuaHost& h, lua_State* L) { if (S(h) && S(h)->debug_mark) S(h)->debug_mark(nn(L, 1), nn(L, 2), ss(L, 3), nn(L, 4, 1), nn(L, 5, 0), nn(L, 6, 0)); return 0; }
|
||||||
|
int key_q(LuaHost& h, lua_State* L, int mode) { lua_pushboolean(L, S(h) && S(h)->key && S(h)->key(ni(L, 1), mode)); return 1; }
|
||||||
|
int n_InputIsKeyDown(LuaHost& h, lua_State* L) { return key_q(h, L, 0); }
|
||||||
|
int n_InputIsKeyJustDown(LuaHost& h, lua_State* L) { return key_q(h, L, 1); }
|
||||||
|
int n_InputIsKeyJustUp(LuaHost& h, lua_State* L) { return key_q(h, L, 2); }
|
||||||
|
int mouse_q(LuaHost& h, lua_State* L, int mode) { lua_pushboolean(L, S(h) && S(h)->mouse_button && S(h)->mouse_button(ni(L, 1), mode)); return 1; }
|
||||||
|
int n_InputIsMouseButtonDown(LuaHost& h, lua_State* L) { return mouse_q(h, L, 0); }
|
||||||
|
int n_InputIsMouseButtonJustDown(LuaHost& h, lua_State* L) { return mouse_q(h, L, 1); }
|
||||||
|
int n_InputIsMouseButtonJustUp(LuaHost& h, lua_State* L) { return mouse_q(h, L, 2); }
|
||||||
|
int n_InputGetMousePosOnScreen(LuaHost& h, lua_State* L) { double x = 0, y = 0; if (S(h) && S(h)->mouse_screen) S(h)->mouse_screen(x, y); lua_pushnumber(L, x); lua_pushnumber(L, y); return 2; }
|
||||||
|
int n_JoyFalse(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; } // (no joystick support: none is connected)
|
||||||
|
int n_JoyZero(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); return 1; }
|
||||||
|
int n_JoyStick(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
|
||||||
|
|
||||||
|
// ---- music ----
|
||||||
|
int n_GameTriggerMusicEvent(LuaHost& h, lua_State* L) { if (S(h) && S(h)->music_event) S(h)->music_event(ss(L, 1), bb(L, 2, true), nn(L, 3), nn(L, 4)); return 0; }
|
||||||
|
int n_GameTriggerMusicCue(LuaHost& h, lua_State* L) { if (S(h) && S(h)->music_cue) S(h)->music_cue(ss(L, 1)); return 0; }
|
||||||
|
int n_GameTriggerMusicFadeOutAndDequeueAll(LuaHost& h, lua_State* L) { if (S(h) && S(h)->music_fade_out) S(h)->music_fade_out(nn(L, 1, 1.0)); return 0; }
|
||||||
|
|
||||||
|
// ---- post fx ----
|
||||||
|
int n_GameSetPostFxParameter(LuaHost& h, lua_State* L) { if (h.world) R(h)->postfx[ss(L, 1)] = {(float)nn(L, 2), (float)nn(L, 3), (float)nn(L, 4), (float)nn(L, 5)}; return 0; }
|
||||||
|
int n_GameUnsetPostFxParameter(LuaHost& h, lua_State* L) { if (h.world) R(h)->postfx.erase(ss(L, 1)); return 0; }
|
||||||
|
int n_GameSetPostFxTextureParameter(LuaHost& h, lua_State* L) { if (h.world) R(h)->postfx_textures[ss(L, 1)] = ss(L, 2); return 0; }
|
||||||
|
int n_GameUnsetPostFxTextureParameter(LuaHost& h, lua_State* L) { if (h.world) R(h)->postfx_textures.erase(ss(L, 1)); return 0; }
|
||||||
|
|
||||||
|
// ---- rigid bodies ----
|
||||||
|
int first_body(LuaHost& h, int entity) { if (!(S(h) && S(h)->body_ids)) return 0; const auto v = S(h)->body_ids(entity); return v.empty() ? 0 : v.front(); }
|
||||||
|
int n_PhysicsAddBodyImage(LuaHost& h, lua_State* L) {
|
||||||
|
if (!(S(h) && S(h)->add_body)) return nret(L);
|
||||||
|
const int id = S(h)->add_body(ni(L, 1), ss(L, 2), ss(L, 3), nn(L, 4), nn(L, 5), bb(L, 6, false), bb(L, 7, false), 0, 0);
|
||||||
|
if (!id) return nret(L);
|
||||||
|
lua_pushnumber(L, id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_PhysicsAddBodyCreateBox(LuaHost& h, lua_State* L) {
|
||||||
|
if (!(S(h) && S(h)->add_body)) return nret(L);
|
||||||
|
const int id = S(h)->add_body(ni(L, 1), "", ss(L, 2), nn(L, 3), nn(L, 4), bb(L, 7, false), false, ni(L, 5), ni(L, 6));
|
||||||
|
if (!id) return nret(L);
|
||||||
|
lua_pushnumber(L, id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_PhysicsAddJoint(LuaHost& h, lua_State* L) {
|
||||||
|
if (!(S(h) && S(h)->add_joint)) return nret(L);
|
||||||
|
const int id = S(h)->add_joint(ni(L, 1), ni(L, 2), ni(L, 3), nn(L, 4), nn(L, 5), ss(L, 6));
|
||||||
|
if (!id) return nret(L);
|
||||||
|
lua_pushnumber(L, id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_PhysicsApplyTorque(LuaHost& h, lua_State* L) { const int b = first_body(h, ni(L, 1)); if (b && S(h)->body_torque) S(h)->body_torque(b, nn(L, 2)); return 0; }
|
||||||
|
int n_PhysicsApplyTorqueToComponent(LuaHost& h, lua_State* L) { const int b = first_body(h, ni(L, 1)); if (b && S(h)->body_torque) S(h)->body_torque(b, nn(L, 3)); return 0; }
|
||||||
|
int n_PhysicsSetStatic(LuaHost& h, lua_State* L) { if (S(h) && S(h)->set_static) S(h)->set_static(ni(L, 1), bb(L, 2, true)); return 0; }
|
||||||
|
int n_PhysicsGetComponentAngularVelocity(LuaHost& h, lua_State* L) { double t[6] = {}; const int b = first_body(h, ni(L, 1)); lua_pushnumber(L, b && S(h)->body_get && S(h)->body_get(b, t) ? t[5] : 0.0); return 1; }
|
||||||
|
int n_PhysicsComponentGetTransform(LuaHost& h, lua_State* L) {
|
||||||
|
const int e = h.world ? R(h)->component_entity(ni(L, 1)) : 0;
|
||||||
|
double t[6] = {};
|
||||||
|
const int b = e ? first_body(h, e) : 0;
|
||||||
|
if (!b || !S(h)->body_get || !S(h)->body_get(b, t)) return 0;
|
||||||
|
for (double v : t) lua_pushnumber(L, v);
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
int n_PhysicsComponentSetTransform(LuaHost& h, lua_State* L) {
|
||||||
|
const int e = h.world ? R(h)->component_entity(ni(L, 1)) : 0;
|
||||||
|
const int b = e ? first_body(h, e) : 0;
|
||||||
|
if (!b || !S(h)->body_set) return 0;
|
||||||
|
double t[6]; int n = 0;
|
||||||
|
for (int i = 0; i < 6 && lua_isnumber(L, 2 + i); ++i, ++n) t[i] = lua_tonumber(L, 2 + i);
|
||||||
|
S(h)->body_set(b, t, n);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_PhysicsBodyIDGetFromEntity(LuaHost& h, lua_State* L) { push_ints(L, S(h) && S(h)->body_ids ? S(h)->body_ids(ni(L, 1)) : std::vector<int>()); return 1; }
|
||||||
|
int n_PhysicsBodyIDQueryBodies(LuaHost& h, lua_State* L) {
|
||||||
|
double x0 = nn(L, 1), y0 = nn(L, 2), x1 = nn(L, 3), y1 = nn(L, 4);
|
||||||
|
if (bb(L, 6, false)) { x0 *= kPx; y0 *= kPx; x1 *= kPx; y1 *= kPx; }
|
||||||
|
push_ints(L, S(h) && S(h)->bodies_in_box ? S(h)->bodies_in_box(x0, y0, x1, y1, bb(L, 5, false)) : std::vector<int>());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_PhysicsBodyIDGetTransform(LuaHost& h, lua_State* L) {
|
||||||
|
double t[6] = {};
|
||||||
|
if (!(S(h) && S(h)->body_get && S(h)->body_get(ni(L, 1), t))) return nret(L);
|
||||||
|
for (double v : t) lua_pushnumber(L, v);
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
int n_PhysicsBodyIDSetTransform(LuaHost& h, lua_State* L) {
|
||||||
|
if (!(S(h) && S(h)->body_set)) return 0;
|
||||||
|
double t[6]; int n = 0;
|
||||||
|
for (int i = 0; i < 6 && lua_isnumber(L, 2 + i); ++i, ++n) t[i] = lua_tonumber(L, 2 + i);
|
||||||
|
S(h)->body_set(ni(L, 1), t, n);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int body_push(LuaHost& h, lua_State* L, bool impulse) {
|
||||||
|
if (S(h) && S(h)->body_force) S(h)->body_force(ni(L, 1), nn(L, 2), nn(L, 3), nn(L, 4), nn(L, 5), lua_isnumber(L, 4) && lua_isnumber(L, 5), impulse);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_PhysicsBodyIDApplyForce(LuaHost& h, lua_State* L) { return body_push(h, L, false); }
|
||||||
|
int n_PhysicsBodyIDApplyLinearImpulse(LuaHost& h, lua_State* L) { return body_push(h, L, true); }
|
||||||
|
int n_PhysicsBodyIDApplyTorque(LuaHost& h, lua_State* L) { if (S(h) && S(h)->body_torque) S(h)->body_torque(ni(L, 1), nn(L, 2)); return 0; }
|
||||||
|
int n_PhysicsBodyIDGetWorldCenter(LuaHost& h, lua_State* L) { double t[6] = {}; if (!(S(h) && S(h)->body_get && S(h)->body_get(ni(L, 1), t))) return nret(L); lua_pushnumber(L, t[0]); lua_pushnumber(L, t[1]); return 2; }
|
||||||
|
int n_PhysicsBodyIDGetDamping(LuaHost& h, lua_State* L) { double a = 0, b = 0; if (!(S(h) && S(h)->body_damping_get && S(h)->body_damping_get(ni(L, 1), a, b))) return nret(L); lua_pushnumber(L, a); lua_pushnumber(L, b); return 2; }
|
||||||
|
int n_PhysicsBodyIDSetDamping(LuaHost& h, lua_State* L) { if (S(h) && S(h)->body_damping_set) S(h)->body_damping_set(ni(L, 1), nn(L, 2), nn(L, 3), lua_isnumber(L, 3)); return 0; }
|
||||||
|
int n_PhysicsBodyIDGetGravityScale(LuaHost& h, lua_State* L) { double g = 0; if (!(S(h) && S(h)->body_gravity_get && S(h)->body_gravity_get(ni(L, 1), g))) return nret(L); lua_pushnumber(L, g); return 1; }
|
||||||
|
int n_PhysicsBodyIDSetGravityScale(LuaHost& h, lua_State* L) { if (S(h) && S(h)->body_gravity_set) S(h)->body_gravity_set(ni(L, 1), nn(L, 2)); return 0; }
|
||||||
|
int n_PhysicsBodyIDGetBodyAABB(LuaHost& h, lua_State* L) { double a[4] = {}; if (!(S(h) && S(h)->body_aabb && S(h)->body_aabb(ni(L, 1), a))) return nret(L); for (double v : a) lua_pushnumber(L, v); return 4; }
|
||||||
|
int n_PhysicsPosToGamePos(LuaHost&, lua_State* L) { lua_pushnumber(L, nn(L, 1) * kPx); lua_pushnumber(L, nn(L, 2) * kPx); return 2; }
|
||||||
|
int n_GamePosToPhysicsPos(LuaHost&, lua_State* L) { lua_pushnumber(L, nn(L, 1) / kPx); lua_pushnumber(L, nn(L, 2) / kPx); return 2; }
|
||||||
|
int n_PhysicsVecToGameVec(LuaHost&, lua_State* L) { lua_pushnumber(L, nn(L, 1) * kPx); lua_pushnumber(L, nn(L, 2) * kPx); return 2; }
|
||||||
|
int n_VerletApplyCircularForce(LuaHost& h, lua_State* L) { if (S(h) && S(h)->verlet_circular) S(h)->verlet_circular(nn(L, 1), nn(L, 2), nn(L, 3), nn(L, 4)); return 0; }
|
||||||
|
int n_VerletApplyDirectionalForce(LuaHost& h, lua_State* L) { if (S(h) && S(h)->verlet_directional) S(h)->verlet_directional(ni(L, 1), ni(L, 2), nn(L, 3), nn(L, 4)); return 0; }
|
||||||
|
|
||||||
|
// ---- the mod GUI (an immediate-mode draw list; the host draws it and gives the mouse in GUI coordinates) ----
|
||||||
|
enum GuiOpt { kOptIsDraggable = 0, kOptNonInteractive = 2, kOptAlignLeft = 3, kOptAlign_HorizontalCenter = 16, kOptAlign_Left = 17, kOptFocusSnapToRightEdge = 18, kOptNoPositionTween = 19, kOptHandleDoubleClickAsClick = 20, kOptGamepadDefaultWidget = 21, kOptLayout_InsertOutsideLeft = 22, kOptLayout_InsertOutsideRight = 23, kOptLayout_InsertOutsideAbove = 24, kOptLayout_ForceCalculate = 25, kOptLayout_NextSameLine = 26, kOptLayout_NoLayouting = 27, kOptHorizontalCenter = 28, kOptDrawSemiTransparent = 29, kOptDrawActiveWidgetCursorOnBothSides = 30, kOptDrawWiggle = 31, kOptDrawFreezeWiggle = 32, kOptDrawWaveAnimateOpacity = 33, kOptDrawScaleIn = 34, kOptDrawNoHoverAnimation = 35, kOptDrawActiveWidgetCursorOff = 36, kOptTextRichRendering = 37, kOptClickCancelsDoubleClick = 38, kOptIsExtraDraggable = 39, kOptForceFocusable = 40, kOptPreventDoubleClickAfterClick = 41, kOptScrollContainer_Smooth = 42 };
|
||||||
|
struct Layout { bool horizontal = false; double x = 0, y = 0, cx = 0, cy = 0, mx = 2, my = 2, max_w = 0, max_h = 0; bool no_layout = false; };
|
||||||
|
struct Gui {
|
||||||
|
std::vector<GameServices::GuiItem> items;
|
||||||
|
std::set<int> options, next_options;
|
||||||
|
bool color_next = false; double cr = 1, cg = 1, cb = 1, ca = 1;
|
||||||
|
double z = 0, z_next = 0; bool z_next_set = false;
|
||||||
|
std::vector<int> id_stack;
|
||||||
|
std::vector<Layout> layouts;
|
||||||
|
double scroll_x = 0, scroll_y = 0;
|
||||||
|
struct Box { double x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9; size_t first = 0; double z = 0; };
|
||||||
|
std::vector<Box> boxes;
|
||||||
|
struct Prev { bool clicked = false, right = false, hovered = false; double x = 0, y = 0, w = 0, h = 0; } prev;
|
||||||
|
double anim_alpha = 1, anim_scale = 1;
|
||||||
|
std::map<int, double> slider, anim_start;
|
||||||
|
int dragging = 0;
|
||||||
|
};
|
||||||
|
std::map<int, Gui>& guis() { static std::map<int, Gui> m; return m; }
|
||||||
|
Gui* gui(lua_State* L) { auto it = guis().find(ni(L, 1)); return it == guis().end() ? nullptr : &it->second; }
|
||||||
|
bool opt(Gui& g, int o) { return g.options.count(o) || g.next_options.count(o); }
|
||||||
|
void text_size(LuaHost& h, const std::string& t, double scale, double& w, double& hh) { if (S(h) && S(h)->gui_text_size) S(h)->gui_text_size(t, scale, w, hh); else { w = 6.0 * t.size() * scale; hh = 9.0 * scale; } }
|
||||||
|
void image_size(LuaHost& h, const std::string& f, double& w, double& hh) { if (!(S(h) && S(h)->gui_image_size && S(h)->gui_image_size(f, w, hh))) { w = 16; hh = 16; } }
|
||||||
|
// the widget's place: inside a layout the layout's cursor plus x, y
|
||||||
|
void place(Gui& g, double& x, double& y, double w, double hh) {
|
||||||
|
if (g.layouts.empty() || g.layouts.back().no_layout || opt(g, kOptLayout_NoLayouting)) { x += g.scroll_x; y += g.scroll_y; return; }
|
||||||
|
Layout& l = g.layouts.back();
|
||||||
|
x += l.cx; y += l.cy;
|
||||||
|
if (l.horizontal) { l.cx += x - l.cx + w + l.mx; l.max_h = std::max(l.max_h, hh); }
|
||||||
|
else { l.cy += y - l.cy + hh + l.my; l.max_w = std::max(l.max_w, w); }
|
||||||
|
}
|
||||||
|
bool mouse_in(LuaHost& h, double x, double y, double w, double hh) { double mx = -1, my = -1; if (S(h) && S(h)->mouse_screen) S(h)->mouse_screen(mx, my); return mx >= x && my >= y && mx < x + w && my < y + hh; }
|
||||||
|
void widget_done(LuaHost& h, Gui& g, double x, double y, double w, double hh, bool interactive) {
|
||||||
|
g.prev = Gui::Prev();
|
||||||
|
g.prev.x = x; g.prev.y = y; g.prev.w = w; g.prev.h = hh;
|
||||||
|
if (interactive && !opt(g, kOptNonInteractive)) {
|
||||||
|
g.prev.hovered = mouse_in(h, x, y, w, hh);
|
||||||
|
g.prev.clicked = g.prev.hovered && S(h) && S(h)->mouse_button && S(h)->mouse_button(1, 1);
|
||||||
|
g.prev.right = g.prev.hovered && S(h) && S(h)->mouse_button && S(h)->mouse_button(2, 1);
|
||||||
|
}
|
||||||
|
for (Gui::Box& b : g.boxes) { b.x0 = std::min(b.x0, x); b.y0 = std::min(b.y0, y); b.x1 = std::max(b.x1, x + w); b.y1 = std::max(b.y1, y + hh); }
|
||||||
|
g.next_options.clear(); g.color_next = false; g.z_next_set = false;
|
||||||
|
}
|
||||||
|
void emit(Gui& g, GameServices::GuiItem it) {
|
||||||
|
it.z = g.z_next_set ? g.z_next : g.z;
|
||||||
|
if (g.color_next) { it.r = g.cr; it.g = g.cg; it.b = g.cb; it.alpha *= g.ca; }
|
||||||
|
if (opt(g, kOptDrawSemiTransparent)) it.alpha *= 0.5;
|
||||||
|
it.alpha *= g.anim_alpha; it.scale *= g.anim_scale;
|
||||||
|
g.items.push_back(std::move(it));
|
||||||
|
}
|
||||||
|
int n_GuiCreate(LuaHost&, lua_State* L) { static int next = 1; guis()[next] = Gui(); lua_pushnumber(L, next++); return 1; }
|
||||||
|
int n_GuiDestroy(LuaHost&, lua_State* L) { guis().erase(ni(L, 1)); return 0; }
|
||||||
|
int n_GuiStartFrame(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { g->items.clear(); g->layouts.clear(); g->id_stack.clear(); g->boxes.clear(); g->scroll_x = g->scroll_y = 0; g->next_options.clear(); } return 0; }
|
||||||
|
int n_GuiOptionsAdd(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) g->options.insert(ni(L, 2)); return 0; }
|
||||||
|
int n_GuiOptionsRemove(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) g->options.erase(ni(L, 2)); return 0; }
|
||||||
|
int n_GuiOptionsClear(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) g->options.clear(); return 0; }
|
||||||
|
int n_GuiOptionsAddForNextWidget(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) g->next_options.insert(ni(L, 2)); return 0; }
|
||||||
|
int n_GuiColorSetForNextWidget(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { g->color_next = true; g->cr = nn(L, 2, 1); g->cg = nn(L, 3, 1); g->cb = nn(L, 4, 1); g->ca = nn(L, 5, 1); } return 0; }
|
||||||
|
int n_GuiZSet(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) g->z = nn(L, 2); return 0; }
|
||||||
|
int n_GuiZSetForNextWidget(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { g->z_next = nn(L, 2); g->z_next_set = true; } return 0; }
|
||||||
|
int n_GuiIdPush(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) g->id_stack.push_back(ni(L, 2)); return 0; }
|
||||||
|
int n_GuiIdPushString(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { const std::string s = ss(L, 2); g->id_stack.push_back((int)std::hash<std::string>()(s)); } return 0; }
|
||||||
|
int n_GuiIdPop(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) if (!g->id_stack.empty()) g->id_stack.pop_back(); return 0; }
|
||||||
|
int n_GuiAnimateBegin(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { g->anim_alpha = 1; g->anim_scale = 1; } return 0; }
|
||||||
|
int n_GuiAnimateEnd(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { g->anim_alpha = 1; g->anim_scale = 1; } return 0; }
|
||||||
|
double anim_t(LuaHost& h, Gui& g, int id, bool reset) { const double now = h.world ? R(h)->frame() : 0; if (reset || !g.anim_start.count(id)) g.anim_start[id] = now; return now - g.anim_start[id]; }
|
||||||
|
int n_GuiAnimateAlphaFadeIn(LuaHost& h, lua_State* L) { if (Gui* g = gui(L)) g->anim_alpha = std::clamp(anim_t(h, *g, ni(L, 2), bb(L, 5, false)) * nn(L, 3, 0.1) + nn(L, 4, 0), 0.0, 1.0); return 0; }
|
||||||
|
int n_GuiAnimateScaleIn(LuaHost& h, lua_State* L) { if (Gui* g = gui(L)) g->anim_scale = std::clamp(anim_t(h, *g, ni(L, 2), false) * nn(L, 3, 0.1), 0.0, 1.0); return 0; }
|
||||||
|
int n_GuiText(LuaHost& h, lua_State* L) { // (gui, x, y, text, scale = 1, font = "", font_is_pixel_font = true)
|
||||||
|
Gui* g = gui(L); if (!g) return 0;
|
||||||
|
double x = nn(L, 2), y = nn(L, 3), w, hh;
|
||||||
|
const std::string t = h.translate(ss(L, 4)).empty() ? ss(L, 4) : h.translate(ss(L, 4));
|
||||||
|
const double sc = nn(L, 5, 1);
|
||||||
|
text_size(h, t, sc, w, hh);
|
||||||
|
place(*g, x, y, w, hh);
|
||||||
|
if (opt(*g, kOptAlign_HorizontalCenter) || opt(*g, kOptHorizontalCenter)) x -= w * 0.5;
|
||||||
|
GameServices::GuiItem it; it.kind = 0; it.x = x; it.y = y; it.w = w; it.h = hh; it.scale = sc; it.text = t;
|
||||||
|
emit(*g, it);
|
||||||
|
widget_done(h, *g, x, y, w, hh, false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiTextCentered(LuaHost& h, lua_State* L) { // (gui, x, y, text): centred on x
|
||||||
|
Gui* g = gui(L); if (!g) return 0;
|
||||||
|
double w, hh;
|
||||||
|
const std::string t = h.translate(ss(L, 4)).empty() ? ss(L, 4) : h.translate(ss(L, 4));
|
||||||
|
text_size(h, t, 1, w, hh);
|
||||||
|
double x = nn(L, 2) - w * 0.5, y = nn(L, 3);
|
||||||
|
place(*g, x, y, w, hh);
|
||||||
|
GameServices::GuiItem it; it.kind = 0; it.x = x; it.y = y; it.w = w; it.h = hh; it.text = t;
|
||||||
|
emit(*g, it);
|
||||||
|
widget_done(h, *g, x, y, w, hh, false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiImage(LuaHost& h, lua_State* L) { // (gui, id, x, y, sprite, alpha = 1, scale = 1, scale_y = 0, rotation = 0, rect_animation_playback_type, rect_animation_name)
|
||||||
|
Gui* g = gui(L); if (!g) return 0;
|
||||||
|
double x = nn(L, 3), y = nn(L, 4), w, hh;
|
||||||
|
image_size(h, ss(L, 5), w, hh);
|
||||||
|
const double sc = nn(L, 7, 1), scy = nn(L, 8, 0) != 0 ? nn(L, 8, 0) : sc;
|
||||||
|
place(*g, x, y, w * sc, hh * scy);
|
||||||
|
GameServices::GuiItem it; it.kind = 1; it.x = x; it.y = y; it.w = w * sc; it.h = hh * scy; it.scale = sc; it.alpha = nn(L, 6, 1); it.image = ss(L, 5);
|
||||||
|
emit(*g, it);
|
||||||
|
widget_done(h, *g, x, y, w * sc, hh * scy, true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiImageNinePiece(LuaHost& h, lua_State* L) { // (gui, id, x, y, width, height, alpha = 1, sprite, sprite_highlight)
|
||||||
|
Gui* g = gui(L); if (!g) return 0;
|
||||||
|
double x = nn(L, 3), y = nn(L, 4);
|
||||||
|
const double w = nn(L, 5), hh = nn(L, 6);
|
||||||
|
place(*g, x, y, w, hh);
|
||||||
|
const bool hov = mouse_in(h, x, y, w, hh);
|
||||||
|
GameServices::GuiItem it; it.kind = 2; it.x = x; it.y = y; it.w = w; it.h = hh; it.alpha = nn(L, 7, 1);
|
||||||
|
it.image = hov && lua_isstring(L, 9) ? ss(L, 9) : (lua_isstring(L, 8) ? ss(L, 8) : "data/ui_gfx/decorations/9piece0_gray.png");
|
||||||
|
emit(*g, it);
|
||||||
|
widget_done(h, *g, x, y, w, hh, true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiButton(LuaHost& h, lua_State* L) { // (gui, id, x, y, text) -> clicked, right_clicked
|
||||||
|
Gui* g = gui(L); if (!g) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
|
||||||
|
double x = nn(L, 3), y = nn(L, 4), w, hh;
|
||||||
|
const std::string raw = ss(L, 5);
|
||||||
|
const std::string t = h.translate(raw).empty() ? raw : h.translate(raw);
|
||||||
|
text_size(h, "[" + t + "]", 1, w, hh);
|
||||||
|
place(*g, x, y, w, hh);
|
||||||
|
const bool hov = mouse_in(h, x, y, w, hh);
|
||||||
|
GameServices::GuiItem it; it.kind = 0; it.x = x; it.y = y; it.w = w; it.h = hh; it.text = "[" + t + "]";
|
||||||
|
if (hov && !g->color_next) { it.r = 1; it.g = 1; it.b = 0.6; }
|
||||||
|
emit(*g, it);
|
||||||
|
widget_done(h, *g, x, y, w, hh, true);
|
||||||
|
lua_pushboolean(L, g->prev.clicked); lua_pushboolean(L, g->prev.right);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_GuiImageButton(LuaHost& h, lua_State* L) { // (gui, id, x, y, text, sprite) -> clicked, right_clicked
|
||||||
|
Gui* g = gui(L); if (!g) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
|
||||||
|
double x = nn(L, 3), y = nn(L, 4), w, hh, tw = 0, th = 0;
|
||||||
|
image_size(h, ss(L, 6), w, hh);
|
||||||
|
const std::string t = ss(L, 5);
|
||||||
|
if (!t.empty()) text_size(h, t, 1, tw, th);
|
||||||
|
place(*g, x, y, w + (tw > 0 ? tw + 2 : 0), std::max(hh, th));
|
||||||
|
GameServices::GuiItem it; it.kind = 1; it.x = x; it.y = y; it.w = w; it.h = hh; it.image = ss(L, 6);
|
||||||
|
if (mouse_in(h, x, y, w, hh) && !g->color_next) it.scale = 1.1;
|
||||||
|
emit(*g, it);
|
||||||
|
if (!t.empty()) { GameServices::GuiItem tx; tx.kind = 0; tx.x = x + w + 2; tx.y = y; tx.w = tw; tx.h = th; tx.text = t; g->items.push_back(tx); }
|
||||||
|
widget_done(h, *g, x, y, w + (tw > 0 ? tw + 2 : 0), std::max(hh, th), true);
|
||||||
|
lua_pushboolean(L, g->prev.clicked); lua_pushboolean(L, g->prev.right);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_GuiSlider(LuaHost& h, lua_State* L) { // (gui, id, x, y, text, value, min, max, default, value_display_multiplier, value_formatting, width) -> value
|
||||||
|
Gui* g = gui(L);
|
||||||
|
double v = nn(L, 6);
|
||||||
|
if (!g) { lua_pushnumber(L, v); return 1; }
|
||||||
|
const double mn = nn(L, 7), mx = nn(L, 8), def = nn(L, 9), mul = nn(L, 10, 1), width = nn(L, 12, 100);
|
||||||
|
double x = nn(L, 3), y = nn(L, 4), tw = 0, th = 9;
|
||||||
|
const std::string label = ss(L, 5);
|
||||||
|
if (!label.empty()) text_size(h, label, 1, tw, th);
|
||||||
|
place(*g, x, y, tw + width + 30, th);
|
||||||
|
if (!label.empty()) { GameServices::GuiItem t; t.kind = 0; t.x = x; t.y = y; t.w = tw; t.h = th; t.text = label; emit(*g, t); }
|
||||||
|
const double bx = x + tw + (tw > 0 ? 4 : 0);
|
||||||
|
double mxp = -1, myp = -1;
|
||||||
|
if (S(h) && S(h)->mouse_screen) S(h)->mouse_screen(mxp, myp);
|
||||||
|
const bool hov = mxp >= bx && mxp <= bx + width && myp >= y && myp < y + th;
|
||||||
|
const bool down = S(h) && S(h)->mouse_button && S(h)->mouse_button(1, 0);
|
||||||
|
if (hov && S(h) && S(h)->mouse_button && S(h)->mouse_button(1, 1)) g->dragging = ni(L, 2);
|
||||||
|
if (!down) g->dragging = 0;
|
||||||
|
if (g->dragging == ni(L, 2) && mx > mn) v = mn + std::clamp((mxp - bx) / width, 0.0, 1.0) * (mx - mn);
|
||||||
|
if (hov && S(h) && S(h)->mouse_button && S(h)->mouse_button(2, 1)) v = def;
|
||||||
|
GameServices::GuiItem bar; bar.kind = 2; bar.x = bx; bar.y = y + th * 0.5 - 1; bar.w = width; bar.h = 2; bar.image = "data/ui_gfx/decorations/9piece0_gray.png"; emit(*g, bar);
|
||||||
|
GameServices::GuiItem knob; knob.kind = 2; knob.x = bx + (mx > mn ? (v - mn) / (mx - mn) : 0) * width - 2; knob.y = y; knob.w = 4; knob.h = th; knob.image = "data/ui_gfx/decorations/9piece0.png"; g->items.push_back(knob);
|
||||||
|
char buf[64]; std::snprintf(buf, sizeof buf, "%g", std::round(v * mul * 100.0) / 100.0);
|
||||||
|
std::string vf = ss(L, 11); const size_t p = vf.find("$0"); if (p != std::string::npos) vf.replace(p, 2, buf); else vf = buf;
|
||||||
|
GameServices::GuiItem vt; vt.kind = 0; vt.x = bx + width + 4; vt.y = y; vt.text = vf; g->items.push_back(vt);
|
||||||
|
widget_done(h, *g, x, y, tw + width + 30, th, true);
|
||||||
|
lua_pushnumber(L, v);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_GuiTextInput(LuaHost& h, lua_State* L) { // (gui, id, x, y, text, width, max_length, allowed_characters) -> text (no text entry from the keyboard here: the text stays)
|
||||||
|
Gui* g = gui(L);
|
||||||
|
const std::string t = ss(L, 5);
|
||||||
|
if (g) {
|
||||||
|
double x = nn(L, 3), y = nn(L, 4), w = nn(L, 6, 100), tw, th;
|
||||||
|
text_size(h, t, 1, tw, th);
|
||||||
|
place(*g, x, y, w, th + 2);
|
||||||
|
GameServices::GuiItem bg; bg.kind = 2; bg.x = x; bg.y = y; bg.w = w; bg.h = th + 2; bg.image = "data/ui_gfx/decorations/9piece0_gray.png"; emit(*g, bg);
|
||||||
|
GameServices::GuiItem tx; tx.kind = 0; tx.x = x + 1; tx.y = y + 1; tx.text = t; g->items.push_back(tx);
|
||||||
|
widget_done(h, *g, x, y, w, th + 2, true);
|
||||||
|
}
|
||||||
|
lua_pushstring(L, t.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_GuiBeginAutoBox(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { Gui::Box b; b.first = g->items.size(); b.z = g->z; g->boxes.push_back(b); } return 0; }
|
||||||
|
int n_GuiEndAutoBoxNinePiece(LuaHost&, lua_State* L) { // (gui, margin = 5, size_min_x = 0, size_min_y = 0, mirrorize_over_x_axis, x_axis, sprite, sprite_highlight)
|
||||||
|
Gui* g = gui(L);
|
||||||
|
if (!g || g->boxes.empty()) return 0;
|
||||||
|
Gui::Box b = g->boxes.back(); g->boxes.pop_back();
|
||||||
|
if (b.x1 < b.x0) return 0;
|
||||||
|
const double m = nn(L, 2, 5);
|
||||||
|
GameServices::GuiItem it; it.kind = 2; it.x = b.x0 - m; it.y = b.y0 - m; it.w = std::max(b.x1 - b.x0 + 2 * m, nn(L, 3)); it.h = std::max(b.y1 - b.y0 + 2 * m, nn(L, 4)); it.z = b.z + 1;
|
||||||
|
it.image = lua_isstring(L, 7) ? ss(L, 7) : "data/ui_gfx/decorations/9piece0_gray.png";
|
||||||
|
g->items.insert(g->items.begin() + std::min(b.first, g->items.size()), it);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiTooltip(LuaHost& h, lua_State* L) { // (gui, text, description): beside the previous widget while it is hovered
|
||||||
|
Gui* g = gui(L);
|
||||||
|
if (!g || !g->prev.hovered) return 0;
|
||||||
|
std::string t = ss(L, 2), d = ss(L, 3);
|
||||||
|
if (!h.translate(t).empty()) t = h.translate(t);
|
||||||
|
if (!h.translate(d).empty()) d = h.translate(d);
|
||||||
|
double w1, h1, w2 = 0, h2 = 0;
|
||||||
|
text_size(h, t, 1, w1, h1);
|
||||||
|
if (!d.empty()) text_size(h, d, 1, w2, h2);
|
||||||
|
const double x = g->prev.x + g->prev.w + 4, y = g->prev.y;
|
||||||
|
GameServices::GuiItem bg; bg.kind = 2; bg.x = x - 3; bg.y = y - 3; bg.w = std::max(w1, w2) + 6; bg.h = h1 + h2 + 6; bg.z = -100; bg.image = "data/ui_gfx/decorations/9piece0_gray.png"; g->items.push_back(bg);
|
||||||
|
GameServices::GuiItem a; a.kind = 0; a.x = x; a.y = y; a.text = t; a.z = -101; g->items.push_back(a);
|
||||||
|
if (!d.empty()) { GameServices::GuiItem b; b.kind = 0; b.x = x; b.y = y + h1; b.text = d; b.z = -101; b.r = b.g = b.b = 0.7; g->items.push_back(b); }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiBeginScrollContainer(LuaHost& h, lua_State* L) { // (gui, id, x, y, width, height, scrollbar_gamepad_focusable, margin_x = 2, margin_y = 2): the widgets inside are placed relative to it
|
||||||
|
Gui* g = gui(L); if (!g) return 0;
|
||||||
|
const double x = nn(L, 3), y = nn(L, 4), w = nn(L, 5), hh = nn(L, 6), mx = nn(L, 8, 2), my = nn(L, 9, 2);
|
||||||
|
GameServices::GuiItem bg; bg.kind = 2; bg.x = x - mx; bg.y = y - my; bg.w = w + 2 * mx; bg.h = hh + 2 * my; bg.image = "data/ui_gfx/decorations/9piece0_gray.png"; emit(*g, bg);
|
||||||
|
g->scroll_x = x; g->scroll_y = y;
|
||||||
|
widget_done(h, *g, x, y, w, hh, true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiEndScrollContainer(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { g->scroll_x = g->scroll_y = 0; } return 0; }
|
||||||
|
int layout_begin(lua_State* L, bool horizontal) { // (gui, x, y, position_in_ui_scale = false, margin_x = 2, margin_y = 2)
|
||||||
|
Gui* g = gui(L); if (!g) return 0;
|
||||||
|
Layout l; l.horizontal = horizontal;
|
||||||
|
l.x = nn(L, 2); l.y = nn(L, 3);
|
||||||
|
if (!g->layouts.empty()) { l.x += g->layouts.back().cx; l.y += g->layouts.back().cy; }
|
||||||
|
l.cx = l.x; l.cy = l.y; l.mx = nn(L, 5, 2); l.my = nn(L, 6, 2);
|
||||||
|
g->layouts.push_back(l);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiLayoutBeginHorizontal(LuaHost&, lua_State* L) { return layout_begin(L, true); }
|
||||||
|
int n_GuiLayoutBeginVertical(LuaHost&, lua_State* L) { return layout_begin(L, false); }
|
||||||
|
int n_GuiLayoutAddHorizontalSpacing(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) if (!g->layouts.empty()) g->layouts.back().cx += nn(L, 2, g->layouts.back().mx); return 0; }
|
||||||
|
int n_GuiLayoutAddVerticalSpacing(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) if (!g->layouts.empty()) g->layouts.back().cy += nn(L, 2, g->layouts.back().my); return 0; }
|
||||||
|
int n_GuiLayoutEnd(LuaHost&, lua_State* L) { // the finished layout advances its parent like one widget
|
||||||
|
Gui* g = gui(L);
|
||||||
|
if (!g || g->layouts.empty()) return 0;
|
||||||
|
const Layout l = g->layouts.back(); g->layouts.pop_back();
|
||||||
|
if (!g->layouts.empty()) {
|
||||||
|
Layout& p = g->layouts.back();
|
||||||
|
const double w = l.horizontal ? l.cx - l.x : l.max_w, hh = l.horizontal ? l.max_h : l.cy - l.y;
|
||||||
|
if (p.horizontal) { p.cx = std::max(p.cx, l.x + w + p.mx); p.max_h = std::max(p.max_h, hh); } else { p.cy = std::max(p.cy, l.y + hh + p.my); p.max_w = std::max(p.max_w, w); }
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GuiLayoutBeginLayer(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) { Layout l; l.no_layout = true; g->layouts.push_back(l); } return 0; }
|
||||||
|
int n_GuiLayoutEndLayer(LuaHost&, lua_State* L) { if (Gui* g = gui(L)) if (!g->layouts.empty()) g->layouts.pop_back(); return 0; }
|
||||||
|
int n_GuiGetScreenDimensions(LuaHost& h, lua_State* L) { double w = 640, hh = 360; if (S(h) && S(h)->gui_screen) S(h)->gui_screen(w, hh); lua_pushnumber(L, w); lua_pushnumber(L, hh); return 2; }
|
||||||
|
int n_GuiGetTextDimensions(LuaHost& h, lua_State* L) { double w, hh; std::string t = ss(L, 2); if (!h.translate(t).empty()) t = h.translate(t); text_size(h, t, nn(L, 3, 1), w, hh); lua_pushnumber(L, w); lua_pushnumber(L, hh); return 2; }
|
||||||
|
int n_GuiGetImageDimensions(LuaHost& h, lua_State* L) { double w, hh; image_size(h, ss(L, 2), w, hh); const double s = nn(L, 3, 1); lua_pushnumber(L, w * s); lua_pushnumber(L, hh * s); return 2; }
|
||||||
|
int n_GuiGetPreviousWidgetInfo(LuaHost&, lua_State* L) { // -> clicked, right_clicked, hovered, x, y, width, height, draw_x, draw_y, draw_width, draw_height
|
||||||
|
Gui* g = gui(L);
|
||||||
|
const Gui::Prev p = g ? g->prev : Gui::Prev();
|
||||||
|
lua_pushboolean(L, p.clicked); lua_pushboolean(L, p.right); lua_pushboolean(L, p.hovered);
|
||||||
|
for (double v : {p.x, p.y, p.w, p.h, p.x, p.y, p.w, p.h}) lua_pushnumber(L, v);
|
||||||
|
return 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<GameServices::GuiItem> gui_draw_list() {
|
||||||
|
std::vector<GameServices::GuiItem> all;
|
||||||
|
for (const auto& [id, g] : guis()) all.insert(all.end(), g.items.begin(), g.items.end());
|
||||||
|
std::stable_sort(all.begin(), all.end(), [](const GameServices::GuiItem& a, const GameServices::GuiItem& b) { return a.z > b.z; }); // (a larger z is further back)
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_runtime_natives(LuaHost& h) {
|
||||||
|
struct { const char* name; NativeFn fn; } natives[] = {
|
||||||
|
{"EntityGetClosest", n_EntityGetClosest}, {"EntityGetWithName", n_EntityGetWithName}, {"EntitiesGetMaxID", n_EntitiesGetMaxID}, {"IsInvisible", n_IsInvisible},
|
||||||
|
{"EntityGetHerdRelationSafe", n_EntityGetHerdRelationSafe}, {"GenomeSetHerdId", n_GenomeSetHerdId}, {"EntityGetHotspot", n_EntityGetHotspot},
|
||||||
|
{"EntityGetClosestWormAttractor", n_EntityGetClosestWormAttractor}, {"EntityGetClosestWormDetractor", n_EntityGetClosestWormDetractor},
|
||||||
|
{"ComponentSetValueValueRange", n_ComponentSetValueValueRange}, {"ComponentSetValueValueRangeInt", n_ComponentSetValueValueRange}, {"ComponentRemoveTag", n_ComponentRemoveTag},
|
||||||
|
{"ComponentGetTags", n_ComponentGetTags}, {"ComponentGetVectorSize", n_ComponentGetVectorSize}, {"ComponentGetVectorValue", n_ComponentGetVectorValue}, {"ComponentGetVector", n_ComponentGetVector},
|
||||||
|
{"ComponentGetMembers", n_ComponentGetMembers}, {"ComponentObjectGetMembers", n_ComponentObjectGetMembers}, {"EntitySetDamageFromMaterial", n_EntitySetDamageFromMaterial},
|
||||||
|
{"RemoveMaterialInventoryMaterial", n_RemoveMaterialInventoryMaterial},
|
||||||
|
{"SetTimeOut", n_SetTimeOut}, {"GameGetOrbCountThisRun", n_GameGetOrbCountThisRun}, {"GameGetOrbCountAllTime", n_GameGetOrbCountAllTime}, {"GameGetOrbCollectedThisRun", n_GameGetOrbCollectedThisRun},
|
||||||
|
{"GameGetOrbCollectedAllTime", n_GameGetOrbCollectedAllTime}, {"GameClearOrbsFoundThisRun", n_GameClearOrbsFoundThisRun}, {"GameGetOrbCountTotal", n_GameGetOrbCountTotal},
|
||||||
|
{"GameGetPlayerStatsEntity", n_GameGetPlayerStatsEntity}, {"StatsBiomeReset", n_StatsBiomeReset}, {"StatsLogPlayerKill", n_StatsLogPlayerKill},
|
||||||
|
{"GameTriggerGameOver", n_GameTriggerGameOver}, {"GameOnCompleted", n_GameOnCompleted}, {"GameDoEnding2", n_GameDoEnding2}, {"SetPlayerSpawnLocation", n_SetPlayerSpawnLocation},
|
||||||
|
{"SpawnActionItem", n_SpawnActionItem}, {"SpawnStash", n_SpawnStash}, {"LoadEntityToStash", n_LoadEntityToStash}, {"SpawnApparition", n_SpawnApparition}, {"LoadRagdoll", n_LoadRagdoll},
|
||||||
|
{"GameRegenItemAction", n_GameRegenItemAction}, {"GameRegenItemActionsInContainer", n_GameRegenItemActionsInContainer}, {"GameKillInventoryItem", n_GameKillInventoryItem},
|
||||||
|
{"GamePickUpInventoryItem", n_GamePickUpInventoryItem}, {"GameGetAllInventoryItems", n_GameGetAllInventoryItems}, {"GameDropAllItems", n_GameDropAllItems},
|
||||||
|
{"GameDropPlayerInventoryItems", n_GameDropPlayerInventoryItems}, {"GameDestroyInventoryItems", n_GameDestroyInventoryItems}, {"GameIsInventoryOpen", n_GameIsInventoryOpen},
|
||||||
|
{"EntityIngestMaterial", n_EntityIngestMaterial}, {"EntityRemoveIngestionStatusEffect", n_EntityRemoveIngestionStatusEffect}, {"EntityRemoveStainStatusEffect", n_EntityRemoveStainStatusEffect},
|
||||||
|
{"EntityAddRandomStains", n_EntityAddRandomStains}, {"EntityRefreshSprite", n_EntityRefreshSprite}, {"GamePlayAnimation", n_GamePlayAnimation}, {"GameCreateSpriteForXFrames", n_GameCreateSpriteForXFrames},
|
||||||
|
{"RaytraceSurfacesAndLiquiform", n_RaytraceSurfacesAndLiquiform}, {"GetSurfaceNormal", n_GetSurfaceNormal}, {"FindFreePositionForBody", n_FindFreePositionForBody},
|
||||||
|
{"DoesWorldExistAt", n_DoesWorldExistAt}, {"GameGetSkyVisibility", n_GameGetSkyVisibility}, {"GameGetFogOfWar", n_GameGetFogOfWar}, {"GameGetFogOfWarBilinear", n_GameGetFogOfWarBilinear},
|
||||||
|
{"GameSetFogOfWar", n_GameSetFogOfWar}, {"ConvertMaterialOnAreaInstantly", n_ConvertMaterialOnAreaInstantly}, {"ConvertEverythingToGold", n_ConvertEverythingToGold},
|
||||||
|
{"EntityConvertToMaterial", n_EntityConvertToMaterial}, {"GameCutThroughWorldVertical", n_GameCutThroughWorldVertical}, {"LooseChunk", n_LooseChunk},
|
||||||
|
{"GameCreateParticle", n_GameCreateParticle}, {"GameCreateCosmeticParticle", n_GameCreateCosmeticParticle}, {"GameEmitRainParticles", n_GameEmitRainParticles},
|
||||||
|
{"LoadBackgroundSprite", n_LoadBackgroundSprite}, {"RemovePixelSceneBackgroundSprite", n_RemovePixelSceneBackgroundSprite}, {"RemovePixelSceneBackgroundSprites", n_RemovePixelSceneBackgroundSprites},
|
||||||
|
{"GameSetCameraPos", n_GameSetCameraPos}, {"GameSetCameraFree", n_GameSetCameraFree}, {"DEBUG_GetMouseWorld", n_DEBUG_GetMouseWorld}, {"DEBUG_MARK", n_DEBUG_MARK},
|
||||||
|
{"InputIsKeyDown", n_InputIsKeyDown}, {"InputIsKeyJustDown", n_InputIsKeyJustDown}, {"InputIsKeyJustUp", n_InputIsKeyJustUp}, {"InputGetMousePosOnScreen", n_InputGetMousePosOnScreen},
|
||||||
|
{"InputIsMouseButtonDown", n_InputIsMouseButtonDown}, {"InputIsMouseButtonJustDown", n_InputIsMouseButtonJustDown}, {"InputIsMouseButtonJustUp", n_InputIsMouseButtonJustUp},
|
||||||
|
{"InputIsJoystickButtonDown", n_JoyFalse}, {"InputIsJoystickButtonJustDown", n_JoyFalse}, {"InputIsJoystickConnected", n_JoyFalse}, {"InputGetJoystickAnalogButton", n_JoyZero}, {"InputGetJoystickAnalogStick", n_JoyStick},
|
||||||
|
{"GameTriggerMusicEvent", n_GameTriggerMusicEvent}, {"GameTriggerMusicCue", n_GameTriggerMusicCue}, {"GameTriggerMusicFadeOutAndDequeueAll", n_GameTriggerMusicFadeOutAndDequeueAll},
|
||||||
|
{"GameSetPostFxParameter", n_GameSetPostFxParameter}, {"GameUnsetPostFxParameter", n_GameUnsetPostFxParameter}, {"GameSetPostFxTextureParameter", n_GameSetPostFxTextureParameter}, {"GameUnsetPostFxTextureParameter", n_GameUnsetPostFxTextureParameter},
|
||||||
|
{"PhysicsAddBodyImage", n_PhysicsAddBodyImage}, {"PhysicsAddBodyCreateBox", n_PhysicsAddBodyCreateBox}, {"PhysicsAddJoint", n_PhysicsAddJoint}, {"PhysicsApplyTorque", n_PhysicsApplyTorque},
|
||||||
|
{"PhysicsApplyTorqueToComponent", n_PhysicsApplyTorqueToComponent}, {"PhysicsSetStatic", n_PhysicsSetStatic}, {"PhysicsGetComponentAngularVelocity", n_PhysicsGetComponentAngularVelocity},
|
||||||
|
{"PhysicsComponentGetTransform", n_PhysicsComponentGetTransform}, {"PhysicsComponentSetTransform", n_PhysicsComponentSetTransform}, {"PhysicsBodyIDGetFromEntity", n_PhysicsBodyIDGetFromEntity},
|
||||||
|
{"PhysicsBodyIDQueryBodies", n_PhysicsBodyIDQueryBodies}, {"PhysicsBodyIDGetTransform", n_PhysicsBodyIDGetTransform}, {"PhysicsBodyIDSetTransform", n_PhysicsBodyIDSetTransform},
|
||||||
|
{"PhysicsBodyIDApplyForce", n_PhysicsBodyIDApplyForce}, {"PhysicsBodyIDApplyLinearImpulse", n_PhysicsBodyIDApplyLinearImpulse}, {"PhysicsBodyIDApplyTorque", n_PhysicsBodyIDApplyTorque},
|
||||||
|
{"PhysicsBodyIDGetWorldCenter", n_PhysicsBodyIDGetWorldCenter}, {"PhysicsBodyIDGetDamping", n_PhysicsBodyIDGetDamping}, {"PhysicsBodyIDSetDamping", n_PhysicsBodyIDSetDamping},
|
||||||
|
{"PhysicsBodyIDGetGravityScale", n_PhysicsBodyIDGetGravityScale}, {"PhysicsBodyIDSetGravityScale", n_PhysicsBodyIDSetGravityScale}, {"PhysicsBodyIDGetBodyAABB", n_PhysicsBodyIDGetBodyAABB},
|
||||||
|
{"PhysicsPosToGamePos", n_PhysicsPosToGamePos}, {"GamePosToPhysicsPos", n_GamePosToPhysicsPos}, {"PhysicsVecToGameVec", n_PhysicsVecToGameVec},
|
||||||
|
{"VerletApplyCircularForce", n_VerletApplyCircularForce}, {"VerletApplyDirectionalForce", n_VerletApplyDirectionalForce},
|
||||||
|
{"SetValueNumber", n_SetValue}, {"SetValueInteger", n_SetValue}, {"SetValueBool", n_SetValue}, {"GetValueNumber", n_GetValueNumber}, {"GetValueInteger", n_GetValueInteger}, {"GetValueBool", n_GetValueBool},
|
||||||
|
{"BiomeMapLoad_KeepPlayer", n_BiomeMapLoad_KeepPlayer}, {"BiomeMapLoad", n_BiomeMapLoad_KeepPlayer},
|
||||||
|
{"GuiCreate", n_GuiCreate}, {"GuiDestroy", n_GuiDestroy}, {"GuiStartFrame", n_GuiStartFrame}, {"GuiOptionsAdd", n_GuiOptionsAdd}, {"GuiOptionsRemove", n_GuiOptionsRemove},
|
||||||
|
{"GuiOptionsClear", n_GuiOptionsClear}, {"GuiOptionsAddForNextWidget", n_GuiOptionsAddForNextWidget}, {"GuiColorSetForNextWidget", n_GuiColorSetForNextWidget}, {"GuiZSet", n_GuiZSet},
|
||||||
|
{"GuiZSetForNextWidget", n_GuiZSetForNextWidget}, {"GuiIdPush", n_GuiIdPush}, {"GuiIdPushString", n_GuiIdPushString}, {"GuiIdPop", n_GuiIdPop}, {"GuiAnimateBegin", n_GuiAnimateBegin},
|
||||||
|
{"GuiAnimateEnd", n_GuiAnimateEnd}, {"GuiAnimateAlphaFadeIn", n_GuiAnimateAlphaFadeIn}, {"GuiAnimateScaleIn", n_GuiAnimateScaleIn}, {"GuiText", n_GuiText}, {"GuiTextCentered", n_GuiTextCentered},
|
||||||
|
{"GuiImage", n_GuiImage}, {"GuiImageNinePiece", n_GuiImageNinePiece}, {"GuiButton", n_GuiButton}, {"GuiImageButton", n_GuiImageButton}, {"GuiSlider", n_GuiSlider}, {"GuiTextInput", n_GuiTextInput},
|
||||||
|
{"GuiBeginAutoBox", n_GuiBeginAutoBox}, {"GuiEndAutoBoxNinePiece", n_GuiEndAutoBoxNinePiece}, {"GuiTooltip", n_GuiTooltip}, {"GuiBeginScrollContainer", n_GuiBeginScrollContainer},
|
||||||
|
{"GuiEndScrollContainer", n_GuiEndScrollContainer}, {"GuiLayoutBeginHorizontal", n_GuiLayoutBeginHorizontal}, {"GuiLayoutBeginVertical", n_GuiLayoutBeginVertical},
|
||||||
|
{"GuiLayoutAddHorizontalSpacing", n_GuiLayoutAddHorizontalSpacing}, {"GuiLayoutAddVerticalSpacing", n_GuiLayoutAddVerticalSpacing}, {"GuiLayoutEnd", n_GuiLayoutEnd},
|
||||||
|
{"GuiLayoutBeginLayer", n_GuiLayoutBeginLayer}, {"GuiLayoutEndLayer", n_GuiLayoutEndLayer}, {"GuiGetScreenDimensions", n_GuiGetScreenDimensions}, {"GuiGetTextDimensions", n_GuiGetTextDimensions},
|
||||||
|
{"GuiGetImageDimensions", n_GuiGetImageDimensions}, {"GuiGetPreviousWidgetInfo", n_GuiGetPreviousWidgetInfo},
|
||||||
|
};
|
||||||
|
for (auto& n : natives) h.register_native(n.name, n.fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
// A component a script added (EntityAddComponent) to an entity right after it loaded it, while the world was generated (perks: the sprite, the name, the perk id).
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
struct SpawnComp {
|
||||||
|
std::string type;
|
||||||
|
std::map<std::string, std::string> attrs;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#include "entity/status.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <lua.hpp>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::vector<std::string> split(const std::string& s, const char* seps) {
|
||||||
|
std::vector<std::string> r;
|
||||||
|
std::string cur;
|
||||||
|
for (char c : s) {
|
||||||
|
if (std::strchr(seps, c)) { if (!cur.empty()) r.push_back(cur); cur.clear(); }
|
||||||
|
else cur += c;
|
||||||
|
}
|
||||||
|
if (!cur.empty()) r.push_back(cur);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusSystem::StatusSystem(const MaterialDb& db, LuaHost& host, EntityRuntime& rt) : db_(db), rt_(rt) {
|
||||||
|
std::string err;
|
||||||
|
if (!host.run_file("data/scripts/status_effects/status_list.lua", &err)) return;
|
||||||
|
lua_State* L = host.state();
|
||||||
|
lua_getglobal(L, "status_effects");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
const size_t n = lua_objlen(L, -1);
|
||||||
|
for (size_t i = 1; i <= n; ++i) {
|
||||||
|
lua_rawgeti(L, -1, (int)i);
|
||||||
|
Def d;
|
||||||
|
auto str = [&](const char* k) { lua_getfield(L, -1, k); std::string v = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; lua_pop(L, 1); return v; };
|
||||||
|
auto flag = [&](const char* k) { lua_getfield(L, -1, k); const bool v = lua_toboolean(L, -1) != 0; lua_pop(L, 1); return v; };
|
||||||
|
d.id = str("id"); d.effect_entity = str("effect_entity"); d.ui_name = str("ui_name"); d.ui_icon = str("ui_icon"); d.ui_description = str("ui_description");
|
||||||
|
d.protects_from_fire = flag("protects_from_fire"); d.harmful = flag("is_harmful"); d.permanent = flag("effect_permanent"); d.remove_cells = flag("remove_cells_that_cause_when_activated");
|
||||||
|
lua_getfield(L, -1, "min_threshold_normalized"); d.min_threshold = lua_isnumber(L, -1) ? (float)lua_tonumber(L, -1) : 0.0f; lua_pop(L, 1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (!d.id.empty()) defs_.push_back(std::move(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusSystem::MatInfo& StatusSystem::info(int material) {
|
||||||
|
if (mats_.size() < db_.materials().size()) mats_.resize(db_.materials().size());
|
||||||
|
static const MatInfo none;
|
||||||
|
if (material < 0 || material >= (int)mats_.size()) return none;
|
||||||
|
MatInfo& mi = mats_[material];
|
||||||
|
if (mi.known) return mi;
|
||||||
|
mi.known = true;
|
||||||
|
const Material& m = db_.materials()[material];
|
||||||
|
auto at = [&](const char* k, const char* def) { auto it = m.attrs.find(k); return it == m.attrs.end() ? std::string(def) : it->second; };
|
||||||
|
mi.mask = std::atoi(at("liquid_stains", "0").c_str());
|
||||||
|
mi.threshold = (float)std::atof(at("liquid_sprite_stains_status_threshold", "0.01").c_str()); // 004ac7e0: 0.01
|
||||||
|
mi.drop = (float)std::atof(at("liquid_sprite_stain_shaken_drop_chance", "1").c_str());
|
||||||
|
if (mi.mask) { // the stain statuses: the <Stains> channel and the legacy status_effects list
|
||||||
|
mi.stains = split(at("status_effects", ""), ",; ");
|
||||||
|
for (const std::string& pr : split(at("status.stains", ""), ";")) { const std::string t = pr.substr(0, pr.find(':')); if (std::find(mi.stains.begin(), mi.stains.end(), t) == mi.stains.end()) mi.stains.push_back(t); }
|
||||||
|
}
|
||||||
|
for (const std::string& pr : split(at("status.ingestion", ""), ";")) {
|
||||||
|
const size_t c = pr.find(':');
|
||||||
|
if (c != std::string::npos) mi.ingestion.push_back({pr.substr(0, c), (float)std::atof(pr.c_str() + c + 1)});
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusSystem::sample(int owner, int frame, bool, const std::vector<std::pair<int, int>>& pixels, const std::function<int(int, int)>& material_at, int splash_material) {
|
||||||
|
State& st = states_[owner];
|
||||||
|
const size_t n = pixels.size();
|
||||||
|
const int elapsed = std::clamp(frame - st.last_sample, 1, 64);
|
||||||
|
st.last_sample = frame;
|
||||||
|
st.pixels = n;
|
||||||
|
for (auto it = st.pixel_stain.begin(); it != st.pixel_stain.end(); ++it) { // what is not renewed is shed (the stained pixels fall off as particles, shaken_drop_chance)
|
||||||
|
auto& v = it->second;
|
||||||
|
if (v.size() != n) v.assign(n, 0.0f); // the sprite changed: the map starts afresh
|
||||||
|
const float keep = std::pow(1.0f - 0.0016f * std::max(info(it->first).drop, 0.25f), (float)elapsed);
|
||||||
|
for (float& x : v) x *= keep;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < n; ++i) { // a pixel inside a staining liquid is stained
|
||||||
|
const int m = material_at(pixels[i].first, pixels[i].second);
|
||||||
|
if (m > 0 && info(m).mask) { // (a sprite pixel holds one stain material, 00c22080's parallel material map: the new liquid replaces the old one there)
|
||||||
|
for (auto& [om, ov] : st.pixel_stain) if (om != m && i < ov.size()) ov[i] = 0.0f;
|
||||||
|
auto& v = st.pixel_stain[m]; if (v.size() != n) v.assign(n, 0.0f); v[i] = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (splash_material > 0 && info(splash_material).mask) { // the whole sprite takes the material (one stain material per pixel)
|
||||||
|
for (auto& [om, ov] : st.pixel_stain) if (om != splash_material) std::fill(ov.begin(), ov.end(), 0.0f);
|
||||||
|
auto& v = st.pixel_stain[splash_material]; v.assign(n, 1.0f);
|
||||||
|
}
|
||||||
|
std::map<int, float> cov;
|
||||||
|
for (auto it = st.pixel_stain.begin(); it != st.pixel_stain.end();) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (float x : it->second) sum += x;
|
||||||
|
const float c = n ? sum / (float)n : 0.0f;
|
||||||
|
if (c < 0.002f) { it = st.pixel_stain.erase(it); continue; }
|
||||||
|
cov[it->first] = c;
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
for (auto it = st.coverage.begin(); it != st.coverage.end();) { // a coverage without a pixel map (set from outside, NG_STAIN) is shed as before
|
||||||
|
if (cov.count(it->first)) { ++it; continue; }
|
||||||
|
it->second *= std::pow(1.0f - 0.0016f * std::max(info(it->first).drop, 0.25f), (float)elapsed);
|
||||||
|
if (it->second < 0.002f) it = st.coverage.erase(it); else ++it;
|
||||||
|
}
|
||||||
|
for (auto& [m, c] : cov) st.coverage[m] = c;
|
||||||
|
st.stain.clear();
|
||||||
|
for (auto& [m, cov] : st.coverage) {
|
||||||
|
const MatInfo& mi = info(m);
|
||||||
|
if (!(cov > mi.threshold)) continue; // strictly greater (00c22ef6)
|
||||||
|
for (const std::string& s : mi.stains) {
|
||||||
|
int& cd = st.cooldown[s];
|
||||||
|
if (cd > 0) --cd; else st.stain[s] += cov;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusSystem::ingest(int owner, int material, double amount) {
|
||||||
|
State& st = states_[owner];
|
||||||
|
for (auto& [status, a] : info(material).ingestion) st.ingest[status] += a * (float)amount; // 00b44e80, the global multiplier is 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusSystem::Def* StatusSystem::choose(const std::string& status, float normalized) {
|
||||||
|
const Def* pick = nullptr;
|
||||||
|
for (const Def& d : defs_) {
|
||||||
|
if (d.id != status) continue;
|
||||||
|
if (!pick) pick = &d; // the first registered variant
|
||||||
|
else if (d.min_threshold <= normalized) pick = &d; // then the last qualifying one in registration order (00c24fb0)
|
||||||
|
}
|
||||||
|
return pick;
|
||||||
|
}
|
||||||
|
|
||||||
|
float StatusSystem::stain_amount(int owner, const std::string& status) {
|
||||||
|
auto it = states_.find(owner);
|
||||||
|
if (it == states_.end()) return 0.0f;
|
||||||
|
auto s = it->second.stain.find(status);
|
||||||
|
return s == it->second.stain.end() ? 0.0f : s->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatusSystem::active(int owner, const std::string& status) {
|
||||||
|
auto it = states_.find(owner);
|
||||||
|
if (it == states_.end()) return false;
|
||||||
|
auto s = it->second.stain.find(status);
|
||||||
|
if (s != it->second.stain.end() && s->second >= 0.15f) return true;
|
||||||
|
auto g = it->second.ingest.find(status);
|
||||||
|
return g != it->second.ingest.end() && g->second > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> StatusSystem::active_statuses(int owner) {
|
||||||
|
std::vector<std::string> r;
|
||||||
|
std::set<std::string> seen;
|
||||||
|
for (const Def& d : defs_) if (seen.insert(d.id).second && active(owner, d.id)) r.push_back(d.id);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusSystem::update(int owner, int) {
|
||||||
|
auto found = states_.find(owner);
|
||||||
|
if (found == states_.end()) return;
|
||||||
|
State& st = found->second;
|
||||||
|
std::set<std::string> names;
|
||||||
|
for (auto& kv : st.stain) names.insert(kv.first);
|
||||||
|
for (auto& kv : st.ingest) names.insert(kv.first);
|
||||||
|
std::map<std::string, std::vector<int>> effects; // existing effect entities by status
|
||||||
|
for (int k : rt_.children(owner))
|
||||||
|
for (int gc : rt_.components(k, "GameEffectComponent", "", true)) {
|
||||||
|
const std::string& cs = rt_.comp(gc)->attrs["causing_status_effect"];
|
||||||
|
if (!cs.empty()) { effects[cs].push_back(gc); names.insert(cs); }
|
||||||
|
}
|
||||||
|
if (names.empty()) return;
|
||||||
|
const bool radio_protected = !rt_.game_effect_components(owner, "PROTECTION_RADIOACTIVITY").empty();
|
||||||
|
const bool iron_stomach = !rt_.game_effect_components(owner, "IRON_STOMACH").empty();
|
||||||
|
const bool food_protected = !rt_.game_effect_components(owner, "PROTECTION_FOOD_POISONING").empty();
|
||||||
|
for (const std::string& name : names) {
|
||||||
|
if (name == "RADIOACTIVE" && radio_protected) { st.stain.erase(name); st.ingest.erase(name); }
|
||||||
|
float& ing = st.ingest[name];
|
||||||
|
const bool hadIngestion = ing > 0.0f;
|
||||||
|
const Def* base = choose(name, 0.0f);
|
||||||
|
if (ing > 0.0f && !(base && base->permanent)) { ing -= 1.0f / 60.0f; if (ing <= 0.0f) ing = 0.0f; } // the amount shrinks with the time
|
||||||
|
const float stain = st.stain.count(name) ? st.stain[name] : 0.0f;
|
||||||
|
const bool hadStain = st.prev_stain.count(name) && st.prev_stain[name] >= 0.15f;
|
||||||
|
st.prev_stain[name] = stain;
|
||||||
|
const float normalized = ing / 60.0f;
|
||||||
|
const Def* d = choose(name, normalized);
|
||||||
|
bool ingestion_active = ing > 0.0f && d && d->min_threshold <= normalized + 1e-6f;
|
||||||
|
if (ingestion_active && ((d->harmful && iron_stomach) || (name == "FOOD_POISONING" && food_protected))) ingestion_active = false;
|
||||||
|
const bool is_active = stain >= 0.15f || ingestion_active;
|
||||||
|
if (!hadIngestion && !hadStain && stain < 0.15f && ing <= 0.0f) continue; // 00c24670: a status this entity had not (no stain last time, no ingestion) is left alone: an effect of it that came with a polymorph stays
|
||||||
|
std::vector<int>& have = effects[name];
|
||||||
|
const std::string wanted = d ? d->effect_entity : std::string();
|
||||||
|
bool present = false;
|
||||||
|
for (int gc : have) {
|
||||||
|
RtComponent* c = rt_.comp(gc);
|
||||||
|
if (!c) continue;
|
||||||
|
const bool wrong = !is_active || wanted.empty() || c->attrs["status_template"] != wanted;
|
||||||
|
if (wrong) { // retire: the effect ends with its next update (frames = 1)
|
||||||
|
const std::string& fr = c->attrs["frames"];
|
||||||
|
const int f = fr.empty() ? -1 : std::atoi(fr.c_str());
|
||||||
|
if (f > 1 || f < 0) c->attrs["frames"] = "1";
|
||||||
|
} else present = true;
|
||||||
|
}
|
||||||
|
if (is_active && !wanted.empty() && !present) {
|
||||||
|
if (d && d->remove_cells) { // 00c24670: the stain pixels and the ingestion buffers that caused the status are cleared before the effect loads (also when the load is rejected)
|
||||||
|
st.stain.erase(name); st.ingest.erase(name);
|
||||||
|
for (auto it = st.coverage.begin(); it != st.coverage.end();) {
|
||||||
|
const auto& ss = info(it->first).stains;
|
||||||
|
if (std::find(ss.begin(), ss.end(), name) != ss.end()) { st.pixel_stain.erase(it->first); it = st.coverage.erase(it); } else ++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int e = rt_.load_game_effect_file(owner, wanted);
|
||||||
|
if (e) for (int gc : rt_.components(e, "GameEffectComponent", "", true)) { rt_.comp(gc)->attrs["causing_status_effect"] = name; rt_.comp(gc)->attrs["status_template"] = wanted; }
|
||||||
|
}
|
||||||
|
if (!is_active && ing <= 0.0f && stain <= 0.0f && have.empty()) st.ingest.erase(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> StatusSystem::ingested(int owner) {
|
||||||
|
std::vector<std::string> r;
|
||||||
|
const auto it = states_.find(owner);
|
||||||
|
if (it != states_.end()) for (const auto& kv : it->second.ingest) if (kv.second > 0.0f) r.push_back(kv.first);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusSystem::clear_status(int owner, const std::string& status) {
|
||||||
|
const auto it = states_.find(owner);
|
||||||
|
if (it == states_.end()) return;
|
||||||
|
it->second.stain.erase(status);
|
||||||
|
it->second.ingest[status] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
// Liquid stains, ingestion and native status effects (docs/LIQUID_STAINS_AND_STATUS_EFFECTS.md): material -> stain coverage / ingestion amount -> status ->
|
||||||
|
// effect entity (GameEffectComponent). Per entity: the coverage of every staining material, the stain amount per status, the ingestion amount per status.
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/materials.h"
|
||||||
|
#include "entity/runtime.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
class StatusSystem {
|
||||||
|
public:
|
||||||
|
struct Def {
|
||||||
|
std::string id, effect_entity, ui_name, ui_icon, ui_description;
|
||||||
|
bool protects_from_fire = false, harmful = false, permanent = false, remove_cells = false; // remove_cells: remove_cells_that_cause_when_activated (+61)
|
||||||
|
float min_threshold = 0.0f; // min_threshold_normalized: the ingestion amount / 60 from which this variant applies
|
||||||
|
};
|
||||||
|
struct State {
|
||||||
|
std::map<int, float> coverage; // material -> fraction of the opaque sprite pixels covered with it (sum of pixel_stain / pixel count)
|
||||||
|
std::map<int, std::vector<float>> pixel_stain; // material -> the stain of every opaque sprite pixel (the persistent per-pixel stain map of 00c22080)
|
||||||
|
std::map<std::string, float> stain, ingest; // status name -> stain amount (sum of the coverages that pass the material threshold), ingestion amount
|
||||||
|
std::map<std::string, float> prev_stain; // the stain amount the last update saw (00c24670 +0x60)
|
||||||
|
std::map<std::string, int> cooldown; // stain cooldowns (EntityRemoveStainStatusEffect)
|
||||||
|
int last_sample = -1000;
|
||||||
|
size_t pixels = 0; // the opaque sprite pixels of the last sample
|
||||||
|
};
|
||||||
|
|
||||||
|
StatusSystem(const MaterialDb& db, LuaHost& host, EntityRuntime& rt);
|
||||||
|
bool ok() const { return !defs_.empty(); }
|
||||||
|
const std::vector<Def>& defs() const { return defs_; }
|
||||||
|
State& state(int owner) { return states_[owner]; }
|
||||||
|
void forget(int owner) { states_.erase(owner); }
|
||||||
|
|
||||||
|
// The sampling of the engine (00c22080): the player every 3rd frame or when (frame ^ id) & 15 == 0, the others when (frame ^ id) & 15 == 0. `pixels` are the world cells of the opaque
|
||||||
|
// pixels of the sprite (flipped as drawn, in a stable order), `material_at` gives the material index of a cell (-1: not loaded). A pixel in a staining liquid is stained, the stain
|
||||||
|
// of every pixel is shed over time (shaken_drop_chance); the coverage of a material is its stained pixels over all opaque pixels.
|
||||||
|
bool due(int owner, int frame, bool player) const { return ((frame ^ owner) & 15) == 0 || (player && frame % 3 == 0); }
|
||||||
|
// splash_material > 0: every pixel is stained with it as well (00c23290: the random water stains of a biome modifier, the killer of a spilled flask)
|
||||||
|
void sample(int owner, int frame, bool player, const std::vector<std::pair<int, int>>& pixels, const std::function<int(int, int)>& material_at, int splash_material = 0);
|
||||||
|
// Drinking / swallowing: IngestionSystem::IngestMaterial (00b44e80): amount * pair amount per status.
|
||||||
|
void ingest(int owner, int material, double amount);
|
||||||
|
// 00c24670: activation of the statuses: loads the effect entity of an active status, retires it when it is no longer active.
|
||||||
|
void update(int owner, int frame);
|
||||||
|
|
||||||
|
float stain_amount(int owner, const std::string& status);
|
||||||
|
bool active(int owner, const std::string& status);
|
||||||
|
std::vector<std::string> active_statuses(int owner);
|
||||||
|
std::vector<std::string> ingested(int owner); // the statuses with an ingestion amount (what vomiting can take away)
|
||||||
|
bool stains(int material) { return info(material).mask != 0; } // the material has a stain colour type (it can cover a sprite)
|
||||||
|
void clear_status(int owner, const std::string& status); // its stain and ingestion amounts are zeroed; the next update retires its effect
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct MatInfo { int mask = 0; float threshold = 0.01f, drop = 1.0f; std::vector<std::string> stains; std::vector<std::pair<std::string, float>> ingestion; bool known = false; };
|
||||||
|
const MatInfo& info(int material);
|
||||||
|
const Def* choose(const std::string& status, float normalized_ingestion);
|
||||||
|
const MaterialDb& db_;
|
||||||
|
EntityRuntime& rt_;
|
||||||
|
std::vector<Def> defs_;
|
||||||
|
std::map<int, State> states_;
|
||||||
|
std::vector<MatInfo> mats_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#include "gun/effects.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
float num(const EntityComponent& c, const char* k, float d) {
|
||||||
|
auto it = c.attrs.find(k);
|
||||||
|
return it == c.attrs.end() ? d : (float)std::atof(it->second.c_str());
|
||||||
|
}
|
||||||
|
float num(const std::map<std::string, std::string>& a, const char* k, float d) {
|
||||||
|
auto it = a.find(k);
|
||||||
|
return it == a.end() ? d : (float)std::atof(it->second.c_str());
|
||||||
|
}
|
||||||
|
uint32_t average(const SpriteFrames& s) {
|
||||||
|
uint64_t r = 0, g = 0, b = 0, n = 0;
|
||||||
|
for (size_t i = 0; i < (size_t)s.w * s.h && i < s.texels.size(); ++i) {
|
||||||
|
const uint32_t t = s.texels[i];
|
||||||
|
if ((t >> 24) < 128) continue;
|
||||||
|
r += (t >> 16) & 255; g += (t >> 8) & 255; b += t & 255; ++n;
|
||||||
|
}
|
||||||
|
if (!n) return 0xFFFFB040u;
|
||||||
|
return 0xFF000000u | (uint32_t)(r / n) << 16 | (uint32_t)(g / n) << 8 | (uint32_t)(b / n);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const SpriteFrames* EffectLib::sprite(const std::string& file, const std::string& animation) {
|
||||||
|
if (file.empty()) return nullptr;
|
||||||
|
const std::string key = file + "|" + animation;
|
||||||
|
auto it = sprites_.find(key);
|
||||||
|
if (it == sprites_.end()) {
|
||||||
|
auto s = std::make_unique<SpriteFrames>();
|
||||||
|
if (!load_sprite_frames(wak_, file, animation, *s)) s.reset();
|
||||||
|
it = sprites_.emplace(key, std::move(s)).first;
|
||||||
|
}
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t EffectLib::sprite_color(const SpriteFrames* s) const { return s ? average(*s) : 0xFFFFB040u; }
|
||||||
|
|
||||||
|
ExplosionFx EffectLib::explosion(const std::map<std::string, std::string>& attrs) {
|
||||||
|
ExplosionFx e;
|
||||||
|
auto str = [&](const char* k) { auto it = attrs.find(k); return it == attrs.end() ? std::string() : it->second; };
|
||||||
|
e.radius = num(attrs, "config_explosion.explosion_radius", 0);
|
||||||
|
e.shake = num(attrs, "config_explosion.camera_shake", 0);
|
||||||
|
e.sprite = sprite(str("config_explosion.explosion_sprite"));
|
||||||
|
e.sparks = num(attrs, "config_explosion.sparks_enabled", 0) != 0;
|
||||||
|
e.sparks0 = (int)num(attrs, "config_explosion.sparks_count_min", 0);
|
||||||
|
e.sparks1 = (int)num(attrs, "config_explosion.sparks_count_max", 0);
|
||||||
|
e.material_sparks = num(attrs, "config_explosion.material_sparks_enabled", 0) != 0;
|
||||||
|
e.msparks0 = (int)num(attrs, "config_explosion.material_sparks_count_min", 0);
|
||||||
|
e.msparks1 = (int)num(attrs, "config_explosion.material_sparks_count_max", 0);
|
||||||
|
e.light = num(attrs, "config_explosion.light_enabled", 1) != 0; // (the constructor's defaults, 004b2780: on, 0.08 s, (255, 217, 180), radius x 8)
|
||||||
|
e.light_color = 0xFF000000u | ((uint32_t)std::clamp((int)num(attrs, "config_explosion.light_r", 255), 0, 255) << 16) | ((uint32_t)std::clamp((int)num(attrs, "config_explosion.light_g", 217), 0, 255) << 8) | (uint32_t)std::clamp((int)num(attrs, "config_explosion.light_b", 180), 0, 255);
|
||||||
|
e.light_radius = e.radius * num(attrs, "config_explosion.light_radius_coeff", 8.0f);
|
||||||
|
e.light_frames = std::max(1, (int)std::lround(num(attrs, "config_explosion.light_fade_time", 0.08f) * 60.0f));
|
||||||
|
{
|
||||||
|
std::string sm = str("config_explosion.spark_material");
|
||||||
|
if (sm.empty()) sm = "spark";
|
||||||
|
if (material_color) if (const uint32_t c = material_color(sm)) {
|
||||||
|
const float a = material_alpha ? material_alpha(sm) : 1.0f;
|
||||||
|
auto ch = [&](int sh) { return (uint32_t)std::lround(((c >> sh) & 0xFFu) * a) << sh; };
|
||||||
|
e.spark_color = 0xFF000000u | ch(16) | ch(8) | ch(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.sprite) e.color = average(*e.sprite);
|
||||||
|
e.material_color = e.color;
|
||||||
|
const std::string mat = str("config_explosion.create_cell_material");
|
||||||
|
if (!mat.empty() && material_color) { const uint32_t c = material_color(mat); if (c) e.material_color = c; }
|
||||||
|
e.valid = e.sprite || e.sparks || e.material_sparks;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectLib::muzzle(const EntityDef& def, ExplosionFx& out) {
|
||||||
|
for (const EntityComponent* c : def.all("SpriteComponent")) {
|
||||||
|
if (c->get("_enabled", "1") == "0") continue;
|
||||||
|
std::string file = c->get("image_file");
|
||||||
|
const size_t p = file.find("$["); // "name_0$[1-5].png": one of five variants, the first is used
|
||||||
|
if (p != std::string::npos) {
|
||||||
|
const size_t e = file.find(']', p);
|
||||||
|
if (e != std::string::npos) file = file.substr(0, p) + std::to_string(std::atoi(file.c_str() + p + 2)) + file.substr(e + 1);
|
||||||
|
}
|
||||||
|
out.sprite = sprite(file, c->get("rect_animation"));
|
||||||
|
if (!out.sprite) continue;
|
||||||
|
out.pivot_x = (float)std::atof(c->get("offset_x", "0").c_str());
|
||||||
|
out.pivot_y = (float)std::atof(c->get("offset_y", "0").c_str());
|
||||||
|
out.additive = c->get("additive", "0") == "1";
|
||||||
|
out.wait = 2;
|
||||||
|
out.color = average(*out.sprite);
|
||||||
|
out.valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectLib::projectile(const EntityDef& def, ProjFx& fx) {
|
||||||
|
for (const EntityComponent* c : def.all("LightComponent")) {
|
||||||
|
if (c->get("_enabled", "1") == "0") continue;
|
||||||
|
fx.light = {true, (int)num(*c, "r", 255), (int)num(*c, "g", 255), (int)num(*c, "b", 255), num(*c, "radius", 20)};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const EntityComponent* c : def.all("ProjectileComponent")) {
|
||||||
|
if (num(*c, "shoot_light_flash_radius", 0) > 0)
|
||||||
|
fx.shoot_flash = {true, (int)num(*c, "shoot_light_flash_r", 255), (int)num(*c, "shoot_light_flash_g", 255), (int)num(*c, "shoot_light_flash_b", 255), num(*c, "shoot_light_flash_radius", 0)};
|
||||||
|
auto attrs = c->attrs;
|
||||||
|
if (!def.all("PhysicsBodyComponent").empty() && !def.all("PhysicsBodyCollisionDamageComponent").empty() && !def.all("DamageModelComponent").empty()) // a breakable thrown item: its explosion is the ExplodeOnDamage one (projectiles.cpp break_speed)
|
||||||
|
for (const EntityComponent* eod : def.all("ExplodeOnDamageComponent")) { for (const auto& [k, v] : eod->attrs) if (k.compare(0, 17, "config_explosion.") == 0) attrs[k] = v; break; }
|
||||||
|
fx.boom = explosion(attrs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const EntityComponent* c : def.all("SpriteComponent"))
|
||||||
|
if (c->get("_enabled", "1") != "0" && c->get("additive", "0") == "1") fx.additive = true;
|
||||||
|
for (const EntityComponent* c : def.all("ParticleEmitterComponent")) {
|
||||||
|
if (c->get("_enabled", "1") == "0" || c->get("is_emitting", "1") == "0" || (num(*c, "emit_cosmetic_particles", 0) == 0 && num(*c, "create_real_particles", 0) == 0)) continue;
|
||||||
|
if (!c->get("image_animation_file").empty()) continue; // (an image animation - the pixels of a picture, frame by frame, played once or looped: the host emits it on the projectile's twin, main.cpp)
|
||||||
|
EmitterDef e;
|
||||||
|
const std::string mat = c->get("emitted_material_name");
|
||||||
|
e.real = num(*c, "create_real_particles", 0) != 0 && !mat.empty();
|
||||||
|
e.only_space = num(*c, "emit_only_if_there_is_space", 0) != 0;
|
||||||
|
e.material = mat;
|
||||||
|
if (material_color && !mat.empty()) { const uint32_t col = material_color(mat); if (col) e.color = col; }
|
||||||
|
e.glow = !material_glow || mat.empty() || material_glow(mat);
|
||||||
|
e.rmin = num(*c, "area_circle_radius.min", 0); e.rmax = num(*c, "area_circle_radius.max", 0);
|
||||||
|
if (material_alpha && !mat.empty() && !e.glow) e.alpha = (uint32_t)std::lround(std::clamp(material_alpha(mat), 0.0f, 1.0f) * 255.0f);
|
||||||
|
if (material_alpha && !mat.empty() && !e.real && e.glow && num(*c, "render_ultrabright", 0) == 0) { // (premultiplied: these particles are additive; render_ultrabright (6b:10) draws them at the full colour)
|
||||||
|
const float a = material_alpha(mat);
|
||||||
|
auto ch = [&](int sh) { return (uint32_t)std::lround(((e.color >> sh) & 0xFFu) * a) << sh; };
|
||||||
|
e.color = (e.color & 0xFF000000u) | ch(16) | ch(8) | ch(0);
|
||||||
|
}
|
||||||
|
e.trail = num(*c, "is_trail", 0) != 0;
|
||||||
|
e.trail_gap = num(*c, "trail_gap", 0);
|
||||||
|
e.delay = std::max(0, (int)num(*c, "delay_frames", 0));
|
||||||
|
e.away = num(*c, "velocity_always_away_from_center", 0);
|
||||||
|
e.attractor = num(*c, "attractor_force", 0);
|
||||||
|
e.as_long = num(*c, "draw_as_long", 0) != 0;
|
||||||
|
e.xv0 = num(*c, "x_vel_min", 0) / 60.0f; e.xv1 = num(*c, "x_vel_max", 0) / 60.0f;
|
||||||
|
e.yv0 = num(*c, "y_vel_min", 0) / 60.0f; e.yv1 = num(*c, "y_vel_max", 0) / 60.0f;
|
||||||
|
e.gravity = num(*c, "gravity.y", 0) / 3600.0f;
|
||||||
|
e.count0 = (int)num(*c, "count_min", 1); e.count1 = std::max(e.count0, (int)num(*c, "count_max", 1));
|
||||||
|
e.life0 = num(*c, "lifetime_min", 0.2f) * 60.0f; e.life1 = std::max(e.life0, num(*c, "lifetime_max", 0.2f) * 60.0f);
|
||||||
|
e.interval0 = std::max(1, (int)num(*c, "emission_interval_min_frames", 1)); e.interval1 = std::max(e.interval0, (int)num(*c, "emission_interval_max_frames", 1));
|
||||||
|
e.jitter = num(*c, "airflow_force", 0) * 0.004f;
|
||||||
|
e.ox0 = num(*c, "x_pos_offset_min", 0); e.ox1 = num(*c, "x_pos_offset_max", 0);
|
||||||
|
e.oy0 = num(*c, "y_pos_offset_min", 0); e.oy1 = num(*c, "y_pos_offset_max", 0);
|
||||||
|
e.fade = num(*c, "fade_based_on_lifetime", 0) != 0; // (the component default is 0; 43 projectile emitters leave it out)
|
||||||
|
e.collide = num(*c, "collide_with_grid", 1) != 0;
|
||||||
|
e.force = num(*c, "cosmetic_force_create", 1) != 0;
|
||||||
|
fx.emitters.push_back(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
// Visual effect definitions read from the entity files: the particle emitters of a projectile (trails of cosmetic particles), its light,
|
||||||
|
// the muzzle flash, and the explosion (animated sprite, sparks, material sparks, light flash). The effects themselves are simulated and
|
||||||
|
// drawn on the GPU (shaders/proj_*.comp, fx_*.comp, tables built by gun/gpu_fx.*); this is only the data.
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entity/entity.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct LightDef { bool on = false; int r = 255, g = 255, b = 255; float radius = 0; };
|
||||||
|
|
||||||
|
struct EmitterDef { // ParticleEmitterComponent (cosmetic particles only)
|
||||||
|
uint32_t color = 0xFFFFFFFFu; // colour of the emitted material
|
||||||
|
bool trail = false; // is_trail: spread along the path travelled since the last frame
|
||||||
|
float trail_gap = 0; // trail_gap (px): a trail emits one particle per this much of the path (00bc75e0); 0 = count_min..max
|
||||||
|
float away = 0; // velocity_always_away_from_center (px/s): added along the way out from the emitter
|
||||||
|
float attractor = 0; // attractor_force: the particle is pulled to the emitter, v += (emitter - pos) x dt x this (00713e20)
|
||||||
|
bool as_long = false; // draw_as_long: drawn as a streak along its motion (6a:08)
|
||||||
|
float xv0 = 0, xv1 = 0, yv0 = 0, yv1 = 0; // velocity range, cells per frame
|
||||||
|
float gravity = 0; // cells per frame^2
|
||||||
|
int count0 = 1, count1 = 1; // particles per emission
|
||||||
|
float life0 = 10, life1 = 10; // frames
|
||||||
|
int interval0 = 1, interval1 = 1; // frames between emissions
|
||||||
|
int delay = 0; // delay_frames: nothing is emitted before this many frames (00bc8bb0: a positive delay counts down first)
|
||||||
|
float jitter = 0; // airflow: random velocity per frame
|
||||||
|
float ox0 = 0, ox1 = 0, oy0 = 0, oy1 = 0; // spawn offset ranges
|
||||||
|
bool fade = true;
|
||||||
|
bool collide = true; // collide_with_grid: the particles bounce on / die in solid and liquid cells
|
||||||
|
bool force = true; // cosmetic_force_create: 0 = a particle is made only in an empty cell
|
||||||
|
bool real = false; // create_real_particles: the particles are cells of the material when they land (acid, slime, blood...)
|
||||||
|
bool only_space = false; // emit_only_if_there_is_space: a painted cell does not replace a gas or liquid (00bc75e0 +0x67)
|
||||||
|
std::string material;
|
||||||
|
float rmin = 0, rmax = 0; // area_circle_radius: the particles start on a disc around the point (radius sqrt(u) between min and max)
|
||||||
|
bool glow = true; // the material glows (gfx_glow): drawn additive; else alpha-blended with the material's alpha
|
||||||
|
uint32_t alpha = 255; // (the material's alpha, for the blended ones)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ExplosionFx { // config_explosion.* of a projectile or an exploding prop
|
||||||
|
const SpriteFrames* sprite = nullptr;
|
||||||
|
float radius = 0, shake = 0;
|
||||||
|
bool sparks = false, material_sparks = false, light = false;
|
||||||
|
int sparks0 = 0, sparks1 = 0, msparks0 = 0, msparks1 = 0;
|
||||||
|
uint32_t color = 0xFFFFB040u; // tint of the flash and the sparks (average sprite colour)
|
||||||
|
uint32_t material_color = 0; // colour of the material sparks (the cells the blast creates, else the flash colour)
|
||||||
|
float pivot_x = -1, pivot_y = -1; // sprite pivot (the point placed at the effect position), -1 = the sprite file's offset
|
||||||
|
bool additive = false; // drawn as light
|
||||||
|
uint32_t light_color = 0xFFFFD9B4u; // light_r / g / b (ConfigExplosion constructor 004b2780: 255, 217, 180)
|
||||||
|
float light_radius = 0; // explosion_radius x light_radius_coeff (default 8)
|
||||||
|
int light_frames = 5; // light_fade_time (default 0.08 s) in frames
|
||||||
|
uint32_t spark_color = 0xFFFFC060u; // the colour of spark_material (default "spark"), scaled by its alpha (the sparks are drawn as light)
|
||||||
|
int wait = 0; // game frames per animation frame, 0 = the sprite's own
|
||||||
|
bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProjFx {
|
||||||
|
LightDef light, shoot_flash;
|
||||||
|
std::vector<EmitterDef> emitters;
|
||||||
|
ExplosionFx boom, muzzle; // muzzle: the sprite of the muzzle flash, drawn oriented along the shot
|
||||||
|
bool additive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loads and caches the effect resources.
|
||||||
|
class EffectLib {
|
||||||
|
public:
|
||||||
|
explicit EffectLib(const Wak& wak) : wak_(wak) {}
|
||||||
|
std::function<uint32_t(const std::string&)> material_color; // opaque ARGB of a material, 0 when unknown
|
||||||
|
std::function<bool(const std::string&)> material_glow; // the material has gfx_glow (its cosmetic particles are light); unset: all are
|
||||||
|
std::function<float(const std::string&)> material_alpha; // the alpha of the material's colour (plasma_fading 0x77): the cosmetic particles of the trails are drawn as light scaled by it
|
||||||
|
const SpriteFrames* sprite(const std::string& file, const std::string& animation = "");
|
||||||
|
uint32_t sprite_color(const SpriteFrames* s) const; // the average colour of its texels (the tint of an explosion's flash and sparks)
|
||||||
|
ExplosionFx explosion(const std::map<std::string, std::string>& attrs); // attrs with the "config_explosion." prefix (projectiles, exploding props)
|
||||||
|
void projectile(const EntityDef& def, ProjFx& out);
|
||||||
|
void muzzle(const EntityDef& flash, ExplosionFx& out); // the muzzle flash entity (muzzle_flash_file): its sprite
|
||||||
|
private:
|
||||||
|
const Wak& wak_;
|
||||||
|
std::map<std::string, std::unique_ptr<SpriteFrames>> sprites_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
#include "gun/gpu_fx.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr double kDeg = 3.14159265358979323846 / 180.0;
|
||||||
|
constexpr size_t kCastBase = 4; // float index of the cast lists in the host buffer
|
||||||
|
constexpr size_t kNodeBase = kCastBase + 1024 * 4;
|
||||||
|
constexpr size_t kSpecBase = kNodeBase + 8192 * 4; // the specs are read as ints
|
||||||
|
|
||||||
|
uint32_t fbits(float f) { uint32_t u; std::memcpy(&u, &f, 4); return u; }
|
||||||
|
|
||||||
|
struct Words { // 4 words of a table entry
|
||||||
|
uint32_t* w;
|
||||||
|
void f(int i, float v) { w[i] = fbits(v); }
|
||||||
|
void i(int k, int32_t v) { w[k] = (uint32_t)v; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void GpuFx::attach(uint32_t* tables, uint32_t* tex, void* host) {
|
||||||
|
tables_ = tables; tex_ = tex;
|
||||||
|
hostF_ = (float*)host; hostI_ = (int32_t*)host;
|
||||||
|
std::memset(host, 0, 16);
|
||||||
|
std::memset(tables, 0, kTableBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFx::disable_gpu_lua() {
|
||||||
|
gpu_lua_enabled = false;
|
||||||
|
for (int type = 0; type < types_; ++type) {
|
||||||
|
auto& flags = tables_[(type * kTypeStride + 11) * 4 + 1];
|
||||||
|
if ((flags & (1u << 20)) != 0) flags = (flags & ~((1u << 20) | (255u << 21))) | 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpuFx::sprite_words(const uint32_t* texels, int count) {
|
||||||
|
auto it = texOf_.find(texels);
|
||||||
|
if (it != texOf_.end()) return it->second;
|
||||||
|
if (texNext_ + (size_t)count > kTexWords - 32768) return -1; // the last 32768 words hold the wands in the hand
|
||||||
|
std::memcpy(tex_ + texNext_, texels, (size_t)count * 4);
|
||||||
|
const int off = (int)texNext_;
|
||||||
|
texNext_ += (size_t)count;
|
||||||
|
texOf_[texels] = off;
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t half_bits(float f) { // float -> IEEE half (round to nearest, no denormals needed: the forces are small positive numbers)
|
||||||
|
uint32_t x; std::memcpy(&x, &f, 4);
|
||||||
|
const uint32_t sign = (x >> 16) & 0x8000u;
|
||||||
|
int e = (int)((x >> 23) & 255) - 127 + 15;
|
||||||
|
uint32_t m = x & 0x7FFFFFu;
|
||||||
|
if (e <= 0) return sign;
|
||||||
|
if (e >= 31) return sign | 0x7BFFu;
|
||||||
|
uint32_t h = sign | ((uint32_t)e << 10) | (m >> 13);
|
||||||
|
if (m & 0x1000u) ++h;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpuFx::boom_for(const ExplosionFx& fx) {
|
||||||
|
if (!fx.valid) return -1;
|
||||||
|
char key[320];
|
||||||
|
std::snprintf(key, sizeof key, "%p|%g|%u|%u|%d%d%d%d|%d,%d,%d,%d|%g,%g|%d|%u,%g,%d,%u", (const void*)fx.sprite, fx.radius, fx.color, fx.material_color, fx.sparks, fx.material_sparks, fx.light, fx.additive, fx.sparks0, fx.sparks1, fx.msparks0, fx.msparks1, fx.pivot_x, fx.pivot_y, fx.wait, fx.light_color, fx.light_radius, fx.light_frames, fx.spark_color);
|
||||||
|
auto it = boomOf_.find(key);
|
||||||
|
if (it != boomOf_.end()) return it->second;
|
||||||
|
if (booms_ >= kMaxBooms) return -1;
|
||||||
|
uint32_t* b = tables_ + (size_t)(kBoomBase + booms_ * kBoomStride) * 4;
|
||||||
|
Words b0{b}, b1{b + 4}, b2{b + 8}, b3{b + 12};
|
||||||
|
int off = -1, w = 0, h = 0, frames = 1, wait = 6;
|
||||||
|
if (fx.sprite && fx.sprite->ok()) {
|
||||||
|
off = sprite_words(fx.sprite->texels.data(), (int)fx.sprite->texels.size());
|
||||||
|
w = fx.sprite->w; h = fx.sprite->h; frames = fx.sprite->frames; wait = fx.sprite->wait;
|
||||||
|
}
|
||||||
|
if (off < 0) { off = 0; w = h = 0; }
|
||||||
|
if (fx.wait > 0) wait = fx.wait;
|
||||||
|
const float pivx = fx.pivot_x >= 0 ? fx.pivot_x : (fx.sprite ? fx.sprite->off_x : w * 0.5f), pivy = fx.pivot_y >= 0 ? fx.pivot_y : (fx.sprite ? fx.sprite->off_y : h * 0.5f);
|
||||||
|
b0.i(0, off); b0.i(1, w); b0.i(2, h); b0.i(3, frames);
|
||||||
|
// b1.y: the light colour; b1.w: flags, bits 4-15 the light radius, 16-27 its fade frames; b2.x: sparks_count_min | the spark colour << 8
|
||||||
|
b1.i(0, wait); b1.i(1, (int)(fx.light_color & 0xFFFFFFu)); b1.i(2, (int)(fx.material_color & 0xFFFFFFu));
|
||||||
|
b1.i(3, (int)((fx.sparks ? 1u : 0u) | (fx.material_sparks ? 2u : 0u) | (fx.light ? 4u : 0u) | (fx.additive ? 8u : 0u) | ((uint32_t)std::clamp((int)std::lround(fx.light_radius), 0, 4095) << 4) | ((uint32_t)std::clamp(fx.light_frames, 0, 4095) << 16)));
|
||||||
|
b2.i(0, (int)((uint32_t)std::clamp(fx.sparks0, 0, 255) | ((fx.spark_color & 0xFFFFFFu) << 8))); b2.i(1, std::max(std::min(fx.sparks0, 255), fx.sparks1)); b2.i(2, fx.msparks0); b2.i(3, std::max(fx.msparks0, fx.msparks1));
|
||||||
|
b3.f(0, fx.radius); b3.f(1, fx.shake); b3.f(2, pivx); b3.f(3, pivy);
|
||||||
|
return boomOf_[key] = booms_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpuFx::type_for(const ProjDef& d, ProjectileSim* sim) {
|
||||||
|
auto it = typeOf_.find(&d);
|
||||||
|
if (it != typeOf_.end()) return it->second;
|
||||||
|
if (types_ >= kMaxTypes) return 0;
|
||||||
|
const int loadFirst = 0, loadCount = d.load_files.empty() ? 0 : 1; // load_this_entity is spawned by the host (an event of the explosion); the type only says whether there is any
|
||||||
|
const int first_emit = emitters_;
|
||||||
|
for (const EmitterDef& e : d.fx.emitters) {
|
||||||
|
if (emitters_ >= kMaxEmitters) break;
|
||||||
|
if (std::getenv("NG_EMITLOG")) std::printf("emitter %d of a type: colour %08x glow %d count %d-%d life %.0f-%.0f r %.0f-%.0f away %.0f attractor %.0f long %d%c", emitters_, e.color, (int)e.glow, e.count0, e.count1, e.life0, e.life1, e.rmin, e.rmax, e.away, e.attractor, (int)e.as_long, 10);
|
||||||
|
uint32_t* w = tables_ + (size_t)(kEmitBase + emitters_ * kEmitStride) * 4;
|
||||||
|
Words e0{w}, e1{w + 4}, e2{w + 8}, e3{w + 12};
|
||||||
|
// e0.y: flags (bit 5: not glowing - blended), bits 8-19 / 20-31: area_circle_radius max / min in quarter pixels; e0.x bits 24-31: the alpha of a blended one
|
||||||
|
const uint32_t qmax = (uint32_t)std::clamp((int)std::lround(e.rmax * 4.0f), 0, 4095), qmin = (uint32_t)std::clamp((int)std::lround(e.rmin * 4.0f), 0, 4095);
|
||||||
|
e0.i(0, (int)((e.color & 0xFFFFFFu) | (e.glow ? 0u : (e.alpha & 0xFFu) << 24))); e0.i(1, (int)((e.trail ? 1u : 0u) | (e.fade ? 2u : 0u) | (e.real ? 4u : 0u) | (e.collide ? 0u : 8u) | (e.force ? 0u : 16u) | (e.glow ? 0u : 32u) | (e.only_space ? 64u : 0u) | (e.as_long ? 128u : 0u) | qmax << 8 | qmin << 20)); e0.f(2, e.life0); e0.f(3, e.life1);
|
||||||
|
e1.f(0, e.xv0); e1.f(1, e.xv1); e1.f(2, e.yv0); e1.f(3, e.yv1);
|
||||||
|
e2.f(0, e.gravity); e2.i(1, (int)(std::min(e.interval0 + e.interval1, 255) | std::clamp((int)std::lround(e.attractor), 0, 255) << 8) | (std::clamp((int)std::lround(e.away), -32768, 32767) << 16)); e2.i(2, std::min(e.count0, 65535) | (std::min(e.delay, 32767) << 16)); e2.i(3, std::max(e.count0, e.count1)); // (z: count_min | delay_frames << 16) // (y: the mean interval in half frames (min + max) | attractor_force << 8 | velocity_always_away_from_center (px/s, signed) << 16)
|
||||||
|
e3.f(0, e.jitter); e3.i(1, e.real && material_index ? material_index(e.material) : 0); e3.f(2, 0.5f * (e.ox1 - e.ox0)); e3.f(3, 0.5f * (e.oy1 - e.oy0));
|
||||||
|
++emitters_;
|
||||||
|
}
|
||||||
|
const int n_emit = emitters_ - first_emit;
|
||||||
|
if (std::getenv("NG_EMITLOG")) std::printf("type %d %s: emitters %d..+%d%c", types_, d.file.c_str(), first_emit, n_emit, 10);
|
||||||
|
uint32_t* t = tables_ + (size_t)(types_ * kTypeStride) * 4;
|
||||||
|
Words t0{t}, t1{t + 4}, t2{t + 8}, t3{t + 12}, t4{t + 16}, t5{t + 20}, t6{t + 24}, t7{t + 28}, t8{t + 32}, t9{t + 36}, t10{t + 40}, t11{t + 44}, t12{t + 48}, t13{t + 52}, t14{t + 56}, t15{t + 60}, t16{t + 64}, t17{t + 68};
|
||||||
|
t0.f(0, d.speed_min); t0.f(1, d.speed_max); t0.f(2, d.damage); t0.f(3, (float)d.lifetime);
|
||||||
|
const bool square = d.sprite_w == d.sprite_h;
|
||||||
|
int crit_condition = 0; // CRITICAL_HIT_BOOST of a hit effect: +100 % critical chance while the SHOOTER has the game effect (1 ON_FIRE, 2 BLOODY, 3 OILED, 4 WET)
|
||||||
|
for (const ProjDef::HitEffect& he : d.hit_effects)
|
||||||
|
if (he.effect == "CRITICAL_HIT_BOOST") crit_condition = he.condition_effect == "ON_FIRE" ? 1 : he.condition_effect == "BLOODY" ? 2 : he.condition_effect == "OILED" ? 3 : he.condition_effect == "WET" ? 4 : 5;
|
||||||
|
int boost_stain = 0, boost_mult = 0; // DAMAGE_BOOST of a hit effect: a factor on the damage while the victim has the stain
|
||||||
|
for (const ProjDef::HitEffect& he : d.hit_effects)
|
||||||
|
if (he.effect == "DAMAGE_BOOST") {
|
||||||
|
static const char* stains[6] = {"WET", "OILED", "BLOODY", "ALCOHOLIC", "SLIMY", "RADIOACTIVE"};
|
||||||
|
for (int i = 0; i < 6; ++i) if (he.condition_status == stains[i]) { boost_stain = i + 1; boost_mult = (int)he.value; }
|
||||||
|
}
|
||||||
|
t1.f(0, d.gravity); t1.f(1, d.air_friction / 60.0f); t1.f(2, d.spread_rad);
|
||||||
|
t1.i(3, (d.invisible ? 8192 : 0) | (d.particle ? (int32_t)0x80000000u : 0) | (d.dont_damage_shooter ? 16384 : 0) | (d.not_scaled_by_gamefx ? 32768 : 0) | (d.on_collision_die ? 1 : 0) | (d.explode_on_death ? 2 : 0) | (d.hole ? 4 : 0) | ((!square && d.sprite_w > 0) ? 8 : 0) | (d.fx.additive ? 16 : 0) | (d.physics ? 32 : 0) | (d.die_on_liquid ? 64 : 0) | (!d.hit_effects.empty() ? 512 : 0) | (d.friendly_fire ? 128 : 0) | (d.dont_hit_player ? 256 : 0) | ((d.collide_with_shooter_frames < 0 ? 255 : std::min(d.collide_with_shooter_frames, 254)) << 16) | ((crit_condition & 7) << 10) | ((boost_stain & 7) << 24) | ((std::min(boost_mult, 15) & 15) << 27));
|
||||||
|
const int mat = (!d.explosion_material.empty() && material_index) ? material_index(d.explosion_material) : -1;
|
||||||
|
t2.f(0, d.explosion_radius); t2.f(1, d.explosion_damage); t2.f(2, d.max_durability + 4096.0f * (d.stains && !std::getenv("NG_NOSTAINS") ? 1.0f + std::min(4000.0f, std::round(std::max(0.0f, d.stains_radius))) : 0.0f)); // (+ 4096 x (1 + stains_radius) when stains_enabled: explode_rays.comp)
|
||||||
|
t2.i(3, mat); // create_cell_material (it was lost at the end of the comment above from eb55046 on: no explosion made its cells)
|
||||||
|
t3.f(0, d.explosion_cell_probability / 100.0f); t3.i(1, boom_for(d.fx.boom)); t3.i(2, first_emit); t3.i(3, n_emit);
|
||||||
|
t4.f(0, d.fx.light.on ? d.fx.light.r / 255.0f : 0.0f); t4.f(1, d.fx.light.on ? d.fx.light.g / 255.0f : 0.0f); t4.f(2, d.fx.light.on ? d.fx.light.b / 255.0f : 0.0f); t4.f(3, d.fx.light.on ? d.fx.light.radius : 0.0f);
|
||||||
|
int off = 0;
|
||||||
|
if (!d.sprite.empty() && d.sprite_w > 0) { off = sprite_words(d.sprite.data(), (int)d.sprite.size()); if (off < 0) off = 0; }
|
||||||
|
const int sw = off >= 0 && !d.sprite.empty() ? d.sprite_w : 0, sh = d.sprite_h;
|
||||||
|
t5.i(0, off); t5.i(1, sw); t5.i(2, sh); t5.i(3, d.sprite_frames);
|
||||||
|
const LightDef& fl = d.fx.shoot_flash;
|
||||||
|
t6.i(0, d.sprite_wait); t6.i(1, (int)(d.color & 0xFFFFFFu));
|
||||||
|
t6.i(2, fl.on ? (int)(((uint32_t)fl.r << 16 | (uint32_t)fl.g << 8 | (uint32_t)fl.b) & 0xFFFFFFu) : 0); t6.f(3, fl.on ? fl.radius : 0.0f);
|
||||||
|
t7.i(0, boom_for(d.fx.muzzle)); t7.i(1, d.bounces_left); t7.f(2, d.ray_energy); t7.i(3, (int)(half_bits(d.physics_min) | (half_bits(d.physics_max) << 16))); // the physics force range as two halves
|
||||||
|
t8.f(0, d.liquid_drag); t8.i(1, d.displace_liquid ? 1 : 0); t8.f(2, d.knockback_force); t8.f(3, d.damage_mortals ? 1.0f : 0.0f);
|
||||||
|
t9.f(0, d.hit_knockback_force); t9.f(1, d.velocity_mass); t9.f(2, d.ragdoll_force_multiplier); t9.i(3, loadFirst | (std::min(loadCount, 255) << 12) | (std::min((int)(d.die_speed * 256.0f), 4095) << 20));
|
||||||
|
t10.f(0, d.homing.detect); t10.f(1, d.homing.mult); t10.f(2, d.homing.coeff); t10.f(3, d.homing.max_turn); // HomingComponent
|
||||||
|
t11.i(0, d.homing.on ? (1 | (d.homing.rotate ? 2 : 0) | (d.homing.who_shot ? 4 : 0) | (d.homing.tag_bits << 4)) : 0);
|
||||||
|
const bool automaticLua = gpu_lua_enabled && d.gpu_lua_every > 0 && !d.needs_twin;
|
||||||
|
const bool hostTwin = d.needs_twin || (d.gpu_lua_every > 0 && !gpu_lua_enabled);
|
||||||
|
t11.i(1, (hostTwin ? 1 : 0) | (d.collide_with_world ? 0 : 2) | (d.penetrate_entities ? 4 : 0) | (d.collide_with_entities ? 0 : 8) | ((d.ragdoll_fx & 15) << 4) | (std::clamp((int)std::lround(d.ragdoll_explosion_mult * 256.0f), 0, 4095) << 8) | (automaticLua ? (1 << 20) | (d.gpu_lua_every << 21) : 0)); // bit 20 / bits 21..28: automatic GPU Lua / cadence; bits 4..19 retain ragdoll effects
|
||||||
|
t11.i(2, std::min(d.damage_every_x_frames, 65535) | (std::clamp((int)std::lround(d.break_speed), 0, 65535) << 16)); // (+ the break speed of a breakable thrown body in px/s, bits 16-31)
|
||||||
|
t[(11 * 4) + 1] |= (d.query_projectile ? (1u << 29) : 0u) | (d.query_velocity ? (1u << 30) : 0u);
|
||||||
|
t11.i(3, d.homing.target_enc); // HomingComponent predefined_target as a GPU slot (see ProjDef::Homing::target_enc)
|
||||||
|
for (int i = 0; i < 12; ++i) (i < 4 ? t12 : i < 8 ? t13 : t14).f(i % 4, d.dmg_type[i]); // damage_by_type channels
|
||||||
|
// the world collision of 00c073b0 and the integration of 00c370c0
|
||||||
|
t15.f(0, d.penetrate_world_velocity_coeff); t15.f(1, d.ground_penetration_coeff * d.velocity_mass); t15.i(2, d.ground_penetration_max_durability); t15.f(3, d.bounce_energy);
|
||||||
|
t16.i(0, (d.penetrate_world ? 1 : 0) | (d.bounce_always ? 2 : 0) | (d.bounce_at_any_angle ? 4 : 0) | (d.on_collision_remove_projectile ? 8 : 0) | (d.die_on_low_velocity ? 16 : 0) | (d.apply_terminal_velocity ? 64 : 0) | (d.damage_scaled_by_speed ? 128 : 0) | (d.projectile_type == 1 ? 256 : 0) | (d.projectile_type == 3 ? 512 : 0) | (!d.spawn_entity.empty() ? 1024 : 0) | (!d.bounce_fx_file.empty() ? 2048 : 0)); // 1024: on_collision_spawn_entity, 2048: bounce_fx_file // 256: LIGHTNING, 512: MATERIAL_PARTICLE
|
||||||
|
t16.f(1, d.die_on_low_velocity_limit); t16.f(2, d.terminal_velocity); t16.f(3, d.physics_impulse_coeff);
|
||||||
|
if (d.verlet.on && verlets_ < kMaxVerlet) { // VERLET (the tentacles): bit 4096 of t16.x, the index + 1 in bits 13-17; the chain's parameters and point sprites in the VERLET region
|
||||||
|
const int vi = verlets_++;
|
||||||
|
t[64] |= 4096u | ((uint32_t)(vi + 1) << 13);
|
||||||
|
uint32_t* v = tables_ + (size_t)(kVerletBase + vi * kVerletStride) * 4;
|
||||||
|
Words v0{v}, v1{v + 4}, v2{v + 8}, v3{v + 12};
|
||||||
|
const ProjDef::Verlet& vd = d.verlet;
|
||||||
|
v0.i(0, vd.points); v0.f(1, vd.rest); v0.f(2, vd.stiff); v0.f(3, vd.damp);
|
||||||
|
v1.i(0, vd.cells ? 1 : 0); v1.f(1, vd.liquid_damp); v1.i(2, vd.fade); v1.i(3, (int)std::min<size_t>(vd.parts.size(), 16));
|
||||||
|
v2.f(0, vd.dmg_radius); v2.f(1, vd.dmg_min); v2.f(2, vd.dmg_max); v2.f(3, vd.dmg_coeff);
|
||||||
|
v3.f(0, vd.impulse); v3.f(1, vd.force_radius); v3.f(2, vd.phys_impulse); v3.f(3, 0.0f);
|
||||||
|
for (size_t k = 0; k < vd.parts.size() && k < 16; ++k) {
|
||||||
|
const ProjDef::Verlet::Part& pt = vd.parts[k];
|
||||||
|
Words s{v + 16 + k * 4};
|
||||||
|
const int so = pt.w > 0 ? sprite_words(pt.tex.data(), (int)pt.tex.size()) : -1;
|
||||||
|
s.i(0, so); s.i(1, pt.w | (pt.h << 16)); s.f(2, pt.ox); s.f(3, pt.oy);
|
||||||
|
}
|
||||||
|
if (std::getenv("NG_TYPELOG")) std::printf("type %d %s: VERLET %d, %d points, %zu sprites%c", types_, d.file.c_str(), vi, vd.points, vd.parts.size(), 10);
|
||||||
|
}
|
||||||
|
const int dpm = !d.death_particle.empty() && material_index ? material_index(d.death_particle) : -1;
|
||||||
|
t17.f(0, d.gravity_x); t17.f(1, d.damage_scale_max_speed); t17.i(2, d.lifetime_randomness); t17.i(3, dpm > 0 ? (dpm + 1) | (std::min(d.death_particle_count, 32767) << 16) : 0);
|
||||||
|
if (std::getenv("NG_TYPELOG")) std::printf("type %d %s: t1.w %08x t16.x %08x bounces %d ocd %d%c", types_, d.file.c_str(), t[12 + 3], t[64], d.bounces_left, (int)d.on_collision_die, 10);
|
||||||
|
typeEffects_.resize(types_ + 1);
|
||||||
|
typeEffects_[types_] = d.hit_effects;
|
||||||
|
typeDef_.resize(types_ + 1);
|
||||||
|
typeDef_[types_] = &d;
|
||||||
|
return typeOf_[&d] = types_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpuFx::add_node(ProjectileSim& sim, const Shot& s, double child_mult) {
|
||||||
|
const int node = nodeNext_++ % (kMaxNodes - kPersistent);
|
||||||
|
const int ring = kMaxSpecs - kPersistent;
|
||||||
|
int first = specNext_ % ring;
|
||||||
|
if (first + (int)s.projectiles.size() > ring) { specNext_ += ring - first; first = 0; } // the specs of a node stay together
|
||||||
|
specNext_ += (int)s.projectiles.size();
|
||||||
|
fill_node(sim, s, node, first, child_mult);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpuFx::persistent_node(ProjectileSim& sim, const std::string& file) {
|
||||||
|
if (persistNode_ >= kPersistent || persistSpec_ >= kPersistent) return -1;
|
||||||
|
Shot s;
|
||||||
|
s.projectiles.resize(1);
|
||||||
|
s.projectiles[0].file = file;
|
||||||
|
const int node = kMaxNodes - kPersistent + persistNode_++;
|
||||||
|
const int first = kMaxSpecs - kPersistent + persistSpec_++;
|
||||||
|
fill_node(sim, s, node, first);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFx::fill_node(ProjectileSim& sim, const Shot& s, int node, int first, double child_mult) {
|
||||||
|
const int n = (int)s.projectiles.size();
|
||||||
|
float* nf = hostF_ + kNodeBase + (size_t)node * 16;
|
||||||
|
const ShotState& st = s.state;
|
||||||
|
nf[0] = (float)first; nf[1] = (float)n; nf[2] = (float)st.get("damage_electricity_add"); nf[3] = (float)st.get("damage_ice_add"); // the wand's adds of the electricity and ice channels
|
||||||
|
nf[4] = (float)(std::max(0.0, st.get("spread_degrees", 0)) * kDeg); nf[5] = (float)(st.get("pattern_degrees", 0) * kDeg);
|
||||||
|
nf[6] = (float)(st.get("speed_multiplier", 1) * child_mult); nf[7] = (float)st.get("lifetime_add", 0); // (child_mult: the payload's own child_speed_multiplier when a world hit casts it, 00c06250)
|
||||||
|
nf[8] = (float)st.get("damage_projectile_add"); // the projectile channel (00c0b440: every damage_*_add goes to its own channel; melee / fire / slice / drill adds do not occur in gun_actions.lua and are not carried)
|
||||||
|
nf[9] = (float)(st.get("gravity", 0) / 3600.0); nf[10] = (float)st.get("bounces", 0); nf[11] = (float)st.get("explosion_radius", 0);
|
||||||
|
nf[12] = (float)st.get("damage_explosion_add", 0); nf[13] = (float)(std::min(8191.0, std::max(0.0, st.get("damage_critical_chance", 0))) + 8192.0 * std::clamp((int)st.get("lightning_count", 0), 0, 15)); // + lightning_count x 8192 (00c0b440: added to config_explosion electricity_count, +408) nf[14] = (float)st.get("knockback_force", 0); // knockback_force added to the projectile's (00c0b440: +5d0)
|
||||||
|
{ const std::string tm = st.gets("trail_material"); std::string first_tm; for (size_t pos = 0; pos < tm.size(); ++pos) { if (tm[pos] == ',') break; first_tm += tm[pos]; } // trail_material: a real-particle trail emitter of that material (00c0b440: count += trail_material_amount; the first material of the list)
|
||||||
|
const int tmi = (!first_tm.empty() && material_index) ? material_index(first_tm) : -1; const int amount = std::clamp((int)st.get("trail_material_amount", 0), 0, 2047);
|
||||||
|
nf[15] = (float)((st.get("friendly_fire", 0) != 0 ? 1 : 0) | (tmi >= 0 ? ((tmi + 1) << 1) | (amount << 13) : 0)); } // nf[13]: the critical hit chance in percent (the wand and the modifiers)
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const ShotProjectile& sp = s.projectiles[i];
|
||||||
|
int32_t* sw = hostI_ + kSpecBase + (size_t)(first + i) % kMaxSpecs * 16;
|
||||||
|
std::memset(sw, 0, 64);
|
||||||
|
{ const std::string ge = st.gets("game_effect_entities"), cm = st.gets("material");
|
||||||
|
const std::string key = st.gets("extra_entities") + (cm.empty() ? "" : "|cm=" + cm + ":" + std::to_string((int)st.get("material_amount", 0))) + (ge.empty() ? "" : "|ge=" + ge);
|
||||||
|
const ProjDef* pd = sp.def_key.empty() ? sim.def(sp.file, key) : sim.def(sp.def_key);
|
||||||
|
pd = sim.explosion_variant(pd, nf[11], nf[12]); // (00c06460: the explosion of a cast that adds explosion_radius)
|
||||||
|
sw[0] = type_for(*pd, &sim); }
|
||||||
|
const int nt = std::min<int>(3, (int)sp.triggers.size());
|
||||||
|
sw[1] = nt;
|
||||||
|
for (int t = 0; t < nt; ++t) {
|
||||||
|
const ShotTrigger& tr = sp.triggers[t];
|
||||||
|
sw[4 + t * 4] = (int)tr.type; sw[5 + t * 4] = tr.delay;
|
||||||
|
// 00c06250 (a world hit casts the hit-world and waiting timer triggers): every projectile of the payload gets speed_multiplier x its own child_speed_multiplier (the payload's
|
||||||
|
// state, a fresh one per trigger: gun.lua create_shot); the other ways of casting (00c06c60, 00c091f0, 00c06fc0, 00c0e3e0) and the cast itself (00c0b440) do not use it.
|
||||||
|
// Node .z casts the payload as it is, node .w for a world hit (the same node when the multiplier is 1).
|
||||||
|
const bool has = tr.shot && !tr.shot->projectiles.empty();
|
||||||
|
sw[6 + t * 4] = has ? add_node(sim, *tr.shot, 1.0) : -1;
|
||||||
|
const double csm = has ? tr.shot->state.get("child_speed_multiplier", 1) : 1.0;
|
||||||
|
sw[7 + t * 4] = !has ? -1 : (csm == 1.0 ? sw[6 + t * 4] : add_node(sim, *tr.shot, csm));
|
||||||
|
}
|
||||||
|
for (int t = nt; t < 3; ++t) { sw[6 + t * 4] = -1; sw[7 + t * 4] = -1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GpuFx::request(int frame, double x, double y, double dx, double dy, int node, int flags, int shooter, int herd) {
|
||||||
|
const int list = frame & 1;
|
||||||
|
int32_t& count = hostI_[list];
|
||||||
|
if (count >= kMaxRequests) return false;
|
||||||
|
float* r = hostF_ + kCastBase + (size_t)((list * 256 + count) * 2) * 4;
|
||||||
|
r[0] = (float)x; r[1] = (float)y; r[2] = (float)dx; r[3] = (float)dy;
|
||||||
|
r[4] = (float)node; r[5] = (float)(1 + (herd << 3)); r[6] = (float)(frame * 7 + count); r[7] = (float)(flags + shooter * 2);
|
||||||
|
++count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GpuFx::cast(ProjectileSim& sim, const ShotResult& shot, double x, double y, double dx, double dy, int frame, int shooter, int herd) {
|
||||||
|
if (shot.root.projectiles.empty()) return false;
|
||||||
|
return request(frame, x, y, dx, dy, add_node(sim, shot.root), 1, shooter, herd);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GpuFx::shoot(int frame, double x, double y, double vx, double vy, int node, int team, int shooter, int herd, bool scripts_done) {
|
||||||
|
if (!request(frame, x, y, vx, vy, node, 1, shooter, herd)) return false;
|
||||||
|
float* r = hostF_ + kCastBase + (size_t)((((frame & 1) * 256 + hostI_[frame & 1] - 1) * 2)) * 4;
|
||||||
|
r[5] = (float)(team + 4 + (herd << 3) + (scripts_done ? (1 << 30) : 0)); // bit 2: the speed is the length of the direction vector, bit 30: the script_shot scripts have run
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFx::boom(const ExplosionFx& fx, double x, double y, int frame) {
|
||||||
|
const int b = boom_for(fx);
|
||||||
|
if (b >= 0) request(frame, x, y, 0, 0, -b - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
// Host side of the GPU wand/projectile/effect system (shaders/proj_spawn.comp, proj_step.comp, fx_*.comp). The CPU runs the spell
|
||||||
|
// script (gun.lua) and then only uploads: the tables of projectile types, explosion visuals and emitters (read from the entity files),
|
||||||
|
// and the shot tree of every cast as nodes and specs plus one cast request. Everything after that happens on the GPU.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "gun/projectiles.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
class GpuFx {
|
||||||
|
public:
|
||||||
|
// Layout constants shared with shaders/fx_common.glsl.
|
||||||
|
static constexpr int kTypeStride = 18, kMaxTypes = 512, kBoomBase = 9216, kBoomStride = 4, kMaxBooms = 64, kEmitBase = 9472, kEmitStride = 4, kMaxEmitters = 1024, kLoadBase = 13568, kMaxLoads = 128;
|
||||||
|
static constexpr int kVerletBase = kLoadBase + kMaxLoads, kVerletStride = 20, kMaxVerlet = 32; // the VERLET types: 4 vec4 of parameters, then the sprites of 16 points
|
||||||
|
static constexpr size_t kTableBytes = (size_t)(kVerletBase + kMaxVerlet * kVerletStride) * 16;
|
||||||
|
static constexpr size_t kTexWords = 1u << 20;
|
||||||
|
static constexpr int kMaxNodes = 2048, kMaxSpecs = 4096, kMaxRequests = 256, kPersistent = 128; // the last 128 nodes and specs are permanent (creature attacks), the rest is a ring
|
||||||
|
static constexpr size_t kHostBytes = 16 + 1024 * 16 + 8192 * 16 + 16384 * 16;
|
||||||
|
static constexpr int kMaxProj = 32768; // the projectile pool (shaders/fx_state.glsl FP_MAX); a new projectile takes a free record, a live one is overwritten only when the pool is full
|
||||||
|
static constexpr size_t kStateBytes = 16 + (size_t)kMaxProj * 96 + 16384 * 48 + 512 * 48; // (animations: 0..255 the GPU's ring, 256..511 the host's)
|
||||||
|
static constexpr size_t kLightBytes = 16 + 512 * 32;
|
||||||
|
static constexpr size_t kEventBytes = 16 + 512 * 16;
|
||||||
|
|
||||||
|
std::function<int(const std::string&)> material_index; // cell material id of a name, -1 when unknown
|
||||||
|
void attach(uint32_t* tables, uint32_t* tex, void* host);
|
||||||
|
// Casts a shot from (x, y) along (dx, dy) on the frame `frame` (the request goes into the list that frame consumes).
|
||||||
|
bool cast(ProjectileSim& sim, const ShotResult& shot, double x, double y, double dx, double dy, int frame, int shooter_slot_plus_1 = 1, int herd_plus_1 = 0); // the shooter is the slot + 1 of the entity that casts (the player: 1)
|
||||||
|
// A shot of an entity file from a script (GameShootProjectile): `node` from persistent_node, velocity in cells per frame (it replaces the file's speed), team 1 the player's, 2 enemies.
|
||||||
|
bool shoot(int frame, double x, double y, double vx, double vy, int node, int team, int shooter_slot_plus_1, int herd_plus_1, bool scripts_done = false); // scripts_done: the script_shot scripts have already seen this shot
|
||||||
|
// Visual-only explosion (exploding props): the sprite, sparks and light flash of config_explosion attributes.
|
||||||
|
void boom(const ExplosionFx& fx, double x, double y, int frame);
|
||||||
|
// A node that casts one projectile of an entity file (no modifiers, no triggers) and stays valid: the ranged attack of a creature.
|
||||||
|
int persistent_node(ProjectileSim& sim, const std::string& file);
|
||||||
|
const std::vector<ProjDef::HitEffect>& type_effects(int type) const { static const std::vector<ProjDef::HitEffect> none; return type >= 0 && type < (int)typeEffects_.size() ? typeEffects_[type] : none; }
|
||||||
|
int type_count() const { return (int)typeDef_.size(); }
|
||||||
|
bool gpu_lua_enabled = false;
|
||||||
|
void disable_gpu_lua(); // source changed: existing/future automatic types request CPU twins instead
|
||||||
|
uint32_t type_flags(int type) const { return tables_ && type >= 0 && type < types_ ? tables_[(type * kTypeStride + 11) * 4 + 1] : 1u; }
|
||||||
|
const ProjDef* type_def(int type) const { return type >= 0 && type < (int)typeDef_.size() ? typeDef_[type] : nullptr; }
|
||||||
|
int blast_type(const ProjDef& d) { return type_for(d); }
|
||||||
|
int variant_type(const ProjDef& d, ProjectileSim* sim) { return type_for(d, sim); } // the type of a projectile the scripts changed (a definition of its twin's components) // the projectile type of an explosion that the GPU spawns (burning cells)
|
||||||
|
|
||||||
|
public:
|
||||||
|
int boom_index(const ExplosionFx& fx) { return boom_for(fx); } // (the host's sprite particles: main.cpp)
|
||||||
|
private:
|
||||||
|
int type_for(const ProjDef& d, ProjectileSim* sim = nullptr);
|
||||||
|
int boom_for(const ExplosionFx& fx);
|
||||||
|
int sprite_words(const uint32_t* texels, int count); // appends to the sprite table, returns the offset or -1
|
||||||
|
int add_node(ProjectileSim& sim, const Shot& s, double child_mult = 1.0);
|
||||||
|
void fill_node(ProjectileSim& sim, const Shot& s, int node, int first, double child_mult = 1.0); // child_mult: the parent's child_speed_multiplier (a trigger payload)
|
||||||
|
bool request(int frame, double x, double y, double dx, double dy, int node, int flags, int shooter = 0, int herd = 0);
|
||||||
|
|
||||||
|
uint32_t* tables_ = nullptr;
|
||||||
|
uint32_t* tex_ = nullptr;
|
||||||
|
float* hostF_ = nullptr;
|
||||||
|
int32_t* hostI_ = nullptr;
|
||||||
|
size_t texNext_ = 0;
|
||||||
|
int loads_ = 0;
|
||||||
|
int verlets_ = 0;
|
||||||
|
int types_ = 0, booms_ = 0, emitters_ = 0, nodeNext_ = 0, specNext_ = 0, persistNode_ = 0, persistSpec_ = 0;
|
||||||
|
std::map<const ProjDef*, int> typeOf_;
|
||||||
|
std::vector<const ProjDef*> typeDef_; // the definition behind every projectile type (stable: owned by the ProjectileSim map)
|
||||||
|
std::vector<std::vector<ProjDef::HitEffect>> typeEffects_; // the hit effects of every projectile type
|
||||||
|
std::map<std::string, int> boomOf_;
|
||||||
|
std::map<const void*, int> texOf_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
#include <cctype>
|
||||||
|
#include "gun/gun.h"
|
||||||
|
|
||||||
|
#include <lua.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <regex>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
GunRuntime& G(LuaHost& h) { return *static_cast<GunRuntime*>(h.user); }
|
||||||
|
double nn(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : def; }
|
||||||
|
std::string ss(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; }
|
||||||
|
|
||||||
|
// ---- the engine side of gun.lua: recording the shot ----
|
||||||
|
|
||||||
|
int n_BeginProjectile(LuaHost& h, lua_State* L) {
|
||||||
|
GunRuntime& g = G(h);
|
||||||
|
if (g.shot_stack.empty()) return 0;
|
||||||
|
ShotProjectile p;
|
||||||
|
p.file = ss(L, 1);
|
||||||
|
Shot* top = g.shot_stack.back();
|
||||||
|
top->projectiles.push_back(std::move(p));
|
||||||
|
g.proj_stack.push_back(&top->projectiles.back());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_EndProjectile(LuaHost& h, lua_State*) {
|
||||||
|
GunRuntime& g = G(h);
|
||||||
|
if (!g.proj_stack.empty()) g.proj_stack.pop_back();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int begin_trigger(LuaHost& h, ShotTrigger::Type t, int delay) {
|
||||||
|
GunRuntime& g = G(h);
|
||||||
|
if (g.proj_stack.empty()) return 0;
|
||||||
|
ShotTrigger tr;
|
||||||
|
tr.type = t;
|
||||||
|
tr.delay = delay;
|
||||||
|
tr.shot = std::make_unique<Shot>();
|
||||||
|
ShotProjectile* p = g.proj_stack.back();
|
||||||
|
p->triggers.push_back(std::move(tr));
|
||||||
|
g.shot_stack.push_back(p->triggers.back().shot.get());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_BeginTriggerTimer(LuaHost& h, lua_State* L) { return begin_trigger(h, ShotTrigger::Timer, (int)nn(L, 1)); }
|
||||||
|
int n_BeginTriggerHitWorld(LuaHost& h, lua_State*) { return begin_trigger(h, ShotTrigger::HitWorld, 0); }
|
||||||
|
int n_BeginTriggerDeath(LuaHost& h, lua_State*) { return begin_trigger(h, ShotTrigger::Death, 0); }
|
||||||
|
int n_EndTrigger(LuaHost& h, lua_State*) {
|
||||||
|
GunRuntime& g = G(h);
|
||||||
|
if (g.shot_stack.size() > 1) g.shot_stack.pop_back();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// RegisterGunAction(field1, field2, ...): the finished modifier state of the shot being built
|
||||||
|
int n_RegisterGunAction(LuaHost& h, lua_State* L) {
|
||||||
|
GunRuntime& g = G(h);
|
||||||
|
if (g.shot_stack.empty()) return 0;
|
||||||
|
ShotState& st = g.shot_stack.back()->state;
|
||||||
|
const int n = lua_gettop(L);
|
||||||
|
for (int i = 1; i <= n && i <= (int)g.action_fields.size(); ++i) {
|
||||||
|
const std::string& name = g.action_fields[i - 1];
|
||||||
|
if (lua_isboolean(L, i)) st.num[name] = lua_toboolean(L, i) ? 1.0 : 0.0;
|
||||||
|
else if (lua_type(L, i) == LUA_TNUMBER) st.num[name] = lua_tonumber(L, i);
|
||||||
|
else if (lua_isstring(L, i)) st.str[name] = lua_tostring(L, i);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_RegisterGunShotEffects(LuaHost& h, lua_State* L) { if (G(h).current) G(h).current->recoil = nn(L, 1); return 0; }
|
||||||
|
int n_StartReload(LuaHost& h, lua_State* L) { if (G(h).current) G(h).current->reload_frames = std::max(1, (int)nn(L, 1)); return 0; }
|
||||||
|
int n_OnActionPlayed(LuaHost& h, lua_State* L) { if (G(h).current) G(h).current->played.push_back(ss(L, 1)); return 0; }
|
||||||
|
int n_NgActionEvaluated(LuaHost& h, lua_State* L) { if (G(h).current) G(h).current->evaluated.push_back(ss(L, 1)); return 0; }
|
||||||
|
int n_OnNotEnoughMana(LuaHost& h, lua_State*) { if (G(h).current) G(h).current->not_enough_mana = true; return 0; }
|
||||||
|
int n_ActionUsesRemainingChanged(LuaHost& h, lua_State* L) { G(h).set_card_uses((int)nn(L, 1), (int)nn(L, 2)); if ((int)nn(L, 2) == 0 && G(h).current) G(h).current->action_consumed = true; lua_pushboolean(L, 1); return 1; }
|
||||||
|
int n_Noop(LuaHost&, lua_State*) { return 0; }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int ShotResult::projectile_count() const {
|
||||||
|
int n = 0;
|
||||||
|
std::vector<const Shot*> st{&root};
|
||||||
|
while (!st.empty()) {
|
||||||
|
const Shot* s = st.back(); st.pop_back();
|
||||||
|
n += (int)s->projectiles.size();
|
||||||
|
for (auto& p : s->projectiles) for (auto& t : p.triggers) if (t.shot) st.push_back(t.shot.get());
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
GunRuntime::GunRuntime(const Wak& wak, EntityRuntime& rt) : rt_(rt) {
|
||||||
|
host_ = std::make_unique<LuaHost>(wak, rng_);
|
||||||
|
host_->world = &rt_;
|
||||||
|
host_->user = this;
|
||||||
|
// names of the RegisterGunAction arguments, in order, from the game's generated reflection file
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (wak.read("data/scripts/gun/gunaction_generated.lua", raw)) {
|
||||||
|
const std::string text((const char*)raw.data(), raw.size());
|
||||||
|
std::smatch m;
|
||||||
|
if (std::regex_search(text, m, std::regex(R"(function ConfigGunActionInfo_ReadToLua\(([^)]*)\))"))) {
|
||||||
|
std::stringstream ss2(m[1].str());
|
||||||
|
std::string tok;
|
||||||
|
while (std::getline(ss2, tok, ',')) {
|
||||||
|
tok.erase(std::remove_if(tok.begin(), tok.end(), [](unsigned char c) { return std::isspace(c); }), tok.end());
|
||||||
|
if (!tok.empty()) action_fields.push_back(tok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct { const char* name; NativeFn fn; } natives[] = {
|
||||||
|
{"BeginProjectile", n_BeginProjectile}, {"EndProjectile", n_EndProjectile},
|
||||||
|
{"BeginTriggerTimer", n_BeginTriggerTimer}, {"BeginTriggerHitWorld", n_BeginTriggerHitWorld}, {"BeginTriggerDeath", n_BeginTriggerDeath},
|
||||||
|
{"EndTrigger", n_EndTrigger}, {"RegisterGunAction", n_RegisterGunAction}, {"RegisterGunShotEffects", n_RegisterGunShotEffects},
|
||||||
|
{"RegisterGun", n_Noop}, {"StartReload", n_StartReload}, {"OnActionPlayed", n_OnActionPlayed}, {"NgActionEvaluated", n_NgActionEvaluated},
|
||||||
|
{"OnNotEnoughManaForAction", n_OnNotEnoughMana}, {"ActionUsesRemainingChanged", n_ActionUsesRemainingChanged},
|
||||||
|
{"ActionUsed", n_Noop}, {"LogAction", n_Noop}, {"SetProjectileConfigs", n_Noop}, {"Reflection_RegisterProjectile", n_Noop}, {"RegisterProjectile", n_Noop}, {"BaabInstruction", n_Noop},
|
||||||
|
};
|
||||||
|
for (auto& n : natives) host_->register_native(n.name, n.fn);
|
||||||
|
std::string err;
|
||||||
|
if (action_fields.empty()) { load_error_ = "cannot read the RegisterGunAction field list"; return; }
|
||||||
|
if (!host_->run_file("data/scripts/gun/gun.lua", &err)) { load_error_ = err; return; }
|
||||||
|
{ // every action function reports itself when it runs (ShotResult::evaluated), the calls one action makes of another included; the cards are clones of these
|
||||||
|
const char* wrap = "for _, a in ipairs(actions) do local f, id = a.action, a.id; if type(f) == 'function' then a.action = function(...) NgActionEvaluated(id); return f(...) end end end";
|
||||||
|
if (!host_->run_string(wrap, "evaluated", &err)) { load_error_ = err; return; }
|
||||||
|
}
|
||||||
|
// (gun.lua's own order_deck() runs: it shuffles a shuffling wand with SetRandomSeed(GameGetFrameNum()) and sorts the other wands' deck back into card order by
|
||||||
|
// deck_index; an earlier override only shuffled, so a non-shuffling wand kept the order of its discard pile after a reload - tools/wand_compare.py)
|
||||||
|
loaded_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GunRuntime::ability(int wand) {
|
||||||
|
auto v = rt_.components(wand, "AbilityComponent", "", true);
|
||||||
|
return v.empty() ? 0 : v.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
double GunRuntime::abil(int wand, const char* key, double def) {
|
||||||
|
const int c = ability(wand);
|
||||||
|
if (!c) return def;
|
||||||
|
LVal v = rt_.get_value(c, key, false);
|
||||||
|
return v.t == LVal::T::Str && !v.s.empty() ? std::atof(v.s.c_str()) : def;
|
||||||
|
}
|
||||||
|
|
||||||
|
double GunRuntime::mana() const { return const_cast<GunRuntime*>(this)->abil(wand_, "mana", 0); }
|
||||||
|
double GunRuntime::mana_max() const { return const_cast<GunRuntime*>(this)->abil(wand_, "mana_max", 0); }
|
||||||
|
|
||||||
|
std::vector<GunRuntime::CardRef> GunRuntime::cards(int wand) {
|
||||||
|
std::vector<CardRef> r;
|
||||||
|
std::vector<int> kids = rt_.children(wand);
|
||||||
|
auto slot_of = [&](int k) { // the order of the spells is the order of their slots (ItemComponent.inventory_slot.x), empty slots do not count
|
||||||
|
for (int cid : rt_.components(k, "ItemComponent", "", true)) { const std::string& s = rt_.comp(cid)->attrs["inventory_slot.x"]; if (!s.empty()) return std::atoi(s.c_str()); }
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
std::sort(kids.begin(), kids.end(), [&](int a, int b) { const int sa = slot_of(a), sb = slot_of(b); return sa != sb ? sa < sb : a < b; });
|
||||||
|
for (int k : kids) {
|
||||||
|
auto c = rt_.components(k, "ItemActionComponent", "", true);
|
||||||
|
if (c.empty()) continue;
|
||||||
|
CardRef cr;
|
||||||
|
cr.entity = k;
|
||||||
|
LVal a = rt_.get_value(c.front(), "action_id", false);
|
||||||
|
cr.action = a.t == LVal::T::Str ? a.s : "";
|
||||||
|
if (const ActionInfo* ai = action(cr.action)) cr.action = ai->id; // (the deck holds the action's own id: gun.lua compares them as they are)
|
||||||
|
LVal p = rt_.get_value(c.front(), "permanently_attached", false);
|
||||||
|
cr.permanent = p.t == LVal::T::Str && p.s == "1";
|
||||||
|
for (int ic : rt_.components(k, "ItemComponent", "", true)) cr.permanent |= rt_.comp(ic)->attrs["permanently_attached"] == "1";
|
||||||
|
if (!cr.action.empty()) r.push_back(cr);
|
||||||
|
}
|
||||||
|
// always-cast cards first, like the engine plays them
|
||||||
|
std::stable_sort(r.begin(), r.end(), [](const CardRef& a, const CardRef& b) { return a.permanent > b.permanent; });
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GunRuntime::equip(int wand) {
|
||||||
|
if (!loaded_ || !rt_.alive(wand) || !ability(wand)) return false;
|
||||||
|
auto num = [&](const char* k, double d) { return abil(wand, k, d); };
|
||||||
|
std::ostringstream lua;
|
||||||
|
lua << "gun = {}; ConfigGun_Init(gun)\n"
|
||||||
|
<< "gun.actions_per_round = " << (int)num("gun_config.actions_per_round", 1) << "\n"
|
||||||
|
<< "gun.shuffle_deck_when_empty = " << (num("gun_config.shuffle_deck_when_empty", 0) != 0 ? "true" : "false") << "\n"
|
||||||
|
<< "gun.reload_time = " << (int)num("gun_config.reload_time", 40) << "\n"
|
||||||
|
<< "gun.deck_capacity = " << (int)num("gun_config.deck_capacity", 2) << "\n"
|
||||||
|
<< "state_from_game = {}; ConfigGunActionInfo_Init(state_from_game)\n"
|
||||||
|
<< "state_from_game.fire_rate_wait = " << (int)num("gunaction_config.fire_rate_wait", 0) << "\n"
|
||||||
|
<< "state_from_game.spread_degrees = " << num("gunaction_config.spread_degrees", 0) << "\n"
|
||||||
|
<< "state_from_game.speed_multiplier = " << num("gunaction_config.speed_multiplier", 1) << "\n"
|
||||||
|
<< "_clear_deck(false)\n";
|
||||||
|
for (const CardRef& c : cards(wand))
|
||||||
|
if (!c.permanent) lua << "_add_card_to_deck(\"" << c.action << "\", " << c.entity << ", " << card_uses(c.entity) << ", true)\n";
|
||||||
|
std::string err;
|
||||||
|
if (!host_->run_string(lua.str(), "equip", &err)) { std::fprintf(stderr, "gun equip: %s\n", err.c_str()); return false; }
|
||||||
|
wand_ = wand;
|
||||||
|
ready_frame_ = 0;
|
||||||
|
reload_until_ = 0;
|
||||||
|
last_regen_frame_ = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GunRuntime::fire(int frame, ShotResult& out) {
|
||||||
|
if (!loaded_ || !wand_ || !rt_.alive(wand_)) return false;
|
||||||
|
if (frame < ready_frame_ || frame < reload_until_) return false;
|
||||||
|
const int comp = ability(wand_);
|
||||||
|
// (the mana charges every frame in the host's AbilityComponent update: mana_charge_speed per second up to mana_max, every wand, held or not)
|
||||||
|
double mana = abil(wand_, "mana", 0);
|
||||||
|
last_regen_frame_ = frame;
|
||||||
|
|
||||||
|
out = ShotResult{};
|
||||||
|
out.cards = (int)cards(wand_).size();
|
||||||
|
current = &out;
|
||||||
|
shot_stack.assign(1, &out.root);
|
||||||
|
proj_stack.clear();
|
||||||
|
std::string err;
|
||||||
|
// TODO(to-do #2): Cessation: GetUpdatedEntityID() must be the shooter here; mCessationDo -> polymorph native is missing
|
||||||
|
bool ok = host_->call_function("_start_shot", {LVal::number(mana)}, &err);
|
||||||
|
if (ok) for (const CardRef& c : cards(wand_)) if (c.permanent) host_->call_function("_play_permanent_card", {LVal::string(c.action)}, &err);
|
||||||
|
if (ok) ok = host_->call_function("_draw_actions_for_shot", {LVal::boolean(true)}, &err);
|
||||||
|
if (ok) ok = host_->call_function("_handle_reload", {}, &err); // 00b2a310 calls it again after the draw (00b2a120): the draw clears `reloading` at its end, so a deck an over-draw left empty is refilled and the reload starts here
|
||||||
|
current = nullptr;
|
||||||
|
shot_stack.clear();
|
||||||
|
proj_stack.clear();
|
||||||
|
if (!ok) { out.error = err; return true; }
|
||||||
|
lua_State* L = host_->state();
|
||||||
|
lua_getglobal(L, "mana");
|
||||||
|
out.mana = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : mana;
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (comp) rt_.set_value(comp, "mana", LVal::number(out.mana));
|
||||||
|
const int cast_delay = (int)out.root.state.get("fire_rate_wait", 0);
|
||||||
|
ready_frame_ = frame + std::max(1, cast_delay);
|
||||||
|
if (out.reload_frames > 0) { reload_until_ = frame + out.reload_frames; ready_frame_ = std::max(ready_frame_, reload_until_); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GunRuntime::card_uses(int card) {
|
||||||
|
for (int cid : rt_.components(card, "ItemActionComponent", "", true)) {
|
||||||
|
const ActionInfo* ai = action(rt_.comp(cid)->attrs["action_id"]);
|
||||||
|
bool unlimited_perk = false;
|
||||||
|
for (int wc : rt_.components(rt_.world_state_entity(), "WorldStateComponent", "", true)) unlimited_perk |= rt_.comp(wc)->attrs["perk_infinite_spells"] == "1";
|
||||||
|
if (unlimited_perk && ai && !ai->never_unlimited) return -1;
|
||||||
|
for (int ic : rt_.components(card, "ItemComponent", "", true)) { const std::string& u = rt_.comp(ic)->attrs["uses_remaining"]; if (!u.empty()) return std::atoi(u.c_str()); }
|
||||||
|
return ai ? ai->max_uses : -1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GunRuntime::set_card_uses(int card, int uses) {
|
||||||
|
for (int ic : rt_.components(card, "ItemComponent", "", true)) rt_.comp(ic)->attrs["uses_remaining"] = std::to_string(uses);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GunRuntime::ActionInfo* GunRuntime::action(const std::string& id) {
|
||||||
|
if (!actions_cached_ && loaded_) {
|
||||||
|
actions_cached_ = true;
|
||||||
|
lua_State* L = host_->state();
|
||||||
|
lua_getglobal(L, "actions");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
const size_t n = lua_objlen(L, -1);
|
||||||
|
for (size_t i = 1; i <= n; ++i) {
|
||||||
|
lua_rawgeti(L, -1, (int)i);
|
||||||
|
ActionInfo a;
|
||||||
|
lua_getfield(L, -1, "id"); a.id = ss(L, -1); lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "name"); a.name = ss(L, -1); lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "sprite"); a.sprite = ss(L, -1); lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "type"); a.type = (int)nn(L, -1); lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "description"); a.description = ss(L, -1); lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "mana"); a.mana = nn(L, -1); lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "max_uses"); a.max_uses = lua_isnumber(L, -1) ? (int)nn(L, -1) : -1; lua_pop(L, 1);
|
||||||
|
lua_getfield(L, -1, "never_unlimited"); a.never_unlimited = lua_toboolean(L, -1) != 0; lua_pop(L, 1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
actions_[a.id] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
auto it = actions_.find(id);
|
||||||
|
if (it == actions_.end()) { // (the engine finds an action whatever the case: make_random_card gives it string.lower(id), the hourglass chamber's cards)
|
||||||
|
std::string up = id;
|
||||||
|
for (char& ch : up) ch = (char)std::toupper((unsigned char)ch);
|
||||||
|
it = actions_.find(up);
|
||||||
|
}
|
||||||
|
return it == actions_.end() ? nullptr : &it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
// The spell engine: runs the game's own data/scripts/gun/gun.lua (draw order, multicast, modifiers, triggers, mana, reload) in a
|
||||||
|
// dedicated Lua state and records the "shot" it produces, the way the engine's C++ side does (BeginProjectile / EndProjectile /
|
||||||
|
// BeginTrigger* / RegisterGunAction ...). Turning a shot into flying projectiles is up to the caller (gun/projectiles.h).
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/random.h"
|
||||||
|
#include "entity/runtime.h"
|
||||||
|
#include "lua/lua_host.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
struct ShotState { // the modifiers c.* a shot ended with (ConfigGunActionInfo)
|
||||||
|
std::map<std::string, double> num;
|
||||||
|
std::map<std::string, std::string> str;
|
||||||
|
double get(const char* k, double def = 0) const { auto it = num.find(k); return it == num.end() ? def : it->second; }
|
||||||
|
std::string gets(const char* k) const { auto it = str.find(k); return it == str.end() ? std::string() : it->second; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Shot;
|
||||||
|
|
||||||
|
struct ShotTrigger {
|
||||||
|
enum Type { Timer = 1, HitWorld = 2, Death = 3 } type = Timer;
|
||||||
|
int delay = 0; // frames, timer triggers
|
||||||
|
std::unique_ptr<Shot> shot; // what is cast when the trigger fires
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShotProjectile {
|
||||||
|
std::string file; // projectile entity xml
|
||||||
|
std::string def_key; // a definition of its own (the changed copy a script_shot script made), used instead of file + extra entities
|
||||||
|
std::vector<ShotTrigger> triggers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Shot {
|
||||||
|
ShotState state;
|
||||||
|
std::vector<ShotProjectile> projectiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShotResult {
|
||||||
|
Shot root;
|
||||||
|
double mana = 0; // mana left after the shot
|
||||||
|
int reload_frames = 0; // > 0: the deck ran out, a reload was started
|
||||||
|
double recoil = 0;
|
||||||
|
bool not_enough_mana = false;
|
||||||
|
bool action_consumed = false; // ActionUsesRemainingChanged(.., 0) during the shot (00b28930)
|
||||||
|
int cards = 0; // the wand's cards loaded for the shot (00b29cd0; 0: casting an empty wand)
|
||||||
|
std::vector<std::string> played; // action ids in play order (drawn and played from the hand)
|
||||||
|
std::vector<std::string> evaluated; // every action function call in order, those another action makes too (Greek letters, divide, duplicate): the spells as they are evaluated
|
||||||
|
std::string error;
|
||||||
|
int projectile_count() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GunRuntime {
|
||||||
|
public:
|
||||||
|
GunRuntime(const Wak& wak, EntityRuntime& rt);
|
||||||
|
bool ok() const { return loaded_; }
|
||||||
|
void set_seed(uint32_t s) { rng_.set_world_seed(s); }
|
||||||
|
const std::string& load_error() const { return load_error_; }
|
||||||
|
|
||||||
|
// Loads the wand (a runtime entity with an AbilityComponent and card children) into the gun script. Resets its deck state.
|
||||||
|
bool equip(int wand);
|
||||||
|
int equipped() const { return wand_; }
|
||||||
|
|
||||||
|
// One trigger pull on the equipped wand at game frame `frame`: checks the cast delay, regenerates mana, runs the script.
|
||||||
|
// Returns false (and leaves `out` empty) when the wand is not ready.
|
||||||
|
bool fire(int frame, ShotResult& out);
|
||||||
|
|
||||||
|
// Per-frame mana regeneration for the equipped wand is applied lazily inside fire(); this reads the current values.
|
||||||
|
double mana() const;
|
||||||
|
double mana_max() const;
|
||||||
|
int ready_in(int frame) const { return std::max(0, ready_frame_ - frame); }
|
||||||
|
bool reloading(int frame) const { return frame < reload_until_; }
|
||||||
|
int reload_left(int frame) const { return std::max(0, reload_until_ - frame); }
|
||||||
|
// FROZEN_SPEED_UP (00b015d0 case 0x57): one more frame off the held wand's next-usable / reload frames every frame (the timers run twice as fast)
|
||||||
|
void hasten(int frame) { if (ready_frame_ > frame) --ready_frame_; if (reload_until_ > frame) --reload_until_; }
|
||||||
|
|
||||||
|
struct ActionInfo { std::string id, name, description, sprite; int type = 0; double mana = 0; int max_uses = -1; bool never_unlimited = false; };
|
||||||
|
const ActionInfo* action(const std::string& id);
|
||||||
|
std::vector<const ActionInfo*> all_actions() { action(""); std::vector<const ActionInfo*> v; for (const auto& kv : actions_) v.push_back(&kv.second); return v; } // (by id)
|
||||||
|
// Spell ids of a wand, in deck order (permanent ones flagged).
|
||||||
|
struct CardRef { int entity = 0; std::string action; bool permanent = false; };
|
||||||
|
std::vector<CardRef> cards(int wand);
|
||||||
|
// The uses a spell card has left (-1: unlimited): ItemComponent.uses_remaining, initially the action's max_uses; the perk that makes spells unlimited (WorldStateComponent.perk_infinite_spells)
|
||||||
|
// sets every action that is not in the exclusion set (action_never_unlimited) to -1 (00b83cb0, 00b83b00).
|
||||||
|
int card_uses(int card);
|
||||||
|
void set_card_uses(int card, int uses);
|
||||||
|
|
||||||
|
// ---- called by the natives (public for the free functions in gun.cpp) ----
|
||||||
|
ShotResult* current = nullptr;
|
||||||
|
std::vector<Shot*> shot_stack;
|
||||||
|
std::vector<ShotProjectile*> proj_stack;
|
||||||
|
std::vector<std::string> action_fields; // order of the RegisterGunAction arguments
|
||||||
|
|
||||||
|
private:
|
||||||
|
int ability(int wand);
|
||||||
|
double abil(int wand, const char* key, double def);
|
||||||
|
EntityRuntime& rt_;
|
||||||
|
GameRandom rng_;
|
||||||
|
std::unique_ptr<LuaHost> host_;
|
||||||
|
bool loaded_ = false;
|
||||||
|
std::string load_error_;
|
||||||
|
int wand_ = 0, ready_frame_ = 0, reload_until_ = 0, last_regen_frame_ = 0;
|
||||||
|
std::map<std::string, ActionInfo> actions_;
|
||||||
|
bool actions_cached_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
#include "gun/projectiles.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
float attr(const EntityComponent* c, const char* k, float d) {
|
||||||
|
if (!c) return d;
|
||||||
|
auto it = c->attrs.find(k);
|
||||||
|
return it == c->attrs.end() ? d : (float)std::atof(it->second.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityComponent* first(const EntityDef& d, const char* type) {
|
||||||
|
auto v = d.all(type);
|
||||||
|
return v.empty() ? nullptr : v.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr double kDeg = 3.14159265358979323846 / 180.0;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const ProjDef* ProjectileSim::def(const std::string& file) {
|
||||||
|
auto it = defs_.find(file);
|
||||||
|
if (it != defs_.end()) return &it->second;
|
||||||
|
if (const EntityDef* e = db_.get(file)) return def_from(file, *e);
|
||||||
|
return &(defs_[file] = ProjDef());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::disable_gpu_lua() {
|
||||||
|
gpu_lua_enabled = false;
|
||||||
|
for (auto& kv : defs_) if (kv.second.gpu_lua_every > 0) {
|
||||||
|
kv.second.gpu_lua_every = 0;
|
||||||
|
kv.second.needs_twin = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entity has components the GPU does not simulate (CellEater, MagicConvertMaterial, area effects, scripts...): the host keeps a runtime twin of the projectile
|
||||||
|
static bool host_runs(const EntityDef& e, bool gpu_script = false) {
|
||||||
|
for (const char* rich : {"CellEaterComponent", "MagicConvertMaterialComponent", "GameAreaEffectComponent", "AreaDamageComponent", "LightningComponent", "LaserEmitterComponent", "MaterialSeaSpawnerComponent", "TeleportProjectileComponent", "EnergyShieldComponent", "ExplosionComponent", "LooseGroundComponent", "ArcComponent", "LightningComponent", "BlackHoleComponent", "MaterialSuckerComponent"})
|
||||||
|
if (!e.all(rich).empty()) return true;
|
||||||
|
if (!gpu_script) for (const EntityComponent* lc : e.all("LuaComponent")) for (const auto& kv : lc->attrs) if (kv.first.rfind("script_", 0) == 0 && !kv.second.empty()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gpu_script_contract(const EntityDef& e) {
|
||||||
|
if (!e.children.empty()) return false;
|
||||||
|
// Conservative: unknown components may require spawn/death/host effects.
|
||||||
|
for (const auto& c : e.comps)
|
||||||
|
if (c.type != "LuaComponent" && c.type != "ProjectileComponent" && c.type != "VelocityComponent" &&
|
||||||
|
c.type != "SpriteComponent" && c.type != "ParticleEmitterComponent" && c.type != "AudioComponent" &&
|
||||||
|
c.type != "LightComponent" && c.type != "VariableStorageComponent") return false;
|
||||||
|
const auto scripts = e.all("LuaComponent"), projectiles = e.all("ProjectileComponent"), velocities = e.all("VelocityComponent");
|
||||||
|
if (scripts.size() != 1 || projectiles.size() != 1 || velocities.size() != 1) return false;
|
||||||
|
const auto* lc = scripts.front();
|
||||||
|
if (lc->get("_enabled", "1") == "0" || projectiles.front()->get("_enabled", "1") == "0" || velocities.front()->get("_enabled", "1") == "0") return false;
|
||||||
|
if (lc->get("script_source_file") != "data/scripts/projectiles/grenade_anti_gravity.lua" || lc->get("execute_every_n_frame", "1") != "2") return false;
|
||||||
|
for (const char* key : {"execute_on_added", "execute_on_removed", "execute_times", "remove_after_executed", "enable_coroutines", "call_init_function"})
|
||||||
|
if (lc->get(key, "0") != "0") return false;
|
||||||
|
for (const auto& kv : lc->attrs) if (kv.first.rfind("script_", 0) == 0 && kv.first != "script_source_file" && !kv.second.empty()) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gpu_extra_contract(const EntityDef& e) {
|
||||||
|
if (!e.children.empty()) return false;
|
||||||
|
for (const auto& c : e.comps)
|
||||||
|
if (c.type != "HomingComponent" && c.type != "ParticleEmitterComponent" && c.type != "LightComponent" && c.type != "HitEffectComponent" && c.type != "VariableStorageComponent") return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_homing(const EntityDef& e, ProjDef& d) {
|
||||||
|
for (const EntityComponent* hc : e.all("HomingComponent")) {
|
||||||
|
if (hc->get("_enabled", "1") == "0" || d.homing.on) continue;
|
||||||
|
auto f = [&](const char* k, float v) { auto it = hc->attrs.find(k); return it == hc->attrs.end() ? v : (float)std::atof(it->second.c_str()); };
|
||||||
|
d.homing.on = true;
|
||||||
|
d.homing.detect = f("detect_distance", 150); d.homing.mult = f("homing_velocity_multiplier", 0.9f); d.homing.coeff = f("homing_targeting_coeff", 160); d.homing.max_turn = f("max_turn_rate", 0.05f);
|
||||||
|
d.homing.rotate = f("just_rotate_towards_target", 0) != 0; d.homing.who_shot = f("target_who_shot", 0) != 0;
|
||||||
|
d.homing.target = (int)f("predefined_target", 0);
|
||||||
|
const std::string tag = hc->get("target_tag", "homing_target");
|
||||||
|
d.homing.tag_bits = tag == "mortal" ? 512 : tag == "prey" ? 1024 : tag == "homing_target" ? 2048 : tag == "player_unit" ? 4096 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjDef* ProjectileSim::def_from(const std::string& file, const EntityDef& entity, const std::string& extras) {
|
||||||
|
auto it0 = defs_.find(file);
|
||||||
|
if (it0 != defs_.end()) return &it0->second;
|
||||||
|
ProjDef d;
|
||||||
|
d.file = entity.path;
|
||||||
|
// Match EntityRuntime's CSV handling, including spaces around a tag.
|
||||||
|
std::istringstream tags(entity.tags);std::string tag;
|
||||||
|
while(std::getline(tags,tag,',')) {
|
||||||
|
auto begin=tag.find_first_not_of(' '), end=tag.find_last_not_of(' ');
|
||||||
|
if(begin!=std::string::npos&&tag.substr(begin,end-begin+1)=="projectile")d.query_projectile=true;
|
||||||
|
}
|
||||||
|
unsigned velocity_count=0;
|
||||||
|
for(const auto* component:entity.all("VelocityComponent"))if(component->get("_enabled","1")!="0")++velocity_count;
|
||||||
|
d.query_velocity = velocity_count == 1;
|
||||||
|
d.query_unsupported = d.query_projectile && velocity_count > 1;
|
||||||
|
{
|
||||||
|
const EntityDef* e = &entity;
|
||||||
|
const EntityComponent* pc = first(*e, "ProjectileComponent");
|
||||||
|
// A thrown item that breaks (egg_monster.xml): PhysicsBodyCollisionDamageComponent (00be12a0) turns a new contact faster than speed_threshold into speed x damage_multiplier
|
||||||
|
// of damage, the DamageModel dies, ExplodeOnDamageComponent (00ae35d0, explode_on_death_percent) explodes with ITS config_explosion (load_this_entity: the hatchling).
|
||||||
|
// One impact decides it here (the hp is not kept between impacts): the break speed is max(speed_threshold, hp / damage_multiplier)
|
||||||
|
EntityComponent breakPc;
|
||||||
|
if (pc && !e->all("PhysicsBodyComponent").empty()) {
|
||||||
|
const EntityComponent* cd = first(*e, "PhysicsBodyCollisionDamageComponent");
|
||||||
|
const EntityComponent* dm = first(*e, "DamageModelComponent");
|
||||||
|
const EntityComponent* eod = first(*e, "ExplodeOnDamageComponent");
|
||||||
|
if (cd && dm && eod && attr(eod, "explode_on_death_percent", 1) > 0) {
|
||||||
|
const float mult = attr(cd, "damage_multiplier", 0.016667f), hp = attr(dm, "hp", 1);
|
||||||
|
d.break_speed = std::max(attr(cd, "speed_threshold", 60), mult > 0 ? hp / mult : 1e9f);
|
||||||
|
breakPc = *pc;
|
||||||
|
for (auto& [k, v] : eod->attrs) if (k.compare(0, 17, "config_explosion.") == 0) breakPc.attrs[k] = v;
|
||||||
|
breakPc.attrs["on_death_explode"] = "1";
|
||||||
|
pc = &breakPc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const EntityComponent* vc = first(*e, "VelocityComponent");
|
||||||
|
d.speed_min = attr(pc, "speed_min", 300) / 60.0f;
|
||||||
|
d.speed_max = attr(pc, "speed_max", 300) / 60.0f;
|
||||||
|
d.damage = attr(pc, "damage", 0.1f) + attr(pc, "damage_by_type.projectile", 0.0f);
|
||||||
|
{
|
||||||
|
static const char* chan[12] = {"melee", "electricity", "fire", "ice", "slice", "drill", "radioactive", "poison", "healing", "curse", "holy", "explosion"};
|
||||||
|
for (int i = 0; i < 12; ++i) d.dmg_type[i] = attr(pc, (std::string("damage_by_type.") + chan[i]).c_str(), 0.0f);
|
||||||
|
}
|
||||||
|
d.lifetime = (int)attr(pc, "lifetime", 60);
|
||||||
|
d.on_collision_die = attr(pc, "on_collision_die", 1) != 0;
|
||||||
|
d.collide_with_world = attr(pc, "collide_with_world", 1) != 0; d.collide_with_entities = attr(pc, "collide_with_entities", 1) != 0; d.penetrate_entities = attr(pc, "penetrate_entities", 0) != 0;
|
||||||
|
d.damage_every_x_frames = std::max(1, (int)attr(pc, "damage_every_x_frames", 2));
|
||||||
|
d.bounces_left = (int)attr(pc, "bounces_left", 0);
|
||||||
|
d.die_on_liquid = attr(pc, "die_on_liquid_collision", 0) != 0;
|
||||||
|
d.displace_liquid = attr(vc, "displace_liquid", 1) != 0; // VelocityComponent, constructor 00776280: on
|
||||||
|
d.invisible = e->all("SpriteComponent").empty(); // (no SpriteComponent: the projectile itself draws nothing - wall_piece.xml is only its green particles and its light)
|
||||||
|
if (!pc) { // a loaded particle entity: SetStartVelocityComponent, LifetimeComponent, DieIfSpeedBelowComponent of its file
|
||||||
|
d.particle = true;
|
||||||
|
d.creature = first(*e, "AnimalAIComponent") || first(*e, "PhysicsAIComponent") || first(*e, "WormAIComponent");
|
||||||
|
if (d.creature) d.needs_twin = true; // (its twin event makes the creature)
|
||||||
|
d.damage = 0;
|
||||||
|
d.on_collision_die = false;
|
||||||
|
if (!first(*e, "SetStartVelocityComponent")) { d.speed_min = d.speed_max = 0; d.gravity = 0; } // a static spell entity (touch_*, fields, seas): it stays where it was cast
|
||||||
|
if (const EntityComponent* lt = first(*e, "LifetimeComponent")) d.lifetime = (int)attr(lt, "lifetime", 60);
|
||||||
|
if (const EntityComponent* ds = first(*e, "DieIfSpeedBelowComponent")) d.die_speed = attr(ds, "min_speed", 0) / 60.0f;
|
||||||
|
}
|
||||||
|
if (pc) { auto lt = pc->attrs.find("config_explosion.load_this_entity"); if (lt != pc->attrs.end()) d.load_files = lt->second; }
|
||||||
|
if (pc) for (int k = (int)attr(pc, "config_explosion.electricity_count", 0); k > 0; --k) d.load_files += (d.load_files.empty() ? "" : ",") + std::string("data/entities/misc/electricity.xml"); // 00657960: electricity_count loads of electricity.xml at the blast // the host spawns these as real entities (runtime) when the explosion happens
|
||||||
|
parse_homing(*e, d);
|
||||||
|
d.gpu_lua_every = gpu_lua_enabled && gpu_script_contract(*e) ? 2 : 0;
|
||||||
|
if (host_runs(*e, d.gpu_lua_every != 0)) d.needs_twin = true;
|
||||||
|
if (pc) { // damage_game_effect_entities: effect entity files loaded on the entity the projectile hits
|
||||||
|
const std::string ge = pc->get("damage_game_effect_entities");
|
||||||
|
for (size_t pos = 0; pos < ge.size();) { size_t end = ge.find(',', pos); if (end == std::string::npos) end = ge.size(); if (end > pos) d.hit_effects.push_back({"LOAD_GAME_EFFECT", ge.substr(pos, end - pos), "", "", 0.0f}); pos = end + 1; }
|
||||||
|
}
|
||||||
|
for (const EntityComponent* he : e->all("HitEffectComponent"))
|
||||||
|
d.hit_effects.push_back({he->get("effect_hit", ""), he->get("value_string", ""), he->get("condition_effect", ""), he->get("condition_status", ""), (float)std::atof(he->get("value", "0").c_str())});
|
||||||
|
d.friendly_fire = attr(pc, "friendly_fire", 0) != 0;
|
||||||
|
d.dont_hit_player = attr(pc, "dont_hit_player", 0) != 0;
|
||||||
|
d.collide_with_shooter_frames = (int)attr(pc, "collide_with_shooter_frames", -1); // (-1: never hits its shooter)
|
||||||
|
d.physics = !e->all("PhysicsBodyComponent").empty();
|
||||||
|
if (d.physics) d.on_collision_die = false;
|
||||||
|
d.explode_on_death = attr(pc, "on_death_explode", 0) != 0 || attr(pc, "on_lifetime_out_explode", 0) != 0;
|
||||||
|
d.explosion_radius = attr(pc, "config_explosion.explosion_radius", 20); // (the ConfigExplosion constructor 004b2780: radius 20, damage 5, hole_enabled on)
|
||||||
|
d.explosion_damage = attr(pc, "config_explosion.damage", 5);
|
||||||
|
if (!std::isfinite(d.explosion_radius) || d.explosion_radius < 0) d.explosion_radius = 0; // (a script's -inf / NaN: spells_to_power.lua's log(0); the game can crash on it)
|
||||||
|
if (!std::isfinite(d.explosion_damage)) d.explosion_damage = 0;
|
||||||
|
d.hole = attr(pc, "config_explosion.hole_enabled", 1) != 0;
|
||||||
|
if (pc) { auto it = pc->attrs.find("config_explosion.create_cell_material"); d.explosion_material = it != pc->attrs.end() ? it->second : "fire"; } // ConfigExplosion constructor 004b2780: create_cell_material "fire", create_cell_probability 5
|
||||||
|
d.explosion_cell_probability = attr(pc, "config_explosion.create_cell_probability", 5);
|
||||||
|
d.max_durability = attr(pc, "config_explosion.max_durability_to_destroy", 10);
|
||||||
|
d.stains = attr(pc, "config_explosion.stains_enabled", 1) != 0; d.stains_radius = attr(pc, "config_explosion.stains_radius", 0);
|
||||||
|
d.physics_min = attr(pc, "config_explosion.physics_explosion_power.min", 0);
|
||||||
|
d.physics_max = attr(pc, "config_explosion.physics_explosion_power.max", 0.2f);
|
||||||
|
d.ragdoll_explosion_mult = attr(pc, "config_explosion.physics_multiplier_ragdoll_force", 1.0f);
|
||||||
|
d.ray_energy = attr(pc, "config_explosion.ray_energy", 20000);
|
||||||
|
d.explosion_audio = attr(pc, "config_explosion.audio_enabled", 1) != 0;
|
||||||
|
if (pc) { auto it = pc->attrs.find("config_explosion.audio_event_name"); if (it != pc->attrs.end()) d.explosion_audio_event = it->second; }
|
||||||
|
d.explosion_audio_liquid = attr(pc, "config_explosion.audio_liquid_amount_normalized", 0);
|
||||||
|
d.knockback_force = attr(pc, "config_explosion.knockback_force", 1.0f);
|
||||||
|
d.damage_mortals = attr(pc, "config_explosion.damage_mortals", 1) != 0;
|
||||||
|
d.dont_damage_shooter = attr(pc, "explosion_dont_damage_shooter", 0) != 0;
|
||||||
|
d.not_scaled_by_gamefx = attr(pc, "config_explosion.not_scaled_by_gamefx", 0) != 0;
|
||||||
|
d.hit_knockback_force = attr(pc, "knockback_force", 0.0f);
|
||||||
|
d.ragdoll_force_multiplier = attr(pc, "ragdoll_force_multiplier", 0.025f);
|
||||||
|
if (pc) { // (the packet's RAGDOLL_FX: 00c091f0 copies ragdoll_fx_on_collision)
|
||||||
|
static const char* fxNames[10] = {"NONE", "NORMAL", "BLOOD_EXPLOSION", "BLOOD_SPRAY", "FROZEN", "CONVERT_TO_MATERIAL", "CUSTOM_RAGDOLL_ENTITY", "DISINTEGRATED", "NO_RAGDOLL_FILE", "PLAYER_RAGDOLL_CAMERA"};
|
||||||
|
const auto it = pc->attrs.find("ragdoll_fx_on_collision");
|
||||||
|
if (it != pc->attrs.end()) for (int k = 0; k < 10; ++k) if (it->second == fxNames[k]) d.ragdoll_fx = k;
|
||||||
|
}
|
||||||
|
d.velocity_mass = attr(vc, "mass", 0.05f); // (VelocityComponent mass, default 0.05: component documentation)
|
||||||
|
if (!d.particle) d.spread_rad = attr(pc, "direction_random_rad", 0);
|
||||||
|
if (pc) { // world collision (00c073b0)
|
||||||
|
d.penetrate_world = attr(pc, "penetrate_world", 0) != 0;
|
||||||
|
d.penetrate_world_velocity_coeff = attr(pc, "penetrate_world_velocity_coeff", 0.6f);
|
||||||
|
d.ground_penetration_coeff = attr(pc, "ground_penetration_coeff", 0);
|
||||||
|
d.ground_penetration_max_durability = (int)attr(pc, "ground_penetration_max_durability_to_destroy", 0);
|
||||||
|
d.bounce_energy = attr(pc, "bounce_energy", 0.5f);
|
||||||
|
d.bounce_always = attr(pc, "bounce_always", 0) != 0;
|
||||||
|
d.bounce_at_any_angle = attr(pc, "bounce_at_any_angle", 0) != 0;
|
||||||
|
d.on_collision_remove_projectile = attr(pc, "on_collision_remove_projectile", 0) != 0;
|
||||||
|
d.die_on_low_velocity = attr(pc, "die_on_low_velocity", 0) != 0;
|
||||||
|
d.die_on_low_velocity_limit = attr(pc, "die_on_low_velocity_limit", 50);
|
||||||
|
d.physics_impulse_coeff = attr(pc, "physics_impulse_coeff", 300);
|
||||||
|
d.damage_scaled_by_speed = attr(pc, "damage_scaled_by_speed", 0) != 0;
|
||||||
|
d.damage_scale_max_speed = attr(pc, "damage_scale_max_speed", 0);
|
||||||
|
d.lifetime_randomness = (int)attr(pc, "lifetime_randomness", 0);
|
||||||
|
d.bounce_fx_file = pc->get("bounce_fx_file", "");
|
||||||
|
if (attr(pc, "on_collision_spawn_entity", 1) != 0) { d.spawn_entity = pc->get("spawn_entity", ""); d.spawn_entity_is_projectile = attr(pc, "spawn_entity_is_projectile", 0) != 0; }
|
||||||
|
{ const std::string pt = pc->get("projectile_type", ""); d.projectile_type = pt == "LIGHTNING" ? 1 : pt == "VERLET" ? 2 : pt == "MATERIAL_PARTICLE" ? 3 : 0; }
|
||||||
|
if (d.projectile_type == 2) { // VERLET: the chain is the child entity with a VerletPhysicsComponent, its point sprites its SpriteComponents; the damage the root's VerletWeaponComponent
|
||||||
|
ProjDef::Verlet& v = d.verlet;
|
||||||
|
for (const EntityComponent* vw : e->all("VerletWeaponComponent")) {
|
||||||
|
v.dmg_radius = attr(vw, "damage_radius", 5); v.dmg_min = attr(vw, "damage_min_step", 0.01f); v.dmg_max = attr(vw, "damage_max", 1); v.dmg_coeff = attr(vw, "damage_coeff", 1);
|
||||||
|
v.impulse = attr(vw, "impulse_coeff", 1); v.fade = (int)attr(vw, "fade_duration_frames", 10);
|
||||||
|
v.force_radius = attr(vw, "physics_force_radius", 3); v.phys_impulse = attr(vw, "physics_impulse_coeff", 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const EntityDef::Child& ch : e->children) {
|
||||||
|
const EntityDef* cd = db_.get(ch.path);
|
||||||
|
if (!cd || cd->all("VerletPhysicsComponent").empty()) continue;
|
||||||
|
const EntityComponent* vp = cd->all("VerletPhysicsComponent").front();
|
||||||
|
v.points = std::clamp((int)attr(vp, "num_points", 2), 2, 16); v.rest = attr(vp, "resting_distance", 2); v.stiff = attr(vp, "stiffness", 1);
|
||||||
|
v.damp = attr(vp, "velocity_dampening", 0.99f); v.liquid_damp = attr(vp, "liquid_damping", 0.7f); v.cells = attr(vp, "collide_with_cells", 1) != 0;
|
||||||
|
for (const EntityComponent* sc : cd->all("SpriteComponent")) {
|
||||||
|
SpriteFrames sf;
|
||||||
|
ProjDef::Verlet::Part part;
|
||||||
|
if (load_sprite_frames(wak_, sc->get("image_file"), sc->get("rect_animation"), sf)) { part.tex.assign(sf.texels.begin(), sf.texels.begin() + (size_t)sf.w * sf.h); part.w = sf.w; part.h = sf.h; }
|
||||||
|
part.ox = attr(sc, "offset_x", 0); part.oy = attr(sc, "offset_y", 0);
|
||||||
|
v.parts.push_back(std::move(part));
|
||||||
|
}
|
||||||
|
v.on = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (attr(pc, "on_death_emit_particle", 0) != 0 || pc->get("projectile_type", "") == "MATERIAL_PARTICLE") { d.death_particle = pc->get("on_death_emit_particle_type", ""); d.death_particle_count = (int)attr(pc, "on_death_emit_particle_count", 1); }
|
||||||
|
}
|
||||||
|
if (vc) {
|
||||||
|
d.gravity = attr(vc, "gravity_y", 400) / 3600.0f;
|
||||||
|
d.air_friction = attr(vc, "air_friction", 0.55f); // constructor 00776280
|
||||||
|
d.liquid_drag = attr(vc, "liquid_drag", 1.0f);
|
||||||
|
d.gravity_x = attr(vc, "gravity_x", 0) / 3600.0f;
|
||||||
|
d.terminal_velocity = attr(vc, "terminal_velocity", 1000);
|
||||||
|
d.apply_terminal_velocity = attr(vc, "apply_terminal_velocity", 1) != 0;
|
||||||
|
} else {
|
||||||
|
d.gravity = 0;
|
||||||
|
}
|
||||||
|
for (const EntityComponent* c : e->all("SpriteComponent")) {
|
||||||
|
if (c->get("_enabled", "1") == "0") continue;
|
||||||
|
SpriteFrames sf;
|
||||||
|
if (load_sprite_frames(wak_, c->get("image_file"), c->get("rect_animation"), sf)) {
|
||||||
|
d.sprite = std::move(sf.texels); d.sprite_w = sf.w; d.sprite_h = sf.h; d.sprite_frames = sf.frames; d.sprite_wait = sf.wait;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (d.sprite.empty()) { // physics projectiles (bombs) are drawn from their physics image
|
||||||
|
for (const EntityComponent* c : e->all("PhysicsImageShapeComponent")) {
|
||||||
|
SpriteFrames sf;
|
||||||
|
if (load_sprite_frames(wak_, c->get("image_file"), "", sf)) { if (fxlib && fxlib->material_color && !c->get("material").empty()) tint_key_image(sf.texels, fxlib->material_color(c->get("material"))); d.sprite = std::move(sf.texels); d.sprite_w = sf.w; d.sprite_h = sf.h; d.sprite_frames = 1; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fxlib) {
|
||||||
|
fxlib->projectile(*e, d.fx);
|
||||||
|
if (pc) {
|
||||||
|
auto mf = pc->attrs.find("muzzle_flash_file");
|
||||||
|
if (mf != pc->attrs.end()) if (const EntityDef* fd = db_.get(mf->second)) fxlib->muzzle(*fd, d.fx.muzzle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!d.sprite.empty()) { // colour for sprite-less rendering: average of the opaque pixels
|
||||||
|
uint64_t r = 0, g = 0, b = 0, n = 0;
|
||||||
|
for (uint32_t t : std::vector<uint32_t>(d.sprite.begin(), d.sprite.begin() + (size_t)d.sprite_w * d.sprite_h)) { if ((t >> 24) < 128) continue; r += (t >> 16) & 255; g += (t >> 8) & 255; b += t & 255; ++n; }
|
||||||
|
if (n) d.color = 0xFF000000u | (uint32_t)(r / n) << 16 | (uint32_t)(g / n) << 8 | (uint32_t)(b / n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!extras.empty()) apply_extras(d, extras);
|
||||||
|
d.extras = extras;
|
||||||
|
return &(defs_[file] = d);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::apply_extras(ProjDef& d, const std::string& extrasIn) {
|
||||||
|
std::string extras = extrasIn;
|
||||||
|
const size_t cp = extras.find("|cm=");
|
||||||
|
if (cp != std::string::npos) { // the shot's material / material_amount (00c0b440: config_explosion create_cell_material +41c, create_cell_probability += amount +434)
|
||||||
|
size_t ce = extras.find('|', cp + 4); if (ce == std::string::npos) ce = extras.size();
|
||||||
|
const std::string cm = extras.substr(cp + 4, ce - cp - 4);
|
||||||
|
extras = extras.substr(0, cp) + extras.substr(ce);
|
||||||
|
const size_t colon = cm.find(':');
|
||||||
|
if (colon != std::string::npos) { d.explosion_material = cm.substr(0, colon); d.explosion_cell_probability += (float)std::atof(cm.c_str() + colon + 1); }
|
||||||
|
}
|
||||||
|
const size_t gp = extras.find("|ge=");
|
||||||
|
if (gp != std::string::npos) { // the shot's game_effect_entities (00c0b440: forwarded through the effect-application helper): effect files loaded on the hit entity
|
||||||
|
const std::string ge = extras.substr(gp + 4);
|
||||||
|
extras = extras.substr(0, gp);
|
||||||
|
for (size_t pos = 0; pos < ge.size();) { size_t end = ge.find(',', pos); if (end == std::string::npos) end = ge.size(); if (end > pos) d.hit_effects.push_back({"LOAD_GAME_EFFECT", ge.substr(pos, end - pos), "", "", 0.0f}); pos = end + 1; }
|
||||||
|
}
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < extras.size() && fxlib) {
|
||||||
|
size_t end = extras.find(',', pos);
|
||||||
|
if (end == std::string::npos) end = extras.size();
|
||||||
|
const std::string path = extras.substr(pos, end - pos);
|
||||||
|
pos = end + 1;
|
||||||
|
if (path.empty()) continue;
|
||||||
|
if (const EntityDef* e = db_.get(path)) {
|
||||||
|
if (d.gpu_lua_every > 0 && !gpu_extra_contract(*e)) d.needs_twin = true;
|
||||||
|
if (host_runs(*e)) d.needs_twin = true; // an extra entity with scripts (SPELLS_TO_POWER, the orbit / clipping / accelerating shots...): it lives as a child of the projectile's twin
|
||||||
|
for (const EntityComponent* he : e->all("HitEffectComponent")) // the effects of the extra entities (the hit fx modifiers of the wands)
|
||||||
|
d.hit_effects.push_back({he->get("effect_hit", ""), he->get("value_string", ""), he->get("condition_effect", ""), he->get("condition_status", ""), (float)std::atof(he->get("value", "0").c_str())});
|
||||||
|
parse_homing(*e, d); // (a homing spell adds its HomingComponent through the extra entities)
|
||||||
|
for (const EntityComponent* lc : e->all("LuaComponent")) { // the bounce modifiers (bounce_explosion.lua, bounce_spark, bounce_laser, bounce_larpa): a run-once script of the extra entity sets the
|
||||||
|
// projectile's bounce_fx_file; the engine runs it at the projectile's first update, before any bounce, and the twin's retype comes two frames late (a shot at a near
|
||||||
|
// wall bounced without its explosion), so the file it assigns is taken here
|
||||||
|
const std::string sf = lc->get("script_source_file", "");
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (sf.empty() || !wak_.read(sf, raw)) continue;
|
||||||
|
const std::string src(raw.begin(), raw.end());
|
||||||
|
const size_t k = src.find("vars.bounce_fx_file");
|
||||||
|
const size_t q0 = k == std::string::npos ? k : src.find('"', k), q1 = q0 == std::string::npos ? q0 : src.find('"', q0 + 1);
|
||||||
|
if (q1 != std::string::npos) d.bounce_fx_file = src.substr(q0 + 1, q1 - q0 - 1);
|
||||||
|
}
|
||||||
|
ProjFx tmp;
|
||||||
|
fxlib->projectile(*e, tmp);
|
||||||
|
d.fx.emitters.insert(d.fx.emitters.end(), tmp.emitters.begin(), tmp.emitters.end());
|
||||||
|
if (!d.fx.light.on && tmp.light.on) d.fx.light = tmp.light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjDef* ProjectileSim::def(const std::string& file, const std::string& extras) {
|
||||||
|
if (extras.empty()) return def(file);
|
||||||
|
const std::string key = file + "|" + extras;
|
||||||
|
auto it = defs_.find(key);
|
||||||
|
if (it != defs_.end()) return &it->second;
|
||||||
|
ProjDef d = *def(file);
|
||||||
|
apply_extras(d, extras);
|
||||||
|
d.extras = extras;
|
||||||
|
return &(defs_[key] = d);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjDef* ProjectileSim::explosion_variant(const ProjDef* base, float add_r, float add_dmg) {
|
||||||
|
if (!base || !(add_r > 0.0f)) return base;
|
||||||
|
const auto key = std::make_tuple(base, add_r, add_dmg);
|
||||||
|
if (auto it = xvars_.find(key); it != xvars_.end()) return &it->second;
|
||||||
|
ProjDef d = *base;
|
||||||
|
const float R = std::max(base->explosion_radius + add_r, add_r); // (00c0c539: the larger of the component's new radius and the cast's)
|
||||||
|
const char* spr = nullptr;
|
||||||
|
float dmin = -1.0f, ray = -1.0f, shake = -1.0f;
|
||||||
|
int s0 = d.fx.boom.sparks0, s1 = d.fx.boom.sparks1;
|
||||||
|
bool sparks = d.fx.boom.sparks;
|
||||||
|
if (R >= 128.0f) { dmin = 20.0f; spr = "data/particles/explosion_128.xml"; sparks = true; s0 = std::max(s0, 100); s1 = std::max(s1, 200); ray = 1200000.0f; shake = 60.0f; }
|
||||||
|
else if (R >= 64.0f) { dmin = 15.0f; spr = "data/particles/explosion_128.xml"; sparks = true; s0 = 20; s1 = 40; ray = 800000.0f; shake = 40.0f; }
|
||||||
|
else if (R >= 32.0f) { dmin = 13.0f; spr = "data/particles/explosion_064.xml"; sparks = true; s0 = std::max(s0, 7); s1 = std::max(s1, 20); ray = 500000.0f; shake = 30.0f; }
|
||||||
|
else if (R >= 16.0f) { spr = "data/particles/explosion_032.xml"; sparks = true; s0 = std::max(s0, 5); s1 = std::max(s1, 10); ray = 200000.0f; shake = 20.0f; }
|
||||||
|
else if (R >= 12.0f) spr = "data/particles/explosion_012.xml";
|
||||||
|
else if (R >= 8.0f) spr = "data/particles/explosion_008.xml";
|
||||||
|
if (dmin >= 0.0f) d.explosion_damage = std::max(base->explosion_damage + add_dmg, dmin) - add_dmg;
|
||||||
|
if (ray >= 0.0f) d.ray_energy = std::max(d.ray_energy, ray);
|
||||||
|
d.stains_radius = 1.5f * R; // (+0xec)
|
||||||
|
if (R >= 10.0f) d.explosion_cell_probability = std::max(d.explosion_cell_probability, 5.0f); // (+0xa8, the material default "fire"; and shake_vegetation on, not modelled)
|
||||||
|
ExplosionFx& fx = d.fx.boom;
|
||||||
|
if (spr && fxlib) if (const SpriteFrames* sf = fxlib->sprite(spr)) { const bool ownMat = fx.material_color != fx.color; fx.sprite = sf; fx.color = fxlib->sprite_color(sf); if (!ownMat) fx.material_color = fx.color; }
|
||||||
|
fx.sparks = sparks; fx.sparks0 = s0; fx.sparks1 = s1;
|
||||||
|
if (shake >= 0.0f) fx.shake = std::max(fx.shake, shake);
|
||||||
|
const float coeff = fx.radius > 0.0f && fx.light_radius > 0.0f ? fx.light_radius / fx.radius : 8.0f;
|
||||||
|
fx.radius = R; fx.light_radius = R * coeff;
|
||||||
|
fx.wait = 0; // (explosion_sprite_lifetime 0: the sprite plays its own animation)
|
||||||
|
fx.valid = fx.sprite || fx.sparks || fx.material_sparks;
|
||||||
|
return &(xvars_[key] = std::move(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::cast(const std::shared_ptr<ShotResult>& owner, const Shot& shot, double x, double y, double dirx, double diry, double child_mult) {
|
||||||
|
const double base = std::atan2(diry, dirx);
|
||||||
|
const int n = (int)shot.projectiles.size();
|
||||||
|
const double spread = std::max(0.0, shot.state.get("spread_degrees", 0)) * kDeg;
|
||||||
|
const double pattern = shot.state.get("pattern_degrees", 0) * kDeg;
|
||||||
|
const double speed_mult = shot.state.get("speed_multiplier", 1) * child_mult;
|
||||||
|
std::uniform_real_distribution<double> U(0.0, 1.0);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const ShotProjectile& sp = shot.projectiles[i];
|
||||||
|
const ProjDef* d = explosion_variant(def(sp.file), (float)shot.state.get("explosion_radius", 0), (float)shot.state.get("damage_explosion_add", 0));
|
||||||
|
SimProjectile p;
|
||||||
|
double ang = base + (U(rng_) - 0.5) * 2.0 * spread * 0.5 + (U(rng_) - 0.5) * 2.0 * d->spread_rad;
|
||||||
|
if (n > 1 && pattern > 0) ang += -pattern * 0.5 + pattern * (double)i / (double)(n - 1);
|
||||||
|
const double speed = (d->speed_min + (d->speed_max - d->speed_min) * U(rng_)) * speed_mult;
|
||||||
|
p.x = x; p.y = y;
|
||||||
|
p.vx = std::cos(ang) * speed; p.vy = std::sin(ang) * speed;
|
||||||
|
p.life = std::max(1, d->lifetime + (int)shot.state.get("lifetime_add", 0));
|
||||||
|
p.damage = d->damage + (float)(shot.state.get("damage_projectile_add") + shot.state.get("damage_melee_add") + shot.state.get("damage_electricity_add") +
|
||||||
|
shot.state.get("damage_fire_add") + shot.state.get("damage_ice_add") + shot.state.get("damage_slice_add") +
|
||||||
|
shot.state.get("damage_drill_add"));
|
||||||
|
p.gravity = d->gravity + shot.state.get("gravity", 0) / 3600.0;
|
||||||
|
p.friction = d->air_friction;
|
||||||
|
p.bounces = (int)shot.state.get("bounces", 0);
|
||||||
|
p.def = d;
|
||||||
|
p.explosion_radius = d->explosion_radius + (float)shot.state.get("explosion_radius", 0);
|
||||||
|
p.explosion_damage = d->explosion_damage + (float)shot.state.get("damage_explosion_add", 0);
|
||||||
|
p.owner = owner;
|
||||||
|
p.spec = &sp;
|
||||||
|
p.fired.assign(sp.triggers.size(), 0);
|
||||||
|
projectiles.push_back(std::move(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::fire_triggers(SimProjectile& p, int mode, double dx, double dy) {
|
||||||
|
if (!p.spec) return;
|
||||||
|
std::uniform_real_distribution<double> U(0.0, 1.0);
|
||||||
|
for (size_t i = 0; i < p.spec->triggers.size(); ++i) {
|
||||||
|
const auto k = p.spec->triggers[i].type;
|
||||||
|
const bool go = mode == 1 ? (k == ShotTrigger::Timer && p.spec->triggers[i].delay > 0 && p.age == p.spec->triggers[i].delay)
|
||||||
|
: (mode == 2 || mode == 3) ? (k == ShotTrigger::Timer || k == ShotTrigger::HitWorld)
|
||||||
|
: mode == 4 ? k == ShotTrigger::Timer : k == ShotTrigger::Death;
|
||||||
|
if (!go) continue;
|
||||||
|
double vx = dx, vy = dy;
|
||||||
|
if (mode == 5 && (vx * vx + vy * vy) * 3600.0 < 100.0) { const double a = U(rng_) * 6.2831855; vx = std::cos(a); vy = std::sin(a); } // 00c0e3e0: at rest, a random direction
|
||||||
|
fire_trigger(p, (int)i, vx, vy, mode == 3, mode == 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::fire_trigger(SimProjectile& p, int index, double dx, double dy, bool keep, bool world_hit) {
|
||||||
|
if (p.fired[index]) return;
|
||||||
|
if (!keep) p.fired[index] = 1;
|
||||||
|
const ShotTrigger& t = p.spec->triggers[index];
|
||||||
|
if (!t.shot) return;
|
||||||
|
const double len = std::hypot(dx, dy);
|
||||||
|
if (len < 1e-6) { dx = 0; dy = 1; } else { dx /= len; dy /= len; }
|
||||||
|
const double x = p.x, y = p.y; // at the projectile itself (00c0b440)
|
||||||
|
auto owner = p.owner;
|
||||||
|
const Shot* sub = t.shot.get();
|
||||||
|
const double cm = world_hit ? sub->state.get("child_speed_multiplier", 1) : 1.0; // 00c06250: a world hit scales the payload by its own child_speed_multiplier
|
||||||
|
deferred_.push_back([this, owner, sub, x, y, dx, dy, cm]() { cast(owner, *sub, x, y, dx, dy, cm); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::kill(SimProjectile& p, const Hooks& h, bool hit_world) {
|
||||||
|
if (!p.alive) return;
|
||||||
|
p.alive = false;
|
||||||
|
if (hit_world) fire_triggers(p, 2, -p.vx, -p.vy); // (the hit that stops it: no surface normal here, back the way it came)
|
||||||
|
if (p.age >= p.life) fire_triggers(p, 4, p.vx, p.vy);
|
||||||
|
fire_triggers(p, 5, p.vx, p.vy);
|
||||||
|
if (p.def && p.def->explode_on_death && p.explosion_radius > 0) {
|
||||||
|
if (h.area_damage) h.area_damage(p.x, p.y, p.explosion_radius, p.def->damage_mortals ? p.explosion_damage : 0.0, p.def->knockback_force);
|
||||||
|
if (h.dig && p.def->hole) h.dig(p.x, p.y, p.explosion_radius, p.def->max_durability);
|
||||||
|
if (h.spawn_material && !p.def->explosion_material.empty() && p.def->explosion_cell_probability > 0)
|
||||||
|
h.spawn_material(p.x, p.y, p.explosion_radius, p.def->explosion_material, p.def->explosion_cell_probability / 100.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectileSim::step(const Hooks& h) {
|
||||||
|
for (SimProjectile& p : projectiles) {
|
||||||
|
if (!p.alive) continue;
|
||||||
|
++p.age;
|
||||||
|
fire_triggers(p, 1, p.vx, p.vy); // the countdown runs before the flight of the update (00c06c60)
|
||||||
|
p.vy += p.gravity;
|
||||||
|
if (p.friction > 0) { const double f = 1.0 - p.friction / 60.0; p.vx *= f; p.vy *= f; }
|
||||||
|
const int steps = std::max(1, (int)std::ceil(std::max(std::fabs(p.vx), std::fabs(p.vy))));
|
||||||
|
for (int i = 0; i < steps && p.alive; ++i) {
|
||||||
|
const double nx = p.x + p.vx / steps, ny = p.y + p.vy / steps;
|
||||||
|
if (h.solid && h.solid((int)std::floor(nx), (int)std::floor(ny))) {
|
||||||
|
if (p.bounces > 0 || !p.def->on_collision_die) { // bounce off the surface (axis that is blocked)
|
||||||
|
const bool bx = h.solid((int)std::floor(nx), (int)std::floor(p.y));
|
||||||
|
const bool by = h.solid((int)std::floor(p.x), (int)std::floor(ny));
|
||||||
|
if (bx || !by) p.vx = -p.vx * 0.85;
|
||||||
|
if (by || !bx) p.vy = -p.vy * 0.85;
|
||||||
|
fire_triggers(p, 2, p.vx, p.vy); // 00c06250: a world hit fires the hit-world and waiting timers in the bounced direction, the projectile lives on
|
||||||
|
if (p.bounces > 0) --p.bounces;
|
||||||
|
else if (std::hypot(p.vx, p.vy) < 0.2) p.alive = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
kill(p, h, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p.x = nx; p.y = ny;
|
||||||
|
if (h.hit_creature && h.hit_creature(p.x, p.y, 2.0, p.damage)) { fire_triggers(p, 3, p.vx, p.vy); kill(p, h, false); break; }
|
||||||
|
}
|
||||||
|
if (!p.alive) continue;
|
||||||
|
if (p.age >= p.life) kill(p, h, false);
|
||||||
|
}
|
||||||
|
projectiles.erase(std::remove_if(projectiles.begin(), projectiles.end(), [](const SimProjectile& p) { return !p.alive; }), projectiles.end());
|
||||||
|
auto pending = std::move(deferred_);
|
||||||
|
deferred_.clear();
|
||||||
|
for (auto& f : pending) f();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#pragma once
|
||||||
|
// Flying projectiles of the player's wand: a shot (gun/gun.h) becomes projectile entities that move, collide with the world
|
||||||
|
// and creatures, bounce, time out, fire their triggers and explode. Simulated on the CPU against a callback view of the world.
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entity/entity.h"
|
||||||
|
#include "gun/effects.h"
|
||||||
|
#include "gun/gun.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
// Values read from a projectile entity file (per-second values converted to cells per frame).
|
||||||
|
struct ProjDef {
|
||||||
|
float speed_min = 10, speed_max = 10; // cells per frame
|
||||||
|
float damage = 0.1f; // hp units (x25 on screen): the projectile channel (damage + damage_by_type.projectile)
|
||||||
|
float dmg_type[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // damage_by_type: melee, electricity, fire, ice, slice, drill, radioactive, poison, healing, curse, holy, explosion (each hurts through its own multiplier of the victim)
|
||||||
|
int lifetime = 60;
|
||||||
|
float gravity = 0.05f; // cells per frame^2 (VelocityComponent gravity_y / 3600)
|
||||||
|
float air_friction = 0; // fraction of the speed lost per second
|
||||||
|
bool on_collision_die = true;
|
||||||
|
int bounces_left = 0; // ProjectileComponent bounces_left: grenades bounce this often before they explode
|
||||||
|
bool friendly_fire = false; // ProjectileComponent.friendly_fire: hurts the herd of the shooter too
|
||||||
|
bool dont_hit_player = false; // dont_hit_player: never hurts the player, whatever the herds
|
||||||
|
int collide_with_shooter_frames = 0; // the shooter itself is not hit during the first frames
|
||||||
|
// HitEffectComponent (of the projectile and of its extra entities): what a hit does to the victim
|
||||||
|
struct HitEffect { std::string effect, value_string, condition_effect, condition_status; float value = 0; };
|
||||||
|
std::vector<HitEffect> hit_effects;
|
||||||
|
std::string file; // the entity file it was built from
|
||||||
|
std::string extras; // the shot's extra entities (+ "|ge=" game effects) applied on top of it
|
||||||
|
bool needs_twin = false;
|
||||||
|
int gpu_lua_every = 0; // reviewed stateless script; automatic GPU cadence when no host components remain
|
||||||
|
bool query_projectile = false, query_velocity = false, query_unsupported = false;
|
||||||
|
bool collide_with_world = true, collide_with_entities = true, penetrate_entities = false; // ProjectileComponent
|
||||||
|
int damage_every_x_frames = 2; // a penetrating projectile hurts an entity it overlaps every so many frames (ProjectileComponent, default 2) // the projectile has components the GPU does not simulate (CellEater, MagicConvertMaterial, scripts, area effects...): the host keeps a runtime entity (twin) for it
|
||||||
|
struct Homing { // HomingComponent (constructor 00a0b1d0, update 00b323f0)
|
||||||
|
bool on = false;
|
||||||
|
float detect = 150, mult = 0.9f, coeff = 160, max_turn = 0.05f; // detect_distance, homing_velocity_multiplier, homing_targeting_coeff, max_turn_rate
|
||||||
|
bool rotate = false, who_shot = false; // just_rotate_towards_target, target_who_shot
|
||||||
|
int target = 0; // predefined_target (an entity id; scripts set it on the twin)
|
||||||
|
int target_enc = 0; // its GPU slot: entity slot + 1, or -(projectile slot + 1), 0 none (set by the host when it retypes the twin)
|
||||||
|
int tag_bits = 2048; // target_tag (default "homing_target") as the entity tag bit: 512 mortal, 1024 prey, 2048 homing_target, 4096 player_unit
|
||||||
|
} homing;
|
||||||
|
bool particle = false; // an entity without ProjectileComponent that only moves and emits (the entities an explosion loads): no damage, no impact sparks
|
||||||
|
float die_speed = 0; // DieIfSpeedBelowComponent min_speed (cells per frame)
|
||||||
|
std::string load_files; // config_explosion.load_this_entity (comma separated entity files): the host spawns them as real entities when the explosion happens
|
||||||
|
bool invisible = false; // an entity with no ProjectileComponent and no sprite (the gold pickup effect: only emitters): nothing is drawn for its body
|
||||||
|
bool die_on_liquid = false; // die_on_liquid_collision
|
||||||
|
float liquid_drag = 1.0f; // VelocityComponent liquid_drag (constructor default 1.0): the share of the speed lost per liquid cell around, per second
|
||||||
|
bool displace_liquid = true; // ProjectileComponent displace_liquid: the liquid around is pushed aside (the constructor default is not recovered, the explicit values of the data are honoured)
|
||||||
|
bool physics = false; // a thrown physics body (bombs): bounces and rolls, explodes only when its fuse runs out
|
||||||
|
bool creature = false; // a cast creature (SUMMON_WANDGHOST's wand_ghost_player.xml: no ProjectileComponent, an AI): the host spawns it as a whole entity, the GPU projectile only carries it out of the wand
|
||||||
|
float break_speed = 0; // a breakable thrown body (SUMMON_EGG's egg_monster.xml: PhysicsBodyCollisionDamage + DamageModel + ExplodeOnDamage): an impact faster than this (px/s) kills it and it explodes with the ExplodeOnDamage config
|
||||||
|
bool explode_on_death = false;
|
||||||
|
float explosion_radius = 0, explosion_damage = 0, max_durability = 8, ray_energy = 20000;
|
||||||
|
bool stains = true; float stains_radius = 0; // config_explosion stains_enabled (default on, 004b2780 +0xe8), stains_radius (+0xec, 0 = 0.4 x the radius): the burnt ring // ConfigExplosion constructor 004b2780: ray_energy 20000, max_durability_to_destroy 10, knockback_force 1, damage_mortals on
|
||||||
|
float knockback_force = 1.0f; // ConfigExplosion.knockback_force
|
||||||
|
bool damage_mortals = true;
|
||||||
|
bool dont_damage_shooter = false; // ProjectileComponent explosion_dont_damage_shooter (+38a, default false): only then the explosion ignores the shooter (00c0a930)
|
||||||
|
bool not_scaled_by_gamefx = false; // ConfigExplosion not_scaled_by_gamefx (+154): the perks' Shot-time scaling leaves the explosion alone
|
||||||
|
float hit_knockback_force = 0.0f; // ProjectileComponent knockback_force (+5d0, default 0): a hit knocks back by knockback_force x speed x the projectile's VelocityComponent mass (00c091f0)
|
||||||
|
float ragdoll_explosion_mult = 1.0f; // config_explosion physics_multiplier_ragdoll_force (ConfigExplosion +0x108, default 1): scales the impulse an explosion gives the entities
|
||||||
|
int ragdoll_fx = 1; // ProjectileComponent ragdoll_fx_on_collision (+53c): RAGDOLL_FX NONE 0, NORMAL 1, BLOOD_EXPLOSION 2, BLOOD_SPRAY 3, FROZEN 4, CONVERT_TO_MATERIAL 5, CUSTOM_RAGDOLL_ENTITY 6, DISINTEGRATED 7, NO_RAGDOLL_FILE 8, PLAYER_RAGDOLL_CAMERA 9
|
||||||
|
float ragdoll_force_multiplier = 0.025f; // ProjectileComponent (+5d4, default 0.025): scales the force vector of the hit
|
||||||
|
float velocity_mass = 0.05f; // VelocityComponent mass (+50, default 0.05)
|
||||||
|
float physics_min = 0, physics_max = 0.2f; // config_explosion.physics_explosion_power.min / .max: the force on rigid bodies (00657960)
|
||||||
|
std::string explosion_material; // config_explosion.create_cell_material (e.g. fire)
|
||||||
|
bool explosion_audio = true; // config_explosion.audio_enabled (constructor 004b2780: on)
|
||||||
|
std::string explosion_audio_event; // config_explosion.audio_event_name (empty: explosion / explosion_small by the radius, 00657960)
|
||||||
|
float explosion_audio_liquid = 0; // config_explosion.audio_liquid_amount_normalized
|
||||||
|
float explosion_cell_probability = 0; // percent of the cells in the blast that become that material
|
||||||
|
bool hole = false; // the explosion digs
|
||||||
|
float spread_rad = 0; // direction_random_rad
|
||||||
|
uint32_t color = 0xFFFFD040u;
|
||||||
|
std::vector<uint32_t> sprite; // all animation frames one after the other, ARGB
|
||||||
|
int sprite_w = 0, sprite_h = 0, sprite_frames = 1, sprite_wait = 6;
|
||||||
|
ProjFx fx; // emitters, light, muzzle flash, explosion visuals
|
||||||
|
// ProjectileComponent world collision (00c073b0; defaults from component_documentation.txt, offsets in analysis/projcomp/projectile_fields.txt)
|
||||||
|
bool penetrate_world = false; // +504: moves through the world, slowed to penetrate_world_velocity_coeff of the step inside it
|
||||||
|
float penetrate_world_velocity_coeff = 0.6f; // +508
|
||||||
|
float ground_penetration_coeff = 0; // +338: with VelocityComponent mass, the kinetic energy spent on the cells it digs through (00c08a30)
|
||||||
|
int ground_penetration_max_durability = 0; // +33c: cells of a material more durable than this stop it (0: no limit)
|
||||||
|
float bounce_energy = 0.5f; // +304
|
||||||
|
bool bounce_always = false, bounce_at_any_angle = false; // +308, +309
|
||||||
|
bool on_collision_remove_projectile = false; // +50e: a world hit leaves the entity without its ProjectileComponent (it stays where it stuck)
|
||||||
|
bool die_on_low_velocity = false; // +369
|
||||||
|
float die_on_low_velocity_limit = 50; // +36c, pixels per second
|
||||||
|
float physics_impulse_coeff = 300; // +52c: the push on a physics body it hits (impulse = coeff x velocity)
|
||||||
|
bool damage_scaled_by_speed = false; // +534
|
||||||
|
float damage_scale_max_speed = 0; // +538
|
||||||
|
int lifetime_randomness = 0; // +50: lifetime + random(-r, r)
|
||||||
|
std::string spawn_entity; // +510: on_collision_spawn_entity (+50f, default 1): this is loaded (or shot, spawn_entity_is_projectile +528) where it dies of a hit (00c08b90)
|
||||||
|
bool spawn_entity_is_projectile = false;
|
||||||
|
std::string bounce_fx_file; // +30c: loaded where the projectile bounces (00c073b0, at most 10 a frame); Explosive bounce sets it
|
||||||
|
int projectile_type = 0; // +48: PROJECTILE 0, LIGHTNING 1 (00c0d290: the aim x 5400 px/s), VERLET 2, MATERIAL_PARTICLE 3
|
||||||
|
struct Verlet { // projectile_type VERLET (the tentacles): the chain of the child entity's VerletPhysicsComponent and the root's VerletWeaponComponent (proj_step.comp verlet_step)
|
||||||
|
bool on = false;
|
||||||
|
int points = 16, fade = 10;
|
||||||
|
float rest = 4, stiff = 1, damp = 0.99f, liquid_damp = 0.7f;
|
||||||
|
bool cells = true;
|
||||||
|
float dmg_radius = 5, dmg_min = 0.01f, dmg_max = 1, dmg_coeff = 1, impulse = 1, force_radius = 3, phys_impulse = 1; // (physics_force_radius / physics_impulse_coeff: the push on the bodies)
|
||||||
|
struct Part { std::vector<uint32_t> tex; int w = 0, h = 0; float ox = 0, oy = 0; };
|
||||||
|
std::vector<Part> parts; // the sprites of the points, in order (each child Base file's SpriteComponent)
|
||||||
|
} verlet;
|
||||||
|
std::string death_particle; // on_death_emit_particle (+362): the material emitted on death, on_death_emit_particle_count (+364) of them
|
||||||
|
int death_particle_count = 0;
|
||||||
|
// VelocityComponent (00c370c0)
|
||||||
|
float gravity_x = 0; // cells per frame^2
|
||||||
|
float terminal_velocity = 1000; // pixels per second, applied when apply_terminal_velocity (default on)
|
||||||
|
bool apply_terminal_velocity = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SimProjectile {
|
||||||
|
double x = 0, y = 0, vx = 0, vy = 0;
|
||||||
|
int age = 0, life = 60;
|
||||||
|
float damage = 0.1f;
|
||||||
|
double gravity = 0, friction = 0;
|
||||||
|
int bounces = 0;
|
||||||
|
const ProjDef* def = nullptr;
|
||||||
|
float explosion_radius = 0, explosion_damage = 0;
|
||||||
|
std::shared_ptr<ShotResult> owner; // keeps the shot tree (triggers) alive
|
||||||
|
const ShotProjectile* spec = nullptr;
|
||||||
|
std::vector<char> fired; // per trigger: already fired
|
||||||
|
bool alive = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProjectileSim {
|
||||||
|
public:
|
||||||
|
ProjectileSim(const Wak& wak, EntityDb& db) : wak_(wak), db_(db) {}
|
||||||
|
|
||||||
|
struct Hooks {
|
||||||
|
std::function<bool(int x, int y)> solid; // cell blocks projectiles
|
||||||
|
std::function<bool(double x, double y, double r, double damage)> hit_creature; // direct hit: true if a creature was hit
|
||||||
|
std::function<void(double x, double y, double r, double damage, double knockback_force)> area_damage; // explosion damage and knockback to creatures (00657960)
|
||||||
|
std::function<void(double x, double y, double r, double max_durability)> dig; // explosion digs a hole
|
||||||
|
std::function<void(double x, double y, double r, const std::string& material, double probability)> spawn_material; // explosion fills air with a material
|
||||||
|
};
|
||||||
|
|
||||||
|
// Casts a shot (or a trigger's sub-shot) from a point along a direction.
|
||||||
|
void cast(const std::shared_ptr<ShotResult>& owner, const Shot& shot, double x, double y, double dirx, double diry, double child_mult = 1.0);
|
||||||
|
void step(const Hooks& h);
|
||||||
|
const ProjDef* def(const std::string& file);
|
||||||
|
// A definition built from an entity description that is not a file (the runtime copy of a projectile after the scripts changed it), stored under `key`.
|
||||||
|
const ProjDef* def_from(const std::string& key, const EntityDef& entity, const std::string& extras = ""); // + the extra entities of the shot (emitters, light, hit effects, homing)
|
||||||
|
EntityDb& db() { return db_; }
|
||||||
|
// The projectile with the extra entities a shot carries (c.extra_entities, comma separated): their particle emitters and light are added.
|
||||||
|
const ProjDef* def(const std::string& file, const std::string& extras);
|
||||||
|
void apply_extras(ProjDef& d, const std::string& extras);
|
||||||
|
// 00c06460: a cast that adds explosion_radius (c.explosion_radius > 0, 00c0b440) sets the explosion up for its new radius by size class - the least damage
|
||||||
|
// (13 from 32, 15 from 64, 20 from 128), the sprite, the sparks, ray_energy, camera_shake, stains_radius 1.5 x R, create_cell_probability at least 5 from 10.
|
||||||
|
// The variant keeps the file's explosion_radius (the shot adds its own) and is made so that its damage + add_damage is the engine's value.
|
||||||
|
const ProjDef* explosion_variant(const ProjDef* base, float add_radius, float add_damage);
|
||||||
|
|
||||||
|
std::vector<SimProjectile> projectiles;
|
||||||
|
EffectLib* fxlib = nullptr; // optional: loads emitters etc. from the projectile files
|
||||||
|
bool gpu_lua_enabled = false; // set before building definitions, only after native startup compilation succeeds
|
||||||
|
void disable_gpu_lua();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void kill(SimProjectile& p, const Hooks& h, bool hit_world);
|
||||||
|
// the triggers as proj_step.comp fires them (mode 1 countdown, 2 world hit, 3 creature impact, 4 lifetime end, 5 removal); dx/dy: the direction to cast in
|
||||||
|
void fire_triggers(SimProjectile& p, int mode, double dx, double dy);
|
||||||
|
void fire_trigger(SimProjectile& p, int index, double dx, double dy, bool keep = false, bool world_hit = false);
|
||||||
|
const Wak& wak_;
|
||||||
|
EntityDb& db_;
|
||||||
|
std::map<std::string, ProjDef> defs_;
|
||||||
|
std::map<std::tuple<const ProjDef*, float, float>, ProjDef> xvars_; // (explosion_variant: map nodes keep their addresses)
|
||||||
|
std::mt19937 rng_{12345};
|
||||||
|
std::vector<std::function<void()>> deferred_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
// Generated by tools/gen_api_names.py from lua_api_documentation.txt. Do not edit.
|
||||||
|
"EntityLoad",
|
||||||
|
"EntityLoadEndGameItem",
|
||||||
|
"EntityLoadCameraBound",
|
||||||
|
"EntityLoadToEntity",
|
||||||
|
"EntitySave",
|
||||||
|
"EntityCreateNew",
|
||||||
|
"EntityKill",
|
||||||
|
"EntityGetIsAlive",
|
||||||
|
"EntityAddComponent",
|
||||||
|
"EntityRemoveComponent",
|
||||||
|
"EntityGetAllComponents",
|
||||||
|
"EntityGetComponent",
|
||||||
|
"EntityGetFirstComponent",
|
||||||
|
"EntityGetComponentIncludingDisabled",
|
||||||
|
"EntityGetFirstComponentIncludingDisabled",
|
||||||
|
"EntitySetTransform",
|
||||||
|
"EntityApplyTransform",
|
||||||
|
"EntityGetTransform",
|
||||||
|
"EntityAddChild",
|
||||||
|
"EntityGetAllChildren",
|
||||||
|
"EntityGetParent",
|
||||||
|
"EntityGetRootEntity",
|
||||||
|
"EntityRemoveFromParent",
|
||||||
|
"EntitySetComponentsWithTagEnabled",
|
||||||
|
"EntitySetComponentIsEnabled",
|
||||||
|
"EntityGetName",
|
||||||
|
"EntitySetName",
|
||||||
|
"EntityGetTags",
|
||||||
|
"EntityGetWithTag",
|
||||||
|
"EntityGetInRadius",
|
||||||
|
"EntityGetInRadiusWithTag",
|
||||||
|
"EntityGetClosest",
|
||||||
|
"EntityGetClosestWithTag",
|
||||||
|
"EntityGetWithName",
|
||||||
|
"EntityAddTag",
|
||||||
|
"EntityRemoveTag",
|
||||||
|
"EntityHasTag",
|
||||||
|
"EntityGetFilename",
|
||||||
|
"EntitiesGetMaxID",
|
||||||
|
"ComponentGetValue",
|
||||||
|
"ComponentGetValueBool",
|
||||||
|
"ComponentGetValueInt",
|
||||||
|
"ComponentGetValueFloat",
|
||||||
|
"ComponentGetValueVector2",
|
||||||
|
"ComponentSetValue",
|
||||||
|
"ComponentSetValueVector2",
|
||||||
|
"ComponentSetValueValueRange",
|
||||||
|
"ComponentSetValueValueRangeInt",
|
||||||
|
"ComponentSetMetaCustom",
|
||||||
|
"ComponentGetMetaCustom",
|
||||||
|
"ComponentObjectGetValue",
|
||||||
|
"ComponentObjectSetValue",
|
||||||
|
"ComponentAddTag",
|
||||||
|
"ComponentRemoveTag",
|
||||||
|
"ComponentGetTags",
|
||||||
|
"ComponentHasTag",
|
||||||
|
"ComponentGetValue2",
|
||||||
|
"ComponentSetValue2",
|
||||||
|
"ComponentObjectGetValue2",
|
||||||
|
"ComponentObjectSetValue2",
|
||||||
|
"EntityAddComponent2",
|
||||||
|
"ComponentGetVectorSize",
|
||||||
|
"ComponentGetVectorValue",
|
||||||
|
"ComponentGetVector",
|
||||||
|
"ComponentGetIsEnabled",
|
||||||
|
"ComponentGetEntity",
|
||||||
|
"ComponentGetMembers",
|
||||||
|
"ComponentObjectGetMembers",
|
||||||
|
"ComponentGetTypeName",
|
||||||
|
"GetUpdatedEntityID",
|
||||||
|
"GetUpdatedComponentID",
|
||||||
|
"SetTimeOut",
|
||||||
|
"RegisterSpawnFunction",
|
||||||
|
"SpawnActionItem",
|
||||||
|
"SpawnStash",
|
||||||
|
"SpawnApparition",
|
||||||
|
"LoadEntityToStash",
|
||||||
|
"AddMaterialInventoryMaterial",
|
||||||
|
"RemoveMaterialInventoryMaterial",
|
||||||
|
"GetMaterialInventoryMainMaterial",
|
||||||
|
"GameScreenshake",
|
||||||
|
"GameOnCompleted",
|
||||||
|
"GameGiveAchievement",
|
||||||
|
"GameDoEnding2",
|
||||||
|
"GetParallelWorldPosition",
|
||||||
|
"BiomeMapLoad_KeepPlayer",
|
||||||
|
"BiomeMapLoad",
|
||||||
|
"BiomeSetValue",
|
||||||
|
"BiomeGetValue",
|
||||||
|
"BiomeObjectSetValue",
|
||||||
|
"BiomeVegetationSetValue",
|
||||||
|
"BiomeMaterialSetValue",
|
||||||
|
"BiomeMaterialGetValue",
|
||||||
|
"GameIsIntroPlaying",
|
||||||
|
"GameGetIsGamepadConnected",
|
||||||
|
"GameGetWorldStateEntity",
|
||||||
|
"GameGetPlayerStatsEntity",
|
||||||
|
"GameGetOrbCountAllTime",
|
||||||
|
"GameGetOrbCountThisRun",
|
||||||
|
"GameGetOrbCollectedThisRun",
|
||||||
|
"GameGetOrbCollectedAllTime",
|
||||||
|
"GameClearOrbsFoundThisRun",
|
||||||
|
"GameGetOrbCountTotal",
|
||||||
|
"CellFactory_GetName",
|
||||||
|
"CellFactory_GetType",
|
||||||
|
"CellFactory_GetUIName",
|
||||||
|
"CellFactory_GetAllLiquids",
|
||||||
|
"CellFactory_GetAllSands",
|
||||||
|
"CellFactory_GetAllGases",
|
||||||
|
"CellFactory_GetAllFires",
|
||||||
|
"CellFactory_GetAllSolids",
|
||||||
|
"CellFactory_GetTags",
|
||||||
|
"CellFactory_HasTag",
|
||||||
|
"GameGetCameraPos",
|
||||||
|
"GameSetCameraPos",
|
||||||
|
"GameSetCameraFree",
|
||||||
|
"GameGetCameraBounds",
|
||||||
|
"GameRegenItemAction",
|
||||||
|
"GameRegenItemActionsInContainer",
|
||||||
|
"GameRegenItemActionsInPlayer",
|
||||||
|
"GameKillInventoryItem",
|
||||||
|
"GamePickUpInventoryItem",
|
||||||
|
"GameGetAllInventoryItems",
|
||||||
|
"GameDropAllItems",
|
||||||
|
"GameDropPlayerInventoryItems",
|
||||||
|
"GameDestroyInventoryItems",
|
||||||
|
"GameIsInventoryOpen",
|
||||||
|
"GameTriggerGameOver",
|
||||||
|
"LoadPixelScene",
|
||||||
|
"LoadBackgroundSprite",
|
||||||
|
"RemovePixelSceneBackgroundSprite",
|
||||||
|
"RemovePixelSceneBackgroundSprites",
|
||||||
|
"GameCreateCosmeticParticle",
|
||||||
|
"GameCreateParticle",
|
||||||
|
"GameCreateSpriteForXFrames",
|
||||||
|
"GameShootProjectile",
|
||||||
|
"EntityInflictDamage",
|
||||||
|
"EntityIngestMaterial",
|
||||||
|
"EntityRemoveIngestionStatusEffect",
|
||||||
|
"EntityRemoveStainStatusEffect",
|
||||||
|
"EntityAddRandomStains",
|
||||||
|
"EntitySetDamageFromMaterial",
|
||||||
|
"EntityRefreshSprite",
|
||||||
|
"EntityGetWandCapacity",
|
||||||
|
"EntityGetHotspot",
|
||||||
|
"GamePlayAnimation",
|
||||||
|
"GameGetVelocityCompVelocity",
|
||||||
|
"GameGetGameEffect",
|
||||||
|
"GameGetGameEffectCount",
|
||||||
|
"LoadGameEffectEntityTo",
|
||||||
|
"GetGameEffectLoadTo",
|
||||||
|
"PolymorphTableAddEntity",
|
||||||
|
"PolymorphTableRemoveEntity",
|
||||||
|
"PolymorphTableGet",
|
||||||
|
"PolymorphTableSet",
|
||||||
|
"SetPlayerSpawnLocation",
|
||||||
|
"UnlockItem",
|
||||||
|
"GameGetPotionColorUint",
|
||||||
|
"EntityGetFirstHitboxCenter",
|
||||||
|
"Raytrace",
|
||||||
|
"RaytraceSurfaces",
|
||||||
|
"RaytraceSurfacesAndLiquiform",
|
||||||
|
"RaytracePlatforms",
|
||||||
|
"FindFreePositionForBody",
|
||||||
|
"GetSurfaceNormal",
|
||||||
|
"GameGetSkyVisibility",
|
||||||
|
"GameGetFogOfWar",
|
||||||
|
"GameGetFogOfWarBilinear",
|
||||||
|
"GameSetFogOfWar",
|
||||||
|
"DoesWorldExistAt",
|
||||||
|
"StringToHerdId",
|
||||||
|
"HerdIdToString",
|
||||||
|
"GetHerdRelation",
|
||||||
|
"EntityGetHerdRelation",
|
||||||
|
"EntityGetHerdRelationSafe",
|
||||||
|
"GenomeSetHerdId",
|
||||||
|
"EntityGetClosestWormAttractor",
|
||||||
|
"EntityGetClosestWormDetractor",
|
||||||
|
"GamePrint",
|
||||||
|
"GamePrintImportant",
|
||||||
|
"DEBUG_GetMouseWorld",
|
||||||
|
"DEBUG_MARK",
|
||||||
|
"GameGetFrameNum",
|
||||||
|
"GameGetRealWorldTimeSinceStarted",
|
||||||
|
"InputIsKeyDown",
|
||||||
|
"InputIsKeyJustDown",
|
||||||
|
"InputIsKeyJustUp",
|
||||||
|
"InputGetMousePosOnScreen",
|
||||||
|
"InputIsMouseButtonDown",
|
||||||
|
"InputIsMouseButtonJustDown",
|
||||||
|
"InputIsMouseButtonJustUp",
|
||||||
|
"InputIsJoystickButtonDown",
|
||||||
|
"InputIsJoystickButtonJustDown",
|
||||||
|
"InputGetJoystickAnalogButton",
|
||||||
|
"InputIsJoystickConnected",
|
||||||
|
"InputGetJoystickAnalogStick",
|
||||||
|
"IsPlayer",
|
||||||
|
"IsInvisible",
|
||||||
|
"GameIsDailyRun",
|
||||||
|
"GameIsDailyRunOrDailyPracticeRun",
|
||||||
|
"GameIsModeFullyDeterministic",
|
||||||
|
"GlobalsSetValue",
|
||||||
|
"GlobalsGetValue",
|
||||||
|
"MagicNumbersGetValue",
|
||||||
|
"SetWorldSeed",
|
||||||
|
"SessionNumbersGetValue",
|
||||||
|
"SessionNumbersSetValue",
|
||||||
|
"SessionNumbersSave",
|
||||||
|
"AutosaveDisable",
|
||||||
|
"StatsGetValue",
|
||||||
|
"StatsGlobalGetValue",
|
||||||
|
"StatsBiomeGetValue",
|
||||||
|
"StatsBiomeReset",
|
||||||
|
"StatsLogPlayerKill",
|
||||||
|
"CreateItemActionEntity",
|
||||||
|
"GetRandomActionWithType",
|
||||||
|
"GetRandomAction",
|
||||||
|
"GameGetDateAndTimeUTC",
|
||||||
|
"GameGetDateAndTimeLocal",
|
||||||
|
"GameEmitRainParticles",
|
||||||
|
"GameCutThroughWorldVertical",
|
||||||
|
"BiomeMapSetSize",
|
||||||
|
"BiomeMapGetSize",
|
||||||
|
"BiomeMapSetPixel",
|
||||||
|
"BiomeMapGetPixel",
|
||||||
|
"BiomeMapConvertPixelFromUintToInt",
|
||||||
|
"BiomeMapLoadImage",
|
||||||
|
"BiomeMapLoadImageCropped",
|
||||||
|
"BiomeMapGetVerticalPositionInsideBiome",
|
||||||
|
"BiomeMapGetName",
|
||||||
|
"SetRandomSeed",
|
||||||
|
"Random",
|
||||||
|
"Randomf",
|
||||||
|
"RandomDistribution",
|
||||||
|
"RandomDistributionf",
|
||||||
|
"ProceduralRandom",
|
||||||
|
"ProceduralRandomf",
|
||||||
|
"ProceduralRandomi",
|
||||||
|
"PhysicsAddBodyImage",
|
||||||
|
"PhysicsAddBodyCreateBox",
|
||||||
|
"PhysicsAddJoint",
|
||||||
|
"PhysicsApplyForce",
|
||||||
|
"PhysicsApplyTorque",
|
||||||
|
"PhysicsApplyTorqueToComponent",
|
||||||
|
"PhysicsApplyForceOnArea",
|
||||||
|
"PhysicsRemoveJoints",
|
||||||
|
"PhysicsSetStatic",
|
||||||
|
"PhysicsGetComponentVelocity",
|
||||||
|
"PhysicsGetComponentAngularVelocity",
|
||||||
|
"PhysicsComponentGetTransform",
|
||||||
|
"PhysicsComponentSetTransform",
|
||||||
|
"PhysicsBodyIDGetFromEntity",
|
||||||
|
"PhysicsBodyIDQueryBodies",
|
||||||
|
"PhysicsBodyIDGetTransform",
|
||||||
|
"PhysicsBodyIDSetTransform",
|
||||||
|
"PhysicsBodyIDApplyForce",
|
||||||
|
"PhysicsBodyIDApplyLinearImpulse",
|
||||||
|
"PhysicsBodyIDApplyTorque",
|
||||||
|
"PhysicsBodyIDGetWorldCenter",
|
||||||
|
"PhysicsBodyIDGetDamping",
|
||||||
|
"PhysicsBodyIDSetDamping",
|
||||||
|
"PhysicsBodyIDGetGravityScale",
|
||||||
|
"PhysicsBodyIDSetGravityScale",
|
||||||
|
"PhysicsBodyIDGetBodyAABB",
|
||||||
|
"PhysicsBody2InitFromComponents",
|
||||||
|
"PhysicsPosToGamePos",
|
||||||
|
"GamePosToPhysicsPos",
|
||||||
|
"PhysicsVecToGameVec",
|
||||||
|
"GameVecToPhysicsVec",
|
||||||
|
"LooseChunk",
|
||||||
|
"VerletApplyCircularForce",
|
||||||
|
"VerletApplyDirectionalForce",
|
||||||
|
"AddFlagPersistent",
|
||||||
|
"RemoveFlagPersistent",
|
||||||
|
"HasFlagPersistent",
|
||||||
|
"GameAddFlagRun",
|
||||||
|
"GameRemoveFlagRun",
|
||||||
|
"GameHasFlagRun",
|
||||||
|
"GameTriggerMusicEvent",
|
||||||
|
"GameTriggerMusicCue",
|
||||||
|
"GameTriggerMusicFadeOutAndDequeueAll",
|
||||||
|
"GamePlaySound",
|
||||||
|
"GameEntityPlaySound",
|
||||||
|
"GameEntityPlaySoundLoop",
|
||||||
|
"GameSetPostFxParameter",
|
||||||
|
"GameUnsetPostFxParameter",
|
||||||
|
"GameSetPostFxTextureParameter",
|
||||||
|
"GameUnsetPostFxTextureParameter",
|
||||||
|
"GameTextGetTranslatedOrNot",
|
||||||
|
"GameTextGet",
|
||||||
|
"GuiCreate",
|
||||||
|
"GuiDestroy",
|
||||||
|
"GuiStartFrame",
|
||||||
|
"GuiOptionsAdd",
|
||||||
|
"GuiOptionsRemove",
|
||||||
|
"GuiOptionsClear",
|
||||||
|
"GuiOptionsAddForNextWidget",
|
||||||
|
"GuiColorSetForNextWidget",
|
||||||
|
"GuiZSet",
|
||||||
|
"GuiZSetForNextWidget",
|
||||||
|
"GuiIdPush",
|
||||||
|
"GuiIdPushString",
|
||||||
|
"GuiIdPop",
|
||||||
|
"GuiAnimateBegin",
|
||||||
|
"GuiAnimateEnd",
|
||||||
|
"GuiAnimateAlphaFadeIn",
|
||||||
|
"GuiAnimateScaleIn",
|
||||||
|
"GuiText",
|
||||||
|
"GuiTextCentered",
|
||||||
|
"GuiImage",
|
||||||
|
"GuiImageNinePiece",
|
||||||
|
"GuiButton",
|
||||||
|
"GuiImageButton",
|
||||||
|
"GuiSlider",
|
||||||
|
"GuiTextInput",
|
||||||
|
"GuiBeginAutoBox",
|
||||||
|
"GuiEndAutoBoxNinePiece",
|
||||||
|
"GuiTooltip",
|
||||||
|
"GuiBeginScrollContainer",
|
||||||
|
"GuiEndScrollContainer",
|
||||||
|
"GuiLayoutBeginHorizontal",
|
||||||
|
"GuiLayoutBeginVertical",
|
||||||
|
"GuiLayoutAddHorizontalSpacing",
|
||||||
|
"GuiLayoutAddVerticalSpacing",
|
||||||
|
"GuiLayoutEnd",
|
||||||
|
"GuiLayoutBeginLayer",
|
||||||
|
"GuiLayoutEndLayer",
|
||||||
|
"GuiGetScreenDimensions",
|
||||||
|
"GuiGetTextDimensions",
|
||||||
|
"GuiGetImageDimensions",
|
||||||
|
"GuiGetPreviousWidgetInfo",
|
||||||
|
"GameIsBetaBuild",
|
||||||
|
"DebugGetIsDevBuild",
|
||||||
|
"DebugEnableTrailerMode",
|
||||||
|
"GameGetIsTrailerModeEnabled",
|
||||||
|
"Debug_SaveTestPlayer",
|
||||||
|
"DebugBiomeMapGetFilename",
|
||||||
|
"EntityConvertToMaterial",
|
||||||
|
"ConvertEverythingToGold",
|
||||||
|
"ConvertMaterialEverywhere",
|
||||||
|
"ConvertMaterialOnAreaInstantly",
|
||||||
|
"LoadRagdoll",
|
||||||
|
"GetDailyPracticeRunSeed",
|
||||||
|
"ModIsEnabled",
|
||||||
|
"ModGetActiveModIDs",
|
||||||
|
"ModGetAPIVersion",
|
||||||
|
"ModDoesFileExist",
|
||||||
|
"ModMaterialFilesGet",
|
||||||
|
"ModSettingGet",
|
||||||
|
"ModSettingSet",
|
||||||
|
"ModSettingGetNextValue",
|
||||||
|
"ModSettingSetNextValue",
|
||||||
|
"ModSettingRemove",
|
||||||
|
"ModSettingGetCount",
|
||||||
|
"ModSettingGetAtIndex",
|
||||||
|
"StreamingGetIsConnected",
|
||||||
|
"StreamingGetConnectedChannelName",
|
||||||
|
"StreamingGetVotingCycleDurationFrames",
|
||||||
|
"StreamingGetRandomViewerName",
|
||||||
|
"StreamingGetSettingsGhostsNamedAfterViewers",
|
||||||
|
"StreamingSetCustomPhaseDurations",
|
||||||
|
"StreamingForceNewVoting",
|
||||||
|
"StreamingSetVotingEnabled",
|
||||||
|
"ModLuaFileAppend",
|
||||||
|
"ModLuaFileGetAppends",
|
||||||
|
"ModLuaFileSetAppends",
|
||||||
|
"ModTextFileGetContent",
|
||||||
|
"ModTextFileSetContent",
|
||||||
|
"ModTextFileWhoSetContent",
|
||||||
|
"ModImageMakeEditable",
|
||||||
|
"ModImageIdFromFilename",
|
||||||
|
"ModImageGetPixel",
|
||||||
|
"ModImageSetPixel",
|
||||||
|
"ModImageWhoSetContent",
|
||||||
|
"ModImageDoesExist",
|
||||||
|
"ModMagicNumbersFileAdd",
|
||||||
|
"ModMaterialsFileAdd",
|
||||||
|
"ModRegisterAudioEventMappings",
|
||||||
|
"ModRegisterMusicBank",
|
||||||
|
"ModDevGenerateSpriteUVsForDirectory",
|
||||||
|
"RegisterProjectile",
|
||||||
|
"RegisterGunAction",
|
||||||
|
"RegisterGunShotEffects",
|
||||||
|
"BeginProjectile",
|
||||||
|
"EndProjectile",
|
||||||
|
"BeginTriggerTimer",
|
||||||
|
"BeginTriggerHitWorld",
|
||||||
|
"BeginTriggerDeath",
|
||||||
|
"EndTrigger",
|
||||||
|
"SetProjectileConfigs",
|
||||||
|
"StartReload",
|
||||||
|
"ActionUsesRemainingChanged",
|
||||||
|
"ActionUsed",
|
||||||
|
"LogAction",
|
||||||
|
"OnActionPlayed",
|
||||||
|
"OnNotEnoughManaForAction",
|
||||||
|
"BaabInstruction",
|
||||||
|
"SetValueNumber",
|
||||||
|
"GetValueNumber",
|
||||||
|
"SetValueInteger",
|
||||||
|
"GetValueInteger",
|
||||||
|
"SetValueBool",
|
||||||
|
"GetValueBool",
|
||||||
|
"dofile",
|
||||||
|
"dofile_once",
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#pragma once
|
||||||
|
// The entity/component part of Noita's Lua API, expressed as an interface the game world implements.
|
||||||
|
// LuaHost forwards EntityGetFirstComponent, ComponentGetValue2, EntityLoad, EntityKill, ... to it.
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entity/spawn_comp.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
// A Lua value crossing the boundary.
|
||||||
|
struct LVal {
|
||||||
|
enum class T { Nil, Bool, Num, Str } t = T::Nil;
|
||||||
|
bool b = false;
|
||||||
|
double n = 0;
|
||||||
|
std::string s;
|
||||||
|
static LVal nil() { return {}; }
|
||||||
|
static LVal boolean(bool v) { LVal r; r.t = T::Bool; r.b = v; return r; }
|
||||||
|
static LVal number(double v) { LVal r; r.t = T::Num; r.n = v; return r; }
|
||||||
|
static LVal string(std::string v) { LVal r; r.t = T::Str; r.s = std::move(v); return r; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class IEntityWorld {
|
||||||
|
public:
|
||||||
|
virtual ~IEntityWorld() = default;
|
||||||
|
|
||||||
|
// ---- entities ----
|
||||||
|
virtual int load(const std::string& path, double x, double y) = 0; // 0 on failure
|
||||||
|
virtual int load_with(const std::string& path, double x, double y, const std::vector<SpawnComp>& extra) { (void)extra; return load(path, x, y); } // the same with components merged in at the spawn ("=Type" merges into the first of that type)
|
||||||
|
virtual void load_to_entity(const std::string& path, int entity) = 0; // EntityLoadToEntity: the file's components are added to the entity (not its tags, not its children)
|
||||||
|
virtual void kill(int id) = 0;
|
||||||
|
virtual void physics_init(int /*id*/) {} // PhysicsBody2InitFromComponents
|
||||||
|
virtual bool alive(int id) = 0;
|
||||||
|
virtual bool get_transform(int id, double& x, double& y, double& rot, double& sx, double& sy) = 0;
|
||||||
|
virtual void set_transform(int id, double x, double y, double rot, double sx, double sy) = 0;
|
||||||
|
virtual std::string get_name(int id) = 0;
|
||||||
|
virtual void set_name(int id, const std::string& name) = 0;
|
||||||
|
virtual std::string get_filename(int id) = 0;
|
||||||
|
virtual bool has_tag(int id, const std::string& tag) = 0;
|
||||||
|
virtual void add_tag(int id, const std::string& tag) = 0;
|
||||||
|
virtual void remove_tag(int id, const std::string& tag) = 0;
|
||||||
|
virtual std::string get_tags(int id) = 0; // comma separated
|
||||||
|
virtual std::vector<int> with_tag(const std::string& tag) = 0; // empty tag = all
|
||||||
|
virtual std::vector<int> in_radius(double x, double y, double r, const std::string& tag) = 0;
|
||||||
|
virtual void script_query(bool on) { (void)on; } // a Lua query follows (with_tag / in_radius): the GPU-only projectiles get their twins first
|
||||||
|
virtual int parent(int id) = 0;
|
||||||
|
virtual std::vector<int> children(int id) = 0;
|
||||||
|
virtual void add_child(int parent, int child) = 0;
|
||||||
|
// A spell card inside a wand (AddGunAction / AddGunActionPermanent): a hidden child entity carrying the action id.
|
||||||
|
virtual int add_gun_action(int wand, const std::string& action_id, bool permanent) = 0;
|
||||||
|
|
||||||
|
// ---- components ----
|
||||||
|
virtual std::vector<int> components(int entity, const std::string& type, const std::string& tag, bool include_disabled) = 0;
|
||||||
|
virtual int add_component(int entity, const std::string& type, const std::map<std::string, std::string>& attrs) = 0;
|
||||||
|
virtual void remove_component(int comp) = 0;
|
||||||
|
virtual void set_component_enabled(int comp, bool enabled) = 0;
|
||||||
|
virtual bool component_enabled(int comp) = 0;
|
||||||
|
virtual std::string component_type(int comp) = 0;
|
||||||
|
virtual int component_entity(int comp) = 0;
|
||||||
|
virtual bool comp_has_tag(int comp, const std::string& tag) = 0;
|
||||||
|
virtual LVal get_value(int comp, const std::string& key, bool typed) = 0; // nil if the key is unknown
|
||||||
|
virtual void set_value(int comp, const std::string& key, const LVal& v) = 0;
|
||||||
|
|
||||||
|
// ---- context of the running script ----
|
||||||
|
virtual int updated_entity() = 0;
|
||||||
|
virtual int updated_component() = 0;
|
||||||
|
virtual int frame() = 0;
|
||||||
|
virtual std::vector<int> game_effect_components(int entity, const std::string& effect) { (void)entity; (void)effect; return {}; } // GameEffectComponents of the entity and its children (empty name = all)
|
||||||
|
virtual int load_game_effect(int entity, const std::string& effect, bool always_new, int& effect_entity) { (void)entity; (void)effect; (void)always_new; effect_entity = 0; return 0; } // GetGameEffectLoadTo: the component (and entity) of the effect on the entity, loaded when missing
|
||||||
|
virtual int load_game_effect_file(int entity, const std::string& file) { (void)entity; (void)file; return 0; } // LoadGameEffectEntityTo
|
||||||
|
virtual void shoot_projectile(int shooter, double x, double y, double tx, double ty, int entity, bool send_message = true) { (void)shooter; (void)x; (void)y; (void)tx; (void)ty; (void)entity; (void)send_message; } // GameShootProjectile
|
||||||
|
virtual void play_sound(const std::string& bank, const std::string& event, double x, double y) { (void)bank; (void)event; (void)x; (void)y; } // GamePlaySound
|
||||||
|
virtual void physics_remove_joints(double x0, double y0, double x1, double y1) { (void)x0; (void)y0; (void)x1; (void)y1; } // PhysicsRemoveJoints
|
||||||
|
virtual void screenshake(double amount, double x, double y) { (void)amount; (void)x; (void)y; } // GameScreenshake
|
||||||
|
virtual void convert_material_everywhere(int from, int to) { (void)from; (void)to; } // ConvertMaterialEverywhere (fungal shifts)
|
||||||
|
virtual void entity_play_sound(int entity, const std::string& name) { (void)entity; (void)name; } // GameEntityPlaySound: through every AudioComponent of the entity
|
||||||
|
virtual void entity_play_sound_loop(int entity, const std::string& tag, double intensity, double intensity2) { (void)entity; (void)tag; (void)intensity; (void)intensity2; } // GameEntityPlaySoundLoop
|
||||||
|
// EntityInflictDamage (007b4030 -> 00ab9740 -> TakeDamage_Impl 00ab9a90): damage of a type (a DAMAGE_* name) through the entity's enabled DamageModelComponent; the world position defaults to the entity's
|
||||||
|
virtual void inflict_damage(int entity, double amount, const std::string& type, const std::string& description, const std::string& ragdoll_fx, double impulse_x, double impulse_y, int responsible, double world_x, double world_y, double knockback) { (void)entity; (void)amount; (void)type; (void)description; (void)ragdoll_fx; (void)impulse_x; (void)impulse_y; (void)responsible; (void)world_x; (void)world_y; (void)knockback; }
|
||||||
|
virtual std::string biome_name(double x, double y) { (void)x; (void)y; return "_EMPTY_"; } // BiomeMapGetName: "$biome_coalmine"...
|
||||||
|
virtual int world_state_entity() = 0;
|
||||||
|
virtual void remove_from_parent(int id) { (void)id; } // EntityRemoveFromParent
|
||||||
|
virtual bool camera_bounds(double& x, double& y, double& w, double& h) { (void)x; (void)y; (void)w; (void)h; return false; } // GameGetCameraBounds / GameGetCameraPos
|
||||||
|
virtual void regen_item_actions(int player) { (void)player; } // GameRegenItemActionsInPlayer: every spell the player carries has its uses back
|
||||||
|
virtual int create_entity(const std::string& name) { (void)name; return 0; } // EntityCreateNew: an entity without components
|
||||||
|
virtual void comp_add_tag(int comp, const std::string& tag) { (void)comp; (void)tag; } // ComponentAddTag
|
||||||
|
virtual int herd_relation(int a, int b) { (void)a; (void)b; return 0; } // EntityGetHerdRelation
|
||||||
|
virtual int wand_capacity(int wand) { (void)wand; return 0; } // EntityGetWandCapacity: the slots without the always-cast spells
|
||||||
|
virtual void print(const std::string& title, const std::string& text, bool important) = 0;
|
||||||
|
virtual void physics_force(int entity, double fx, double fy) { (void)entity; (void)fx; (void)fy; } // PhysicsApplyForce
|
||||||
|
struct BodyInfo { int entity = 0; double mass = 1, x = 0, y = 0, vx = 0, vy = 0, angular = 0; }; // a rigid body: mass (kg), position (pixels), velocity (pixels per second)
|
||||||
|
virtual std::vector<BodyInfo> bodies_in_area(double x0, double y0, double x1, double y1) { (void)x0; (void)y0; (void)x1; (void)y1; return {}; } // PhysicsApplyForceOnArea
|
||||||
|
virtual void physics_area_force(int entity, double fx, double fy) { (void)entity; (void)fx; (void)fy; } // the force of PhysicsApplyForceOnArea's callback (newtons on the body)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
// Lua natives of the entity / component API; they forward to LuaHost::world (IEntityWorld).
|
||||||
|
#include <lua.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include "entity/material_mix.h"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "lua/entity_api.h"
|
||||||
|
#include "lua/lua_host.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
double nn(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : def; }
|
||||||
|
std::string ss(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; }
|
||||||
|
IEntityWorld* W(LuaHost& h) { return h.world; }
|
||||||
|
|
||||||
|
void push_val(lua_State* L, const LVal& v) {
|
||||||
|
switch (v.t) {
|
||||||
|
case LVal::T::Nil: lua_pushnil(L); break;
|
||||||
|
case LVal::T::Bool: lua_pushboolean(L, v.b); break;
|
||||||
|
case LVal::T::Num: lua_pushnumber(L, v.n); break;
|
||||||
|
case LVal::T::Str: lua_pushstring(L, v.s.c_str()); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LVal read_val(lua_State* L, int i) {
|
||||||
|
if (lua_isboolean(L, i)) return LVal::boolean(lua_toboolean(L, i) != 0);
|
||||||
|
if (lua_type(L, i) == LUA_TNUMBER) return LVal::number(lua_tonumber(L, i));
|
||||||
|
if (lua_isstring(L, i)) return LVal::string(lua_tostring(L, i));
|
||||||
|
return LVal::nil();
|
||||||
|
}
|
||||||
|
|
||||||
|
void push_list(lua_State* L, const std::vector<int>& v) {
|
||||||
|
lua_createtable(L, (int)v.size(), 0);
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) { lua_pushnumber(L, v[i]); lua_rawseti(L, -2, (int)i + 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- entities ----
|
||||||
|
int n_EntityLoad(LuaHost& h, lua_State* L) {
|
||||||
|
if (!W(h)) {
|
||||||
|
HostEvent e; e.kind = HostEvent::Kind::EntityLoad; e.path = ss(L, 1); e.x = nn(L, 2); e.y = nn(L, 3);
|
||||||
|
e.id = h.next_entity_id++;
|
||||||
|
lua_pushnumber(L, e.id);
|
||||||
|
h.add_event(std::move(e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, W(h)->load(ss(L, 1), nn(L, 2), nn(L, 3)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_PhysicsBody2InitFromComponents(LuaHost& h, lua_State* L) { if (W(h)) W(h)->physics_init((int)nn(L, 1)); return 0; }
|
||||||
|
int n_EntityKill(LuaHost& h, lua_State* L) { if (W(h)) W(h)->kill((int)nn(L, 1)); return 0; }
|
||||||
|
int n_EntityGetIsAlive(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->alive((int)nn(L, 1))); return 1; }
|
||||||
|
int n_EntityGetTransform(LuaHost& h, lua_State* L) {
|
||||||
|
double x = 0, y = 0, r = 0, sx = 1, sy = 1;
|
||||||
|
if (W(h)) W(h)->get_transform((int)nn(L, 1), x, y, r, sx, sy);
|
||||||
|
lua_pushnumber(L, x); lua_pushnumber(L, y); lua_pushnumber(L, r); lua_pushnumber(L, sx); lua_pushnumber(L, sy);
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
int n_EntitySetTransform(LuaHost& h, lua_State* L) {
|
||||||
|
if (!W(h)) return 0;
|
||||||
|
double x, y, r, sx, sy;
|
||||||
|
const int id = (int)nn(L, 1);
|
||||||
|
if (!W(h)->get_transform(id, x, y, r, sx, sy)) return 0;
|
||||||
|
W(h)->set_transform(id, nn(L, 2, x), nn(L, 3, y), nn(L, 4, r), nn(L, 5, sx), nn(L, 6, sy));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_EntityGetName(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->get_name((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||||
|
int n_EntitySetName(LuaHost& h, lua_State* L) { if (W(h)) W(h)->set_name((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||||
|
int n_EntityGetFilename(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->get_filename((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||||
|
int n_EntityHasTag(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->has_tag((int)nn(L, 1), ss(L, 2))); return 1; }
|
||||||
|
int n_EntityAddTag(LuaHost& h, lua_State* L) { if (W(h)) W(h)->add_tag((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||||
|
int n_EntityRemoveTag(LuaHost& h, lua_State* L) { if (W(h)) W(h)->remove_tag((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||||
|
int n_EntityGetTags(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->get_tags((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||||
|
struct ScriptQuery { // (the query is a script's: the lazy twins of the GPU projectiles are made for it)
|
||||||
|
IEntityWorld* w; explicit ScriptQuery(IEntityWorld* a) : w(a) { if (w) w->script_query(true); } ~ScriptQuery() { if (w) w->script_query(false); }
|
||||||
|
};
|
||||||
|
int n_EntityGetWithTag(LuaHost& h, lua_State* L) { ScriptQuery q(W(h)); push_list(L, W(h) ? W(h)->with_tag(ss(L, 1)) : std::vector<int>{}); return 1; }
|
||||||
|
int n_EntityGetInRadiusWithTag(LuaHost& h, lua_State* L) {
|
||||||
|
ScriptQuery q(W(h));
|
||||||
|
push_list(L, W(h) ? W(h)->in_radius(nn(L, 1), nn(L, 2), nn(L, 3), ss(L, 4)) : std::vector<int>{});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityGetInRadius(LuaHost& h, lua_State* L) {
|
||||||
|
ScriptQuery q(W(h));
|
||||||
|
push_list(L, W(h) ? W(h)->in_radius(nn(L, 1), nn(L, 2), nn(L, 3), "") : std::vector<int>{});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityGetClosestWithTag(LuaHost& h, lua_State* L) {
|
||||||
|
int best = 0;
|
||||||
|
if (W(h)) {
|
||||||
|
double bd = 1e300;
|
||||||
|
ScriptQuery q(W(h));
|
||||||
|
for (int id : W(h)->with_tag(ss(L, 3))) {
|
||||||
|
double x, y, r, sx, sy;
|
||||||
|
if (!W(h)->get_transform(id, x, y, r, sx, sy)) continue;
|
||||||
|
const double d = (x - nn(L, 1)) * (x - nn(L, 1)) + (y - nn(L, 2)) * (y - nn(L, 2));
|
||||||
|
if (d < bd) { bd = d; best = id; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, best);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityGetParent(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->parent((int)nn(L, 1)) : 0); return 1; }
|
||||||
|
int n_EntityGetRootEntity(LuaHost& h, lua_State* L) { // the topmost parent (the entity itself when it has none)
|
||||||
|
int id = (int)nn(L, 1);
|
||||||
|
for (int guard = 0; W(h) && guard < 64; ++guard) { const int p = W(h)->parent(id); if (p <= 0) break; id = p; }
|
||||||
|
lua_pushnumber(L, id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityGetAllChildren(LuaHost& h, lua_State* L) {
|
||||||
|
if (!W(h)) { lua_pushnil(L); return 1; }
|
||||||
|
auto c = W(h)->children((int)nn(L, 1));
|
||||||
|
if (c.empty()) lua_pushnil(L); else push_list(L, c);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- components ----
|
||||||
|
// The value of a component field, through the entity runtime or, during world generation, the components of a spawn (LuaHost::gen_get)
|
||||||
|
LVal gv(LuaHost& h, int cid, const std::string& key, bool typed) {
|
||||||
|
if (W(h)) return W(h)->get_value(cid, key, typed);
|
||||||
|
std::string v;
|
||||||
|
if (!h.gen_get(cid, key, v)) return LVal::nil();
|
||||||
|
if (typed) { char* end = nullptr; const double d = std::strtod(v.c_str(), &end); if (!v.empty() && end && *end == 0) return LVal::number(d); }
|
||||||
|
return LVal::string(v);
|
||||||
|
}
|
||||||
|
void sv(LuaHost& h, int cid, const std::string& key, const LVal& v) {
|
||||||
|
if (W(h)) { W(h)->set_value(cid, key, v); return; }
|
||||||
|
std::string s2;
|
||||||
|
if (v.t == LVal::T::Num) { char b[64]; std::snprintf(b, sizeof b, "%.17g", v.n); s2 = b; }
|
||||||
|
else if (v.t == LVal::T::Bool) s2 = v.b ? "1" : "0";
|
||||||
|
else s2 = v.s;
|
||||||
|
h.gen_set(cid, key, s2);
|
||||||
|
}
|
||||||
|
int comps(LuaHost& h, lua_State* L, bool first, bool include_disabled) {
|
||||||
|
std::vector<int> v;
|
||||||
|
if (W(h)) v = W(h)->components((int)nn(L, 1), ss(L, 2), ss(L, 3), include_disabled);
|
||||||
|
else v = h.gen_components((int)nn(L, 1), ss(L, 2), ss(L, 3));
|
||||||
|
if (first) { if (v.empty()) lua_pushnil(L); else lua_pushnumber(L, v.front()); return 1; }
|
||||||
|
if (v.empty()) lua_pushnil(L); else push_list(L, v);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityGetFirstComponent(LuaHost& h, lua_State* L) { return comps(h, L, true, false); }
|
||||||
|
int n_EntityGetFirstComponentIncludingDisabled(LuaHost& h, lua_State* L) { return comps(h, L, true, true); }
|
||||||
|
int n_EntityGetComponent(LuaHost& h, lua_State* L) { return comps(h, L, false, false); }
|
||||||
|
int n_EntityGetComponentIncludingDisabled(LuaHost& h, lua_State* L) { return comps(h, L, false, true); }
|
||||||
|
|
||||||
|
int add_component(LuaHost& h, lua_State* L) {
|
||||||
|
std::map<std::string, std::string> attrs;
|
||||||
|
if (lua_istable(L, 3)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, 3)) {
|
||||||
|
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||||
|
std::string v;
|
||||||
|
if (lua_isboolean(L, -1)) v = lua_toboolean(L, -1) ? "1" : "0";
|
||||||
|
else if (lua_type(L, -1) == LUA_TNUMBER) { char b[64]; std::snprintf(b, sizeof b, "%g", lua_tonumber(L, -1)); v = b; }
|
||||||
|
else if (lua_isstring(L, -1)) v = lua_tostring(L, -1);
|
||||||
|
attrs[lua_tostring(L, -2)] = v;
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!W(h)) { // world generation: the component goes with the spawn of the entity
|
||||||
|
SpawnComp sc; sc.type = ss(L, 2); sc.attrs = attrs;
|
||||||
|
if (h.add_spawn_comp((int)nn(L, 1), std::move(sc))) lua_pushnumber(L, h.next_entity_id++); else lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, W(h)->add_component((int)nn(L, 1), ss(L, 2), attrs));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_EntityAddComponent(LuaHost& h, lua_State* L) { return add_component(h, L); }
|
||||||
|
int n_EntityAddComponent2(LuaHost& h, lua_State* L) { return add_component(h, L); }
|
||||||
|
int n_EntityRemoveComponent(LuaHost& h, lua_State* L) { if (W(h)) W(h)->remove_component((int)nn(L, 2)); return 0; }
|
||||||
|
int n_EntitySetComponentIsEnabled(LuaHost& h, lua_State* L) { if (W(h)) W(h)->set_component_enabled((int)nn(L, 2), lua_toboolean(L, 3) != 0); return 0; }
|
||||||
|
int n_ComponentGetIsEnabled(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->component_enabled((int)nn(L, 1))); return 1; }
|
||||||
|
int n_ComponentGetTypeName(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->component_type((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||||
|
int n_ComponentGetEntity(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->component_entity((int)nn(L, 1)) : 0); return 1; }
|
||||||
|
int n_ComponentHasTag(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->comp_has_tag((int)nn(L, 1), ss(L, 2))); return 1; }
|
||||||
|
|
||||||
|
// ComponentGetValue: always a string ("" when unknown)
|
||||||
|
int n_ComponentGetValue(LuaHost& h, lua_State* L) {
|
||||||
|
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||||
|
lua_pushstring(L, v.t == LVal::T::Str ? v.s.c_str() : "");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// ComponentGetValue2: a vector field (vec2 x / y, ValueRange min / max: flattened as "field.x" ...) gives two values, as the engine's typed getter does
|
||||||
|
static const char* const kVecSuffix[2][2] = {{".x", ".y"}, {".min", ".max"}};
|
||||||
|
int n_ComponentGetValue2(LuaHost& h, lua_State* L) {
|
||||||
|
const int cid = (int)nn(L, 1);
|
||||||
|
const std::string key = ss(L, 2);
|
||||||
|
LVal v = gv(h, cid, key, true);
|
||||||
|
if (v.t == LVal::T::Nil) {
|
||||||
|
for (const auto& pr : kVecSuffix) {
|
||||||
|
LVal a = gv(h, cid, key + pr[0], true);
|
||||||
|
if (a.t == LVal::T::Nil) continue;
|
||||||
|
push_val(L, a); push_val(L, gv(h, cid, key + pr[1], true));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (key == "mVelocity") { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } // (not written yet: at rest)
|
||||||
|
}
|
||||||
|
push_val(L, v);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentGetValueInt(LuaHost& h, lua_State* L) {
|
||||||
|
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||||
|
lua_pushnumber(L, v.t == LVal::T::Str ? std::floor(std::atof(v.s.c_str())) : 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentGetValueFloat(LuaHost& h, lua_State* L) {
|
||||||
|
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||||
|
lua_pushnumber(L, v.t == LVal::T::Str ? std::atof(v.s.c_str()) : 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentGetValueBool(LuaHost& h, lua_State* L) {
|
||||||
|
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||||
|
lua_pushboolean(L, v.t == LVal::T::Str && (v.s == "1" || v.s == "true"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentSetValue(LuaHost& h, lua_State* L) {
|
||||||
|
sv(h, (int)nn(L, 1), ss(L, 2), LVal::string(ss(L, 3)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_ComponentSetValue2(LuaHost& h, lua_State* L) {
|
||||||
|
const int cid = (int)nn(L, 1);
|
||||||
|
const std::string key = ss(L, 2);
|
||||||
|
if (lua_gettop(L) >= 4 && gv(h, cid, key, true).t == LVal::T::Nil) { // a vector field: two values (x / y, or min / max of a ValueRange)
|
||||||
|
const bool range = gv(h, cid, key + ".min", true).t != LVal::T::Nil;
|
||||||
|
sv(h, cid, key + (range ? ".min" : ".x"), read_val(L, 3));
|
||||||
|
sv(h, cid, key + (range ? ".max" : ".y"), read_val(L, 4));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
sv(h, cid, key, read_val(L, 3));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// ComponentObjectGetValue(comp, object, field): objects are flattened as "object.field"
|
||||||
|
int n_ComponentObjectGetValue(LuaHost& h, lua_State* L) {
|
||||||
|
LVal v = W(h) ? W(h)->get_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), false) : LVal::nil();
|
||||||
|
lua_pushstring(L, v.t == LVal::T::Str ? v.s.c_str() : "");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentObjectGetValue2(LuaHost& h, lua_State* L) {
|
||||||
|
push_val(L, W(h) ? W(h)->get_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), true) : LVal::nil());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_ComponentObjectSetValue(LuaHost& h, lua_State* L) {
|
||||||
|
if (W(h)) W(h)->set_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), LVal::string(ss(L, 4)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_ComponentObjectSetValue2(LuaHost& h, lua_State* L) {
|
||||||
|
if (W(h)) W(h)->set_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), read_val(L, 4));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n_EntitySetComponentsWithTagEnabled(LuaHost& h, lua_State* L) {
|
||||||
|
if (!W(h)) return 0;
|
||||||
|
for (int c : W(h)->components((int)nn(L, 1), "", ss(L, 2), true)) W(h)->set_component_enabled(c, lua_toboolean(L, 3) != 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// vectors are stored as "x,y"
|
||||||
|
int n_ComponentSetValueVector2(LuaHost& h, lua_State* L) { // the field's x / y
|
||||||
|
sv(h, (int)nn(L, 1), ss(L, 2) + ".x", LVal::number(nn(L, 3))); sv(h, (int)nn(L, 1), ss(L, 2) + ".y", LVal::number(nn(L, 4)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_ComponentGetValueVector2(LuaHost& h, lua_State* L) {
|
||||||
|
double a = 0, b = 0;
|
||||||
|
if (W(h)) {
|
||||||
|
const LVal x = W(h)->get_value((int)nn(L, 1), ss(L, 2) + ".x", true), y = W(h)->get_value((int)nn(L, 1), ss(L, 2) + ".y", true);
|
||||||
|
if (x.t == LVal::T::Num) a = x.n;
|
||||||
|
if (y.t == LVal::T::Num) b = y.n;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, a); lua_pushnumber(L, b);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n_EntityAddChild(LuaHost& h, lua_State* L) { if (W(h)) W(h)->add_child((int)nn(L, 1), (int)nn(L, 2)); return 0; }
|
||||||
|
int n_AddGunAction(LuaHost& h, lua_State* L) { if (std::getenv("NG_WANDDBG")) std::printf("AddGunAction(%d, %s)%c", (int)nn(L, 1), lua_isstring(L, 2) ? lua_tostring(L, 2) : "<nil>", 10); if (W(h)) W(h)->add_gun_action((int)nn(L, 1), ss(L, 2), false); return 0; }
|
||||||
|
int n_AddGunActionPermanent(LuaHost& h, lua_State* L) { if (W(h)) W(h)->add_gun_action((int)nn(L, 1), ss(L, 2), true); return 0; }
|
||||||
|
int n_IsPlayer(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->has_tag((int)nn(L, 1), "player_unit")); return 1; }
|
||||||
|
int n_GameGetPotionColorUint(LuaHost&, lua_State* L) { lua_pushnumber(L, 0xFF4080C0); return 1; }
|
||||||
|
// The contents of a flask or barrel: the first MaterialInventoryComponent holds one main material (count_per_material_type.Material.material / .count).
|
||||||
|
int n_AddMaterialInventoryMaterial(LuaHost& h, lua_State* L) {
|
||||||
|
if (!W(h)) return 0;
|
||||||
|
auto c = W(h)->components((int)nn(L, 1), "MaterialInventoryComponent", "", true);
|
||||||
|
if (c.empty()) return 0;
|
||||||
|
auto parts = mix_get(*W(h), c.front());
|
||||||
|
mix_add(parts, ss(L, 2), nn(L, 3));
|
||||||
|
mix_set(*W(h), c.front(), parts);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GetMaterialInventoryMainMaterial(LuaHost& h, lua_State* L) { // (entity, ignore_box2d_materials = true) -> the id of the largest part of the first MaterialInventoryComponent, 0 if none
|
||||||
|
int best = 0;
|
||||||
|
if (W(h) && h.material_type) {
|
||||||
|
const bool skipBox2d = lua_gettop(L) < 2 || lua_toboolean(L, 2);
|
||||||
|
auto c = W(h)->components((int)nn(L, 1), "MaterialInventoryComponent", "", true);
|
||||||
|
if (!c.empty()) {
|
||||||
|
const auto list = h.material_list ? h.material_list() : decltype(h.material_list()){};
|
||||||
|
double most = 0;
|
||||||
|
for (const MixPart& pt : mix_get(*W(h), c.front())) {
|
||||||
|
const int id = h.material_type(pt.mat);
|
||||||
|
if (id <= 0 || pt.n <= most) continue;
|
||||||
|
if (skipBox2d && id < (int)list.size() && std::find(list[id].tags.begin(), list[id].tags.end(), "box2d") != list[id].tags.end()) continue;
|
||||||
|
most = pt.n; best = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, best);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_StatsGlobalGetValue(LuaHost&, lua_State* L) { const char* k = lua_tostring(L, 1); lua_pushstring(L, k && std::string(k) == "death_count" ? std::to_string(lua_death_count()).c_str() : "0"); return 1; }
|
||||||
|
int n_PhysicsApplyForce(LuaHost& h, lua_State* L) { if (W(h)) W(h)->physics_force((int)nn(L, 1), nn(L, 2), nn(L, 3)); return 0; }
|
||||||
|
int n_GameVecToPhysicsVec(LuaHost&, lua_State* L) { lua_pushnumber(L, nn(L, 1) * 0.1); lua_pushnumber(L, nn(L, 2) * 0.1); return 2; } // 10 pixels per physics unit
|
||||||
|
// the action's custom_xml_file (gun_actions.lua): the engine loads that card file instead of the generic one (it has the spell's sprite and its extras)
|
||||||
|
static std::string action_card_file(lua_State* L, const std::string& id) {
|
||||||
|
std::string file;
|
||||||
|
lua_getglobal(L, "actions");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
const size_t n = lua_objlen(L, -1);
|
||||||
|
for (size_t i = 1; i <= n && file.empty(); ++i) {
|
||||||
|
lua_rawgeti(L, -1, (int)i);
|
||||||
|
lua_getfield(L, -1, "id");
|
||||||
|
const bool match = lua_isstring(L, -1) && id == lua_tostring(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (match) { lua_getfield(L, -1, "custom_xml_file"); if (lua_isstring(L, -1)) file = lua_tostring(L, -1); lua_pop(L, 1); }
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return file.empty() ? std::string("data/entities/misc/custom_cards/action.xml") : file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A spell card lying in the world: the action's card file (custom_xml_file, else the generic card) carrying the action id.
|
||||||
|
int n_CreateItemActionEntity(LuaHost& h, lua_State* L) {
|
||||||
|
const std::string cardFile = action_card_file(L, ss(L, 1));
|
||||||
|
if (!W(h)) { // world generation (the shop spells of a holy mountain): the card is loaded with its action, merged into the card's ItemActionComponent ("=" spawn component)
|
||||||
|
HostEvent e; e.kind = HostEvent::Kind::EntityLoad; e.path = cardFile; e.x = nn(L, 2); e.y = nn(L, 3);
|
||||||
|
e.id = h.next_entity_id++;
|
||||||
|
const int id = e.id;
|
||||||
|
h.add_event(std::move(e));
|
||||||
|
SpawnComp sc; sc.type = "=ItemActionComponent"; sc.attrs["action_id"] = ss(L, 1);
|
||||||
|
h.add_spawn_comp(id, std::move(sc));
|
||||||
|
lua_pushnumber(L, id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::vector<SpawnComp> extra(1); // (the action is the card's from its spawn on: its sprite is chosen then)
|
||||||
|
extra[0].type = "=ItemActionComponent"; extra[0].attrs["action_id"] = ss(L, 1);
|
||||||
|
const int id = W(h)->load_with(cardFile, nn(L, 2), nn(L, 3), extra);
|
||||||
|
if (id) {
|
||||||
|
auto c = W(h)->components(id, "ItemActionComponent", "", true);
|
||||||
|
const int comp = c.empty() ? W(h)->add_component(id, "ItemActionComponent", {}) : c.front();
|
||||||
|
W(h)->set_value(comp, "action_id", LVal::string(ss(L, 1)));
|
||||||
|
W(h)->set_name(id, ss(L, 1));
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- context ----
|
||||||
|
int n_GetUpdatedEntityID(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->updated_entity() : 0); return 1; }
|
||||||
|
int n_GetUpdatedComponentID(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->updated_component() : 0); return 1; }
|
||||||
|
int n_GameGetFrameNum(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->frame() : 0); return 1; }
|
||||||
|
int n_BiomeMapGetName(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->biome_name(lua_tonumber(L, 1), lua_tonumber(L, 2)).c_str() : "_EMPTY_"); return 1; }
|
||||||
|
// 007b3c30: send_message (default true) - false: no Message_Shot, the shooter's script_shot scripts do not see it; the 8th argument (verlet_parent_entity) is looked up and not used
|
||||||
|
int n_GameShootProjectile(LuaHost& h, lua_State* L) { if (W(h)) W(h)->shoot_projectile((int)lua_tonumber(L, 1), lua_tonumber(L, 2), lua_tonumber(L, 3), lua_tonumber(L, 4), lua_tonumber(L, 5), (int)lua_tonumber(L, 6), lua_gettop(L) < 7 || lua_toboolean(L, 7) != 0); return 0; }
|
||||||
|
int n_GetGameEffectLoadTo(LuaHost& h, lua_State* L) { // ( entity, effect name, always_load_new ) -> component id, entity id
|
||||||
|
int fx_entity = 0;
|
||||||
|
const int cid = W(h) ? W(h)->load_game_effect((int)nn(L, 1), ss(L, 2), lua_toboolean(L, 3) != 0, fx_entity) : 0;
|
||||||
|
if (cid) lua_pushnumber(L, cid); else lua_pushnil(L);
|
||||||
|
lua_pushnumber(L, fx_entity);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_LoadGameEffectEntityTo(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->load_game_effect_file((int)nn(L, 1), ss(L, 2)) : 0); return 1; }
|
||||||
|
int n_GameGetGameEffect(LuaHost& h, lua_State* L) { // ( entity, effect name ) -> component id or 0
|
||||||
|
const std::vector<int> v = W(h) ? W(h)->game_effect_components((int)nn(L, 1), ss(L, 2)) : std::vector<int>();
|
||||||
|
lua_pushnumber(L, v.empty() ? 0 : v.front());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int n_GameGetWorldStateEntity(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->world_state_entity() : 0); return 1; }
|
||||||
|
int n_PhysicsRemoveJoints(LuaHost& h, lua_State* L) { if (W(h)) W(h)->physics_remove_joints(nn(L, 1), nn(L, 2), nn(L, 3), nn(L, 4)); return 0; } // ( world_pos_min_x, world_pos_min_y, world_pos_max_x, world_pos_max_y )
|
||||||
|
int n_ConvertMaterialEverywhere(LuaHost& h, lua_State* L) { if (W(h)) W(h)->convert_material_everywhere((int)nn(L, 1), (int)nn(L, 2)); return 0; } // ( material_from_type, material_to_type )
|
||||||
|
int n_GameScreenshake(LuaHost& h, lua_State* L) { if (W(h)) W(h)->screenshake(nn(L, 1), lua_isnumber(L, 2) ? lua_tonumber(L, 2) : 0.0, lua_isnumber(L, 3) ? lua_tonumber(L, 3) : 0.0); return 0; } // ( strength, x = 0, y = 0 )
|
||||||
|
int n_GamePlaySound(LuaHost& h, lua_State* L) { if (W(h)) W(h)->play_sound(ss(L, 1), ss(L, 2), lua_tonumber(L, 3), lua_tonumber(L, 4)); return 0; }
|
||||||
|
int n_GameEntityPlaySound(LuaHost& h, lua_State* L) { if (W(h)) W(h)->entity_play_sound((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||||
|
int n_GameEntityPlaySoundLoop(LuaHost& h, lua_State* L) { if (W(h)) W(h)->entity_play_sound_loop((int)nn(L, 1), ss(L, 2), lua_tonumber(L, 3), lua_isnumber(L, 4) ? lua_tonumber(L, 4) : 0.0); return 0; }
|
||||||
|
// EntityInflictDamage( entity, amount, damage_type, description, ragdoll_fx, impulse_x, impulse_y, entity_who_is_responsible = 0, world_pos_x = entity_x, world_pos_y = entity_y, knockback_force = 0 )
|
||||||
|
// (007b4030: fewer than 7 arguments is an error and does nothing; the optional ones count by lua_gettop)
|
||||||
|
int n_EntityInflictDamage(LuaHost& h, lua_State* L) {
|
||||||
|
const int n = lua_gettop(L);
|
||||||
|
if (n < 7 || !W(h)) return 0;
|
||||||
|
W(h)->inflict_damage((int)nn(L, 1), lua_tonumber(L, 2), ss(L, 3), ss(L, 4), ss(L, 5), lua_tonumber(L, 6), lua_tonumber(L, 7), n >= 8 ? (int)lua_tointeger(L, 8) : 0,
|
||||||
|
n >= 9 ? lua_tonumber(L, 9) : 0.0, n >= 10 ? lua_tonumber(L, 10) : 0.0, n >= 11 ? lua_tonumber(L, 11) : 0.0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GamePrint(LuaHost& h, lua_State* L) { if (W(h)) W(h)->print(ss(L, 1), "", false); return 0; }
|
||||||
|
int n_GamePrintImportant(LuaHost& h, lua_State* L) { if (W(h)) W(h)->print(ss(L, 1), ss(L, 2), true); return 0; }
|
||||||
|
int n_GameGetGameEffectCount(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? (double)W(h)->game_effect_components((int)nn(L, 1), ss(L, 2)).size() : 0.0); return 1; }
|
||||||
|
// GameTextGet("$key", args...): no translation files in data.wak, so the key itself (readable form) is returned.
|
||||||
|
int n_GameTextGet(LuaHost& h, lua_State* L) { // the English text of the key; $0.. $9 are the arguments (translated themselves when they are keys)
|
||||||
|
const std::string key = ss(L, 1);
|
||||||
|
std::string t = key.empty() || key[0] != '$' ? key : h.translate(key);
|
||||||
|
if (t.empty()) t = key;
|
||||||
|
for (int a = 0; a < 10; ++a) {
|
||||||
|
const std::string tag = "$" + std::to_string(a);
|
||||||
|
std::string arg = ss(L, 2 + a);
|
||||||
|
if (!arg.empty() && arg[0] == '$') { const std::string& tr = h.translate(arg); if (!tr.empty()) arg = tr; }
|
||||||
|
for (size_t p = 0; (p = t.find(tag, p)) != std::string::npos;) { t.replace(p, tag.size(), arg); p += arg.size(); }
|
||||||
|
}
|
||||||
|
lua_pushstring(L, t.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the centre of the first hitbox of an entity (its position when it has none)
|
||||||
|
int n_EntityGetFirstHitboxCenter(LuaHost& h, lua_State* L) {
|
||||||
|
double x = 0, y = 0, r = 0, sx = 1, sy = 1;
|
||||||
|
const int id = (int)nn(L, 1);
|
||||||
|
if (!W(h) || !W(h)->get_transform(id, x, y, r, sx, sy)) { lua_pushnil(L); return 1; }
|
||||||
|
for (int c : W(h)->components(id, "HitboxComponent", "", false)) {
|
||||||
|
auto g = [&](const char* k) { const LVal v = W(h)->get_value(c, k, false); return v.t == LVal::T::Str ? std::atof(v.s.c_str()) : 0.0; };
|
||||||
|
lua_pushnumber(L, x + (g("aabb_min_x") + g("aabb_max_x")) * 0.5 * sx);
|
||||||
|
lua_pushnumber(L, y + (g("aabb_min_y") + g("aabb_max_y")) * 0.5 * sy);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, x); lua_pushnumber(L, y);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_EntityGetAllComponents(LuaHost& h, lua_State* L) { push_list(L, W(h) ? W(h)->components((int)nn(L, 1), "", "", true) : std::vector<int>()); return 1; }
|
||||||
|
int n_EntityRemoveFromParent(LuaHost& h, lua_State* L) { if (W(h)) W(h)->remove_from_parent((int)nn(L, 1)); return 0; }
|
||||||
|
int n_GameGetCameraBounds(LuaHost& h, lua_State* L) {
|
||||||
|
double x = 0, y = 0, w = 0, ht = 0;
|
||||||
|
if (W(h)) W(h)->camera_bounds(x, y, w, ht);
|
||||||
|
lua_pushnumber(L, x); lua_pushnumber(L, y); lua_pushnumber(L, w); lua_pushnumber(L, ht); // GameGetCameraBounds() -> x, y, w, h (007aea80)
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
// GameGetVelocityCompVelocity( entity ) -> vx, vy: VelocityComponent.mVelocity of the entity (pixels per second)
|
||||||
|
int n_GameGetVelocityCompVelocity(LuaHost& h, lua_State* L) {
|
||||||
|
double vx = 0, vy = 0;
|
||||||
|
if (W(h)) for (int c : W(h)->components((int)nn(L, 1), "VelocityComponent", "", true)) {
|
||||||
|
const LVal x = W(h)->get_value(c, "mVelocity.x", true), y = W(h)->get_value(c, "mVelocity.y", true);
|
||||||
|
if (x.t == LVal::T::Num) vx = x.n;
|
||||||
|
if (y.t == LVal::T::Num) vy = y.n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, vx); lua_pushnumber(L, vy);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
// PhysicsApplyForceOnArea( calculate_force_for_body, entity_id, x0, y0, x1, y1 ): the callback gets (entity, mass, x, y, vel_x, vel_y, vel_angular) for every rigid body in the area and
|
||||||
|
// returns force_pos_x, force_pos_y, force_x, force_y, force_angular; the force is applied to the body (black_hole_gravity.lua)
|
||||||
|
int n_PhysicsApplyForceOnArea(LuaHost& h, lua_State* L) {
|
||||||
|
if (!W(h) || !lua_isfunction(L, 1)) return 0;
|
||||||
|
const double x0 = nn(L, 3), y0 = nn(L, 4), x1 = nn(L, 5), y1 = nn(L, 6);
|
||||||
|
for (const IEntityWorld::BodyInfo& b : W(h)->bodies_in_area(x0, y0, x1, y1)) {
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_pushnumber(L, b.entity); lua_pushnumber(L, b.mass); lua_pushnumber(L, b.x); lua_pushnumber(L, b.y); lua_pushnumber(L, b.vx); lua_pushnumber(L, b.vy); lua_pushnumber(L, b.angular);
|
||||||
|
if (lua_pcall(L, 7, 5, 0) != 0) { lua_pop(L, 1); continue; }
|
||||||
|
const double fx = lua_isnumber(L, -3) ? lua_tonumber(L, -3) : 0.0, fy = lua_isnumber(L, -2) ? lua_tonumber(L, -2) : 0.0;
|
||||||
|
lua_pop(L, 5);
|
||||||
|
if (fx != 0.0 || fy != 0.0) W(h)->physics_area_force(b.entity, fx, fy);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int n_GameGetCameraPos(LuaHost& h, lua_State* L) {
|
||||||
|
double x = 0, y = 0, w = 0, ht = 0;
|
||||||
|
if (W(h)) W(h)->camera_bounds(x, y, w, ht);
|
||||||
|
lua_pushnumber(L, x + w * 0.5); lua_pushnumber(L, y + ht * 0.5);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int n_GameRegenItemActionsInPlayer(LuaHost& h, lua_State* L) { if (W(h)) W(h)->regen_item_actions((int)nn(L, 1)); return 0; }
|
||||||
|
int n_PhysicsGetComponentVelocity(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } // (the velocity of a body lives on the GPU)
|
||||||
|
int n_EntityCreateNew(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->create_entity(ss(L, 1)) : 0); return 1; }
|
||||||
|
int n_ComponentAddTag(LuaHost& h, lua_State* L) { if (W(h)) W(h)->comp_add_tag((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||||
|
int n_EntityGetHerdRelation(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->herd_relation((int)nn(L, 1), (int)nn(L, 2)) : 0); return 1; }
|
||||||
|
int n_EntityGetWandCapacity(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->wand_capacity((int)nn(L, 1)) : 0); return 1; }
|
||||||
|
int n_GameGetIsTrailerModeEnabled(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; }
|
||||||
|
int n_EntityLoadToEntity(LuaHost& h, lua_State* L) { // the file's components are added to the entity (its tags are not)
|
||||||
|
if (W(h)) W(h)->load_to_entity(ss(L, 1), (int)nn(L, 2));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void register_entity_natives(LuaHost& h) {
|
||||||
|
struct { const char* name; NativeFn fn; } natives[] = {
|
||||||
|
{"EntityLoad", n_EntityLoad}, {"EntityLoadCameraBound", n_EntityLoad}, {"EntityLoadEndGameItem", n_EntityLoad},
|
||||||
|
{"GameGetVelocityCompVelocity", n_GameGetVelocityCompVelocity}, {"PhysicsApplyForceOnArea", n_PhysicsApplyForceOnArea}, {"EntityKill", n_EntityKill}, {"PhysicsBody2InitFromComponents", n_PhysicsBody2InitFromComponents}, {"EntityGetIsAlive", n_EntityGetIsAlive},
|
||||||
|
{"EntityGetTransform", n_EntityGetTransform}, {"EntitySetTransform", n_EntitySetTransform}, {"EntityApplyTransform", n_EntitySetTransform},
|
||||||
|
{"EntityGetName", n_EntityGetName}, {"EntitySetName", n_EntitySetName}, {"EntityGetFilename", n_EntityGetFilename},
|
||||||
|
{"EntityHasTag", n_EntityHasTag}, {"EntityAddTag", n_EntityAddTag}, {"EntityRemoveTag", n_EntityRemoveTag}, {"EntityGetTags", n_EntityGetTags},
|
||||||
|
{"EntityGetWithTag", n_EntityGetWithTag}, {"EntityGetInRadiusWithTag", n_EntityGetInRadiusWithTag}, {"EntityGetInRadius", n_EntityGetInRadius},
|
||||||
|
{"EntityGetClosestWithTag", n_EntityGetClosestWithTag}, {"EntityGetParent", n_EntityGetParent}, {"EntityGetRootEntity", n_EntityGetRootEntity},
|
||||||
|
{"EntityGetAllChildren", n_EntityGetAllChildren},
|
||||||
|
{"EntityGetFirstComponent", n_EntityGetFirstComponent}, {"EntityGetFirstComponentIncludingDisabled", n_EntityGetFirstComponentIncludingDisabled},
|
||||||
|
{"EntityGetComponent", n_EntityGetComponent}, {"EntityGetComponentIncludingDisabled", n_EntityGetComponentIncludingDisabled},
|
||||||
|
{"EntityAddComponent", n_EntityAddComponent}, {"EntityAddComponent2", n_EntityAddComponent2}, {"EntityRemoveComponent", n_EntityRemoveComponent},
|
||||||
|
{"EntitySetComponentIsEnabled", n_EntitySetComponentIsEnabled}, {"ComponentGetIsEnabled", n_ComponentGetIsEnabled},
|
||||||
|
{"ComponentGetTypeName", n_ComponentGetTypeName}, {"ComponentGetEntity", n_ComponentGetEntity}, {"ComponentHasTag", n_ComponentHasTag},
|
||||||
|
{"ComponentGetValue", n_ComponentGetValue}, {"ComponentGetValue2", n_ComponentGetValue2}, {"ComponentGetValueInt", n_ComponentGetValueInt},
|
||||||
|
{"ComponentGetValueFloat", n_ComponentGetValueFloat}, {"ComponentGetValueBool", n_ComponentGetValueBool},
|
||||||
|
{"ComponentSetValue", n_ComponentSetValue}, {"ComponentSetValue2", n_ComponentSetValue2},
|
||||||
|
{"ComponentObjectGetValue", n_ComponentObjectGetValue}, {"ComponentObjectGetValue2", n_ComponentObjectGetValue2},
|
||||||
|
{"ComponentObjectSetValue", n_ComponentObjectSetValue}, {"ComponentObjectSetValue2", n_ComponentObjectSetValue2},
|
||||||
|
{"AddGunAction", n_AddGunAction}, {"AddGunActionPermanent", n_AddGunActionPermanent}, {"EntityAddChild", n_EntityAddChild}, {"IsPlayer", n_IsPlayer}, {"GameGetPotionColorUint", n_GameGetPotionColorUint},
|
||||||
|
{"CreateItemActionEntity", n_CreateItemActionEntity}, {"AddMaterialInventoryMaterial", n_AddMaterialInventoryMaterial}, {"GetMaterialInventoryMainMaterial", n_GetMaterialInventoryMainMaterial}, {"StatsGlobalGetValue", n_StatsGlobalGetValue}, {"PhysicsApplyForce", n_PhysicsApplyForce}, {"GameVecToPhysicsVec", n_GameVecToPhysicsVec},
|
||||||
|
{"EntitySetComponentsWithTagEnabled", n_EntitySetComponentsWithTagEnabled}, {"ComponentSetValueVector2", n_ComponentSetValueVector2},
|
||||||
|
{"ComponentGetValueVector2", n_ComponentGetValueVector2},
|
||||||
|
{"GetUpdatedEntityID", n_GetUpdatedEntityID}, {"GetUpdatedComponentID", n_GetUpdatedComponentID}, {"GameGetFrameNum", n_GameGetFrameNum},
|
||||||
|
{"GameGetWorldStateEntity", n_GameGetWorldStateEntity}, {"BiomeMapGetName", n_BiomeMapGetName}, {"GameShootProjectile", n_GameShootProjectile}, {"GetGameEffectLoadTo", n_GetGameEffectLoadTo}, {"LoadGameEffectEntityTo", n_LoadGameEffectEntityTo}, {"GameGetGameEffect", n_GameGetGameEffect}, {"GamePrint", n_GamePrint}, {"GamePlaySound", n_GamePlaySound}, {"PhysicsRemoveJoints", n_PhysicsRemoveJoints}, {"GameScreenshake", n_GameScreenshake}, {"ConvertMaterialEverywhere", n_ConvertMaterialEverywhere}, {"GameEntityPlaySound", n_GameEntityPlaySound}, {"GameEntityPlaySoundLoop", n_GameEntityPlaySoundLoop}, {"EntityInflictDamage", n_EntityInflictDamage}, {"GamePrintImportant", n_GamePrintImportant},
|
||||||
|
{"EntityGetFirstHitboxCenter", n_EntityGetFirstHitboxCenter}, {"EntityGetAllComponents", n_EntityGetAllComponents}, {"GameRegenItemActionsInPlayer", n_GameRegenItemActionsInPlayer}, {"PhysicsGetComponentVelocity", n_PhysicsGetComponentVelocity}, {"EntityRemoveFromParent", n_EntityRemoveFromParent}, {"GameGetCameraBounds", n_GameGetCameraBounds}, {"GameGetCameraPos", n_GameGetCameraPos}, {"EntityCreateNew", n_EntityCreateNew}, {"ComponentAddTag", n_ComponentAddTag}, {"EntityGetHerdRelation", n_EntityGetHerdRelation}, {"EntityGetWandCapacity", n_EntityGetWandCapacity}, {"GameGetIsTrailerModeEnabled", n_GameGetIsTrailerModeEnabled}, {"EntityLoadToEntity", n_EntityLoadToEntity}, {"ComponentGetMetaCustom", n_ComponentGetValue}, {"ComponentSetMetaCustom", n_ComponentSetValue},
|
||||||
|
{"GameGetGameEffectCount", n_GameGetGameEffectCount}, {"GameTextGet", n_GameTextGet}, {"GameTextGetTranslatedOrNot", n_GameTextGet},
|
||||||
|
};
|
||||||
|
for (auto& n : natives) h.register_native(n.name, n.fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ng
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
#define NOMINMAX
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#include "lua/gpu_lua.h"
|
||||||
|
#include "lua/gpu_lua_profiles.h"
|
||||||
|
#include "gpu_lua_sources.h"
|
||||||
|
#include <shaderc/shaderc.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
// This target deliberately uses the exact vendored LuaJIT compiler/bytecode.
|
||||||
|
// No source is evaluated: luaL_loadbuffer only parses and creates a prototype.
|
||||||
|
extern "C" {
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lua.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "lua/gpu_lua_proto.h"
|
||||||
|
|
||||||
|
namespace ng::gpu_lua {
|
||||||
|
uint64_t fingerprint(const unsigned char* data, std::size_t size) {
|
||||||
|
uint64_t h=14695981039346656037ull;
|
||||||
|
for(std::size_t i=0;i<size;++i)if(data[i]!=13)h=(h^data[i])*1099511628211ull;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
namespace {
|
||||||
|
const char* const library="data/scripts/lib/utilities.lua";
|
||||||
|
const std::map<std::string,unsigned> apis={
|
||||||
|
{"GetUpdatedEntityID",1},{"EntityGetTransform",2},{"EntityGetFirstComponent",3},
|
||||||
|
{"ComponentGetValue2",4},{"ComponentSetValue2",5},{"dofile_once",6},{"dofile",7},
|
||||||
|
{"RaytraceSurfaces",8},{"EntityApplyTransform",9},
|
||||||
|
{"ComponentGetValueVector2",4},{"ComponentSetValueVector2",5}};
|
||||||
|
const std::map<std::string,unsigned> math_apis={
|
||||||
|
{"abs",10},{"sqrt",11},{"atan2",12},{"cos",13},{"sin",14},{"max",15}};
|
||||||
|
using GCproto=NgLuaProto;
|
||||||
|
using BCIns=NgLuaInstruction;
|
||||||
|
static NgLuaOp bc_op(BCIns i){return i.op;}
|
||||||
|
static unsigned bc_a(BCIns i){return i.a;}
|
||||||
|
static unsigned bc_b(BCIns i){return i.b;}
|
||||||
|
static unsigned bc_c(BCIns i){return i.c;}
|
||||||
|
static unsigned bc_d(BCIns i){return i.d;}
|
||||||
|
static int bc_j(BCIns i){return i.jump;}
|
||||||
|
struct Instructions {GCproto* p;BCIns operator[](unsigned i){return ng_lua_instruction(p,i);}};
|
||||||
|
static Instructions proto_bc(GCproto* p){return {p};}
|
||||||
|
std::string number(double n) {
|
||||||
|
const float f=static_cast<float>(n);uint32_t bits;std::memcpy(&bits,&f,4);
|
||||||
|
std::ostringstream out;out<<"uintBitsToFloat(0x"<<std::hex<<bits<<"u)";return out.str();
|
||||||
|
}
|
||||||
|
struct Parsed {
|
||||||
|
std::unique_ptr<lua_State,decltype(&lua_close)> state{luaL_newstate(),lua_close};
|
||||||
|
GCproto view{};GCproto* pt=&view;
|
||||||
|
explicit Parsed(const std::string& source,const std::string& path) {
|
||||||
|
if(!state)throw std::runtime_error("Lua GPU: cannot allocate parser");
|
||||||
|
if(source.size()>1024*1024)throw std::runtime_error(path+": Lua GPU source limit exceeded");
|
||||||
|
if(!source.empty()&&static_cast<unsigned char>(source[0])==0x1b)throw std::runtime_error(path+": Lua source required, bytecode input rejected");
|
||||||
|
if(luaL_loadbuffer(state.get(),source.data(),source.size(),path.c_str())!=0)
|
||||||
|
throw std::runtime_error(lua_tostring(state.get(),-1));
|
||||||
|
// Check the linked VM's frame ABI before dereferencing private structures.
|
||||||
|
std::vector<unsigned char> dump;
|
||||||
|
lua_dump(state.get(),[](lua_State*,const void* p,size_t n,void* dst)->int {
|
||||||
|
auto& b=*static_cast<std::vector<unsigned char>*>(dst);
|
||||||
|
b.insert(b.end(),static_cast<const unsigned char*>(p),static_cast<const unsigned char*>(p)+n);return 0;
|
||||||
|
},&dump);
|
||||||
|
if(dump.size()<5||dump[3]!=2||bool(dump[4]&8)!=bool(ng_lua_frame_shift()))throw std::runtime_error("Lua GPU: linked LuaJIT bytecode ABI mismatch");
|
||||||
|
if(!ng_lua_view(state.get(),pt))throw std::runtime_error("Lua GPU: expected Lua prototype");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
uint64_t compiled_fingerprint(Parsed& parsed) {
|
||||||
|
uint64_t digest=14695981039346656037ull;
|
||||||
|
const int status=ng_lua_dump_stripped(parsed.state.get(),parsed.pt,
|
||||||
|
[](lua_State*,const void* bytes,size_t size,void* context)->int {
|
||||||
|
auto& hash=*static_cast<uint64_t*>(context);
|
||||||
|
const auto* data=static_cast<const unsigned char*>(bytes);
|
||||||
|
// Bytecode is binary: CR bytes must participate in this digest.
|
||||||
|
for(size_t i=0;i<size;++i)hash=(hash^data[i])*1099511628211ull;
|
||||||
|
return 0;
|
||||||
|
},&digest);
|
||||||
|
if(status)throw std::runtime_error("Lua GPU: cannot fingerprint compiled library");
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
unsigned line(GCproto* pt,unsigned ip){return ng_lua_instruction(pt,ip).line;}
|
||||||
|
std::string str(GCproto* pt,unsigned d) {
|
||||||
|
size_t length=0;const char* bytes=ng_lua_string(pt,d,&length);
|
||||||
|
if(!bytes)throw std::runtime_error("Lua GPU: invalid string constant");
|
||||||
|
std::string result(bytes,length);
|
||||||
|
if(std::any_of(result.begin(),result.end(),[](unsigned char c){return c>=128;}))throw std::runtime_error("Lua GPU: ASCII strings required");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
double numeric(GCproto* pt,unsigned d) {
|
||||||
|
if(d>=pt->sizekn)throw std::runtime_error("Lua GPU: invalid numeric constant");
|
||||||
|
return ng_lua_number(pt,d);
|
||||||
|
}
|
||||||
|
struct Generator {
|
||||||
|
std::string path;
|
||||||
|
Parsed root;
|
||||||
|
std::map<std::string,unsigned> symbols;
|
||||||
|
std::map<std::string,std::string> exports;
|
||||||
|
std::map<std::string,unsigned> globals;
|
||||||
|
uint64_t library_semantic = 0;
|
||||||
|
explicit Generator(const std::string& source,const std::string& file):path(file),root(source,file){}
|
||||||
|
unsigned intern(const std::string& s) {
|
||||||
|
auto [it,added]=symbols.emplace(s,static_cast<unsigned>(symbols.size()+1));return it->second;
|
||||||
|
}
|
||||||
|
std::string literal(GCproto* p,BCIns ins) {
|
||||||
|
const auto op=bc_op(ins);const unsigned d=bc_d(ins);
|
||||||
|
if(op==NG_BC_KSHORT)return "ngp_num("+number(static_cast<int16_t>(d))+")";
|
||||||
|
if(op==NG_BC_KNUM)return "ngp_num("+number(numeric(p,d))+")";
|
||||||
|
if(op==NG_BC_KSTR)return "ngp_string("+std::to_string(intern(str(p,d)))+"u)";
|
||||||
|
if(op==NG_BC_KPRI&&d<=2)return d==0?"ngp_nil()":d==1?"ngp_bool(false)":"ngp_bool(true)";
|
||||||
|
throw std::runtime_error("Lua GPU: unsupported literal");
|
||||||
|
}
|
||||||
|
void read_exports(const std::string& source) {
|
||||||
|
// Reviewed utilities: top-level constants precede function/table values.
|
||||||
|
// Those unread inert definitions are omitted; function bodies never run.
|
||||||
|
Parsed lib(source,library);library_semantic=compiled_fingerprint(lib);
|
||||||
|
// Both reviewed distributions have the same constants / edit_component
|
||||||
|
// behavior. Steam additionally defines unused debug/vector/color/texture
|
||||||
|
// helpers. Comments, source paths and line information are not behavior.
|
||||||
|
if(library_semantic!=6066579123591393726ull && library_semantic!=13023294535491161211ull)
|
||||||
|
throw std::runtime_error(std::string(library)+": unreviewed compiled library (fingerprint "+std::to_string(library_semantic)+")");
|
||||||
|
std::map<unsigned,std::string> values;
|
||||||
|
for(unsigned i=1;i<lib.pt->sizebc;++i){
|
||||||
|
BCIns ins=proto_bc(lib.pt)[i];auto op=bc_op(ins);unsigned a=bc_a(ins),d=bc_d(ins);
|
||||||
|
if(op==NG_BC_FNEW)break;
|
||||||
|
if(op==NG_BC_KSHORT||op==NG_BC_KNUM||op==NG_BC_KPRI||op==NG_BC_KSTR)values[a]=literal(lib.pt,ins);
|
||||||
|
else if(op==NG_BC_GSET&&values.count(a))exports[str(lib.pt,d)]=values.at(a);
|
||||||
|
else throw std::runtime_error(std::string(library)+": unsupported library initializer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string body() {
|
||||||
|
GCproto* p=root.pt;
|
||||||
|
if(p->sizebc>1024||p->framesize>64||p->sizeuv)throw std::runtime_error("Lua GPU: target limit exceeded (1024 instructions, 64 registers, no upvalues)");
|
||||||
|
auto reg=[&](unsigned r){if(r>=p->framesize)throw std::runtime_error("Lua GPU: register range exceeded");return "ngp_r["+std::to_string(r)+"]";};
|
||||||
|
std::ostringstream cases;
|
||||||
|
for(unsigned ip=1;ip<p->sizebc;++ip){
|
||||||
|
const BCIns ins=proto_bc(p)[ip];const auto op=bc_op(ins);
|
||||||
|
const unsigned a=bc_a(ins),b=bc_b(ins),c=bc_c(ins),d=bc_d(ins);
|
||||||
|
auto fail=[&](const std::string& why){throw std::runtime_error(path+":"+std::to_string(line(p,ip))+": "+why+" ("+ins.name+")");};
|
||||||
|
cases<<"case "<<ip<<": {ngp_line="<<line(p,ip)<<"u;ngp_ip="<<ip+1<<";";
|
||||||
|
auto test=[&](const std::string& cond){
|
||||||
|
if(ip+1>=p->sizebc||bc_op(proto_bc(p)[ip+1])!=NG_BC_JMP)fail("expected conditional jump");
|
||||||
|
cases<<"if(!("<<cond<<"))ngp_ip++;";
|
||||||
|
};
|
||||||
|
if(op==NG_BC_KSHORT||op==NG_BC_KNUM||op==NG_BC_KPRI||op==NG_BC_KSTR)cases<<reg(a)<<"="<<literal(p,ins)<<";";
|
||||||
|
else if(op==NG_BC_MOV)cases<<reg(a)<<"="<<reg(d)<<";";
|
||||||
|
else if(op==NG_BC_KNIL){reg(d);for(unsigned k=a;k<=d;k++)cases<<reg(k)<<"=ngp_nil();";}
|
||||||
|
else if(op==NG_BC_NOT)cases<<reg(a)<<"=ngp_bool(!ngp_truth("<<reg(d)<<"));";
|
||||||
|
else if(op==NG_BC_UNM)cases<<reg(a)<<"=ngp_num(-ngp_number("<<reg(d)<<"));";
|
||||||
|
else if(op==NG_BC_GGET){
|
||||||
|
const std::string name=str(p,d);const auto api=apis.find(name);
|
||||||
|
if(api!=apis.end())cases<<reg(a)<<"=NgpValue(4u,0.0,"<<api->second<<"u);";
|
||||||
|
else if(name=="math")cases<<reg(a)<<"=NgpValue(5u,0.0,1u);";
|
||||||
|
else if(exports.count(name)){
|
||||||
|
auto [it,added]=globals.emplace(name,static_cast<unsigned>(globals.size()));
|
||||||
|
cases<<reg(a)<<"=ngp_g["<<it->second<<"];";
|
||||||
|
}else fail("unsupported global/API '"+name+"'");
|
||||||
|
}else if(op==NG_BC_TGETS){
|
||||||
|
const auto field=str(p,c);const auto api=math_apis.find(field);
|
||||||
|
if(api==math_apis.end())fail("unsupported math field '"+field+"'");
|
||||||
|
cases<<"if("<<reg(b)<<".tag!=5u)ngp_fault(3u);"<<reg(a)<<"=NgpValue(4u,0.0,"<<api->second<<"u);";
|
||||||
|
}else if(op==NG_BC_CALL){
|
||||||
|
if(b==0||c==0||b>9||c>9)fail("only fixed calls with up to eight arguments/results are supported");
|
||||||
|
cases<<"NgpTuple args=ngp_empty();args.count="<<c-1<<";";
|
||||||
|
for(unsigned k=0;k<c-1;k++)cases<<"args.v["<<k<<"]="<<reg(a+1+ng_lua_frame_shift()+k)<<";";
|
||||||
|
cases<<"NgpTuple result=ngp_call("<<reg(a)<<",args);";
|
||||||
|
for(unsigned k=0;k<b-1;k++)cases<<reg(a+k)<<"=result.v["<<k<<"];";
|
||||||
|
}else if(op==NG_BC_JMP||op==NG_BC_UCLO){
|
||||||
|
const int target=static_cast<int>(ip)+1+static_cast<int>(bc_j(ins));
|
||||||
|
if(target<1||target>=static_cast<int>(p->sizebc))fail("jump target out of range");
|
||||||
|
cases<<"ngp_ip="<<target<<";";
|
||||||
|
}else if(op==NG_BC_RET0||op==NG_BC_RET1||op==NG_BC_RET)cases<<"projectile=ngp_proj;return ngp_error;";
|
||||||
|
else if(op==NG_BC_LOOP){} // Marker only; backedges are ordinary JMP.
|
||||||
|
else if(op>=NG_BC_ISLT&&op<=NG_BC_ISGT){
|
||||||
|
std::string cond="ngp_less("+reg(a)+","+reg(d)+","+((op==NG_BC_ISLE||op==NG_BC_ISGT)?"true":"false")+")";
|
||||||
|
// LuaJIT GE/GT are the complement tests, including unordered NaN.
|
||||||
|
if(op==NG_BC_ISGE||op==NG_BC_ISGT)cond="!"+cond;
|
||||||
|
test(cond);
|
||||||
|
}else if(op>=NG_BC_ISEQV&&op<=NG_BC_ISNEP){
|
||||||
|
std::string rhs;
|
||||||
|
if(op<=NG_BC_ISNEV)rhs=reg(d);
|
||||||
|
else if(op<=NG_BC_ISNES)rhs="ngp_string("+std::to_string(intern(str(p,d)))+"u)";
|
||||||
|
else if(op<=NG_BC_ISNEN)rhs="ngp_num("+number(numeric(p,d))+")";
|
||||||
|
else {if(d>2)fail("invalid primitive");rhs=d==0?"ngp_nil()":d==1?"ngp_bool(false)":"ngp_bool(true)";}
|
||||||
|
const bool unequal=(static_cast<int>(op)-NG_BC_ISEQV)%2!=0;
|
||||||
|
test(std::string(unequal?"!":"")+"ngp_equal("+reg(a)+","+rhs+")");
|
||||||
|
}else if(op==NG_BC_IST||op==NG_BC_ISF||op==NG_BC_ISTC||op==NG_BC_ISFC){
|
||||||
|
const bool truth=op==NG_BC_IST||op==NG_BC_ISTC;
|
||||||
|
std::string cond=std::string(truth?"":"!")+"ngp_truth("+reg(d)+")";
|
||||||
|
if(op==NG_BC_ISTC||op==NG_BC_ISFC)cases<<"if("<<cond<<")"<<reg(a)<<"="<<reg(d)<<";";
|
||||||
|
test(cond);
|
||||||
|
}else if(op>=NG_BC_ADDVN&&op<=NG_BC_POW){
|
||||||
|
const int index=static_cast<int>(op)-NG_BC_ADDVN;
|
||||||
|
const int arithmetic=op==NG_BC_POW?5:index%5;
|
||||||
|
std::string left,right;
|
||||||
|
if(index<5){left=reg(b);right="ngp_num("+number(numeric(p,c))+")";}
|
||||||
|
else if(index<10){left="ngp_num("+number(numeric(p,c))+")";right=reg(b);}
|
||||||
|
else {left=reg(b);right=reg(c);}
|
||||||
|
cases<<reg(a)<<"=ngp_arithmetic("<<arithmetic<<","<<left<<","<<right<<");";
|
||||||
|
}else fail("unsupported Lua GPU instruction");
|
||||||
|
cases<<"break;}\n";
|
||||||
|
}
|
||||||
|
std::ostringstream out;
|
||||||
|
out<<"NgpValue ngp_g["<<std::max<std::size_t>(1,globals.size())<<"];\nvoid ngp_module_init(){";
|
||||||
|
for(const auto& [name,index]:globals)out<<"ngp_g["<<index<<"]="<<exports.at(name)<<";";
|
||||||
|
out<<"}\nuvec4 ngp_execute(inout FProj projectile,vec3 owner,float owner_id){\n"
|
||||||
|
"ngp_proj=projectile;ngp_owner=owner;ngp_owner_id=owner_id;ngp_failed=false;ngp_loaded=false;ngp_error=uvec4(0u);ngp_line=0u;\n"
|
||||||
|
"for(int i=0;i<ngp_g.length();i++)ngp_g[i]=ngp_nil();\nNgpValue ngp_r["<<unsigned(p->framesize)<<"];\n"
|
||||||
|
"for(int i=0;i<ngp_r.length();i++)ngp_r[i]=ngp_nil();int ngp_ip=1;\n"
|
||||||
|
"for(int budget=0;budget<4096&&!ngp_failed;budget++){switch(ngp_ip){\n"<<cases.str()<<
|
||||||
|
"default:ngp_fault(3u);break;}}\nif(!ngp_failed)ngp_fault(4u);projectile=ngp_proj;return ngp_error;}\n";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
std::string preamble() {
|
||||||
|
std::ostringstream out;
|
||||||
|
for(const auto& [label,value]:std::map<std::string,std::string>{{"PROJECTILE","ProjectileComponent"},{"VELOCITY","VelocityComponent"},{"WHO","mWhoShot"},{"GRAVITY","gravity_y"},{"MOTION","mVelocity"},{"UTIL",library}})
|
||||||
|
out<<"const uint NGP_"<<label<<"="<<intern(value)<<"u;\n";
|
||||||
|
out<<"uint ngp_rank(uint s){switch(s){";
|
||||||
|
unsigned rank=0;for(const auto& [s,id]:symbols)out<<"case "<<id<<"u:return "<<rank++<<"u;";
|
||||||
|
out<<"default:return 0u;}}\nbool ngp_parse_number(uint s,out float n){n=0.0;switch(s){";
|
||||||
|
for(const auto& [s,id]:symbols){
|
||||||
|
lua_pushlstring(root.state.get(),s.data(),s.size());
|
||||||
|
if(lua_isnumber(root.state.get(),-1))out<<"case "<<id<<"u:n="<<number(lua_tonumber(root.state.get(),-1))<<";return true;";
|
||||||
|
lua_pop(root.state.get(),1);
|
||||||
|
}
|
||||||
|
out<<"default:return false;}}\nvoid ngp_module_init();\n";return out.str();
|
||||||
|
}
|
||||||
|
std::string scalar_body() {
|
||||||
|
// Typed, acyclic SSA lowering for fused additive neighbor bodies. There
|
||||||
|
// is no register interpreter or dynamic API dispatch in the inner loop.
|
||||||
|
struct Value {int kind=0;std::string expr;unsigned api=0;double constant=NAN;};
|
||||||
|
enum {Number=1,Self,Velocity,Function,String,Math};
|
||||||
|
auto* p=root.pt;std::vector<Value> r(p->framesize);std::ostringstream out;
|
||||||
|
out<<"uvec4 ngp_execute(inout FProj projectile,vec3 owner,float owner_id){\n";
|
||||||
|
auto fail=[&](unsigned ip,const std::string& why){throw std::runtime_error(path+":"+std::to_string(line(p,ip))+": scalar neighbor lowering: "+why);};
|
||||||
|
auto numeric_value=[&](const Value& v,unsigned ip){if(v.kind!=Number)fail(ip,"numeric value required");return v.expr;};
|
||||||
|
auto temp=[&](unsigned reg,unsigned ip,const std::string& expr){
|
||||||
|
const auto name="v"+std::to_string(ip)+"_"+std::to_string(reg);
|
||||||
|
out<<"float "<<name<<"="<<expr<<";\n";r.at(reg)={Number,name};
|
||||||
|
};
|
||||||
|
for(unsigned ip=1;ip<p->sizebc;++ip) {
|
||||||
|
const auto ins=proto_bc(p)[ip];const auto op=bc_op(ins);unsigned a=bc_a(ins),b=bc_b(ins),c=bc_c(ins),d=bc_d(ins);
|
||||||
|
if(op==NG_BC_KNUM||op==NG_BC_KSHORT){double n=op==NG_BC_KNUM?numeric(p,d):static_cast<int16_t>(d);r.at(a)={Number,number(n),0,n};}
|
||||||
|
else if(op==NG_BC_KSTR)r.at(a)={String,str(p,d)};
|
||||||
|
else if(op==NG_BC_MOV)r.at(a)=r.at(d);
|
||||||
|
else if(op==NG_BC_GGET) {
|
||||||
|
auto name=str(p,d);auto api=apis.find(name);
|
||||||
|
if(name=="math")r.at(a)={Math};
|
||||||
|
else if(api!=apis.end())r.at(a)={Function,"",api->second};
|
||||||
|
else fail(ip,"unknown global "+name);
|
||||||
|
}else if(op==NG_BC_TGETS) {
|
||||||
|
auto api=math_apis.find(str(p,c));
|
||||||
|
if(r.at(b).kind!=Math||api==math_apis.end())fail(ip,"unsupported table access");
|
||||||
|
r.at(a)={Function,"",api->second};
|
||||||
|
}else if(op>=NG_BC_ADDVN&&op<=NG_BC_POW) {
|
||||||
|
int index=int(op)-NG_BC_ADDVN, arithmetic=op==NG_BC_POW?5:index%5;
|
||||||
|
Value left,right;
|
||||||
|
if(index<5){left=r.at(b);double n=numeric(p,c);right={Number,number(n),0,n};}
|
||||||
|
else if(index<10){double n=numeric(p,c);left={Number,number(n),0,n};right=r.at(b);}
|
||||||
|
else{left=r.at(b);right=r.at(c);}
|
||||||
|
auto x=numeric_value(left,ip),y=numeric_value(right,ip);
|
||||||
|
if(arithmetic==5){if(right.constant!=2.0)fail(ip,"only square powers are reviewed");temp(a,ip,"("+x+"*"+x+")");}
|
||||||
|
else if(arithmetic<4)temp(a,ip,"("+x+std::string(1,"+-*/"[arithmetic])+y+")");
|
||||||
|
else fail(ip,"unreviewed arithmetic");
|
||||||
|
}else if(op==NG_BC_UNM)temp(a,ip,"(-"+numeric_value(r.at(d),ip)+")");
|
||||||
|
else if(op>=NG_BC_ISLT&&op<=NG_BC_ISGT) {
|
||||||
|
if(ip+1>=p->sizebc||bc_op(proto_bc(p)[ip+1])!=NG_BC_JMP)fail(ip,"conditional jump required");
|
||||||
|
auto jump=proto_bc(p)[ip+1];int target=int(ip)+2+bc_j(jump);
|
||||||
|
if(target<=int(ip)||target>=int(p->sizebc)||bc_op(proto_bc(p)[target])!=NG_BC_RET0)fail(ip,"only terminal forward guards are reviewed");
|
||||||
|
auto x=numeric_value(r.at(a),ip),y=numeric_value(r.at(d),ip);
|
||||||
|
std::string condition="("+x+((op==NG_BC_ISLE||op==NG_BC_ISGT)?"<=":"<")+y+")";
|
||||||
|
if(op==NG_BC_ISGE||op==NG_BC_ISGT)condition="!"+condition;
|
||||||
|
out<<"if("<<condition<<")return uvec4(0u);\n";++ip;
|
||||||
|
}else if(op==NG_BC_CALL) {
|
||||||
|
if(r.at(a).kind!=Function||b==0||c==0)fail(ip,"fixed native call required");
|
||||||
|
std::vector<Value> args;for(unsigned k=0;k<c-1;++k)args.push_back(r.at(a+1+ng_lua_frame_shift()+k));
|
||||||
|
auto count=[&](unsigned n){if(args.size()!=n)fail(ip,"argument count mismatch");};
|
||||||
|
auto num=[&](unsigned n){return numeric_value(args.at(n),ip);};
|
||||||
|
auto api=r.at(a).api;
|
||||||
|
if(api==1){count(0);r.at(a)={Self};}
|
||||||
|
else if(api==2){
|
||||||
|
count(1);std::string source;
|
||||||
|
if(args[0].kind==Self)source="projectile.a";
|
||||||
|
else if(args[0].kind==Number&&args[0].constant==1.0)source="owner";
|
||||||
|
else fail(ip,"unreviewed entity handle");
|
||||||
|
if(b!=3)fail(ip,"two transform results required");
|
||||||
|
temp(a,ip,source+".x");temp(a+1,ip,source+".y");
|
||||||
|
}else if(api==3){count(2);if(args[0].kind!=Self||args[1].kind!=String||args[1].expr!="VelocityComponent")fail(ip,"velocity component required");r.at(a)={Velocity};}
|
||||||
|
else if(api==4||api==5){
|
||||||
|
count(api==4?2:4);
|
||||||
|
if(args[0].kind!=Velocity||args[1].kind!=String||args[1].expr!="mVelocity")fail(ip,"velocity vector required");
|
||||||
|
if(api==4){if(b!=3)fail(ip,"two velocity results required");temp(a,ip,"projectile.a.z*60.0");temp(a+1,ip,"projectile.a.w*60.0");}
|
||||||
|
else {if(b!=1)fail(ip,"write must have no results");out<<"projectile.a.zw=vec2("<<num(2)<<","<<num(3)<<")/60.0;\n";}
|
||||||
|
}else if(api>=10&&api<=15){
|
||||||
|
count(api==12||api==15?2:1);if(b!=2)fail(ip,"one math result required");
|
||||||
|
const char* names[]={"abs","sqrt","ngp_scalar_atan","cos","sin","max"};
|
||||||
|
temp(a,ip,std::string(names[api-10])+"("+num(0)+(args.size()==2?","+num(1):"")+")");
|
||||||
|
}else fail(ip,"unreviewed native API");
|
||||||
|
}else if(op==NG_BC_RET0)out<<"return uvec4(0u);\n";
|
||||||
|
else fail(ip,std::string("unsupported instruction ")+ins.name);
|
||||||
|
}
|
||||||
|
out<<"}\n";
|
||||||
|
return "float ngp_scalar_atan(float y,float x){float a;if(y==0.0)a=(floatBitsToUint(x)&0x80000000u)!=0u?3.141592653589793:0.0;else if(x==0.0)a=1.5707963267948966;else a=atan(abs(y),x);return (floatBitsToUint(y)&0x80000000u)!=0u?-a:a;}\n"+out.str();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t library_fingerprint(const std::string& source) {
|
||||||
|
Parsed parsed(source,library);return compiled_fingerprint(parsed);
|
||||||
|
}
|
||||||
|
bool dependency_matches(const Dependency& dep,const unsigned char* data,std::size_t size) {
|
||||||
|
const auto raw=fingerprint(data,size);
|
||||||
|
if(raw==dep.fingerprint||(dep.semantic!=0&&raw==dep.validated_fingerprint))return true;
|
||||||
|
if(dep.semantic==0)return false;
|
||||||
|
try {
|
||||||
|
if(library_fingerprint(std::string(reinterpret_cast<const char*>(data),size))!=dep.semantic)return false;
|
||||||
|
dep.validated_fingerprint=raw;return true;
|
||||||
|
}catch(const std::exception&){return false;}
|
||||||
|
}
|
||||||
|
|
||||||
|
Program translate(const std::string& source,const Reader& read,const std::string& path) {
|
||||||
|
Generator gen(source,path);const std::string lib=read(library);gen.read_exports(lib);
|
||||||
|
Program p;
|
||||||
|
if(program_id(path)>=first_neighbor_program)p.function=gen.scalar_body();
|
||||||
|
else {
|
||||||
|
p.function=gen.body(); // Discover symbols/globals before constructing helpers.
|
||||||
|
p.function=gen.preamble()+sources::runtime+p.function;
|
||||||
|
}
|
||||||
|
p.shader=std::string(sources::layout)+sources::terrain+p.function+
|
||||||
|
"uvec4 ngp_dispatch(uint program,inout FProj p,vec3 owner,float owner_id){if(program!=1u)return uvec4(14u,0u,0u,0u);return ngp_execute(p,owner,owner_id);}\n"
|
||||||
|
"uvec4 ngp_neighbor_dispatch(uint program,inout FProj p,vec3 owner){return uvec4(14u,0u,0u,0u);}\n"+sources::dispatch;
|
||||||
|
p.instructions=gen.root.pt->sizebc-1;p.registers=gen.root.pt->framesize;
|
||||||
|
for(const auto& [path,bytes]:std::map<std::string,std::string>{{path,source},{library,lib}})
|
||||||
|
p.dependencies.push_back({path,fingerprint(reinterpret_cast<const unsigned char*>(bytes.data()),bytes.size())});
|
||||||
|
for(auto& dep:p.dependencies)if(dep.path==library)dep.semantic=gen.library_semantic;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
Program translate_reviewed(const Reader& read) {
|
||||||
|
Program bundle;std::map<std::string,Dependency> dependencies;
|
||||||
|
std::ostringstream dispatch,neighbors;
|
||||||
|
dispatch<<"uvec4 ngp_dispatch(uint program,inout FProj p,vec3 owner,float owner_id){switch(program){";
|
||||||
|
neighbors<<"uvec4 ngp_neighbor_dispatch(uint program,inout FProj p,vec3 owner){switch(program){";
|
||||||
|
unsigned id=0;
|
||||||
|
for(const auto& profile:profiles) {
|
||||||
|
++id;const auto source=read(profile.path);
|
||||||
|
if(fingerprint(reinterpret_cast<const unsigned char*>(source.data()),source.size())!=profile.fingerprint)
|
||||||
|
throw std::runtime_error(std::string(profile.path)+": modified projectile needs engine binding review");
|
||||||
|
std::string lowered=source;
|
||||||
|
if(id>=first_neighbor_program) {
|
||||||
|
// Fuse this reviewed additive neighbor loop into a per-target kernel.
|
||||||
|
// Preserve the original arithmetic, both distance guards and constants.
|
||||||
|
// Query/iteration/edit_component plumbing becomes GPU gather dispatch.
|
||||||
|
std::vector<std::string> lines;std::istringstream input(source);std::string line;
|
||||||
|
while(std::getline(input,line)) {
|
||||||
|
line.erase(std::remove(line.begin(),line.end(),'\r'),line.end());
|
||||||
|
auto start=line.find_first_not_of(" \t");
|
||||||
|
if(start!=std::string::npos)lines.push_back(line.substr(start));
|
||||||
|
}
|
||||||
|
auto find=[&](const std::string& prefix) {
|
||||||
|
std::string result;unsigned count=0;
|
||||||
|
for(const auto& candidate:lines)if(candidate.rfind(prefix,0)==0){result=candidate;++count;}
|
||||||
|
if(count!=1)throw std::runtime_error(std::string(profile.path)+": neighbor lowering shape changed: "+prefix);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
std::ostringstream pair;
|
||||||
|
if(std::string(profile.path)=="data/scripts/projectiles/effect_slow_bullets.lua") {
|
||||||
|
pair<<"local comp=EntityGetFirstComponent(GetUpdatedEntityID(), 'VelocityComponent')\n"
|
||||||
|
"local vel_x,vel_y=ComponentGetValueVector2(comp,'mVelocity')\n"
|
||||||
|
<<find("vel_x = vel_x *")<<'\n'<<find("vel_y = vel_y *")<<'\n'
|
||||||
|
<<"ComponentSetValueVector2(comp,'mVelocity',vel_x,vel_y)\n";
|
||||||
|
} else {
|
||||||
|
pair<<"local px,py=EntityGetTransform((GetUpdatedEntityID()))\nlocal x,y=EntityGetTransform(1)\n"
|
||||||
|
<<find("local distance =")<<'\n'<<find("local distance_full =")<<'\n';
|
||||||
|
auto diamond=find("if ( distance < distance_full *");auto end=diamond.find(" and ");
|
||||||
|
if(end==std::string::npos)throw std::runtime_error("neighbor diamond guard missing");
|
||||||
|
pair<<diamond.substr(0,end)<<" then\n"<<find("distance = math.sqrt")<<"\nlocal "<<find("direction =")<<'\n'
|
||||||
|
<<find("if ( distance < distance_full )")<<'\n'
|
||||||
|
<<find("local gravity_percent =")<<'\n'<<find("local gravity_coeff =")<<'\n'
|
||||||
|
<<"local comp=EntityGetFirstComponent(GetUpdatedEntityID(), 'VelocityComponent')\n"
|
||||||
|
"local vel_x,vel_y=ComponentGetValueVector2(comp,'mVelocity')\n"
|
||||||
|
<<find("local offset_x =")<<'\n'<<find("local offset_y =")<<'\n'
|
||||||
|
<<find("vel_x = vel_x +")<<'\n'<<find("vel_y = vel_y +")<<'\n'
|
||||||
|
<<"ComponentSetValueVector2(comp,'mVelocity',vel_x,vel_y)\nend\nend\n";
|
||||||
|
}
|
||||||
|
lowered=pair.str();
|
||||||
|
}
|
||||||
|
auto part=translate(lowered,read,profile.path);
|
||||||
|
// The dependency is always the original asset, never the fused fragment.
|
||||||
|
for(auto& dep:part.dependencies)if(dep.path==profile.path)dep.fingerprint=profile.fingerprint;
|
||||||
|
// Each module owns its constants, registers and dofile_once globals.
|
||||||
|
// Only the terrain query is shared; it carries no invocation state.
|
||||||
|
for(const std::string prefix:{"ngp_","NGP_"}) {
|
||||||
|
const std::string replacement=prefix+std::to_string(id)+"_";
|
||||||
|
size_t at=0;
|
||||||
|
while((at=part.function.find(prefix,at))!=std::string::npos) {
|
||||||
|
if(part.function.compare(at,11,"ngp_surface")==0){at+=11;continue;}
|
||||||
|
part.function.replace(at,prefix.size(),replacement);at+=replacement.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const std::string type:{"NgpValue","NgpTuple"}) {
|
||||||
|
size_t at=0;const auto replacement=type+std::to_string(id);
|
||||||
|
while((at=part.function.find(type,at))!=std::string::npos) {
|
||||||
|
part.function.replace(at,type.size(),replacement);at+=replacement.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bundle.function+=part.function;
|
||||||
|
bundle.instructions+=part.instructions;bundle.registers=std::max(bundle.registers,part.registers);
|
||||||
|
for(const auto& dep:part.dependencies)dependencies[dep.path]=dep;
|
||||||
|
dispatch<<"case "<<id<<"u:return ngp_"<<id<<"_execute(p,owner,owner_id);";
|
||||||
|
if(id>=first_neighbor_program)neighbors<<"case "<<id<<"u:return ngp_"<<id<<"_execute(p,owner,1.0);";
|
||||||
|
}
|
||||||
|
dispatch<<"default:return uvec4(14u,0u,0u,0u);}}\n";
|
||||||
|
neighbors<<"default:return uvec4(14u,0u,0u,0u);}}\n";
|
||||||
|
bundle.function+=dispatch.str()+neighbors.str();
|
||||||
|
bundle.shader=std::string(sources::layout)+sources::terrain+bundle.function+sources::dispatch;
|
||||||
|
for(const auto& [path,dep]:dependencies)bundle.dependencies.push_back(dep);
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint32_t> compile_spirv(const std::string& source,const std::filesystem::path& compiler_dll) {
|
||||||
|
struct Dll {HMODULE module;~Dll(){if(module)FreeLibrary(module);}} dll{
|
||||||
|
LoadLibraryExW(std::filesystem::absolute(compiler_dll).c_str(),nullptr,LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)};
|
||||||
|
if(!dll.module)throw std::runtime_error("Lua GPU: cannot load native compiler "+compiler_dll.string()+" (Windows error "+std::to_string(GetLastError())+")");
|
||||||
|
auto get=[&](const char* name){auto f=GetProcAddress(dll.module,name);if(!f)throw std::runtime_error(std::string("Lua GPU: missing compiler symbol ")+name);return f;};
|
||||||
|
#define NG_SHADERC(name) const auto name##_fn=reinterpret_cast<decltype(&name)>(get(#name))
|
||||||
|
NG_SHADERC(shaderc_compiler_initialize);NG_SHADERC(shaderc_compiler_release);
|
||||||
|
NG_SHADERC(shaderc_compile_options_initialize);NG_SHADERC(shaderc_compile_options_release);
|
||||||
|
NG_SHADERC(shaderc_compile_options_set_target_env);NG_SHADERC(shaderc_compile_options_set_optimization_level);
|
||||||
|
NG_SHADERC(shaderc_compile_into_spv);NG_SHADERC(shaderc_result_release);
|
||||||
|
NG_SHADERC(shaderc_result_get_compilation_status);NG_SHADERC(shaderc_result_get_error_message);
|
||||||
|
NG_SHADERC(shaderc_result_get_length);NG_SHADERC(shaderc_result_get_bytes);
|
||||||
|
#undef NG_SHADERC
|
||||||
|
std::unique_ptr<shaderc_compiler,decltype(shaderc_compiler_release_fn)> compiler(shaderc_compiler_initialize_fn(),shaderc_compiler_release_fn);
|
||||||
|
std::unique_ptr<shaderc_compile_options,decltype(shaderc_compile_options_release_fn)> options(shaderc_compile_options_initialize_fn(),shaderc_compile_options_release_fn);
|
||||||
|
if(!compiler||!options)throw std::runtime_error("Lua GPU: native compiler allocation failed");
|
||||||
|
shaderc_compile_options_set_target_env_fn(options.get(),shaderc_target_env_vulkan,shaderc_env_version_vulkan_1_1);
|
||||||
|
// Keep optimization in the Vulkan driver. The SDK optimizer expands this
|
||||||
|
// bounded-register control flow heavily and crashes for larger VM bundles.
|
||||||
|
// Straight-line neighbor kernels avoid the VM entirely.
|
||||||
|
shaderc_compile_options_set_optimization_level_fn(options.get(),shaderc_optimization_level_zero);
|
||||||
|
std::unique_ptr<shaderc_compilation_result,decltype(shaderc_result_release_fn)> result(
|
||||||
|
shaderc_compile_into_spv_fn(compiler.get(),source.data(),source.size(),shaderc_compute_shader,script,"main",options.get()),shaderc_result_release_fn);
|
||||||
|
if(!result)throw std::runtime_error("Lua GPU: shader compiler returned no result");
|
||||||
|
if(shaderc_result_get_compilation_status_fn(result.get())!=shaderc_compilation_status_success)
|
||||||
|
throw std::runtime_error(std::string("Lua GPU GLSL: ")+shaderc_result_get_error_message_fn(result.get()));
|
||||||
|
const auto bytes=shaderc_result_get_length_fn(result.get());
|
||||||
|
if(bytes%4||bytes<20)throw std::runtime_error("Lua GPU: invalid SPIR-V output");
|
||||||
|
std::vector<uint32_t> code(bytes/4);std::memcpy(code.data(),shaderc_result_get_bytes_fn(result.get()),bytes);return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 416 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user