diff --git a/Content.IntegrationTests/DMProject/Tests/change_area_appearance.dm b/Content.IntegrationTests/DMProject/Tests/change_area_appearance.dm index f1e805efb0..2940477c6f 100644 --- a/Content.IntegrationTests/DMProject/Tests/change_area_appearance.dm +++ b/Content.IntegrationTests/DMProject/Tests/change_area_appearance.dm @@ -2,7 +2,7 @@ color = rgb(255,0,0) /datum/unit_test/test_change_area_appearance/RunTest() - var/area/subtype/S = new() + var/area/subtype/S = areas_by_type[/area/subtype] var/list/block_turfs = block(locate(1,1,1), locate(2,2,2)) for(var/turf/T in block_turfs) S.contents += T diff --git a/Content.IntegrationTests/DMProject/Tests/range.dm b/Content.IntegrationTests/DMProject/Tests/range.dm index b4e92d4086..7ea0f5dd20 100644 --- a/Content.IntegrationTests/DMProject/Tests/range.dm +++ b/Content.IntegrationTests/DMProject/Tests/range.dm @@ -1,40 +1,128 @@ -//Tests that /proc/range() is iterating along the correct, wonky path +// tests all of range's possible cases + +#define LOC(x, y) locate(x, y, 3) + +/obj/contained/one +/obj/contained/two + +/datum/unit_test/range/proc/run_case(atom/center, list/expected, identifier, isorange) + var/error_index = 0 + var/list/result = isorange ? orange(center, 1) : range(center, 1) + try + if(result.len != expected.len) + error_index = expected.len + CRASH("result is [result.len > expected.len ? "longer" : "shorter"] than expected") + for(var/index in 1 to result.len) + if(result[index] != expected[index]) + error_index = index + CRASH("result does not match expected") + catch(var/exception/exc) + var/list/error_output = list() + error_output += "[identifier]: [exc]" + error_output += "expected:" + for(var/i in 1 to expected.len) + var/atom/A = expected[i] + error_output += ("\t([A.x], [A.y]) [A.type] [i == error_index ? "<-- here" : null]") + error_output += "got:" + for(var/i in 1 to result.len) + var/atom/A = result[i] + error_output += ("\t([A.x], [A.y]) [A.type] [i == error_index ? "<-- here" : null]") + + + CRASH(error_output.Join("\n")) + +// Tests the implementation of range() and orange() /datum/unit_test/range/RunTest() world.maxx = world.maxy = 5 - //Test that it goes in the right order - var/list/correctCoordinates = list( - list(3,3), - list(2,2), - list(2,3), - list(2,4), - list(3,2), - list(3,4), - list(4,2), - list(4,3), - list(4,4) + + var/turf/center = LOC(3, 3) + var/area/outer_area = areas_by_type[/area] + var/obj/container = new /obj(center) + var/obj/contained = new /obj/contained/one(container) + var/obj/contained_trash_1 = new /obj/contained/two(contained) + var/obj/contained_trash_2 = new /obj/contained/two(contained) + var/obj/contained_trash_3 = new /obj/contained/two(contained) + + var/list/turf_range_case = list( + LOC(3, 3), + outer_area, + container, + LOC(2, 2), + LOC(2, 3), + LOC(2, 4), + LOC(3, 2), + LOC(3, 4), + LOC(4, 2), + LOC(4, 3), + LOC(4, 4), ) - var/i = 1 - var/turf/centre = locate(3,3,1) - for(var/x in range(1,centre)) - var/turf/T = x - ASSERT(!isnull(T)) - var/list/coords = correctCoordinates[i] - ASSERT(coords[1] == T.x) - ASSERT(coords[2] == T.y) - i += 1 - if(i != 10) - CRASH("range(1,centre) iterated over [i - 1] tiles, expected 9") - //Test that arguments are parsed correctly - var/std = range(1,centre) - if(std ~! range(centre,1)) - CRASH("range(1,centre) and range(centre,1) do not return the same result.") - if(std ~! range("3x3",centre)) - CRASH("ViewRange argument parsing for range() isn't working correctly.") - //Test that getting the range from a mob includes the mob's loc. - var/list/mob_seen_turfs = list() - var/mob/test/timmy = new(centre) - for(var/turf/x in range(1,timmy)) - mob_seen_turfs += list(x) - if(std ~! mob_seen_turfs) - CRASH("Using a non-/turf Center for range() did not work correctly.") - del(timmy) \ No newline at end of file + + var/list/turf_orange_case = list( + LOC(2, 2), + outer_area, + LOC(2, 3), + LOC(2, 4), + LOC(3, 2), + LOC(3, 4), + LOC(4, 2), + LOC(4, 3), + LOC(4, 4), + ) + + var/list/container_range_case = list( + contained, + LOC(3, 3), + outer_area, + container, + LOC(2, 2), + LOC(2, 3), + LOC(2, 4), + LOC(3, 2), + LOC(3, 4), + LOC(4, 2), + LOC(4, 3), + LOC(4, 4), + ) + + var/list/container_orange_case = list( + LOC(3, 3), + outer_area, + LOC(2, 2), + LOC(2, 3), + LOC(2, 4), + LOC(3, 2), + LOC(3, 4), + LOC(4, 2), + LOC(4, 3), + LOC(4, 4), + ) + + var/list/contained_range_case = list( + contained_trash_1, + contained_trash_2, + contained_trash_3, + container, + contained, + ) + + var/list/contained_orange_case = list( + container, + ) + + + run_case(center, turf_range_case, nameof(turf_range_case), FALSE) + run_case(center, turf_orange_case, nameof(turf_orange_case), TRUE) + run_case(container, container_range_case, nameof(container_range_case), FALSE) + run_case(container, container_orange_case, nameof(container_orange_case), TRUE) + run_case(contained, contained_range_case, nameof(contained_range_case), FALSE) + run_case(contained, contained_orange_case, nameof(contained_orange_case), TRUE) + + // FIXME: these pass in BYOND, but the way we iterate over area turfs diverges + // var/list/area_range_case = list(outer_area) + outer_area.contents + // var/list/area_orange_case = area_range_case.Copy() + // run_case(outer_area, area_range_case, nameof(area_range_case), FALSE) + // run_case(outer_area, area_orange_case, nameof(area_orange_case), TRUE) + + del(container) + +#undef LOC diff --git a/Content.IntegrationTests/DMProject/code.dm b/Content.IntegrationTests/DMProject/code.dm index fdf82c2fb0..52cf462802 100644 --- a/Content.IntegrationTests/DMProject/code.dm +++ b/Content.IntegrationTests/DMProject/code.dm @@ -3,6 +3,11 @@ /turf/border /mob/test +var/global/list/areas_by_type = list() +/area/New() + areas_by_type[type] = src + + //The actual tests //NOTE: Tests placed in the IntegrationTests suite // should actually require a normal server in order to work. @@ -26,6 +31,9 @@ throw EXCEPTION("You must override RunTest()") /world/New() + // prepare areas + for(var/area_subtype in typesof(/area) - /area) + new area_subtype() for(var/subtype in typesof(/datum/unit_test)) if(subtype == /datum/unit_test) //skip the base class continue diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs index 209d6c870b..683c4a1417 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs @@ -60,8 +60,8 @@ protected override bool TryGetVar(string varName, out DreamValue value) { return true; case "view": // Number if square & centerable, string representation otherwise - if (View is { IsSquare: true, IsCenterable: true }) { - value = new DreamValue(View.Range); + if (View.CanSquareRange) { + value = new DreamValue(View.SquareRange.Value); } else { value = new DreamValue(View.ToString()); } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs b/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs index 434781f3a7..dba2a799ce 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs @@ -232,8 +232,8 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "view": // Number if square & centerable, string representation otherwise - if (DefaultView.IsSquare && DefaultView.IsCenterable) { - value = new DreamValue(DefaultView.Range); + if (DefaultView.CanSquareRange) { + value = new DreamValue(DefaultView.SquareRange.Value); } else { value = new DreamValue(DefaultView.ToString()); } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs index f599f2b3e4..05084fcffb 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs @@ -199,6 +199,88 @@ public static (DreamObjectAtom?, ViewRange) ResolveViewArguments(DreamManager dr return tiles; } + public static DreamValue HandleRange(NativeProc.Bundle bundle, DreamObject? usr, bool includeCenter) { + (DreamObjectAtom? center, ViewRange range) = DreamProcNativeHelpers.ResolveViewArguments(bundle.DreamManager, usr as DreamObjectAtom, bundle.Arguments); + if (center is null) + return new DreamValue(bundle.ObjectTree.CreateList()); + + HashSet seenAreas = []; + DreamList rangeList = bundle.ObjectTree.CreateList(range.Height * range.Width); + + void AddToList(DreamValue value) { + rangeList.AddValue(value); + if(value.TryGetValueAsDreamObject(out var turfValue) && !seenAreas.Contains(turfValue.Cell.Area)) { + var area = turfValue.Cell.Area; + rangeList.AddValue(new(area)); + seenAreas.Add(area); + } + } + + if(center is DreamObjectArea areaCenter) { // yeah you can do this + // setting rangeList directly cause we'll never hit the area case + rangeList.AddValue(new(center)); + foreach(var turf in areaCenter.Turfs) { + rangeList.AddValue(new(turf)); + foreach(var content in turf.Contents.EnumerateValues()) { + rangeList.AddValue(content); + } + } + + return new(rangeList); + } + else if(center is DreamObjectTurf turfCenter) { + if(includeCenter) { // if we're orange, we want to skip the else block too + AddToList(new(center)); + foreach(DreamValue content in turfCenter.Contents.EnumerateValues()) { + AddToList(content); + } + } + } + else { // we're getting the range of a container + // add our contents first + if(includeCenter) { + if(center.TryGetVariable("contents", out var centerContents) && centerContents.TryGetValueAsDreamList(out var centerContentsList)) { + foreach(DreamValue content in centerContentsList.EnumerateValues()) { + AddToList(content); + } + } + + centerContents.Dispose(); + } + + // the loc's contents will include us + if (center.TryGetVariable("loc", out DreamValue centerLoc)) { + if (centerLoc.TryGetValueAsDreamObject(out var centerLocObject)) { + AddToList(centerLoc); + + using var contents = centerLocObject.GetVariable("contents"); + if (contents.TryGetValueAsDreamList(out var locContentsList)) { + foreach (DreamValue content in locContentsList.EnumerateValues()) { + if(!includeCenter && content.TryGetValueAsDreamObject(out var dreamObject) && dreamObject == center) + continue; + AddToList(content); + } + } + } + + centerLoc.Dispose(); + if(centerLocObject is not DreamObjectTurf) { + return new(rangeList); + } + } + } + + // finally, add the surrounding turfs + foreach (var turf in DreamProcNativeHelpers.MakeViewSpiral(center, range)) { + AddToList(new DreamValue(turf)); + foreach (DreamValue content in turf.Contents.EnumerateValues()) { + AddToList(content); + } + } + + return new(rangeList); + } + public static DreamValue HandleViewersHearers(NativeProc.Bundle bundle, DreamObject? usr, bool ignoreLight) { DreamValue? depthValue = null; DreamObjectAtom? center = null; @@ -228,7 +310,7 @@ public static DreamValue HandleViewersHearers(NativeProc.Bundle bundle, DreamObj var centerPos = bundle.AtomManager.GetAtomPosition(center); if (depthValue is null || !depthValue.Value.TryGetValueAsInteger(out var depth)) - depth = bundle.DreamManager.WorldInstance.DefaultView.Range; + depth = bundle.DreamManager.WorldInstance.DefaultView.BiggestAxis; foreach (var mob in bundle.MapManager.GetMobsInRange(centerPos, depth)) { var (_, range) = ResolveViewArguments(bundle.DreamManager, mob, bundle.Arguments); @@ -283,7 +365,7 @@ public static DreamValue HandleOviewersOhearers(NativeProc.Bundle bundle, DreamO var centerPos = bundle.AtomManager.GetAtomPosition(center); if (depthValue is null || !depthValue.Value.TryGetValueAsInteger(out var depth)) - depth = bundle.DreamManager.WorldInstance.DefaultView.Range; + depth = bundle.DreamManager.WorldInstance.DefaultView.BiggestAxis; foreach (var atom in bundle.AtomManager.EnumerateAtoms(bundle.ObjectTree.Mob)) { var mob = (DreamObjectMob)atom; diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 93eeea6915..cc88ef74dc 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -1815,18 +1815,7 @@ public static DreamValue NativeProc_ohearers(NativeProc.Bundle bundle, DreamObje [DreamProcParameter("Dist", Type = DreamValueTypeFlag.Float, DefaultValue = 5)] [DreamProcParameter("Center", Type = DreamValueTypeFlag.DreamObject)] public static DreamValue NativeProc_orange(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { - (DreamObjectAtom? center, ViewRange range) = DreamProcNativeHelpers.ResolveViewArguments(bundle.DreamManager, usr as DreamObjectAtom, bundle.Arguments); - if (center is null) - return new DreamValue(bundle.ObjectTree.CreateList()); - DreamList rangeList = bundle.ObjectTree.CreateList(range.Height * range.Width); - foreach (var turf in DreamProcNativeHelpers.MakeViewSpiral(center, range)) { - rangeList.AddValue(new DreamValue(turf)); - foreach (DreamValue content in turf.Contents.EnumerateValues()) { - rangeList.AddValue(content); - } - } - - return new DreamValue(rangeList); + return DreamProcNativeHelpers.HandleRange(bundle, usr, false); } [DreamProc("oview")] @@ -1972,48 +1961,7 @@ public static DreamValue NativeProc_rand_seed(NativeProc.Bundle bundle, DreamObj [DreamProcParameter("Dist", Type = DreamValueTypeFlag.Float, DefaultValue = 5)] [DreamProcParameter("Center", Type = DreamValueTypeFlag.DreamObject)] public static DreamValue NativeProc_range(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { - (DreamObjectAtom? center, ViewRange range) = DreamProcNativeHelpers.ResolveViewArguments(bundle.DreamManager, usr as DreamObjectAtom, bundle.Arguments); - if (center is null) - return new DreamValue(bundle.ObjectTree.CreateList()); - - DreamList rangeList = bundle.ObjectTree.CreateList(range.Height * range.Width); - - //Have to include centre - rangeList.AddValue(new DreamValue(center)); - - if(center.TryGetVariable("contents", out var centerContents) && centerContents.TryGetValueAsDreamList(out var centerContentsList)) { - foreach(DreamValue content in centerContentsList.EnumerateValues()) { - rangeList.AddValue(content); - } - } - - centerContents.Dispose(); - - // If it's not a /turf, we have to include its loc and the loc's contents - if (center is not DreamObjectTurf && center.TryGetVariable("loc",out DreamValue centerLoc)) { - if (centerLoc.TryGetValueAsDreamObject(out var centerLocObject)) { - rangeList.AddValue(centerLoc); - - using var contents = centerLocObject.GetVariable("contents"); - if (contents.TryGetValueAsDreamList(out var locContentsList)) { - foreach (DreamValue content in locContentsList.EnumerateValues()) { - rangeList.AddValue(content); - } - } - } - - centerLoc.Dispose(); - } - - //And then everything else - foreach (var turf in DreamProcNativeHelpers.MakeViewSpiral(center, range)) { - rangeList.AddValue(new DreamValue(turf)); - foreach (DreamValue content in turf.Contents.EnumerateValues()) { - rangeList.AddValue(content); - } - } - - return new DreamValue(rangeList); + return DreamProcNativeHelpers.HandleRange(bundle, usr, true); } [DreamProc("ref")] diff --git a/OpenDreamShared/Dream/ViewRange.cs b/OpenDreamShared/Dream/ViewRange.cs index cc216c210e..5bfe7ad040 100644 --- a/OpenDreamShared/Dream/ViewRange.cs +++ b/OpenDreamShared/Dream/ViewRange.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Robust.Shared.Maths; namespace OpenDreamShared.Dream; @@ -9,20 +10,23 @@ namespace OpenDreamShared.Dream; /// public readonly struct ViewRange { public readonly int Width, Height; - public bool IsSquare => (Width == Height); + public int BiggestAxis => Math.Max(Width, Height); public int CenterX => Width / 2; public int CenterY => Height / 2; public Vector2i Center => (CenterX, CenterY); - //View can be centered in both directions? + public bool IsSquare => Width == Height; public bool IsCenterable => (Width % 2 == 1) && (Height % 2 == 1); + [MemberNotNullWhen(true, nameof(SquareRange))] + public bool CanSquareRange => IsSquare && IsCenterable; + /// - /// The distance this ViewRange covers in every direction if and - /// are true + /// The distance this ViewRange covers in every direction + /// if is true /// - public int Range => (IsSquare && IsCenterable) ? (Width - 1) / 2 : 0; + public int? SquareRange => CanSquareRange ? (Width - 1) / 2 : null; public ViewRange(int range) { // A square covering "range" cells in each direction