diff options
-rw-r--r-- | builtin/game/features.lua | 1 | ||||
-rw-r--r-- | doc/lua_api.txt | 346 | ||||
-rw-r--r-- | src/client/particles.cpp | 442 | ||||
-rw-r--r-- | src/client/particles.h | 70 | ||||
-rw-r--r-- | src/network/clientpackethandler.cpp | 89 | ||||
-rw-r--r-- | src/network/networkprotocol.h | 95 | ||||
-rw-r--r-- | src/particles.cpp | 127 | ||||
-rw-r--r-- | src/particles.h | 375 | ||||
-rw-r--r-- | src/script/common/c_content.cpp | 2 | ||||
-rw-r--r-- | src/script/common/c_converter.h | 1 | ||||
-rw-r--r-- | src/script/lua_api/l_particleparams.h | 279 | ||||
-rw-r--r-- | src/script/lua_api/l_particles.cpp | 214 | ||||
-rw-r--r-- | src/script/lua_api/l_particles_local.cpp | 114 | ||||
-rw-r--r-- | src/server.cpp | 75 | ||||
-rw-r--r-- | src/util/numeric.cpp | 11 | ||||
-rw-r--r-- | src/util/numeric.h | 23 | ||||
-rw-r--r-- | src/util/pointer.h | 13 |
17 files changed, 1992 insertions, 285 deletions
diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 0d55bb01f..73b16361e 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -21,6 +21,7 @@ core.features = { use_texture_alpha_string_modes = true, degrotate_240_steps = true, abm_min_max_y = true, + particlespawner_tweenable = true, dynamic_add_media_table = true, get_sky_as_table = true, } diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 2bf1e2171..f7fdad56e 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -4856,6 +4856,11 @@ Utilities abm_min_max_y = true, -- dynamic_add_media supports passing a table with options (5.5.0) dynamic_add_media_table = true, + -- particlespawners support texpools and animation of properties, + -- particle textures support smooth fade and scale animations, and + -- sprite-sheet particle animations can by synced to the lifetime + -- of individual particles (5.6.0) + particlespawner_tweenable = true, -- allows get_sky to return a table instead of separate values (5.6.0) get_sky_as_table = true, } @@ -8984,6 +8989,8 @@ Used by `minetest.add_particle`. texture = "image.png", -- The texture of the particle + -- v5.6.0 and later: also supports the table format described in the + -- following section playername = "singleplayer", -- Optional, if specified spawns particle only on the player's client @@ -9005,6 +9012,12 @@ Used by `minetest.add_particle`. -- If set to a valid number 1-6, specifies the tile from which the -- particle texture is picked. -- Otherwise, the default behavior is used. (currently: any random tile) + + drag = {x=0, y=0, z=0}, + -- v5.6.0 and later: Optional drag value, consult the following section + + bounce = {min = ..., max = ..., bias = 0}, + -- v5.6.0 and later: Optional bounce range, consult the following section } @@ -9013,7 +9026,20 @@ Used by `minetest.add_particle`. Used by `minetest.add_particlespawner`. +Before v5.6.0, particlespawners used a different syntax and had a more limited set +of features. Definition fields that are the same in both legacy and modern versions +are shown in the next listing, and the fields that are used by legacy versions are +shown separated by a comment; the modern fields are too complex to compactly +describe in this manner and are documented after the listing. + +The older syntax can be used in combination with the newer syntax (e.g. having +`minpos`, `maxpos`, and `pos` all set) to support older servers. On newer servers, +the new syntax will override the older syntax; on older servers, the newer syntax +will be ignored. + { + -- Common fields (same name and meaning in both new and legacy syntax) + amount = 1, -- Number of particles spawned over the time period `time`. @@ -9022,22 +9048,6 @@ Used by `minetest.add_particlespawner`. -- If time is 0 spawner has infinite lifespan and spawns the `amount` on -- a per-second basis. - minpos = {x=0, y=0, z=0}, - maxpos = {x=0, y=0, z=0}, - minvel = {x=0, y=0, z=0}, - maxvel = {x=0, y=0, z=0}, - minacc = {x=0, y=0, z=0}, - maxacc = {x=0, y=0, z=0}, - minexptime = 1, - maxexptime = 1, - minsize = 1, - maxsize = 1, - -- The particles' properties are random values between the min and max - -- values. - -- applies to: pos, velocity, acceleration, expirationtime, size - -- If `node` is set, min and maxsize can be set to 0 to spawn - -- randomly-sized particles (just like actual node dig particles). - collisiondetection = false, -- If true collide with `walkable` nodes and, depending on the -- `object_collision` field, objects too. @@ -9066,8 +9076,11 @@ Used by `minetest.add_particlespawner`. animation = {Tile Animation definition}, -- Optional, specifies how to animate the particles' texture + -- v5.6.0 and later: set length to -1 to sychronize the length + -- of the animation with the expiration time of individual particles. + -- (-2 causes the animation to be played twice, and so on) - glow = 0 + glow = 0, -- Optional, specify particle self-luminescence in darkness. -- Values 0-14. @@ -9081,8 +9094,307 @@ Used by `minetest.add_particlespawner`. -- If set to a valid number 1-6, specifies the tile from which the -- particle texture is picked. -- Otherwise, the default behavior is used. (currently: any random tile) + + -- Legacy definition fields + + minpos = {x=0, y=0, z=0}, + maxpos = {x=0, y=0, z=0}, + minvel = {x=0, y=0, z=0}, + maxvel = {x=0, y=0, z=0}, + minacc = {x=0, y=0, z=0}, + maxacc = {x=0, y=0, z=0}, + minexptime = 1, + maxexptime = 1, + minsize = 1, + maxsize = 1, + -- The particles' properties are random values between the min and max + -- values. + -- applies to: pos, velocity, acceleration, expirationtime, size + -- If `node` is set, min and maxsize can be set to 0 to spawn + -- randomly-sized particles (just like actual node dig particles). + } + +### Modern definition fields + +After v5.6.0, spawner properties can be defined in several different ways depending +on the level of control you need. `pos` for instance can be set as a single vector, +in which case all particles will appear at that exact point throughout the lifetime +of the spawner. Alternately, it can be specified as a min-max pair, specifying a +cubic range the particles can appear randomly within. Finally, some properties can +be animated by suffixing their key with `_tween` (e.g. `pos_tween`) and supplying +a tween table. + +The following definitions are all equivalent, listed in order of precedence from +lowest (the legacy syntax) to highest (tween tables). If multiple forms of a +property definition are present, the highest-precidence form will be selected +and all lower-precedence fields will be ignored, allowing for graceful +degradation in older clients). + + { + -- old syntax + maxpos = {x = 0, y = 0, z = 0}, + minpos = {x = 0, y = 0, z = 0}, + + -- absolute value + pos = 0, + -- all components of every particle's position vector will be set to this + -- value + + -- vec3 + pos = vector.new(0,0,0), + -- all particles will appear at this exact position throughout the lifetime + -- of the particlespawner + + -- vec3 range + pos = { + -- the particle will appear at a position that is picked at random from + -- within a cubic range + + min = vector.new(0,0,0), + -- `min` is the minimum value this property will be set to in particles + -- spawned by the generator + + max = vector.new(0,0,0), + -- `max` is the minimum value this property will be set to in particles + -- spawned by the generator + + bias = 0, + -- when `bias` is 0, all random values are exactly as likely as any + -- other. when it is positive, the higher it is, the more likely values + -- will appear towards the minimum end of the allowed spectrum. when + -- it is negative, the lower it is, the more likely values will appear + -- towards the maximum end of the allowed spectrum. the curve is + -- exponential and there is no particular maximum or minimum value + }, + + -- tween table + pos_tween = {...}, + -- a tween table should consist of a list of frames in the same form as the + -- untweened pos property above, which the engine will interpolate between, + -- and optionally a number of properties that control how the interpolation + -- takes place. currently **only two frames**, the first and the last, are + -- used, but extra frames are accepted for the sake of forward compatibility. + -- any of the above definition styles can be used here as well in any combination + -- supported by the property type + + pos_tween = { + style = "fwd", + -- linear animation from first to last frame (default) + style = "rev", + -- linear animation from last to first frame + style = "pulse", + -- linear animation from first to last then back to first again + style = "flicker", + -- like "pulse", but slightly randomized to add a bit of stutter + + reps = 1, + -- number of times the animation is played over the particle's lifespan + + start = 0.0, + -- point in the spawner's lifespan at which the animation begins. 0 is + -- the very beginning, 1 is the very end + + -- frames can be defined in a number of different ways, depending on the + -- underlying type of the property. for now, all but the first and last + -- frame are ignored + + -- frames + + -- floats + 0, 0, + + -- vec3s + vector.new(0,0,0), + vector.new(0,0,0), + + -- vec3 ranges + { min = vector.new(0,0,0), max = vector.new(0,0,0), bias = 0 }, + { min = vector.new(0,0,0), max = vector.new(0,0,0), bias = 0 }, + + -- mixed + 0, { min = vector.new(0,0,0), max = vector.new(0,0,0), bias = 0 }, + }, } +All of the properties that can be defined in this way are listed in the next +section, along with the datatypes they accept. + +#### List of particlespawner properties +All of the properties in this list can be animated with `*_tween` tables +unless otherwise specified. For example, `jitter` can be tweened by setting +a `jitter_tween` table instead of (or in addition to) a `jitter` table/value. +Types used are defined in the previous section. + +* vec3 range `pos`: the position at which particles can appear +* vec3 range `vel`: the initial velocity of the particle +* vec3 range `acc`: the direction and speed with which the particle + accelerates +* vec3 range `jitter`: offsets the velocity of each particle by a random + amount within the specified range each frame. used to create Brownian motion. +* vec3 range `drag`: the amount by which absolute particle velocity along + each axis is decreased per second. a value of 1.0 means that the particle + will be slowed to a stop over the space of a second; a value of -1.0 means + that the particle speed will be doubled every second. to avoid interfering + with gravity provided by `acc`, a drag vector like `vector.new(1,0,1)` can + be used instead of a uniform value. +* float range `bounce`: how bouncy the particles are when `collisiondetection` + is turned on. values less than or equal to `0` turn off particle bounce; + `1` makes the particles bounce without losing any velocity, and `2` makes + them double their velocity with every bounce. `bounce` is not bounded but + values much larger than `1.0` probably aren't very useful. +* float range `exptime`: the number of seconds after which the particle + disappears. +* table `attract`: sets the birth orientation of particles relative to various + shapes defined in world coordinate space. this is an alternative means of + setting the velocity which allows particles to emerge from or enter into + some entity or node on the map, rather than simply being assigned random + velocity values within a range. the velocity calculated by this method will + be **added** to that specified by `vel` if `vel` is also set, so in most + cases **`vel` should be set to 0**. `attract` has the fields: + * string `kind`: selects the kind of shape towards which the particles will + be oriented. it must have one of the following values: + * `"none"`: no attractor is set and the `attractor` table is ignored + * `"point"`: the particles are attracted to a specific point in space. + use this also if you want a sphere-like effect, in combination with + the `radius` property. + * `"line"`: the particles are attracted to an (infinite) line passing + through the points `origin` and `angle`. use this for e.g. beacon + effects, energy beam effects, etc. + * `"plane"`: the particles are attracted to an (infinite) plane on whose + surface `origin` designates a point in world coordinate space. use this + for e.g. particles entering or emerging from a portal. + * float range `strength`: the speed with which particles will move towards + `attractor`. If negative, the particles will instead move away from that + point. + * vec3 `origin`: the origin point of the shape towards which particles will + initially be oriented. functions as an offset if `origin_attached` is also + set. + * vec3 `direction`: sets the direction in which the attractor shape faces. for + lines, this sets the angle of the line; e.g. a vector of (0,1,0) will + create a vertical line that passes through `origin`. for planes, `direction` + is the surface normal of an infinite plane on whose surface `origin` is + a point. functions as an offset if `direction_attached` is also set. + * entity `origin_attached`: allows the origin to be specified as an offset + from the position of an entity rather than a coordinate in world space. + * entity `direction_attached`: allows the direction to be specified as an offset + from the position of an entity rather than a coordinate in world space. + * bool `die_on_contact`: if true, the particles' lifetimes are adjusted so + that they will die as they cross the attractor threshold. this behavior + is the default but is undesirable for some kinds of animations; set it to + false to allow particles to live out their natural lives. +* vec3 range `radius`: if set, particles will be arranged in a sphere around + `pos`. A constant can be used to create a spherical shell of particles, a + vector to create an ovoid shell, and a range to create a volume; e.g. + `{min = 0.5, max = 1, bias = 1}` will allow particles to appear between 0.5 + and 1 nodes away from `pos` but will cluster them towards the center of the + sphere. Usually if `radius` is used, `pos` should be a single point, but it + can still be a range if you really know what you're doing (e.g. to create a + "roundcube" emitter volume). + +### Textures + +In versions before v5.6.0, particlespawner textures could only be specified as a single +texture string. After v5.6.0, textures can now be specified as a table as well. This +table contains options that allow simple animations to be applied to the texture. + + texture = { + name = "mymod_particle_texture.png", + -- the texture specification string + + alpha = 1.0, + -- controls how visible the particle is; at 1.0 the particle is fully + -- visible, at 0, it is completely invisible. + + alpha_tween = {1, 0}, + -- can be used instead of `alpha` to animate the alpha value over the + -- particle's lifetime. these tween tables work identically to the tween + -- tables used in particlespawner properties, except that time references + -- are understood with respect to the particle's lifetime, not the + -- spawner's. {1,0} fades the particle out over its lifetime. + + scale = 1, + scale = {x = 1, y = 1}, + -- scales the texture onscreen + + scale_tween = { + {x = 1, y = 1}, + {x = 0, y = 1}, + }, + -- animates the scale over the particle's lifetime. works like the + -- alpha_tween table, but can accept two-dimensional vectors as well as + -- integer values. the example value would cause the particle to shrink + -- in one dimension over the course of its life until it disappears + + blend = "alpha", + -- (default) blends transparent pixels with those they are drawn atop + -- according to the alpha channel of the source texture. useful for + -- e.g. material objects like rocks, dirt, smoke, or node chunks + blend = "add", + -- adds the value of pixels to those underneath them, modulo the sources + -- alpha channel. useful for e.g. bright light effects like sparks or fire + blend = "screen", + -- like "add" but less bright. useful for subtler light effecs. note that + -- this is NOT formally equivalent to the "screen" effect used in image + -- editors and compositors, as it does not respect the alpha channel of + -- of the image being blended + blend = "sub", + -- the inverse of "add"; the value of the source pixel is subtracted from + -- the pixel underneath it. a white pixel will turn whatever is underneath + -- it black; a black pixel will be "transparent". useful for creating + -- darkening effects + + animation = {Tile Animation definition}, + -- overrides the particlespawner's global animation property for a single + -- specific texture + } + +Instead of setting a single texture definition, it is also possible to set a +`texpool` property. A `texpool` consists of a list of possible particle textures. +Every time a particle is spawned, the engine will pick a texture at random from +the `texpool` and assign it as that particle's texture. You can also specify a +`texture` in addition to a `texpool`; the `texture` value will be ignored on newer +clients but will be sent to older (pre-v5.6.0) clients that do not implement +texpools. + + texpool = { + "mymod_particle_texture.png"; + { name = "mymod_spark.png", fade = "out" }, + { + name = "mymod_dust.png", + alpha = 0.3, + scale = 1.5, + animation = { + type = "vertical_frames", + aspect_w = 16, aspect_h = 16, + + length = 3, + -- the animation lasts for 3s and then repeats + length = -3, + -- repeat the animation three times over the particle's lifetime + -- (post-v5.6.0 clients only) + }, + }, + } + +#### List of animatable texture properties + +While animated particlespawner values vary over the course of the particlespawner's +lifetime, animated texture properties vary over the lifespans of the individual +particles spawned with that texture. So a particle with the texture property + + alpha_tween = { + 0.0, 1.0, + style = "pulse", + reps = 4, + } + +would be invisible at its spawning, pulse visible four times throughout its +lifespan, and then vanish again before expiring. + +* float `alpha` (0.0 - 1.0): controls the visibility of the texture +* vec2 `scale`: controls the size of the displayed billboard onscreen. Its units + are multiples of the parent particle's assigned size (see the `size` property above) + `HTTPRequest` definition ------------------------ diff --git a/src/client/particles.cpp b/src/client/particles.cpp index 288826a5f..a1de1bb98 100644 --- a/src/client/particles.cpp +++ b/src/client/particles.cpp @@ -34,23 +34,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" /* - Utility -*/ - -static f32 random_f32(f32 min, f32 max) -{ - return rand() / (float)RAND_MAX * (max - min) + min; -} - -static v3f random_v3f(v3f min, v3f max) -{ - return v3f( - random_f32(min.X, max.X), - random_f32(min.Y, max.Y), - random_f32(min.Z, max.Z)); -} - -/* Particle */ @@ -59,25 +42,71 @@ Particle::Particle( LocalPlayer *player, ClientEnvironment *env, const ParticleParameters &p, - video::ITexture *texture, + const ClientTexRef& texture, v2f texpos, v2f texsize, video::SColor color ): scene::ISceneNode(((Client *)gamedef)->getSceneManager()->getRootSceneNode(), - ((Client *)gamedef)->getSceneManager()) + ((Client *)gamedef)->getSceneManager()), + m_texture(texture) { // Misc m_gamedef = gamedef; m_env = env; + // translate blend modes to GL blend functions + video::E_BLEND_FACTOR bfsrc, bfdst; + video::E_BLEND_OPERATION blendop; + const auto blendmode = texture.tex != nullptr + ? texture.tex -> blendmode + : ParticleParamTypes::BlendMode::alpha; + + switch (blendmode) { + case ParticleParamTypes::BlendMode::alpha: + bfsrc = video::EBF_SRC_ALPHA; + bfdst = video::EBF_ONE_MINUS_SRC_ALPHA; + blendop = video::EBO_ADD; + break; + + case ParticleParamTypes::BlendMode::add: + bfsrc = video::EBF_SRC_ALPHA; + bfdst = video::EBF_DST_ALPHA; + blendop = video::EBO_ADD; + break; + + case ParticleParamTypes::BlendMode::sub: + bfsrc = video::EBF_SRC_ALPHA; + bfdst = video::EBF_DST_ALPHA; + blendop = video::EBO_REVSUBTRACT; + break; + + case ParticleParamTypes::BlendMode::screen: + bfsrc = video::EBF_ONE; + bfdst = video::EBF_ONE_MINUS_SRC_COLOR; + blendop = video::EBO_ADD; + break; + + default: assert(false); + } + // Texture m_material.setFlag(video::EMF_LIGHTING, false); m_material.setFlag(video::EMF_BACK_FACE_CULLING, false); m_material.setFlag(video::EMF_BILINEAR_FILTER, false); m_material.setFlag(video::EMF_FOG_ENABLE, true); - m_material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; - m_material.setTexture(0, texture); + + // correctly render layered transparent particles -- see #10398 + m_material.setFlag(video::EMF_ZWRITE_ENABLE, true); + + // enable alpha blending and set blend mode + m_material.MaterialType = video::EMT_ONETEXTURE_BLEND; + m_material.MaterialTypeParam = video::pack_textureBlendFunc( + bfsrc, bfdst, + video::EMFN_MODULATE_1X, + video::EAS_TEXTURE | video::EAS_VERTEX_COLOR); + m_material.BlendOperation = blendop; + m_material.setTexture(0, m_texture.ref); m_texpos = texpos; m_texsize = texsize; m_animation = p.animation; @@ -90,6 +119,9 @@ Particle::Particle( m_pos = p.pos; m_velocity = p.vel; m_acceleration = p.acc; + m_drag = p.drag; + m_jitter = p.jitter; + m_bounce = p.bounce; m_expiration = p.expirationtime; m_player = player; m_size = p.size; @@ -98,6 +130,8 @@ Particle::Particle( m_object_collision = p.object_collision; m_vertical = p.vertical; m_glow = p.glow; + m_alpha = 0; + m_parent = nullptr; // Irrlicht stuff const float c = p.size / 2; @@ -111,6 +145,14 @@ Particle::Particle( updateVertices(); } +Particle::~Particle() +{ + /* if our textures aren't owned by a particlespawner, we need to clean + * them up ourselves when the particle dies */ + if (m_parent == nullptr) + delete m_texture.tex; +} + void Particle::OnRegisterSceneNode() { if (IsVisible) @@ -134,6 +176,12 @@ void Particle::render() void Particle::step(float dtime) { m_time += dtime; + + // apply drag (not handled by collisionMoveSimple) and brownian motion + v3f av = vecAbsolute(m_velocity); + av -= av * (m_drag * dtime); + m_velocity = av*vecSign(m_velocity) + v3f(m_jitter.pickWithin())*dtime; + if (m_collisiondetection) { aabb3f box = m_collisionbox; v3f p_pos = m_pos * BS; @@ -141,17 +189,41 @@ void Particle::step(float dtime) collisionMoveResult r = collisionMoveSimple(m_env, m_gamedef, BS * 0.5f, box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr, m_object_collision); - if (m_collision_removal && r.collides) { - // force expiration of the particle - m_expiration = -1.0; + + f32 bounciness = m_bounce.pickWithin(); + if (r.collides && (m_collision_removal || bounciness > 0)) { + if (m_collision_removal) { + // force expiration of the particle + m_expiration = -1.0f; + } else if (bounciness > 0) { + /* cheap way to get a decent bounce effect is to only invert the + * largest component of the velocity vector, so e.g. you don't + * have a rock immediately bounce back in your face when you try + * to skip it across the water (as would happen if we simply + * downscaled and negated the velocity vector). this means + * bounciness will work properly for cubic objects, but meshes + * with diagonal angles and entities will not yield the correct + * visual. this is probably unavoidable */ + if (av.Y > av.X && av.Y > av.Z) { + m_velocity.Y = -(m_velocity.Y * bounciness); + } else if (av.X > av.Y && av.X > av.Z) { + m_velocity.X = -(m_velocity.X * bounciness); + } else if (av.Z > av.Y && av.Z > av.X) { + m_velocity.Z = -(m_velocity.Z * bounciness); + } else { // well now we're in a bit of a pickle + m_velocity = -(m_velocity * bounciness); + } + } } else { - m_pos = p_pos / BS; m_velocity = p_velocity / BS; } + m_pos = p_pos / BS; } else { + // apply acceleration m_velocity += m_acceleration * dtime; m_pos += m_velocity * dtime; } + if (m_animation.type != TAT_NONE) { m_animation_time += dtime; int frame_length_i, frame_count; @@ -165,11 +237,21 @@ void Particle::step(float dtime) } } + // animate particle alpha in accordance with settings + if (m_texture.tex != nullptr) + m_alpha = m_texture.tex -> alpha.blend(m_time / (m_expiration+0.1f)); + else + m_alpha = 1.f; + // Update lighting updateLight(); // Update model updateVertices(); + + // Update position -- see #10398 + v3s16 camera_offset = m_env->getCameraOffset(); + setPosition(m_pos*BS - intToFloat(camera_offset, BS)); } void Particle::updateLight() @@ -189,7 +271,7 @@ void Particle::updateLight() light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0); u8 m_light = decode_light(light + m_glow); - m_color.set(255, + m_color.set(m_alpha*255, m_light * m_base_color.getRed() / 255, m_light * m_base_color.getGreen() / 255, m_light * m_base_color.getBlue() / 255); @@ -198,6 +280,12 @@ void Particle::updateLight() void Particle::updateVertices() { f32 tx0, tx1, ty0, ty1; + v2f scale; + + if (m_texture.tex != nullptr) + scale = m_texture.tex -> scale.blend(m_time / (m_expiration+0.1)); + else + scale = v2f(1.f, 1.f); if (m_animation.type != TAT_NONE) { const v2u32 texsize = m_material.getTexture(0)->getSize(); @@ -218,16 +306,24 @@ void Particle::updateVertices() ty1 = m_texpos.Y + m_texsize.Y; } - m_vertices[0] = video::S3DVertex(-m_size / 2, -m_size / 2, + auto half = m_size * .5f, + hx = half * scale.X, + hy = half * scale.Y; + m_vertices[0] = video::S3DVertex(-hx, -hy, 0, 0, 0, 0, m_color, tx0, ty1); - m_vertices[1] = video::S3DVertex(m_size / 2, -m_size / 2, + m_vertices[1] = video::S3DVertex(hx, -hy, 0, 0, 0, 0, m_color, tx1, ty1); - m_vertices[2] = video::S3DVertex(m_size / 2, m_size / 2, + m_vertices[2] = video::S3DVertex(hx, hy, 0, 0, 0, 0, m_color, tx1, ty0); - m_vertices[3] = video::S3DVertex(-m_size / 2, m_size / 2, + m_vertices[3] = video::S3DVertex(-hx, hy, 0, 0, 0, 0, m_color, tx0, ty0); - v3s16 camera_offset = m_env->getCameraOffset(); + + // see #10398 + // v3s16 camera_offset = m_env->getCameraOffset(); + // particle position is now handled by step() + m_box.reset(v3f()); + for (video::S3DVertex &vertex : m_vertices) { if (m_vertical) { v3f ppos = m_player->getPosition()/BS; @@ -238,7 +334,6 @@ void Particle::updateVertices() vertex.Pos.rotateXZBy(m_player->getYaw()); } m_box.addInternalPoint(vertex.Pos); - vertex.Pos += m_pos*BS - intToFloat(camera_offset, BS); } } @@ -251,7 +346,8 @@ ParticleSpawner::ParticleSpawner( LocalPlayer *player, const ParticleSpawnerParameters &p, u16 attached_id, - video::ITexture *texture, + std::unique_ptr<ClientTexture[]>& texpool, + size_t texcount, ParticleManager *p_manager ): m_particlemanager(p_manager), p(p) @@ -259,21 +355,66 @@ ParticleSpawner::ParticleSpawner( m_gamedef = gamedef; m_player = player; m_attached_id = attached_id; - m_texture = texture; + m_texpool = std::move(texpool); + m_texcount = texcount; m_time = 0; + m_active = 0; + m_dying = false; m_spawntimes.reserve(p.amount + 1); for (u16 i = 0; i <= p.amount; i++) { - float spawntime = rand() / (float)RAND_MAX * p.time; + float spawntime = myrand_float() * p.time; m_spawntimes.push_back(spawntime); } + + size_t max_particles = 0; // maximum number of particles likely to be visible at any given time + if (p.time != 0) { + auto maxGenerations = p.time / std::min(p.exptime.start.min, p.exptime.end.min); + max_particles = p.amount / maxGenerations; + } else { + auto longestLife = std::max(p.exptime.start.max, p.exptime.end.max); + max_particles = p.amount * longestLife; + } + + p_manager->reserveParticleSpace(max_particles * 1.2); +} + +namespace { + GenericCAO *findObjectByID(ClientEnvironment *env, u16 id) { + if (id == 0) + return nullptr; + return env->getGenericCAO(id); + } } void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, const core::matrix4 *attached_absolute_pos_rot_matrix) { + float fac = 0; + if (p.time != 0) { // ensure safety from divide-by-zeroes + fac = m_time / (p.time+0.1f); + } + + auto r_pos = p.pos.blend(fac); + auto r_vel = p.vel.blend(fac); + auto r_acc = p.acc.blend(fac); + auto r_drag = p.drag.blend(fac); + auto r_radius = p.radius.blend(fac); + auto r_jitter = p.jitter.blend(fac); + auto r_bounce = p.bounce.blend(fac); + v3f attractor_origin = p.attractor_origin.blend(fac); + v3f attractor_direction = p.attractor_direction.blend(fac); + auto attractor_obj = findObjectByID(env, p.attractor_attachment); + auto attractor_direction_obj = findObjectByID(env, p.attractor_direction_attachment); + + auto r_exp = p.exptime.blend(fac); + auto r_size = p.size.blend(fac); + auto r_attract = p.attract.blend(fac); + auto attract = r_attract.pickWithin(); + v3f ppos = m_player->getPosition() / BS; - v3f pos = random_v3f(p.minpos, p.maxpos); + v3f pos = r_pos.pickWithin(); + v3f sphere_radius = r_radius.pickWithin(); // Need to apply this first or the following check // will be wrong for attached spawners @@ -287,15 +428,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, pos.Z += camera_offset.Z; } - if (pos.getDistanceFrom(ppos) > radius) + if (pos.getDistanceFromSQ(ppos) > radius*radius) return; // Parameters for the single particle we're about to spawn ParticleParameters pp; pp.pos = pos; - pp.vel = random_v3f(p.minvel, p.maxvel); - pp.acc = random_v3f(p.minacc, p.maxacc); + pp.vel = r_vel.pickWithin(); + pp.acc = r_acc.pickWithin(); + pp.drag = r_drag.pickWithin(); + pp.jitter = r_jitter; + pp.bounce = r_bounce; if (attached_absolute_pos_rot_matrix) { // Apply attachment rotation @@ -303,30 +447,137 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, attached_absolute_pos_rot_matrix->rotateVect(pp.acc); } - pp.expirationtime = random_f32(p.minexptime, p.maxexptime); + if (attractor_obj) + attractor_origin += attractor_obj->getPosition() / BS; + if (attractor_direction_obj) { + auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix(); + if (attractor_absolute_pos_rot_matrix) + attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction); + } + + pp.expirationtime = r_exp.pickWithin(); + + if (sphere_radius != v3f()) { + f32 l = sphere_radius.getLength(); + v3f mag = sphere_radius; + mag.normalize(); + + v3f ofs = v3f(l,0,0); + ofs.rotateXZBy(myrand_range(0.f,360.f)); + ofs.rotateYZBy(myrand_range(0.f,360.f)); + ofs.rotateXYBy(myrand_range(0.f,360.f)); + + pp.pos += ofs * mag; + } + + if (p.attractor_kind != ParticleParamTypes::AttractorKind::none && attract != 0) { + v3f dir; + f32 dist = 0; /* =0 necessary to silence warning */ + switch (p.attractor_kind) { + case ParticleParamTypes::AttractorKind::none: + break; + + case ParticleParamTypes::AttractorKind::point: { + dist = pp.pos.getDistanceFrom(attractor_origin); + dir = pp.pos - attractor_origin; + dir.normalize(); + break; + } + + case ParticleParamTypes::AttractorKind::line: { + // https://github.com/minetest/minetest/issues/11505#issuecomment-915612700 + const auto& lorigin = attractor_origin; + v3f ldir = attractor_direction; + ldir.normalize(); + auto origin_to_point = pp.pos - lorigin; + auto scalar_projection = origin_to_point.dotProduct(ldir); + auto point_on_line = lorigin + (ldir * scalar_projection); + + dist = pp.pos.getDistanceFrom(point_on_line); + dir = (point_on_line - pp.pos); + dir.normalize(); + dir *= -1; // flip it around so strength=1 attracts, not repulses + break; + } + + case ParticleParamTypes::AttractorKind::plane: { + // https://github.com/minetest/minetest/issues/11505#issuecomment-915612700 + const v3f& porigin = attractor_origin; + v3f normal = attractor_direction; + normal.normalize(); + v3f point_to_origin = porigin - pp.pos; + f32 factor = normal.dotProduct(point_to_origin); + if (numericAbsolute(factor) == 0.0f) { + dir = normal; + } else { + factor = numericSign(factor); + dir = normal * factor; + } + dist = numericAbsolute(normal.dotProduct(pp.pos - porigin)); + dir *= -1; // flip it around so strength=1 attracts, not repulses + break; + } + } + + f32 speedTowards = numericAbsolute(attract) * dist; + v3f avel = dir * speedTowards; + if (attract > 0 && speedTowards > 0) { + avel *= -1; + if (p.attractor_kill) { + // make sure the particle dies after crossing the attractor threshold + f32 timeToCenter = dist / speedTowards; + if (timeToCenter < pp.expirationtime) + pp.expirationtime = timeToCenter; + } + } + pp.vel += avel; + } + p.copyCommon(pp); - video::ITexture *texture; + ClientTexRef texture; v2f texpos, texsize; video::SColor color(0xFFFFFFFF); if (p.node.getContent() != CONTENT_IGNORE) { const ContentFeatures &f = m_particlemanager->m_env->getGameDef()->ndef()->get(p.node); - if (!ParticleManager::getNodeParticleParams(p.node, f, pp, &texture, + if (!ParticleManager::getNodeParticleParams(p.node, f, pp, &texture.ref, texpos, texsize, &color, p.node_tile)) return; } else { - texture = m_texture; + if (m_texcount == 0) + return; + texture = decltype(texture)(m_texpool[m_texcount == 1 ? 0 : myrand_range(0,m_texcount-1)]); texpos = v2f(0.0f, 0.0f); texsize = v2f(1.0f, 1.0f); + if (texture.tex->animated) + pp.animation = texture.tex->animation; + } + + // synchronize animation length with particle life if desired + if (pp.animation.type != TAT_NONE) { + if (pp.animation.type == TAT_VERTICAL_FRAMES && + pp.animation.vertical_frames.length < 0) { + auto& a = pp.animation.vertical_frames; + // we add a tiny extra value to prevent the first frame + // from flickering back on just before the particle dies + a.length = (pp.expirationtime / -a.length) + 0.1; + } else if (pp.animation.type == TAT_SHEET_2D && + pp.animation.sheet_2d.frame_length < 0) { + auto& a = pp.animation.sheet_2d; + auto frames = a.frames_w * a.frames_h; + auto runtime = (pp.expirationtime / -a.frame_length) + 0.1; + pp.animation.sheet_2d.frame_length = frames / runtime; + } } // Allow keeping default random size - if (p.maxsize > 0.0f) - pp.size = random_f32(p.minsize, p.maxsize); + if (p.size.start.max > 0.0f || p.size.end.max > 0.0f) + pp.size = r_size.pickWithin(); - m_particlemanager->addParticle(new Particle( + ++m_active; + auto pa = new Particle( m_gamedef, m_player, env, @@ -335,7 +586,9 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, texpos, texsize, color - )); + ); + pa->m_parent = this; + m_particlemanager->addParticle(pa); } void ParticleSpawner::step(float dtime, ClientEnvironment *env) @@ -348,7 +601,7 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env) bool unloaded = false; const core::matrix4 *attached_absolute_pos_rot_matrix = nullptr; if (m_attached_id) { - if (GenericCAO *attached = dynamic_cast<GenericCAO *>(env->getActiveObject(m_attached_id))) { + if (GenericCAO *attached = env->getGenericCAO(m_attached_id)) { attached_absolute_pos_rot_matrix = attached->getAbsolutePosRotMatrix(); } else { unloaded = true; @@ -379,7 +632,7 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env) return; for (int i = 0; i <= p.amount; i++) { - if (rand() / (float)RAND_MAX < dtime) + if (myrand_float() < dtime) spawnParticle(env, radius, attached_absolute_pos_rot_matrix); } } @@ -408,9 +661,15 @@ void ParticleManager::stepSpawners(float dtime) { MutexAutoLock lock(m_spawner_list_lock); for (auto i = m_particle_spawners.begin(); i != m_particle_spawners.end();) { - if (i->second->get_expired()) { - delete i->second; - m_particle_spawners.erase(i++); + if (i->second->getExpired()) { + // the particlespawner owns the textures, so we need to make + // sure there are no active particles before we free it + if (i->second->m_active == 0) { + delete i->second; + m_particle_spawners.erase(i++); + } else { + ++i; + } } else { i->second->step(dtime, m_env); ++i; @@ -423,6 +682,10 @@ void ParticleManager::stepParticles(float dtime) MutexAutoLock lock(m_particle_list_lock); for (auto i = m_particles.begin(); i != m_particles.end();) { if ((*i)->get_expired()) { + if ((*i)->m_parent) { + assert((*i)->m_parent->m_active != 0); + --(*i)->m_parent->m_active; + } (*i)->remove(); delete *i; i = m_particles.erase(i); @@ -464,13 +727,29 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, const ParticleSpawnerParameters &p = *event->add_particlespawner.p; - video::ITexture *texture = - client->tsrc()->getTextureForMesh(p.texture); + // texture pool + std::unique_ptr<ClientTexture[]> texpool = nullptr; + size_t txpsz = 0; + if (!p.texpool.empty()) { + txpsz = p.texpool.size(); + texpool = decltype(texpool)(new ClientTexture [txpsz]); + + for (size_t i = 0; i < txpsz; ++i) { + texpool[i] = ClientTexture(p.texpool[i], client->tsrc()); + } + } else { + // no texpool in use, use fallback texture + txpsz = 1; + texpool = decltype(texpool)(new ClientTexture[1] { + ClientTexture(p.texture, client->tsrc()) + }); + } auto toadd = new ParticleSpawner(client, player, p, event->add_particlespawner.attached_id, - texture, + texpool, + txpsz, this); addParticleSpawner(event->add_particlespawner.id, toadd); @@ -481,7 +760,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, case CE_SPAWN_PARTICLE: { ParticleParameters &p = *event->spawn_particle; - video::ITexture *texture; + ClientTexRef texture; v2f texpos, texsize; video::SColor color(0xFFFFFFFF); @@ -489,11 +768,15 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, if (p.node.getContent() != CONTENT_IGNORE) { const ContentFeatures &f = m_env->getGameDef()->ndef()->get(p.node); - if (!getNodeParticleParams(p.node, f, p, &texture, texpos, - texsize, &color, p.node_tile)) - texture = nullptr; + getNodeParticleParams(p.node, f, p, &texture.ref, texpos, + texsize, &color, p.node_tile); } else { - texture = client->tsrc()->getTextureForMesh(p.texture); + /* with no particlespawner to own the texture, we need + * to save it on the heap. it will be freed when the + * particle is destroyed */ + auto texstore = new ClientTexture(p.texture, client->tsrc()); + + texture = ClientTexRef(*texstore); texpos = v2f(0.0f, 0.0f); texsize = v2f(1.0f, 1.0f); } @@ -502,7 +785,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, if (oldsize > 0.0f) p.size = oldsize; - if (texture) { + if (texture.ref) { Particle *toadd = new Particle(client, player, m_env, p, texture, texpos, texsize, color); @@ -529,7 +812,7 @@ bool ParticleManager::getNodeParticleParams(const MapNode &n, if (tilenum > 0 && tilenum <= 6) texid = tilenum - 1; else - texid = rand() % 6; + texid = myrand_range(0,5); const TileLayer &tile = f.tiles[texid].layers[0]; p.animation.type = TAT_NONE; @@ -539,13 +822,13 @@ bool ParticleManager::getNodeParticleParams(const MapNode &n, else *texture = tile.texture; - float size = (rand() % 8) / 64.0f; + float size = (myrand_range(0,8)) / 64.0f; p.size = BS * size; if (tile.scale) size /= tile.scale; texsize = v2f(size * 2.0f, size * 2.0f); - texpos.X = (rand() % 64) / 64.0f - texsize.X; - texpos.Y = (rand() % 64) / 64.0f - texsize.Y; + texpos.X = (myrand_range(0,64)) / 64.0f - texsize.X; + texpos.Y = (myrand_range(0,64)) / 64.0f - texsize.Y; if (tile.has_color) *color = tile.color; @@ -577,20 +860,20 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef, LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f) { ParticleParameters p; - video::ITexture *texture; + video::ITexture *ref = nullptr; v2f texpos, texsize; video::SColor color; - if (!getNodeParticleParams(n, f, p, &texture, texpos, texsize, &color)) + if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color)) return; - p.expirationtime = (rand() % 100) / 100.0f; + p.expirationtime = myrand_range(0, 100) / 100.0f; // Physics p.vel = v3f( - (rand() % 150) / 50.0f - 1.5f, - (rand() % 150) / 50.0f, - (rand() % 150) / 50.0f - 1.5f + myrand_range(-1.5f,1.5f), + myrand_range(0.f,3.f), + myrand_range(-1.5f,1.5f) ); p.acc = v3f( 0.0f, @@ -598,9 +881,9 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef, 0.0f ); p.pos = v3f( - (f32)pos.X + (rand() % 100) / 200.0f - 0.25f, - (f32)pos.Y + (rand() % 100) / 200.0f - 0.25f, - (f32)pos.Z + (rand() % 100) / 200.0f - 0.25f + (f32)pos.X + myrand_range(0.f, .5f) - .25f, + (f32)pos.Y + myrand_range(0.f, .5f) - .25f, + (f32)pos.Z + myrand_range(0.f, .5f) - .25f ); Particle *toadd = new Particle( @@ -608,7 +891,7 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef, player, m_env, p, - texture, + ClientTexRef(ref), texpos, texsize, color); @@ -616,6 +899,12 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef, addParticle(toadd); } +void ParticleManager::reserveParticleSpace(size_t max_estimate) +{ + MutexAutoLock lock(m_particle_list_lock); + m_particles.reserve(m_particles.size() + max_estimate); +} + void ParticleManager::addParticle(Particle *toadd) { MutexAutoLock lock(m_particle_list_lock); @@ -634,7 +923,6 @@ void ParticleManager::deleteParticleSpawner(u64 id) MutexAutoLock lock(m_spawner_list_lock); auto it = m_particle_spawners.find(id); if (it != m_particle_spawners.end()) { - delete it->second; - m_particle_spawners.erase(it); + it->second->setDying(); } } diff --git a/src/client/particles.h b/src/client/particles.h index 2011f0262..36be903f1 100644 --- a/src/client/particles.h +++ b/src/client/particles.h @@ -31,20 +31,53 @@ class ClientEnvironment; struct MapNode; struct ContentFeatures; +struct ClientTexture +{ + /* per-spawner structure used to store the ParticleTexture structs + * that spawned particles will refer to through ClientTexRef */ + ParticleTexture tex; + video::ITexture *ref = nullptr; + + ClientTexture() = default; + ClientTexture(const ClientTexture&) = default; + ClientTexture(const ServerParticleTexture& p, ITextureSource *t): + tex(p), + ref(t->getTextureForMesh(p.string)) {}; +}; + +struct ClientTexRef +{ + /* per-particle structure used to avoid massively duplicating the + * fairly large ParticleTexture struct */ + ParticleTexture* tex = nullptr; + video::ITexture* ref = nullptr; + ClientTexRef() = default; + ClientTexRef(const ClientTexRef&) = default; + + /* constructor used by particles spawned from a spawner */ + ClientTexRef(ClientTexture& t): + tex(&t.tex), ref(t.ref) {}; + + /* constructor used for node particles */ + ClientTexRef(decltype(ref) tp): ref(tp) {}; +}; + +class ParticleSpawner; + class Particle : public scene::ISceneNode { - public: +public: Particle( - IGameDef* gamedef, + IGameDef *gamedef, LocalPlayer *player, ClientEnvironment *env, const ParticleParameters &p, - video::ITexture *texture, + const ClientTexRef &texture, v2f texpos, v2f texsize, video::SColor color ); - ~Particle() = default; + ~Particle(); virtual const aabb3f &getBoundingBox() const { @@ -69,9 +102,12 @@ class Particle : public scene::ISceneNode bool get_expired () { return m_expiration < m_time; } + ParticleSpawner *m_parent; + private: void updateLight(); void updateVertices(); + void setVertexAlpha(float a); video::S3DVertex m_vertices[4]; float m_time = 0.0f; @@ -81,14 +117,19 @@ private: IGameDef *m_gamedef; aabb3f m_box; aabb3f m_collisionbox; + ClientTexRef m_texture; video::SMaterial m_material; v2f m_texpos; v2f m_texsize; v3f m_pos; v3f m_velocity; v3f m_acceleration; + v3f m_drag; + ParticleParamTypes::v3fRange m_jitter; + ParticleParamTypes::f32Range m_bounce; LocalPlayer *m_player; float m_size; + //! Color without lighting video::SColor m_base_color; //! Final rendered color @@ -102,24 +143,27 @@ private: float m_animation_time = 0.0f; int m_animation_frame = 0; u8 m_glow; + float m_alpha = 0.0f; }; class ParticleSpawner { public: - ParticleSpawner(IGameDef* gamedef, + ParticleSpawner(IGameDef *gamedef, LocalPlayer *player, const ParticleSpawnerParameters &p, u16 attached_id, - video::ITexture *texture, + std::unique_ptr<ClientTexture[]> &texpool, + size_t texcount, ParticleManager* p_manager); - ~ParticleSpawner() = default; - void step(float dtime, ClientEnvironment *env); - bool get_expired () - { return p.amount <= 0 && p.time != 0; } + size_t m_active; + + bool getExpired() const + { return m_dying || (p.amount <= 0 && p.time != 0); } + void setDying() { m_dying = true; } private: void spawnParticle(ClientEnvironment *env, float radius, @@ -127,10 +171,12 @@ private: ParticleManager *m_particlemanager; float m_time; + bool m_dying; IGameDef *m_gamedef; LocalPlayer *m_player; ParticleSpawnerParameters p; - video::ITexture *m_texture; + std::unique_ptr<ClientTexture[]> m_texpool; + size_t m_texcount; std::vector<float> m_spawntimes; u16 m_attached_id; }; @@ -156,6 +202,8 @@ public: void addNodeParticle(IGameDef *gamedef, LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f); + void reserveParticleSpace(size_t max_estimate); + /** * This function is only used by client particle spawners * diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index 1901c6675..25c1d2690 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -994,18 +994,18 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt) p.amount = readU16(is); p.time = readF32(is); - p.minpos = readV3F32(is); - p.maxpos = readV3F32(is); - p.minvel = readV3F32(is); - p.maxvel = readV3F32(is); - p.minacc = readV3F32(is); - p.maxacc = readV3F32(is); - p.minexptime = readF32(is); - p.maxexptime = readF32(is); - p.minsize = readF32(is); - p.maxsize = readF32(is); + + // older protocols do not support tweening, and send only + // static ranges, so we can't just use the normal serialization + // functions for the older values. + p.pos.start.legacyDeSerialize(is); + p.vel.start.legacyDeSerialize(is); + p.acc.start.legacyDeSerialize(is); + p.exptime.start.legacyDeSerialize(is); + p.size.start.legacyDeSerialize(is); + p.collisiondetection = readU8(is); - p.texture = deSerializeString32(is); + p.texture.string = deSerializeString32(is); server_id = readU32(is); @@ -1018,6 +1018,8 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt) p.glow = readU8(is); p.object_collision = readU8(is); + bool legacy_format = true; + // This is kinda awful do { u16 tmp_param0 = readU16(is); @@ -1026,7 +1028,70 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt) p.node.param0 = tmp_param0; p.node.param2 = readU8(is); p.node_tile = readU8(is); - } while (0); + + // v >= 5.6.0 + f32 tmp_sbias = readF32(is); + if (is.eof()) + break; + + // initial bias must be stored separately in the stream to preserve + // backwards compatibility with older clients, which do not support + // a bias field in their range "format" + p.pos.start.bias = tmp_sbias; + p.vel.start.bias = readF32(is); + p.acc.start.bias = readF32(is); + p.exptime.start.bias = readF32(is); + p.size.start.bias = readF32(is); + + p.pos.end.deSerialize(is); + p.vel.end.deSerialize(is); + p.acc.end.deSerialize(is); + p.exptime.end.deSerialize(is); + p.size.end.deSerialize(is); + + // properties for legacy texture field + p.texture.deSerialize(is, m_proto_ver, true); + + p.drag.deSerialize(is); + p.jitter.deSerialize(is); + p.bounce.deSerialize(is); + ParticleParamTypes::deSerializeParameterValue(is, p.attractor_kind); + using ParticleParamTypes::AttractorKind; + if (p.attractor_kind != AttractorKind::none) { + p.attract.deSerialize(is); + p.attractor_origin.deSerialize(is); + p.attractor_attachment = readU16(is); + /* we only check the first bit, in order to allow this value + * to be turned into a bit flag field later if needed */ + p.attractor_kill = !!(readU8(is) & 1); + if (p.attractor_kind != AttractorKind::point) { + p.attractor_direction.deSerialize(is); + p.attractor_direction_attachment = readU16(is); + } + } + p.radius.deSerialize(is); + + u16 texpoolsz = readU16(is); + p.texpool.reserve(texpoolsz); + for (u16 i = 0; i < texpoolsz; ++i) { + ServerParticleTexture newtex; + newtex.deSerialize(is, m_proto_ver); + p.texpool.push_back(newtex); + } + + legacy_format = false; + } while(0); + + if (legacy_format) { + // there's no tweening data to be had, so we need to set the + // legacy params to constant values, otherwise everything old + // will tween to zero + p.pos.end = p.pos.start; + p.vel.end = p.vel.start; + p.acc.end = p.acc.start; + p.exptime.end = p.exptime.start; + p.size.end = p.size.start; + } auto event = new ClientEvent(); event->type = CE_ADD_PARTICLESPAWNER; diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 33e49afa4..e3fd32866 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -207,6 +207,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Minimap modes PROTOCOL VERSION 40: TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added + Added new particlespawner parameters (5.6.0) */ #define LATEST_PROTOCOL_VERSION 40 @@ -511,11 +512,12 @@ enum ToClientCommand TOCLIENT_SPAWN_PARTICLE = 0x46, /* - v3f1000 pos - v3f1000 velocity - v3f1000 acceleration - f1000 expirationtime - f1000 size + -- struct range<T> { T min, T max, f32 bias }; + v3f pos + v3f velocity + v3f acceleration + f32 expirationtime + f32 size u8 bool collisiondetection u32 len u8[len] texture @@ -524,22 +526,26 @@ enum ToClientCommand TileAnimation animation u8 glow u8 object_collision + v3f drag + range<v3f> bounce */ TOCLIENT_ADD_PARTICLESPAWNER = 0x47, /* + -- struct range<T> { T min, T max, f32 bias }; + -- struct tween<T> { T start, T end }; u16 amount - f1000 spawntime - v3f1000 minpos - v3f1000 maxpos - v3f1000 minvel - v3f1000 maxvel - v3f1000 minacc - v3f1000 maxacc - f1000 minexptime - f1000 maxexptime - f1000 minsize - f1000 maxsize + f32 spawntime + v3f minpos + v3f maxpos + v3f minvel + v3f maxvel + v3f minacc + v3f maxacc + f32 minexptime + f32 maxexptime + f32 minsize + f32 maxsize u8 bool collisiondetection u32 len u8[len] texture @@ -549,6 +555,63 @@ enum ToClientCommand TileAnimation animation u8 glow u8 object_collision + + f32 pos_start_bias + f32 vel_start_bias + f32 acc_start_bias + f32 exptime_start_bias + f32 size_start_bias + + range<v3f> pos_end + -- i.e v3f pos_end_min + -- v3f pos_end_max + -- f32 pos_end_bias + range<v3f> vel_end + range<v3f> acc_end + + tween<range<v3f>> drag + -- i.e. v3f drag_start_min + -- v3f drag_start_max + -- f32 drag_start_bias + -- v3f drag_end_min + -- v3f drag_end_max + -- f32 drag_end_bias + tween<range<v3f>> jitter + tween<range<f32>> bounce + + u8 attraction_kind + none = 0 + point = 1 + line = 2 + plane = 3 + + if attraction_kind > none { + tween<range<f32>> attract_strength + tween<v3f> attractor_origin + u16 attractor_origin_attachment_object_id + u8 spawner_flags + bit 1: attractor_kill (particles dies on contact) + if attraction_mode > point { + tween<v3f> attractor_angle + u16 attractor_origin_attachment_object_id + } + } + + tween<range<v3f>> radius + tween<range<v3f>> drag + + u16 texpool_sz + texpool_sz.times { + u8 flags + -- bit 0: animated + -- other bits free & ignored as of proto v40 + tween<f32> alpha + tween<v2f> scale + if flags.animated { + TileAnimation animation + } + } + */ TOCLIENT_DELETE_PARTICLESPAWNER_LEGACY = 0x48, // Obsolete diff --git a/src/particles.cpp b/src/particles.cpp index 14c987958..19b3418b7 100644 --- a/src/particles.cpp +++ b/src/particles.cpp @@ -18,7 +18,103 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "particles.h" -#include "util/serialize.h" +#include <type_traits> +using namespace ParticleParamTypes; + +#define PARAM_PVFN(n) ParticleParamTypes::n##ParameterValue +v2f PARAM_PVFN(pick) (float* f, const v2f a, const v2f b) { + return v2f( + numericalBlend(f[0], a.X, b.X), + numericalBlend(f[1], a.Y, b.Y) + ); +} + +v3f PARAM_PVFN(pick) (float* f, const v3f a, const v3f b) { + return v3f( + numericalBlend(f[0], a.X, b.X), + numericalBlend(f[1], a.Y, b.Y), + numericalBlend(f[2], a.Z, b.Z) + ); +} + +v2f PARAM_PVFN(interpolate) (float fac, const v2f a, const v2f b) + { return b.getInterpolated(a, fac); } +v3f PARAM_PVFN(interpolate) (float fac, const v3f a, const v3f b) + { return b.getInterpolated(a, fac); } + +#define PARAM_DEF_SRZR(T, wr, rd) \ + void PARAM_PVFN(serialize) (std::ostream& os, T v) {wr(os,v); } \ + void PARAM_PVFN(deSerialize)(std::istream& is, T& v) {v = rd(is);} + + +#define PARAM_DEF_NUM(T, wr, rd) PARAM_DEF_SRZR(T, wr, rd) \ + T PARAM_PVFN(interpolate)(float fac, const T a, const T b) \ + { return numericalBlend<T>(fac,a,b); } \ + T PARAM_PVFN(pick) (float* f, const T a, const T b) \ + { return numericalBlend<T>(f[0],a,b); } + +PARAM_DEF_NUM(u8, writeU8, readU8); PARAM_DEF_NUM(s8, writeS8, readS8); +PARAM_DEF_NUM(u16, writeU16, readU16); PARAM_DEF_NUM(s16, writeS16, readS16); +PARAM_DEF_NUM(u32, writeU32, readU32); PARAM_DEF_NUM(s32, writeS32, readS32); +PARAM_DEF_NUM(f32, writeF32, readF32); +PARAM_DEF_SRZR(v2f, writeV2F32, readV2F32); +PARAM_DEF_SRZR(v3f, writeV3F32, readV3F32); + +enum class ParticleTextureFlags : u8 { + /* each value specifies a bit in a bitmask; if the maximum value + * goes above 1<<7 the type of the flags field must be changed + * from u8, which will necessitate a protocol change! */ + + // the first bit indicates whether the texture is animated + animated = 1, + + /* the next three bits indicate the blending mode of the texture + * blendmode is encoded by (flags |= (u8)blend << 1); retrieve with + * (flags & ParticleTextureFlags::blend) >> 1. note that the third + * bit is currently reserved for adding more blend modes in the future */ + blend = 0x7 << 1, +}; + +/* define some shorthand so we don't have to repeat ourselves or use + * decltype everywhere */ +using FlagT = std::underlying_type_t<ParticleTextureFlags>; + +void ServerParticleTexture::serialize(std::ostream &os, u16 protocol_ver, bool newPropertiesOnly) const +{ + /* newPropertiesOnly is used to de/serialize parameters of the legacy texture + * field, which are encoded separately from the texspec string */ + FlagT flags = 0; + if (animated) + flags |= FlagT(ParticleTextureFlags::animated); + if (blendmode != BlendMode::alpha) + flags |= FlagT(blendmode) << 1; + serializeParameterValue(os, flags); + + alpha.serialize(os); + scale.serialize(os); + if (!newPropertiesOnly) + os << serializeString32(string); + + if (animated) + animation.serialize(os, protocol_ver); +} + +void ServerParticleTexture::deSerialize(std::istream &is, u16 protocol_ver, bool newPropertiesOnly) +{ + FlagT flags = 0; + deSerializeParameterValue(is, flags); + + animated = !!(flags & FlagT(ParticleTextureFlags::animated)); + blendmode = BlendMode((flags & FlagT(ParticleTextureFlags::blend)) >> 1); + + alpha.deSerialize(is); + scale.deSerialize(is); + if (!newPropertiesOnly) + string = deSerializeString32(is); + + if (animated) + animation.deSerialize(is, protocol_ver); +} void ParticleParameters::serialize(std::ostream &os, u16 protocol_ver) const { @@ -28,7 +124,7 @@ void ParticleParameters::serialize(std::ostream &os, u16 protocol_ver) const writeF32(os, expirationtime); writeF32(os, size); writeU8(os, collisiondetection); - os << serializeString32(texture); + os << serializeString32(texture.string); writeU8(os, vertical); writeU8(os, collision_removal); animation.serialize(os, 6); /* NOT the protocol ver */ @@ -37,6 +133,20 @@ void ParticleParameters::serialize(std::ostream &os, u16 protocol_ver) const writeU16(os, node.param0); writeU8(os, node.param2); writeU8(os, node_tile); + writeV3F32(os, drag); + jitter.serialize(os); + bounce.serialize(os); +} + +template <typename T, T (reader)(std::istream& is)> +inline bool streamEndsBeforeParam(T& val, std::istream& is) +{ + // This is kinda awful + T tmp = reader(is); + if (is.eof()) + return true; + val = tmp; + return false; } void ParticleParameters::deSerialize(std::istream &is, u16 protocol_ver) @@ -47,17 +157,20 @@ void ParticleParameters::deSerialize(std::istream &is, u16 protocol_ver) expirationtime = readF32(is); size = readF32(is); collisiondetection = readU8(is); - texture = deSerializeString32(is); + texture.string = deSerializeString32(is); vertical = readU8(is); collision_removal = readU8(is); animation.deSerialize(is, 6); /* NOT the protocol ver */ glow = readU8(is); object_collision = readU8(is); - // This is kinda awful - u16 tmp_param0 = readU16(is); - if (is.eof()) + + if (streamEndsBeforeParam<u16, readU16>(node.param0, is)) return; - node.param0 = tmp_param0; node.param2 = readU8(is); node_tile = readU8(is); + + if (streamEndsBeforeParam<v3f, readV3F32>(drag, is)) + return; + jitter.deSerialize(is); + bounce.deSerialize(is); } diff --git a/src/particles.h b/src/particles.h index 6f518b771..622fee099 100644 --- a/src/particles.h +++ b/src/particles.h @@ -20,19 +20,352 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include <string> +#include <sstream> +#include <vector> +#include <ctgmath> +#include <type_traits> #include "irrlichttypes_bloated.h" #include "tileanimation.h" #include "mapnode.h" +#include "util/serialize.h" +#include "util/numeric.h" // This file defines the particle-related structures that both the server and // client need. The ParticleManager and rendering is in client/particles.h -struct CommonParticleParams { +namespace ParticleParamTypes +{ + template <bool cond, typename T> + using enableIf = typename std::enable_if<cond, T>::type; + // std::enable_if_t does not appear to be present in GCC???? + // std::is_enum_v also missing. wtf. these are supposed to be + // present as of c++14 + + template<typename T> using BlendFunction = T(float,T,T); + #define DECL_PARAM_SRZRS(type) \ + void serializeParameterValue (std::ostream& os, type v); \ + void deSerializeParameterValue(std::istream& is, type& r); + #define DECL_PARAM_OVERLOADS(type) DECL_PARAM_SRZRS(type) \ + type interpolateParameterValue(float fac, const type a, const type b); \ + type pickParameterValue (float* facs, const type a, const type b); + + DECL_PARAM_OVERLOADS(u8); DECL_PARAM_OVERLOADS(s8); + DECL_PARAM_OVERLOADS(u16); DECL_PARAM_OVERLOADS(s16); + DECL_PARAM_OVERLOADS(u32); DECL_PARAM_OVERLOADS(s32); + DECL_PARAM_OVERLOADS(f32); + DECL_PARAM_OVERLOADS(v2f); + DECL_PARAM_OVERLOADS(v3f); + + /* C++ is a strongly typed language. this means that enums cannot be implicitly + * cast to integers, as they can be in C. while this may sound good in principle, + * it means that our normal serialization functions cannot be called on + * enumerations unless they are explicitly cast to a particular type first. this + * is problematic, because in C++ enums can have any integral type as an underlying + * type, and that type would need to be named everywhere an enumeration is + * de/serialized. + * + * this is obviously not cool, both in terms of writing legible, succinct code, + * and in terms of robustness: the underlying type might be changed at some point, + * e.g. if a bitmask gets too big for its britches. we could use an equivalent of + * `std::to_underlying(value)` everywhere we need to deal with enumerations, but + * that's hideous and unintuitive. instead, we supply the following functions to + * transparently map enumeration types to their underlying values. */ + + template <typename E, enableIf<std::is_enum<E>::value, bool> = true> + void serializeParameterValue(std::ostream& os, E k) { + serializeParameterValue(os, (std::underlying_type_t<E>)k); + } + + template <typename E, enableIf<std::is_enum<E>::value, bool> = true> + void deSerializeParameterValue(std::istream& is, E& k) { + std::underlying_type_t<E> v; + deSerializeParameterValue(is, v); + k = (E)v; + } + + /* this is your brain on C++. */ + + template <typename T, size_t PN> + struct Parameter + { + using ValType = T; + using pickFactors = float[PN]; + + T val; + using This = Parameter<T, PN>; + + Parameter() = default; + Parameter(const This& a) = default; + template <typename... Args> + Parameter(Args... args) : val(args...) {} + + virtual void serialize(std::ostream &os) const + { serializeParameterValue (os, this->val); } + virtual void deSerialize(std::istream &is) + { deSerializeParameterValue(is, this->val); } + + virtual T interpolate(float fac, const This& against) const + { + return interpolateParameterValue(fac, this->val, against.val); + } + + static T pick(float* f, const This& a, const This& b) + { + return pickParameterValue(f, a.val, b.val); + } + + operator T() const { return val; } + T operator=(T b) { return val = b; } + + }; + + template <typename T> T numericalBlend(float fac, T min, T max) + { return min + ((max - min) * fac); } + + template <typename T, size_t N> + struct VectorParameter : public Parameter<T,N> { + using This = VectorParameter<T,N>; + template <typename... Args> + VectorParameter(Args... args) : Parameter<T,N>(args...) {} + }; + + template <typename T, size_t PN> + inline std::string dump(const Parameter<T,PN>& p) + { + return std::to_string(p.val); + } + + template <typename T, size_t N> + inline std::string dump(const VectorParameter<T,N>& v) + { + std::ostringstream oss; + if (N == 3) + oss << PP(v.val); + else + oss << PP2(v.val); + return oss.str(); + } + + using u8Parameter = Parameter<u8, 1>; using s8Parameter = Parameter<s8, 1>; + using u16Parameter = Parameter<u16, 1>; using s16Parameter = Parameter<s16, 1>; + using u32Parameter = Parameter<u32, 1>; using s32Parameter = Parameter<s32, 1>; + + using f32Parameter = Parameter<f32, 1>; + + using v2fParameter = VectorParameter<v2f, 2>; + using v3fParameter = VectorParameter<v3f, 3>; + + template <typename T> + struct RangedParameter + { + using ValType = T; + using This = RangedParameter<T>; + + T min, max; + f32 bias = 0; + + RangedParameter() = default; + RangedParameter(const This& a) = default; + RangedParameter(T _min, T _max) : min(_min), max(_max) {} + template <typename M> RangedParameter(M b) : min(b), max(b) {} + + // these functions handle the old range serialization "format"; bias must + // be manually encoded in a separate part of the stream. NEVER ADD FIELDS + // TO THESE FUNCTIONS + void legacySerialize(std::ostream& os) const + { + min.serialize(os); + max.serialize(os); + } + void legacyDeSerialize(std::istream& is) + { + min.deSerialize(is); + max.deSerialize(is); + } + + // these functions handle the format used by new fields. new fields go here + void serialize(std::ostream &os) const + { + legacySerialize(os); + writeF32(os, bias); + } + void deSerialize(std::istream &is) + { + legacyDeSerialize(is); + bias = readF32(is); + } + + This interpolate(float fac, const This against) const + { + This r; + r.min = min.interpolate(fac, against.min); + r.max = max.interpolate(fac, against.max); + r.bias = bias; + return r; + } + + T pickWithin() const + { + typename T::pickFactors values; + auto p = numericAbsolute(bias) + 1; + for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); ++i) { + if (bias < 0) + values[i] = 1.0f - pow(myrand_float(), p); + else + values[i] = pow(myrand_float(), p); + } + return T::pick(values, min, max); + } + + }; + + template <typename T> + inline std::string dump(const RangedParameter<T>& r) + { + std::ostringstream s; + s << "range<" << dump(r.min) << " ~ " << dump(r.max); + if (r.bias != 0) + s << " :: " << r.bias; + s << ">"; + return s.str(); + } + + enum class TweenStyle : u8 { fwd, rev, pulse, flicker }; + + template <typename T> + struct TweenedParameter + { + using ValType = T; + using This = TweenedParameter<T>; + + TweenStyle style = TweenStyle::fwd; + u16 reps = 1; + f32 beginning = 0.0f; + + T start, end; + + TweenedParameter() = default; + TweenedParameter(const This& a) = default; + TweenedParameter(T _start, T _end) : start(_start), end(_end) {} + template <typename M> TweenedParameter(M b) : start(b), end(b) {} + + T blend(float fac) const + { + // warp time coordinates in accordance w/ settings + if (fac > beginning) { + // remap for beginning offset + auto len = 1 - beginning; + fac -= beginning; + fac /= len; + + // remap for repetitions + fac *= reps; + if (fac > 1) // poor man's modulo + fac -= (decltype(reps))fac; + + // remap for style + switch (style) { + case TweenStyle::fwd: /* do nothing */ break; + case TweenStyle::rev: fac = 1.0f - fac; break; + case TweenStyle::pulse: + case TweenStyle::flicker: { + if (fac > 0.5f) { + fac = 1.f - (fac*2.f - 1.f); + } else { + fac = fac * 2; + } + if (style == TweenStyle::flicker) { + fac *= myrand_range(0.7f, 1.0f); + } + } + } + if (fac>1.f) + fac = 1.f; + else if (fac<0.f) + fac = 0.f; + } else { + fac = (style == TweenStyle::rev) ? 1.f : 0.f; + } + + return start.interpolate(fac, end); + } + + void serialize(std::ostream &os) const + { + writeU8(os, static_cast<u8>(style)); + writeU16(os, reps); + writeF32(os, beginning); + start.serialize(os); + end.serialize(os); + } + void deSerialize(std::istream &is) + { + style = static_cast<TweenStyle>(readU8(is)); + reps = readU16(is); + beginning = readF32(is); + start.deSerialize(is); + end.deSerialize(is); + } + }; + + template <typename T> + inline std::string dump(const TweenedParameter<T>& t) + { + std::ostringstream s; + const char* icon; + switch (t.style) { + case TweenStyle::fwd: icon = "→"; break; + case TweenStyle::rev: icon = "←"; break; + case TweenStyle::pulse: icon = "↔"; break; + case TweenStyle::flicker: icon = "↯"; break; + } + s << "tween<"; + if (t.reps != 1) + s << t.reps << "x "; + s << dump(t.start) << " "<<icon<<" " << dump(t.end) << ">"; + return s.str(); + } + + enum class AttractorKind : u8 { none, point, line, plane }; + enum class BlendMode : u8 { alpha, add, sub, screen }; + + // these are consistently-named convenience aliases to make code more readable without `using ParticleParamTypes` declarations + using v3fRange = RangedParameter<v3fParameter>; + using f32Range = RangedParameter<f32Parameter>; + + using v2fTween = TweenedParameter<v2fParameter>; + using v3fTween = TweenedParameter<v3fParameter>; + using f32Tween = TweenedParameter<f32Parameter>; + using v3fRangeTween = TweenedParameter<v3fRange>; + using f32RangeTween = TweenedParameter<f32Range>; + + #undef DECL_PARAM_SRZRS + #undef DECL_PARAM_OVERLOADS +} + +struct ParticleTexture +{ + bool animated = false; + ParticleParamTypes::BlendMode blendmode = ParticleParamTypes::BlendMode::alpha; + TileAnimationParams animation; + ParticleParamTypes::f32Tween alpha{1.0f}; + ParticleParamTypes::v2fTween scale{v2f(1.0f)}; +}; + +struct ServerParticleTexture : public ParticleTexture +{ + std::string string; + void serialize(std::ostream &os, u16 protocol_ver, bool newPropertiesOnly = false) const; + void deSerialize(std::istream &is, u16 protocol_ver, bool newPropertiesOnly = false); +}; + +struct CommonParticleParams +{ bool collisiondetection = false; bool collision_removal = false; bool object_collision = false; bool vertical = false; - std::string texture; + ServerParticleTexture texture; struct TileAnimationParams animation; u8 glow = 0; MapNode node; @@ -58,22 +391,42 @@ struct CommonParticleParams { } }; -struct ParticleParameters : CommonParticleParams { - v3f pos; - v3f vel; - v3f acc; - f32 expirationtime = 1; - f32 size = 1; +struct ParticleParameters : CommonParticleParams +{ + v3f pos, vel, acc, drag; + f32 size = 1, expirationtime = 1; + ParticleParamTypes::f32Range bounce; + ParticleParamTypes::v3fRange jitter; void serialize(std::ostream &os, u16 protocol_ver) const; void deSerialize(std::istream &is, u16 protocol_ver); }; -struct ParticleSpawnerParameters : CommonParticleParams { +struct ParticleSpawnerParameters : CommonParticleParams +{ u16 amount = 1; - v3f minpos, maxpos, minvel, maxvel, minacc, maxacc; f32 time = 1; - f32 minexptime = 1, maxexptime = 1, minsize = 1, maxsize = 1; + + std::vector<ServerParticleTexture> texpool; + + ParticleParamTypes::v3fRangeTween + pos, vel, acc, drag, radius, jitter; + + ParticleParamTypes::AttractorKind + attractor_kind; + ParticleParamTypes::v3fTween + attractor_origin, attractor_direction; + // object IDs + u16 attractor_attachment = 0, + attractor_direction_attachment = 0; + // do particles disappear when they cross the attractor threshold? + bool attractor_kill = true; + + ParticleParamTypes::f32RangeTween + exptime{1.0f}, + size {1.0f}, + attract{0.0f}, + bounce {0.0f}; // For historical reasons no (de-)serialization methods here }; diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 5595f3b14..10670a60a 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -42,7 +42,7 @@ struct EnumString es_TileAnimationType[] = {TAT_NONE, "none"}, {TAT_VERTICAL_FRAMES, "vertical_frames"}, {TAT_SHEET_2D, "sheet_2d"}, - {0, NULL}, + {0, nullptr}, }; /******************************************************************************/ diff --git a/src/script/common/c_converter.h b/src/script/common/c_converter.h index a14eb9186..5fea3c21f 100644 --- a/src/script/common/c_converter.h +++ b/src/script/common/c_converter.h @@ -100,6 +100,7 @@ void setboolfield(lua_State *L, int table, const char *fieldname, bool value); v3f checkFloatPos (lua_State *L, int index); +v2f check_v2f (lua_State *L, int index); v3f check_v3f (lua_State *L, int index); v3s16 check_v3s16 (lua_State *L, int index); diff --git a/src/script/lua_api/l_particleparams.h b/src/script/lua_api/l_particleparams.h new file mode 100644 index 000000000..4fefc5e3a --- /dev/null +++ b/src/script/lua_api/l_particleparams.h @@ -0,0 +1,279 @@ +/* +Minetest +Copyright (C) 2021 velartrill, Lexi Hale <lexi@hale.su> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once +#include "lua_api/l_particles.h" +#include "lua_api/l_object.h" +#include "lua_api/l_internal.h" +#include "common/c_converter.h" +#include "common/c_content.h" +#include "server.h" +#include "particles.h" + +namespace LuaParticleParams +{ + using namespace ParticleParamTypes; + + template<typename T> + inline void readNumericLuaValue(lua_State* L, T& ret) + { + if (lua_isnil(L,-1)) + return; + + if (std::is_integral<T>()) + ret = lua_tointeger(L, -1); + else + ret = lua_tonumber(L, -1); + } + + template <typename T, size_t N> + inline void readNumericLuaValue(lua_State* L, Parameter<T,N>& ret) + { + readNumericLuaValue<T>(L, ret.val); + } + + // these are unfortunately necessary as C++ intentionally disallows function template + // specialization and there's no way to make template overloads reliably resolve correctly + inline void readLuaValue(lua_State* L, f32Parameter& ret) { readNumericLuaValue(L, ret); } + inline void readLuaValue(lua_State* L, f32& ret) { readNumericLuaValue(L, ret); } + inline void readLuaValue(lua_State* L, u16& ret) { readNumericLuaValue(L, ret); } + inline void readLuaValue(lua_State* L, u8& ret) { readNumericLuaValue(L, ret); } + + inline void readLuaValue(lua_State* L, v3fParameter& ret) + { + if (lua_isnil(L, -1)) + return; + + if (lua_isnumber(L, -1)) { // shortcut for uniform vectors + auto n = lua_tonumber(L, -1); + ret = v3fParameter(n,n,n); + } else { + ret = (v3fParameter)check_v3f(L, -1); + } + } + + inline void readLuaValue(lua_State* L, v2fParameter& ret) + { + if (lua_isnil(L, -1)) + return; + + if (lua_isnumber(L, -1)) { // shortcut for uniform vectors + auto n = lua_tonumber(L, -1); + ret = v2fParameter(n,n); + } else { + ret = (v2fParameter)check_v2f(L, -1); + } + } + + inline void readLuaValue(lua_State* L, TweenStyle& ret) + { + if (lua_isnil(L, -1)) + return; + + static const EnumString opts[] = { + {(int)TweenStyle::fwd, "fwd"}, + {(int)TweenStyle::rev, "rev"}, + {(int)TweenStyle::pulse, "pulse"}, + {(int)TweenStyle::flicker, "flicker"}, + {0, nullptr}, + }; + + luaL_checktype(L, -1, LUA_TSTRING); + int v = (int)TweenStyle::fwd; + if (!string_to_enum(opts, v, lua_tostring(L, -1))) { + throw LuaError("tween style must be one of ('fwd', 'rev', 'pulse', 'flicker')"); + } + ret = (TweenStyle)v; + } + + inline void readLuaValue(lua_State* L, AttractorKind& ret) + { + if (lua_isnil(L, -1)) + return; + + static const EnumString opts[] = { + {(int)AttractorKind::none, "none"}, + {(int)AttractorKind::point, "point"}, + {(int)AttractorKind::line, "line"}, + {(int)AttractorKind::plane, "plane"}, + {0, nullptr}, + }; + + luaL_checktype(L, -1, LUA_TSTRING); + int v = (int)AttractorKind::none; + if (!string_to_enum(opts, v, lua_tostring(L, -1))) { + throw LuaError("attractor kind must be one of ('none', 'point', 'line', 'plane')"); + } + ret = (AttractorKind)v; + } + + inline void readLuaValue(lua_State* L, BlendMode& ret) + { + if (lua_isnil(L, -1)) + return; + + static const EnumString opts[] = { + {(int)BlendMode::alpha, "alpha"}, + {(int)BlendMode::add, "add"}, + {(int)BlendMode::sub, "sub"}, + {(int)BlendMode::screen, "screen"}, + {0, nullptr}, + }; + + luaL_checktype(L, -1, LUA_TSTRING); + int v = (int)BlendMode::alpha; + if (!string_to_enum(opts, v, lua_tostring(L, -1))) { + throw LuaError("blend mode must be one of ('alpha', 'add', 'sub', 'screen')"); + } + ret = (BlendMode)v; + } + + template <typename T> void + readLuaValue(lua_State* L, RangedParameter<T>& field) + { + if (lua_isnil(L,-1)) + return; + if (!lua_istable(L,-1)) // is this is just a literal value? + goto set_uniform; + + lua_getfield(L, -1, "min"); + // handle convenience syntax for non-range values + if (lua_isnil(L,-1)) { + lua_pop(L, 1); + goto set_uniform; + } + readLuaValue(L,field.min); + lua_pop(L, 1); + + lua_getfield(L, -1, "max"); + readLuaValue(L,field.max); + lua_pop(L, 1); + + lua_getfield(L, -1, "bias"); + if (!lua_isnil(L,-1)) + readLuaValue(L,field.bias); + lua_pop(L, 1); + return; + + set_uniform: + readLuaValue(L, field.min); + readLuaValue(L, field.max); + } + + template <typename T> void + readLegacyValue(lua_State* L, const char* name, T& field) {} + + template <typename T> void + readLegacyValue(lua_State* L, const char* name, RangedParameter<T>& field) + { + int tbl = lua_gettop(L); + lua_pushliteral(L, "min"); + lua_pushstring(L, name); + lua_concat(L, 2); + lua_gettable(L, tbl); + if (!lua_isnil(L, -1)) { + readLuaValue(L, field.min); + } + lua_settop(L, tbl); + + lua_pushliteral(L, "max"); + lua_pushstring(L, name); + lua_concat(L, 2); + lua_gettable(L, tbl); + if (!lua_isnil(L, -1)) { + readLuaValue(L, field.max); + } + lua_settop(L, tbl); + } + + template <typename T> void + readTweenTable(lua_State* L, const char* name, TweenedParameter<T>& field) + { + int tbl = lua_gettop(L); + + lua_pushstring(L, name); + lua_pushliteral(L, "_tween"); + lua_concat(L, 2); + lua_gettable(L, tbl); + if(lua_istable(L, -1)) { + int tween = lua_gettop(L); + // get the starting value + lua_pushinteger(L, 1), lua_gettable(L, tween); + readLuaValue(L, field.start); + lua_pop(L, 1); + + // get the final value -- use len instead of 2 so that this + // gracefully degrades if keyframe support is later added + lua_pushinteger(L, (lua_Integer)lua_objlen(L, -1)), lua_gettable(L, tween); + readLuaValue(L, field.end); + lua_pop(L, 1); + + // get the effect settings + lua_getfield(L, -1, "style"); + lua_isnil(L,-1) || (readLuaValue(L, field.style), true); + lua_pop(L, 1); + + lua_getfield(L, -1, "reps"); + lua_isnil(L,-1) || (readLuaValue(L, field.reps), true); + lua_pop(L, 1); + + lua_getfield(L, -1, "start"); + lua_isnil(L,-1) || (readLuaValue(L, field.beginning), true); + lua_pop(L, 1); + + goto done; + } else { + lua_pop(L,1); + } + // the table is not present; check for nonanimated values + + lua_getfield(L, tbl, name); + if(!lua_isnil(L, -1)) { + readLuaValue(L, field.start); + lua_settop(L, tbl); + goto set_uniform; + } else { + lua_pop(L,1); + } + + // the goto did not trigger, so this table is not present either + // check for pre-5.6.0 legacy values + readLegacyValue(L, name, field.start); + + set_uniform: + field.end = field.start; + done: + lua_settop(L, tbl); // clean up after ourselves + } + + inline u16 readAttachmentID(lua_State* L, const char* name) + { + u16 id = 0; + lua_getfield(L, -1, name); + if (!lua_isnil(L, -1)) { + ObjectRef *ref = ObjectRef::checkobject(L, -1); + if (auto obj = ObjectRef::getobject(ref)) + id = obj->getId(); + } + lua_pop(L, 1); + return id; + } + + void readTexValue(lua_State* L, ServerParticleTexture& tex); +} diff --git a/src/script/lua_api/l_particles.cpp b/src/script/lua_api/l_particles.cpp index a51c4fe20..586c7dc73 100644 --- a/src/script/lua_api/l_particles.cpp +++ b/src/script/lua_api/l_particles.cpp @@ -20,30 +20,50 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_particles.h" #include "lua_api/l_object.h" #include "lua_api/l_internal.h" +#include "lua_api/l_particleparams.h" #include "common/c_converter.h" #include "common/c_content.h" #include "server.h" #include "particles.h" -// add_particle({pos=, velocity=, acceleration=, expirationtime=, -// size=, collisiondetection=, collision_removal=, object_collision=, -// vertical=, texture=, player=}) -// pos/velocity/acceleration = {x=num, y=num, z=num} -// expirationtime = num (seconds) -// size = num -// collisiondetection = bool -// collision_removal = bool -// object_collision = bool -// vertical = bool -// texture = e.g."default_wood.png" -// animation = TileAnimation definition -// glow = num +void LuaParticleParams::readTexValue(lua_State* L, ServerParticleTexture& tex) +{ + StackUnroller unroll(L); + + tex.animated = false; + if (lua_isstring(L, -1)) { + tex.string = lua_tostring(L, -1); + return; + } + + luaL_checktype(L, -1, LUA_TTABLE); + lua_getfield(L, -1, "name"); + tex.string = luaL_checkstring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "animation"); + if (! lua_isnil(L, -1)) { + tex.animated = true; + tex.animation = read_animation_definition(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "blend"); + LuaParticleParams::readLuaValue(L, tex.blendmode); + lua_pop(L, 1); + + LuaParticleParams::readTweenTable(L, "alpha", tex.alpha); + LuaParticleParams::readTweenTable(L, "scale", tex.scale); + +} + +// add_particle({...}) int ModApiParticles::l_add_particle(lua_State *L) { NO_MAP_LOCK_REQUIRED; // Get parameters - struct ParticleParameters p; + ParticleParameters p; std::string playername; if (lua_gettop(L) > 1) // deprecated @@ -56,7 +76,7 @@ int ModApiParticles::l_add_particle(lua_State *L) p.expirationtime = luaL_checknumber(L, 4); p.size = luaL_checknumber(L, 5); p.collisiondetection = readParam<bool>(L, 6); - p.texture = luaL_checkstring(L, 7); + p.texture.string = luaL_checkstring(L, 7); if (lua_gettop(L) == 8) // only spawn for a single player playername = luaL_checkstring(L, 8); } @@ -108,7 +128,12 @@ int ModApiParticles::l_add_particle(lua_State *L) p.animation = read_animation_definition(L, -1); lua_pop(L, 1); - p.texture = getstringfield_default(L, 1, "texture", p.texture); + lua_getfield(L, 1, "texture"); + if (!lua_isnil(L, -1)) { + LuaParticleParams::readTexValue(L, p.texture); + } + lua_pop(L, 1); + p.glow = getintfield_default(L, 1, "glow", p.glow); lua_getfield(L, 1, "node"); @@ -119,34 +144,26 @@ int ModApiParticles::l_add_particle(lua_State *L) p.node_tile = getintfield_default(L, 1, "node_tile", p.node_tile); playername = getstringfield_default(L, 1, "playername", ""); + + lua_getfield(L, 1, "drag"); + if (lua_istable(L, -1)) + p.drag = check_v3f(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 1, "jitter"); + LuaParticleParams::readLuaValue(L, p.jitter); + lua_pop(L, 1); + + lua_getfield(L, 1, "bounce"); + LuaParticleParams::readLuaValue(L, p.bounce); + lua_pop(L, 1); } getServer(L)->spawnParticle(playername, p); return 1; } -// add_particlespawner({amount=, time=, -// minpos=, maxpos=, -// minvel=, maxvel=, -// minacc=, maxacc=, -// minexptime=, maxexptime=, -// minsize=, maxsize=, -// collisiondetection=, -// collision_removal=, -// object_collision=, -// vertical=, -// texture=, -// player=}) -// minpos/maxpos/minvel/maxvel/minacc/maxacc = {x=num, y=num, z=num} -// minexptime/maxexptime = num (seconds) -// minsize/maxsize = num -// collisiondetection = bool -// collision_removal = bool -// object_collision = bool -// vertical = bool -// texture = e.g."default_wood.png" -// animation = TileAnimation definition -// glow = num +// add_particlespawner({...}) int ModApiParticles::l_add_particlespawner(lua_State *L) { NO_MAP_LOCK_REQUIRED; @@ -156,24 +173,31 @@ int ModApiParticles::l_add_particlespawner(lua_State *L) ServerActiveObject *attached = NULL; std::string playername; + using namespace ParticleParamTypes; if (lua_gettop(L) > 1) //deprecated { log_deprecated(L, "Deprecated add_particlespawner call with " "individual parameters instead of definition"); p.amount = luaL_checknumber(L, 1); p.time = luaL_checknumber(L, 2); - p.minpos = check_v3f(L, 3); - p.maxpos = check_v3f(L, 4); - p.minvel = check_v3f(L, 5); - p.maxvel = check_v3f(L, 6); - p.minacc = check_v3f(L, 7); - p.maxacc = check_v3f(L, 8); - p.minexptime = luaL_checknumber(L, 9); - p.maxexptime = luaL_checknumber(L, 10); - p.minsize = luaL_checknumber(L, 11); - p.maxsize = luaL_checknumber(L, 12); + auto minpos = check_v3f(L, 3); + auto maxpos = check_v3f(L, 4); + auto minvel = check_v3f(L, 5); + auto maxvel = check_v3f(L, 6); + auto minacc = check_v3f(L, 7); + auto maxacc = check_v3f(L, 8); + auto minexptime = luaL_checknumber(L, 9); + auto maxexptime = luaL_checknumber(L, 10); + auto minsize = luaL_checknumber(L, 11); + auto maxsize = luaL_checknumber(L, 12); + p.pos = v3fRange(minpos, maxpos); + p.vel = v3fRange(minvel, maxvel); + p.acc = v3fRange(minacc, maxacc); + p.exptime = f32Range(minexptime, maxexptime); + p.size = f32Range(minsize, maxsize); + p.collisiondetection = readParam<bool>(L, 13); - p.texture = luaL_checkstring(L, 14); + p.texture.string = luaL_checkstring(L, 14); if (lua_gettop(L) == 15) // only spawn for a single player playername = luaL_checkstring(L, 15); } @@ -182,40 +206,46 @@ int ModApiParticles::l_add_particlespawner(lua_State *L) p.amount = getintfield_default(L, 1, "amount", p.amount); p.time = getfloatfield_default(L, 1, "time", p.time); - lua_getfield(L, 1, "minpos"); - if (lua_istable(L, -1)) - p.minpos = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "maxpos"); - if (lua_istable(L, -1)) - p.maxpos = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "minvel"); - if (lua_istable(L, -1)) - p.minvel = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "maxvel"); - if (lua_istable(L, -1)) - p.maxvel = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "minacc"); - if (lua_istable(L, -1)) - p.minacc = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "maxacc"); - if (lua_istable(L, -1)) - p.maxacc = check_v3f(L, -1); - lua_pop(L, 1); + // set default values + p.exptime = 1; + p.size = 1; + + // read spawner parameters from the table + LuaParticleParams::readTweenTable(L, "pos", p.pos); + LuaParticleParams::readTweenTable(L, "vel", p.vel); + LuaParticleParams::readTweenTable(L, "acc", p.acc); + LuaParticleParams::readTweenTable(L, "size", p.size); + LuaParticleParams::readTweenTable(L, "exptime", p.exptime); + LuaParticleParams::readTweenTable(L, "drag", p.drag); + LuaParticleParams::readTweenTable(L, "jitter", p.jitter); + LuaParticleParams::readTweenTable(L, "bounce", p.bounce); + lua_getfield(L, 1, "attract"); + if (!lua_isnil(L, -1)) { + luaL_checktype(L, -1, LUA_TTABLE); + lua_getfield(L, -1, "kind"); + LuaParticleParams::readLuaValue(L, p.attractor_kind); + lua_pop(L,1); + + lua_getfield(L, -1, "die_on_contact"); + if (!lua_isnil(L, -1)) + p.attractor_kill = readParam<bool>(L, -1); + lua_pop(L,1); + + if (p.attractor_kind != AttractorKind::none) { + LuaParticleParams::readTweenTable(L, "strength", p.attract); + LuaParticleParams::readTweenTable(L, "origin", p.attractor_origin); + p.attractor_attachment = LuaParticleParams::readAttachmentID(L, "origin_attached"); + if (p.attractor_kind != AttractorKind::point) { + LuaParticleParams::readTweenTable(L, "direction", p.attractor_direction); + p.attractor_direction_attachment = LuaParticleParams::readAttachmentID(L, "direction_attached"); + } + } + } else { + p.attractor_kind = AttractorKind::none; + } + lua_pop(L,1); + LuaParticleParams::readTweenTable(L, "radius", p.radius); - p.minexptime = getfloatfield_default(L, 1, "minexptime", p.minexptime); - p.maxexptime = getfloatfield_default(L, 1, "maxexptime", p.maxexptime); - p.minsize = getfloatfield_default(L, 1, "minsize", p.minsize); - p.maxsize = getfloatfield_default(L, 1, "maxsize", p.maxsize); p.collisiondetection = getboolfield_default(L, 1, "collisiondetection", p.collisiondetection); p.collision_removal = getboolfield_default(L, 1, @@ -234,11 +264,29 @@ int ModApiParticles::l_add_particlespawner(lua_State *L) attached = ObjectRef::getobject(ref); } + lua_getfield(L, 1, "texture"); + if (!lua_isnil(L, -1)) { + LuaParticleParams::readTexValue(L, p.texture); + } + lua_pop(L, 1); + p.vertical = getboolfield_default(L, 1, "vertical", p.vertical); - p.texture = getstringfield_default(L, 1, "texture", p.texture); playername = getstringfield_default(L, 1, "playername", ""); p.glow = getintfield_default(L, 1, "glow", p.glow); + lua_getfield(L, 1, "texpool"); + if (lua_istable(L, -1)) { + size_t tl = lua_objlen(L, -1); + p.texpool.reserve(tl); + for (size_t i = 0; i < tl; ++i) { + lua_pushinteger(L, i+1), lua_gettable(L, -2); + p.texpool.emplace_back(); + LuaParticleParams::readTexValue(L, p.texpool.back()); + lua_pop(L,1); + } + } + lua_pop(L, 1); + lua_getfield(L, 1, "node"); if (lua_istable(L, -1)) p.node = readnode(L, -1, getGameDef(L)->ndef()); diff --git a/src/script/lua_api/l_particles_local.cpp b/src/script/lua_api/l_particles_local.cpp index cc68b13a5..62cbab8e9 100644 --- a/src/script/lua_api/l_particles_local.cpp +++ b/src/script/lua_api/l_particles_local.cpp @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/c_converter.h" #include "lua_api/l_internal.h" #include "lua_api/l_object.h" +#include "lua_api/l_particleparams.h" #include "client/particles.h" #include "client/client.h" #include "client/clientevent.h" @@ -49,6 +50,19 @@ int ModApiParticlesLocal::l_add_particle(lua_State *L) p.acc = check_v3f(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "drag"); + if (lua_istable(L, -1)) + p.drag = check_v3f(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 1, "jitter"); + LuaParticleParams::readLuaValue(L, p.jitter); + lua_pop(L, 1); + + lua_getfield(L, 1, "bounce"); + LuaParticleParams::readLuaValue(L, p.bounce); + lua_pop(L, 1); + p.expirationtime = getfloatfield_default(L, 1, "expirationtime", p.expirationtime); p.size = getfloatfield_default(L, 1, "size", p.size); @@ -64,7 +78,11 @@ int ModApiParticlesLocal::l_add_particle(lua_State *L) p.animation = read_animation_definition(L, -1); lua_pop(L, 1); - p.texture = getstringfield_default(L, 1, "texture", p.texture); + lua_getfield(L, 1, "texture"); + if (!lua_isnil(L, -1)) { + LuaParticleParams::readTexValue(L,p.texture); + } + lua_pop(L, 1); p.glow = getintfield_default(L, 1, "glow", p.glow); lua_getfield(L, 1, "node"); @@ -88,44 +106,50 @@ int ModApiParticlesLocal::l_add_particlespawner(lua_State *L) // Get parameters ParticleSpawnerParameters p; - p.amount = getintfield_default(L, 1, "amount", p.amount); p.time = getfloatfield_default(L, 1, "time", p.time); - lua_getfield(L, 1, "minpos"); - if (lua_istable(L, -1)) - p.minpos = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "maxpos"); - if (lua_istable(L, -1)) - p.maxpos = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "minvel"); - if (lua_istable(L, -1)) - p.minvel = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "maxvel"); - if (lua_istable(L, -1)) - p.maxvel = check_v3f(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 1, "minacc"); - if (lua_istable(L, -1)) - p.minacc = check_v3f(L, -1); - lua_pop(L, 1); + // set default values + p.exptime = 1; + p.size = 1; + + // read spawner parameters from the table + using namespace ParticleParamTypes; + LuaParticleParams::readTweenTable(L, "pos", p.pos); + LuaParticleParams::readTweenTable(L, "vel", p.vel); + LuaParticleParams::readTweenTable(L, "acc", p.acc); + LuaParticleParams::readTweenTable(L, "size", p.size); + LuaParticleParams::readTweenTable(L, "exptime", p.exptime); + LuaParticleParams::readTweenTable(L, "drag", p.drag); + LuaParticleParams::readTweenTable(L, "jitter", p.jitter); + LuaParticleParams::readTweenTable(L, "bounce", p.bounce); + lua_getfield(L, 1, "attract"); + if (!lua_isnil(L, -1)) { + luaL_checktype(L, -1, LUA_TTABLE); + lua_getfield(L, -1, "kind"); + LuaParticleParams::readLuaValue(L, p.attractor_kind); + lua_pop(L,1); + + lua_getfield(L, -1, "die_on_contact"); + if (!lua_isnil(L, -1)) + p.attractor_kill = readParam<bool>(L, -1); + lua_pop(L,1); + + if (p.attractor_kind != AttractorKind::none) { + LuaParticleParams::readTweenTable(L, "strength", p.attract); + LuaParticleParams::readTweenTable(L, "origin", p.attractor_origin); + p.attractor_attachment = LuaParticleParams::readAttachmentID(L, "origin_attached"); + if (p.attractor_kind != AttractorKind::point) { + LuaParticleParams::readTweenTable(L, "direction", p.attractor_direction); + p.attractor_direction_attachment = LuaParticleParams::readAttachmentID(L, "direction_attached"); + } + } + } else { + p.attractor_kind = AttractorKind::none; + } + lua_pop(L,1); + LuaParticleParams::readTweenTable(L, "radius", p.radius); - lua_getfield(L, 1, "maxacc"); - if (lua_istable(L, -1)) - p.maxacc = check_v3f(L, -1); - lua_pop(L, 1); - - p.minexptime = getfloatfield_default(L, 1, "minexptime", p.minexptime); - p.maxexptime = getfloatfield_default(L, 1, "maxexptime", p.maxexptime); - p.minsize = getfloatfield_default(L, 1, "minsize", p.minsize); - p.maxsize = getfloatfield_default(L, 1, "maxsize", p.maxsize); p.collisiondetection = getboolfield_default(L, 1, "collisiondetection", p.collisiondetection); p.collision_removal = getboolfield_default(L, 1, @@ -137,10 +161,28 @@ int ModApiParticlesLocal::l_add_particlespawner(lua_State *L) p.animation = read_animation_definition(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "texture"); + if (!lua_isnil(L, -1)) { + LuaParticleParams::readTexValue(L, p.texture); + } + lua_pop(L, 1); + p.vertical = getboolfield_default(L, 1, "vertical", p.vertical); - p.texture = getstringfield_default(L, 1, "texture", p.texture); p.glow = getintfield_default(L, 1, "glow", p.glow); + lua_getfield(L, 1, "texpool"); + if (lua_istable(L, -1)) { + size_t tl = lua_objlen(L, -1); + p.texpool.reserve(tl); + for (size_t i = 0; i < tl; ++i) { + lua_pushinteger(L, i+1), lua_gettable(L, -2); + p.texpool.emplace_back(); + LuaParticleParams::readTexValue(L, p.texpool.back()); + lua_pop(L,1); + } + } + lua_pop(L, 1); + lua_getfield(L, 1, "node"); if (lua_istable(L, -1)) p.node = readnode(L, -1, getGameDef(L)->ndef()); diff --git a/src/server.cpp b/src/server.cpp index 4eee14a30..6ca45f2d3 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -160,6 +160,7 @@ v3f ServerPlayingSound::getPos(ServerEnvironment *env, bool *pos_exists) const return sao->getBasePosition(); } } + return v3f(0,0,0); } @@ -1599,7 +1600,12 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version, if (peer_id == PEER_ID_INEXISTENT) { std::vector<session_t> clients = m_clients.getClientIDs(); - const v3f pos = (p.minpos + p.maxpos) / 2.0f * BS; + const v3f pos = ( + p.pos.start.min.val + + p.pos.start.max.val + + p.pos.end.min.val + + p.pos.end.max.val + ) / 4.0f * BS; const float radius_sq = radius * radius; /* Don't send short-lived spawners to distant players. * This could be replaced with proper tracking at some point. */ @@ -1627,11 +1633,19 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version, NetworkPacket pkt(TOCLIENT_ADD_PARTICLESPAWNER, 100, peer_id); - pkt << p.amount << p.time << p.minpos << p.maxpos << p.minvel - << p.maxvel << p.minacc << p.maxacc << p.minexptime << p.maxexptime - << p.minsize << p.maxsize << p.collisiondetection; + pkt << p.amount << p.time; + { // serialize legacy fields + std::ostringstream os(std::ios_base::binary); + p.pos.start.legacySerialize(os); + p.vel.start.legacySerialize(os); + p.acc.start.legacySerialize(os); + p.exptime.start.legacySerialize(os); + p.size.start.legacySerialize(os); + pkt.putRawString(os.str()); + } + pkt << p.collisiondetection; - pkt.putLongString(p.texture); + pkt.putLongString(p.texture.string); pkt << id << p.vertical << p.collision_removal << attached_id; { @@ -1642,6 +1656,51 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version, pkt << p.glow << p.object_collision; pkt << p.node.param0 << p.node.param2 << p.node_tile; + { // serialize new fields + // initial bias for older properties + pkt << p.pos.start.bias + << p.vel.start.bias + << p.acc.start.bias + << p.exptime.start.bias + << p.size.start.bias; + + std::ostringstream os(std::ios_base::binary); + + // final tween frames of older properties + p.pos.end.serialize(os); + p.vel.end.serialize(os); + p.acc.end.serialize(os); + p.exptime.end.serialize(os); + p.size.end.serialize(os); + + // properties for legacy texture field + p.texture.serialize(os, protocol_version, true); + + // new properties + p.drag.serialize(os); + p.jitter.serialize(os); + p.bounce.serialize(os); + ParticleParamTypes::serializeParameterValue(os, p.attractor_kind); + if (p.attractor_kind != ParticleParamTypes::AttractorKind::none) { + p.attract.serialize(os); + p.attractor_origin.serialize(os); + writeU16(os, p.attractor_attachment); /* object ID */ + writeU8(os, p.attractor_kill); + if (p.attractor_kind != ParticleParamTypes::AttractorKind::point) { + p.attractor_direction.serialize(os); + writeU16(os, p.attractor_direction_attachment); + } + } + p.radius.serialize(os); + + ParticleParamTypes::serializeParameterValue(os, (u16)p.texpool.size()); + for (const auto& tex : p.texpool) { + tex.serialize(os, protocol_version); + } + + pkt.putRawString(os.str()); + } + Send(&pkt); } @@ -3267,7 +3326,7 @@ bool Server::hudSetFlags(RemotePlayer *player, u32 flags, u32 mask) u32 new_hud_flags = (player->hud_flags & ~mask) | flags; if (new_hud_flags == player->hud_flags) // no change return true; - + SendHUDSetFlags(player->getPeerId(), flags, mask); player->hud_flags = new_hud_flags; @@ -3692,8 +3751,8 @@ v3f Server::findSpawnPos() s32 range = MYMIN(1 + i, range_max); // We're going to try to throw the player to this position v2s16 nodepos2d = v2s16( - -range + (myrand() % (range * 2)), - -range + (myrand() % (range * 2))); + -range + myrand_range(0, range*2), + -range + myrand_range(0, range*2)); // Get spawn level at point s16 spawn_level = m_emerge->getSpawnLevelAtPoint(nodepos2d); // Continue if MAX_MAP_GENERATION_LIMIT was returned by the mapgen to diff --git a/src/util/numeric.cpp b/src/util/numeric.cpp index 702ddce95..aa3bb843d 100644 --- a/src/util/numeric.cpp +++ b/src/util/numeric.cpp @@ -46,11 +46,22 @@ void myrand_bytes(void *out, size_t len) g_pcgrand.bytes(out, len); } +float myrand_float() +{ + u32 uv = g_pcgrand.next(); + return (float)uv / (float)U32_MAX; +} + int myrand_range(int min, int max) { return g_pcgrand.range(min, max); } +float myrand_range(float min, float max) +{ + return (max-min) * myrand_float() + min; +} + /* 64-bit unaligned version of MurmurHash diff --git a/src/util/numeric.h b/src/util/numeric.h index 32a6f4312..265046a63 100644 --- a/src/util/numeric.h +++ b/src/util/numeric.h @@ -223,6 +223,8 @@ u32 myrand(); void mysrand(unsigned int seed); void myrand_bytes(void *out, size_t len); int myrand_range(int min, int max); +float myrand_range(float min, float max); +float myrand_float(); /* Miscellaneous functions @@ -446,3 +448,24 @@ inline irr::video::SColor multiplyColorValue(const irr::video::SColor &color, fl core::clamp<u32>(color.getGreen() * mod, 0, 255), core::clamp<u32>(color.getBlue() * mod, 0, 255)); } + +template <typename T> inline T numericAbsolute(T v) { return v < 0 ? T(-v) : v; } +template <typename T> inline T numericSign(T v) { return T(v < 0 ? -1 : (v == 0 ? 0 : 1)); } + +inline v3f vecAbsolute(v3f v) +{ + return v3f( + numericAbsolute(v.X), + numericAbsolute(v.Y), + numericAbsolute(v.Z) + ); +} + +inline v3f vecSign(v3f v) +{ + return v3f( + numericSign(v.X), + numericSign(v.Y), + numericSign(v.Z) + ); +} diff --git a/src/util/pointer.h b/src/util/pointer.h index 245ac85bf..b659cea0e 100644 --- a/src/util/pointer.h +++ b/src/util/pointer.h @@ -45,7 +45,7 @@ public: Buffer() { m_size = 0; - data = NULL; + data = nullptr; } Buffer(unsigned int size) { @@ -53,7 +53,7 @@ public: if(size != 0) data = new T[size]; else - data = NULL; + data = nullptr; } // Disable class copy @@ -82,7 +82,7 @@ public: memcpy(data, t, size); } else - data = NULL; + data = nullptr; } ~Buffer() @@ -166,7 +166,7 @@ public: if(m_size != 0) data = new T[m_size]; else - data = NULL; + data = nullptr; refcount = new unsigned int; memset(data,0,sizeof(T)*m_size); (*refcount) = 1; @@ -201,7 +201,7 @@ public: memcpy(data, t, m_size); } else - data = NULL; + data = nullptr; refcount = new unsigned int; (*refcount) = 1; } @@ -216,7 +216,7 @@ public: memcpy(data, *buffer, buffer.getSize()); } else - data = NULL; + data = nullptr; refcount = new unsigned int; (*refcount) = 1; } @@ -256,3 +256,4 @@ private: unsigned int m_size; unsigned int *refcount; }; + |