From 9dd6adc04d4a7477ccec17faff1645834fa03fff Mon Sep 17 00:00:00 2001 From: elenaran Date: Fri, 17 Jan 2025 19:02:08 -0600 Subject: [PATCH 1/4] Added stronghold functions and also some unit testing --- src/objects/finder.c | 156 ++++++++++++++++++++++++++++++++++++++++ src/objects/generator.c | 4 ++ tests/test_finder.py | 90 +++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 tests/test_finder.py diff --git a/src/objects/finder.c b/src/objects/finder.c index aed828b..c0e5d3f 100644 --- a/src/objects/finder.c +++ b/src/objects/finder.c @@ -70,6 +70,159 @@ static PyObject *Finder_get_structure_config(FinderObject *self, PyObject *args) } +static PyObject *Finder_is_stronghold_biome(FinderObject *self, PyObject *args) { + int id; + + if (!PyArg_ParseTuple(args, "i", &id)) { + return NULL; + } + + return PyBool_FromLong(isStrongholdBiome(self->version, id)); + +} + +static PyObject *Finder_init_first_stronghold(FinderObject *self, PyObject *args) { + uint64_t s48; + + if (!PyArg_ParseTuple(args, "K", &s48)) { + return NULL; + } + StrongholdIter sh; + initFirstStronghold(&sh, self->version, s48); + + PosObject *pos = Pos_new(&PosType, NULL, NULL); + pos->pos.x = sh.pos.x; + pos->pos.z = sh.pos.z; + + PosObject *nextapprox = Pos_new(&PosType, NULL, NULL); + + nextapprox->pos.x = sh.nextapprox.x; + nextapprox->pos.z = sh.nextapprox.z; + + + PyObject *dict = PyDict_New(); + + // Add struct fields to dictionary + PyDict_SetItemString(dict, "pos", (PyObject *)pos); + PyDict_SetItemString(dict, "nextapprox", (PyObject *)nextapprox); + PyDict_SetItemString(dict, "index", PyLong_FromLong(sh.index)); + PyDict_SetItemString(dict, "ringnum", PyLong_FromLong(sh.ringnum)); + PyDict_SetItemString(dict, "ringmax", PyLong_FromLong(sh.ringmax)); + PyDict_SetItemString(dict, "ringidx", PyLong_FromLong(sh.ringidx)); + PyDict_SetItemString(dict, "angle", PyFloat_FromDouble(sh.angle)); + PyDict_SetItemString(dict, "dist", PyFloat_FromDouble(sh.dist)); + PyDict_SetItemString(dict, "rnds", PyLong_FromLongLong(sh.rnds)); + + + PyObject *py_tuple = PyTuple_New(2); + PyTuple_SetItem(py_tuple, 0, nextapprox); // Steals a reference to PosObject + PyTuple_SetItem(py_tuple, 1, dict); // Steals a reference to py_dict + + return py_tuple; +} + + +static PyObject *Finder_next_stronghold(FinderObject *self, PyObject *args) { + PyObject *dict_obj; + PyObject *gen_obj; + + // Parse the arguments as a PyObject (dictionary) and GENERATOR_OBJECT_TYPE + if (!PyArg_ParseTuple(args, "O" GENERATOR_OBJECT_TYPE, &dict_obj, &GeneratorType, &gen_obj)) { + return NULL; + } + + // Check if the first argument is a dictionary + if (!PyDict_Check(dict_obj)) { + PyErr_SetString(PyExc_TypeError, "First parameter must be a dictionary"); + return NULL; + } + + // Check if the second argument is a GeneratorObject + if (!PyObject_TypeCheck(gen_obj, &GeneratorType)) { + PyErr_SetString(PyExc_TypeError, "Second parameter must be a GeneratorObject"); + return NULL; + } + + // Extract the 'pos' value from the dictionary + PyObject *pos_obj = PyDict_GetItemString(dict_obj, "pos"); + if (!pos_obj || !PyObject_TypeCheck(pos_obj, &PosType)) { + PyErr_SetString(PyExc_TypeError, "Dictionary must contain 'pos' key with a PosObject"); + return NULL; + } + + // Extract other values from the dictionary + PyObject *nextapprox_obj = PyDict_GetItemString(dict_obj, "nextapprox"); + if (!nextapprox_obj || !PyObject_TypeCheck(nextapprox_obj, &PosType)) { + PyErr_SetString(PyExc_TypeError, "Dictionary must contain 'nextapprox' key with a PosObject"); + return NULL; + } + + long index = PyLong_AsLong(PyDict_GetItemString(dict_obj, "index")); + long ringnum = PyLong_AsLong(PyDict_GetItemString(dict_obj, "ringnum")); + long ringmax = PyLong_AsLong(PyDict_GetItemString(dict_obj, "ringmax")); + long ringidx = PyLong_AsLong(PyDict_GetItemString(dict_obj, "ringidx")); + double angle = PyFloat_AsDouble(PyDict_GetItemString(dict_obj, "angle")); + double dist = PyFloat_AsDouble(PyDict_GetItemString(dict_obj, "dist")); + unsigned long long rnds = PyLong_AsUnsignedLongLong(PyDict_GetItemString(dict_obj, "rnds")); + + // Access the C 'Pos' structures + PosObject *pos = (PosObject *)pos_obj; + PosObject *nextapprox = (PosObject *)nextapprox_obj; + + + StrongholdIter sh; + sh.pos = pos->pos; + sh.nextapprox = nextapprox->pos; + sh.index = index; + sh.ringnum = ringnum; + sh.ringmax = ringmax; + sh.ringidx = ringidx; + sh.angle = angle; + sh.dist = dist; + sh.rnds = rnds; + sh.mc = self->version; + + GeneratorObject *generator_obj = (GeneratorObject *)gen_obj; + Generator g = generator_obj->generator; + + + int valid = nextStronghold(&sh, &g); + + PosObject *posOut = Pos_new(&PosType, NULL, NULL); + posOut->pos.x = sh.pos.x; + posOut->pos.z = sh.pos.z; + + PosObject *nextapproxOut = Pos_new(&PosType, NULL, NULL); + + nextapproxOut->pos.x = sh.nextapprox.x; + nextapproxOut->pos.z = sh.nextapprox.z; + + PyObject *dict = PyDict_New(); + + // Add struct fields to dictionary + PyDict_SetItemString(dict, "pos", (PyObject *)posOut); + PyDict_SetItemString(dict, "nextapprox", (PyObject *)nextapproxOut); + PyDict_SetItemString(dict, "index", PyLong_FromLong(sh.index)); + PyDict_SetItemString(dict, "ringnum", PyLong_FromLong(sh.ringnum)); + PyDict_SetItemString(dict, "ringmax", PyLong_FromLong(sh.ringmax)); + PyDict_SetItemString(dict, "ringidx", PyLong_FromLong(sh.ringidx)); + PyDict_SetItemString(dict, "angle", PyFloat_FromDouble(sh.angle)); + PyDict_SetItemString(dict, "dist", PyFloat_FromDouble(sh.dist)); + PyDict_SetItemString(dict, "rnds", PyLong_FromLongLong(sh.rnds)); + + PyObject *py_bool = PyBool_FromLong((long)valid); + if (!py_bool) { + return NULL; // Return NULL to indicate an error + } + + PyObject *py_tuple = PyTuple_New(2); + PyTuple_SetItem(py_tuple, 0, py_bool); // Steals a reference to py_bool + PyTuple_SetItem(py_tuple, 1, dict); // Steals a reference to py_dict + + return py_tuple; +} + + static PyObject *Finder_chunk_generate_rnd(FinderObject *self, PyObject *args) { uint64_t seed; int chunkX; @@ -111,6 +264,9 @@ static PyMemberDef Finder_members[] = { static PyMethodDef Finder_methods[] = { {"get_structure_config", (PyCFunction)Finder_get_structure_config, METH_VARARGS, "Finds a structure's configuration parameters"}, + {"is_stronghold_biome", (PyCFunction)Finder_is_stronghold_biome, METH_VARARGS, "Checks if the biome is valid for stronghold placement"}, + {"init_first_stronghold", (PyCFunction)Finder_init_first_stronghold, METH_VARARGS, "Initialises first stronghold"}, + {"next_stronghold", (PyCFunction)Finder_next_stronghold, METH_VARARGS, "Finds next stronghold"}, {"chunk_generate_rnd", (PyCFunction)Finder_chunk_generate_rnd, METH_VARARGS, "Initialises and returns a random seed used in the chunk generation"}, {"get_structure_pos", (PyCFunction)Finder_get_structure_pos, METH_VARARGS, "Finds a structures position within the given region"}, {NULL} /* Sentinel */ diff --git a/src/objects/generator.c b/src/objects/generator.c index 50d7080..4f6cf50 100644 --- a/src/objects/generator.c +++ b/src/objects/generator.c @@ -12,6 +12,10 @@ typedef struct { Generator generator; } GeneratorObject; +extern PyTypeObject GeneratorType; + +#define GENERATOR_OBJECT_TYPE "O!" + static int Generator_traverse(GeneratorObject *self, visitproc visit, void *arg) { return 0; } diff --git a/tests/test_finder.py b/tests/test_finder.py new file mode 100644 index 0000000..1bf6131 --- /dev/null +++ b/tests/test_finder.py @@ -0,0 +1,90 @@ +import pytest +from pybiomes import Finder, Generator, Pos +from pybiomes.biomes import plains +from pybiomes.structures import Village +from pybiomes.versions import MC_1_21_WD + +@pytest.fixture +def finder(): + return Finder(version=MC_1_21_WD) + +def test_get_structure_config(finder): + result = finder.get_structure_config(Village) + assert isinstance(result, dict) + assert 'salt' in result + assert result['salt'] == 10387312 + assert 'regionSize' in result + assert result['regionSize'] == 34 + assert 'chunkRange' in result + assert result['chunkRange'] == 26 + assert 'structType' in result + assert result['structType'] == Village + assert 'dim' in result + assert result['dim'] == 0 + assert 'rarity' in result + assert result['rarity'] == 0 + + +def test_is_stronghold_biome(finder): + result = finder.is_stronghold_biome(plains) + assert isinstance(result, bool) + +def test_init_first_stronghold(finder): + seed = 1234567890 + nextapprox, sh = finder.init_first_stronghold(seed) + assert isinstance(nextapprox, Pos) + assert nextapprox.x == -520 and nextapprox.z == -2600 + assert isinstance(sh, dict) + assert sh['pos'].x == 0 and sh['pos'].z == 0 + assert sh['nextapprox'].x == -520 and sh['nextapprox'].z == -2600 + assert sh['index'] == 0 + assert sh['ringnum'] == 0 + assert sh['ringmax'] == 3 + assert sh['ringidx'] == 0 + assert sh['angle'] == 4.5127238872158175 + assert sh['dist'] == 166.02303278628128 + assert sh['rnds'] == 197462054985395 + +def test_next_stronghold(finder): + sh = { + 'pos': Pos(x=0, z=0), + 'nextapprox': Pos(x=-520, z=-2600), + 'index': 0, + 'ringnum': 0, + 'ringmax': 3, + 'ringidx': 0, + 'angle': 4.5127238872158175, + 'dist': 166.02303278628128, + 'rnds': 197462054985395 + } + generator = Generator(MC_1_21_WD, 0) + is_valid, sh = finder.next_stronghold(sh, generator) + assert isinstance(is_valid, bool) + assert is_valid + assert isinstance(sh, dict) + assert sh['pos'].x == -524 and sh['pos'].z == -2604 + assert sh['nextapprox'].x == 1528 and sh['nextapprox'].z == 520 + assert sh['index'] == 1 + assert sh['ringnum'] == 0 + assert sh['ringmax'] == 3 + assert sh['ringidx'] == 1 + assert sh['angle'] == 6.607118989609013 + assert sh['dist'] == 99.97235323528389 + assert sh['rnds'] == 228792104649703 + +def test_chunk_generate_rnd(finder): + seed = 1234567890 + chunkX = 0 + chunkZ = 0 + rnd = finder.chunk_generate_rnd(seed, chunkX, chunkZ) + assert isinstance(rnd, int) + assert rnd == 24016250047 + +def test_get_structure_pos(finder): + structure = Village + seed = 1234567890 + reg_x = 0 + reg_z = 0 + pos = finder.get_structure_pos(structure, seed, reg_x, reg_z) + assert pos is None or isinstance(pos, Pos) + assert pos.x == 256 and pos.z == 64 From 56c319fb9b7b71988879e4b23752cf97da7daa34 Mon Sep 17 00:00:00 2001 From: elenaran Date: Sun, 31 Aug 2025 01:28:50 -0500 Subject: [PATCH 2/4] Adding support for Xoroshiro rng, as well as setAttemptSeed(), getPopulationSeed(), and getVariant() from finders --- src/bind.c | 7 ++++ src/objects/finder.c | 71 ++++++++++++++++++++++++++++++++++ src/objects/rng.c | 90 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) diff --git a/src/bind.c b/src/bind.c index d0f784a..3f2ff21 100644 --- a/src/bind.c +++ b/src/bind.c @@ -48,6 +48,10 @@ PyMODINIT_FUNC PyInit_pybiomes(void){ if (PyType_Ready(&RngType) < 0) { return NULL; } + + if (PyType_Ready(&XoroshiroType) < 0) { + return NULL; + } PyObject *base = PyModule_Create(&pybiomes); @@ -86,6 +90,9 @@ PyMODINIT_FUNC PyInit_pybiomes(void){ Py_INCREF(&RngType); PyModule_AddObject(base, "Rng", (PyObject *)&RngType); + + Py_INCREF(&XoroshiroType); + PyModule_AddObject(base, "Xoroshiro", (PyObject *)&XoroshiroType); return base; } \ No newline at end of file diff --git a/src/objects/finder.c b/src/objects/finder.c index c0e5d3f..b999fc6 100644 --- a/src/objects/finder.c +++ b/src/objects/finder.c @@ -5,6 +5,10 @@ #include #include "structmember.h" +// Have to manually declare the function prototype here since it's not declared in +// finders.h, and causes issues when Python has to guess the return type. +uint64_t getPopulationSeed(int mc, uint64_t ws, int x, int z); + #include "../external/cubiomes/finders.h" typedef struct { @@ -47,6 +51,28 @@ static int Finder_init(FinderObject *self, PyObject *args, PyObject *kwds) { return 0; } +static PyObject *Finder_set_attempt_seed(FinderObject *self, PyObject *args) { + uint64_t s; + int cx, cz; + + if (!PyArg_ParseTuple(args, "Kii", &s, &cx, &cz)) { + return NULL; + } + setAttemptSeed(&s, cx, cz); + return PyLong_FromUnsignedLongLong(s); +} + +static PyObject *Finder_get_population_seed(FinderObject *self, PyObject *args) { + long long ws; + int x, z; + + if (!PyArg_ParseTuple(args, "Kii", &ws, &x, &z)) { + return NULL; + } + uint64_t result = getPopulationSeed(self->version, ws, x, z); + return PyLong_FromUnsignedLongLong(result); +} + static PyObject *Finder_get_structure_config(FinderObject *self, PyObject *args) { int structureType; @@ -258,17 +284,62 @@ static PyObject *Finder_get_structure_pos(FinderObject *self, PyObject *args) { return (PyObject *)ret; } +static PyObject *Finder_get_variant(FinderObject *self, PyObject *args) { + int structType; + unsigned long long seed; + int blockX, blockZ, biomeID; + + if (!PyArg_ParseTuple(args, "iKiii", &structType, &seed, &blockX, &blockZ, &biomeID)) { + return NULL; + } + + StructureVariant sv; + int success = getVariant(&sv, structType, self->version, seed, blockX, blockZ, biomeID); + + if (success == 0) { + Py_RETURN_NONE; + } + + PyObject *dict = PyDict_New(); + if (!dict) { + return PyErr_NoMemory(); + } + + PyDict_SetItemString(dict, "abandoned", PyBool_FromLong(sv.abandoned)); + PyDict_SetItemString(dict, "giant", PyBool_FromLong(sv.giant)); + PyDict_SetItemString(dict, "underground", PyBool_FromLong(sv.underground)); + PyDict_SetItemString(dict, "airpocket", PyBool_FromLong(sv.airpocket)); + PyDict_SetItemString(dict, "basement", PyBool_FromLong(sv.basement)); + PyDict_SetItemString(dict, "cracked", PyBool_FromLong(sv.cracked)); + PyDict_SetItemString(dict, "size", PyLong_FromLong(sv.size)); + PyDict_SetItemString(dict, "start", PyLong_FromLong(sv.start)); + PyDict_SetItemString(dict, "biome", PyLong_FromLong(sv.biome)); + PyDict_SetItemString(dict, "rotation", PyLong_FromLong(sv.rotation)); + PyDict_SetItemString(dict, "mirror", PyLong_FromLong(sv.mirror)); + PyDict_SetItemString(dict, "x", PyLong_FromLong(sv.x)); + PyDict_SetItemString(dict, "y", PyLong_FromLong(sv.y)); + PyDict_SetItemString(dict, "z", PyLong_FromLong(sv.z)); + PyDict_SetItemString(dict, "sx", PyLong_FromLong(sv.sx)); + PyDict_SetItemString(dict, "sy", PyLong_FromLong(sv.sy)); + PyDict_SetItemString(dict, "sz", PyLong_FromLong(sv.sz)); + + return dict; +} + static PyMemberDef Finder_members[] = { {NULL} /* Sentinel */ }; static PyMethodDef Finder_methods[] = { + {"set_attempt_seed", (PyCFunction)Finder_set_attempt_seed, METH_VARARGS, "Sets an attempt seed from a population seed and coordinates"}, + {"get_population_seed", (PyCFunction)Finder_get_population_seed, METH_VARARGS, "Generates a population seed from a world seed and coordinates."}, {"get_structure_config", (PyCFunction)Finder_get_structure_config, METH_VARARGS, "Finds a structure's configuration parameters"}, {"is_stronghold_biome", (PyCFunction)Finder_is_stronghold_biome, METH_VARARGS, "Checks if the biome is valid for stronghold placement"}, {"init_first_stronghold", (PyCFunction)Finder_init_first_stronghold, METH_VARARGS, "Initialises first stronghold"}, {"next_stronghold", (PyCFunction)Finder_next_stronghold, METH_VARARGS, "Finds next stronghold"}, {"chunk_generate_rnd", (PyCFunction)Finder_chunk_generate_rnd, METH_VARARGS, "Initialises and returns a random seed used in the chunk generation"}, {"get_structure_pos", (PyCFunction)Finder_get_structure_pos, METH_VARARGS, "Finds a structures position within the given region"}, + {"get_variant", (PyCFunction)Finder_get_variant, METH_VARARGS, "Gets a structures variant data (rotation, bounding box, etc.)"}, {NULL} /* Sentinel */ }; diff --git a/src/objects/rng.c b/src/objects/rng.c index d35a6c9..0eb20b1 100644 --- a/src/objects/rng.c +++ b/src/objects/rng.c @@ -12,6 +12,11 @@ typedef struct { uint64_t seed; } RngObject; +typedef struct { + PyObject_HEAD + Xoroshiro state; +} XoroshiroObject; + static int Rng_traverse(RngObject *self, visitproc visit, void *arg) { return 0; } @@ -111,3 +116,88 @@ static PyTypeObject RngType = { .tp_methods = Rng_methods, }; + +// --- Xoroshiro Object Methods --- + +static int Xoroshiro_traverse(XoroshiroObject *self, visitproc visit, void *arg) { + return 0; +} + +static int Xoroshiro_clear(XoroshiroObject *self) { + return 0; +} + +static void Xoroshiro_dealloc(XoroshiroObject *self) { + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static PyObject *Xoroshiro_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + XoroshiroObject *self; + self = (XoroshiroObject *) type->tp_alloc(type, 0); + if (!self) { // A good practice is to check if allocation failed. + return NULL; + } + return (PyObject *)self; +} + +static int Xoroshiro_init(XoroshiroObject *self, PyObject *args, PyObject *kwds) { + return 0; +} + +static PyObject *Xoroshiro_set_seed(XoroshiroObject *self, PyObject *args) { + uint64_t value; + if (!PyArg_ParseTuple(args, "K", &value)) { + return NULL; + } + xSetSeed(&(self->state), value); + Py_RETURN_NONE; +} + +static PyObject *Xoroshiro_next_long(XoroshiroObject *self, PyObject *args) { + return PyLong_FromUnsignedLongLong(xNextLong(&(self->state))); +} + +static PyObject *Xoroshiro_next_int(XoroshiroObject *self, PyObject *args) { + uint32_t n; + if (!PyArg_ParseTuple(args, "I", &n)) { + return NULL; + } + return PyLong_FromLong(xNextInt(&(self->state), n)); +} + +static PyObject *Xoroshiro_next_float(XoroshiroObject *self, PyObject *args) { + return PyFloat_FromDouble(xNextFloat(&(self->state))); +} + +static PyObject *Xoroshiro_next_double(XoroshiroObject *self, PyObject *args) { + return PyFloat_FromDouble(xNextDouble(&(self->state))); +} + +static PyObject *Xoroshiro_next_long_j(XoroshiroObject *self, PyObject *args) { + return PyLong_FromUnsignedLongLong(xNextLongJ(&(self->state))); +} + +static PyMethodDef Xoroshiro_methods[] = { + {"set_seed", (PyCFunction)Xoroshiro_set_seed, METH_VARARGS, "Set the seed value"}, + {"next_long", (PyCFunction)Xoroshiro_next_long, METH_NOARGS, "Generate the next long random number"}, + {"next_int", (PyCFunction)Xoroshiro_next_int, METH_VARARGS, "Generate the next int random number up to n"}, + {"next_float", (PyCFunction)Xoroshiro_next_float, METH_NOARGS, "Generate the next float random number"}, + {"next_double", (PyCFunction)Xoroshiro_next_double, METH_NOARGS, "Generate the next double random number"}, + {"next_long_j", (PyCFunction)Xoroshiro_next_long_j, METH_NOARGS, "Generate the next long random number from two separate xNextLong calls"}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject XoroshiroType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pybiomes.Xoroshiro", + .tp_doc = "Xoroshiro random number generator", + .tp_basicsize = sizeof(XoroshiroObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = Xoroshiro_new, + .tp_init = (initproc)Xoroshiro_init, + .tp_dealloc = (destructor)Xoroshiro_dealloc, + .tp_traverse = (traverseproc) Xoroshiro_traverse, + .tp_clear = (inquiry) Xoroshiro_clear, + .tp_methods = Xoroshiro_methods, +}; From a6982516bc4de3c1632c1056dff0437047b4ce48 Mon Sep 17 00:00:00 2001 From: elenaran Date: Fri, 5 Sep 2025 20:10:33 -0500 Subject: [PATCH 3/4] Adding Xoroshiro nextIntJ function, noise & biomenoise, getSpawn, and mapApproxHeight --- src/bind.c | 29 ++++++++ src/objects/biomenoise.c | 80 +++++++++++++++++++++ src/objects/finder.c | 25 +++++++ src/objects/generator.c | 47 +++++++++++++ src/objects/noise.c | 147 +++++++++++++++++++++++++++++++++++++++ src/objects/rng.c | 9 +++ 6 files changed, 337 insertions(+) create mode 100644 src/objects/biomenoise.c create mode 100644 src/objects/noise.c diff --git a/src/bind.c b/src/bind.c index 3f2ff21..f46e9d4 100644 --- a/src/bind.c +++ b/src/bind.c @@ -6,6 +6,8 @@ #include "pybiomes.c" #include "objects/range.c" +#include "objects/noise.c" +#include "objects/biomenoise.c" #include "objects/generator.c" #include "objects/position.c" #include "objects/finder.c" @@ -52,6 +54,21 @@ PyMODINIT_FUNC PyInit_pybiomes(void){ if (PyType_Ready(&XoroshiroType) < 0) { return NULL; } + // Noise module objects + if (PyType_Ready(&PerlinNoiseType) < 0) { + return NULL; + } + if (PyType_Ready(&OctaveNoiseType) < 0) { + return NULL; + } + if (PyType_Ready(&DoublePerlinNoiseType) < 0) { + return NULL; + } + + // Biome noise module objects + if (PyType_Ready(&SurfaceNoiseType) < 0) { + return NULL; + } PyObject *base = PyModule_Create(&pybiomes); @@ -93,6 +110,18 @@ PyMODINIT_FUNC PyInit_pybiomes(void){ Py_INCREF(&XoroshiroType); PyModule_AddObject(base, "Xoroshiro", (PyObject *)&XoroshiroType); + + Py_INCREF(&PerlinNoiseType); + PyModule_AddObject(base, "PerlinNoise", (PyObject *)&PerlinNoiseType); + + Py_INCREF(&OctaveNoiseType); + PyModule_AddObject(base, "OctaveNoise", (PyObject *)&OctaveNoiseType); + + Py_INCREF(&DoublePerlinNoiseType); + PyModule_AddObject(base, "DoublePerlinNoise", (PyObject *)&DoublePerlinNoiseType); + + Py_INCREF(&SurfaceNoiseType); + PyModule_AddObject(base, "SurfaceNoise", (PyObject *)&SurfaceNoiseType); return base; } \ No newline at end of file diff --git a/src/objects/biomenoise.c b/src/objects/biomenoise.c new file mode 100644 index 0000000..bee1eb9 --- /dev/null +++ b/src/objects/biomenoise.c @@ -0,0 +1,80 @@ +#include +#include +#include +#include + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" + +// Include the cubiomes header files directly +#include "../external/cubiomes/noise.h" +#include "../external/cubiomes/biomenoise.h" + +// Forward declare the PyTypeObject defined in noise.c +extern PyTypeObject PerlinNoiseType; +extern PyTypeObject OctaveNoiseType; +extern PyTypeObject DoublePerlinNoiseType; + +/****************************************************************************** + * SurfaceNoise Object + ******************************************************************************/ + +typedef struct { + PyObject_HEAD + SurfaceNoise noise; +} SurfaceNoiseObject; + +static int SurfaceNoise_init(SurfaceNoiseObject *self, PyObject *args, PyObject *kwds) { + self->noise = (SurfaceNoise){0}; + return 0; +} + +static void SurfaceNoise_dealloc(SurfaceNoiseObject *self) { + PyObject_GC_UnTrack(self); + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static PyMemberDef SurfaceNoise_members[] = { + {"xzScale", T_DOUBLE, offsetof(SurfaceNoiseObject, noise.xzScale), 0, "xzScale value"}, + {"yScale", T_DOUBLE, offsetof(SurfaceNoiseObject, noise.yScale), 0, "yScale value"}, + {"xzFactor", T_DOUBLE, offsetof(SurfaceNoiseObject, noise.xzFactor), 0, "xzFactor value"}, + {"yFactor", T_DOUBLE, offsetof(SurfaceNoiseObject, noise.yFactor), 0, "yFactor value"}, + {NULL} /* Sentinel */ +}; + +/* + * Wrapper function to initialize the SurfaceNoise struct. + * This function is now a method of the SurfaceNoise object, so 'self' + * is a pointer to the object instance. + */ +static PyObject* SurfaceNoise_init_surface_noise(SurfaceNoiseObject *self, PyObject *args) { + int dim; + uint64_t seed; + + if (!PyArg_ParseTuple(args, "iK", &dim, &seed)) { + return NULL; + } + + initSurfaceNoise(&self->noise, dim, seed); + Py_RETURN_NONE; +} + +static PyMethodDef SurfaceNoise_methods[] = { + {"init_surface_noise", (PyCFunction)SurfaceNoise_init_surface_noise, METH_VARARGS, "Initializes a SurfaceNoise struct with a seed."}, + {NULL} /* Sentinel */ +}; + +PyTypeObject SurfaceNoiseType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pybiomes.SurfaceNoise", + .tp_doc = "SurfaceNoise object", + .tp_basicsize = sizeof(SurfaceNoiseObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = PyType_GenericNew, + .tp_init = (initproc)SurfaceNoise_init, + .tp_dealloc = (destructor)SurfaceNoise_dealloc, + .tp_members = SurfaceNoise_members, + .tp_methods = SurfaceNoise_methods, +}; diff --git a/src/objects/finder.c b/src/objects/finder.c index b999fc6..0e5d132 100644 --- a/src/objects/finder.c +++ b/src/objects/finder.c @@ -247,7 +247,31 @@ static PyObject *Finder_next_stronghold(FinderObject *self, PyObject *args) { return py_tuple; } + + +static PyObject *Finder_get_spawn(FinderObject *self, PyObject *args) { + PyObject *gen_obj; + + if (!PyArg_ParseTuple(args, "O!", &GeneratorType, &gen_obj)) { + return NULL; + } + // Check if the argument is a GeneratorObject + if (!PyObject_TypeCheck(gen_obj, &GeneratorType)) { + PyErr_SetString(PyExc_TypeError, "Parameter must be a GeneratorObject"); + return NULL; + } + + GeneratorObject *generator_obj = (GeneratorObject *)gen_obj; + Generator g = generator_obj->generator; + Pos spawn_pos = getSpawn(&g); + PosObject *ret = Pos_new(&PosType, NULL, NULL); + + ret->pos.x = spawn_pos.x; + ret->pos.z = spawn_pos.z; + return (PyObject *)ret; +} + static PyObject *Finder_chunk_generate_rnd(FinderObject *self, PyObject *args) { uint64_t seed; @@ -337,6 +361,7 @@ static PyMethodDef Finder_methods[] = { {"is_stronghold_biome", (PyCFunction)Finder_is_stronghold_biome, METH_VARARGS, "Checks if the biome is valid for stronghold placement"}, {"init_first_stronghold", (PyCFunction)Finder_init_first_stronghold, METH_VARARGS, "Initialises first stronghold"}, {"next_stronghold", (PyCFunction)Finder_next_stronghold, METH_VARARGS, "Finds next stronghold"}, + {"get_spawn", (PyCFunction)Finder_get_spawn, METH_VARARGS, "Gets world spawn position"}, {"chunk_generate_rnd", (PyCFunction)Finder_chunk_generate_rnd, METH_VARARGS, "Initialises and returns a random seed used in the chunk generation"}, {"get_structure_pos", (PyCFunction)Finder_get_structure_pos, METH_VARARGS, "Finds a structures position within the given region"}, {"get_variant", (PyCFunction)Finder_get_variant, METH_VARARGS, "Gets a structures variant data (rotation, bounding box, etc.)"}, diff --git a/src/objects/generator.c b/src/objects/generator.c index 4f6cf50..1f9a133 100644 --- a/src/objects/generator.c +++ b/src/objects/generator.c @@ -5,8 +5,12 @@ #include #include "structmember.h" +#include "../external/cubiomes/biomenoise.h" #include "../external/cubiomes/generator.h" +extern PyTypeObject SurfaceNoiseType; + + typedef struct { PyObject_HEAD Generator generator; @@ -123,11 +127,54 @@ static PyObject *Generator_is_viable_structure_pos(GeneratorObject *self, PyObje return PyBool_FromLong(ret); } +static PyObject *Generator_map_approx_height(GeneratorObject *self, PyObject *args) { + PyObject *sn_obj; + int x, z, w, h; + + if (!PyArg_ParseTuple(args, "O!iiii", &SurfaceNoiseType, &sn_obj, &x, &z, &w, &h)) { + return NULL; + } + + SurfaceNoiseObject *sn = (SurfaceNoiseObject *)sn_obj; + + float *y = (float *)malloc(w * h * sizeof(float)); + int *ids = (int *)malloc(w * h * sizeof(int)); + + if (!y || !ids) { + PyErr_SetString(PyExc_MemoryError, "Failed to allocate memory."); + free(y); + free(ids); + return NULL; + } + + int result = mapApproxHeight(y, ids, &self->generator, &sn->noise, x, z, w, h); + + if (result != 0) { + PyErr_SetString(PyExc_RuntimeError, "mapApproxHeight returned a non-zero value, indicating an error."); + free(y); + free(ids); + return NULL; + } + PyObject *y_list = PyList_New(w * h); + PyObject *ids_list = PyList_New(w * h); + + for (int i = 0; i < w * h; i++) { + PyList_SetItem(y_list, i, PyFloat_FromDouble(y[i])); + PyList_SetItem(ids_list, i, PyLong_FromLong(ids[i])); + } + + free(y); + free(ids); + + return PyTuple_Pack(2, y_list, ids_list); +} + static PyMethodDef Generator_methods[] = { {"apply_seed", (PyCFunction) Generator_apply_seed, METH_VARARGS, "Applies a seed to the generator"}, {"get_biome_at", (PyCFunction) Generator_get_biome_at, METH_VARARGS, "Get the biome at the specified location"}, {"gen_biomes", (PyCFunction) Generator_gen_biomes, METH_VARARGS, "Get the biome at the specified location"}, {"is_viable_structure_pos", (PyCFunction) Generator_is_viable_structure_pos, METH_VARARGS, "Get the biome at the specified location"}, + {"map_approx_height", (PyCFunction)Generator_map_approx_height, METH_VARARGS, "Maps an approximation of the Overworld surface height."}, {NULL} /* Sentinel */ }; diff --git a/src/objects/noise.c b/src/objects/noise.c new file mode 100644 index 0000000..7d5c988 --- /dev/null +++ b/src/objects/noise.c @@ -0,0 +1,147 @@ +#include +#include +#include +#include + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" + +// Include the cubiomes header files directly +#include "../external/cubiomes/noise.h" + +// Forward declare PyTypeObject since these types reference each other +extern PyTypeObject PerlinNoiseType; +extern PyTypeObject OctaveNoiseType; +extern PyTypeObject DoublePerlinNoiseType; + +/****************************************************************************** + * PerlinNoise Object + ******************************************************************************/ + +typedef struct { + PyObject_HEAD + PerlinNoise noise; +} PerlinNoiseObject; + +static int PerlinNoise_init(PerlinNoiseObject *self, PyObject *args, PyObject *kwds) { + self->noise = (PerlinNoise){0}; + return 0; +} + +static void PerlinNoise_dealloc(PerlinNoiseObject *self) { + PyObject_GC_UnTrack(self); + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static PyMemberDef PerlinNoise_members[] = { + {"amplitude", T_DOUBLE, offsetof(PerlinNoiseObject, noise.amplitude), 0, "amplitude value"}, + {"lacunarity", T_DOUBLE, offsetof(PerlinNoiseObject, noise.lacunarity), 0, "lacunarity value"}, + {"d2", T_DOUBLE, offsetof(PerlinNoiseObject, noise.d2), 0, "d2 value"}, + {"t2", T_DOUBLE, offsetof(PerlinNoiseObject, noise.t2), 0, "t2 value"}, + {NULL} /* Sentinel */ +}; + +static PyMethodDef PerlinNoise_methods[] = { + {NULL} /* Sentinel */ +}; + +PyTypeObject PerlinNoiseType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pybiomes.PerlinNoise", + .tp_doc = "PerlinNoise object", + .tp_basicsize = sizeof(PerlinNoiseObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = PyType_GenericNew, + .tp_init = (initproc)PerlinNoise_init, + .tp_dealloc = (destructor)PerlinNoise_dealloc, + .tp_members = PerlinNoise_members, + .tp_methods = PerlinNoise_methods, +}; + + +/****************************************************************************** + * OctaveNoise Object + ******************************************************************************/ + +typedef struct { + PyObject_HEAD + OctaveNoise noise; +} OctaveNoiseObject; + +static int OctaveNoise_init(OctaveNoiseObject *self, PyObject *args, PyObject *kwds) { + self->noise = (OctaveNoise){0}; + return 0; +} + +static void OctaveNoise_dealloc(OctaveNoiseObject *self) { + PyObject_GC_UnTrack(self); + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static PyMemberDef OctaveNoise_members[] = { + {"oct_count", T_INT, offsetof(OctaveNoiseObject, noise.octcnt), 0, "octave count"}, + {NULL} /* Sentinel */ +}; + +static PyMethodDef OctaveNoise_methods[] = { + {NULL} /* Sentinel */ +}; + +PyTypeObject OctaveNoiseType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pybiomes.OctaveNoise", + .tp_doc = "OctaveNoise object", + .tp_basicsize = sizeof(OctaveNoiseObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = PyType_GenericNew, + .tp_init = (initproc)OctaveNoise_init, + .tp_dealloc = (destructor)OctaveNoise_dealloc, + .tp_members = OctaveNoise_members, + .tp_methods = OctaveNoise_methods, +}; + + +/****************************************************************************** + * DoublePerlinNoise Object + ******************************************************************************/ + +typedef struct { + PyObject_HEAD + DoublePerlinNoise noise; +} DoublePerlinNoiseObject; + +static int DoublePerlinNoise_init(DoublePerlinNoiseObject *self, PyObject *args, PyObject *kwds) { + self->noise = (DoublePerlinNoise){0}; + return 0; +} + +static void DoublePerlinNoise_dealloc(DoublePerlinNoiseObject *self) { + PyObject_GC_UnTrack(self); + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static PyMemberDef DoublePerlinNoise_members[] = { + {"amplitude", T_DOUBLE, offsetof(DoublePerlinNoiseObject, noise.amplitude), 0, "amplitude value"}, + {NULL} /* Sentinel */ +}; + +static PyMethodDef DoublePerlinNoise_methods[] = { + {NULL} /* Sentinel */ +}; + +PyTypeObject DoublePerlinNoiseType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pybiomes.DoublePerlinNoise", + .tp_doc = "DoublePerlinNoise object", + .tp_basicsize = sizeof(DoublePerlinNoiseObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = PyType_GenericNew, + .tp_init = (initproc)DoublePerlinNoise_init, + .tp_dealloc = (destructor)DoublePerlinNoise_dealloc, + .tp_members = DoublePerlinNoise_members, + .tp_methods = DoublePerlinNoise_methods, +}; diff --git a/src/objects/rng.c b/src/objects/rng.c index 0eb20b1..913bbb5 100644 --- a/src/objects/rng.c +++ b/src/objects/rng.c @@ -177,6 +177,14 @@ static PyObject *Xoroshiro_next_long_j(XoroshiroObject *self, PyObject *args) { return PyLong_FromUnsignedLongLong(xNextLongJ(&(self->state))); } +static PyObject *Xoroshiro_next_int_j(XoroshiroObject *self, PyObject *args) { + uint32_t n; + if (!PyArg_ParseTuple(args, "I", &n)) { + return NULL; + } + return PyLong_FromLong(xNextIntJ(&(self->state), n)); +} + static PyMethodDef Xoroshiro_methods[] = { {"set_seed", (PyCFunction)Xoroshiro_set_seed, METH_VARARGS, "Set the seed value"}, {"next_long", (PyCFunction)Xoroshiro_next_long, METH_NOARGS, "Generate the next long random number"}, @@ -184,6 +192,7 @@ static PyMethodDef Xoroshiro_methods[] = { {"next_float", (PyCFunction)Xoroshiro_next_float, METH_NOARGS, "Generate the next float random number"}, {"next_double", (PyCFunction)Xoroshiro_next_double, METH_NOARGS, "Generate the next double random number"}, {"next_long_j", (PyCFunction)Xoroshiro_next_long_j, METH_NOARGS, "Generate the next long random number from two separate xNextLong calls"}, + {"next_int_j", (PyCFunction)Xoroshiro_next_int_j, METH_VARARGS, "Generate the next int random number up to n using the worldgenrandom method"}, {NULL} /* Sentinel */ }; From 062835fd98d1499479d1c1e6af7145e1c92fee42 Mon Sep 17 00:00:00 2001 From: elenaran Date: Fri, 5 Sep 2025 23:18:34 -0500 Subject: [PATCH 4/4] Adding unit tests and fixing gen_biomes() --- src/objects/generator.c | 39 ++++++++++++----- tests/test_finder.py | 44 +++++++++++++++++++ tests/test_generator.py | 94 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 tests/test_generator.py diff --git a/src/objects/generator.c b/src/objects/generator.c index 1f9a133..8ebcb6f 100644 --- a/src/objects/generator.c +++ b/src/objects/generator.c @@ -89,29 +89,48 @@ static PyObject *Generator_get_biome_at(GeneratorObject *self, PyObject *args) { } static PyObject *Generator_gen_biomes(GeneratorObject *self, PyObject *args) { - PyObject *range; - - if (!PyArg_ParseTuple(args, "O", &range)) { + int x, y, z, sx, sy, sz, scale; + + // The C function is now configured to expect seven integers. + if (!PyArg_ParseTuple(args, "iiiiiii", &x, &y, &z, &sx, &sy, &sz, &scale)) { return NULL; } - RangeObject *range_cast = (RangeObject *)range; - - Range r = range_cast->range; + Range r; + r.scale = scale; + r.x = x; + r.y = y; + r.z = z; + r.sx = sx; + r.sy = sy; + r.sz = sz; int *biomeIds = allocCache(&self->generator, r); + if (!biomeIds) { + PyErr_SetString(PyExc_MemoryError, "Failed to allocate memory for biome cache."); + return NULL; + } + genBiomes(&self->generator, biomeIds, r); size_t len = getMinCacheSize(&self->generator, r.scale, r.sx, r.sy, r.sz); - PyObject *list = PyList_New(len); + if (!list) { + free(biomeIds); + return NULL; + } for (size_t i = 0; i < len; i++) { - PyList_SetItem(list, i, PyLong_FromLong(biomeIds[i])); + PyObject *biome_py_obj = PyLong_FromLong(biomeIds[i]); + if (!biome_py_obj) { + Py_DECREF(list); + free(biomeIds); + return NULL; + } + PyList_SetItem(list, i, biome_py_obj); } - + free(biomeIds); - return list; } diff --git a/tests/test_finder.py b/tests/test_finder.py index 1bf6131..5b7384b 100644 --- a/tests/test_finder.py +++ b/tests/test_finder.py @@ -8,6 +8,20 @@ def finder(): return Finder(version=MC_1_21_WD) +def test_set_attempt_seed(finder): + seed = 1234567890 + cx, cz = 123, 456 + updated_seed = finder.set_attempt_seed(seed, cx, cz) + assert isinstance(updated_seed, int) + assert updated_seed == 206826366665763 + +def test_get_population_seed(finder): + world_seed = 1234567890 + x, z = 123, 456 + population_seed = finder.get_population_seed(world_seed, x, z) + assert isinstance(population_seed, int) + assert population_seed == 9200318741546110857 + def test_get_structure_config(finder): result = finder.get_structure_config(Village) assert isinstance(result, dict) @@ -45,6 +59,15 @@ def test_init_first_stronghold(finder): assert sh['dist'] == 166.02303278628128 assert sh['rnds'] == 197462054985395 +def test_get_spawn(finder): + seed = 1234567890 + generator = Generator(MC_1_21_WD, seed) + spawn_pos = finder.get_spawn(generator) + + assert isinstance(spawn_pos, Pos) + assert spawn_pos.x == 8 + assert spawn_pos.z == 8 + def test_next_stronghold(finder): sh = { 'pos': Pos(x=0, z=0), @@ -88,3 +111,24 @@ def test_get_structure_pos(finder): pos = finder.get_structure_pos(structure, seed, reg_x, reg_z) assert pos is None or isinstance(pos, Pos) assert pos.x == 256 and pos.z == 64 + +def test_get_variant(finder): + struct_type = Village + seed = 1234567890 + block_x, block_z = 288, 1984 + biome_id = plains + + variant = finder.get_variant(struct_type, seed, block_x, block_z, biome_id) + + assert isinstance(variant, dict) + assert 'abandoned' in variant + assert 'start' in variant + assert 'rotation' in variant + + assert variant['abandoned'] == False + assert variant['start'] == 0 + assert variant['rotation'] == 1 + + invalid_biome_id = 999 + variant_none = finder.get_variant(struct_type, seed, block_x, block_z, invalid_biome_id) + assert variant_none is None diff --git a/tests/test_generator.py b/tests/test_generator.py new file mode 100644 index 0000000..11be330 --- /dev/null +++ b/tests/test_generator.py @@ -0,0 +1,94 @@ +import pytest +from pybiomes import Generator, Pos +from pybiomes.biomes import plains, river +from pybiomes.dimensions import DIM_OVERWORLD +from pybiomes.structures import Village +from pybiomes.versions import MC_1_21_WD + +@pytest.fixture +def generator(): + # A specific seed is used to ensure repeatable results for testing. + return Generator(version=MC_1_21_WD, flags=0) + +def test_apply_seed(generator): + # This test verifies that applying a seed correctly initializes the generator. + seed_to_test = 1234567890 + generator.apply_seed(seed_to_test, DIM_OVERWORLD) + assert True + +def test_get_biome_at(generator): + # This test checks if the generator returns the correct biome ID for a specific coordinate. + seed = 1234567890 + generator.apply_seed(seed, DIM_OVERWORLD) + + scale, x, y, z = 4, 288, 256, 1984 + biome_id = generator.get_biome_at(scale, x>>2, y>>2, z>>2) + + assert isinstance(biome_id, int) + assert biome_id == plains + +def test_gen_biomes(generator): + # This test verifies the bulk biome generation functionality. + seed = 1234567890 + generator.apply_seed(seed, DIM_OVERWORLD) + + # Define a test range. We will create a dictionary to mirror the C struct. + test_range = { + 'scale': 16, + 'x': 0, 'y': 60, 'z': 0, + 'sx': 16, 'sy': 1, 'sz': 16 + } + + # Pass individual values to the Python function as positional arguments, including scale. + biomes = generator.gen_biomes( + test_range['x'], test_range['y'], test_range['z'], + test_range['sx'], test_range['sy'], test_range['sz'], + test_range['scale'] + ) + assert len(biomes) == 16 * 1 * 16 # sx * sy * sz + assert biomes[0] == 21 + assert biomes[255] == 45 + +def test_is_viable_structure_pos(generator): + # This test checks if a given location is a viable spot for a structure. + seed = 1234567890 + generator.apply_seed(seed, DIM_OVERWORLD) + + # A valid position for a village. + valid_x, valid_z = 288, 1984 + flags = 0 + is_valid = generator.is_viable_structure_pos(Village, valid_x, valid_z, flags) + assert isinstance(is_valid, bool) + assert is_valid == True + + # A position that is not viable. + invalid_x, invalid_z = 1000, 1000 + is_invalid = generator.is_viable_structure_pos(Village, invalid_x, invalid_z, flags) + assert isinstance(is_invalid, bool) + assert is_invalid == False + +def test_map_approx_height(generator): + # This test verifies the approximate height mapping function. + # It requires a SurfaceNoise object and specific coordinates. + try: + from pybiomes import SurfaceNoise + except ImportError: + pytest.skip("SurfaceNoise is not available, skipping test.") + + seed = 1234567890 + surface_noise = SurfaceNoise() + surface_noise.init_surface_noise(DIM_OVERWORLD, seed) + + generator.apply_seed(seed, DIM_OVERWORLD) + # Get height and biome IDs for a single block. + x, z, w, h = 288, 1984, 1, 1 + y_list, ids_list = generator.map_approx_height(surface_noise, x>>2, z>>2, w, h) + + assert isinstance(y_list, list) + assert isinstance(ids_list, list) + assert len(y_list) == w * h + assert len(ids_list) == w * h + + # The height value will be a float, so we need to allow for some tolerance. + assert pytest.approx(y_list[0], 0.01) == 77.12 + assert ids_list[0] == plains