diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e69ff0..67814bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,9 +48,10 @@ set(MAIN_FILES src/model.c src/network.c src/path.c + src/png.c + src/random.c src/vecmath.c src/wav.c - src/png.c ) set(GL_FILES diff --git a/docs/modding_lua.txt b/docs/modding_lua.txt index 0415266..93924c0 100644 --- a/docs/modding_lua.txt +++ b/docs/modding_lua.txt @@ -726,6 +726,20 @@ common.json_write(fname, value) @ to tell the difference between tables and arrays it checks for the existence of the key "1" +common.prng_new(seed, stream) @ + creates a new pseudo-random number generator instance + + seed and stream are both optional + + returns a table of the following functions: + - seed(seed, stream) + - allows reseeding of the RNG, both arguments are optional + - random() + - random(max) + - random(min, max) + - returns a random number between min and max, defaulting to values of 0 and 1 + + str = common.net_pack(fmt, ...) @ packs data into a string diff --git a/include/common.h b/include/common.h index 218e6ef..ad644a5 100644 --- a/include/common.h +++ b/include/common.h @@ -449,6 +449,11 @@ enum BT_MAX }; +typedef struct prng { + uint64_t state; + uint64_t stream; +} prng_t; + typedef struct packet packet_t; struct packet { @@ -688,6 +693,12 @@ img_t *img_parse_png(int len, const char *data, lua_State *L); img_t *img_load_png(const char *fname, lua_State *L); void img_write_png(const char *fname, img_t *img); +// random.c +void prng_seed(prng_t *rng, uint64_t seed, uint64_t stream); +uint32_t prng_random(prng_t *rng); +double prng_random_double(prng_t *rng); +double prng_random_double_range(prng_t *rng, double minimum, double maximum); + // vecmath.c vec4f_t mtx_apply_vec(matrix_t *mtx, vec4f_t *vec); void mtx_identity(matrix_t *mtx); diff --git a/src/lua.c b/src/lua.c index 689bb2d..1d13058 100644 --- a/src/lua.c +++ b/src/lua.c @@ -193,6 +193,7 @@ int icelua_fn_client_mk_set_title(lua_State *L) #include "lua_mus.h" #include "lua_model.h" #include "lua_net.h" +#include "lua_random.h" #include "lua_tcp.h" #include "lua_udp.h" #include "lua_util.h" @@ -319,6 +320,7 @@ struct icelua_entry icelua_common[] = { {icelua_fn_common_net_unpack_array, "net_unpack_array"}, {icelua_fn_common_net_send, "net_send"}, {icelua_fn_common_net_recv, "net_recv"}, + {icelua_fn_common_prng_new, "prng_new"}, {icelua_fn_common_tcp_connect, "tcp_connect"}, {icelua_fn_common_tcp_send, "tcp_send"}, {icelua_fn_common_tcp_recv, "tcp_recv"}, diff --git a/src/lua_random.h b/src/lua_random.h new file mode 100644 index 0000000..91fab0e --- /dev/null +++ b/src/lua_random.h @@ -0,0 +1,104 @@ +/* + This file is part of Iceball. + + Iceball is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Iceball is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Iceball. If not, see . +*/ + +// Notes: +// * There's no `get_seed` function because state is stored in a uint64, which +// we can't shove into a double. We could return the original seed and steps +// taken, but without a way to take arbitrary steps, this is pretty useless. +// It would also require storing more state, but it's only 128 bits at the +// moment, which is already pretty small for a PRNG. +// * We could add a clone function that creates a new one with the same state. +// Useless for networking though. + +int icelua_fn_cl_prng_seed(lua_State *L) +{ + int top = icelua_assert_stack(L, 0, 2); + uint64_t seed = 0; // TODO: Seed from time or something, if not given + uint64_t stream = 0; + if (top >= 1) { + seed = (uint64_t)lua_tointeger(L, 1); + if (top >= 2) { + stream = (uint64_t)lua_tointeger(L, 2); + } + } + prng_t *rng = lua_touserdata(L, lua_upvalueindex(1)); + prng_seed(rng, seed, stream); + return 0; +} + +// Simulates Lua's math.random +// 0 args: [0-1] +// 1 args: [0-max] +// 2 args: [min-max] +int icelua_fn_cl_prng_random(lua_State *L) +{ + int top = icelua_assert_stack(L, 0, 2); + prng_t *rng = lua_touserdata(L, lua_upvalueindex(1)); + double result; + if (top == 0) { + result = prng_random_double(rng); + } else { + double minimum; + double maximum; + if (top == 1) { + minimum = 0; + maximum = lua_tonumber(L, 1); + } else { + minimum = lua_tonumber(L, 1); + maximum = lua_tonumber(L, 2); + } + result = prng_random_double_range(rng, minimum, maximum); + } + lua_pushnumber(L, result); + return 1; +} + +int icelua_fn_common_prng_new(lua_State *L) +{ + int top = icelua_assert_stack(L, 0, 2); + uint64_t seed = 0; // TODO: Seed from time or something, if not given + uint64_t stream = 0; + if (top >= 1) { + seed = (uint64_t)lua_tointeger(L, 1); + if (top >= 2) { + stream = (uint64_t)lua_tointeger(L, 2); + } + } + + // "this" table + lua_createtable(L, 0, 4); + + // PRNG state - uses upvalues, not visible to Lua, but hey, GC + prng_t *rng = lua_newuserdata(L, sizeof(prng_t)); + prng_seed(rng, seed, stream); + + lua_pushstring(L, "seed"); // Function name + lua_pushvalue(L, -2); // Duplicate RNG reference to top of stack + lua_pushcclosure(L, &icelua_fn_cl_prng_seed, 1); // Create closure, 1 upvalue (the RNG state) + lua_settable(L, -4); // Insert closure into table + + lua_pushstring(L, "random"); // Function name + lua_pushvalue(L, -2); // Duplicate RNG reference to top of stack + lua_pushcclosure(L, &icelua_fn_cl_prng_random, 1); // Create closure, 1 upvalue (the RNG state) + lua_settable(L, -4); // Insert closure into table + + // Pop the RNG state off the stack. + lua_pop(L, 1); + + return 1; + +} diff --git a/src/random.c b/src/random.c new file mode 100644 index 0000000..29c940e --- /dev/null +++ b/src/random.c @@ -0,0 +1,66 @@ +/* + This file is part of Iceball. + + Iceball is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Iceball is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Iceball. If not, see . +*/ + +#include "common.h" + +// Basic implementation of the PCG psuedorandom number generator. +// Specifically, PCG-XSH-RR, as that's what the paper recommends for general use. +// This may diverge from the official implementations, but I wanted to give credit. +// TODO: We could drop the stream part and use a set value. We have no real use +// for it, and it complicates the API. It does give us more possible sequences though. +// On the other hand, using a set value does allow us to ensure that a relatively +// good value is chosen (co-primes, etc.). + +#define PRNG_MULTIPLIER 6364136223846793005ULL + +void prng_seed(prng_t *rng, uint64_t seed, uint64_t stream) { + rng->state = 0; + // This must be odd (although it will still work sub-optimally if even) + rng->stream = (stream << 1) | 1; + // Initialise the state + prng_random(rng); + rng->state += seed; + // Properly initialise the state (diverge the streams) + prng_random(rng); +} + +uint32_t prng_random(prng_t *rng) { + uint64_t state = rng->state; + + // Update stored state + rng->state = state * PRNG_MULTIPLIER + rng->stream; + + // Generate number + // Top 5 bits specify rotation (for 32 bit result): + // * 64 - 5 = 59 + // * 32 - 5 = 27 + // * (5 + 32) / 2 = 18 + uint32_t xor_shifted = (uint32_t)(((state >> 18) ^ state) >> 27); + uint32_t rotation = (uint32_t)(state >> 59); + return (xor_shifted >> rotation) | (xor_shifted << (-rotation & 31)); +} + +double prng_random_double(prng_t *rng) { + uint32_t random = prng_random(rng); + return (double)random / UINT32_MAX; +} + +double prng_random_double_range(prng_t *rng, double minimum, double maximum) { + uint32_t random = prng_random(rng); + double d = (double)random / UINT32_MAX; + return minimum + d * (maximum - minimum); +}