This commit is contained in:
Gaël committed 2026-10-19 20:59:15 +02:00
commit 87bff164f1
416 files changed
+279512

No files matched your search

+20
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+20
View File
@@ -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`.
+27
View File
@@ -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
}
+26
View File
@@ -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);
}
+18
View File
@@ -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]); }
+192
View File
@@ -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);
}
}
+23
View File
@@ -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;
}
+84
View File
@@ -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))); }
+90
View File
@@ -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);
}
}
}
+191
View File
@@ -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);
}
}
}
}
+722
View File
@@ -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); }
}
+16
View File
@@ -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);
}
+124
View File
@@ -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();
}
}
}
+18
View File
@@ -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); }
+112
View File
@@ -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));
}
+69
View File
@@ -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;
}
}
+208
View File
@@ -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;
}
+102
View File
@@ -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
}
+7
View File
@@ -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
};
+263
View File
@@ -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;
}
}
}
+133
View File
@@ -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;
}
+99
View File
@@ -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)
}
+266
View File
@@ -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)
}
}
+23
View File
@@ -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)));
}
+121
View File
@@ -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();
}
+15
View File
@@ -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);
}
+21
View File
@@ -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();
}
+38
View File
@@ -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));
}
+119
View File
@@ -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);
}
+25
View File
@@ -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;
}
+50
View File
@@ -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;
}
+91
View File
@@ -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; }
}
+722
View File
@@ -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;
}
+75
View File
@@ -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;
}
}
}
+127
View File
@@ -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(); }
}
}
}
}
+457
View File
@@ -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
}
+123
View File
@@ -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;
}
}
+11
View File
@@ -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];
}
+71
View File
@@ -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)
}
+53
View File
@@ -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; }
}
+67
View File
@@ -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);
}
}
+28
View File
@@ -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)
}
}
+310
View File
@@ -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;
}
}
+23
View File
@@ -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));
}
+18
View File
@@ -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);
}
+132
View File
@@ -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);
}
}
+451
View File
@@ -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);
}
+224
View File
@@ -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
}
}
+362
View File
@@ -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
+94
View File
@@ -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
View File
@@ -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
+420
View File
@@ -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
+192
View File
@@ -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
View File
@@ -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
+216
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+151
View File
@@ -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
+34
View File
@@ -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
+88
View File
@@ -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
+39
View File
@@ -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
+60
View File
@@ -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
+186
View File
@@ -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
+75
View File
@@ -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
+29
View File
@@ -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
+36
View File
@@ -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
+73
View File
@@ -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
+147
View File
@@ -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
+121
View File
@@ -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
+47
View File
@@ -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
+127
View File
@@ -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[] = {
{"&amp;", '&'}, {"&lt;", '<'}, {"&gt;", '>'}, {"&quot;", '"'}, {"&apos;", '\''}};
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
+34
View File
@@ -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
+152
View File
@@ -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
+33
View File
@@ -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
+857
View File
@@ -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
+217
View File
@@ -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
+93
View File
@@ -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
+47
View File
@@ -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; }
}
+14
View File
@@ -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
}
+55
View File
@@ -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
+250
View File
@@ -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
+945
View File
@@ -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
+11
View File
@@ -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;
};
}
+233
View File
@@ -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
+66
View File
@@ -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
+156
View File
@@ -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
+84
View File
@@ -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
+272
View File
@@ -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
+73
View File
@@ -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
View File
@@ -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
View File
@@ -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
+484
View File
@@ -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
+177
View File
@@ -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
+406
View File
@@ -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",
+97
View File
@@ -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
+514
View File
@@ -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
+456
View File
@@ -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