diff --git a/Content.Tests/DMProject/Tests/Generator/GeneratorOperators.dm b/Content.Tests/DMProject/Tests/Generator/GeneratorOperators.dm new file mode 100644 index 0000000000..9beb4a7964 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Generator/GeneratorOperators.dm @@ -0,0 +1,98 @@ +// Generators can be chained together with math operators, producing a new generator. +// Constant generators (low == high) are used here so the results are deterministic. + +/proc/assert_num(generator/gen, expected) + var/result = gen.Rand() + ASSERT(isnum(result)) + ASSERT(result == expected) + +// A generator built from an operator always produces a 3D vector, even from 2D operands +/proc/assert_vec(generator/gen, x, y, z) + var/vector/result = gen.Rand() + ASSERT(istype(result, /vector)) + ASSERT(result.len == 3) + ASSERT(result.x == x) + ASSERT(result.y == y) + ASSERT(result.z == z) + +/proc/RunTest() + var/generator/five = generator("num", 5, 5) + var/generator/two = generator("num", 2, 2) + var/generator/vec = generator("vector", vector(3, 4), vector(3, 4)) + var/generator/vec3d = generator("vector", vector(3, 4, 5), vector(3, 4, 5)) + + // Operators return a generator, not a value + ASSERT(istype(five + two, /generator)) + + // Number generators with another generator + assert_num(five + two, 7) + assert_num(five - two, 3) + assert_num(five * two, 10) + + // Number generators with a plain number + assert_num(five + 3, 8) + assert_num(five - 3, 2) + assert_num(five * 3, 15) + assert_num(five / 2, 2.5) + assert_num(five ** 2, 25) + assert_num(-five, -5) + + // null counts as 0 + assert_num(five + null, 5) + assert_num(five - null, 5) + assert_num(five * null, 0) + + // Operators can be chained + assert_num((five + 1) * 2, 12) + + // A number generator reduces a vector operand down to its last component + assert_num(five + vector(1, 2), 7) + assert_num(five + vector(1, 2, 3), 8) + + // An uncombined generator keeps producing 2D vectors + var/vector/plain = vec.Rand() + ASSERT(plain.len == 2) + + // A number is applied to every component of a vector + assert_vec(vec + 2, 5, 6, 2) + assert_vec(vec - 2, 1, 2, -2) + assert_vec(vec * 2, 6, 8, 0) + assert_vec(vec / 2, 1.5, 2, 0) + assert_vec(vec ** 2, 9, 16, 0) + assert_vec(-vec, -3, -4, 0) + assert_vec(vec + two, 5, 6, 2) + assert_vec(vec * two, 6, 8, 0) + assert_vec(vec3d + 2, 5, 6, 7) + + // Vector generators combine component-wise + assert_vec(vec + vector(1, 2), 4, 6, 0) + assert_vec(vec - vector(1, 2), 2, 2, 0) + assert_vec(vec * vector(2, 3), 6, 12, 0) + assert_vec(vec3d * vector(2, 2, 2), 6, 8, 10) + + // A list of 2 or 3 numbers works in place of a vector + assert_vec(vec + list(1, 2), 4, 6, 0) + assert_vec(vec3d * list(2, 2, 2), 6, 8, 10) + + // Multiplying by a matrix transforms the vector + assert_vec(vec * matrix(2, 0, 10, 0, 3, 20), 16, 32, 0) + + // A color matrix does a 3D transform, with red,green,blue mapping to x,y,z + assert_vec(vec3d * list(2,0,0,0, 0,3,0,0, 0,0,4,0, 0,0,0,1, 10,20,30,0), 16, 32, 50) + + // The assignment forms rebind the var to the combined generator + var/generator/combined = generator("num", 5, 5) + combined += 2 + assert_num(combined, 7) + combined -= 1 + assert_num(combined, 6) + combined *= 2 + assert_num(combined, 12) + combined /= 4 + assert_num(combined, 3) + + // Operands are re-rolled on every Rand() call + var/generator/random = generator("num", 0, 1) + generator("num", 10, 20) + for(var/i in 1 to 20) + var/result = random.Rand() + ASSERT(result >= 10 && result <= 21) diff --git a/Content.Tests/DMProject/Tests/Generator/GeneratorParticles.dm b/Content.Tests/DMProject/Tests/Generator/GeneratorParticles.dm new file mode 100644 index 0000000000..60d8e25d3a --- /dev/null +++ b/Content.Tests/DMProject/Tests/Generator/GeneratorParticles.dm @@ -0,0 +1,15 @@ +// Generators built from operators must still be usable for particle effects +/proc/RunTest() + var/particles/P = new + P.lifespan = generator("num", 10, 20) + 5 + P.fade = generator("num", 1, 2) * 2 + P.position = generator("vector", vector(1, 1), vector(2, 2)) * 2 + P.velocity = generator("circle", 1, 2) + vector(1, 1) + P.scale = -generator("vector", vector(1, 1), vector(2, 2)) + P.drift = generator("sphere", 1, 2) * matrix(2, 0, 0, 0, 2, 0) + + var/generator/spun = generator("vector", vector(1, 0), vector(1, 0)) + spun.Turn(90) + P.gravity = spun + + ASSERT(P.lifespan != null) diff --git a/Content.Tests/DMProject/Tests/Generator/GeneratorTurn.dm b/Content.Tests/DMProject/Tests/Generator/GeneratorTurn.dm new file mode 100644 index 0000000000..d38c302604 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Generator/GeneratorTurn.dm @@ -0,0 +1,34 @@ +// /generator/proc/Turn() rotates the vectors a generator produces around the XY plane. +// Unlike most Turn() procs it modifies the generator in place and returns it. + +/proc/assert_turn(generator/gen, angle, x, y, z) + ASSERT(gen.Turn(angle) == gen) // Modified in place + + var/vector/result = gen.Rand() + ASSERT(result.len == 3) + ASSERT(abs(result.x - x) < 0.0001) + ASSERT(abs(result.y - y) < 0.0001) + ASSERT(abs(result.z - z) < 0.0001) + +/proc/unit_vector_generator() + return generator("vector", vector(1, 0), vector(1, 0)) + +/proc/RunTest() + assert_turn(unit_vector_generator(), 90, 0, 1, 0) + assert_turn(unit_vector_generator(), -90, 0, -1, 0) + assert_turn(unit_vector_generator(), 180, -1, 0, 0) + assert_turn(unit_vector_generator(), 45, 0.707107, 0.707107, 0) + + // A full rotation comes back around + assert_turn(unit_vector_generator(), 360, 1, 0, 0) + + // The Z component is left alone + assert_turn(generator("vector", vector(1, 0, 7), vector(1, 0, 7)), 90, 0, 1, 7) + + // Turning something already combined with an operator works too + assert_turn(generator("vector", vector(3, 4), vector(3, 4)) * 2, 90, -8, 6, 0) + + // Turning a number generator does nothing + var/generator/num = generator("num", 5, 5) + ASSERT(num.Turn(90) == num) + ASSERT(num.Rand() == 5) diff --git a/Content.Tests/DMProject/Tests/Generator/GeneratorVectorArgs.dm b/Content.Tests/DMProject/Tests/Generator/GeneratorVectorArgs.dm new file mode 100644 index 0000000000..6b1135864b --- /dev/null +++ b/Content.Tests/DMProject/Tests/Generator/GeneratorVectorArgs.dm @@ -0,0 +1,22 @@ +// Building generators out of a vector that's held in a var must leave that var alone +/proc/RunTest() + var/vector/v = vector(1, 2) + + var/generator/first = generator("vector", v, v) + var/generator/second = generator("box", v, v) + var/generator/third = generator("square", v, v) + + var/vector/result = first.Rand() + ASSERT(result.x == 1) + ASSERT(result.y == 2) + ASSERT(second.Rand() != null) + ASSERT(third.Rand() != null) + + // The vector is unharmed and still usable + ASSERT(v.len == 2) + ASSERT(v.x == 1) + ASSERT(v.y == 2) + + var/vector/sum = v + vector(1, 1) + ASSERT(sum.x == 2) + ASSERT(sum.y == 3) diff --git a/Content.Tests/DMProject/Tests/Vector/VectorAdd.dm b/Content.Tests/DMProject/Tests/Vector/VectorAdd.dm index 3fbc180f9a..f69fe91932 100644 --- a/Content.Tests/DMProject/Tests/Vector/VectorAdd.dm +++ b/Content.Tests/DMProject/Tests/Vector/VectorAdd.dm @@ -11,3 +11,4 @@ ASSERT(A.x == 7) ASSERT(A.y == 7) ASSERT(A.z == 4) + ASSERT(A.len == 3) // Combining with a 3D vector makes it 3D diff --git a/Content.Tests/DMProject/Tests/Vector/VectorDivide.dm b/Content.Tests/DMProject/Tests/Vector/VectorDivide.dm index be622b0a6b..d52918cd2c 100644 --- a/Content.Tests/DMProject/Tests/Vector/VectorDivide.dm +++ b/Content.Tests/DMProject/Tests/Vector/VectorDivide.dm @@ -20,3 +20,4 @@ ASSERT(A.x == 0.75) ASSERT(A.y == 0.75) ASSERT(A.z == 0) + ASSERT(A.len == 3) // Combining with a 3D vector makes it 3D diff --git a/Content.Tests/DMProject/Tests/Vector/VectorLen.dm b/Content.Tests/DMProject/Tests/Vector/VectorLen.dm new file mode 100644 index 0000000000..55546f56d4 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Vector/VectorLen.dm @@ -0,0 +1,16 @@ +// A vector is only 3D when a third component is actually given +/proc/RunTest() + var/vector/two = vector(3, 4) + ASSERT(two.len == 2) + ASSERT(two.z == 0) + + var/vector/three = vector(3, 4, 5) + ASSERT(three.len == 3) + + // An explicit null still counts as a third component + var/vector/nullZ = vector(3, 4, null) + ASSERT(nullZ.len == 3) + ASSERT(nullZ.z == 0) + + var/vector/direct = new /vector(3, 4) + ASSERT(direct.len == 2) diff --git a/Content.Tests/DMProject/Tests/Vector/VectorMultiply.dm b/Content.Tests/DMProject/Tests/Vector/VectorMultiply.dm index 53aa5972d7..dafed56be6 100644 --- a/Content.Tests/DMProject/Tests/Vector/VectorMultiply.dm +++ b/Content.Tests/DMProject/Tests/Vector/VectorMultiply.dm @@ -20,3 +20,4 @@ ASSERT(A.x == 12) ASSERT(A.y == 12) ASSERT(A.z == 0) + ASSERT(A.len == 3) // Combining with a 3D vector makes it 3D diff --git a/Content.Tests/DMProject/Tests/Vector/VectorSubtract.dm b/Content.Tests/DMProject/Tests/Vector/VectorSubtract.dm index 18f60fa4ba..cff7121e85 100644 --- a/Content.Tests/DMProject/Tests/Vector/VectorSubtract.dm +++ b/Content.Tests/DMProject/Tests/Vector/VectorSubtract.dm @@ -11,3 +11,4 @@ ASSERT(A.x == -1) ASSERT(A.y == -1) ASSERT(A.z == -4) + ASSERT(A.len == 3) // Combining with a 3D vector makes it 3D diff --git a/DMCompiler/DMStandard/Types/Generator.dm b/DMCompiler/DMStandard/Types/Generator.dm index 6b476b75f9..79890ea6e4 100644 --- a/DMCompiler/DMStandard/Types/Generator.dm +++ b/DMCompiler/DMStandard/Types/Generator.dm @@ -5,6 +5,7 @@ var/_binobj as opendream_unimplemented /generator/proc/Rand() +/generator/proc/Turn(angle) /* Generator Theory diff --git a/DMCompiler/DMStandard/Types/Vector.dm b/DMCompiler/DMStandard/Types/Vector.dm index afcc159dfe..27f85eacd0 100644 --- a/DMCompiler/DMStandard/Types/Vector.dm +++ b/DMCompiler/DMStandard/Types/Vector.dm @@ -24,4 +24,3 @@ set opendream_unimplemented = TRUE /proc/vector(x, y, z) - return new /vector(x, y, z) diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs index 6c9aafad5a..3a40ded9a4 100644 --- a/OpenDreamRuntime/Objects/DreamObject.cs +++ b/OpenDreamRuntime/Objects/DreamObject.cs @@ -489,6 +489,18 @@ public virtual DreamValue OperatorDivideRef(DreamValue b, DMProcState state) { throw new InvalidOperationException($"Division cannot be done between {this} and {b}"); } + // ** + [MustDisposeResource] + public virtual DreamValue OperatorPower(DreamValue b, DMProcState state) { + throw new InvalidOperationException($"Power cannot be done between {this} and {b}"); + } + + // - (unary) + [MustDisposeResource] + public virtual DreamValue OperatorNegate(DMProcState state) { + return new DreamValue(0); // BYOND evaluates most objects as 0 here + } + // | [MustDisposeResource] public virtual DreamValue OperatorOr(DreamValue b, DMProcState state) { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectGenerator.cs b/OpenDreamRuntime/Objects/Types/DreamObjectGenerator.cs index eec83033c8..69f6df6887 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectGenerator.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectGenerator.cs @@ -1,4 +1,7 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; using OpenDreamRuntime.Procs; +using OpenDreamRuntime.Procs.Native; using OpenDreamShared.Dream; namespace OpenDreamRuntime.Objects.Types; @@ -80,6 +83,195 @@ public T RequireType() where T : IGenerator { return casted; } + /// + /// Rotates the vectors this generator produces, in-place. + /// + /// + /// A no-op for a generator producing numbers, matching BYOND. + /// + public void Turn(float angle) { + if (Generator is IGeneratorNum) + return; + + Generator = new GeneratorTurn(Generator, angle); + } + + #region Operators + + public override DreamValue OperatorAdd(DreamValue b, DMProcState state) { + if (TryOperate(b, GeneratorOperation.Add, out var result)) + return result; + + return base.OperatorAdd(b, state); + } + + public override DreamValue OperatorSubtract(DreamValue b, DMProcState state) { + if (TryOperate(b, GeneratorOperation.Subtract, out var result)) + return result; + + return base.OperatorSubtract(b, state); + } + + public override DreamValue OperatorMultiply(DreamValue b, DMProcState state) { + if (TryOperate(b, GeneratorOperation.Multiply, out var result)) + return result; + + return base.OperatorMultiply(b, state); + } + + public override DreamValue OperatorMultiplyRef(DreamValue b, DMProcState state) { + return OperatorMultiply(b, state); + } + + /// + /// BYOND only accepts a plain number here; dividing by a generator, vector or matrix is an + /// "Undefined operation" runtime error over there. + /// + public override DreamValue OperatorDivide(DreamValue b, DMProcState state) { + if (!b.TryGetValueAsFloat(out var divisor)) + return base.OperatorDivide(b, state); + if (divisor == 0) + throw new DivideByZeroException("Cannot divide a generator by zero"); + + return Combine(new GeneratorNum(divisor), GeneratorOperation.Divide); + } + + public override DreamValue OperatorDivideRef(DreamValue b, DMProcState state) { + return OperatorDivide(b, state); + } + + public override DreamValue OperatorPower(DreamValue b, DMProcState state) { + if (!b.TryGetValueAsFloat(out var exponent)) + return base.OperatorPower(b, state); + + return Combine(new GeneratorNum(exponent), GeneratorOperation.Power); + } + + // -x, equivalent to multiplying by -1 + public override DreamValue OperatorNegate(DMProcState state) { + return Combine(new GeneratorNum(-1f), GeneratorOperation.Multiply); + } + + public override DreamValue OperatorAppend(DreamValue b) { + if (TryOperate(b, GeneratorOperation.Add, out var result)) + return result; + + return base.OperatorAppend(b); + } + + public override DreamValue OperatorRemove(DreamValue b) { + if (TryOperate(b, GeneratorOperation.Subtract, out var result)) + return result; + + return base.OperatorRemove(b); + } + + #endregion Operators + + /// + /// Combines this generator with another generator or value, producing a new generator + /// + /// false if the given value can't be used as an operand + private bool TryOperate(DreamValue b, GeneratorOperation operation, [MustDisposeResource] out DreamValue result) { + // Multiplying a vector by a matrix transforms it. Every other combination treats the matrix as a plain value. + if (operation == GeneratorOperation.Multiply && Generator is not IGeneratorNum && TryGetTransform(b, out var transform)) { + result = new DreamValue(CreateChild(new GeneratorMatrixTransform(Generator, transform))); + return true; + } + + if (!TryCreateOperand(b, out var operand)) { + result = DreamValue.Null; + return false; + } + + result = Combine(operand, operation); + return true; + } + + [MustDisposeResource] + private DreamValue Combine(IGenerator operand, GeneratorOperation operation) { + // The left-hand side decides whether the result is a number or a vector + IGenerator combined = Generator is IGeneratorNum + ? new GeneratorArithmeticNum(Generator, operand, operation) + : new GeneratorArithmeticVector(Generator, operand, operation); + + return new DreamValue(CreateChild(combined)); + } + + private DreamObjectGenerator CreateChild(IGenerator generator) { + return new DreamObjectGenerator(ObjectDefinition) { + Generator = generator + }; + } + + /// + /// Interprets a value as something a generator can be combined with + /// + private bool TryCreateOperand(DreamValue value, [NotNullWhen(true)] out IGenerator? operand) { + if (value.IsNull) { // BYOND treats null as 0 here + operand = new GeneratorNum(0f); + return true; + } + + if (value.TryGetValueAsFloat(out var number)) { + operand = new GeneratorNum(number); + return true; + } + + if (value.TryGetValueAsDreamObject(out var generator)) { + operand = generator.Generator; + return true; + } + + if (value.TryGetValueAsDreamObject(out var vector)) { + operand = CreateConstant(vector); + return true; + } + + // A matrix used as a plain value is the matrix applied to (1,1,1) + if (TryGetTransform(value, out var transform)) { + operand = new GeneratorVector3(IGenerator.Transform(transform, Vector3.One)); + return true; + } + + if (value.TryGetValueAsDreamList(out var list) && list.GetLength() is 2 or 3 && + DreamObjectVector.TryCreateFromValue(value, ObjectTree, out var listVector)) { + operand = CreateConstant(listVector); + listVector.DecRef(); + return true; + } + + operand = null; + return false; + } + + /// + /// Interprets a value as a matrix to transform a vector by, either a /matrix or a color matrix + /// + private static bool TryGetTransform(DreamValue value, out ColorMatrix matrix) { + if (value.TryGetValueAsDreamObject(out var transform)) { + // A /matrix is a 2D transform; it leaves nothing behind on the Z axis + matrix = new ColorMatrix( + transform.A, transform.D, 0f, 0f, + transform.B, transform.E, 0f, 0f, + 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 0f, + transform.C, transform.F, 0f, 0f); + return true; + } + + // Lengths 2 and 3 are a vector, not a color matrix + if (value.TryGetValueAsDreamList(out var list) && list.GetLength() is 9 or 12 or 16 or 20) + return DreamProcNativeHelpers.TryParseColorMatrix(list, out matrix); + + matrix = default; + return false; + } + + private static IGenerator CreateConstant(DreamObjectVector vector) { + return vector.Is3D ? new GeneratorVector3(vector.AsVector3) : new GeneratorVector2(vector.AsVector2); + } + private GeneratorDistribution DistributionNumberToEnum(int number) { return number switch { 0 => GeneratorDistribution.Uniform, @@ -90,4 +282,3 @@ private GeneratorDistribution DistributionNumberToEnum(int number) { }; } } - diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectVector.cs b/OpenDreamRuntime/Objects/Types/DreamObjectVector.cs index cf695d3acc..c9e4eea312 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectVector.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectVector.cs @@ -150,6 +150,7 @@ public override DreamValue OperatorMultiplyRef(DreamValue b, DMProcState state) IncRef(); return new DreamValue(this); } else if (b.TryGetValueAsDreamObject(out var right)) { + Is3D = Is3D || right.Is3D; // Must come before touching Z, which does nothing while we're 2D X *= right.X; Y *= right.Y; Z *= right.Z; @@ -199,6 +200,8 @@ public override DreamValue OperatorDivideRef(DreamValue b, DMProcState state) { } else if (b.TryGetValueAsDreamObject(out var right)) { if (right.X == 0 || right.Y == 0 || (Is3D && right.Z == 0)) throw new DivideByZeroException("Cannot divide vector by zero vector component"); + + Is3D = Is3D || right.Is3D; // Must come before touching Z, which does nothing while we're 2D X /= right.X; Y /= right.Y; Z = right.Z == 0 ? 0 : Z / right.Z; @@ -211,10 +214,10 @@ public override DreamValue OperatorDivideRef(DreamValue b, DMProcState state) { public override DreamValue OperatorAppend(DreamValue b) { if (b.TryGetValueAsDreamObject(out var right)) { + Is3D = Is3D || right.Is3D; // Must come before touching Z, which does nothing while we're 2D X += right.X; Y += right.Y; Z += right.Z; - Is3D = Is3D || right.Is3D; IncRef(); return new DreamValue(this); @@ -225,10 +228,10 @@ public override DreamValue OperatorAppend(DreamValue b) { public override DreamValue OperatorRemove(DreamValue b) { if (b.TryGetValueAsDreamObject(out var right)) { + Is3D = Is3D || right.Is3D; // Must come before touching Z, which does nothing while we're 2D X -= right.X; Y -= right.Y; Z -= right.Z; - Is3D = Is3D || right.Is3D; IncRef(); return new DreamValue(this); @@ -301,9 +304,12 @@ protected override void SetVar(string varName, DreamValue value) { /// Attempt to create a from a DreamValue
/// A vector can be created from a list containing 2 or 3 numbers /// + /// The caller owns a reference to the returned vector and is responsible for releasing it public static bool TryCreateFromValue(DreamValue value, DreamObjectTree tree, [NotNullWhen(true)] out DreamObjectVector? vector) { - if (value.TryGetValueAsDreamObject(out vector)) + if (value.TryGetValueAsDreamObject(out vector)) { + vector.IncRef(); // The value we were given keeps its own reference return true; + } if (value.TryGetValueAsDreamList(out var list)) { var length = list.GetLength(); diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 5fa41ba065..72c49d6e4c 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -900,8 +900,16 @@ private static DreamValue AppendHelper(DMProcState state) { result = IconOperationAdd(state, first, second); } else if (first.TryGetValueAsDreamObject(out var firstObj)) { if (firstObj != null) { - state.PopReference(reference); - return firstObj.OperatorAppend(second); + var opResult = firstObj.OperatorAppend(second); + + // Most types append to themselves in-place, but the ones producing a new + // object (such as /generator) need that object assigned back to the reference + if (opResult.TryGetValueAsDreamObject(out var opResultObj) && ReferenceEquals(opResultObj, firstObj)) + state.PopReference(reference); + else + state.AssignReference(reference, opResult); + + return opResult; } result = second; @@ -1385,6 +1393,14 @@ public static ProcStatus MultiplyReference(DMProcState state) { public static ProcStatus Negate(DMProcState state) { using var first = state.Pop(); + + if (first.TryGetValueAsDreamObject(out var firstDreamObject)) { + using var result = firstDreamObject.OperatorNegate(state); + + state.Push(result); + return ProcStatus.Continue; + } + float value = first.UnsafeGetValueAsFloat(); state.Push(new DreamValue(-value)); @@ -1395,6 +1411,13 @@ public static ProcStatus Power(DMProcState state) { using var second = state.Pop(); using var first = state.Pop(); + if (first.TryGetValueAsDreamObject(out var firstDreamObject)) { + using var result = firstDreamObject.OperatorPower(second, state); + + state.Push(result); + return ProcStatus.Continue; + } + if (!first.TryGetValueAsFloat(out var floatFirst) && !first.IsNull) throw new Exception($"Invalid power operation on {first} and {second}"); @@ -1412,9 +1435,15 @@ public static ProcStatus Remove(DMProcState state) { DreamValue result; switch (first.Type) { case DreamValue.DreamValueType.DreamObject when !first.IsNull: { - using var opResult = first.MustGetValueAsDreamObject()!.OperatorRemove(second); + var firstObj = first.MustGetValueAsDreamObject()!; + using var opResult = firstObj.OperatorRemove(second); + + // Same as Append; types producing a new object need it assigned back + if (opResult.TryGetValueAsDreamObject(out var opResultObj) && ReferenceEquals(opResultObj, firstObj)) + state.PopReference(reference); + else + state.AssignReference(reference, opResult); - state.PopReference(reference); state.Push(opResult); return ProcStatus.Continue; } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index eef8a523b7..2ac6b5a4d5 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -117,6 +117,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_trunc); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_turn); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_typesof); + objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_vector); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_uppertext); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_url_decode); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_url_encode); @@ -214,6 +215,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_RowsAffected); objectTree.SetNativeProc(objectTree.Generator, DreamProcNativeGenerator.NativeProc_Rand); + objectTree.SetNativeProc(objectTree.Generator, DreamProcNativeGenerator.NativeProc_Turn); SetOverridableNativeProc(objectTree, objectTree.World, DreamProcNativeWorld.NativeProc_Error); SetOverridableNativeProc(objectTree, objectTree.World, DreamProcNativeWorld.NativeProc_Reboot); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeGenerator.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeGenerator.cs index 8dc321ebaa..c7a8964fe7 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeGenerator.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeGenerator.cs @@ -2,6 +2,7 @@ using OpenDreamRuntime.Objects.Types; using OpenDreamShared.Dream; using Robust.Shared.Random; +using DreamValueTypeFlag = OpenDreamRuntime.DreamValue.DreamValueTypeFlag; namespace OpenDreamRuntime.Procs.Native; @@ -27,4 +28,21 @@ public static DreamValue NativeProc_Rand(NativeProc.Bundle bundle, DreamObject? throw new Exception($"Invalid generator for Rand: {genObj}"); } } + + /// Rotates the vectors this generator produces around the XY plane + /// Unlike the global turn(), this modifies the generator in-place and returns it + [DreamProc("Turn")] + [DreamProcParameter("angle", Type = DreamValueTypeFlag.Float)] + public static DreamValue NativeProc_Turn(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var genObj = (DreamObjectGenerator)src!; + + var angleArg = bundle.GetArgument(0, "angle"); + if (!angleArg.TryGetValueAsFloat(out var angle)) + throw new Exception($"number required for 2nd argument: turn({genObj}, {angleArg})"); + + genObj.Turn(angle); + + genObj.IncRef(); + return new DreamValue(genObj); + } } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 7fc1e108f7..a56164d863 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -3207,6 +3207,19 @@ public static DreamValue NativeProc_viewers(NativeProc.Bundle bundle, DreamObjec return DreamProcNativeHelpers.HandleViewersHearers(bundle, usr, false); } + [DreamProc("vector")] + [DreamProcParameter("x")] + [DreamProcParameter("y")] + [DreamProcParameter("z")] + public static DreamValue NativeProc_vector(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + // Only the arguments actually given get passed along; a third one makes the vector 3D even when it's null + var argumentCount = Math.Min(bundle.Arguments.Length, 3); + var vector = bundle.ObjectTree.CreateObject(bundle.ObjectTree.Vector); + + vector.InitSpawn(new DreamProcArguments(bundle.Arguments[..argumentCount])); + return new DreamValue(vector); + } + [DreamProc("walk")] [DreamProcParameter("Ref", Type = DreamValueTypeFlag.DreamObject)] [DreamProcParameter("Dir", Type = DreamValueTypeFlag.Float)] diff --git a/OpenDreamShared/Dream/Generator.cs b/OpenDreamShared/Dream/Generator.cs index 62e2e002b7..32ad4dea73 100644 --- a/OpenDreamShared/Dream/Generator.cs +++ b/OpenDreamShared/Dream/Generator.cs @@ -13,6 +13,14 @@ public enum GeneratorDistribution { Square } +public enum GeneratorOperation { + Add, + Subtract, + Multiply, + Divide, + Power +} + public interface IGenerator { public static float GenerateNum(IRobustRandom random, float low, float high, GeneratorDistribution distribution) { return distribution switch { @@ -24,6 +32,64 @@ public static float GenerateNum(IRobustRandom random, float low, float high, Gen _ => throw new ArgumentOutOfRangeException(nameof(distribution), distribution, null) }; } + + public static float Operate(GeneratorOperation operation, float left, float right) { + return operation switch { + GeneratorOperation.Add => left + right, + GeneratorOperation.Subtract => left - right, + GeneratorOperation.Multiply => left * right, + GeneratorOperation.Divide => left / right, + GeneratorOperation.Power => MathF.Pow(left, right), + _ => throw new ArgumentOutOfRangeException(nameof(operation), operation, null) + }; + } + + public static Vector3 Operate(GeneratorOperation operation, Vector3 left, Vector3 right) { + return new Vector3( + Operate(operation, left.X, right.X), + Operate(operation, left.Y, right.Y), + Operate(operation, left.Z, right.Z) + ); + } + + /// + /// Samples a generator being used where a single number is expected. + /// + /// + /// BYOND reduces a vector-producing operand down to its last component here (Y when 2D, Z when 3D). + /// + public static float GenerateScalar(IGenerator generator, IRobustRandom random) { + return generator switch { + IGeneratorNum num => num.Generate(random), + IGeneratorVector vector => vector.PrefersVector3 + ? vector.GenerateVector3(random).Z + : vector.GenerateVector2(random).Y, + _ => 0f + }; + } + + /// + /// Samples a generator being used where a vector is expected. + /// + /// A number is broadcast to every component, matching BYOND. + public static Vector3 GenerateVector(IGenerator generator, IRobustRandom random) { + return generator switch { + IGeneratorVector vector => vector.GenerateVector3(random), + IGeneratorNum num => new Vector3(num.Generate(random)), + _ => Vector3.Zero + }; + } + + /// + /// Applies a color matrix to a vector, mapping x,y,z onto red,green,blue + /// + public static Vector3 Transform(in ColorMatrix matrix, Vector3 vector) { + return new Vector3( + matrix.c11 * vector.X + matrix.c21 * vector.Y + matrix.c31 * vector.Z + matrix.c51, + matrix.c12 * vector.X + matrix.c22 * vector.Y + matrix.c32 * vector.Z + matrix.c52, + matrix.c13 * vector.X + matrix.c23 * vector.Y + matrix.c33 * vector.Z + matrix.c53 + ); + } } public interface IGeneratorNum : IGenerator { @@ -219,6 +285,115 @@ public override string ToString() { } } +/// +/// The result of combining a number-producing generator with another generator or value +/// +[Serializable, NetSerializable] +public sealed class GeneratorArithmeticNum(IGenerator left, IGenerator right, GeneratorOperation operation) : IGeneratorNum, IGeneratorVector { + public bool PrefersVector3 { get; set; } = false; + + public float Generate(IRobustRandom random) { + return IGenerator.Operate(operation, IGenerator.GenerateScalar(left, random), IGenerator.GenerateScalar(right, random)); + } + + public Vector2 GenerateVector2(IRobustRandom random) { + return new Vector2(Generate(random)); + } + + public Vector3 GenerateVector3(IRobustRandom random) { + return new Vector3(Generate(random)); + } + + public override string ToString() { + return $"{left} {OperationToString(operation)} {right}"; + } + + internal static string OperationToString(GeneratorOperation operation) { + return operation switch { + GeneratorOperation.Add => "+", + GeneratorOperation.Subtract => "-", + GeneratorOperation.Multiply => "*", + GeneratorOperation.Divide => "/", + GeneratorOperation.Power => "**", + _ => "?" + }; + } +} + +/// +/// The result of combining a vector-producing generator with another generator or value +/// +/// These always produce a 3D vector, even when both operands are 2D. That's what BYOND does. +[Serializable, NetSerializable] +public sealed class GeneratorArithmeticVector(IGenerator left, IGenerator right, GeneratorOperation operation) : IGeneratorVector { + public bool PrefersVector3 { get; set; } = true; + + public Vector2 GenerateVector2(IRobustRandom random) { + var vector = GenerateVector3(random); + + return new Vector2(vector.X, vector.Y); + } + + public Vector3 GenerateVector3(IRobustRandom random) { + return IGenerator.Operate(operation, IGenerator.GenerateVector(left, random), IGenerator.GenerateVector(right, random)); + } + + public override string ToString() { + return $"{left} {GeneratorArithmeticNum.OperationToString(operation)} {right}"; + } +} + +/// +/// The result of multiplying a vector-producing generator by a matrix +/// +[Serializable, NetSerializable] +public sealed class GeneratorMatrixTransform(IGenerator inner, ColorMatrix matrix) : IGeneratorVector { + public bool PrefersVector3 { get; set; } = true; + + public Vector2 GenerateVector2(IRobustRandom random) { + var vector = GenerateVector3(random); + + return new Vector2(vector.X, vector.Y); + } + + public Vector3 GenerateVector3(IRobustRandom random) { + return IGenerator.Transform(matrix, IGenerator.GenerateVector(inner, random)); + } + + public override string ToString() { + return $"{inner} * matrix"; + } +} + +/// +/// The result of , rotating a generated vector in the XY plane +/// +[Serializable, NetSerializable] +public sealed class GeneratorTurn(IGenerator inner, float angle) : IGeneratorVector { + public bool PrefersVector3 { get; set; } = true; + + public Vector2 GenerateVector2(IRobustRandom random) { + var vector = GenerateVector3(random); + + return new Vector2(vector.X, vector.Y); + } + + public Vector3 GenerateVector3(IRobustRandom random) { + var vector = IGenerator.GenerateVector(inner, random); + var (sin, cos) = MathF.SinCos(angle * MathF.PI / 180f); + + return new Vector3( + cos * vector.X - sin * vector.Y, + sin * vector.X + cos * vector.Y, + vector.Z + ); + } + + public override string ToString() { + return $"turn({inner}, {angle})"; + } +} + [Serializable, NetSerializable] public sealed class GeneratorCube(Vector3 low, Vector3 high, GeneratorDistribution distribution) : IGeneratorVector { public bool PrefersVector3 { get; set; } = true;