diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e90ceaddd9..f9ca64226e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,7 +28,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.100 + dotnet-version: 9.0.100 - name: Install dependencies run: dotnet restore - name: Build @@ -59,7 +59,7 @@ jobs: tar -czvf output_tools/OpenDreamServer_TOOLS_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }}.tar.gz -C output_tools OpenDreamServer_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }} - name: Upload artifact if: github.event_name == 'push' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-${{ matrix.os }} path: | @@ -73,7 +73,7 @@ jobs: if: github.event_name == 'push' steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts - name: Publish latest release diff --git a/.github/workflows/compiler-test.yml b/.github/workflows/compiler-test.yml index 60ec6de731..9d44fc0634 100644 --- a/.github/workflows/compiler-test.yml +++ b/.github/workflows/compiler-test.yml @@ -18,8 +18,6 @@ jobs: run: | cd main/ git submodule update --init --recursive - - name: Pull engine updates - uses: space-wizards/submodule-dependency@v0.1.5 - name: Update Engine Submodules run: | cd main/RobustToolbox/ @@ -27,7 +25,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.100 + dotnet-version: 9.0.100 - name: Install compiler dependencies run: dotnet restore main/DMCompiler/DMCompiler.csproj - name: Install disassembler dependencies @@ -38,6 +36,10 @@ jobs: run: dotnet build main/DMDisassembler/DMDisassembler.csproj --property WarningLevel=0 --configuration Release --no-restore /m - name: Compile TestGame run: main\bin\DMCompiler\DMCompiler.exe main\TestGame\environment.dme --suppress-unimplemented + - name: Compile TestGame in directory + run: | + cd main\TestGame\ + ..\bin\DMCompiler\DMCompiler.exe environment.dme --suppress-unimplemented - name: Checkout /tg/station Master uses: actions/checkout@v2 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cdc9c261c4..befc90e53e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,9 +3,11 @@ name: Lint on: pull_request: branches: [master] + types: [opened, synchronize, reopened, ready_for_review] jobs: lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: @@ -16,7 +18,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.100 + dotnet-version: 9.0.100 - name: Setup Resharper run: dotnet tool install -g JetBrains.ReSharper.GlobalTools - name: Run Linter diff --git a/Content.IntegrationTests/DMProject/Tests/atom_appearance.dm b/Content.IntegrationTests/DMProject/Tests/atom_appearance.dm new file mode 100644 index 0000000000..4b07dda7e3 --- /dev/null +++ b/Content.IntegrationTests/DMProject/Tests/atom_appearance.dm @@ -0,0 +1,10 @@ +/obj/thingtocopy + name = "hello" + desc = "this is a thing" + +/proc/test_appearance() + var/obj/thingtocopy/T = new() + var/obj/otherthing = new() + otherthing.appearance = T.appearance + ASSERT(otherthing.name == T.name) + ASSERT(otherthing.desc == T.desc) \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/Tests/filter_initial.dm b/Content.IntegrationTests/DMProject/Tests/filter_initial.dm new file mode 100644 index 0000000000..65812623c0 --- /dev/null +++ b/Content.IntegrationTests/DMProject/Tests/filter_initial.dm @@ -0,0 +1,16 @@ +/obj/blurry + filters = filter(type="blur", size=2) + +/obj/veryblurry + filters = list(type="blur", size=4) + +/obj/notatallblurry + filters = list() + +/proc/test_filter_init() + var/obj/veryblurry/VB = new() + ASSERT(length(VB.filters) == 1) + var/obj/blurry/B = new() + ASSERT(length(B.filters) == 1) + var/obj/notatallblurry/NAB = new() + ASSERT(length(NAB.filters) == 0) \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/Tests/icons.dmi b/Content.IntegrationTests/DMProject/Tests/icons.dmi new file mode 100644 index 0000000000..401b72d431 Binary files /dev/null and b/Content.IntegrationTests/DMProject/Tests/icons.dmi differ diff --git a/Content.Tests/DMProject/Tests/Image/subclass.dm b/Content.IntegrationTests/DMProject/Tests/image.dm similarity index 85% rename from Content.Tests/DMProject/Tests/Image/subclass.dm rename to Content.IntegrationTests/DMProject/Tests/image.dm index 1a8010a04a..e436c22df3 100644 --- a/Content.Tests/DMProject/Tests/Image/subclass.dm +++ b/Content.IntegrationTests/DMProject/Tests/image.dm @@ -2,7 +2,9 @@ plane = 123 icon_state = "subclass" -/proc/RunTest() +/proc/test_images() + ASSERT(image('icons.dmi', "mob") != null) + var/image/test = new /image/subclass ASSERT(test.plane == 123) ASSERT(test.icon_state == "subclass") diff --git a/Content.Tests/DMProject/Tests/Statements/For/nonlocal_var.dm b/Content.IntegrationTests/DMProject/Tests/nonlocal_var.dm similarity index 86% rename from Content.Tests/DMProject/Tests/Statements/For/nonlocal_var.dm rename to Content.IntegrationTests/DMProject/Tests/nonlocal_var.dm index 8f01871b2c..e0a8432b69 100644 --- a/Content.Tests/DMProject/Tests/Statements/For/nonlocal_var.dm +++ b/Content.IntegrationTests/DMProject/Tests/nonlocal_var.dm @@ -10,6 +10,6 @@ out += dir ASSERT(out == 14) -/proc/RunTest() +/proc/test_nonlocal_var() var/mob/m = new m.dodir() diff --git a/Content.IntegrationTests/DMProject/Tests/string_interpolation.dm b/Content.IntegrationTests/DMProject/Tests/string_interpolation.dm new file mode 100644 index 0000000000..f64128b0c1 --- /dev/null +++ b/Content.IntegrationTests/DMProject/Tests/string_interpolation.dm @@ -0,0 +1,13 @@ +/obj/blombo + name = "Blombo" + gender = FEMALE + +/obj/blorpo + name = "Blorpo" + gender = MALE + +/proc/test_string_interpolation() + var/obj/blombo/b = new + var/obj/blorpo/b2 = new + var/result_text = "[b]? Nobody likes \him. \He is awful! Unlike [b2]. \He is pretty cool!" + ASSERT(result_text == "Blombo? Nobody likes her. She is awful! Unlike Blorpo. He is pretty cool!") \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/code.dm b/Content.IntegrationTests/DMProject/code.dm index ebb99dd087..6e6827200f 100644 --- a/Content.IntegrationTests/DMProject/code.dm +++ b/Content.IntegrationTests/DMProject/code.dm @@ -30,4 +30,9 @@ test_color_matrix() test_range() test_verb_duplicate() + test_appearance() + test_nonlocal_var() + test_images() + test_filter_init() + test_string_interpolation() world.log << "IntegrationTests successful, /world/New() exiting..." \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/environment.dme b/Content.IntegrationTests/DMProject/environment.dme index a4abe97328..80f5c14baf 100644 --- a/Content.IntegrationTests/DMProject/environment.dme +++ b/Content.IntegrationTests/DMProject/environment.dme @@ -1,7 +1,12 @@ #include "code.dm" +#include "Tests/atom_appearance.dm" #include "Tests/block.dm" #include "Tests/color_matrix.dm" #include "Tests/range.dm" #include "Tests/verb_duplicate.dm" +#include "Tests/nonlocal_var.dm" +#include "Tests/image.dm" +#include "Tests/filter_initial.dm" +#include "Tests/string_interpolation.dm" #include "map.dmm" #include "interface.dmf" \ No newline at end of file diff --git a/Content.Tests/ContentUnitTest.cs b/Content.Tests/ContentUnitTest.cs index c102584fb6..b309b09f73 100644 --- a/Content.Tests/ContentUnitTest.cs +++ b/Content.Tests/ContentUnitTest.cs @@ -3,6 +3,7 @@ using System.Reflection; using OpenDreamClient; using OpenDreamRuntime; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Rendering; using OpenDreamShared; using OpenDreamShared.Rendering; diff --git a/Content.Tests/DMProject/Broken Tests/Expression/Constants/numeral_inf.dm b/Content.Tests/DMProject/Broken Tests/Expression/Constants/numeral_inf.dm deleted file mode 100644 index 4fc8f87043..0000000000 --- a/Content.Tests/DMProject/Broken Tests/Expression/Constants/numeral_inf.dm +++ /dev/null @@ -1,9 +0,0 @@ - -//# issue 464 - -/proc/RunTest() - var/a = 1#INF - var/b = -1#IND - // TODO: This actually varies by OS I believe, this is the Windows result. (OpenDream is broken either way) - ASSERT(a == "inf") - ASSERT(b == 0) diff --git a/Content.Tests/DMProject/Broken Tests/Expression/proccall_keyword_param.dm b/Content.Tests/DMProject/Broken Tests/Expression/proccall_keyword_param.dm deleted file mode 100644 index 87f90f6e81..0000000000 --- a/Content.Tests/DMProject/Broken Tests/Expression/proccall_keyword_param.dm +++ /dev/null @@ -1,13 +0,0 @@ - -//# issue 655 -//# issue 265 - -// TODO: We error on this when BYOND doesn't. Revisit this test when we can selectively disable/enable errors with pragmas - -/obj/proc/nullproc(null, temp) - ASSERT(null == 1) - ASSERT(temp == 2) - -/proc/RunTest() - var/obj/o = new - o.nullproc(1,2) diff --git a/Content.Tests/DMProject/Broken Tests/Procs/Arglist/initial.dm b/Content.Tests/DMProject/Broken Tests/Procs/Arglist/initial.dm deleted file mode 100644 index 2e77abf518..0000000000 --- a/Content.Tests/DMProject/Broken Tests/Procs/Arglist/initial.dm +++ /dev/null @@ -1,6 +0,0 @@ - -/proc/_initial(...) - return initial(arglist(args)) - -/proc/RunTest() - return \ No newline at end of file diff --git a/Content.Tests/DMProject/Broken Tests/Tree/pathop_lhs.dm b/Content.Tests/DMProject/Broken Tests/Tree/pathop_lhs.dm deleted file mode 100644 index 92520fa2ee..0000000000 --- a/Content.Tests/DMProject/Broken Tests/Tree/pathop_lhs.dm +++ /dev/null @@ -1,15 +0,0 @@ - -//# issue 617 - -/atom/movable - var/paths = list() - top - - proc/do_assign() - paths += .top - -/proc/RunTest() - var/atom/movable/t = new - t.do_assign() - ASSERT(length(t.paths) == 1) - ASSERT(t.paths[1] == /atom/movable/top) diff --git a/Content.Tests/DMProject/BrokenTests/Const/ConstProc.dm b/Content.Tests/DMProject/BrokenTests/Const/ConstProc.dm new file mode 100644 index 0000000000..cce0731347 --- /dev/null +++ b/Content.Tests/DMProject/BrokenTests/Const/ConstProc.dm @@ -0,0 +1,6 @@ +var/const/ConstProc1_a = rgb(0,0,255) + +/proc/RunTest() + var/const/ConstProc1_b = rgb(0,0,255) + ASSERT(ConstProc1_a == "#0000ff") + ASSERT(ConstProc1_b == "#0000ff") diff --git a/Content.Tests/DMProject/Broken Tests/Const/ConstSort.dm b/Content.Tests/DMProject/BrokenTests/Const/ConstSort.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Const/ConstSort.dm rename to Content.Tests/DMProject/BrokenTests/Const/ConstSort.dm diff --git a/Content.Tests/DMProject/Broken Tests/Expression/Modified Type/basic.dm b/Content.Tests/DMProject/BrokenTests/Expression/ModifiedType/basic.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Expression/Modified Type/basic.dm rename to Content.Tests/DMProject/BrokenTests/Expression/ModifiedType/basic.dm diff --git a/Content.Tests/DMProject/Broken Tests/Expression/Modified Type/multi.dm b/Content.Tests/DMProject/BrokenTests/Expression/ModifiedType/multi.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Expression/Modified Type/multi.dm rename to Content.Tests/DMProject/BrokenTests/Expression/ModifiedType/multi.dm diff --git a/Content.Tests/DMProject/Broken Tests/Expression/Modified Type/space_before_brace.dm b/Content.Tests/DMProject/BrokenTests/Expression/ModifiedType/space_before_brace.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Expression/Modified Type/space_before_brace.dm rename to Content.Tests/DMProject/BrokenTests/Expression/ModifiedType/space_before_brace.dm diff --git a/Content.Tests/DMProject/Broken Tests/Expression/String/raw2.dm b/Content.Tests/DMProject/BrokenTests/Expression/String/raw2.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Expression/String/raw2.dm rename to Content.Tests/DMProject/BrokenTests/Expression/String/raw2.dm diff --git a/Content.Tests/DMProject/Broken Tests/Expression/escape_identifier.dm b/Content.Tests/DMProject/BrokenTests/Expression/escape_identifier.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Expression/escape_identifier.dm rename to Content.Tests/DMProject/BrokenTests/Expression/escape_identifier.dm diff --git a/Content.Tests/DMProject/Broken Tests/Indent/closing_brace.dm b/Content.Tests/DMProject/BrokenTests/Indent/closing_brace.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Indent/closing_brace.dm rename to Content.Tests/DMProject/BrokenTests/Indent/closing_brace.dm diff --git a/Content.Tests/DMProject/Broken Tests/Indent/ignore_close_bracket_indent.dm b/Content.Tests/DMProject/BrokenTests/Indent/ignore_close_bracket_indent.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Indent/ignore_close_bracket_indent.dm rename to Content.Tests/DMProject/BrokenTests/Indent/ignore_close_bracket_indent.dm diff --git a/Content.Tests/DMProject/Broken Tests/Indent/whitespace_before_var.dm b/Content.Tests/DMProject/BrokenTests/Indent/whitespace_before_var.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Indent/whitespace_before_var.dm rename to Content.Tests/DMProject/BrokenTests/Indent/whitespace_before_var.dm diff --git a/Content.Tests/DMProject/Broken Tests/Operators/ninto.dm b/Content.Tests/DMProject/BrokenTests/Operators/ninto.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Operators/ninto.dm rename to Content.Tests/DMProject/BrokenTests/Operators/ninto.dm diff --git a/Content.Tests/DMProject/Broken Tests/Operators/valid_and_null.dm b/Content.Tests/DMProject/BrokenTests/Operators/valid_and_null.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Operators/valid_and_null.dm rename to Content.Tests/DMProject/BrokenTests/Operators/valid_and_null.dm diff --git a/Content.Tests/DMProject/Broken Tests/Operators/valid_and_null_assign.dm b/Content.Tests/DMProject/BrokenTests/Operators/valid_and_null_assign.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Operators/valid_and_null_assign.dm rename to Content.Tests/DMProject/BrokenTests/Operators/valid_and_null_assign.dm diff --git a/Content.Tests/DMProject/Broken Tests/Preprocessor/define_var_clash1.dm b/Content.Tests/DMProject/BrokenTests/Preprocessor/define_var_clash1.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Preprocessor/define_var_clash1.dm rename to Content.Tests/DMProject/BrokenTests/Preprocessor/define_var_clash1.dm diff --git a/Content.Tests/DMProject/Broken Tests/Procs/Filter/outline.dm b/Content.Tests/DMProject/BrokenTests/Procs/Filter/outline.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Procs/Filter/outline.dm rename to Content.Tests/DMProject/BrokenTests/Procs/Filter/outline.dm diff --git a/Content.Tests/DMProject/Broken Tests/Procs/allow_null_argname.dm b/Content.Tests/DMProject/BrokenTests/Procs/allow_null_argname.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Procs/allow_null_argname.dm rename to Content.Tests/DMProject/BrokenTests/Procs/allow_null_argname.dm diff --git a/Content.Tests/DMProject/Broken Tests/Procs/indeterminate_assoc_arg.dm b/Content.Tests/DMProject/BrokenTests/Procs/indeterminate_assoc_arg.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Procs/indeterminate_assoc_arg.dm rename to Content.Tests/DMProject/BrokenTests/Procs/indeterminate_assoc_arg.dm diff --git a/Content.Tests/DMProject/Broken Tests/Special Procs/new/atom_bad_loc.dm b/Content.Tests/DMProject/BrokenTests/SpecialProcs/new/atom_bad_loc.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Special Procs/new/atom_bad_loc.dm rename to Content.Tests/DMProject/BrokenTests/SpecialProcs/new/atom_bad_loc.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/Assign/to_src.dm b/Content.Tests/DMProject/BrokenTests/Statements/Assign/to_src.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/Assign/to_src.dm rename to Content.Tests/DMProject/BrokenTests/Statements/Assign/to_src.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/Control Flow/sleep/no_paren.dm b/Content.Tests/DMProject/BrokenTests/Statements/ControlFlow/sleep/no_paren.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/Control Flow/sleep/no_paren.dm rename to Content.Tests/DMProject/BrokenTests/Statements/ControlFlow/sleep/no_paren.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/expr_only.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/expr_only.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/expr_only.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/expr_only.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/expr_only_decl.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/expr_only_decl.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/expr_only_decl.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/expr_only_decl.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/fakeout4.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/fakeout4.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/fakeout4.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/fakeout4.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/incr_multi_expr.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/incr_multi_expr.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/incr_multi_expr.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/incr_multi_expr.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/init_multi_expr.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/init_multi_expr.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/init_multi_expr.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/init_multi_expr.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/var_only.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/var_only.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/var_only.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/var_only.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/var_only_as.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/var_only_as.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/var_only_as.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/var_only_as.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/var_only_typed.dm b/Content.Tests/DMProject/BrokenTests/Statements/For/var_only_typed.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Statements/For/var_only_typed.dm rename to Content.Tests/DMProject/BrokenTests/Statements/For/var_only_typed.dm diff --git a/Content.Tests/DMProject/Broken Tests/Stdlib/List/bitwise_ops.dm b/Content.Tests/DMProject/BrokenTests/Stdlib/List/bitwise_ops.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Stdlib/List/bitwise_ops.dm rename to Content.Tests/DMProject/BrokenTests/Stdlib/List/bitwise_ops.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero1.dm b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero1.dm similarity index 64% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero1.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero1.dm index bf3227ba91..6fed176183 100644 --- a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero1.dm +++ b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero1.dm @@ -1,4 +1,4 @@ -// COMPILE ERROR +// COMPILE ERROR OD0011 var/a = 1 / 0 diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero3.dm b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero3.dm similarity index 66% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero3.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero3.dm index c3b8f68e03..95f4fef712 100644 --- a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero3.dm +++ b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero3.dm @@ -1,4 +1,4 @@ -// COMPILE ERROR +// COMPILE ERROR OD0011 var/static/a = 1 / 0 diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero4.dm b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero4.dm similarity index 76% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero4.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero4.dm index 227b380ca5..73064bbe1d 100644 --- a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero4.dm +++ b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero4.dm @@ -1,4 +1,4 @@ -// COMPILE ERROR +// COMPILE ERROR OD0011 /proc/one() return 1 diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero6.dm b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero6.dm similarity index 70% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero6.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero6.dm index 7cb5c6ccc5..b637530b11 100644 --- a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero6.dm +++ b/Content.Tests/DMProject/BrokenTests/Tree/Const/Div_Zero/div_zero6.dm @@ -1,4 +1,4 @@ -// COMPILE ERROR +// COMPILE ERROR OD0011 var/const/a = 0 var/b = 1 / a diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/const_fn.dm b/Content.Tests/DMProject/BrokenTests/Tree/Const/const_fn.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/const_fn.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Const/const_fn.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Global/Static Scope/no_objproc_in_static.dm b/Content.Tests/DMProject/BrokenTests/Tree/Global/StaticScope/no_objproc_in_static.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Global/Static Scope/no_objproc_in_static.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Global/StaticScope/no_objproc_in_static.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Global/Static Scope/scope2.dm b/Content.Tests/DMProject/BrokenTests/Tree/Global/StaticScope/scope2.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Global/Static Scope/scope2.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Global/StaticScope/scope2.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Global/Static Scope/scope5.dm b/Content.Tests/DMProject/BrokenTests/Tree/Global/StaticScope/scope5.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Global/Static Scope/scope5.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Global/StaticScope/scope5.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Global/Var/vars_field.dm b/Content.Tests/DMProject/BrokenTests/Tree/Global/Var/vars_field.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Global/Var/vars_field.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Global/Var/vars_field.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Override/override_toplevel_nonexist.dm b/Content.Tests/DMProject/BrokenTests/Tree/Override/override_toplevel_nonexist.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Override/override_toplevel_nonexist.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Override/override_toplevel_nonexist.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Override/override_x.dm b/Content.Tests/DMProject/BrokenTests/Tree/Override/override_x.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Override/override_x.dm rename to Content.Tests/DMProject/BrokenTests/Tree/Override/override_x.dm diff --git a/Content.Tests/DMProject/Broken Tests/Type Inference/parent_loop.dm b/Content.Tests/DMProject/BrokenTests/TypeInference/parent_loop.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Type Inference/parent_loop.dm rename to Content.Tests/DMProject/BrokenTests/TypeInference/parent_loop.dm diff --git a/Content.Tests/DMProject/Broken Tests/Type Inference/paths.dm b/Content.Tests/DMProject/BrokenTests/TypeInference/paths.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Type Inference/paths.dm rename to Content.Tests/DMProject/BrokenTests/TypeInference/paths.dm diff --git a/Content.Tests/DMProject/Broken Tests/Typemaker/arg_implicit_null.dm b/Content.Tests/DMProject/BrokenTests/Typemaker/arg_implicit_null.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Typemaker/arg_implicit_null.dm rename to Content.Tests/DMProject/BrokenTests/Typemaker/arg_implicit_null.dm diff --git a/Content.Tests/DMProject/Broken Tests/Typemaker/proc_path_error.dm b/Content.Tests/DMProject/BrokenTests/Typemaker/proc_path_error.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Typemaker/proc_path_error.dm rename to Content.Tests/DMProject/BrokenTests/Typemaker/proc_path_error.dm diff --git a/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm b/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm index 97b3ef4fee..128041a6ce 100644 --- a/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm +++ b/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm @@ -1,7 +1,10 @@ -/datum/test/var/bar = "foobar" +/datum/test /proc/RunTest() var/datum/test/D = __IMPLIED_TYPE__ - ASSERT(D.bar == "foobar") + ASSERT(D == /datum/test) + D = ArgumentTest(__IMPLIED_TYPE__) +/proc/ArgumentTest(some_argument) + ASSERT(some_argument == /datum/test) \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Expression/Constants/stringify_inf.dm b/Content.Tests/DMProject/Tests/Expression/Constants/stringify_inf.dm new file mode 100644 index 0000000000..615f039d31 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Expression/Constants/stringify_inf.dm @@ -0,0 +1,8 @@ + +/proc/RunTest() + var/a = 1#INF + var/b = -1#INF + var/c = -1#IND + ASSERT("[a]" == "inf") + ASSERT("[b]" == "-inf") + ASSERT("[c]" == "nan") diff --git a/Content.Tests/DMProject/Broken Tests/Expression/String/raw1.dm b/Content.Tests/DMProject/Tests/Expression/String/raw1.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Expression/String/raw1.dm rename to Content.Tests/DMProject/Tests/Expression/String/raw1.dm diff --git a/Content.Tests/DMProject/Tests/Expression/String/raw2.dm b/Content.Tests/DMProject/Tests/Expression/String/raw2.dm new file mode 100644 index 0000000000..01f9e97343 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Expression/String/raw2.dm @@ -0,0 +1,7 @@ + +//# issue 380 + +/proc/RunTest() + var/a = @(ZZZ) +asdfZZZ + ASSERT(a == "asdf") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Expression/String/raw3.dm b/Content.Tests/DMProject/Tests/Expression/String/raw3.dm new file mode 100644 index 0000000000..b750f3610b --- /dev/null +++ b/Content.Tests/DMProject/Tests/Expression/String/raw3.dm @@ -0,0 +1,7 @@ + +//# issue 380 + +/proc/RunTest() + var/a = @(ZZQ) +asdfZZQ + ASSERT(a == "asdf") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Expression/proccall_keyword_param.dm b/Content.Tests/DMProject/Tests/Expression/proccall_keyword_param.dm new file mode 100644 index 0000000000..89b4065260 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Expression/proccall_keyword_param.dm @@ -0,0 +1,11 @@ + +//# issue 655 +//# issue 265 + +/datum/proc/nullproc(null, temp) + ASSERT(isnull(null)) + ASSERT(temp == 2) + +/proc/RunTest() + var/datum/D = new + D.nullproc(1,2) diff --git a/Content.Tests/DMProject/Tests/Image/Image.dm b/Content.Tests/DMProject/Tests/Image/Image.dm deleted file mode 100644 index b50648f3e6..0000000000 --- a/Content.Tests/DMProject/Tests/Image/Image.dm +++ /dev/null @@ -1,2 +0,0 @@ -/proc/RunTest() - ASSERT(image('icons.dmi', "mob") != null) \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/List/ListAppendSelf.dm b/Content.Tests/DMProject/Tests/List/ListAppendSelf.dm new file mode 100644 index 0000000000..606389e1b7 --- /dev/null +++ b/Content.Tests/DMProject/Tests/List/ListAppendSelf.dm @@ -0,0 +1,8 @@ +/proc/RunTest() + var/L1 = list(1, 2) + L1 += L1 + ASSERT(L1 ~= list(1, 2, 1, 2)) + + var/L2 = list(a=1, b=2) + L2 += L2 + ASSERT(L2 ~= list(a=1, b=2, "a", "b")) diff --git a/Content.Tests/DMProject/Broken Tests/List/ListNullArg.dm b/Content.Tests/DMProject/Tests/List/ListNullArg.dm similarity index 64% rename from Content.Tests/DMProject/Broken Tests/List/ListNullArg.dm rename to Content.Tests/DMProject/Tests/List/ListNullArg.dm index 082351cda2..ccbb0c7643 100644 --- a/Content.Tests/DMProject/Broken Tests/List/ListNullArg.dm +++ b/Content.Tests/DMProject/Tests/List/ListNullArg.dm @@ -1,7 +1,7 @@ // RUNTIME ERROR /proc/ListNullArg2(a[5][3]) - ASSERT(a[1].len == 3) + ASSERT(a[1].len == 3) // a should be null /proc/RunTest() ListNullArg2() diff --git a/Content.Tests/DMProject/Broken Tests/List/ListNullArg1.dm b/Content.Tests/DMProject/Tests/List/ListNullArg1.dm similarity index 65% rename from Content.Tests/DMProject/Broken Tests/List/ListNullArg1.dm rename to Content.Tests/DMProject/Tests/List/ListNullArg1.dm index ade75cf317..9b8360c394 100644 --- a/Content.Tests/DMProject/Broken Tests/List/ListNullArg1.dm +++ b/Content.Tests/DMProject/Tests/List/ListNullArg1.dm @@ -1,7 +1,7 @@ // RUNTIME ERROR /proc/ListNullArg1(a[5]) - ASSERT(a.len == 5) + ASSERT(a.len == 5) // a should be null /proc/RunTest() ListNullArg1() diff --git a/Content.Tests/DMProject/Tests/Operators/Division.dm b/Content.Tests/DMProject/Tests/Operators/Division.dm index c60702e8fc..0250ae9dcd 100644 --- a/Content.Tests/DMProject/Tests/Operators/Division.dm +++ b/Content.Tests/DMProject/Tests/Operators/Division.dm @@ -7,39 +7,39 @@ var/list/expected = list( 1, "Error", + 10, "Error", + "Error", // index 5 "Error", "Error", "Error", "Error", + "Error", // index 10 "Error", "Error", "Error", "Error", + "Error", // index 15 "Error", "Error", "Error", "Error", + "Error", // index 20 "Error", "Error", - "Error", - "Error", - "Error", - "Error", - "Error", - 0, 0, 0, + 0, // index 25 0, 0, 0, 0, + 0, // index 30 0, 0, 0, - 0, - "Error", "Error", + "Error", // index 35 "Error", "Error", "Error", @@ -128,4 +128,4 @@ "Error" ) - test_binary_operator(/proc/divide, expected) \ No newline at end of file + test_binary_operator(/proc/divide, expected) diff --git a/Content.Tests/DMProject/Tests/Operators/Shared/operator_testing.dm b/Content.Tests/DMProject/Tests/Operators/Shared/operator_testing.dm index d2b2071478..749bc9c0a4 100644 --- a/Content.Tests/DMProject/Tests/Operators/Shared/operator_testing.dm +++ b/Content.Tests/DMProject/Tests/Operators/Shared/operator_testing.dm @@ -33,7 +33,7 @@ var/list/operator_test_values = list( result = "Error" if (result ~! expected_result) - CRASH("Expected [json_encode(expected_result)] for [json_encode(a)], instead got [json_encode(result)]") + CRASH("Expected [json_encode(expected_result)] for [json_encode(a)], instead got [json_encode(result)] at index [i - 1]") /proc/test_binary_operator(var/operator_proc, var/list/expected) var/i = 1 @@ -48,4 +48,4 @@ var/list/operator_test_values = list( result = "Error" if (result ~! expected_result) - CRASH("Expected [json_encode(expected_result)] for [json_encode(a)] and [json_encode(b)], instead got [json_encode(result)]") + CRASH("Expected [json_encode(expected_result)] for [json_encode(a)] and [json_encode(b)], instead got [json_encode(result)] at index [i - 1]") diff --git a/Content.Tests/DMProject/Tests/Operators/valid_and_null.dm b/Content.Tests/DMProject/Tests/Operators/valid_and_null.dm new file mode 100644 index 0000000000..d7655bda0c --- /dev/null +++ b/Content.Tests/DMProject/Tests/Operators/valid_and_null.dm @@ -0,0 +1,130 @@ +//simple test of all basic operators with valid and C(null) arguments +//We can't be having const folding in here +#define C(X) pick(X) +/proc/RunTest() + var/a = C(2) + ASSERT(!C(null) == C(1)) + ASSERT(!!C(null) == C(0)) + + ASSERT(~C(1) == 16777214) + ASSERT(~C(0) == 16777215) + ASSERT(~C(null) == 16777215) + + ASSERT(C(1) + C(1) == C(2)) + ASSERT(C(null) + C(1) == C(1)) + ASSERT(C(1) + C(null) == C(1)) + + ASSERT(C(1) - C(1) == C(0)) + ASSERT(C(null) - C(1) == C(-1)) + ASSERT(C(1) - C(null) == C(1)) + + a = C(2) + ASSERT(-a == C(-2)) + a = C(null) + ASSERT(-a == C(0)) + + a = C(1) + ASSERT(a++ == C(1)) + ASSERT(a == C(2)) + ASSERT(++a == C(3)) + ASSERT(a == C(3)) + + ASSERT(a-- == C(3)) + ASSERT(a == C(2)) + ASSERT(--a == C(1)) + ASSERT(a == C(1)) + + a = C(null) + ASSERT(a-- == C(null)) + ASSERT(a == C(-1)) + a = C(null) + ASSERT(--a == C(-1)) + ASSERT(a == C(-1)) + a = C(null) + ASSERT(a++ == C(null)) + ASSERT(a == C(1)) + a = C(null) + ASSERT(++a == C(1)) + ASSERT(a == C(1)) + + ASSERT(C(2) ** C(3) == 8) + ASSERT(C(2) ** C(null) == C(1)) + ASSERT(C(null) ** C(2) == C(0)) + + ASSERT(C(2) * C(3) == 6) + ASSERT(C(2) * C(null) == C(0)) + ASSERT(C(null) * C(2) == C(0)) + + ASSERT(C(4) / C(2) == C(2)) + ASSERT(C(null) / C(2) == C(0)) + ASSERT(C(2) / C(null) == C(2)) + ASSERT(C(null) / C(null) == C(0)) + + ASSERT(C(4) % C(3) == C(1)) + ASSERT(C(null) % C(3) == C(0)) + //ASSERT(C(4) % C(null) == div by zero) + + ASSERT(C(1) < C(1) == C(0)) + ASSERT(C(null) < C(1) == C(1)) + ASSERT(C(1) < C(null) == C(0)) + + ASSERT(C(1) <= C(1) == C(1)) + ASSERT(C(null) <= C(1) == C(1)) + ASSERT(C(1) <= C(null) == C(0)) + + ASSERT(C(1) > C(1) == C(0)) + ASSERT(C(null) > C(1) == C(0)) + ASSERT(C(1) > C(null) == C(1)) + + ASSERT(C(1) >= C(1) == C(1)) + ASSERT(C(null) >= C(1) == C(0)) + ASSERT(C(1) >= C(null) == C(1)) + + ASSERT(C(1) << C(1) == C(2)) + ASSERT(C(null) << C(1) == C(0)) + ASSERT(C(1) << C(null) == C(1)) + + ASSERT(C(1) >> C(1) == C(0)) + ASSERT(C(null) >> C(1) == C(0)) + ASSERT(C(1) >> C(null) == C(1)) + + ASSERT((C(1) == C(1)) == C(1)) + ASSERT((C(null) == C(null)) == C(1)) + ASSERT((C(null) == C(0)) == C(0)) + + ASSERT((C(1) != C(null)) == C(1)) + ASSERT((C(null) != C(1)) == C(1)) + ASSERT((C(null) != C(0)) == C(1)) + + ASSERT((C(1) <> C(null)) == C(1)) + ASSERT((C(null) <> C(1)) == C(1)) + ASSERT((C(null) <> C(0)) == C(1)) + + ASSERT((C(1) ~= C(1)) == C(1)) + ASSERT((C(null) ~= C(null)) == C(1)) + ASSERT((C(null) ~= C(0)) == C(0)) + + ASSERT((C(1) ~! C(1)) == C(0)) + ASSERT((C(null) ~! C(null)) == C(0)) + ASSERT((C(null) ~! C(0)) == C(1)) + + ASSERT((C(1) & C(1)) == C(1)) + ASSERT((C(null) & C(1)) == C(0)) + ASSERT((C(1) & C(null)) == C(0)) + + ASSERT((C(1) ^ C(1)) == C(0)) + ASSERT((C(null) ^ C(5)) == C(5)) + ASSERT((C(5) ^ C(null)) == C(5)) + + ASSERT((C(1) | C(1)) == C(1)) + ASSERT((C(null) | C(1)) == C(1)) + ASSERT((C(1) | C(null)) == C(1)) + + ASSERT((C(1) && C(1)) == C(1)) + ASSERT((C(null) && C(1)) == C(null)) + ASSERT((C(1) && C(null)) == C(null)) + + ASSERT((C(1) || C(1)) == C(1)) + ASSERT((C(null) || C(1)) == C(1)) + ASSERT((C(1) || C(null)) == C(1)) + diff --git a/Content.Tests/DMProject/Tests/Operators/valid_and_null_assign.dm b/Content.Tests/DMProject/Tests/Operators/valid_and_null_assign.dm new file mode 100644 index 0000000000..d71ebb8eb5 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Operators/valid_and_null_assign.dm @@ -0,0 +1,128 @@ +//simple test of all basic assignment operators with valid and C(null) arguments +//We can't be having const folding in here +#define C(X) pick(X) +/proc/RunTest() + var/a = C(1) + a += C(1) + ASSERT(a == C(2)) + a = null + a += C(1) + ASSERT(a == C(1)) + a = C(1) + a += C(null) + ASSERT(a == C(1)) + + a = C(1) + a -= C(1) + ASSERT(a == C(0)) + a = null + a -= C(1) + ASSERT(a == C(-1)) + a = C(1) + a -= C(null) + ASSERT(a == C(1)) + + a = C(2) + a *= C(3) + ASSERT(a == C(6)) + a = null + a *= C(2) + ASSERT(a == C(0)) + a = C(2) + a *= C(null) + ASSERT(a == C(0)) + + a = C(4) + a /= C(2) + ASSERT(a == C(2)) + a = null + a /= C(2) + ASSERT(a == C(0)) + //a = C(2) + //a /= C(null) //Undefined operation error in BYOND + //ASSERT(a == C(2)) + + a = C(4) + a %= C(3) + ASSERT(a == C(1)) + a = null + a %= C(3) + ASSERT(a == C(0)) + //a = C(4) + //a %= C(null) //divide by zero error in byond + //ASSERT(a == C(4)) + + a = C(1) + a &= C(1) + ASSERT(a == C(1)) + a = null + a &= C(1) + ASSERT(a == C(0)) + a = C(1) + a &= C(null) + ASSERT(a == C(0)) + + a = C(1) + a |= C(1) + ASSERT(a == C(1)) + a = null + a |= C(1) + ASSERT(a == C(1)) + a = C(1) + a |= C(null) + ASSERT(a == C(1)) + + a = C(1) + a ^= C(1) + ASSERT(a == C(0)) + a = null + a ^= C(1) + ASSERT(a == C(1)) + a = C(1) + a ^= C(null) + ASSERT(a == C(1)) + + a = C(1) + a &&= C(1) + ASSERT(a == C(1)) + a = null + a &&= C(1) + ASSERT(a == C(null)) + a = C(1) + a &&= C(null) + ASSERT(a == C(null)) + + a = C(1) + a ||= C(1) + ASSERT(a == C(1)) + a = null + a ||= C(1) + ASSERT(a == C(1)) + a = C(1) + a ||= C(null) + ASSERT(a == C(1)) + + a = C(1) + a <<= C(1) + ASSERT(a == C(2)) + a = null + a <<= C(1) + ASSERT(a == C(0)) + a = C(1) + a <<= C(null) + ASSERT(a == C(1)) + + a = C(1) + a >>= C(1) + ASSERT(a == C(0)) + a = null + a >>= C(1) + ASSERT(a == C(0)) + a = C(1) + a >>= C(null) + ASSERT(a == C(1)) + + a := C(5) + ASSERT(a == C(5)) + a := C(null) + ASSERT(a == null) diff --git a/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm b/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm index 2d2d609cc8..4aa1d76373 100644 --- a/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm +++ b/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm @@ -1,8 +1,10 @@ +/datum/var/name + #define DEFINE_FLOORS(_PATH, _VARS) \ - /obj/simulated/floor/_PATH{_VARS};\ - /obj/unsimulated/floor/_PATH{_VARS};\ - /obj/simulated/floor/airless/_PATH{_VARS};\ - /obj/unsimulated/floor/airless/_PATH{_VARS}; + /datum/simulated/floor/_PATH{_VARS};\ + /datum/unsimulated/floor/_PATH{_VARS};\ + /datum/simulated/floor/airless/_PATH{_VARS};\ + /datum/unsimulated/floor/airless/_PATH{_VARS}; var/list/gvars_datum_init_order = list() @@ -42,8 +44,8 @@ proc/RunTest() ASSERT(test.len == 4) InitGlobaltest2() ASSERT(test2.len == 2) - var/obj/simulated/floor/carpet/regalcarpet/C1 = new() - var/obj/simulated/floor/carpet/regalcarpet/border/C2 = new() + var/datum/simulated/floor/carpet/regalcarpet/C1 = new() + var/datum/simulated/floor/carpet/regalcarpet/border/C2 = new() ASSERT(C1.name == "regal carpet") ASSERT(C2.name == "regal carpet border") diff --git a/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm b/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm index a3d4448a56..2adb74318d 100644 --- a/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm +++ b/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm @@ -1,15 +1,17 @@ -/obj/thing_1/dodaa +/datum/var/name + +/datum/thing_1/dodaa name = "underscore 1 test" -#define NUMPATH_OBJDEF(num) /obj/thing_##num/name = #num +#define NUMPATH_OBJDEF(num) /datum/thing_##num/name = #num NUMPATH_OBJDEF(4) NUMPATH_OBJDEF(stuff) /proc/RunTest() - var/obj/thing_1/dodaa/D = new + var/datum/thing_1/dodaa/D = new ASSERT(D.name == "underscore 1 test") - var/obj/thing_4/T = new + var/datum/thing_4/T = new ASSERT(T.name == "4") - var/obj/thing_stuff/Y = new + var/datum/thing_stuff/Y = new ASSERT(Y.name == "stuff") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Procs/Arglist/initial.dm b/Content.Tests/DMProject/Tests/Procs/Arglist/initial.dm new file mode 100644 index 0000000000..716becd5d4 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Procs/Arglist/initial.dm @@ -0,0 +1,6 @@ + +/proc/_initial(...) + ASSERT(initial(arglist(args))[1] == "foo") + +/proc/RunTest() + _initial("foo") diff --git a/Content.Tests/DMProject/Tests/Regex/regex_find_replace.dm b/Content.Tests/DMProject/Tests/Regex/regex_find_replace.dm new file mode 100644 index 0000000000..d5463d7498 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Regex/regex_find_replace.dm @@ -0,0 +1,20 @@ +var/models = "/obj/cable/brown{\n\ticon_state = \"2-8\"\n\t},\n/obj/cable/brown{\n\ticon_state = \"4-8\"\n\t},\n/turf/simulated/floor/orangeblack,\n/area/station/devzone" +var/result_match = "/obj/cable/brown{\n\ticon_state = \"1\"\n\t},\n/obj/cable/brown{\n\ticon_state = \"2\"\n\t},\n/turf/simulated/floor/orangeblack,\n/area/station/devzone" + +/proc/RunTest() + var/list/originalStrings = list() + var/regex/noStrings = regex(@{"(["])(?:(?=(\\?))\2(.|\n))*?\1"}) + var/stringIndex = 1 + var/found + do + found = noStrings.Find(models, noStrings.next) + if(found) + var indexText = {""[stringIndex]""} + stringIndex++ + var match = copytext(noStrings.match, 2, -1) // Strip quotes + models = noStrings.Replace(models, indexText, found) + originalStrings[indexText] = (match) + while(found) + ASSERT(models == result_match) + ASSERT(originalStrings ~= list("\"1\"" = "2-8", "\"2\"" = "4-8")) + ASSERT(stringIndex == 3) \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Savefile/ExportText.dm b/Content.Tests/DMProject/Tests/Savefile/ExportText.dm index b99c840a7c..7ecb792a14 100644 --- a/Content.Tests/DMProject/Tests/Savefile/ExportText.dm +++ b/Content.Tests/DMProject/Tests/Savefile/ExportText.dm @@ -1,8 +1,9 @@ -/obj/savetest - var/obj/savetest/recurse = null +/datum/savetest + var/name + var/datum/savetest/recurse = null /proc/RunTest() - var/obj/savetest/O = new() //create a test object + var/datum/savetest/O = new() //create a test object O.name = "test" //O.recurse = O //TODO diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_global_var.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_global_var.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_global_var.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_global_var.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_list_idx.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_list_idx.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_list_idx.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_list_idx.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_null.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_null.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_null.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_null.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_parent_type.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_parent_type.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_parent_type.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_parent_type.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_path.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_path.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_path.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_path.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_proc_args.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_proc_args.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_proc_args.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_proc_args.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_type.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_type.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_type.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_type.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/initial/initial_vars_index.dm b/Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_vars_index.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/initial/initial_vars_index.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/initial/initial_vars_index.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/issaved/issaved_list_idx.dm b/Content.Tests/DMProject/Tests/SpecialProcs/issaved/issaved_list_idx.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/issaved/issaved_list_idx.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/issaved/issaved_list_idx.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/issaved/issaved_vars_index.dm b/Content.Tests/DMProject/Tests/SpecialProcs/issaved/issaved_vars_index.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/issaved/issaved_vars_index.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/issaved/issaved_vars_index.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/locate/tag_deleted_obj.dm b/Content.Tests/DMProject/Tests/SpecialProcs/locate/tag_deleted_obj.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/locate/tag_deleted_obj.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/locate/tag_deleted_obj.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/locate/tag_empty_string.dm b/Content.Tests/DMProject/Tests/SpecialProcs/locate/tag_empty_string.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/locate/tag_empty_string.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/locate/tag_empty_string.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/locate/tag_nulled.dm b/Content.Tests/DMProject/Tests/SpecialProcs/locate/tag_nulled.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/locate/tag_nulled.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/locate/tag_nulled.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/nameof/nameof.dm b/Content.Tests/DMProject/Tests/SpecialProcs/nameof/nameof.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/nameof/nameof.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/nameof/nameof.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/nameof/nameof_error.dm b/Content.Tests/DMProject/Tests/SpecialProcs/nameof/nameof_error.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/nameof/nameof_error.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/nameof/nameof_error.dm diff --git a/Content.Tests/DMProject/Tests/Special Procs/new/newlist.dm b/Content.Tests/DMProject/Tests/SpecialProcs/new/newlist.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Special Procs/new/newlist.dm rename to Content.Tests/DMProject/Tests/SpecialProcs/new/newlist.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/blocks_1.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/blocks_1.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/blocks_1.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/blocks_1.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/empty_blocks.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/empty_blocks.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/empty_blocks.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/empty_blocks.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/empty_proc.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/empty_proc.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/empty_proc.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/empty_proc.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/goto_inside_loop.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/goto_inside_loop.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/goto_inside_loop.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/goto_inside_loop.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/label_before_var.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/label_before_var.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/label_before_var.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/label_before_var.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labelled_loop_1.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labelled_loop_1.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labelled_loop_1.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labelled_loop_1.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labelled_loop_2.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labelled_loop_2.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labelled_loop_2.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labelled_loop_2.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labelled_loop_3.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labelled_loop_3.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labelled_loop_3.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labelled_loop_3.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_1.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_1.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_1.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_1.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_2.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_2.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_2.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_2.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_3.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_3.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_3.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_3.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_bad_jump.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_bad_jump.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_bad_jump.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_bad_jump.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_bad_link.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_bad_link.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_bad_link.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_bad_link.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_order.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_order.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_order.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_order.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_order_bad.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_order_bad.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_order_bad.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_order_bad.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/labels_scoped.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_scoped.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/labels_scoped.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/labels_scoped.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/sleep/negative.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/sleep/negative.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/sleep/negative.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/sleep/negative.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/spawn_oneliner.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/spawn_oneliner.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/spawn_oneliner.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/spawn_oneliner.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/spawn_self_test.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/spawn_self_test.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/spawn_self_test.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/spawn_self_test.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/spawn_while_enumerating.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/spawn_while_enumerating.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/spawn_while_enumerating.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/spawn_while_enumerating.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch1.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch1.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch1.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch1.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch2.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch2.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch2.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch2.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch3.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch3.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch3.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch3.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch4.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch4.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch4.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch4.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch5.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch5.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch5.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch5.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch_nested.dm b/Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch_nested.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Control Flow/try_catch_nested.dm rename to Content.Tests/DMProject/Tests/Statements/ControlFlow/try_catch_nested.dm diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/reuse_decl_const.dm b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const.dm similarity index 89% rename from Content.Tests/DMProject/Broken Tests/Statements/For/reuse_decl_const.dm rename to Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const.dm index 4b404cb05c..2f39dc4f12 100644 --- a/Content.Tests/DMProject/Broken Tests/Statements/For/reuse_decl_const.dm +++ b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const.dm @@ -1,4 +1,4 @@ -// COMPILE ERROR +// COMPILE ERROR OD0501 /datum var/const/idx = 0 diff --git a/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const2.dm b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const2.dm new file mode 100644 index 0000000000..a8144a4359 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const2.dm @@ -0,0 +1,18 @@ +// COMPILE ERROR OD0501 + +/datum + var/const/idx = 0 + var/c = 0 + proc/do_loop() + for (idx in list(1,2,3)) + c += idx + +/proc/RunTest() + var/datum/d = new + d.do_loop() + + var/const/idx = 0 + var/c = 0 + for (idx in list(1,2,3)) + c += idx + diff --git a/Content.Tests/DMProject/Tests/Statements/Var Decl/as_in_decl.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/as_in_decl.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Var Decl/as_in_decl.dm rename to Content.Tests/DMProject/Tests/Statements/VarDecl/as_in_decl.dm diff --git a/Content.Tests/DMProject/Tests/Statements/VarDecl/cant_override_final.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/cant_override_final.dm new file mode 100644 index 0000000000..1bc2612c38 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/VarDecl/cant_override_final.dm @@ -0,0 +1,7 @@ +// COMPILE ERROR OD0407 + +/datum + var/final/foo = 1 + +/datum/a + foo = 2 // Can't override a final var \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm new file mode 100644 index 0000000000..8edf10558f --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm @@ -0,0 +1,9 @@ + +//# issue 2139 + +var/foo = 2 +var bar = 3 + +/proc/RunTest() + ASSERT(foo == 2) + ASSERT(bar == 3) diff --git a/Content.Tests/DMProject/Tests/Statements/Var Decl/var_decl_expr.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/var_decl_expr.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Var Decl/var_decl_expr.dm rename to Content.Tests/DMProject/Tests/Statements/VarDecl/var_decl_expr.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Var Decl/var_decl_multi.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/var_decl_multi.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Var Decl/var_decl_multi.dm rename to Content.Tests/DMProject/Tests/Statements/VarDecl/var_decl_multi.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Var Decl/var_name_keyword1.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/var_name_keyword1.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Var Decl/var_name_keyword1.dm rename to Content.Tests/DMProject/Tests/Statements/VarDecl/var_name_keyword1.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Var Decl/var_name_keyword2.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/var_name_keyword2.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Statements/Var Decl/var_name_keyword2.dm rename to Content.Tests/DMProject/Tests/Statements/VarDecl/var_name_keyword2.dm diff --git a/Content.Tests/DMProject/Tests/Statements/extra_token_pragma.dm b/Content.Tests/DMProject/Tests/Statements/extra_token_pragma.dm new file mode 100644 index 0000000000..ce54b039e4 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/extra_token_pragma.dm @@ -0,0 +1,6 @@ +// COMPILE ERROR OD3205 +#pragma ExtraToken error + +/proc/RunTest() + if(1). + ASSERT(TRUE) diff --git a/Content.Tests/DMProject/Broken Tests/Statements/If/thing_after_if.dm b/Content.Tests/DMProject/Tests/Statements/thing_after_statement.dm similarity index 58% rename from Content.Tests/DMProject/Broken Tests/Statements/If/thing_after_if.dm rename to Content.Tests/DMProject/Tests/Statements/thing_after_statement.dm index cd943fa17f..194f1ab340 100644 --- a/Content.Tests/DMProject/Broken Tests/Statements/If/thing_after_if.dm +++ b/Content.Tests/DMProject/Tests/Statements/thing_after_statement.dm @@ -8,3 +8,7 @@ ASSERT(TRUE) else ASSERT(FALSE) + for(var/i in 1 to 1): + ASSERT(TRUE) + for(var/i in 1 to 1). + ASSERT(TRUE) diff --git a/Content.Tests/DMProject/Broken Tests/Stdlib/Array/arg.dm b/Content.Tests/DMProject/Tests/Stdlib/Array/arg.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Stdlib/Array/arg.dm rename to Content.Tests/DMProject/Tests/Stdlib/Array/arg.dm diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm index 385d9c0c88..1322bff03a 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm @@ -1,8 +1,8 @@ -/obj/blombo - name = "Blombo" +/datum/blombo + var/name = "Blombo" /proc/RunTest() - var/obj/blombo/b = new + var/datum/blombo/b = new var/result_text = "Nobody likes [b]!" ASSERT(result_text == "Nobody likes Blombo!") diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm index 54cb329034..20bea3f509 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm @@ -1,14 +1,23 @@ +/datum/test/var/name -/obj/blombo - name = "Blombo" - gender = FEMALE - -/obj/blorpo - name = "Blorpo" - gender = MALE +/datum/test/test1 + name = "" +/datum/test/test2 + name = " " // 3 spaces +/datum/test/test3 + name = "\t" /proc/RunTest() - var/obj/blombo/b = new - var/obj/blorpo/b2 = new - var/result_text = "[b]? Nobody likes \him. \He is awful! Unlike [b2]. \He is pretty cool!" - ASSERT(result_text == "Blombo? Nobody likes her. She is awful! Unlike Blorpo. He is pretty cool!") + var/list/correct = list( + "/datum/test/test1: ", + "/datum/test/test2: ", + "/datum/test/test3: \t" + ) + var/i = 1 + for (var/T in typesof(/datum/test)) + if(T == /datum/test) + continue + var/datum/test/D = new T() + var/true_text = correct[i] + ASSERT(true_text == "[T]: \the [D]") + ++i diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm index 5688dd1030..e960741bb0 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm @@ -1,21 +1,15 @@ -/obj/test1 - name = "" -/obj/test2 - name = " " // 3 spaces -/obj/test3 - name = "\t" + /proc/RunTest() - var/list/correct = list( - "/obj/test1: ", - "/obj/test2: ", - "/obj/test3: \t" - ) - var/i = 1 - for (var/T in typesof(/obj)) - if(T == /obj) - continue - var/obj/O = new T() - var/true_text = correct[i] - ASSERT(true_text == "[T]: \the [O]") - ++i + var/text = "["1"]\s" + ASSERT(text == "1s") + text = "[0]\s" + ASSERT(text == "0s") + text = "[null]\s" + ASSERT(text == "s") + text = "[1]\s" + ASSERT(text == "1") + text = "[1.00000001]\s" + ASSERT(text == "1") + text = "[1.0001]\s" + ASSERT(text == "1.0001s") diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm index fc768b0371..d2b0eeb0cd 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm @@ -1,11 +1,30 @@ /proc/RunTest() - var/atom/O = new() - O.name = "foo" - O.gender = FEMALE - var/atom/O2 = new - O2.name = "foob" - O2.gender = MALE - var/text = "[O2], \ref[O], \his" - ASSERT(findtextEx(text,", his") != 0) + var/text = "[0]\th" + ASSERT(text == "0th") + text = "[1]\th" + ASSERT(text == "1st") + text = "[2]\th" + ASSERT(text == "2nd") + text = "[3]\th" + ASSERT(text == "3rd") + text = "[4]\th" + ASSERT(text == "4th") + text = "[-1]\th" + ASSERT(text == "-1th") + text = "[4.52]\th" + ASSERT(text == "4th") + text = "the fitness [1.7]\th is a" + ASSERT(text == "the fitness 1st is a") + text = "the fitness [99999999]\th is a" + ASSERT(text == "the fitness 100000000th is a") + text = "[null]\th" + ASSERT(text == "0th") + var/datum/D = new + text = "[D]\th" + ASSERT(text == "0th") + var/foo = "bar" + text = "[foo]\th" + ASSERT(text == "0th") + diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm index e960741bb0..4d9b638105 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm @@ -1,15 +1,11 @@ - - -/proc/RunTest() - var/text = "["1"]\s" - ASSERT(text == "1s") - text = "[0]\s" - ASSERT(text == "0s") - text = "[null]\s" - ASSERT(text == "s") - text = "[1]\s" - ASSERT(text == "1") - text = "[1.00000001]\s" - ASSERT(text == "1") - text = "[1.0001]\s" - ASSERT(text == "1.0001s") +/proc/RunTest() + ASSERT("\roman[1.5]" == "i") + ASSERT("\roman [1.5]" == " i") + ASSERT("\Roman[1.5]" == "I") + ASSERT("\Roman [1.5]" == " I") + ASSERT("\roman shitposts [1]" == " shitposts i") + ASSERT("\roman shitposts [1] \the [2] [3]\s" == " shitposts i 3s") + ASSERT("\roman[1.#INF]" == "∞") + ASSERT("\roman[-1.#INF]" == "-∞") + ASSERT("\roman [-1.#INF]" == " -∞") + ASSERT("\roman[1.#IND]" == "�") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm index d2b0eeb0cd..9ff619a52a 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm @@ -1,30 +1,39 @@ +/datum/thing + var/name = "thing" +/datum/Thing + var/name = "Thing" + +/datum/proper_thing + var/name = "\proper thing" + +/datum/plural_things + var/name = "things" + var/gender = PLURAL /proc/RunTest() - var/text = "[0]\th" - ASSERT(text == "0th") - text = "[1]\th" - ASSERT(text == "1st") - text = "[2]\th" - ASSERT(text == "2nd") - text = "[3]\th" - ASSERT(text == "3rd") - text = "[4]\th" - ASSERT(text == "4th") - text = "[-1]\th" - ASSERT(text == "-1th") - text = "[4.52]\th" - ASSERT(text == "4th") - text = "the fitness [1.7]\th is a" - ASSERT(text == "the fitness 1st is a") - text = "the fitness [99999999]\th is a" - ASSERT(text == "the fitness 100000000th is a") - text = "[null]\th" - ASSERT(text == "0th") - var/datum/D = new - text = "[D]\th" - ASSERT(text == "0th") - var/foo = "bar" - text = "[foo]\th" - ASSERT(text == "0th") + // Lowercase \a on datums + ASSERT("\a [new /datum/thing]" == "a thing") + ASSERT("\a [new /datum/Thing]" == "Thing") + ASSERT("\a [new /datum/proper_thing]" == "thing") + ASSERT("\a [new /datum/plural_things]" == "some things") + + // Uppercase \A on datums + ASSERT("\A [new /datum/thing]" == "A thing") + ASSERT("\A [new /datum/Thing]" == "Thing") + ASSERT("\A [new /datum/proper_thing]" == "thing") + ASSERT("\A [new /datum/plural_things]" == "Some things") + + // Lowercase \a on strings + ASSERT("\a ["thing"]" == "a thing") + ASSERT("\a ["Thing"]" == "Thing") + ASSERT("\a ["\proper thing"]" == "thing") + + // Uppercase \A on strings + ASSERT("\A ["thing"]" == "A thing") + ASSERT("\A ["Thing"]" == "Thing") + ASSERT("\A ["\proper thing"]" == "thing") + // Invalid \a + ASSERT("\a [123]" == "") + ASSERT("\A [123]" == "") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm deleted file mode 100644 index 4d9b638105..0000000000 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm +++ /dev/null @@ -1,11 +0,0 @@ -/proc/RunTest() - ASSERT("\roman[1.5]" == "i") - ASSERT("\roman [1.5]" == " i") - ASSERT("\Roman[1.5]" == "I") - ASSERT("\Roman [1.5]" == " I") - ASSERT("\roman shitposts [1]" == " shitposts i") - ASSERT("\roman shitposts [1] \the [2] [3]\s" == " shitposts i 3s") - ASSERT("\roman[1.#INF]" == "∞") - ASSERT("\roman[-1.#INF]" == "-∞") - ASSERT("\roman [-1.#INF]" == " -∞") - ASSERT("\roman[1.#IND]" == "�") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm deleted file mode 100644 index 9ff619a52a..0000000000 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm +++ /dev/null @@ -1,39 +0,0 @@ -/datum/thing - var/name = "thing" - -/datum/Thing - var/name = "Thing" - -/datum/proper_thing - var/name = "\proper thing" - -/datum/plural_things - var/name = "things" - var/gender = PLURAL - -/proc/RunTest() - // Lowercase \a on datums - ASSERT("\a [new /datum/thing]" == "a thing") - ASSERT("\a [new /datum/Thing]" == "Thing") - ASSERT("\a [new /datum/proper_thing]" == "thing") - ASSERT("\a [new /datum/plural_things]" == "some things") - - // Uppercase \A on datums - ASSERT("\A [new /datum/thing]" == "A thing") - ASSERT("\A [new /datum/Thing]" == "Thing") - ASSERT("\A [new /datum/proper_thing]" == "thing") - ASSERT("\A [new /datum/plural_things]" == "Some things") - - // Lowercase \a on strings - ASSERT("\a ["thing"]" == "a thing") - ASSERT("\a ["Thing"]" == "Thing") - ASSERT("\a ["\proper thing"]" == "thing") - - // Uppercase \A on strings - ASSERT("\A ["thing"]" == "A thing") - ASSERT("\A ["Thing"]" == "Thing") - ASSERT("\A ["\proper thing"]" == "thing") - - // Invalid \a - ASSERT("\a [123]" == "") - ASSERT("\A [123]" == "") \ No newline at end of file diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero2.dm b/Content.Tests/DMProject/Tests/Tree/Const/Div_Zero/div_zero2.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero2.dm rename to Content.Tests/DMProject/Tests/Tree/Const/Div_Zero/div_zero2.dm diff --git a/Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero5.dm b/Content.Tests/DMProject/Tests/Tree/Const/Div_Zero/div_zero5.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Tree/Const/Div_Zero/div_zero5.dm rename to Content.Tests/DMProject/Tests/Tree/Const/Div_Zero/div_zero5.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/assign_global_to_global.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/assign_global_to_global.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/assign_global_to_global.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/assign_global_to_global.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope1.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope1.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope1.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope1.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope3.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope3.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope3.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope3.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope4.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope4.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope4.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope4.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope_in_assoc_list.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope_in_assoc_list.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/scope_in_assoc_list.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/scope_in_assoc_list.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/static_list.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/static_list.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/static_list.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/static_list.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/static_proc.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/static_proc.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/static_proc.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/static_proc.dm diff --git a/Content.Tests/DMProject/Tests/Tree/Global/Static Scope/static_vars.dm b/Content.Tests/DMProject/Tests/Tree/Global/StaticScope/static_vars.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Tree/Global/Static Scope/static_vars.dm rename to Content.Tests/DMProject/Tests/Tree/Global/StaticScope/static_vars.dm diff --git a/Content.Tests/DMProject/Tests/Type Inference/infer_override.dm b/Content.Tests/DMProject/Tests/TypeInference/infer_override.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Type Inference/infer_override.dm rename to Content.Tests/DMProject/Tests/TypeInference/infer_override.dm diff --git a/Content.Tests/DMProject/Tests/Type Inference/nestedinference.dm b/Content.Tests/DMProject/Tests/TypeInference/nestedinference.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Type Inference/nestedinference.dm rename to Content.Tests/DMProject/Tests/TypeInference/nestedinference.dm diff --git a/Content.Tests/DMProject/Tests/Type Inference/pointless_upcasting.dm b/Content.Tests/DMProject/Tests/TypeInference/pointless_upcasting.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Type Inference/pointless_upcasting.dm rename to Content.Tests/DMProject/Tests/TypeInference/pointless_upcasting.dm diff --git a/Content.Tests/DMProject/Tests/Type Inference/ternary_op.dm b/Content.Tests/DMProject/Tests/TypeInference/ternary_op.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Type Inference/ternary_op.dm rename to Content.Tests/DMProject/Tests/TypeInference/ternary_op.dm diff --git a/Content.Tests/DMProject/Tests/Type Inference/type_var_clash.dm b/Content.Tests/DMProject/Tests/TypeInference/type_var_clash.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Type Inference/type_var_clash.dm rename to Content.Tests/DMProject/Tests/TypeInference/type_var_clash.dm diff --git a/Content.Tests/DMProject/Tests/Type Inference/unknown_type.dm b/Content.Tests/DMProject/Tests/TypeInference/unknown_type.dm similarity index 100% rename from Content.Tests/DMProject/Tests/Type Inference/unknown_type.dm rename to Content.Tests/DMProject/Tests/TypeInference/unknown_type.dm diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 684534e7e2..45de24aafc 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -33,7 +33,7 @@ public enum DMTestFlags { NoReturn = 16, // Shouldn't return (aka stopped by a stack-overflow or runtimes) } - private void OnException(object? sender, Exception exception) => TestContext.WriteLine(exception); + private void OnException(object? sender, Exception exception) => TestContext.Out.WriteLine(exception); [OneTimeSetUp] public void OneTimeSetup() { @@ -63,7 +63,7 @@ private static void Cleanup(string? compiledFile) { [Test, TestCaseSource(nameof(GetTests))] public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) { string initialDirectory = Directory.GetCurrentDirectory(); - TestContext.WriteLine($"--- TEST {sourceFile} | Flags: {testFlags}"); + TestContext.Out.WriteLine($"--- TEST {sourceFile} | Flags: {testFlags}"); try { var dmCompiler = new DMCompiler.DMCompiler(); var compiledFile = Compile(dmCompiler, Path.Join(initialDirectory, TestsDirectory, sourceFile)); @@ -73,12 +73,13 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) { Assert.That(compiledFile, Is.Null, "Expected an error during DM compilation"); Cleanup(compiledFile); - TestContext.WriteLine($"--- PASS {sourceFile}"); + TestContext.Out.WriteLine($"--- PASS {sourceFile}"); return; } Assert.That(compiledFile is not null && File.Exists(compiledFile), "Failed to compile DM source file"); Assert.That(_dreamMan.LoadJson(compiledFile), $"Failed to load {compiledFile}"); + _dreamMan.LastDMException = null; // Nuke any exception from a prior test _dreamMan.StartWorld(); (bool successfulRun, DreamValue? returned, Exception? exception) = RunTest(); @@ -106,7 +107,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) { GC.Collect(); _dreamMan.Update(); Cleanup(compiledFile); - TestContext.WriteLine($"--- PASS {sourceFile}"); + TestContext.Out.WriteLine($"--- PASS {sourceFile}"); } finally { // Restore the original CurrentDirectory, since loading a compiled JSON changes it. Directory.SetCurrentDirectory(initialDirectory); @@ -114,8 +115,6 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) { } private (bool Success, DreamValue? Returned, Exception? except) RunTest() { - var prev = _dreamMan.LastDMException; - DreamValue? retValue = null; Task callTask = null!; @@ -143,7 +142,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) { } } - bool retSuccess = _dreamMan.LastDMException == prev; // Works because "null == null" is true in this language. + bool retSuccess = _dreamMan.LastDMException == null; // Works because "null == null" is true in this language. return (retSuccess, retValue, _dreamMan.LastDMException); } diff --git a/Content.Tests/DummyDreamMapManager.cs b/Content.Tests/DummyDreamMapManager.cs index 12bece3217..76b812bee5 100644 --- a/Content.Tests/DummyDreamMapManager.cs +++ b/Content.Tests/DummyDreamMapManager.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using DMCompiler.Json; -using OpenDreamRuntime; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; @@ -26,9 +26,9 @@ public void InitializeAtoms(List? maps) { } public void SetTurf(DreamObjectTurf turf, DreamObjectDefinition type, DreamProcArguments creationArguments) { } - public void SetTurfAppearance(DreamObjectTurf turf, IconAppearance appearance) { } + public void SetTurfAppearance(DreamObjectTurf turf, MutableAppearance appearance) { } - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance) { } + public void SetAreaAppearance(DreamObjectArea area, MutableAppearance appearance) { } public void SetArea(DreamObjectTurf turf, DreamObjectArea area) { } @@ -54,4 +54,8 @@ public void SetWorldSize(Vector2i size) { } public EntityUid GetZLevelEntity(int z) { return EntityUid.Invalid; } + + public IEnumerable CalculateSteps((int X, int Y, int Z) loc, (int X, int Y, int Z) dest, int distance) { + yield break; + } } diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index 3d5d36dfaa..d583165388 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -132,7 +132,8 @@ public enum DreamProcOpcode : byte { [OpcodeMetadata(-1)] Power = 0x42, //0x43, - //0x44 + [OpcodeMetadata(-2)] + Link = 0x44, [OpcodeMetadata(-3, OpcodeArgType.TypeId)] Prompt = 0x45, [OpcodeMetadata(-3)] @@ -295,6 +296,8 @@ public enum DreamProcOpcode : byte { ReturnReferenceValue = 0x97, [OpcodeMetadata(0, OpcodeArgType.Float)] ReturnFloat = 0x98, + [OpcodeMetadata(1, OpcodeArgType.Reference, OpcodeArgType.String)] + IndexRefWithString = 0x99, } // ReSharper restore MissingBlankLines diff --git a/DMCompiler/Compiler/CompilerError.cs b/DMCompiler/Compiler/CompilerError.cs index f23dc9a2df..dad0930c91 100644 --- a/DMCompiler/Compiler/CompilerError.cs +++ b/DMCompiler/Compiler/CompilerError.cs @@ -22,6 +22,7 @@ public enum WarningCode { ItemDoesntExist = 404, DanglingOverride = 405, StaticOverride = 406, + FinalOverride = 407, // ReSharper disable once InconsistentNaming IAmATeaPot = 418, // TODO: Implement the HTCPC protocol for OD HardConstContext = 500, @@ -74,6 +75,7 @@ public enum WarningCode { AssignmentInConditional = 3202, PickWeightedSyntax = 3203, AmbiguousInOrder = 3204, + ExtraToken = 3205, RuntimeSearchOperator = 3300, // 4000 - 4999 are reserved for runtime configuration. (TODO: Runtime doesn't know about configs yet!) diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs b/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs index 3d556981fa..c025bcf7e6 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs @@ -7,6 +7,15 @@ namespace DMCompiler.Compiler.DM.AST; /// public abstract class DMASTStatement(Location location) : DMASTNode(location); +/// +/// Used when there was an error parsing a statement +/// +/// Emit an error code before creating! +public sealed class DMASTInvalidStatement(Location location) : DMASTStatement(location); + +/// Lone semicolon, statement containing nothing +public sealed class DMASTNullStatement(Location location) : DMASTStatement(location); + public sealed class DMASTObjectDefinition(Location location, DreamPath path, DMASTBlockInner? innerBlock) : DMASTStatement(location) { /// Unlike other Path variables stored by AST nodes, this path is guaranteed to be the real, absolute path of this object definition block.
@@ -68,6 +77,7 @@ public sealed class DMASTObjectVarDefinition( public bool IsStatic => _varDecl.IsStatic; public bool IsConst => _varDecl.IsConst; + public bool IsFinal => _varDecl.IsFinal; public bool IsTmp => _varDecl.IsTmp; public readonly DMComplexValueType ValType = valType; diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs b/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs index c255f68fa2..37067a9929 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs @@ -191,6 +191,14 @@ public sealed class DMASTProcStatementOutputControl( public DMASTExpression Control = control; } +public sealed class DMASTProcStatementLink( + Location location, + DMASTExpression receiver, + DMASTExpression url) : DMASTProcStatement(location) { + public readonly DMASTExpression Receiver = receiver; + public readonly DMASTExpression Url = url; +} + public sealed class DMASTProcStatementFtp( Location location, DMASTExpression receiver, diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 00c566fd16..c0ecacbba4 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -203,6 +203,11 @@ public DMASTFile File() { protected DMASTStatement? Statement() { var loc = CurrentLoc; + if (Current().Type == TokenType.DM_Semicolon) { // A lone semicolon creates a "null statement" (like C) + // Note that we do not consume the semicolon here + return new DMASTNullStatement(loc); + } + DMASTPath? path = Path(); if (path is null) return null; @@ -268,17 +273,36 @@ public DMASTFile File() { return new DMASTProcDefinition(loc, CurrentPath, parameters.ToArray(), procBlock, types); } - //Object definition - if (Block() is { } block) { - Compiler.VerbosePrint($"Parsed object {CurrentPath}"); - return new DMASTObjectDefinition(loc, CurrentPath, block); - } - //Var definition(s) if (CurrentPath.FindElement("var") != -1) { + bool isIndented = false; DreamPath varPath = CurrentPath; List varDefinitions = new(); + var possibleNewline = Current(); + if (Newline()) { + if (Check(TokenType.DM_Indent)) { + isIndented = true; + DMASTPath? newVarPath = Path(); + if (newVarPath == null) { + Emit(WarningCode.InvalidVarDefinition, "Expected a var definition"); + return new DMASTInvalidStatement(CurrentLoc); + } + + varPath = CurrentPath.AddToPath(newVarPath.Path.PathString); + } else { + ReuseToken(possibleNewline); + } + } else if (Current().Type == TokenType.DM_Identifier) { // "var foo" instead of "var/foo" + DMASTPath? newVarPath = Path(); + if (newVarPath == null) { + Emit(WarningCode.InvalidVarDefinition, "Expected a var definition"); + return new DMASTInvalidStatement(CurrentLoc); + } + + varPath = CurrentPath.AddToPath(newVarPath.Path.PathString); + } + while (true) { Whitespace(); @@ -304,24 +328,26 @@ public DMASTFile File() { var varDef = new DMASTObjectVarDefinition(loc, varPath, value, valType); varDefinitions.Add(varDef); - if (Check(TokenType.DM_Comma)) { + if (Check(TokenType.DM_Comma) || (isIndented && Newline())) { Whitespace(); DMASTPath? newVarPath = Path(); if (newVarPath == null) { Emit(WarningCode.InvalidVarDefinition, "Expected a var definition"); break; - } else if (newVarPath.Path.Elements.Length > 1) { // TODO: This is valid DM - Emit(WarningCode.BadToken, newVarPath.Location, "Invalid var name"); - break; } - varPath = CurrentPath.AddToPath("../" + newVarPath.Path.PathString); + varPath = CurrentPath.AddToPath( + isIndented ? newVarPath.Path.PathString + : "../" + newVarPath.Path.PathString); } else { break; } } + if (isIndented) + Consume(TokenType.DM_Dedent, "Expected end of var block"); + return (varDefinitions.Count == 1) ? varDefinitions[0] : new DMASTMultipleObjectVarDefinitions(loc, varDefinitions.ToArray()); @@ -336,6 +362,12 @@ public DMASTFile File() { return new DMASTObjectVarOverride(loc, CurrentPath, value); } + //Object definition + if (Block() is { } block) { + Compiler.VerbosePrint($"Parsed object {CurrentPath}"); + return new DMASTObjectDefinition(loc, CurrentPath, block); + } + //Empty object definition Compiler.VerbosePrint($"Parsed object {CurrentPath} - empty"); return new DMASTObjectDefinition(loc, CurrentPath, null); @@ -517,7 +549,7 @@ public DMASTFile File() { Newline(); Consume(TokenType.DM_RightCurlyBracket, "Expected '}'"); - return new DMASTBlockInner(loc, blockInner.ToArray()); + return new DMASTBlockInner(loc, blockInner?.ToArray() ?? []); } return null; @@ -725,6 +757,16 @@ public DMASTFile File() { DMASTExpression control = procCall.Parameters[1].Value; return new DMASTProcStatementOutputControl(loc, leftShift.LHS, msg, control); } + case "link": { + if (procCall.Parameters.Length != 1) { + Emit(WarningCode.InvalidArgumentCount, procCall.Location, + "link() requires 1 parameter"); + return new DMASTInvalidProcStatement(procCall.Location); + } + + DMASTExpression url = procCall.Parameters[0].Value; + return new DMASTProcStatementLink(loc, leftShift.LHS, url); + } case "ftp": { if (procCall.Parameters.Length is not 1 and not 2) { Emit(WarningCode.InvalidArgumentCount, procCall.Location, @@ -1125,8 +1167,10 @@ private DMASTProcStatementIf If() { BracketWhitespace(); ConsumeRightParenthesis(); - Whitespace(); - Check(TokenType.DM_Colon); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + Whitespace(); DMASTProcStatement? procStatement = ProcStatement(); @@ -1165,6 +1209,10 @@ private DMASTProcStatement For() { Whitespace(); if (Check(TokenType.DM_RightParenthesis)) { + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementInfLoop(loc, GetForBody(loc)); } @@ -1185,6 +1233,10 @@ private DMASTProcStatement For() { if (expr1 is DMASTAssign assign) { ExpressionTo(out var endRange, out var step); Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after to expression"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.LHS, assign.RHS, endRange, step), null, null, dmTypes, GetForBody(loc)); } else { Emit(WarningCode.BadExpression, "Expected = before to in for"); @@ -1197,15 +1249,27 @@ private DMASTProcStatement For() { DMASTExpression? listExpr = Expression(); Whitespace(); Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody(loc)); } if (!Check(ForSeparatorTypes)) { Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 1"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc)); } if (Check(TokenType.DM_RightParenthesis)) { + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc)); } @@ -1221,10 +1285,18 @@ private DMASTProcStatement For() { if (!Check(ForSeparatorTypes)) { Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc)); } if (Check(TokenType.DM_RightParenthesis)) { + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc)); } @@ -1239,6 +1311,10 @@ private DMASTProcStatement For() { } Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 3"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody(loc)); DMASTProcBlockInner GetForBody(Location forLocation) { @@ -1690,7 +1766,9 @@ private List DefinitionParameters(out bool wasIndeterm var loc = Current().Location; Whitespace(); - DMASTExpression? value = PathArray(ref path.Path); + PathArray(ref path.Path); + + DMASTExpression? value = null; DMASTExpression? possibleValues = null; if (Check(TokenType.DM_DoubleSquareBracketEquals)) { diff --git a/DMCompiler/Compiler/DM/DMPath.cs b/DMCompiler/Compiler/DM/DMPath.cs index 752e050f54..d5e2265dd7 100644 --- a/DMCompiler/Compiler/DM/DMPath.cs +++ b/DMCompiler/Compiler/DM/DMPath.cs @@ -1,146 +1,157 @@ -namespace DMCompiler.Compiler.DM { - public abstract class VarDeclInfo { - public DreamPath? TypePath; - public string VarName; +namespace DMCompiler.Compiler.DM; - ///Marks whether the variable is /global/ or /static/. (These are seemingly interchangeable keywords in DM and so are under this same boolean) - public bool IsStatic; +internal abstract class VarDeclInfo { + public DreamPath? TypePath; + public string VarName; - public bool IsConst; - public bool IsList; - } + ///Marks whether the variable is /global/ or /static/. (These are seemingly interchangeable keywords in DM and so are under this same boolean) + public bool IsStatic; - public sealed class ProcVarDeclInfo : VarDeclInfo - { - public ProcVarDeclInfo(DreamPath path) - { - string[] elements = path.Elements; - var readIdx = 0; - List currentPath = new(); - if (elements[readIdx] == "var") - { - readIdx++; - } - while (readIdx < elements.Length - 1) - { - var elem = elements[readIdx]; - if (elem == "static" || elem == "global") - { + public bool IsConst; + public bool IsFinal; + public bool IsList; +} + +internal sealed class ProcVarDeclInfo : VarDeclInfo { + public ProcVarDeclInfo(DreamPath path) { + string[] elements = path.Elements; + var readIdx = 0; + List currentPath = new(); + if (elements[readIdx] == "var") { + readIdx++; + } + + while (readIdx < elements.Length - 1) { + var elem = elements[readIdx]; + switch (elem) { + case "static": + case "global": IsStatic = true; - } - else if (elem == "const") - { + break; + case "const": IsConst = true; - } - else if (elem == "list") - { + break; + case "final": + IsFinal = true; + break; + case "list": IsList = true; - } - else - { + break; + default: currentPath.Add(elem); - } - readIdx += 1; - } - if (currentPath.Count > 0) - { - TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); - } - else - { - TypePath = null; + break; } - VarName = elements[elements.Length - 1]; + + readIdx += 1; } + + if (currentPath.Count > 0) { + TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + } else { + TypePath = null; + } + + VarName = elements[^1]; } +} - public sealed class ObjVarDeclInfo : VarDeclInfo - { - public DreamPath ObjectPath; - public bool IsTmp; - - public ObjVarDeclInfo(DreamPath path) - { - string[] elements = path.Elements; - var readIdx = 0; - List currentPath = new(); - while (readIdx < elements.Length && elements[readIdx] != "var") - { - currentPath.Add(elements[readIdx]); - readIdx += 1; - } - ObjectPath = new DreamPath(path.Type, currentPath.ToArray()); - if (ObjectPath.Elements.Length == 0) // Variables declared in the root scope are inherently static. - { - IsStatic = true; - } - currentPath.Clear(); +internal sealed class ObjVarDeclInfo : VarDeclInfo { + public DreamPath ObjectPath; + public readonly bool IsTmp; + + public ObjVarDeclInfo(DreamPath path) { + string[] elements = path.Elements; + var readIdx = 0; + List currentPath = new(); + while (readIdx < elements.Length && elements[readIdx] != "var") { + currentPath.Add(elements[readIdx]); readIdx += 1; - while (readIdx < elements.Length - 1) - { - var elem = elements[readIdx]; - if (elem == "static" || elem == "global") - { + } + + ObjectPath = new DreamPath(path.Type, currentPath.ToArray()); + if (ObjectPath.Elements.Length == 0) { // Variables declared in the root scope are inherently static. + IsStatic = true; + } + + currentPath.Clear(); + readIdx += 1; + while (readIdx < elements.Length - 1) { + var elem = elements[readIdx]; + switch (elem) { + case "static": + case "global": IsStatic = true; - } - else if (elem == "const") - { + break; + case "const": IsConst = true; - } - else if (elem == "list") - { + break; + case "final": + IsFinal = true; + break; + case "list": IsList = true; - } - else if (elem == "tmp") - { + break; + case "tmp": IsTmp = true; - } - else - { + break; + default: currentPath.Add(elem); - } - readIdx += 1; - } - if (currentPath.Count > 0) - { - TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + break; } - else - { - TypePath = null; - } - VarName = elements[elements.Length - 1]; + + readIdx += 1; + } + + if (currentPath.Count > 0) { + TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + } else { + TypePath = null; } + + VarName = elements[^1]; } +} - public sealed class ProcParameterDeclInfo : VarDeclInfo { - public ProcParameterDeclInfo(DreamPath path) { - string[] elements = path.Elements; - var readIdx = 0; - List currentPath = new(); - if (elements[readIdx] == "var") { - readIdx++; - } - while (readIdx < elements.Length - 1) { - var elem = elements[readIdx]; - if (elem == "static" || elem == "global") { +internal sealed class ProcParameterDeclInfo : VarDeclInfo { + public ProcParameterDeclInfo(DreamPath path) { + string[] elements = path.Elements; + var readIdx = 0; + List currentPath = new(); + if (elements[readIdx] == "var") { + readIdx++; + } + + while (readIdx < elements.Length - 1) { + var elem = elements[readIdx]; + switch (elem) { + case "static": + case "global": //No effect - } else if (elem == "const") { + break; + case "const": //TODO: Parameters can be constant //If they are they can't be assigned to but still cannot be used in const-only contexts (such as switch cases) - } else if (elem == "list") { + break; + case "final": + IsFinal = true; + break; + case "list": IsList = true; - } else { + break; + default: currentPath.Add(elem); - } - readIdx += 1; + break; } - if (currentPath.Count > 0) { - TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); - } else { - TypePath = null; - } - VarName = elements[elements.Length - 1]; + + readIdx += 1; + } + + if (currentPath.Count > 0) { + TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + } else { + TypePath = null; } + + VarName = elements[^1]; } } diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs index 83a3a3a403..659cc3c03b 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs @@ -301,12 +301,31 @@ public Token NextToken(bool ignoreWhitespace = false) { } case '@': { //Raw string char delimiter = Advance(); + var startLoc = CurrentLocation(); + + // @(XYZ) where XYZ is the delimiter + string complexDelimiter = string.Empty; + if (delimiter == '(') { + Advance(); + while (GetCurrent() != ')') { + if (AtEndOfSource()) { + _compiler.Emit(WarningCode.BadExpression, startLoc, + "Unterminated string delimiter"); + break; + } + + complexDelimiter += GetCurrent(); + Advance(); + } + } TokenTextBuilder.Clear(); TokenTextBuilder.Append('@'); TokenTextBuilder.Append(delimiter); + bool isComplex = complexDelimiter != string.Empty; bool isLong = false; + c = Advance(); if (delimiter == '{') { TokenTextBuilder.Append(c); @@ -314,7 +333,33 @@ public Token NextToken(bool ignoreWhitespace = false) { if (c == '"') isLong = true; } - if (isLong) { + if (isComplex) { + TokenTextBuilder.Append(complexDelimiter); + TokenTextBuilder.Append(')'); + + // Ignore a newline immediately after @(complexDelimiter) + if (c == '\r') c = Advance(); + if (c == '\n') c = Advance(); + + var delimIdx = 0; + do { + TokenTextBuilder.Append(c); + + if (GetCurrent() == complexDelimiter[delimIdx]) delimIdx++; + else delimIdx = 0; + + if (delimIdx == complexDelimiter.Length && c == complexDelimiter[^1]) { // latter check ensures a 1-char delimiter actually matches + break; + } + + c = Advance(); + } while (!AtEndOfSource()); + + if (AtEndOfSource()) { + _compiler.Emit(WarningCode.BadExpression, startLoc, + "Unterminated string delimiter"); + } + } else if (isLong) { bool nextCharCanTerm = false; Advance(); @@ -335,6 +380,11 @@ public Token NextToken(bool ignoreWhitespace = false) { if (c == '"') nextCharCanTerm = true; } while (!AtEndOfSource()); + + if (AtEndOfSource()) { + _compiler.Emit(WarningCode.BadExpression, startLoc, + "Unterminated string delimiter"); + } } else { while (c != delimiter && !AtLineEnd() && !AtEndOfSource()) { TokenTextBuilder.Append(c); @@ -342,19 +392,31 @@ public Token NextToken(bool ignoreWhitespace = false) { } } - TokenTextBuilder.Append(c); + if (!isComplex) TokenTextBuilder.Append(c); + if (!HandleLineEnd()) Advance(); string text = TokenTextBuilder.ToString(); string value; - if (isLong) { + if (isComplex) { + // Complex strings need to strip @(complexDelimiter) and a potential final newline. Newline after @(complexDelimiter) is already handled + var trimEnd = complexDelimiter.Length; + if (TokenTextBuilder[^(complexDelimiter.Length + 1)] == '\n') trimEnd += 1; + if (TokenTextBuilder[^(complexDelimiter.Length + 2)] == '\r') trimEnd += 1; + var trimStart = 3 + complexDelimiter.Length; // 3 is from these chars: @() + value = TokenTextBuilder.ToString(trimStart, TokenTextBuilder.Length - (trimStart + trimEnd)); + } else if (isLong) { // Long strings ignore a newline immediately after the @{" and before the "} + if (TokenTextBuilder[3] == '\r') + TokenTextBuilder.Remove(3, 1); if (TokenTextBuilder[3] == '\n') TokenTextBuilder.Remove(3, 1); if (TokenTextBuilder[^3] == '\n') TokenTextBuilder.Remove(TokenTextBuilder.Length - 3, 1); + if (TokenTextBuilder[^3] == '\r') + TokenTextBuilder.Remove(TokenTextBuilder.Length - 3, 1); value = TokenTextBuilder.ToString(3, TokenTextBuilder.Length - 5); } else { @@ -639,6 +701,10 @@ private char GetCurrent() { return _current; } + private Location CurrentLocation() { + return new Location(File, _previousLine, _previousColumn, _isDMStandard); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private char Advance() { int value = _source.Read(); diff --git a/DMCompiler/DM/Builders/DMCodeTreeBuilder.cs b/DMCompiler/DM/Builders/DMCodeTreeBuilder.cs index c4b558c95d..2bf56a8b6f 100644 --- a/DMCompiler/DM/Builders/DMCodeTreeBuilder.cs +++ b/DMCompiler/DM/Builders/DMCodeTreeBuilder.cs @@ -40,6 +40,9 @@ private void ProcessStatement(DMASTStatement statement, DreamPath currentType) { } switch (statement) { + case DMASTInvalidStatement: // An error should have been emitted by whatever made this + case DMASTNullStatement: + break; case DMASTObjectDefinition objectDefinition: CodeTree.AddType(objectDefinition.Path); if (objectDefinition.InnerBlock != null) diff --git a/DMCompiler/DM/Builders/DMExpressionBuilder.cs b/DMCompiler/DM/Builders/DMExpressionBuilder.cs index 6c72adc963..9e25dc50fe 100644 --- a/DMCompiler/DM/Builders/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Builders/DMExpressionBuilder.cs @@ -75,12 +75,12 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe case DMASTDereference deref: result = BuildDereference(deref, inferredPath); break; case DMASTLocate locate: result = BuildLocate(locate, inferredPath); break; case DMASTImplicitIsType implicitIsType: result = BuildImplicitIsType(implicitIsType, inferredPath); break; - case DMASTList list: result = BuildList(list); break; + case DMASTList list: result = BuildList(list, inferredPath); break; case DMASTDimensionalList dimensionalList: result = BuildDimensionalList(dimensionalList, inferredPath); break; case DMASTNewList newList: result = BuildNewList(newList, inferredPath); break; case DMASTAddText addText: result = BuildAddText(addText, inferredPath); break; - case DMASTInput input: result = BuildInput(input); break; - case DMASTPick pick: result = BuildPick(pick); break; + case DMASTInput input: result = BuildInput(input, inferredPath); break; + case DMASTPick pick: result = BuildPick(pick, inferredPath); break; case DMASTLog log: result = BuildLog(log, inferredPath); break; case DMASTCall call: result = BuildCall(call, inferredPath); break; case DMASTExpressionWrapped wrapped: result = BuildExpression(wrapped.Value, inferredPath); break; @@ -327,10 +327,10 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe break; case DMASTGradient gradient: result = new Gradient(gradient.Location, - BuildArgumentList(gradient.Location, gradient.Parameters)); + BuildArgumentList(gradient.Location, gradient.Parameters, inferredPath)); break; case DMASTRgb rgb: - result = new Rgb(rgb.Location, BuildArgumentList(rgb.Location, rgb.Parameters)); + result = new Rgb(rgb.Location, BuildArgumentList(rgb.Location, rgb.Parameters, inferredPath)); break; case DMASTLocateCoordinates locateCoordinates: result = new LocateCoordinates(locateCoordinates.Location, @@ -435,7 +435,7 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe case DMASTVarDeclExpression varDeclExpr: var declIdentifier = new DMASTIdentifier(expression.Location, varDeclExpr.DeclPath.Path.LastElement); - result = BuildIdentifier(declIdentifier); + result = BuildIdentifier(declIdentifier, inferredPath); break; case DMASTVoid: result = BadExpression(WarningCode.BadExpression, expression.Location, "Attempt to use a void expression"); @@ -751,7 +751,7 @@ private DMExpression BuildProcCall(DMASTProcCall procCall, DreamPath? inferredPa } var target = BuildExpression((DMASTExpression)procCall.Callable, inferredPath); - var args = BuildArgumentList(procCall.Location, procCall.Parameters); + var args = BuildArgumentList(procCall.Location, procCall.Parameters, inferredPath); if (target is Proc targetProc) { // GlobalProc handles returnType itself var returnType = targetProc.GetReturnType(ctx.Type); @@ -885,7 +885,7 @@ private DMExpression BuildDereference(DMASTDereference deref, DreamPath? inferre return UnknownReference(callOperation.Location, $"Could not find a global proc named \"{callOperation.Identifier}\""); - var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters); + var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters, inferredPath); var globalProcExpr = new GlobalProc(expr.Location, globalProc); expr = new ProcCall(expr.Location, globalProcExpr, argumentList, DMValueType.Anything); @@ -1023,7 +1023,7 @@ private DMExpression BuildDereference(DMASTDereference deref, DreamPath? inferre case DMASTDereference.CallOperation callOperation: { var field = callOperation.Identifier; - var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters); + var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters, inferredPath); if (!callOperation.NoSearch && !pathIsFuzzy) { if (prevPath == null) { @@ -1081,7 +1081,7 @@ private DMExpression BuildImplicitIsType(DMASTImplicitIsType isType, DreamPath? return new IsTypeInferred(isType.Location, expr, expr.Path.Value); } - private DMExpression BuildList(DMASTList list) { + private DMExpression BuildList(DMASTList list, DreamPath? inferredPath) { (DMExpression? Key, DMExpression Value)[] values = []; if (list.Values != null) { @@ -1089,8 +1089,8 @@ private DMExpression BuildList(DMASTList list) { for (int i = 0; i < list.Values.Length; i++) { DMASTCallParameter value = list.Values[i]; - DMExpression? key = (value.Key != null) ? BuildExpression(value.Key) : null; - DMExpression listValue = BuildExpression(value.Value); + DMExpression? key = (value.Key != null) ? BuildExpression(value.Key, inferredPath) : null; + DMExpression listValue = BuildExpression(value.Value, inferredPath); values[i] = (key, listValue); } @@ -1151,7 +1151,7 @@ private DMExpression BuildAddText(DMASTAddText addText, DreamPath? inferredPath) return new AddText(addText.Location, expArr); } - private DMExpression BuildInput(DMASTInput input) { + private DMExpression BuildInput(DMASTInput input, DreamPath? inferredPath) { DMExpression[] arguments = new DMExpression[input.Parameters.Length]; for (int i = 0; i < input.Parameters.Length; i++) { DMASTCallParameter parameter = input.Parameters[i]; @@ -1161,12 +1161,12 @@ private DMExpression BuildInput(DMASTInput input) { "input() does not take named arguments"); } - arguments[i] = BuildExpression(parameter.Value); + arguments[i] = BuildExpression(parameter.Value, inferredPath); } DMExpression? list = null; if (input.List != null) { - list = BuildExpression(input.List); + list = BuildExpression(input.List, inferredPath); DMValueType objectTypes = DMValueType.Null |DMValueType.Obj | DMValueType.Mob | DMValueType.Turf | DMValueType.Area; @@ -1188,13 +1188,13 @@ private DMExpression BuildInput(DMASTInput input) { return new Input(input.Location, arguments, input.Types.Value, list); } - private DMExpression BuildPick(DMASTPick pick) { + private DMExpression BuildPick(DMASTPick pick, DreamPath? inferredPath) { Pick.PickValue[] pickValues = new Pick.PickValue[pick.Values.Length]; for (int i = 0; i < pickValues.Length; i++) { DMASTPick.PickValue pickValue = pick.Values[i]; - DMExpression? weight = (pickValue.Weight != null) ? BuildExpression(pickValue.Weight) : null; - DMExpression value = BuildExpression(pickValue.Value); + DMExpression? weight = (pickValue.Weight != null) ? BuildExpression(pickValue.Weight, inferredPath) : null; + DMExpression value = BuildExpression(pickValue.Value, inferredPath); if (weight is Prob prob) // pick(prob(50);x, prob(200);y) format weight = prob.P; diff --git a/DMCompiler/DM/Builders/DMProcBuilder.cs b/DMCompiler/DM/Builders/DMProcBuilder.cs index e4fcd52a2d..250ed587e0 100644 --- a/DMCompiler/DM/Builders/DMProcBuilder.cs +++ b/DMCompiler/DM/Builders/DMProcBuilder.cs @@ -101,6 +101,7 @@ public void ProcessStatement(DMASTProcStatement statement) { case DMASTProcStatementBrowse statementBrowse: ProcessStatementBrowse(statementBrowse); break; case DMASTProcStatementBrowseResource statementBrowseResource: ProcessStatementBrowseResource(statementBrowseResource); break; case DMASTProcStatementOutputControl statementOutputControl: ProcessStatementOutputControl(statementOutputControl); break; + case DMASTProcStatementLink statementLink: ProcessStatementLink(statementLink); break; case DMASTProcStatementFtp statementFtp: ProcessStatementFtp(statementFtp); break; case DMASTProcStatementOutput statementOutput: ProcessStatementOutput(statementOutput); break; case DMASTProcStatementInput statementInput: ProcessStatementInput(statementInput); break; @@ -489,6 +490,10 @@ public void ProcessStatementFor(DMASTProcStatementFor statementFor) { var outputVar = _exprBuilder.Create(outputExpr); + if (outputVar is Local { LocalVar: DMProc.LocalConstVariable } or Field { IsConst: true }) { + compiler.Emit(WarningCode.WriteToConstant, outputExpr.Location, "Cannot change constant value"); + } + var start = _exprBuilder.Create(exprRange.StartRange); var end = _exprBuilder.Create(exprRange.EndRange); var step = exprRange.Step != null @@ -520,7 +525,10 @@ public void ProcessStatementFor(DMASTProcStatementFor statementFor) { if (outputVar is Local outputLocal) { outputLocal.LocalVar.ExplicitValueType = statementFor.DMTypes; - } + if(outputLocal.LocalVar is DMProc.LocalConstVariable) + compiler.Emit(WarningCode.WriteToConstant, outputExpr.Location, "Cannot change constant value"); + } else if (outputVar is Field { IsConst: true }) + compiler.Emit(WarningCode.WriteToConstant, outputExpr.Location, "Cannot change constant value"); ProcessStatementForList(list, outputVar, statementFor.DMTypes, statementFor.Body); break; @@ -886,6 +894,12 @@ public void ProcessStatementOutputControl(DMASTProcStatementOutputControl statem proc.OutputControl(); } + public void ProcessStatementLink(DMASTProcStatementLink statementLink) { + _exprBuilder.Emit(statementLink.Receiver); + _exprBuilder.Emit(statementLink.Url); + proc.Link(); + } + public void ProcessStatementFtp(DMASTProcStatementFtp statementFtp) { _exprBuilder.Emit(statementFtp.Receiver); _exprBuilder.Emit(statementFtp.File); diff --git a/DMCompiler/DM/DMCodeTree.Vars.cs b/DMCompiler/DM/DMCodeTree.Vars.cs index 0657c8cccd..f7ff73d79c 100644 --- a/DMCompiler/DM/DMCodeTree.Vars.cs +++ b/DMCompiler/DM/DMCodeTree.Vars.cs @@ -43,13 +43,15 @@ protected void SetVariableValue(DMCompiler compiler, DMObject dmObject, DMVariab if (value.TryAsConstant(compiler, out var constant)) { variable.Value = constant; - return; + + // We want to continue with putting this in the init proc if a base type initializes it to another value + if (!isOverride || !dmObject.IsRuntimeInitialized(variable.Name)) { + return; + } } else if (variable.IsConst) { compiler.Emit(WarningCode.HardConstContext, value.Location, "Value of const var must be a constant"); return; - } - - if (!IsValidRightHandSide(compiler, dmObject, value)) { + } else if (!IsValidRightHandSide(compiler, dmObject, value)) { compiler.Emit(WarningCode.BadExpression, value.Location, $"Invalid initial value for \"{variable.Name}\""); return; @@ -60,7 +62,7 @@ protected void SetVariableValue(DMCompiler compiler, DMObject dmObject, DMVariab var assign = new Assignment(initLoc, field, value); variable.Value = new Null(Location.Internal); - dmObject.InitializationProcExpressions.Add(assign); + dmObject.InitializationProcAssignments.Add((variable.Name, assign)); } /// Whether the given value can be used as an instance variable's initial value @@ -74,6 +76,7 @@ private bool IsValidRightHandSide(DMCompiler compiler, DMObject dmObject, DMExpr "file" => true, "sound" => true, "nameof" => true, + "filter" => true, _ => false }, @@ -124,7 +127,7 @@ private bool HandleGlobalVar(DMCompiler compiler, DMObject dmObject, int pass) { return false; int globalId = compiler.DMObjectTree.CreateGlobal(out DMVariable global, varDef.Type, VarName, varDef.IsConst, - varDef.ValType); + varDef.IsFinal, varDef.ValType); dmObject.AddGlobalVariable(global, globalId); _defined = true; @@ -151,7 +154,7 @@ private bool HandleInstanceVar(DMCompiler compiler, DMObject dmObject) { if (!TryBuildValue(new(compiler, dmObject, null), varDef.Value, varDef.Type, ScopeMode.Normal, out var value)) return false; - var variable = new DMVariable(varDef.Type, VarName, false, varDef.IsConst, varDef.IsTmp, varDef.ValType); + var variable = new DMVariable(varDef.Type, VarName, false, varDef.IsConst, varDef.IsFinal, varDef.IsTmp, varDef.ValType); dmObject.AddVariable(variable); _defined = true; @@ -173,9 +176,16 @@ private bool AlreadyExists(DMCompiler compiler, DMObject dmObject) { $"Duplicate definition of static var \"{VarName}\""); return true; } else if (dmObject.HasLocalVariable(VarName)) { - if (!varDef.Location.InDMStandard) // Duplicate instance vars are not an error in DMStandard - compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, + if (!varDef.Location.InDMStandard) { // Duplicate instance vars are not an error in DMStandard + var variable = dmObject.GetVariable(VarName); + if(variable!.Value is not null) + compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, + $"Duplicate definition of var \"{VarName}\". Previous definition at {variable.Value.Location}"); + else + compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, $"Duplicate definition of var \"{VarName}\""); + } + return true; } else if (IsStatic && VarName == "vars" && dmObject == compiler.DMObjectTree.Root) { compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, "Duplicate definition of global.vars"); @@ -214,6 +224,11 @@ public override bool TryDefineVar(DMCompiler compiler, int pass) { $"Var \"{VarName}\" is const and cannot be modified"); _finished = true; return true; + } else if (variable.IsFinal) { + compiler.Emit(WarningCode.FinalOverride, varOverride.Location, + $"Var \"{VarName}\" is final and cannot be modified"); + _finished = true; + return true; } else if (variable.ValType.IsCompileTimeReadOnly) { compiler.Emit(WarningCode.WriteToConstant, varOverride.Location, $"Var \"{VarName}\" is a native read-only value which cannot be modified"); @@ -259,7 +274,7 @@ public override bool TryDefineVar(DMCompiler compiler, int pass) { } int globalId = compiler.DMObjectTree.CreateGlobal(out DMVariable global, varDecl.Type, varDecl.Name, varDecl.IsConst, - varDecl.ValType); + false, varDecl.ValType); global.Value = new Null(Location.Internal); proc.AddGlobalVariable(global, globalId); diff --git a/DMCompiler/DM/DMObject.cs b/DMCompiler/DM/DMObject.cs index d48d84eb6f..10834f8433 100644 --- a/DMCompiler/DM/DMObject.cs +++ b/DMCompiler/DM/DMObject.cs @@ -1,4 +1,6 @@ -using DMCompiler.Bytecode; +using System.Linq; +using DMCompiler.Bytecode; +using DMCompiler.DM.Expressions; using DMCompiler.Json; namespace DMCompiler.DM; @@ -21,7 +23,7 @@ internal sealed class DMObject(DMCompiler compiler, int id, DreamPath path, DMOb public int? InitializationProc; /// A list of var and verb initializations implicitly done before the user's New() is called. - public readonly List InitializationProcExpressions = new(); + public readonly List<(string Name, Assignment Assignment)> InitializationProcAssignments = new(); public bool IsRoot => Path == DreamPath.Root; @@ -133,6 +135,11 @@ public void AddGlobalVariable(DMVariable global, int id) { TmpVariables.Add(global.Name); } + public bool IsRuntimeInitialized(string varName) { + return InitializationProcAssignments.Any(v => v.Name == varName) + || (Parent?.IsRuntimeInitialized(varName) ?? false); + } + /// /// Recursively searches for a global/static with the given name. /// @@ -152,16 +159,16 @@ public DMComplexValueType GetReturnType(string name) { } public void CreateInitializationProc() { - if (InitializationProcExpressions.Count <= 0 || InitializationProc != null) + if (InitializationProcAssignments.Count <= 0 || InitializationProc != null) return; var init = compiler.DMObjectTree.CreateDMProc(this, null); InitializationProc = init.Id; init.Call(DMReference.SuperProc, DMCallArgumentsType.None, 0); - foreach (DMExpression expression in InitializationProcExpressions) { - init.DebugSource(expression.Location); - expression.EmitPushValue(new(compiler, this, init)); + foreach (var assignment in InitializationProcAssignments) { + init.DebugSource(assignment.Assignment.Location); + assignment.Assignment.EmitPushValue(new(compiler, this, init)); } } diff --git a/DMCompiler/DM/DMObjectTree.cs b/DMCompiler/DM/DMObjectTree.cs index 3dad425614..fe25b20d74 100644 --- a/DMCompiler/DM/DMObjectTree.cs +++ b/DMCompiler/DM/DMObjectTree.cs @@ -162,10 +162,10 @@ public bool TryGetTypeId(DreamPath path, out int typeId) { return null; } - public int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, DMComplexValueType valType) { + public int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, bool isFinal, DMComplexValueType valType) { int id = Globals.Count; - global = new DMVariable(type, name, true, isConst, false, valType); + global = new DMVariable(type, name, true, isConst, isFinal, false, valType); Globals.Add(global); return id; } diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 376fc8a2bd..4dfa008168 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -170,10 +170,9 @@ public void ValidateReturnType(DMExpression expr) { } public ProcDefinitionJson GetJsonRepresentation() { - var optimizer = new BytecodeOptimizer(); var serializer = new AnnotatedBytecodeSerializer(_compiler); - optimizer.Optimize(_compiler, AnnotatedBytecode.GetAnnotatedBytecode()); + _compiler.BytecodeOptimizer.Optimize(AnnotatedBytecode.GetAnnotatedBytecode()); List? arguments = null; if (_parameters.Count > 0) { @@ -480,6 +479,10 @@ public void OutputControl() { WriteOpcode(DreamProcOpcode.OutputControl); } + public void Link() { + WriteOpcode(DreamProcOpcode.Link); + } + public void Ftp() { WriteOpcode(DreamProcOpcode.Ftp); } diff --git a/DMCompiler/DM/DMVariable.cs b/DMCompiler/DM/DMVariable.cs index 2ff3b649e8..dc5cd772cc 100644 --- a/DMCompiler/DM/DMVariable.cs +++ b/DMCompiler/DM/DMVariable.cs @@ -4,24 +4,27 @@ namespace DMCompiler.DM; internal sealed class DMVariable { public DreamPath? Type; - public string Name; - public bool IsGlobal; + public readonly string Name; + public readonly bool IsGlobal; + public readonly bool IsTmp; + public readonly bool IsFinal; + public DMExpression? Value; + public DMComplexValueType ValType; + /// /// NOTE: This DMVariable may be forced constant through opendream_compiletimereadonly. This only marks that the variable has the DM quality of /const/ness. /// - public bool IsConst; - public bool IsTmp; - public DMExpression? Value; - public DMComplexValueType ValType; + public readonly bool IsConst; public bool CanConstFold => (IsConst || ValType.Type.HasFlag(DMValueType.CompiletimeReadonly)) && !ValType.Type.HasFlag(DMValueType.NoConstFold); - public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, bool isTmp, DMComplexValueType? valType = null) { + public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, bool isFinal, bool isTmp, DMComplexValueType? valType = null) { Type = type; Name = name; IsGlobal = isGlobal; IsConst = isConst; + IsFinal = isFinal; IsTmp = isTmp; Value = null; ValType = valType ?? DMValueType.Anything; @@ -32,6 +35,7 @@ public DMVariable(DMVariable copyFrom) { Name = copyFrom.Name; IsGlobal = copyFrom.IsGlobal; IsConst = copyFrom.IsConst; + IsFinal = copyFrom.IsFinal; IsTmp = copyFrom.IsTmp; Value = copyFrom.Value; ValType = copyFrom.ValType; diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index 5b073d4430..5ab9ff86bf 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -552,6 +552,13 @@ public override void EmitPushValue(ExpressionContext ctx) { return; } + if (Expression is Arglist arglist) { + // This happens silently in BYOND + ctx.Compiler.Emit(WarningCode.PointlessBuiltinCall, Location, "calling initial() on arglist() returns the current value"); + arglist.EmitPushArglist(ctx); + return; + } + ctx.Compiler.Emit(WarningCode.BadArgument, Expression.Location, $"can't get initial value of {Expression}"); ctx.Proc.Error(); } diff --git a/DMCompiler/DM/Expressions/Constant.cs b/DMCompiler/DM/Expressions/Constant.cs index d9f6b256a9..5b169cce5b 100644 --- a/DMCompiler/DM/Expressions/Constant.cs +++ b/DMCompiler/DM/Expressions/Constant.cs @@ -117,7 +117,7 @@ public Resource(DMCompiler compiler, Location location, string filePath) : base( // Search every defined FILE_DIR foreach (string resourceDir in compiler.ResourceDirectories) { - var directory = FindDirectory(resourceDir, fileDir); + var directory = FindDirectory(resourceDir == string.Empty ? "./" : resourceDir, fileDir); if (directory != null) { // Perform a case-insensitive search for the file diff --git a/DMCompiler/DM/Expressions/LValue.cs b/DMCompiler/DM/Expressions/LValue.cs index a32902f4f9..d5fd05f468 100644 --- a/DMCompiler/DM/Expressions/LValue.cs +++ b/DMCompiler/DM/Expressions/LValue.cs @@ -122,6 +122,7 @@ public override void EmitPushInitial(ExpressionContext ctx) { // Identifier of field internal sealed class Field(Location location, DMVariable variable, DMComplexValueType valType) : LValue(location, variable.Type) { + public bool IsConst { get; } = variable.IsConst; public override DMComplexValueType ValType => valType; public override void EmitPushInitial(ExpressionContext ctx) { diff --git a/DMCompiler/DMCompiler.cs b/DMCompiler/DMCompiler.cs index 65cac0c3b9..25f6e9a5f3 100644 --- a/DMCompiler/DMCompiler.cs +++ b/DMCompiler/DMCompiler.cs @@ -14,6 +14,7 @@ using DMCompiler.Compiler.DM.AST; using DMCompiler.DM.Builders; using DMCompiler.Json; +using DMCompiler.Optimizer; namespace DMCompiler; @@ -31,11 +32,13 @@ public class DMCompiler { internal readonly DMCodeTree DMCodeTree; internal readonly DMObjectTree DMObjectTree; internal readonly DMProc GlobalInitProc; + internal readonly BytecodeOptimizer BytecodeOptimizer; public DMCompiler() { DMCodeTree = new(this); DMObjectTree = new(this); GlobalInitProc = new(this, -1, DMObjectTree.Root, null); + BytecodeOptimizer = new BytecodeOptimizer(this); } public bool Compile(DMCompilerSettings settings) { @@ -125,6 +128,9 @@ public void AddResourceDirectory(string dir) { preproc.IncludeFile(includeDir, fileName, false); } + // Adds the root of the DM project to FILE_DIR + compiler.AddResourceDirectory(Path.GetDirectoryName(files[0]) ?? "/"); + string compilerDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; string dmStandardDirectory = Path.Join(compilerDirectory, "DMStandard"); @@ -136,7 +142,7 @@ public void AddResourceDirectory(string dir) { // Push the pragma config file to the tippy-top of the stack, super-duper prioritizing it, since it governs some compiler behaviour. string pragmaName; string pragmaDirectory; - if(Settings.PragmaFileOverride is not null) { + if (Settings.PragmaFileOverride is not null) { pragmaDirectory = Path.GetDirectoryName(Settings.PragmaFileOverride); pragmaName = Path.GetFileName(Settings.PragmaFileOverride); } else { @@ -144,7 +150,7 @@ public void AddResourceDirectory(string dir) { pragmaName = "DefaultPragmaConfig.dm"; } - if(!File.Exists(Path.Join(pragmaDirectory,pragmaName))) { + if (!File.Exists(Path.Join(pragmaDirectory, pragmaName))) { ForcedError($"Configuration file '{pragmaName}' not found."); return null; } @@ -333,7 +339,7 @@ private string SaveJson(List maps, string interfaceFile, string ou try { JsonSerializer.Serialize(outputFileHandle, compiledDream, - new JsonSerializerOptions() {DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault}); + new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }); return $"Saved to {outputFile}"; } catch (Exception e) { @@ -346,7 +352,7 @@ private string SaveJson(List maps, string interfaceFile, string ou public void DefineFatalErrors() { foreach (WarningCode code in Enum.GetValues()) { - if((int)code < 1_000) { + if ((int)code < 1_000) { Config.ErrorConfig[code] = ErrorLevel.Error; } } @@ -356,7 +362,7 @@ public void DefineFatalErrors() { /// This method also enforces the rule that all emissions with codes less than 1000 are mandatory errors. ///
public void CheckAllPragmasWereSet() { - foreach(WarningCode code in Enum.GetValues()) { + foreach (WarningCode code in Enum.GetValues()) { if (!Config.ErrorConfig.ContainsKey(code)) { ForcedWarning($"Warning #{(int)code:d4} '{code.ToString()}' was never declared as error, warning, notice, or disabled."); Config.ErrorConfig.Add(code, ErrorLevel.Disabled); diff --git a/DMCompiler/DMCompiler.csproj b/DMCompiler/DMCompiler.csproj index a32f4cae6a..a6c77222db 100644 --- a/DMCompiler/DMCompiler.csproj +++ b/DMCompiler/DMCompiler.csproj @@ -1,7 +1,7 @@  Exe - net8.0 + net9.0 enable Debug;Release;Tools AnyCPU diff --git a/DMCompiler/DMStandard/DefaultPragmaConfig.dm b/DMCompiler/DMStandard/DefaultPragmaConfig.dm index f6adf4e312..103b84ab59 100644 --- a/DMCompiler/DMStandard/DefaultPragmaConfig.dm +++ b/DMCompiler/DMStandard/DefaultPragmaConfig.dm @@ -52,4 +52,5 @@ #pragma AssignmentInConditional warning #pragma PickWeightedSyntax disabled #pragma AmbiguousInOrder warning +#pragma ExtraToken warning #pragma RuntimeSearchOperator disabled diff --git a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm index 9ce5c003d2..dcf0d03822 100644 --- a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm +++ b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm @@ -41,15 +41,15 @@ var/gender = NEUTER var/density = FALSE - var/maptext as opendream_unimplemented + var/maptext = null var/list/filters = null var/appearance var/appearance_flags = 0 - var/maptext_width as opendream_unimplemented - var/maptext_height as opendream_unimplemented - var/maptext_x = 32 as opendream_unimplemented - var/maptext_y = 32 as opendream_unimplemented + var/maptext_width = 32 + var/maptext_height = 32 + var/maptext_x = 0 + var/maptext_y = 0 var/step_x as opendream_unimplemented var/step_y as opendream_unimplemented var/render_source diff --git a/DMCompiler/DMStandard/Types/Image.dm b/DMCompiler/DMStandard/Types/Image.dm index 2b4a00575e..40b7142852 100644 --- a/DMCompiler/DMStandard/Types/Image.dm +++ b/DMCompiler/DMStandard/Types/Image.dm @@ -1,7 +1,7 @@ /image parent_type = /datum - //note these values also need to be set in IconAppearance.cs + //note these values also need to be set in MutableAppearance.cs var/alpha = 255 var/appearance var/appearance_flags = 0 @@ -17,11 +17,11 @@ var/list/filters = list() var/layer = FLOAT_LAYER var/luminosity = 0 as opendream_unimplemented - var/maptext = "i" as opendream_unimplemented - var/maptext_width = 32 as opendream_unimplemented - var/maptext_height = 32 as opendream_unimplemented - var/maptext_x = 0 as opendream_unimplemented - var/maptext_y = 0 as opendream_unimplemented + var/maptext = null + var/maptext_width = 32 + var/maptext_height = 32 + var/maptext_x = 0 + var/maptext_y = 0 var/mouse_over_pointer = 0 as opendream_unimplemented var/mouse_drag_pointer = 0 as opendream_unimplemented var/mouse_drop_pointer = 1 as opendream_unimplemented diff --git a/DMCompiler/DMStandard/UnsortedAdditions.dm b/DMCompiler/DMStandard/UnsortedAdditions.dm index 0268c8d362..f2edd548ca 100644 --- a/DMCompiler/DMStandard/UnsortedAdditions.dm +++ b/DMCompiler/DMStandard/UnsortedAdditions.dm @@ -22,8 +22,6 @@ set opendream_unimplemented = TRUE /proc/issaved(v) set opendream_unimplemented = TRUE -/proc/link(url) - set opendream_unimplemented = TRUE /proc/load_resource(File) set opendream_unimplemented = TRUE proc/missile(Type, Start, End) diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index 614ef790b2..65cd2fca98 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -28,6 +28,8 @@ proc/flist(Path) as /list proc/floor(A) as num proc/fract(n) as num proc/ftime(File, IsCreationTime = 0) as num +proc/get_step_to(Ref, Trg, Min=0) as num +proc/get_steps_to(Ref, Trg, Min=0) as /list proc/gradient(A, index) proc/hascall(Object, ProcName) as num proc/hearers(Depth = world.view, Center = usr) as /list @@ -161,14 +163,6 @@ proc/replacetextEx_char(Haystack as text, Needle, Replacement, Start = 1, End = var/step_dir = get_dir(Ref, Trg) return step(Ref, step_dir, Speed) -/proc/get_step_to(Ref, Trg, Min=0) - set opendream_unimplemented = TRUE - CRASH("/get_step_to() is not implemented") - -/proc/get_steps_to(Ref, Trg, Min=0) as /list - set opendream_unimplemented = TRUE - CRASH("/get_steps_to() is not implemented") - /proc/walk_away(Ref,Trg,Max=5,Lag=0,Speed=0) set opendream_unimplemented = TRUE CRASH("/walk_away() is not implemented") diff --git a/DMCompiler/Json/DreamProcJson.cs b/DMCompiler/Json/DreamProcJson.cs index c7f896a301..070adb86c9 100644 --- a/DMCompiler/Json/DreamProcJson.cs +++ b/DMCompiler/Json/DreamProcJson.cs @@ -3,39 +3,39 @@ namespace DMCompiler.Json; public sealed class ProcDefinitionJson { - public int OwningTypeId { get; set; } - public required string Name { get; set; } - public ProcAttributes Attributes { get; set; } - - public int MaxStackSize { get; set; } - public List? Arguments { get; set; } - public List? Locals { get; set; } - public required List SourceInfo { get; set; } - public byte[]? Bytecode { get; set; } - - public bool IsVerb { get; set; } - public VerbSrc? VerbSrc { get; set; } - public string? VerbName { get; set; } - public string? VerbCategory { get; set; } - public string? VerbDesc { get; set; } - public sbyte Invisibility { get; set; } + public int OwningTypeId { get; init; } + public required string Name { get; init; } + public ProcAttributes Attributes { get; init; } + + public int MaxStackSize { get; init; } + public List? Arguments { get; init; } + public List? Locals { get; init; } + public required List SourceInfo { get; init; } + public byte[]? Bytecode { get; init; } + + public bool IsVerb { get; init; } + public VerbSrc? VerbSrc { get; init; } + public string? VerbName { get; init; } + public string? VerbCategory { get; init; } + public string? VerbDesc { get; init; } + public sbyte Invisibility { get; init; } } -public sealed class ProcArgumentJson { - public required string Name { get; set; } - public DMValueType Type { get; set; } +public struct ProcArgumentJson { + public required string Name { get; init; } + public DMValueType Type { get; init; } } -public sealed class LocalVariableJson { - public int Offset { get; set; } - public int? Remove { get; set; } - public string? Add { get; set; } +public struct LocalVariableJson { + public int Offset { get; init; } + public int? Remove { get; init; } + public string? Add { get; init; } } -public sealed class SourceInfoJson { - public int Offset { get; set; } +public struct SourceInfoJson { + public int Offset { get; init; } public int? File { get; set; } - public int Line { get; set; } + public int Line { get; init; } } public class LineComparer : IEqualityComparer { @@ -43,6 +43,10 @@ public bool Equals(SourceInfoJson? x, SourceInfoJson? y) { return x?.Line == y?.Line; } + public bool Equals(SourceInfoJson x, SourceInfoJson y) { + return x.Line == y.Line; + } + public int GetHashCode(SourceInfoJson obj) { return obj.Line.GetHashCode(); } diff --git a/DMCompiler/Optimizer/BytecodeOptimizer.cs b/DMCompiler/Optimizer/BytecodeOptimizer.cs index 89389db81b..dc1ffb7c7f 100644 --- a/DMCompiler/Optimizer/BytecodeOptimizer.cs +++ b/DMCompiler/Optimizer/BytecodeOptimizer.cs @@ -2,8 +2,10 @@ namespace DMCompiler.Optimizer; -public class BytecodeOptimizer { - internal void Optimize(DMCompiler compiler, List input) { +public class BytecodeOptimizer(DMCompiler compiler) { + private readonly PeepholeOptimizer _peepholeOptimizer = new(compiler); + + internal void Optimize(List input) { if (input.Count == 0) return; @@ -11,10 +13,10 @@ internal void Optimize(DMCompiler compiler, List input) { JoinAndForwardLabels(input); RemoveUnreferencedLabels(input); - PeepholeOptimizer.RunPeephole(compiler, input); + _peepholeOptimizer.RunPeephole(input); } - private static void RemoveUnreferencedLabels(List input) { + private void RemoveUnreferencedLabels(List input) { Dictionary labelReferences = new(); for (int i = 0; i < input.Count; i++) { if (input[i] is AnnotatedBytecodeLabel label) { @@ -38,7 +40,7 @@ private static void RemoveUnreferencedLabels(List input) { } } - private static void JoinAndForwardLabels(List input) { + private void JoinAndForwardLabels(List input) { Dictionary labelAliases = new(); for (int i = 0; i < input.Count; i++) { if (input[i] is AnnotatedBytecodeLabel label) { @@ -74,7 +76,7 @@ private static void JoinAndForwardLabels(List input) { } } - private static bool TryGetLabelName(AnnotatedBytecodeInstruction instruction, [NotNullWhen(true)] out string? labelName) { + private bool TryGetLabelName(AnnotatedBytecodeInstruction instruction, [NotNullWhen(true)] out string? labelName) { foreach (var arg in instruction.GetArgs()) { if (arg is not AnnotatedBytecodeLabel label) continue; diff --git a/DMCompiler/Optimizer/PeepholeOptimizations.cs b/DMCompiler/Optimizer/PeepholeOptimizations.cs index 56f6f4577f..6b2b9ed3e1 100644 --- a/DMCompiler/Optimizer/PeepholeOptimizations.cs +++ b/DMCompiler/Optimizer/PeepholeOptimizations.cs @@ -116,6 +116,34 @@ public void Apply(DMCompiler compiler, List input, int index } } +// PushReferenceValue [ref] +// PushString [string] +// DereferenceIndex +// -> IndexRefWithString [ref, string] +internal sealed class IndexRefWithString : IOptimization { + public OptPass OptimizationPass => OptPass.PeepholeOptimization; + + public ReadOnlySpan GetOpcodes() { + return [ + DreamProcOpcode.PushReferenceValue, + DreamProcOpcode.PushString, + DreamProcOpcode.DereferenceIndex + ]; + } + + public void Apply(DMCompiler compiler, List input, int index) { + AnnotatedBytecodeInstruction firstInstruction = (AnnotatedBytecodeInstruction)(input[index]); + AnnotatedBytecodeReference pushVal = firstInstruction.GetArg(0); + + AnnotatedBytecodeInstruction secondInstruction = (AnnotatedBytecodeInstruction)(input[index + 1]); + AnnotatedBytecodeString strIndex = secondInstruction.GetArg(0); + + input.RemoveRange(index, 3); + input.Insert(index, new AnnotatedBytecodeInstruction(DreamProcOpcode.IndexRefWithString, -1, + [pushVal, strIndex])); + } +} + // PushReferenceValue [ref] // Return // -> ReturnReferenceValue [ref] @@ -186,6 +214,24 @@ public void Apply(DMCompiler compiler, List input, int index } } +// Return +// Jump [label] +// -> Return +internal sealed class RemoveJumpAfterReturn : IOptimization { + public OptPass OptimizationPass => OptPass.PeepholeOptimization; + + public ReadOnlySpan GetOpcodes() { + return [ + DreamProcOpcode.Return, + DreamProcOpcode.Jump + ]; + } + + public void Apply(DMCompiler compiler, List input, int index) { + input.RemoveRange(index + 1, 1); + } +} + // PushFloat [float] // SwitchCase [label] // -> SwitchOnFloat [float] [label] diff --git a/DMCompiler/Optimizer/PeepholeOptimizer.cs b/DMCompiler/Optimizer/PeepholeOptimizer.cs index aad7cd7287..73eb36c776 100644 --- a/DMCompiler/Optimizer/PeepholeOptimizer.cs +++ b/DMCompiler/Optimizer/PeepholeOptimizer.cs @@ -40,6 +40,8 @@ internal enum OptPass : byte { // ReSharper disable once ClassNeverInstantiated.Global internal sealed class PeepholeOptimizer { + private readonly DMCompiler _compiler; + private class OptimizationTreeEntry { public IOptimization? Optimization; public Dictionary? Children; @@ -48,47 +50,44 @@ private class OptimizationTreeEntry { /// /// The optimization passes in the order that they run /// - private static readonly OptPass[] Passes; + private readonly OptPass[] _passes; /// /// Trees matching chains of opcodes to peephole optimizations /// - private static readonly Dictionary[] OptimizationTrees; - - static PeepholeOptimizer() { - Passes = (OptPass[])Enum.GetValues(typeof(OptPass)); - OptimizationTrees = new Dictionary[Passes.Length]; - for (int i = 0; i < OptimizationTrees.Length; i++) { - OptimizationTrees[i] = new Dictionary(); + private readonly Dictionary[] _optimizationTrees; + + public PeepholeOptimizer(DMCompiler compiler) { + _compiler = compiler; + _passes = (OptPass[])Enum.GetValues(typeof(OptPass)); + _optimizationTrees = new Dictionary[_passes.Length]; + for (int i = 0; i < _optimizationTrees.Length; i++) { + _optimizationTrees[i] = new Dictionary(); } } - /// Setup for each - private static void GetOptimizations(DMCompiler compiler) { - var possibleTypes = typeof(IOptimization).Assembly.GetTypes(); - var optimizationTypes = new List(possibleTypes.Length); - - foreach (var type in possibleTypes) { - if (typeof(IOptimization).IsAssignableFrom(type) && type is { IsClass: true, IsAbstract: false }) { - optimizationTypes.Add(type); - } - } + /// Setup for each + private void GetOptimizations() { + foreach (var optType in typeof(IOptimization).Assembly.GetTypes()) { + if (!typeof(IOptimization).IsAssignableFrom(optType) || + optType is not { IsClass: true, IsAbstract: false }) + continue; - foreach (var optType in optimizationTypes) { var opt = (IOptimization)(Activator.CreateInstance(optType)!); var opcodes = opt.GetOpcodes(); if (opcodes.Length < 2) { - compiler.ForcedError(Location.Internal, $"Peephole optimization {optType} must have at least 2 opcodes"); + _compiler.ForcedError(Location.Internal, + $"Peephole optimization {optType} must have at least 2 opcodes"); continue; } - if (!OptimizationTrees[(byte)opt.OptimizationPass].TryGetValue(opcodes[0], out var treeEntry)) { + if (!_optimizationTrees[(byte)opt.OptimizationPass].TryGetValue(opcodes[0], out var treeEntry)) { treeEntry = new() { Children = new() }; - OptimizationTrees[(byte)opt.OptimizationPass].Add(opcodes[0], treeEntry); + _optimizationTrees[(byte)opt.OptimizationPass].Add(opcodes[0], treeEntry); } for (int i = 1; i < opcodes.Length; i++) { @@ -107,14 +106,14 @@ private static void GetOptimizations(DMCompiler compiler) { } } - public static void RunPeephole(DMCompiler compiler, List input) { - GetOptimizations(compiler); - foreach (var optPass in Passes) { - RunPass(compiler, (byte)optPass, input); + public void RunPeephole(List input) { + GetOptimizations(); + foreach (var optPass in _passes) { + RunPass((byte)optPass, input); } } - private static void RunPass(DMCompiler compiler, byte pass, List input) { + private void RunPass(byte pass, List input) { OptimizationTreeEntry? currentOpt = null; int optSize = 0; @@ -125,7 +124,7 @@ int AttemptCurrentOpt(int i) { int offset; if (currentOpt.Optimization?.CheckPreconditions(input, i - optSize) is true) { - currentOpt.Optimization.Apply(compiler, input, i - optSize); + currentOpt.Optimization.Apply(_compiler, input, i - optSize); offset = (optSize + 2); // Run over the new opcodes for potential further optimization } else { // This chain of opcodes did not lead to a valid optimization. @@ -141,7 +140,7 @@ int AttemptCurrentOpt(int i) { var bytecode = input[i]; if (bytecode is not AnnotatedBytecodeInstruction instruction) { i -= AttemptCurrentOpt(i); - i = Math.Max(i, 0); + i = Math.Max(i, -1); // i++ brings -1 back to 0 continue; } @@ -149,7 +148,7 @@ int AttemptCurrentOpt(int i) { if (currentOpt == null) { optSize = 1; - OptimizationTrees[pass].TryGetValue(opcode, out currentOpt); + _optimizationTrees[pass].TryGetValue(opcode, out currentOpt); continue; } @@ -160,7 +159,7 @@ int AttemptCurrentOpt(int i) { } i -= AttemptCurrentOpt(i); - i = Math.Max(i, 0); + i = Math.Max(i, -1); // i++ brings -1 back to 0 } AttemptCurrentOpt(input.Count); diff --git a/DMDisassembler/DMDisassembler.csproj b/DMDisassembler/DMDisassembler.csproj index fc042c6040..6e56f0e729 100644 --- a/DMDisassembler/DMDisassembler.csproj +++ b/DMDisassembler/DMDisassembler.csproj @@ -1,7 +1,7 @@  Exe - net8.0 + net9.0 Debug;Release;Tools AnyCPU false diff --git a/DMDisassembler/DMProc.cs b/DMDisassembler/DMProc.cs index a24ecb282b..44e9d12b0a 100644 --- a/DMDisassembler/DMProc.cs +++ b/DMDisassembler/DMProc.cs @@ -9,7 +9,7 @@ namespace DMDisassembler; internal class DMProc(ProcDefinitionJson json) { - private class DecompiledOpcode(int position, string text) { + internal struct DecompiledOpcode(int position, string text) { public readonly int Position = position; public readonly string Text = text; } @@ -21,19 +21,7 @@ private class DecompiledOpcode(int position, string text) { public Exception exception; public string Decompile() { - List decompiled = new(); - HashSet labeledPositions = new(); - - try { - foreach (var (position, instruction) in new ProcDecoder(Program.CompiledJson.Strings, Bytecode).Disassemble()) { - decompiled.Add(new DecompiledOpcode(position, ProcDecoder.Format(instruction, type => Program.CompiledJson.Types[type].Path))); - if (ProcDecoder.GetJumpDestination(instruction) is int jumpPosition) { - labeledPositions.Add(jumpPosition); - } - } - } catch (Exception ex) { - exception = ex; - } + List decompiled = GetDecompiledOpcodes(out var labeledPositions); StringBuilder result = new StringBuilder(); foreach (DecompiledOpcode decompiledOpcode in decompiled) { @@ -59,6 +47,24 @@ public string Decompile() { return result.ToString(); } + public List GetDecompiledOpcodes(out HashSet labeledPositions) { + List decompiled = new(); + labeledPositions = new(); + + try { + foreach (var (position, instruction) in new ProcDecoder(Program.CompiledJson.Strings, Bytecode).Disassemble()) { + decompiled.Add(new DecompiledOpcode(position, ProcDecoder.Format(instruction, type => Program.CompiledJson.Types[type].Path))); + if (ProcDecoder.GetJumpDestination(instruction) is int jumpPosition) { + labeledPositions.Add(jumpPosition); + } + } + } catch (Exception ex) { + exception = ex; + } + + return decompiled; + } + [CanBeNull] public string[] GetArguments() { if (json.Arguments is null || json.Arguments.Count == 0) return null; diff --git a/DMDisassembler/Program.cs b/DMDisassembler/Program.cs index 638358c600..b5791d7d95 100644 --- a/DMDisassembler/Program.cs +++ b/DMDisassembler/Program.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.Json; +using DMCompiler.Bytecode; using DMCompiler.Json; +using JetBrains.Annotations; namespace DMDisassembler; @@ -13,6 +16,7 @@ internal class Program { public static DMProc GlobalInitProc = null; public static List Procs = null; public static Dictionary AllTypes = null; + public static List TypesById = null; private static readonly string NoTypeSelectedMessage = "No type is selected"; @@ -52,6 +56,8 @@ static void Main(string[] args) { } } + Console.WriteLine("DM Disassembler for OpenDream. Enter a command or \"help\" for more information."); + bool acceptingCommands = true; while (acceptingCommands) { if (_selectedType != null) { @@ -71,6 +77,7 @@ static void Main(string[] args) { switch (command) { case "quit": + case "exit": case "q": acceptingCommands = false; break; case "search": Search(split); break; case "sel": @@ -78,25 +85,132 @@ static void Main(string[] args) { case "list": List(split); break; case "d": case "decompile": Decompile(split); break; + case "stats": Stats(GetArg()); break; case "test-all": TestAll(); break; case "dump-all": DumpAll(); break; - case "help": PrintHelp(); break; - default: Console.WriteLine("Invalid command \"" + command + "\""); break; + case "help": { + PrintHelp(GetArg()); + break; + } + default: Console.WriteLine($"Invalid command \"{command}\""); break; + } + + [CanBeNull] + string GetArg() { + if (split.Length > 2) { + Console.WriteLine($"Command \"{command}\" takes 0 or 1 arguments. Ignoring extra arguments."); + } + + return split.Length > 1 ? split[1] : null; + } + } + } + + private static void PrintHelp([CanBeNull] string command) { + if (string.IsNullOrEmpty(command)) { + AllCommands(); + return; + } + + command = command.ToLower(); + + switch (command) { + case "stats": { + Console.WriteLine("Prints various statistics. Usage: stats [type]"); + Console.WriteLine("Options for [type]:"); + Console.WriteLine("procs-by-type : Prints the number of proc declarations (not overrides) on each type in descending order"); + Console.WriteLine("opcode-count : Prints the number of occurrences for each opcode in descending order"); + break; + } + default: { + Console.WriteLine($"No additional help for \"{command}\""); + AllCommands(); + break; } } + + void AllCommands() { + Console.WriteLine("DM Disassembler for OpenDream"); + Console.WriteLine("Commands and arguments:"); + Console.WriteLine("help [command] : Show additional help for [command] if applicable"); + Console.WriteLine("exit|quit|q : Exits the disassembler"); + Console.WriteLine("search type|proc [name] : Search for a particular typepath or a proc on a selected type"); + Console.WriteLine("select|sel : Select a typepath to run further commands on"); + Console.WriteLine("list procs|globals : List all globals, or all procs on a selected type"); + Console.WriteLine("decompile|d [name] : Decompiles the proc on the selected type"); + Console.WriteLine("stats [type] : Prints various stats about the game. Use \"help stats\" for more info"); + Console.WriteLine("dump-all : Decompiles every proc and writes the output to a file"); + Console.WriteLine("test-all : Tries to decompile every single proc to check for issues with this disassembler; not for production use"); + } } - private static void PrintHelp() { - Console.WriteLine("DM Disassembler for OpenDream"); - Console.WriteLine("Commands and arguments:"); - Console.WriteLine("help : Show this help"); - Console.WriteLine("quit|q : Exits the disassembler"); - Console.WriteLine("search type|proc [name] : Search for a particular typepath or a proc on a selected type"); - Console.WriteLine("select|sel : Select a typepath to run further commands on"); - Console.WriteLine("list procs|globals : List all globals, or all procs on a selected type"); - Console.WriteLine("decompile|d [name] : Decompiles the proc on the selected type"); - Console.WriteLine("dump-all : Decompiles every proc and writes the output to a file"); - Console.WriteLine("test-all : Tries to decompile every single proc to check for issues with this disassembler; not for production use"); + private static void Stats([CanBeNull] string statType) { + if (string.IsNullOrEmpty(statType)) { + PrintHelp("stats"); + return; + } + + switch (statType) { + case "procs-by-type": { + ProcsByType(); + return; + } + case "opcode-count": { + OpcodeCount(); + return; + } + default: { + Console.WriteLine($"Unknown stat \"{statType}\""); + PrintHelp("stats"); + return; + } + } + + void ProcsByType() { + Console.WriteLine("Counting all proc declarations (no overrides) by type. This may take a moment."); + Dictionary typeIdToProcCount = new Dictionary(); + foreach (DMProc proc in Procs) { + if(proc.IsOverride || proc.Name == "") continue; // Don't count overrides or procs + if (typeIdToProcCount.TryGetValue(proc.OwningTypeId, out var count)) { + typeIdToProcCount[proc.OwningTypeId] = count + 1; + } else { + typeIdToProcCount[proc.OwningTypeId] = 1; + } + } + + Console.WriteLine("Type: Proc Declarations"); + foreach (var pair in typeIdToProcCount.OrderByDescending(kvp => kvp.Value)) { + var type = TypesById[pair.Key]; + if (pair.Key == 0) { + Console.WriteLine($": {pair.Value:n0}"); + } else { + Console.WriteLine($"{type.Path}: {pair.Value:n0}"); + } + } + } + + void OpcodeCount() { + Console.WriteLine("Counting all opcode occurrences. This may take a moment."); + Dictionary opcodeToCount = new Dictionary(); + + // We need to fill the dict first in case there's any opcodes with 0 occurrences in the bytecode + foreach (string opcodeName in Enum.GetNames(typeof(DreamProcOpcode))) { + opcodeToCount.Add(opcodeName, 0); + } + + foreach (DMProc proc in Procs) { + var decompiledOpcodes = proc.GetDecompiledOpcodes(out _); + foreach (var opcode in decompiledOpcodes) { + var name = opcode.Text.Split(' ')[0]; + opcodeToCount[name] += 1; + } + } + + Console.WriteLine("Opcode: Count"); + foreach (var pair in opcodeToCount.OrderByDescending(kvp => kvp.Value)) { + Console.WriteLine($"{pair.Key}: {pair.Value:n0}"); + } + } } private static void Search(string[] args) { @@ -224,9 +338,12 @@ private static void LoadAllProcs() { private static void LoadAllTypes() { AllTypes = new Dictionary(CompiledJson.Types.Length); + TypesById = new List(CompiledJson.Types.Length); foreach (DreamTypeJson json in CompiledJson.Types) { - AllTypes.Add(json.Path, new DMType(json)); + var dmType = new DMType(json); + AllTypes.Add(json.Path, dmType); + TypesById.Add(dmType); } //Add global procs to the root type diff --git a/OpenDream.sln.DotSettings b/OpenDream.sln.DotSettings index 68a026431e..9303eadec4 100644 --- a/OpenDream.sln.DotSettings +++ b/OpenDream.sln.DotSettings @@ -3,11 +3,14 @@ WARNING WARNING WARNING - NONE + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW 0 1 1 AABB + RT DM DMF DMI diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index 42ce42f878..f760189b27 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -43,7 +43,7 @@ public ContextMenuPopup() { _metadataQuery = _entityManager.GetEntityQuery(); } - public void RepopulateEntities(ClientObjectReference[] entities, int? turfId) { + public void RepopulateEntities(ClientObjectReference[] entities, uint? turfId) { ContextMenu.RemoveAllChildren(); if (_transformSystem == null) diff --git a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs index 73e1a7c148..b5ff163d17 100644 --- a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs @@ -4,9 +4,11 @@ using OpenDreamClient.Rendering; using OpenDreamShared.Dream; using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Client.ViewVariables; +using Robust.Shared.Map; namespace OpenDreamClient.Input.ContextMenu; @@ -40,33 +42,60 @@ public VerbMenuPopup(ClientVerbSystem? verbSystem, sbyte seeInvisible, ClientObj } #if TOOLS - // If we're compiling with TOOLS and this is an entity, provide the option to use RT's VV or our icon debugger + // We add some additional debugging tools in TOOLS mode + var iconDebugButton = AddButton("Debug Icon"); + + iconDebugButton.OnPressed += _ => { + DreamIcon icon; + switch (_target.Type) { + case ClientObjectReference.RefType.Entity: + var entityManager = IoCManager.Resolve(); + var entityId = entityManager.GetEntity(_target.Entity); + if (!entityManager.TryGetComponent(entityId, out DMISpriteComponent? spriteComponent)) { + Logger.GetSawmill("opendream") + .Error($"Failed to get sprite component for {entityId} when trying to debug its icon"); + return; + } + + icon = spriteComponent.Icon; + break; + case ClientObjectReference.RefType.Turf: + var mapManager = IoCManager.Resolve(); + var mapId = new MapId(_target.TurfZ); + var mapPos = new Vector2(_target.TurfX, _target.TurfY); + if (!mapManager.TryFindGridAt(mapId, mapPos, out var gridUid, out var grid)) { + Logger.GetSawmill("opendream") + .Error($"Failed to get icon for {_target} when trying to debug its icon"); + return; + } + + var entitySystemManager = IoCManager.Resolve(); + var mapSystem = entitySystemManager.GetEntitySystem(); + var appearanceSystem = entitySystemManager.GetEntitySystem(); + var tileRef = mapSystem.GetTileRef(gridUid, grid, (Vector2i)mapPos); + icon = appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId); + break; + default: + return; + } + + new IconDebugWindow(icon).Show(); + }; + + // If this is an entity, provide the option to use RT's VV if (_target.Type == ClientObjectReference.RefType.Entity) { var viewVariablesButton = AddButton("RT ViewVariables"); - var iconDebugButton = AddButton("Debug Icon"); viewVariablesButton.OnPressed += _ => { IoCManager.Resolve().OpenVV(_target.Entity); }; - - iconDebugButton.OnPressed += _ => { - var entityManager = IoCManager.Resolve(); - var entityId = entityManager.GetEntity(_target.Entity); - if (!entityManager.TryGetComponent(entityId, out DMISpriteComponent? spriteComponent)) { - Logger.GetSawmill("opendream") - .Error($"Failed to get sprite component for {entityId} when trying to debug its icon"); - return; - } - - new IconDebugWindow(spriteComponent.Icon).Show(); - }; } #endif } private void AddVerb(int verbId, ClientObjectReference verbSrc, VerbSystem.VerbInfo verbInfo) { var button = AddButton(verbInfo.Name); - var takesTargetArg = verbInfo.GetTargetType() != null && !verbSrc.Equals(_target); + var takesTargetArg = verbInfo.GetTargetType() != null; button.OnPressed += _ => { _verbSystem?.ExecuteVerb(verbSrc, verbId, takesTargetArg ? [_target] : []); diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 0c90fa271b..160b7aa761 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -106,14 +106,14 @@ public void HandleStatClick(string atomRef, bool isRight, bool isMiddle) { } } - private (ClientObjectReference Atom, Vector2i IconPosition)? GetTurfUnderMouse(MapCoordinates mapCoords, out int? turfId) { + private (ClientObjectReference Atom, Vector2i IconPosition)? GetTurfUnderMouse(MapCoordinates mapCoords, out uint? turfId) { // Grid coordinates are half a meter off from entity coordinates mapCoords = new MapCoordinates(mapCoords.Position + new Vector2(0.5f), mapCoords.MapId); if (_mapManager.TryFindGridAt(mapCoords, out var gridEntity, out var grid)) { Vector2i position = _mapSystem.CoordinatesToTile(gridEntity, grid, _mapSystem.MapToGrid(gridEntity, mapCoords)); _mapSystem.TryGetTile(grid, position, out Tile tile); - turfId = tile.TypeId; + turfId = (uint)tile.TypeId; Vector2i turfIconPosition = (Vector2i) ((mapCoords.Position - position) * EyeManager.PixelsPerMeter); MapCoordinates worldPosition = _mapSystem.GridTileToWorld(gridEntity, grid, position); diff --git a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs index 6d8d2d273c..338956b9f4 100644 --- a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs +++ b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs @@ -44,25 +44,29 @@ private void Update() { // Would be nice if we could use ViewVariables instead, but I couldn't find a nice way to do that // Would be especially nice if we could use VV to make these editable - AddPropertyIfNotDefault("Name", appearance.Name, IconAppearance.Default.Name); - AddPropertyIfNotDefault("Icon State", appearance.IconState, IconAppearance.Default.IconState); - AddPropertyIfNotDefault("Direction", appearance.Direction, IconAppearance.Default.Direction); - AddPropertyIfNotDefault("Inherits Direction", appearance.InheritsDirection, IconAppearance.Default.InheritsDirection); - AddPropertyIfNotDefault("Pixel Offset X/Y", appearance.PixelOffset, IconAppearance.Default.PixelOffset); - AddPropertyIfNotDefault("Pixel Offset W/Z", appearance.PixelOffset2, IconAppearance.Default.PixelOffset2); - AddPropertyIfNotDefault("Color", appearance.Color, IconAppearance.Default.Color); - AddPropertyIfNotDefault("Alpha", appearance.Alpha, IconAppearance.Default.Alpha); - AddPropertyIfNotDefault("Glide Size", appearance.GlideSize, IconAppearance.Default.GlideSize); - AddPropertyIfNotDefault("Layer", appearance.Layer, IconAppearance.Default.Layer); - AddPropertyIfNotDefault("Plane", appearance.Plane, IconAppearance.Default.Plane); - AddPropertyIfNotDefault("Blend Mode", appearance.BlendMode, IconAppearance.Default.BlendMode); - AddPropertyIfNotDefault("Appearance Flags", appearance.AppearanceFlags, IconAppearance.Default.AppearanceFlags); - AddPropertyIfNotDefault("Invisibility", appearance.Invisibility, IconAppearance.Default.Invisibility); - AddPropertyIfNotDefault("Opacity", appearance.Opacity, IconAppearance.Default.Opacity); - AddPropertyIfNotDefault("Override", appearance.Override, IconAppearance.Default.Override); - AddPropertyIfNotDefault("Render Source", appearance.RenderSource, IconAppearance.Default.RenderSource); - AddPropertyIfNotDefault("Render Target", appearance.RenderTarget, IconAppearance.Default.RenderTarget); - AddPropertyIfNotDefault("Mouse Opacity", appearance.MouseOpacity, IconAppearance.Default.MouseOpacity); + AddPropertyIfNotDefault("Name", appearance.Name, MutableAppearance.Default.Name); + AddPropertyIfNotDefault("Desc", appearance.Desc, MutableAppearance.Default.Desc); + AddPropertyIfNotDefault("Icon State", appearance.IconState, MutableAppearance.Default.IconState); + AddPropertyIfNotDefault("Direction", appearance.Direction, MutableAppearance.Default.Direction); + AddPropertyIfNotDefault("Inherits Direction", appearance.InheritsDirection, MutableAppearance.Default.InheritsDirection); + AddPropertyIfNotDefault("Pixel Offset X/Y", appearance.PixelOffset, MutableAppearance.Default.PixelOffset); + AddPropertyIfNotDefault("Pixel Offset W/Z", appearance.PixelOffset2, MutableAppearance.Default.PixelOffset2); + AddPropertyIfNotDefault("Color", appearance.Color, MutableAppearance.Default.Color); + AddPropertyIfNotDefault("Alpha", appearance.Alpha, MutableAppearance.Default.Alpha); + AddPropertyIfNotDefault("Glide Size", appearance.GlideSize, MutableAppearance.Default.GlideSize); + AddPropertyIfNotDefault("Layer", appearance.Layer, MutableAppearance.Default.Layer); + AddPropertyIfNotDefault("Plane", appearance.Plane, MutableAppearance.Default.Plane); + AddPropertyIfNotDefault("Blend Mode", appearance.BlendMode, MutableAppearance.Default.BlendMode); + AddPropertyIfNotDefault("Appearance Flags", appearance.AppearanceFlags, MutableAppearance.Default.AppearanceFlags); + AddPropertyIfNotDefault("Invisibility", appearance.Invisibility, MutableAppearance.Default.Invisibility); + AddPropertyIfNotDefault("Opacity", appearance.Opacity, MutableAppearance.Default.Opacity); + AddPropertyIfNotDefault("Override", appearance.Override, MutableAppearance.Default.Override); + AddPropertyIfNotDefault("Render Source", appearance.RenderSource, MutableAppearance.Default.RenderSource); + AddPropertyIfNotDefault("Render Target", appearance.RenderTarget, MutableAppearance.Default.RenderTarget); + AddPropertyIfNotDefault("Mouse Opacity", appearance.MouseOpacity, MutableAppearance.Default.MouseOpacity); + AddPropertyIfNotDefault("Map Text Offset", appearance.MaptextOffset, MutableAppearance.Default.MaptextOffset); + AddPropertyIfNotDefault("Map Text Size", appearance.MaptextSize, MutableAppearance.Default.MaptextSize); + AddPropertyIfNotDefault("Map Text", appearance.Maptext, MutableAppearance.Default.Maptext); foreach (var overlay in _icon.Overlays) { AddDreamIconButton(OverlaysGrid, overlay); diff --git a/OpenDreamClient/Interface/Descriptors/InterfaceDescriptor.cs b/OpenDreamClient/Interface/Descriptors/InterfaceDescriptor.cs index d60c5c099d..f18f68777a 100644 --- a/OpenDreamClient/Interface/Descriptors/InterfaceDescriptor.cs +++ b/OpenDreamClient/Interface/Descriptors/InterfaceDescriptor.cs @@ -16,9 +16,9 @@ public InterfaceDescriptor(List windowDescriptors, List(MacroSetDescriptors) - .Concat(MenuDescriptors).FirstOrDefault(descriptor => descriptor.Name.Value == name); + .Concat(MenuDescriptors).FirstOrDefault(descriptor => descriptor.Id.Value == id); } } diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index 9e1bde3088..1e97447d53 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -40,6 +40,8 @@ internal sealed class DreamInterfaceManager : IDreamInterfaceManager { [Dependency] private readonly IUserInterfaceManager _uiManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ITimerManager _timerManager = default!; + [Dependency] private readonly IUriOpener _uriOpener = default!; + [Dependency] private readonly IGameController _gameController = default!; private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.interface"); @@ -117,6 +119,7 @@ public void Initialize() { _netManager.RegisterNetMessage(RxWinClone); _netManager.RegisterNetMessage(RxWinExists); _netManager.RegisterNetMessage(RxWinGet); + _netManager.RegisterNetMessage(RxLink); _netManager.RegisterNetMessage(RxFtp); _netManager.RegisterNetMessage(RxLoadInterface); _netManager.RegisterNetMessage(); @@ -256,6 +259,29 @@ private void RxWinGet(MsgWinGet message) { })); } + private void RxLink(MsgLink message) { + Uri uri; + try { + uri = new Uri(message.Url); + } catch (Exception e) { + _sawmill.Error($"Received link \"{message.Url}\" which failed to parse as a valid URI: {e.Message}"); + return; + } + + // TODO: This can be a topic call + + if (uri.Scheme is "http" or "https") { + _uriOpener.OpenUri(message.Url); + } else if (uri.Scheme is "ss14" or "ss14s") { + if (_gameController.LaunchState.FromLauncher) + _gameController.Redial(message.Url, "link() used to connect to another server."); + else + _sawmill.Warning("link() only supports connecting to other servers when utilizing the launcher. Ignoring."); + } else { + _sawmill.Warning($"Received link \"{message.Url}\" which is not supported. Ignoring."); + } + } + private void RxFtp(MsgFtp message) { _dreamResource.LoadResourceAsync(message.ResourceId, async resource => { // TODO: Default the filename to message.SuggestedName diff --git a/OpenDreamClient/Interface/DreamStylesheet.cs b/OpenDreamClient/Interface/DreamStylesheet.cs index 3bf0de0a3c..629169934f 100644 --- a/OpenDreamClient/Interface/DreamStylesheet.cs +++ b/OpenDreamClient/Interface/DreamStylesheet.cs @@ -169,7 +169,15 @@ public static Stylesheet Make() { // LineEdit Element() // background color - .Prop(LineEdit.StylePropertyStyleBox, new StyleBoxFlat{ BackgroundColor = Color.FromHex("#D3B5B5"), BorderThickness = new Thickness(1), BorderColor = Color.FromHex("#abadb3")}) + .Prop(LineEdit.StylePropertyStyleBox, new StyleBoxFlat { + BackgroundColor = Color.White, + BorderThickness = new Thickness(2, 2, 1, 1), + BorderColor = Color.FromHex("#abadb3"), + ContentMarginLeftOverride = 2 + 2, + ContentMarginTopOverride = 2 + 2, + ContentMarginRightOverride = 2 + 1, + ContentMarginBottomOverride = 2 + 1, + }) // default font color .Prop("font-color", Color.Black) .Prop("cursor-color", Color.Black), @@ -182,9 +190,36 @@ public static Stylesheet Make() { Element().Pseudo(LineEdit.StylePseudoClassPlaceholder) .Prop("font-color", Color.FromHex("#7d7d7d")), + // TextEdit + Element() + // default font color + .Prop("font-color", Color.Black) + .Prop("cursor-color", Color.Black), + // ItemList selected item Element() - .Prop(ItemList.StylePropertySelectedItemBackground, new StyleBoxFlat { BackgroundColor = Color.Blue }), + .Prop(ItemList.StylePropertyBackground, new StyleBoxFlat { + BackgroundColor = Color.White, + BorderThickness = new Thickness(2, 2, 1, 1), // pretend shadow effect + BorderColor = Color.FromHex("#abadb3") + }) + .Prop(ItemList.StylePropertyItemBackground, new StyleBoxFlat { + ContentMarginLeftOverride = 4, + ContentMarginRightOverride = 8, + Padding = new Thickness(2, 2, 1, 1) + }) + .Prop(ItemList.StylePropertySelectedItemBackground, new StyleBoxFlat { + BackgroundColor = Color.FromHex("#0000aa99"), + // this is INTENDED do not make the overrides a padding + ContentMarginLeftOverride = 4, + ContentMarginRightOverride = 8, + Padding = new Thickness(2, 2, 1, 1) + }) + .Prop(ItemList.StylePropertyDisabledItemBackground, new StyleBoxFlat { + ContentMarginLeftOverride = 4, + ContentMarginRightOverride = 8, + Padding = new Thickness(2, 2, 1, 1) + }), // TabContainer Element() @@ -206,6 +241,37 @@ public static Stylesheet Make() { .Prop(Slider.StylePropertyForeground, new StyleBoxFlat { BackgroundColor = Color.LightGray, BorderThickness = new Thickness(1), BorderColor = Color.Black}) .Prop(Slider.StylePropertyGrabber, new StyleBoxFlat { BackgroundColor = Color.Transparent, BorderThickness = new Thickness(1), BorderColor = Color.Black, ContentMarginLeftOverride=10, ContentMarginRightOverride=10}) .Prop(Slider.StylePropertyFill, new StyleBoxFlat { BackgroundColor = Color.Transparent, BorderThickness = new Thickness(0), BorderColor = Color.Black}), + + // that thing on the top, some might say "topbar" + Element() + .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat { + Padding = new Thickness(1) + }), + + new StyleRule(new SelectorChild( + new SelectorElement(typeof(MenuBar.MenuTopButton), null, null, null), + new SelectorElement(typeof(Label), null, null, null)), + [ + new StyleProperty("font", notoSansFont10) + ]), + + Element() + .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat { + ContentMarginLeftOverride = 3, + ContentMarginRightOverride = 3, + ContentMarginTopOverride = 2, + ContentMarginBottomOverride = 2, + }), + + Element() + .Pseudo(MenuBar.MenuTopButton.StylePseudoClassHover) + .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat { + BackgroundColor = Color.FromHex("#bfbfbf"), + ContentMarginLeftOverride = 3, + ContentMarginRightOverride = 3, + ContentMarginTopOverride = 2, + ContentMarginBottomOverride = 2, + }), }); } } diff --git a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs index 734137f830..ebba9c9ccd 100644 --- a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs @@ -1,6 +1,7 @@ using OpenDreamClient.Interface.Controls; -using OpenDreamClient.Interface.Descriptors; using OpenDreamShared.Dream; +using OpenDreamShared.Network.Messages; +using Robust.Shared.Network; using Robust.Shared.Timing; namespace OpenDreamClient.Interface; @@ -19,7 +20,10 @@ public sealed class DummyDreamInterfaceManager : IDreamInterfaceManager { public ViewRange View => new(5); public bool ShowPopupMenus => true; + [Dependency] private readonly IClientNetManager _netManager = default!; + public void Initialize() { + _netManager.RegisterNetMessage((_) => _netManager.ClientSendMessage(new MsgAckLoadInterface())); } public void FrameUpdate(FrameEventArgs frameEventArgs) { diff --git a/OpenDreamClient/Interface/Html/HtmlParser.cs b/OpenDreamClient/Interface/Html/HtmlParser.cs index c2394d8af9..ef434e83c2 100644 --- a/OpenDreamClient/Interface/Html/HtmlParser.cs +++ b/OpenDreamClient/Interface/Html/HtmlParser.cs @@ -8,6 +8,7 @@ public static class HtmlParser { private const string TagNotClosedError = "HTML tag was not closed"; private static readonly ISawmill Sawmill; + private static readonly HashSet WarnedAttributes = new(); static HtmlParser() { Sawmill = IoCManager.Resolve().GetSawmill("opendream.html_parser"); @@ -24,6 +25,9 @@ void SkipWhitespace() { } void PushCurrentText() { + if (currentText.Length == 0) + return; + appendTo.AddText(currentText.ToString()); currentText.Clear(); } @@ -83,9 +87,6 @@ void PushCurrentText() { appendTo.PushTag(new MarkupNode(tagType, null, ParseAttributes(attributes)), selfClosing: attributes[^1] == "/"); } - break; - case '\n': - appendTo.PushNewline(); break; case '&': // HTML named/numbered entity @@ -122,6 +123,10 @@ void PushCurrentText() { } } + break; + case '\n': + PushCurrentText(); + appendTo.PushNewline(); break; default: currentText.Append(c); @@ -165,7 +170,8 @@ private static Dictionary ParseAttributes(string[] attr parameter = new(color); break; default: - Sawmill.Debug($"Unimplemented HTML attribute \"{attributeName}\""); + if (WarnedAttributes.Add(attributeName)) + Sawmill.Debug($"Unimplemented HTML attribute \"{attributeName}\""); continue; } diff --git a/OpenDreamClient/Interface/Prompts/ListPrompt.cs b/OpenDreamClient/Interface/Prompts/ListPrompt.cs index 8f6b63cf33..e9c2b04943 100644 --- a/OpenDreamClient/Interface/Prompts/ListPrompt.cs +++ b/OpenDreamClient/Interface/Prompts/ListPrompt.cs @@ -10,10 +10,13 @@ internal sealed class ListPrompt : InputWindow { public ListPrompt(string title, string message, string defaultValue, bool canCancel, string[] values, Action? onClose) : base(title, message, canCancel, onClose) { - _itemList = new(); + _itemList = new ItemList(); + // don't make it as long as the width + // really this should check for fontHeight not hacky const 24 + MinHeight = Math.Clamp(100 + (24 * values.Length), MinHeight, MinWidth); - bool foundDefault = false; - foreach (string value in values) { + var foundDefault = false; + foreach (var value in values) { ItemList.Item item = new(_itemList) { Text = value }; diff --git a/OpenDreamClient/Interface/Prompts/TextPrompt.cs b/OpenDreamClient/Interface/Prompts/TextPrompt.cs index 67da9fd3b0..8f189ee3b3 100644 --- a/OpenDreamClient/Interface/Prompts/TextPrompt.cs +++ b/OpenDreamClient/Interface/Prompts/TextPrompt.cs @@ -8,6 +8,8 @@ internal sealed class TextPrompt : InputWindow { public TextPrompt(string title, string message, string defaultValue, bool canCancel, Action? onClose) : base(title, message, canCancel, onClose) { + MinHeight = 100; + _textEdit = new LineEdit { Text = defaultValue, VerticalAlignment = VAlignment.Top diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index c9c7cdf28a..0786c1c4e8 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -10,10 +10,11 @@ namespace OpenDreamClient.Rendering; internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { - private Dictionary _appearances = new(); - private readonly Dictionary>> _appearanceLoadCallbacks = new(); - private readonly Dictionary _turfIcons = new(); + private Dictionary _appearances = new(); + private readonly Dictionary>> _appearanceLoadCallbacks = new(); + private readonly Dictionary _turfIcons = new(); private readonly Dictionary _filterShaders = new(); + private bool _receivedAllAppearancesMsg; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IDreamResourceManager _dreamResourceManager = default!; @@ -24,29 +25,42 @@ internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { public override void Initialize() { SubscribeNetworkEvent(OnNewAppearance); + SubscribeNetworkEvent(e => _appearances.Remove(e.AppearanceId)); SubscribeNetworkEvent(OnAnimation); SubscribeLocalEvent(OnWorldAABB); } public override void Shutdown() { + _receivedAllAppearancesMsg = false; _appearances.Clear(); _appearanceLoadCallbacks.Clear(); _turfIcons.Clear(); + _filterShaders.Clear(); } - public void SetAllAppearances(Dictionary appearances) { + public void SetAllAppearances(Dictionary appearances) { _appearances = appearances; + _receivedAllAppearancesMsg = true; - foreach (KeyValuePair pair in _appearances) { - if (_appearanceLoadCallbacks.TryGetValue(pair.Key, out var callbacks)) { - foreach (var callback in callbacks) callback(pair.Value); - } + //need to do this because all overlays can't be resolved until the whole appearance table is populated + foreach(KeyValuePair pair in _appearances) { + pair.Value.ResolveOverlays(this); + } + + // Callbacks called in another pass to ensure all appearances are initialized first + foreach (var callbackPair in _appearanceLoadCallbacks) { + if (!_appearances.TryGetValue(callbackPair.Key, out var appearance)) + continue; + + foreach (var callback in callbackPair.Value) + callback(appearance); } } - public void LoadAppearance(int appearanceId, Action loadCallback) { - if (_appearances.TryGetValue(appearanceId, out var appearance)) { + public void LoadAppearance(uint appearanceId, Action loadCallback) { + if (_appearances.TryGetValue(appearanceId, out var appearance) && _receivedAllAppearancesMsg) { loadCallback(appearance); + return; } if (!_appearanceLoadCallbacks.ContainsKey(appearanceId)) { @@ -56,8 +70,8 @@ public void LoadAppearance(int appearanceId, Action loadCallback _appearanceLoadCallbacks[appearanceId].Add(loadCallback); } - public DreamIcon GetTurfIcon(int turfId) { - int appearanceId = turfId - 1; + public DreamIcon GetTurfIcon(uint turfId) { + uint appearanceId = turfId; if (!_turfIcons.TryGetValue(appearanceId, out var icon)) { icon = new DreamIcon(_spriteSystem.RenderTargetPool, _gameTiming, _clyde, this, appearanceId); @@ -67,22 +81,35 @@ public DreamIcon GetTurfIcon(int turfId) { return icon; } - private void OnNewAppearance(NewAppearanceEvent e) { - _appearances[e.AppearanceId] = e.Appearance; + public void OnNewAppearance(NewAppearanceEvent e) { + uint appearanceId = e.Appearance.MustGetId(); + _appearances[appearanceId] = e.Appearance; + + // If we haven't received the MsgAllAppearances yet, leave this initialization for later + if (_receivedAllAppearancesMsg) { + _appearances[appearanceId].ResolveOverlays(this); - if (_appearanceLoadCallbacks.TryGetValue(e.AppearanceId, out var callbacks)) { - foreach (var callback in callbacks) callback(e.Appearance); + if (_appearanceLoadCallbacks.TryGetValue(appearanceId, out var callbacks)) { + foreach (var callback in callbacks) callback(_appearances[appearanceId]); + } } } private void OnAnimation(AnimationEvent e) { - EntityUid ent = _entityManager.GetEntity(e.Entity); - if (!_entityManager.TryGetComponent(ent, out var sprite)) - return; - - LoadAppearance(e.TargetAppearanceId, targetAppearance => { - sprite.Icon.StartAppearanceAnimation(targetAppearance, e.Duration, e.Easing, e.Loop, e.Flags, e.Delay, e.ChainAnim); - }); + if(e.Entity == NetEntity.Invalid && e.TurfId is not null) { //it's a turf or area + if(_turfIcons.TryGetValue(e.TurfId.Value-1, out var turfIcon)) + LoadAppearance(e.TargetAppearanceId, targetAppearance => { + turfIcon.StartAppearanceAnimation(targetAppearance, e.Duration, e.Easing, e.Loop, e.Flags, e.Delay, e.ChainAnim); + }); + } else { //image or movable + EntityUid ent = _entityManager.GetEntity(e.Entity); + if (!_entityManager.TryGetComponent(ent, out var sprite)) + return; + + LoadAppearance(e.TargetAppearanceId, targetAppearance => { + sprite.Icon.StartAppearanceAnimation(targetAppearance, e.Duration, e.Easing, e.Loop, e.Flags, e.Delay, e.ChainAnim); + }); + } } private void OnWorldAABB(EntityUid uid, DMISpriteComponent comp, ref WorldAABBEvent e) { @@ -197,4 +224,12 @@ public ShaderInstance GetFilterShader(DreamFilter filter, Dictionary(uid, out var transform); + if (!_entityManager.TryGetComponent(uid, out var transform)) + return; + _lookupSystem.FindAndAddToEntityTree(uid, xform: transform); } diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index e79761ab09..fc3be2d189 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -32,7 +32,7 @@ public int AnimationFrame { } [ViewVariables] - public IconAppearance? Appearance { + public ImmutableAppearance? Appearance { get => CalculateAnimatedAppearance(); private set { if (_appearance?.Equals(value) is true) @@ -42,7 +42,12 @@ private set { UpdateIcon(); } } - private IconAppearance? _appearance; + + private ImmutableAppearance? _appearance; + + //acts as a cache for the mutable appearance, so we don't have to ToMutable() every frame + private MutableAppearance? _animatedAppearance; + private AtomDirection _direction; // TODO: We could cache these per-appearance instead of per-atom public IRenderTexture? CachedTexture { @@ -65,7 +70,7 @@ private set { private bool _animationComplete; private IRenderTexture? _cachedTexture; - public DreamIcon(RenderTargetPool renderTargetPool, IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem appearanceSystem, int appearanceId, + public DreamIcon(RenderTargetPool renderTargetPool, IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem appearanceSystem, uint appearanceId, AtomDirection? parentDir = null) : this(renderTargetPool, gameTiming, clyde, appearanceSystem) { SetAppearance(appearanceId, parentDir); } @@ -88,12 +93,12 @@ public void Dispose() { return CachedTexture.Texture; _textureDirty = false; - frame = DMI.GetState(Appearance.IconState)?.GetFrames(Appearance.Direction)[animationFrame]; + frame = DMI.GetState(Appearance.IconState)?.GetFrames(_direction)[animationFrame]; } else { frame = textureOverride; } - var canSkipFullRender = Appearance?.Filters.Count is 0 or null && + var canSkipFullRender = Appearance?.Filters.Length is 0 or null && iconMetaData.ColorToApply == Color.White && iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity) && iconMetaData.AlphaToApply.Equals(1.0f); @@ -104,13 +109,20 @@ public void Dispose() { TextureRenderOffset = Vector2.Zero; return frame; } else { + if (textureOverride is not null) { //no caching in the presence of overrides + var texture = FullRenderTexture(viewOverlay, handle, iconMetaData, frame); + + renderTargetPool.ReturnAtEndOfFrame(texture); + return texture.Texture; + } + CachedTexture = FullRenderTexture(viewOverlay, handle, iconMetaData, frame); } return CachedTexture?.Texture; } - public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { + public void SetAppearance(uint? appearanceId, AtomDirection? parentDir = null) { // End any animations that are currently happening // Note that this isn't faithful to the original behavior EndAppearanceAnimation(null); @@ -122,9 +134,9 @@ public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { appearanceSystem.LoadAppearance(appearanceId.Value, appearance => { if (parentDir != null && appearance.InheritsDirection) { - appearance = new IconAppearance(appearance) { - Direction = parentDir.Value - }; + _direction = parentDir.Value; + } else { + _direction = appearance.Direction; } Appearance = appearance; @@ -132,7 +144,7 @@ public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { } //three things to do here, chained animations, loops and parallel animations - public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan duration, AnimationEasing easing, int loops, AnimationFlags flags, int delay, bool chainAnim) { + public void StartAppearanceAnimation(ImmutableAppearance endingAppearance, TimeSpan duration, AnimationEasing easing, int loops, AnimationFlags flags, int delay, bool chainAnim) { _appearance = CalculateAnimatedAppearance(); //Animation starts from the current animated appearance DateTime start = DateTime.Now; if(!chainAnim) @@ -156,7 +168,7 @@ public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan d _appearanceAnimations[i] = lastAnim; break; } - + _appearanceAnimations.Add(new AppearanceAnimation(start, duration, endingAppearance, easing, flags, delay, true)); } @@ -205,7 +217,7 @@ private void UpdateAnimation() { DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(Appearance.IconState); if(dmiState == null) return; - DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(Appearance.Direction); + DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(_direction); if (frames.Length <= 1) return; @@ -232,12 +244,14 @@ private void UpdateAnimation() { DirtyTexture(); } - private IconAppearance? CalculateAnimatedAppearance() { - if (_appearanceAnimations == null || _appearance == null) + private ImmutableAppearance? CalculateAnimatedAppearance() { + if (_appearanceAnimations == null || _appearanceAnimations.Count == 0 || _appearance == null) { + _animatedAppearance = null; //null it if _appearanceAnimations is empty return _appearance; + } _textureDirty = true; //if we have animations, we need to recalculate the texture - IconAppearance appearance = new IconAppearance(_appearance); + _animatedAppearance = _appearance.ToMutable(); List? toRemove = null; List? toReAdd = null; for(int i = 0; i < _appearanceAnimations.Count; i++) { @@ -249,7 +263,7 @@ private void UpdateAnimation() { float timeFactor = Math.Clamp((float)(DateTime.Now - animation.Start).Ticks / animation.Duration.Ticks, 0.0f, 1.0f); float factor = 0; if((animation.Easing & AnimationEasing.EaseIn) != 0) - timeFactor = timeFactor/2.0f; + timeFactor /= 2.0f; if((animation.Easing & AnimationEasing.EaseOut) != 0) timeFactor = 0.5f+timeFactor/2.0f; @@ -280,6 +294,7 @@ private void UpdateAnimation() { bounce -= 2.625f; factor = MathF.Pow(bounce, 2) + 0.984375f; } + break; case AnimationEasing.Elastic: //http://www.java2s.com/example/csharp/system/easing-equation-function-for-an-elastic-exponentially-decaying-sine-w.html with d=1, s=pi/2, c=2, b = -1 factor = MathF.Pow(2, -10 * timeFactor) * MathF.Sin((timeFactor - MathF.PI/2.0f) * (2.0f*MathF.PI/0.3f)) + 1.0f; @@ -295,7 +310,7 @@ private void UpdateAnimation() { break; } - IconAppearance endAppearance = animation.EndAppearance; + var endAppearance = animation.EndAppearance; //non-smooth animations /* @@ -307,27 +322,20 @@ private void UpdateAnimation() { suffix */ - if (endAppearance.Direction != _appearance.Direction) { - appearance.Direction = endAppearance.Direction; - } - if (endAppearance.Icon != _appearance.Icon) { - appearance.Icon = endAppearance.Icon; - } - if (endAppearance.IconState != _appearance.IconState) { - appearance.IconState = endAppearance.IconState; - } - if (endAppearance.Invisibility != _appearance.Invisibility) { - appearance.Invisibility = endAppearance.Invisibility; - } - /* TODO maptext - if (endAppearance.MapText != _appearance.MapText) { - appearance.MapText = endAppearance.MapText; - } - */ + if (endAppearance.Direction != _appearance.Direction) + _animatedAppearance.Direction = endAppearance.Direction; + if (endAppearance.Icon != _appearance.Icon) + _animatedAppearance.Icon = endAppearance.Icon; + if (endAppearance.IconState != _appearance.IconState) + _animatedAppearance.IconState = endAppearance.IconState; + if (endAppearance.Invisibility != _appearance.Invisibility) + _animatedAppearance.Invisibility = endAppearance.Invisibility; + if (endAppearance.Maptext != _appearance.Maptext) + _animatedAppearance.Maptext = endAppearance.Maptext; + /* TODO suffix - if (endAppearance.Suffix != _appearance.Suffix) { + if (endAppearance.Suffix != _appearance.Suffix) appearance.Suffix = endAppearance.Suffix; - } */ //smooth animation properties @@ -344,11 +352,11 @@ private void UpdateAnimation() { */ if (endAppearance.Alpha != _appearance.Alpha) { - appearance.Alpha = (byte)Math.Clamp(((1-factor) * _appearance.Alpha) + (factor * endAppearance.Alpha), 0, 255); + _animatedAppearance.Alpha = (byte)Math.Clamp(((1-factor) * _appearance.Alpha) + (factor * endAppearance.Alpha), 0, 255); } if (endAppearance.Color != _appearance.Color) { - appearance.Color = Color.FromSrgb(new Color( + _animatedAppearance.Color = Color.FromSrgb(new Color( Math.Clamp(((1-factor) * _appearance.Color.R) + (factor * endAppearance.Color.R), 0, 1), Math.Clamp(((1-factor) * _appearance.Color.G) + (factor * endAppearance.Color.G), 0, 1), Math.Clamp(((1-factor) * _appearance.Color.B) + (factor * endAppearance.Color.B), 0, 1), @@ -357,12 +365,11 @@ private void UpdateAnimation() { } if (!endAppearance.ColorMatrix.Equals(_appearance.ColorMatrix)){ - ColorMatrix.Interpolate(ref _appearance.ColorMatrix, ref endAppearance.ColorMatrix, factor, out appearance.ColorMatrix); + ColorMatrix.Interpolate(in _appearance.ColorMatrix, in endAppearance.ColorMatrix, factor, out _animatedAppearance.ColorMatrix); } - - if (endAppearance.GlideSize != _appearance.GlideSize) { - appearance.GlideSize = ((1-factor) * _appearance.GlideSize) + (factor * endAppearance.GlideSize); + if (!endAppearance.GlideSize.Equals(_appearance.GlideSize)) { + _animatedAppearance.GlideSize = ((1-factor) * _appearance.GlideSize) + (factor * endAppearance.GlideSize); } /* TODO infraluminosity @@ -371,8 +378,8 @@ private void UpdateAnimation() { } */ - if (endAppearance.Layer != _appearance.Layer) { - appearance.Layer = ((1-factor) * _appearance.Layer) + (factor * endAppearance.Layer); + if (!endAppearance.Layer.Equals(_appearance.Layer)) { + _animatedAppearance.Layer = ((1-factor) * _appearance.Layer) + (factor * endAppearance.Layer); } /* TODO luminosity @@ -381,45 +388,41 @@ private void UpdateAnimation() { } */ - /* TODO maptext - if (endAppearance.MapTextWidth != _appearance.MapTextWidth) { - appearance.MapTextWidth = (ushort)Math.Clamp(((1-factor) * _appearance.MapTextWidth) + (factor * endAppearance.MapTextWidth), 0, 65535); - } + if (endAppearance.MaptextSize != _appearance.MaptextSize) { + Vector2 startingOffset = _appearance.MaptextSize; + Vector2 newMaptextSize = Vector2.Lerp(startingOffset, endAppearance.MaptextSize, factor); - if (endAppearance.MapTextHeight != _appearance.MapTextHeight) { - appearance.MapTextHeight = (ushort)Math.Clamp(((1-factor) * _appearance.MapTextHeight) + (factor * endAppearance.MapTextHeight), 0, 65535); + _animatedAppearance.MaptextSize = (Vector2i)newMaptextSize; } - if (endAppearance.MapTextX != _appearance.MapTextX) { - appearance.MapTextX = (short)Math.Clamp(((1-factor) * _appearance.MapTextX) + (factor * endAppearance.MapTextX), -32768, 32767); - } + if (endAppearance.MaptextOffset != _appearance.MaptextOffset) { + Vector2 startingOffset = _appearance.MaptextOffset; + Vector2 newMaptextOffset = Vector2.Lerp(startingOffset, endAppearance.MaptextOffset, factor); - if (endAppearance.MapTextY != _appearance.MapTextY) { - appearance.MapTextY = (short)Math.Clamp(((1-factor) * _appearance.MapTextY) + (factor * endAppearance.MapTextY), -32768, 32767); + _animatedAppearance.MaptextOffset = (Vector2i)newMaptextOffset; } - */ if (endAppearance.PixelOffset != _appearance.PixelOffset) { - Vector2 startingOffset = appearance.PixelOffset; - Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset, 1.0f-factor); + Vector2 startingOffset = _appearance.PixelOffset; + Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset, factor); - appearance.PixelOffset = (Vector2i)newPixelOffset; + _animatedAppearance.PixelOffset = (Vector2i)newPixelOffset; } if (endAppearance.PixelOffset2 != _appearance.PixelOffset2) { - Vector2 startingOffset = appearance.PixelOffset2; - Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset2, 1.0f-factor); + Vector2 startingOffset = _appearance.PixelOffset2; + Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset2, factor); - appearance.PixelOffset2 = (Vector2i)newPixelOffset; + _animatedAppearance.PixelOffset2 = (Vector2i)newPixelOffset; } if (!endAppearance.Transform.SequenceEqual(_appearance.Transform)) { - appearance.Transform[0] = (1.0f-factor)*_appearance.Transform[0] + (factor * endAppearance.Transform[0]); - appearance.Transform[1] = (1.0f-factor)*_appearance.Transform[1] + (factor * endAppearance.Transform[1]); - appearance.Transform[2] = (1.0f-factor)*_appearance.Transform[2] + (factor * endAppearance.Transform[2]); - appearance.Transform[3] = (1.0f-factor)*_appearance.Transform[3] + (factor * endAppearance.Transform[3]); - appearance.Transform[4] = (1.0f-factor)*_appearance.Transform[4] + (factor * endAppearance.Transform[4]); - appearance.Transform[5] = (1.0f-factor)*_appearance.Transform[5] + (factor * endAppearance.Transform[5]); + _animatedAppearance.Transform[0] = (1.0f-factor)*_appearance.Transform[0] + (factor * endAppearance.Transform[0]); + _animatedAppearance.Transform[1] = (1.0f-factor)*_appearance.Transform[1] + (factor * endAppearance.Transform[1]); + _animatedAppearance.Transform[2] = (1.0f-factor)*_appearance.Transform[2] + (factor * endAppearance.Transform[2]); + _animatedAppearance.Transform[3] = (1.0f-factor)*_appearance.Transform[3] + (factor * endAppearance.Transform[3]); + _animatedAppearance.Transform[4] = (1.0f-factor)*_appearance.Transform[4] + (factor * endAppearance.Transform[4]); + _animatedAppearance.Transform[5] = (1.0f-factor)*_appearance.Transform[5] + (factor * endAppearance.Transform[5]); } if (timeFactor >= 1f) { @@ -437,7 +440,6 @@ private void UpdateAnimation() { AppearanceAnimation repeatAnimation = new AppearanceAnimation(start, animation.Duration, animation.EndAppearance, animation.Easing, animation.Flags, animation.Delay, animation.LastInSequence); toReAdd.Add(repeatAnimation); } - } } @@ -451,7 +453,7 @@ private void UpdateAnimation() { _appearanceAnimations.Add(animation); } - return appearance; + return new(_animatedAppearance, null); //one of the very few times it's okay to do this. } private void UpdateIcon() { @@ -475,16 +477,16 @@ private void UpdateIcon() { } Overlays.Clear(); - foreach (int overlayId in Appearance.Overlays) { - DreamIcon overlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, overlayId, Appearance.Direction); + foreach (var overlayAppearance in Appearance.Overlays) { + DreamIcon overlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, overlayAppearance.MustGetId(), _direction); overlay.SizeChanged += CheckSizeChange; Overlays.Add(overlay); } Underlays.Clear(); - foreach (int underlayId in Appearance.Underlays) { - DreamIcon underlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, underlayId, Appearance.Direction); + foreach (var underlayAppearance in Appearance.Underlays) { + DreamIcon underlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, underlayAppearance.MustGetId(), _direction); underlay.SizeChanged += CheckSizeChange; Underlays.Add(underlay); @@ -552,10 +554,10 @@ private void DirtyTexture() { CachedTexture = null; } - private struct AppearanceAnimation(DateTime start, TimeSpan duration, IconAppearance endAppearance, AnimationEasing easing, AnimationFlags flags, int delay, bool lastInSequence) { + private struct AppearanceAnimation(DateTime start, TimeSpan duration, ImmutableAppearance endAppearance, AnimationEasing easing, AnimationFlags flags, int delay, bool lastInSequence) { public readonly DateTime Start = start; public readonly TimeSpan Duration = duration; - public readonly IconAppearance EndAppearance = endAppearance; + public readonly ImmutableAppearance EndAppearance = endAppearance; public readonly AnimationEasing Easing = easing; public readonly AnimationFlags Flags = flags; public readonly int Delay = delay; diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 55eb129c44..526276bf1e 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -2,7 +2,6 @@ using OpenDreamClient.Interface; using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Shared.Enums; using Robust.Shared.Map; using OpenDreamShared.Dream; using Robust.Shared.Console; @@ -13,6 +12,9 @@ using Robust.Shared.Profiling; using Vector3 = Robust.Shared.Maths.Vector3; using Matrix3x2 = System.Numerics.Matrix3x2; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.RichText; +using Robust.Shared.Enums; namespace OpenDreamClient.Rendering; @@ -20,6 +22,8 @@ namespace OpenDreamClient.Rendering; /// Overlay for rendering world atoms /// internal sealed class DreamViewOverlay : Overlay { + public static ShaderInstance ColorInstance = default!; + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld; public bool ScreenOverlayEnabled = true; @@ -40,6 +44,8 @@ internal sealed class DreamViewOverlay : Overlay { [Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly ProfManager _prof = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly MarkupTagManager _tagManager = default!; private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.view"); @@ -58,13 +64,13 @@ internal sealed class DreamViewOverlay : Overlay { private readonly List _spriteContainer = new(); private readonly Dictionary _blendModeInstances; - public static ShaderInstance ColorInstance = default!; private IRenderTexture? _mouseMapRenderTarget; private IRenderTexture? _baseRenderTarget; private readonly RenderTargetPool _renderTargetPool; private readonly Stack _rendererMetaDataRental = new(); private readonly Stack _rendererMetaDataToReturn = new(); + private readonly MapTextRenderer _mapTextRenderer; private static readonly Matrix3x2 FlipMatrix = Matrix3x2.Identity with { M22 = -1 @@ -99,6 +105,8 @@ public DreamViewOverlay(RenderTargetPool renderTargetPool, TransformSystem trans {BlendMode.Multiply, _protoManager.Index("blend_multiply").InstanceUnique()}, //BLEND_MULTIPLY {BlendMode.InsertOverlay, _protoManager.Index("blend_inset_overlay").InstanceUnique()} //BLEND_INSET_OVERLAY //TODO }; + + _mapTextRenderer = new(_resourceCache, _tagManager); } protected override void Draw(in OverlayDrawArgs args) { @@ -169,7 +177,7 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) } //handles underlays, overlays, appearance flags, images. Adds them to the result list, so they can be sorted and drawn with DrawIcon() - private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid uid, bool isScreen, ref int tieBreaker, List result, RendererMetaData? parentIcon = null, bool keepTogether = false, Vector3? turfCoords = null) { + private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid uid, bool isScreen, ref int tieBreaker, List result, sbyte seeVis, RendererMetaData? parentIcon = null, bool keepTogether = false, Vector3? turfCoords = null) { if (icon.Appearance is null) //in the event that appearance hasn't loaded yet return; @@ -201,7 +209,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u current.ColorMatrixToApply = icon.Appearance.ColorMatrix; } else { current.ColorToApply = parentIcon.ColorToApply * icon.Appearance.Color; - ColorMatrix.Multiply(ref parentIcon.ColorMatrixToApply, ref icon.Appearance.ColorMatrix, out current.ColorMatrixToApply); + ColorMatrix.Multiply(in parentIcon.ColorMatrixToApply, in icon.Appearance.ColorMatrix, out current.ColorMatrixToApply); } if ((icon.Appearance.AppearanceFlags & AppearanceFlags.ResetAlpha) != 0 || keepTogether) //RESET_ALPHA @@ -281,10 +289,10 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u tieBreaker++; if (!keepTogether || (underlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(underlay, current.Position, uid, isScreen, ref tieBreaker, result, current); + ProcessIconComponents(underlay, current.Position, uid, isScreen, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(underlay, current.Position, uid, isScreen, ref tieBreaker, current.KeepTogetherGroup, current, keepTogether); + ProcessIconComponents(underlay, current.Position, uid, isScreen, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -299,10 +307,10 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u tieBreaker++; if (!keepTogether || (overlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(overlay, current.Position, uid, isScreen, ref tieBreaker, result, current); + ProcessIconComponents(overlay, current.Position, uid, isScreen, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(overlay, current.Position, uid, isScreen, ref tieBreaker, current.KeepTogetherGroup, current, keepTogether); + ProcessIconComponents(overlay, current.Position, uid, isScreen, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -316,10 +324,11 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u continue; if(sprite.Icon.Appearance == null) continue; - if(sprite.Icon.Appearance.Override) + if(sprite.Icon.Appearance.Override) { current.MainIcon = sprite.Icon; - else - ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, current); + current.Position = current.Position + (sprite.Icon.Appearance.TotalPixelOffset / (float)EyeManager.PixelsPerMeter); + } else + ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, seeVis, current); } } @@ -327,14 +336,45 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u EntityUid visContentEntity = _entityManager.GetEntity(visContent); if (!_spriteQuery.TryGetComponent(visContentEntity, out var sprite)) continue; + var transform = _xformQuery.GetComponent(visContentEntity); + if (!sprite.IsVisible(transform, seeVis)) + continue; - ProcessIconComponents(sprite.Icon, position, visContentEntity, false, ref tieBreaker, result, current, keepTogether); + ProcessIconComponents(sprite.Icon, position, visContentEntity, false, ref tieBreaker, result, seeVis, current, keepTogether); // TODO: click uid should be set to current.uid again // TODO: vis_flags } - //TODO maptext - note colour + transform apply + //maptext is basically just an image of rendered text added as an overlay + if(icon.Appearance.Maptext != null){ //if has maptext + RendererMetaData maptext = RentRendererMetaData(); + maptext.MainIcon = icon; + maptext.Position = current.Position; + maptext.Uid = current.Uid; + maptext.ClickUid = current.Uid; + maptext.IsScreen = current.IsScreen; + tieBreaker++; + maptext.TieBreaker = tieBreaker; + maptext.Plane = current.Plane; + maptext.Layer = current.Layer; + maptext.RenderSource = null; + maptext.RenderTarget = null; + maptext.MouseOpacity = current.MouseOpacity; + maptext.TransformToApply = current.TransformToApply; + maptext.ColorToApply = current.ColorToApply; + maptext.ColorMatrixToApply = current.ColorMatrixToApply; + maptext.AlphaToApply = current.AlphaToApply; + maptext.BlendMode = current.BlendMode; + + maptext.AppearanceFlags = current.AppearanceFlags; + maptext.AppearanceFlags &= ~AppearanceFlags.PlaneMaster; //doesn't make sense for maptext + + maptext.Maptext = icon.Appearance.Maptext; + maptext.MaptextSize = icon.Appearance.MaptextSize; + maptext.Position += icon.Appearance.MaptextOffset/(float)EyeManager.PixelsPerMeter; + result.Add(maptext); + } //TODO particles - colour and transform don't apply? @@ -396,6 +436,21 @@ public void DrawIcon(DrawingHandleWorld handle, Vector2i renderTargetSize, Rende positionOffset -= ((ktSize/EyeManager.PixelsPerMeter) - Vector2.One) * new Vector2(0.5f); //correct for KT group texture offset } + //Maptext + if(iconMetaData.Maptext != null) { + var maptextSize = iconMetaData.MaptextSize!.Value; + if (maptextSize.X == 0) + maptextSize.X = 32; + if (maptextSize.Y == 0) + maptextSize.Y = 32; + + var renderTarget = _renderTargetPool.Rent(maptextSize); + + _mapTextRenderer.RenderToTarget(handle, renderTarget, iconMetaData.Maptext); + _renderTargetPool.ReturnAtEndOfFrame(renderTarget); + iconMetaData.TextureOverride = renderTarget.Texture; + } + var frame = iconMetaData.GetTexture(this, handle); var pixelPosition = (iconMetaData.Position + positionOffset) * EyeManager.PixelsPerMeter; @@ -458,7 +513,7 @@ private DreamPlane GetPlane(int planeIndex, Vector2i viewportSize) { plane = new(renderTarget); _planes.Add(planeIndex, plane); - _sawmill.Info($"Created plane {planeIndex}"); + _sawmill.Verbose($"Created plane {planeIndex}"); return plane; } @@ -561,7 +616,7 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { // Gather up all the data the view algorithm needs while (tileRefs.MoveNext(out var tileRef)) { var delta = tileRef.GridIndices - eyeTile.GridIndices; - var appearance = _appearanceSystem.GetTurfIcon(tileRef.Tile.TypeId).Appearance; + var appearance = _appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId).Appearance; if (appearance == null) continue; @@ -606,7 +661,7 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { return _tileInfo; } - private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, int seeVis, SightFlags sight, Box2 worldAABB) { + private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, sbyte seeVis, SightFlags sight, Box2 worldAABB) { _spriteContainer.Clear(); // This exists purely because the tiebreaker var needs to exist somewhere @@ -627,7 +682,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU tValue = 0; //pass the turf coords for client.images lookup Vector3 turfCoords = new Vector3(tileRef.X, tileRef.Y, (int) worldPos.MapId); - ProcessIconComponents(_appearanceSystem.GetTurfIcon(tileRef.Tile.TypeId), worldPos.Position - Vector2.One, EntityUid.Invalid, false, ref tValue, _spriteContainer, turfCoords: turfCoords); + ProcessIconComponents(_appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId), worldPos.Position - Vector2.One, EntityUid.Invalid, false, ref tValue, _spriteContainer, seeVis, turfCoords: turfCoords); } // Visible entities @@ -656,7 +711,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU } tValue = 0; - ProcessIconComponents(sprite.Icon, worldPos - new Vector2(0.5f), entity, false, ref tValue, _spriteContainer); + ProcessIconComponents(sprite.Icon, worldPos - new Vector2(0.5f), entity, false, ref tValue, _spriteContainer, seeVis); } } @@ -672,12 +727,13 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU if (sprite.ScreenLocation.MapControl != null) // Don't render screen objects meant for other map controls continue; - Vector2 position = sprite.ScreenLocation.GetViewPosition(worldAABB.BottomLeft, _interfaceManager.View, EyeManager.PixelsPerMeter); + Vector2i dmiIconSize = sprite.Icon.DMI?.IconSize ?? new(EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter); + Vector2 position = sprite.ScreenLocation.GetViewPosition(worldAABB.BottomLeft, _interfaceManager.View, EyeManager.PixelsPerMeter, dmiIconSize); Vector2 iconSize = sprite.Icon.DMI == null ? Vector2.Zero : sprite.Icon.DMI.IconSize / (float)EyeManager.PixelsPerMeter; for (int x = 0; x < sprite.ScreenLocation.RepeatX; x++) { for (int y = 0; y < sprite.ScreenLocation.RepeatY; y++) { tValue = 0; - ProcessIconComponents(sprite.Icon, position + iconSize * new Vector2(x, y), uid, true, ref tValue, _spriteContainer); + ProcessIconComponents(sprite.Icon, position + iconSize * new Vector2(x, y), uid, true, ref tValue, _spriteContainer, seeVis); } } } @@ -783,7 +839,6 @@ public static Matrix3x2 CalculateDrawingMatrix(Matrix3x2 transform, Vector2 pixe * Matrix3x2.CreateTranslation(frameSize/2) //translate back to original position * Matrix3x2.CreateScale(scaleFactors) //scale * CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition-((scaleFactors-Vector2.One)*frameSize/2)); //flip and apply scale-corrected translation - } } @@ -807,6 +862,8 @@ internal sealed class RendererMetaData : IComparable { public BlendMode BlendMode; public MouseOpacity MouseOpacity; public Texture? TextureOverride; + public string? Maptext; + public Vector2i? MaptextSize; public bool IsPlaneMaster => (AppearanceFlags & AppearanceFlags.PlaneMaster) != 0; public bool HasRenderSource => !string.IsNullOrEmpty(RenderSource); @@ -836,6 +893,8 @@ public void Reset() { BlendMode = BlendMode.Default; MouseOpacity = MouseOpacity.Transparent; TextureOverride = null; + Maptext = null; + MaptextSize = null; } public Texture? GetTexture(DreamViewOverlay viewOverlay, DrawingHandleWorld handle) { diff --git a/OpenDreamClient/Rendering/MapTextRenderer.cs b/OpenDreamClient/Rendering/MapTextRenderer.cs new file mode 100644 index 0000000000..d1b4aa6fd0 --- /dev/null +++ b/OpenDreamClient/Rendering/MapTextRenderer.cs @@ -0,0 +1,287 @@ +using System.Diagnostics.Contracts; +using System.Text; +using OpenDreamClient.Interface.Html; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.RichText; +using Robust.Shared.Utility; + +namespace OpenDreamClient.Rendering; + +/// +/// Helper for rendering maptext to a render target. +/// Adapted from RobustToolbox's RichTextEntry. +/// +public sealed class MapTextRenderer(IResourceCache resourceCache, MarkupTagManager tagManager) { + private const float Scale = 1f; + + private readonly VectorFont _defaultFont = + new(resourceCache.GetResource("/Fonts/NotoSans-Regular.ttf"), 8); + + private readonly Color _defaultColor = Color.White; + + // TODO: This is probably unoptimal and could cache a lot of things between frames + public void RenderToTarget(DrawingHandleWorld handle, IRenderTexture texture, string maptext) { + handle.RenderInRenderTarget(texture, () => { + handle.SetTransform(DreamViewOverlay.CreateRenderTargetFlipMatrix(texture.Size, Vector2.Zero)); + + var message = new FormattedMessage(); + HtmlParser.Parse(maptext, message); + + var (height, lineBreaks) = ProcessWordWrap(message, texture.Size.X); + var lineHeight = _defaultFont.GetLineHeight(Scale); + var context = new MarkupDrawingContext(); + context.Color.Push(_defaultColor); + context.Font.Push(_defaultFont); + + var baseLine = new Vector2(0, height - lineHeight); + var lineBreakIndex = 0; + var globalBreakCounter = 0; + + foreach (var node in message) { + var text = ProcessNode(node, context); + if (!context.Color.TryPeek(out var color)) + color = _defaultColor; + if (!context.Font.TryPeek(out var font)) + font = _defaultFont; + + foreach (var rune in text.EnumerateRunes()) { + if (lineBreakIndex < lineBreaks.Count && lineBreaks[lineBreakIndex] == globalBreakCounter) { + baseLine = new(0, baseLine.Y - lineHeight); + lineBreakIndex += 1; + } + + var metric = font.GetCharMetrics(rune, Scale); + Vector2 mod = new Vector2(0); + if (metric.HasValue) + mod.Y += metric.Value.BearingY - (metric.Value.Height - metric.Value.BearingY); + + var advance = font.DrawChar(handle, rune, baseLine + mod, Scale, color); + baseLine.X += advance; + + globalBreakCounter += 1; + } + } + }, Color.Transparent); + } + + private string ProcessNode(MarkupNode node, MarkupDrawingContext context) { + // If a nodes name is null it's a text node. + if (node.Name == null) + return node.Value.StringValue ?? ""; + + //Skip the node if there is no markup tag for it. + if (!tagManager.TryGetMarkupTag(node.Name, null, out var tag)) + return ""; + + if (!node.Closing) { + tag.PushDrawContext(node, context); + return tag.TextBefore(node); + } + + tag.PopDrawContext(node, context); + return tag.TextAfter(node); + } + + private (int, List) ProcessWordWrap(FormattedMessage message, float maxSizeX) { + // This method is gonna suck due to complexity. + // Bear with me here. + // I am so deeply sorry for the person adding stuff to this in the future. + + var lineBreaks = new List(); + var height = _defaultFont.GetLineHeight(Scale); + + int? breakLine; + var wordWrap = new WordWrap(maxSizeX); + var context = new MarkupDrawingContext(); + context.Font.Push(_defaultFont); + context.Color.Push(_defaultColor); + + // Go over every node. + // Nodes can change the markup drawing context and return additional text. + // It's also possible for nodes to return inline controls. They get treated as one large rune. + foreach (var node in message) { + var text = ProcessNode(node, context); + + if (!context.Font.TryPeek(out var font)) + font = _defaultFont; + + // And go over every character. + foreach (var rune in text.EnumerateRunes()) { + if (ProcessRune(rune, out breakLine)) + continue; + + // Uh just skip unknown characters I guess. + if (!font.TryGetCharMetrics(rune, Scale, out var metrics)) + continue; + + if (ProcessMetric(metrics, out breakLine)) + return (height, lineBreaks); + } + } + + breakLine = wordWrap.FinalizeText(); + CheckLineBreak(breakLine); + return (height, lineBreaks); + + bool ProcessRune(Rune rune, out int? outBreakLine) { + wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip); + CheckLineBreak(breakLine); + CheckLineBreak(breakNewLine); + outBreakLine = breakLine; + return skip; + } + + bool ProcessMetric(CharMetrics metrics, out int? outBreakLine) { + wordWrap.NextMetrics(metrics, out breakLine, out var abort); + CheckLineBreak(breakLine); + outBreakLine = breakLine; + return abort; + } + + void CheckLineBreak(int? line) { + if (line is { } l) { + lineBreaks.Add(l); + if (!context.Font.TryPeek(out var font)) + font = _defaultFont; + + height += font.GetLineHeight(Scale); + } + } + } + + /// + /// Helper utility struct for word-wrapping calculations. + /// + private struct WordWrap { + private readonly float _maxSizeX; + + private float _maxUsedWidth; + private Rune _lastRune; + + // Index we put into the LineBreaks list when a line break should occur. + private int _breakIndexCounter; + + private int _nextBreakIndexCounter; + + // If the CURRENT processing word ends up too long, this is the index to put a line break. + private (int index, float lineSize)? _wordStartBreakIndex; + + // Word size in pixels. + private int _wordSizePixels; + + // The horizontal position of the text cursor. + private int _posX; + + // If a word is larger than maxSizeX, we split it. + // We need to keep track of some data to split it into two words. + private (int breakIndex, int wordSizePixels)? _forceSplitData = null; + + public WordWrap(float maxSizeX) { + this = default; + _maxSizeX = maxSizeX; + _lastRune = new Rune('A'); + } + + public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip) { + _breakIndexCounter = _nextBreakIndexCounter; + _nextBreakIndexCounter += rune.Utf16SequenceLength; + + breakLine = null; + breakNewLine = null; + skip = false; + + if (IsWordBoundary(_lastRune, rune) || rune == new Rune('\n')) { + // Word boundary means we know where the word ends. + if (_posX > _maxSizeX && _lastRune != new Rune(' ')) { + DebugTools.Assert(_wordStartBreakIndex.HasValue, + "wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line."); + //Ensure the assert had a chance to run and then just return + if (!_wordStartBreakIndex.HasValue) + return; + + // We ran into a word boundary and the word is too big to fit the previous line. + // So we insert the line break BEFORE the last word. + breakLine = _wordStartBreakIndex!.Value.index; + _maxUsedWidth = Math.Max(_maxUsedWidth, _wordStartBreakIndex.Value.lineSize); + _posX = _wordSizePixels; + } + + // Start a new word since we hit a word boundary. + //wordSize = 0; + _wordSizePixels = 0; + _wordStartBreakIndex = (_breakIndexCounter, _posX); + _forceSplitData = null; + + // Just manually handle newlines. + if (rune == new Rune('\n')) { + _maxUsedWidth = Math.Max(_maxUsedWidth, _posX); + _posX = 0; + _wordStartBreakIndex = null; + skip = true; + breakNewLine = _breakIndexCounter; + } + } + + _lastRune = rune; + } + + public void NextMetrics(in CharMetrics metrics, out int? breakLine, out bool abort) { + abort = false; + breakLine = null; + + // Increase word size and such with the current character. + var oldWordSizePixels = _wordSizePixels; + _wordSizePixels += metrics.Advance; + // TODO: Theoretically, does it make sense to break after the glyph's width instead of its advance? + // It might result in some more tight packing but I doubt it'd be noticeable. + // Also definitely even more complex to implement. + _posX += metrics.Advance; + + if (_posX <= _maxSizeX) + return; + + _forceSplitData ??= (_breakIndexCounter, oldWordSizePixels); + + // Oh hey we get to break a word that doesn't fit on a single line. + if (_wordSizePixels > _maxSizeX) { + var (breakIndex, splitWordSize) = _forceSplitData.Value; + if (splitWordSize == 0) { + // Happens if there's literally not enough space for a single character so uh... + // Yeah just don't. + abort = true; + return; + } + + // Reset forceSplitData so that we can split again if necessary. + _forceSplitData = null; + breakLine = breakIndex; + _wordSizePixels -= splitWordSize; + _wordStartBreakIndex = null; + _maxUsedWidth = Math.Max(_maxUsedWidth, _maxSizeX); + _posX = _wordSizePixels; + } + } + + public int? FinalizeText() { + // This needs to happen because word wrapping doesn't get checked for the last word. + if (_posX > _maxSizeX) { + if (!_wordStartBreakIndex.HasValue) { + throw new Exception( + "wordStartBreakIndex can only be null if the word begins at a new line," + + "in which case this branch shouldn't be reached as" + + "the word would be split due to being longer than a single line."); + } + + return _wordStartBreakIndex.Value.index; + } else { + return null; + } + } + + [Pure] + private static bool IsWordBoundary(Rune a, Rune b) { + return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-'); + } + } +} diff --git a/OpenDreamClient/States/DreamUserInterfaceStateManager.cs b/OpenDreamClient/States/DreamUserInterfaceStateManager.cs index 47b2e7343d..842a3c90e6 100644 --- a/OpenDreamClient/States/DreamUserInterfaceStateManager.cs +++ b/OpenDreamClient/States/DreamUserInterfaceStateManager.cs @@ -12,8 +12,8 @@ namespace OpenDreamClient.States; [UsedImplicitly] public sealed class DreamUserInterfaceStateManager { [Dependency] private readonly IGameController _gameController = default!; - [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IStateManager _stateManager = default!; + [Dependency] private readonly IBaseClient _client = default!; public void Initialize() { _client.RunLevelChanged += ((_, args) => { diff --git a/OpenDreamPackageTool/OpenDreamPackageTool.csproj b/OpenDreamPackageTool/OpenDreamPackageTool.csproj index 7e9d45add5..c971cc32f8 100644 --- a/OpenDreamPackageTool/OpenDreamPackageTool.csproj +++ b/OpenDreamPackageTool/OpenDreamPackageTool.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 enable enable NU1507 diff --git a/OpenDreamPackaging/OpenDreamPackaging.csproj b/OpenDreamPackaging/OpenDreamPackaging.csproj index 03108546ea..17e2a8b044 100644 --- a/OpenDreamPackaging/OpenDreamPackaging.csproj +++ b/OpenDreamPackaging/OpenDreamPackaging.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 enable enable NU1507 diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index 32d2bb5ba9..3ee582911a 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs.Native; @@ -30,12 +31,35 @@ public sealed class AtomManager { private int _nextEmptyTurfSlot; private readonly Dictionary _entityToAtom = new(); - private readonly Dictionary _definitionAppearanceCache = new(); + private readonly Dictionary _definitionAppearanceCache = new(); + + private ServerAppearanceSystem? AppearanceSystem { + get { + if(_appearanceSystem is null) + _entitySystemManager.TryGetEntitySystem(out _appearanceSystem); + return _appearanceSystem; + } + } + + private DMISpriteSystem? DMISpriteSystem { + get { + if(_dmiSpriteSystem is null) + _entitySystemManager.TryGetEntitySystem(out _dmiSpriteSystem); + return _dmiSpriteSystem; + } + } + + private ServerVerbSystem? VerbSystem { + get { + if(_verbSystem is null) + _entitySystemManager.TryGetEntitySystem(out _verbSystem); + return _verbSystem; + } + } - private ServerAppearanceSystem AppearanceSystem => _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); - private ServerVerbSystem VerbSystem => _verbSystem ??= _entitySystemManager.GetEntitySystem(); private ServerAppearanceSystem? _appearanceSystem; private ServerVerbSystem? _verbSystem; + private DMISpriteSystem? _dmiSpriteSystem; // ReSharper disable ForCanBeConvertedToForeach (the collections could be added to) public IEnumerable EnumerateAtoms(TreeEntry? filterType = null) { @@ -191,7 +215,7 @@ public EntityUid CreateMovableEntity(DreamObjectMovable movable) { var entity = _entityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); DMISpriteComponent sprite = _entityManager.AddComponent(entity); - sprite.SetAppearance(GetAppearanceFromDefinition(movable.ObjectDefinition)); + DMISpriteSystem?.SetSpriteAppearance(new(entity, sprite), GetAppearanceFromDefinition(movable.ObjectDefinition)); _entityToAtom.Add(entity, movable); return entity; @@ -209,6 +233,7 @@ public void DeleteMovableEntity(DreamObjectMovable movable) { public bool IsValidAppearanceVar(string name) { switch (name) { case "name": + case "desc": case "icon": case "icon_state": case "dir": @@ -233,6 +258,11 @@ public bool IsValidAppearanceVar(string name) { case "verbs": case "overlays": case "underlays": + case "maptext": + case "maptext_width": + case "maptext_height": + case "maptext_x": + case "maptext_y": return true; // Get/SetAppearanceVar doesn't handle filters right now @@ -242,12 +272,16 @@ public bool IsValidAppearanceVar(string name) { } } - public void SetAppearanceVar(IconAppearance appearance, string varName, DreamValue value) { + public void SetAppearanceVar(MutableAppearance appearance, string varName, DreamValue value) { switch (varName) { case "name": value.TryGetValueAsString(out var name); appearance.Name = name ?? string.Empty; break; + case "desc": + value.TryGetValueAsString(out var desc); + appearance.Desc = desc; + break; case "icon": if (_resourceManager.TryLoadIcon(value, out var icon)) { appearance.Icon = icon.Id; @@ -354,7 +388,7 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal continue; if (!verb.VerbId.HasValue) - VerbSystem.RegisterVerb(verb); + VerbSystem?.RegisterVerb(verb); if (appearance.Verbs.Contains(verb.VerbId!.Value)) continue; @@ -362,11 +396,29 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal } } else if (value.TryGetValueAsProc(out var verb)) { if (!verb.VerbId.HasValue) - VerbSystem.RegisterVerb(verb); + VerbSystem?.RegisterVerb(verb); appearance.Verbs.Add(verb.VerbId!.Value); } + break; + case "maptext": + if(value == DreamValue.Null) + appearance.Maptext = null; + else + value.TryGetValueAsString(out appearance.Maptext); + break; + case "maptext_height": + value.TryGetValueAsInteger(out appearance.MaptextSize.Y); + break; + case "maptext_width": + value.TryGetValueAsInteger(out appearance.MaptextSize.X); + break; + case "maptext_x": + value.TryGetValueAsInteger(out appearance.MaptextOffset.X); + break; + case "maptext_y": + value.TryGetValueAsInteger(out appearance.MaptextOffset.Y); break; case "appearance": throw new Exception("Cannot assign the appearance var on an appearance"); @@ -383,10 +435,19 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal } } - public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { + //TODO THIS IS A SUPER NASTY HACK + public DreamValue GetAppearanceVar(MutableAppearance appearance, string varName) { + return GetAppearanceVar(new ImmutableAppearance(appearance, null), varName); + } + + public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varName) { switch (varName) { case "name": return new(appearance.Name); + case "desc": + if (appearance.Desc == null) + return DreamValue.Null; + return new(appearance.Desc); case "icon": if (appearance.Icon == null) return DreamValue.Null; @@ -455,8 +516,20 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { transform[1], transform[3], transform[5]); return new(matrix); + case "maptext": + return (appearance.Maptext != null) + ? new DreamValue(appearance.Maptext) + : DreamValue.Null; + case "maptext_height": + return new(appearance.MaptextSize.Y); + case "maptext_width": + return new(appearance.MaptextSize.X); + case "maptext_x": + return new(appearance.MaptextOffset.X); + case "maptext_y": + return new(appearance.MaptextOffset.Y); case "appearance": - IconAppearance appearanceCopy = new IconAppearance(appearance); // Return a copy + MutableAppearance appearanceCopy = appearance.ToMutable(); // Return a copy return new(appearanceCopy); // These should be handled by an atom if referenced through one @@ -464,13 +537,11 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { case "underlays": // In BYOND this just creates a new normal list var lays = varName == "overlays" ? appearance.Overlays : appearance.Underlays; - var list = _objectTree.CreateList(lays.Count); + var list = _objectTree.CreateList(lays.Length); if (_appearanceSystem != null) { - foreach (var layId in lays) { - var lay = _appearanceSystem.MustGetAppearance(layId); - - list.AddValue(new(lay)); + foreach (var lay in lays) { + list.AddValue(new(lay.ToMutable())); } } @@ -484,15 +555,15 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { } /// - /// Gets an atom's appearance. + /// Gets an atom's appearance. Will throw if the appearance system is not available. /// /// The atom to find the appearance of. - public IconAppearance? MustGetAppearance(DreamObject atom) { + public ImmutableAppearance MustGetAppearance(DreamObject atom) { return atom switch { - DreamObjectTurf turf => AppearanceSystem.MustGetAppearance(turf.AppearanceId), - DreamObjectMovable movable => movable.SpriteComponent.Appearance, - DreamObjectArea area => AppearanceSystem.MustGetAppearance(area.AppearanceId), - DreamObjectImage image => image.Appearance, + DreamObjectArea area => area.Appearance, + DreamObjectTurf turf => turf.Appearance, + DreamObjectMovable movable => movable.SpriteComponent.Appearance!, + DreamObjectImage image => image.IsMutableAppearance ? AppearanceSystem!.AddAppearance(image.MutableAppearance!, registerAppearance: false) : image.SpriteComponent!.Appearance!, _ => throw new Exception($"Cannot get appearance of {atom}") }; } @@ -500,82 +571,122 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { /// /// Optionally looks up for an appearance. Does not try to create a new one when one is not found for this atom. /// - public bool TryGetAppearance(DreamObject atom, [NotNullWhen(true)] out IconAppearance? appearance) { - if (atom is DreamObjectTurf turf) - appearance = AppearanceSystem.MustGetAppearance(turf.AppearanceId); - else if (atom is DreamObjectMovable movable) - appearance = movable.SpriteComponent.Appearance; - else if (atom is DreamObjectImage image) - appearance = image.Appearance; - else if (atom is DreamObjectArea area) - appearance = AppearanceSystem.MustGetAppearance(area.AppearanceId); - else - appearance = null; + public bool TryGetAppearance(DreamObject atom, [NotNullWhen(true)] out ImmutableAppearance? appearance) { + appearance = atom switch { + DreamObjectArea area => area.Appearance, + DreamObjectTurf turf => turf.Appearance, + DreamObjectMovable { SpriteComponent.Appearance: { } movableAppearance } => movableAppearance, + DreamObjectImage image => image.IsMutableAppearance + ? AppearanceSystem!.AddAppearance(image.MutableAppearance!, registerAppearance: false) + : image.SpriteComponent?.Appearance, + _ => null + }; return appearance is not null; } - public void UpdateAppearance(DreamObject atom, Action update) { - var appearance = MustGetAppearance(atom); - appearance = (appearance != null) ? new(appearance) : new(); // Clone the appearance - + public void UpdateAppearance(DreamObject atom, Action update) { + ImmutableAppearance immutableAppearance = MustGetAppearance(atom); + using var appearance = immutableAppearance.ToMutable(); // Clone the appearance update(appearance); SetAtomAppearance(atom, appearance); } - public void SetAtomAppearance(DreamObject atom, IconAppearance appearance) { + public void SetAtomAppearance(DreamObject atom, MutableAppearance appearance) { if (atom is DreamObjectTurf turf) { _dreamMapManager.SetTurfAppearance(turf, appearance); } else if (atom is DreamObjectMovable movable) { - movable.SpriteComponent.SetAppearance(appearance); + DMISpriteSystem?.SetSpriteAppearance(new(movable.Entity, movable.SpriteComponent), appearance); } else if (atom is DreamObjectImage image) { - image.Appearance = appearance; + if(image.IsMutableAppearance) + image.MutableAppearance = MutableAppearance.GetCopy(appearance); //this needs to be a copy + else + DMISpriteSystem?.SetSpriteAppearance(new(image.Entity, image.SpriteComponent!), appearance); } else if (atom is DreamObjectArea area) { _dreamMapManager.SetAreaAppearance(area, appearance); } } - public void AnimateAppearance(DreamObject atom, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, Action animate) { - if (atom is not DreamObjectMovable movable) - return; //Animating non-movables is unimplemented TODO: should handle images and maybe filters + public void SetMovableScreenLoc(DreamObjectMovable movable, ScreenLocation screenLocation) { + DMISpriteSystem?.SetSpriteScreenLocation(new(movable.Entity, movable.SpriteComponent), screenLocation); + } - IconAppearance appearance = new IconAppearance(movable.SpriteComponent.Appearance); + public void SetSpriteAppearance(Entity ent, MutableAppearance appearance) { + DMISpriteSystem?.SetSpriteAppearance(ent, appearance); + } - animate(appearance); + public void AnimateAppearance(DreamObject atom, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, Action animate) { + MutableAppearance appearance; + EntityUid targetEntity; + DMISpriteComponent? targetComponent = null; + NetEntity ent = NetEntity.Invalid; + uint? turfId = null; + + if (atom is DreamObjectMovable movable) { + targetEntity = movable.Entity; + targetComponent = movable.SpriteComponent; + appearance = MustGetAppearance(atom).ToMutable(); + } else if (atom is DreamObjectImage { IsMutableAppearance: false } image) { + targetEntity = image.Entity; + targetComponent = image.SpriteComponent; + appearance = MustGetAppearance(atom).ToMutable(); + } else if (atom is DreamObjectTurf turf) { + targetEntity = EntityUid.Invalid; + appearance = turf.Appearance.ToMutable(); + } else if (atom is DreamObjectArea area) { + return; + //TODO: animate area appearance + //area appearance should be an overlay on turfs, so could maybe get away with animating that? + } else if (atom is DreamObjectClient client) { + return; + //TODO: animate client appearance + } else if (atom is DreamObjectFilter filter) { + return; + //TODO: animate filters + } else + throw new ArgumentException($"Cannot animate appearance of {atom}"); - // Don't send the updated appearance to clients, they will animate it - movable.SpriteComponent.SetAppearance(appearance, dirty: false); + animate(appearance); - NetEntity ent = _entityManager.GetNetEntity(movable.Entity); + if(targetComponent is not null) { + ent = _entityManager.GetNetEntity(targetEntity); + // Don't send the updated appearance to clients, they will animate it + DMISpriteSystem?.SetSpriteAppearance(new(targetEntity, targetComponent), appearance, dirty: false); + } else if (atom is DreamObjectTurf turf) { + //TODO: turf appearances are just set to the end appearance, they do not get properly animated + _dreamMapManager.SetTurfAppearance(turf, appearance); + turfId = turf.Appearance.MustGetId(); + } else if (atom is DreamObjectArea area) { + //fuck knows, this will trigger a bunch of turf updates to? idek + } - AppearanceSystem.Animate(ent, appearance, duration, easing, loop, flags, delay, chainAnim); + AppearanceSystem?.Animate(ent, appearance, duration, easing, loop, flags, delay, chainAnim, turfId); } - public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out IconAppearance? appearance) { + public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out MutableAppearance? appearance) { if (value.TryGetValueAsAppearance(out var copyFromAppearance)) { - appearance = new(copyFromAppearance); + appearance = MutableAppearance.GetCopy(copyFromAppearance); return true; } if (value.TryGetValueAsDreamObject(out var copyFromImage)) { - appearance = new(copyFromImage.Appearance!); + appearance = MustGetAppearance(copyFromImage).ToMutable(); return true; } if (value.TryGetValueAsType(out var copyFromType)) { - appearance = GetAppearanceFromDefinition(copyFromType.ObjectDefinition); + appearance = MutableAppearance.GetCopy(GetAppearanceFromDefinition(copyFromType.ObjectDefinition)); return true; } if (value.TryGetValueAsDreamObject(out var copyFromAtom)) { - appearance = new(MustGetAppearance(copyFromAtom)); + appearance = MustGetAppearance(copyFromAtom).ToMutable(); return true; } if (_resourceManager.TryLoadIcon(value, out var iconResource)) { - appearance = new IconAppearance() { - Icon = iconResource.Id - }; + appearance = MutableAppearance.Get(); + appearance.Icon = iconResource.Id; return true; } @@ -584,11 +695,12 @@ public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out Ic return false; } - public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { + public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { if (_definitionAppearanceCache.TryGetValue(def, out var appearance)) return appearance; def.TryGetVariable("name", out var nameVar); + def.TryGetVariable("desc", out var descVar); def.TryGetVariable("icon", out var iconVar); def.TryGetVariable("icon_state", out var stateVar); def.TryGetVariable("color", out var colorVar); @@ -606,9 +718,15 @@ public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { def.TryGetVariable("render_target", out var renderTargetVar); def.TryGetVariable("blend_mode", out var blendModeVar); def.TryGetVariable("appearance_flags", out var appearanceFlagsVar); + def.TryGetVariable("maptext", out var maptextVar); + def.TryGetVariable("maptext_width", out var maptextWidthVar); + def.TryGetVariable("maptext_height", out var maptextHeightVar); + def.TryGetVariable("maptext_x", out var maptextXVar); + def.TryGetVariable("maptext_y", out var maptextYVar); - appearance = new IconAppearance(); + appearance = MutableAppearance.Get(); SetAppearanceVar(appearance, "name", nameVar); + SetAppearanceVar(appearance, "desc", descVar); SetAppearanceVar(appearance, "icon", iconVar); SetAppearanceVar(appearance, "icon_state", stateVar); SetAppearanceVar(appearance, "color", colorVar); @@ -626,6 +744,11 @@ public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { SetAppearanceVar(appearance, "render_target", renderTargetVar); SetAppearanceVar(appearance, "blend_mode", blendModeVar); SetAppearanceVar(appearance, "appearance_flags", appearanceFlagsVar); + SetAppearanceVar(appearance, "maptext", maptextVar); + SetAppearanceVar(appearance, "maptext_width", maptextWidthVar); + SetAppearanceVar(appearance, "maptext_height", maptextHeightVar); + SetAppearanceVar(appearance, "maptext_x", maptextXVar); + SetAppearanceVar(appearance, "maptext_y", maptextYVar); if (def.TryGetVariable("transform", out var transformVar) && transformVar.TryGetValueAsDreamObject(out var transformMatrix)) { appearance.Transform = DreamObjectMatrix.MatrixToTransformFloatArray(transformMatrix); diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 0b1a1e5668..4237c4eb9a 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -470,6 +470,19 @@ public void WinClone(string controlId, string cloneId) { Session?.Channel.SendMessage(msg); } + /// + /// Sends a URL to the client to open. + /// Can be a website, a topic call, or another server to connect to. + /// + /// URL to open on the client's side + public void SendLink(string url) { + var msg = new MsgLink { + Url = url + }; + + Session?.Channel.SendMessage(msg); + } + /// /// Prompts the user to save a file to disk /// diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 0c79b2389b..e8373517c8 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -65,6 +65,7 @@ private void InitializeConnectionManager() { _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); + _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(RxAckLoadInterface); diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 20aacd3428..48176de307 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -5,6 +5,7 @@ using System.Threading; using DMCompiler.Bytecode; using DMCompiler.Json; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; @@ -240,7 +241,7 @@ public string CreateRef(DreamValue value) { } else if (value.TryGetValueAsAppearance(out var appearance)) { refType = RefType.DreamAppearance; _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); - idx = (int)_appearanceSystem.AddAppearance(appearance); + idx = (int)_appearanceSystem.AddAppearance(appearance).MustGetId(); } else if (value.TryGetValueAsDreamResource(out var refRsc)) { refType = RefType.DreamResource; idx = refRsc.Id; @@ -323,8 +324,8 @@ public DreamValue LocateRef(string refString) { return new DreamValue(resource); case RefType.DreamAppearance: _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); - return _appearanceSystem.TryGetAppearance(refId, out IconAppearance? appearance) - ? new DreamValue(appearance) + return _appearanceSystem.TryGetAppearanceById((uint) refId, out ImmutableAppearance? appearance) + ? new DreamValue(appearance.ToMutable()) : DreamValue.Null; case RefType.Proc: return new(_objectTree.Procs[refId]); diff --git a/OpenDreamRuntime/DreamValue.cs b/OpenDreamRuntime/DreamValue.cs index d173114d3a..5990080892 100644 --- a/OpenDreamRuntime/DreamValue.cs +++ b/OpenDreamRuntime/DreamValue.cs @@ -102,7 +102,7 @@ public DreamValue(DreamProc value) { _refValue = value; } - public DreamValue(IconAppearance appearance) { + public DreamValue(MutableAppearance appearance) { Type = DreamValueType.Appearance; _refValue = appearance; } @@ -315,9 +315,9 @@ public DreamProc MustGetValueAsProc() { throw new InvalidCastException("Value " + this + " was not the expected type of DreamProc"); } - public readonly bool TryGetValueAsAppearance([NotNullWhen(true)] out IconAppearance? args) { + public readonly bool TryGetValueAsAppearance([NotNullWhen(true)] out MutableAppearance? args) { if (Type == DreamValueType.Appearance) { - args = Unsafe.As(_refValue)!; + args = Unsafe.As(_refValue)!; return true; } @@ -326,9 +326,9 @@ public readonly bool TryGetValueAsAppearance([NotNullWhen(true)] out IconAppeara return false; } - public IconAppearance MustGetValueAsAppearance() { + public MutableAppearance MustGetValueAsAppearance() { if (Type == DreamValueType.Appearance) { - return Unsafe.As(_refValue)!; + return Unsafe.As(_refValue)!; } throw new InvalidCastException("Value " + this + " was not the expected type of Appearance"); @@ -362,6 +362,11 @@ public string Stringify() { case DreamValueType.Float: var floatValue = MustGetValueAsFloat(); + if (float.IsInfinity(floatValue)) { + var str = float.IsPositiveInfinity(floatValue) ? "inf" : "-inf"; + return str; + } + if (floatValue > 16777216f) { return floatValue.ToString("g6"); } @@ -371,6 +376,8 @@ public string Stringify() { return floatValue.ToString("g8"); } + if (float.IsNaN(floatValue)) return "nan"; + return floatValue.ToString("g6"); case DreamValueType.DreamResource: diff --git a/OpenDreamRuntime/Input/MouseInputSystem.cs b/OpenDreamRuntime/Input/MouseInputSystem.cs index 8956e300cf..ce7c748620 100644 --- a/OpenDreamRuntime/Input/MouseInputSystem.cs +++ b/OpenDreamRuntime/Input/MouseInputSystem.cs @@ -1,6 +1,5 @@ -using System.Collections.Specialized; -using System.Text; -using System.Web; +using System.Text; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects.Types; using OpenDreamShared.Input; diff --git a/OpenDreamRuntime/Map/DreamMapManager.Pathfinding.cs b/OpenDreamRuntime/Map/DreamMapManager.Pathfinding.cs new file mode 100644 index 0000000000..1cc2093fb9 --- /dev/null +++ b/OpenDreamRuntime/Map/DreamMapManager.Pathfinding.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; +using OpenDreamRuntime.Procs.Native; +using OpenDreamShared.Dream; + +namespace OpenDreamRuntime.Map; + +public partial class DreamMapManager { + private sealed class PathFindNode : IDisposable, IEquatable { + private static readonly Stack Pool = new(); + + public int X, Y; + public PathFindNode? Parent; + public int NeededSteps; + + public static PathFindNode GetNode(int x, int y) { + if (!Pool.TryPop(out var node)) { + node = new(); + } + + node.Parent = null; + node.X = x; + node.Y = y; + node.NeededSteps = 0; + return node; + } + + public void Dispose() { + Pool.Push(this); + } + + public bool Equals(PathFindNode? other) { + if (other is null) + return false; + return X == other.X && Y == other.Y; + } + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() { + return HashCode.Combine(X, Y); + } + } + + public IEnumerable CalculateSteps((int X, int Y, int Z) loc, (int X, int Y, int Z) dest, int distance) { + int z = loc.Z; + if (z != dest.Z) // Different Z-levels are unreachable + yield break; + + HashSet explored = new(); + Queue toExplore = new(); + + toExplore.Enqueue(PathFindNode.GetNode(loc.X, loc.Y)); + + void Explore(PathFindNode current, int offsetX, int offsetY) { + var nextX = current.X + offsetX; + var nextY = current.Y + offsetY; + if (nextX < 1 || nextX > Size.X || nextY < 1 || nextY > Size.Y) + return; // This is outside of map bounds + + var next = PathFindNode.GetNode(nextX, nextY); + if (explored.Contains(next)) + return; + if (!TryGetCellAt(new(next.X, next.Y), z, out var cell) || cell.Turf.IsDense) + return; + + if (!toExplore.Contains(next)) + toExplore.Enqueue(next); + else if (next.NeededSteps >= current.NeededSteps + 1) + return; + + next.NeededSteps = current.NeededSteps + 1; + next.Parent = current; + } + + while (toExplore.TryDequeue(out var node)) { + var distX = node.X - dest.X; + var distY = node.Y - dest.Y; + if (Math.Sqrt(distX * distX + distY * distY) <= distance) { // Path to the destination was found + Stack path = new(node.NeededSteps); + + while (node.Parent != null) { + var stepDir = DreamProcNativeHelpers.GetDir((node.Parent.X, node.Parent.Y, z), (node.X, node.Y, z)); + + node = node.Parent; + path.Push(stepDir); + } + + while (path.TryPop(out var step)) { + yield return step; + } + + break; + } + + explored.Add(node); + Explore(node, 1, 0); + Explore(node, 1, 1); + Explore(node, 0, 1); + Explore(node, -1, 1); + Explore(node, -1, 0); + Explore(node, -1, -1); + Explore(node, 0, -1); + Explore(node, 1, -1); + } + + foreach (var node in explored) + node.Dispose(); + foreach (var node in toExplore) + node.Dispose(); + } +} diff --git a/OpenDreamRuntime/DreamMapManager.cs b/OpenDreamRuntime/Map/DreamMapManager.cs similarity index 85% rename from OpenDreamRuntime/DreamMapManager.cs rename to OpenDreamRuntime/Map/DreamMapManager.cs index 89864ab959..408b0e0da6 100644 --- a/OpenDreamRuntime/DreamMapManager.cs +++ b/OpenDreamRuntime/Map/DreamMapManager.cs @@ -10,12 +10,12 @@ using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Utility; -using Level = OpenDreamRuntime.IDreamMapManager.Level; -using Cell = OpenDreamRuntime.IDreamMapManager.Cell; +using Level = OpenDreamRuntime.Map.IDreamMapManager.Level; +using Cell = OpenDreamRuntime.Map.IDreamMapManager.Cell; -namespace OpenDreamRuntime; +namespace OpenDreamRuntime.Map; -public sealed class DreamMapManager : IDreamMapManager { +public sealed partial class DreamMapManager : IDreamMapManager { [Dependency] private readonly DreamManager _dreamManager = default!; [Dependency] private readonly AtomManager _atomManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; @@ -162,7 +162,7 @@ private void SetTurf(Vector2i pos, int z, DreamObjectDefinition type, DreamProcA cell.Turf.SetTurfType(type); - IconAppearance turfAppearance = _atomManager.GetAppearanceFromDefinition(cell.Turf.ObjectDefinition); + MutableAppearance turfAppearance = _atomManager.GetAppearanceFromDefinition(cell.Turf.ObjectDefinition); SetTurfAppearance(cell.Turf, turfAppearance); cell.Turf.InitSpawn(creationArguments); @@ -176,40 +176,56 @@ public void SetTurf(DreamObjectTurf turf, DreamObjectDefinition type, DreamProcA /// Caches the turf/area appearance pair instead of recreating and re-registering it for every turf in the game. /// This is cleared out when an area appearance changes /// - private readonly Dictionary, IconAppearance> _turfAreaLookup = new(); - - public void SetTurfAppearance(DreamObjectTurf turf, IconAppearance appearance) { - if(turf.Cell.Area.AppearanceId != 0) - if(!appearance.Overlays.Contains(turf.Cell.Area.AppearanceId)) { - if(!_turfAreaLookup.TryGetValue((appearance, turf.Cell.Area.AppearanceId), out var newAppearance)) { - newAppearance = new(appearance); - newAppearance.Overlays.Add(turf.Cell.Area.AppearanceId); - _turfAreaLookup.Add((appearance, turf.Cell.Area.AppearanceId), newAppearance); + private readonly Dictionary, MutableAppearance> _turfAreaLookup = new(); + + public void SetTurfAppearance(DreamObjectTurf turf, MutableAppearance appearance) { + if(turf.Cell.Area.Appearance != _appearanceSystem.DefaultAppearance) + if(!appearance.Overlays.Contains(turf.Cell.Area.Appearance)) { + if(!_turfAreaLookup.TryGetValue((appearance, turf.Cell.Area.Appearance.MustGetId()), out var newAppearance)) { + newAppearance = MutableAppearance.GetCopy(appearance); + newAppearance.Overlays.Add(turf.Cell.Area.Appearance); + _turfAreaLookup.Add((appearance, turf.Cell.Area.Appearance.MustGetId()), newAppearance); } appearance = newAppearance; } - int appearanceId = _appearanceSystem.AddAppearance(appearance); + var immutableAppearance = _appearanceSystem.AddAppearance(appearance); var level = _levels[turf.Z - 1]; - int turfId = (appearanceId + 1); // +1 because 0 is used for empty turfs - level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile(turfId); - turf.AppearanceId = appearanceId; + uint turfId = immutableAppearance.MustGetId(); + level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile((int)turfId); + turf.Appearance = immutableAppearance; } - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance) { + public void SetAreaAppearance(DreamObjectArea area, MutableAppearance appearance) { //if an area changes appearance, invalidate the lookup _turfAreaLookup.Clear(); - int oldAppearance = area.AppearanceId; - area.AppearanceId = _appearanceSystem.AddAppearance(appearance); - foreach (var turf in area.Turfs) { - var turfAppearance = _atomManager.MustGetAppearance(turf); + var oldAppearance = area.Appearance; + appearance.AppearanceFlags |= AppearanceFlags.ResetColor | AppearanceFlags.ResetAlpha | AppearanceFlags.ResetTransform; + area.Appearance = _appearanceSystem.AddAppearance(appearance); + + //get all unique turf appearances + //create the new version of each of those appearances + //for each turf, update the appropriate ID - if(turfAppearance is null) continue; + Dictionary oldToNewAppearance = new(); + foreach (var turf in area.Turfs) { + if(oldToNewAppearance.TryGetValue(turf.Appearance, out var newAppearance)) + turf.Appearance = newAppearance; + else { + MutableAppearance turfAppearance = _atomManager.MustGetAppearance(turf).ToMutable(); + + turfAppearance.Overlays.Remove(oldAppearance); + turfAppearance.Overlays.Add(area.Appearance); + newAppearance = _appearanceSystem.AddAppearance(turfAppearance); + oldToNewAppearance.Add(turf.Appearance, newAppearance); + turf.Appearance = newAppearance; + } - turfAppearance.Overlays.Remove(oldAppearance); - SetTurfAppearance(turf, turfAppearance); + var level = _levels[turf.Z - 1]; + uint turfId = newAppearance.MustGetId(); + level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile((int)turfId); } } @@ -443,9 +459,12 @@ public DreamObjectArea Area { _area.Turfs.Remove(Turf); _area.ResetCoordinateCache(); + var oldArea = _area; _area = value; _area.Turfs.Add(Turf); _area.ResetCoordinateCache(); + + Turf.OnAreaChange(oldArea); } } @@ -471,11 +490,13 @@ public Cell(DreamObjectArea area, DreamObjectTurf turf) { public void UpdateTiles(); public void SetTurf(DreamObjectTurf turf, DreamObjectDefinition type, DreamProcArguments creationArguments); - public void SetTurfAppearance(DreamObjectTurf turf, IconAppearance appearance); - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance); + public void SetTurfAppearance(DreamObjectTurf turf, MutableAppearance appearance); + public void SetAreaAppearance(DreamObjectArea area, MutableAppearance appearance); public bool TryGetCellAt(Vector2i pos, int z, [NotNullWhen(true)] out Cell? cell); public bool TryGetTurfAt(Vector2i pos, int z, [NotNullWhen(true)] out DreamObjectTurf? turf); public void SetZLevels(int levels); public void SetWorldSize(Vector2i size); public EntityUid GetZLevelEntity(int z); + + public IEnumerable CalculateSteps((int X, int Y, int Z) loc, (int X, int Y, int Z) dest, int distance); } diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs index cfa73be66b..e3e4389248 100644 --- a/OpenDreamRuntime/Objects/DreamObject.cs +++ b/OpenDreamRuntime/Objects/DreamObject.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Runtime.CompilerServices; using DMCompiler.Bytecode; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Rendering; using OpenDreamRuntime.Resources; @@ -145,6 +146,8 @@ public void Delete(bool possiblyThreaded = false) { } public bool IsSubtypeOf(TreeEntry ancestor) { + if(Deleted) //null deref protection, deleted objects don't have ObjectDefinition anymore + return false; return ObjectDefinition.IsSubtypeOf(ancestor); } @@ -373,10 +376,15 @@ public string GetNameUnformatted() { /// /// Returns the name of this object with no formatting evaluated /// - /// public string GetRawName() { - if (!TryGetVariable("name", out DreamValue nameVar) || !nameVar.TryGetValueAsString(out string? name)) - return ObjectDefinition.Type.ToString(); + string name = ObjectDefinition.Type; + + if (this is DreamObjectAtom) { + if (AtomManager.TryGetAppearance(this, out var appearance)) + name = appearance.Name; + } else if (TryGetVariable("name", out DreamValue nameVar) && nameVar.TryGetValueAsString(out var nameVarStr)) { + name = nameVarStr; + } return name; } diff --git a/OpenDreamRuntime/Objects/DreamObjectDefinition.cs b/OpenDreamRuntime/Objects/DreamObjectDefinition.cs index 670299e238..9c89943200 100644 --- a/OpenDreamRuntime/Objects/DreamObjectDefinition.cs +++ b/OpenDreamRuntime/Objects/DreamObjectDefinition.cs @@ -1,9 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Rendering; using OpenDreamRuntime.Resources; -using OpenDreamShared.Dream; using Robust.Server.GameObjects; using Robust.Server.GameStates; using Robust.Server.Player; diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index f2492129f4..668b00cab0 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -1,7 +1,9 @@ +using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Threading.Tasks; using DMCompiler.Json; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.DebugAdapter; @@ -47,8 +49,8 @@ public sealed class DreamObjectTree { public TreeEntry Obj { get; private set; } public TreeEntry Mob { get; private set; } - private readonly Dictionary _pathToType = new(); - private Dictionary _globalProcIds; + private FrozenDictionary _pathToType = FrozenDictionary.Empty; + private FrozenDictionary _globalProcIds = FrozenDictionary.Empty; [Dependency] private readonly AtomManager _atomManager = default!; [Dependency] private readonly DreamManager _dreamManager = default!; @@ -124,13 +126,15 @@ public bool TryGetGlobalProc(string name, [NotNullWhen(true)] out DreamProc? glo public IEnumerable GetAllDescendants(TreeEntry treeEntry) { yield return treeEntry; - foreach (int typeId in treeEntry.InheritingTypes) { - TreeEntry type = Types[typeId]; - IEnumerator typeChildren = GetAllDescendants(type).GetEnumerator(); + if (treeEntry.InheritingTypes is not null) { + foreach (int typeId in treeEntry.InheritingTypes) { + TreeEntry type = Types[typeId]; + IEnumerator typeChildren = GetAllDescendants(type).GetEnumerator(); - while (typeChildren.MoveNext()) yield return typeChildren.Current; + while (typeChildren.MoveNext()) yield return typeChildren.Current; - typeChildren.Dispose(); + typeChildren.Dispose(); + } } } @@ -267,14 +271,17 @@ private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? proc //First pass: Create types and set them up for initialization Types[0] = Root; + var pathToType = new Dictionary(types.Length); for (int i = 1; i < Types.Length; i++) { var path = types[i].Path; var type = new TreeEntry(path, i); Types[i] = type; - _pathToType[path] = type; + pathToType[path] = type; } + _pathToType = pathToType.ToFrozenDictionary(); + World = GetTreeEntry("/world"); List = GetTreeEntry("/list"); Client = GetTreeEntry("/client"); @@ -307,7 +314,7 @@ private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? proc if (jsonType.Parent != null) { TreeEntry parent = Types[jsonType.Parent.Value]; - + parent.InheritingTypes ??= new List(1); parent.InheritingTypes.Add(i); type.ParentEntry = parent; } @@ -426,13 +433,15 @@ private void LoadProcsFromJson(ProcDefinitionJson[]? jsonProcs, int[]? jsonGloba } if (jsonGlobalProcs != null) { - _globalProcIds = new(jsonGlobalProcs.Length); + Dictionary globalProcIds = new(jsonGlobalProcs.Length); foreach (var procId in jsonGlobalProcs) { var proc = Procs[procId]; - _globalProcIds.Add(proc.Name, procId); + globalProcIds.Add(proc.Name, procId); } + + _globalProcIds = globalProcIds.ToFrozenDictionary(); } } @@ -482,11 +491,13 @@ public void SetNativeProc(TreeEntry type, Func private IEnumerable TraversePostOrder(TreeEntry from) { - foreach (int typeId in from.InheritingTypes) { - TreeEntry type = Types[typeId]; - using IEnumerator typeChildren = TraversePostOrder(type).GetEnumerator(); + if (from.InheritingTypes is not null) { + foreach (int typeId in from.InheritingTypes) { + TreeEntry type = Types[typeId]; + using IEnumerator typeChildren = TraversePostOrder(type).GetEnumerator(); - while (typeChildren.MoveNext()) yield return typeChildren.Current; + while (typeChildren.MoveNext()) yield return typeChildren.Current; + } } yield return from; @@ -499,7 +510,7 @@ public sealed class TreeEntry { public readonly int Id; public DreamObjectDefinition ObjectDefinition; public TreeEntry ParentEntry; - public readonly List InheritingTypes = new(); + public List? InheritingTypes; /// /// This node's index in the inheritance tree based on a depth-first search
diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index 793886360b..0689448131 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Runtime.CompilerServices; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Rendering; using OpenDreamShared.Dream; @@ -297,11 +298,14 @@ public override DreamValue OperatorOr(DreamValue b, DMProcState state) { public override DreamValue OperatorAppend(DreamValue b) { if (b.TryGetValueAsDreamList(out var bList)) { - foreach (DreamValue value in bList.GetValues()) { - if (bList._associativeValues?.TryGetValue(value, out var assocValue) is true) { - SetValue(value, assocValue); - } else { - AddValue(value); + var values = bList.GetValues(); + var valueCount = values.Count; // Some lists return a reference to their internal values list which could change with each loop + for (int i = 0; i < valueCount; i++) { + var value = values[i]; + AddValue(value); // Always add the value + if (bList._associativeValues?.TryGetValue(value, out var assocValue) is true) { // Ensure the associated value is correct + _associativeValues ??= new(); + _associativeValues[value] = assocValue; } } } else { @@ -574,14 +578,14 @@ public override int GetLength() { // atom's verbs list // Keeps track of an appearance's verbs (atom.verbs, mutable_appearance.verbs, etc) public sealed class VerbsList(DreamObjectTree objectTree, AtomManager atomManager, ServerVerbSystem? verbSystem, DreamObjectAtom atom) : DreamList(objectTree.List.ObjectDefinition, 0) { - public override DreamValue GetValue(DreamValue key) { + public override DreamValue GetValue(DreamValue key) { if (verbSystem == null) - return DreamValue.Null; + return DreamValue.Null; if (!key.TryGetValueAsInteger(out var index)) throw new Exception($"Invalid index into verbs list: {key}"); var verbs = GetVerbs(); - if (index < 1 || index > verbs.Count) + if (index < 1 || index > verbs.Length) throw new Exception($"Out of bounds index on verbs list: {index}"); return new DreamValue(verbSystem.GetVerb(verbs[index - 1])); @@ -592,7 +596,7 @@ public override List GetValues() { if (appearance == null || verbSystem == null) return new List(); - var values = new List(appearance.Verbs.Count); + var values = new List(appearance.Verbs.Length); foreach (var verbId in appearance.Verbs) { var verb = verbSystem.GetVerb(verbId); @@ -631,11 +635,11 @@ public override void Cut(int start = 1, int end = 0) { } public override int GetLength() { - return GetVerbs().Count; + return GetVerbs().Length; } - private List GetVerbs() { - IconAppearance? appearance = atomManager.MustGetAppearance(atom); + private int[] GetVerbs() { + var appearance = atomManager.MustGetAppearance(atom); if (appearance == null) throw new Exception("Atom has no appearance"); @@ -648,7 +652,6 @@ private List GetVerbs() { public sealed class DreamOverlaysList : DreamList { [Dependency] private readonly AtomManager _atomManager = default!; private readonly ServerAppearanceSystem? _appearanceSystem; - private readonly DreamObject _owner; private readonly bool _isUnderlays; @@ -665,13 +668,11 @@ public override List GetValues() { if (appearance == null || _appearanceSystem == null) return new List(); - var overlays = GetOverlaysList(appearance); - var values = new List(overlays.Count); + var overlays = GetOverlaysArray(appearance); + var values = new List(overlays.Length); foreach (var overlay in overlays) { - var overlayAppearance = _appearanceSystem.MustGetAppearance(overlay); - - values.Add(new(overlayAppearance)); + values.Add(new(overlay.ToMutable())); } return values; @@ -679,10 +680,9 @@ public override List GetValues() { public override void Cut(int start = 1, int end = 0) { _atomManager.UpdateAppearance(_owner, appearance => { - List overlaysList = GetOverlaysList(appearance); + var overlaysList = GetOverlaysList(appearance); int count = overlaysList.Count + 1; if (end == 0 || end > count) end = count; - overlaysList.RemoveRange(start - 1, end - start); }); } @@ -691,16 +691,15 @@ public override DreamValue GetValue(DreamValue key) { if (!key.TryGetValueAsInteger(out var overlayIndex) || overlayIndex < 1) throw new Exception($"Invalid index into {(_isUnderlays ? "underlays" : "overlays")} list: {key}"); - IconAppearance appearance = GetAppearance(); - List overlaysList = GetOverlaysList(appearance); - if (overlayIndex > overlaysList.Count) - throw new Exception($"Atom only has {overlaysList.Count} {(_isUnderlays ? "underlay" : "overlay")}(s), cannot index {overlayIndex}"); + ImmutableAppearance appearance = _atomManager.MustGetAppearance(_owner); + var overlaysList = GetOverlaysArray(appearance); + if (overlayIndex > overlaysList.Length) + throw new Exception($"Atom only has {overlaysList.Length} {(_isUnderlays ? "underlay" : "overlay")}(s), cannot index {overlayIndex}"); if (_appearanceSystem == null) return DreamValue.Null; - int overlayId = GetOverlaysList(appearance)[overlayIndex - 1]; - IconAppearance overlayAppearance = _appearanceSystem.MustGetAppearance(overlayId); + var overlayAppearance = overlaysList[overlayIndex - 1].ToMutable(); return new DreamValue(overlayAppearance); } @@ -712,11 +711,14 @@ public override void AddValue(DreamValue value) { if (_appearanceSystem == null) return; - _atomManager.UpdateAppearance(_owner, appearance => { - IconAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, appearance.Icon); - overlayAppearance ??= new IconAppearance(); + var overlayAppearance = CreateOverlayAppearance(_atomManager, value, _atomManager.MustGetAppearance(_owner).Icon); + var immutableOverlay = _appearanceSystem.AddAppearance(overlayAppearance ?? MutableAppearance.Default); + overlayAppearance?.Dispose(); - GetOverlaysList(appearance).Add(_appearanceSystem.AddAppearance(overlayAppearance)); + //after UpdateApparance is done, the atom is set with a new immutable appearance containing a hard ref to the overlay + //only /mutable_appearance handles it differently, and that's done in DreamObjectImage + _atomManager.UpdateAppearance(_owner, appearance => { + GetOverlaysList(appearance).Add(immutableOverlay); }); } @@ -724,38 +726,34 @@ public override void RemoveValue(DreamValue value) { if (_appearanceSystem == null) return; - _atomManager.UpdateAppearance(_owner, appearance => { - IconAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, appearance.Icon); - if (overlayAppearance == null || !_appearanceSystem.TryGetAppearanceId(overlayAppearance, out var id)) - return; + MutableAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, _atomManager.MustGetAppearance(_owner).Icon); + if (overlayAppearance == null) + return; - GetOverlaysList(appearance).Remove(id); + _atomManager.UpdateAppearance(_owner, appearance => { + GetOverlaysList(appearance).Remove(_appearanceSystem.AddAppearance(overlayAppearance, registerAppearance:false)); + overlayAppearance.Dispose(); }); } public override int GetLength() { - return GetOverlaysList(GetAppearance()).Count; + return GetOverlaysArray(_atomManager.MustGetAppearance(_owner)).Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private List GetOverlaysList(IconAppearance appearance) => + private List GetOverlaysList(MutableAppearance appearance) => _isUnderlays ? appearance.Underlays : appearance.Overlays; - private IconAppearance GetAppearance() { - IconAppearance? appearance = _atomManager.MustGetAppearance(_owner); - if (appearance == null) - throw new Exception("Atom has no appearance"); - - return appearance; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ImmutableAppearance[] GetOverlaysArray(ImmutableAppearance appearance) => + _isUnderlays ? appearance.Underlays : appearance.Overlays; - public static IconAppearance? CreateOverlayAppearance(AtomManager atomManager, DreamValue value, int? defaultIcon) { - IconAppearance overlay; + public static MutableAppearance? CreateOverlayAppearance(AtomManager atomManager, DreamValue value, int? defaultIcon) { + MutableAppearance overlay; if (value.TryGetValueAsString(out var iconState)) { - overlay = new IconAppearance() { - IconState = iconState - }; + overlay = MutableAppearance.Get(); + overlay.IconState = iconState; overlay.Icon ??= defaultIcon; } else if (atomManager.TryCreateAppearanceFrom(value, out var overlayAppearance)) { overlay = overlayAppearance; @@ -820,12 +818,12 @@ public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth public override void AddValue(DreamValue value) { EntityUid entity; if (value.TryGetValueAsDreamObject(out var movable)) { - if(_visContents.Contains(movable)) + if (_visContents.Contains(movable)) return; // vis_contents cannot contain duplicates _visContents.Add(movable); entity = movable.Entity; } else if (value.TryGetValueAsDreamObject(out var turf)) { - if(_visContents.Contains(turf)) + if (_visContents.Contains(turf)) return; // vis_contents cannot contain duplicates _visContents.Add(turf); entity = EntityUid.Invalid; // TODO: Support turfs in vis_contents @@ -883,9 +881,15 @@ public override void Cut(int start = 1, int end = 0) { } public int GetIndexOfFilter(DreamFilter filter) { - IconAppearance appearance = GetAppearance(); + ImmutableAppearance appearance = GetAppearance(); + int i = 0; + while(i < appearance.Filters.Length) { + if(appearance.Filters[i] == filter) + return i; + i++; + } - return appearance.Filters.IndexOf(filter) + 1; + return -1; } public void SetFilter(int index, DreamFilter? filter) { @@ -910,9 +914,9 @@ public override DreamValue GetValue(DreamValue key) { if (!key.TryGetValueAsInteger(out var filterIndex) || filterIndex < 1) throw new Exception($"Invalid index into filter list: {key}"); - IconAppearance appearance = GetAppearance(); - if (filterIndex > appearance.Filters.Count) - throw new Exception($"Atom only has {appearance.Filters.Count} filter(s), cannot index {filterIndex}"); + ImmutableAppearance appearance = GetAppearance(); + if (filterIndex > appearance.Filters.Length) + throw new Exception($"Atom only has {appearance.Filters.Length} filter(s), cannot index {filterIndex}"); DreamFilter filter = appearance.Filters[filterIndex - 1]; DreamObjectFilter filterObject = ObjectTree.CreateObject(ObjectTree.Filter); @@ -921,8 +925,8 @@ public override DreamValue GetValue(DreamValue key) { } public override List GetValues() { - IconAppearance appearance = GetAppearance(); - List filterList = new List(appearance.Filters.Count); + ImmutableAppearance appearance = GetAppearance(); + List filterList = new List(appearance.Filters.Length); foreach (var filter in appearance.Filters) { DreamObjectFilter filterObject = ObjectTree.CreateObject(ObjectTree.Filter); @@ -934,7 +938,7 @@ public override List GetValues() { } public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth = false) { - if (!value.TryGetValueAsDreamObject(out var filterObject) &&!value.IsNull) + if (!value.TryGetValueAsDreamObject(out var filterObject) && !value.IsNull) throw new Exception($"Cannot set value of filter list to {value}"); if (!key.TryGetValueAsInteger(out var filterIndex) || filterIndex < 1) throw new Exception($"Invalid index into filter list: {key}"); @@ -960,11 +964,11 @@ public override void AddValue(DreamValue value) { } public override int GetLength() { - return GetAppearance().Filters.Count; + return GetAppearance().Filters.Length; } - private IconAppearance GetAppearance() { - IconAppearance? appearance = _atomManager.MustGetAppearance(_owner); + private ImmutableAppearance GetAppearance() { + ImmutableAppearance? appearance = _atomManager.MustGetAppearance(_owner); if (appearance == null) throw new Exception("Atom has no appearance"); @@ -973,20 +977,10 @@ private IconAppearance GetAppearance() { } // client.screen list -public sealed class ClientScreenList : DreamList { - private readonly DreamObjectTree _objectTree; - private readonly ServerScreenOverlaySystem? _screenOverlaySystem; - - private readonly DreamConnection _connection; +public sealed class ClientScreenList(DreamObjectTree objectTree, ServerScreenOverlaySystem? screenOverlaySystem, DreamConnection connection) + : DreamList(objectTree.List.ObjectDefinition, 0) { private readonly List _screenObjects = new(); - public ClientScreenList(DreamObjectTree objectTree, ServerScreenOverlaySystem? screenOverlaySystem, DreamConnection connection) : base(objectTree.List.ObjectDefinition, 0) { - _objectTree = objectTree; - _screenOverlaySystem = screenOverlaySystem; - - _connection = connection; - } - public override bool ContainsValue(DreamValue value) { return _screenObjects.Contains(value); } @@ -1010,7 +1004,7 @@ public override void AddValue(DreamValue value) { if (!value.TryGetValueAsDreamObject(out var movable)) return; - _screenOverlaySystem?.AddScreenObject(_connection, movable); + screenOverlaySystem?.AddScreenObject(connection, movable); _screenObjects.Add(value); } @@ -1018,7 +1012,7 @@ public override void RemoveValue(DreamValue value) { if (!value.TryGetValueAsDreamObject(out var movable)) return; - _screenOverlaySystem?.RemoveScreenObject(_connection, movable); + screenOverlaySystem?.RemoveScreenObject(connection, movable); _screenObjects.Remove(value); } @@ -1029,7 +1023,7 @@ public override void Cut(int start = 1, int end = 0) { if (!_screenObjects[i].TryGetValueAsDreamObject(out var movable)) continue; - _screenOverlaySystem?.RemoveScreenObject(_connection, movable); + screenOverlaySystem?.RemoveScreenObject(connection, movable); } _screenObjects.RemoveRange(start - 1, end - start); @@ -1040,7 +1034,6 @@ public override int GetLength() { } } - // client.images list public sealed class ClientImagesList : DreamList { private readonly ServerClientImagesSystem? _clientImagesSystem; @@ -1167,7 +1160,7 @@ public override void AddValue(DreamValue value) { if (!value.TryGetValueAsDreamObject(out var movable)) throw new Exception($"Cannot add {value} to turf contents"); - movable.SetVariable("loc", new(Cell.Turf)); + movable.SetLoc(Cell.Turf); } public override void Cut(int start = 1, int end = 0) { @@ -1175,7 +1168,7 @@ public override void Cut(int start = 1, int end = 0) { if (end == 0 || end > movableCount) end = movableCount; for (int i = start; i < end; i++) { - Cell.Movables[i - 1].SetVariable("loc", DreamValue.Null); + Cell.Movables[i - 1].SetLoc(null); } } @@ -1253,6 +1246,81 @@ public override int GetLength() { } } +// mob.contents, obj.contents list +public sealed class MovableContentsList(DreamObjectDefinition listDef, DreamObjectMovable owner, TransformComponent transform) : DreamList(listDef, 0) { + public override DreamValue GetValue(DreamValue key) { + if (!key.TryGetValueAsInteger(out var index)) + throw new Exception($"Invalid index into movable contents list: {key}"); + if (index < 1 || index > transform.ChildCount) + throw new Exception($"Out of bounds index on movable contents list: {index}"); + + using var childEnumerator = transform.ChildEnumerator; + while (index >= 1) { + childEnumerator.MoveNext(out EntityUid child); + + if (index == 1) { + if (AtomManager.TryGetMovableFromEntity(child, out var childObject)) + return new DreamValue(childObject); + else + throw new Exception($"Invalid child in movable contents list: {child}"); + } + + index--; + } + + throw new Exception($"Out of bounds index on movable contents list after iterating: {key}"); + } + + public override List GetValues() { + List values = new List(transform.ChildCount); + using var childEnumerator = transform.ChildEnumerator; + + while (childEnumerator.MoveNext(out EntityUid child)) { + if (!AtomManager.TryGetMovableFromEntity(child, out var childObject)) + continue; + + values.Add(new DreamValue(childObject)); + } + + return values; + } + + public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth = false) { + throw new Exception("Cannot set an index of movable contents list"); + } + + public override void AddValue(DreamValue value) { + if (!value.TryGetValueAsDreamObject(out var dreamObject)) + throw new Exception($"Cannot add {value} to movable contents"); + + dreamObject.SetLoc(owner); + } + + public override void RemoveValue(DreamValue value) { + if (!value.TryGetValueAsDreamObject(out var movable)) + throw new Exception($"Cannot remove {value} from movable contents"); + if (movable.Loc != owner) + return; // This object wasn't in our contents to begin with + + movable.SetLoc(null); + } + + public override bool ContainsValue(DreamValue value) { + if (!value.TryGetValueAsDreamObject(out var dreamObject)) + return false; + + return dreamObject.Loc == owner; + } + + public override void Cut(int start = 1, int end = 0) { + // TODO + } + + public override int GetLength() { + return transform.ChildCount; + } +} + // proc args list internal sealed class ProcArgsList(DreamObjectDefinition listDef, DMProcState state) : DreamList(listDef, 0) { public override DreamValue GetValue(DreamValue key) { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs b/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs index e94f2c82d7..2a5d56f77f 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs @@ -1,4 +1,6 @@ -namespace OpenDreamRuntime.Objects.Types; +using OpenDreamShared.Dream; + +namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectArea : DreamObjectAtom { public int X { @@ -22,8 +24,8 @@ public int Z { } } + public ImmutableAppearance Appearance; public readonly HashSet Turfs; - public int AppearanceId; private readonly AreaContentsList _contents; @@ -31,9 +33,10 @@ public int Z { private int? _cachedX, _cachedY, _cachedZ; public DreamObjectArea(DreamObjectDefinition objectDefinition) : base(objectDefinition) { + Appearance = AppearanceSystem!.DefaultAppearance; Turfs = new(); - AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); _contents = new(ObjectTree.List.ObjectDefinition, this); + AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } /// diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index df8754eb48..20cba60f33 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -1,12 +1,9 @@ using OpenDreamRuntime.Procs; -using OpenDreamShared.Dream; namespace OpenDreamRuntime.Objects.Types; [Virtual] public class DreamObjectAtom : DreamObject { - public string? Name; - public string? Desc; public readonly DreamOverlaysList Overlays; public readonly DreamOverlaysList Underlays; public readonly DreamVisContentsList VisContents; @@ -22,11 +19,6 @@ public DreamObjectAtom(DreamObjectDefinition objectDefinition) : base(objectDefi AtomManager.AddAtom(this); } - public override void Initialize(DreamProcArguments args) { - ObjectDefinition.Variables["name"].TryGetValueAsString(out Name); - ObjectDefinition.Variables["desc"].TryGetValueAsString(out Desc); - } - protected override void HandleDeletion(bool possiblyThreaded) { // SAFETY: RemoveAtom is not threadsafe. if (possiblyThreaded) { @@ -39,6 +31,13 @@ protected override void HandleDeletion(bool possiblyThreaded) { base.HandleDeletion(possiblyThreaded); } + public string GetRTEntityDesc() { + if (AtomManager.TryGetAppearance(this, out var appearance) && appearance.Desc != null) + return appearance.Desc; + + return ObjectDefinition.Type; + } + protected override bool TryGetVar(string varName, out DreamValue value) { switch (varName) { // x/y/z/loc should be overriden by subtypes @@ -50,15 +49,8 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "loc": value = DreamValue.Null; return true; - - case "name": - value = (Name != null) ? new(Name) : DreamValue.Null; - return true; - case "desc": - value = (Desc != null) ? new(Desc) : DreamValue.Null; - return true; case "appearance": - var appearanceCopy = new IconAppearance(AtomManager.MustGetAppearance(this)!); + var appearanceCopy = AtomManager.MustGetAppearance(this).ToMutable(); value = new(appearanceCopy); return true; @@ -84,7 +76,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { default: if (AtomManager.IsValidAppearanceVar(varName)) { - var appearance = AtomManager.MustGetAppearance(this)!; + var appearance = AtomManager.MustGetAppearance(this); value = AtomManager.GetAppearanceVar(appearance, varName); return true; @@ -102,21 +94,15 @@ protected override void SetVar(string varName, DreamValue value) { case "z": case "loc": break; - - case "name": - value.TryGetValueAsString(out Name); - break; - case "desc": - value.TryGetValueAsString(out Desc); - break; case "appearance": if (!AtomManager.TryCreateAppearanceFrom(value, out var newAppearance)) return; // Ignore attempts to set an invalid appearance // The dir does not get changed - newAppearance.Direction = AtomManager.MustGetAppearance(this)!.Direction; + newAppearance.Direction = AtomManager.MustGetAppearance(this).Direction; AtomManager.SetAtomAppearance(this, newAppearance); + newAppearance.Dispose(); break; case "overlays": { Overlays.Cut(); @@ -178,12 +164,7 @@ protected override void SetVar(string varName, DreamValue value) { default: if (AtomManager.IsValidAppearanceVar(varName)) { // Basically AtomManager.UpdateAppearance() but without the performance impact of using actions - var appearance = AtomManager.MustGetAppearance(this); - - // Clone the appearance - // TODO: We can probably avoid cloning while the DMISpriteComponent is dirty - appearance = (appearance != null) ? new(appearance) : new(); - + using var appearance = AtomManager.MustGetAppearance(this).ToMutable(); AtomManager.SetAppearanceVar(appearance, varName, value); AtomManager.SetAtomAppearance(this, appearance); break; diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs index 97cfae5a57..4f208c090f 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs @@ -6,13 +6,14 @@ namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectImage : DreamObject { - public IconAppearance? Appearance; - + public EntityUid Entity = EntityUid.Invalid; + public readonly DMISpriteComponent? SpriteComponent; private DreamObject? _loc; private DreamList _overlays; private DreamList _underlays; private readonly DreamList _filters; - private EntityUid _entity = EntityUid.Invalid; + public readonly bool IsMutableAppearance; + public MutableAppearance? MutableAppearance; /// /// All the args in /image/New() after "icon" and "loc", in their correct order @@ -31,20 +32,26 @@ public DreamObjectImage(DreamObjectDefinition objectDefinition) : base(objectDef _overlays = ObjectTree.CreateList(); _underlays = ObjectTree.CreateList(); _filters = ObjectTree.CreateList(); + IsMutableAppearance = true; } else { _overlays = new DreamOverlaysList(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, false); _underlays = new DreamOverlaysList(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, true); _filters = new DreamFilterList(ObjectTree.List.ObjectDefinition, this); + IsMutableAppearance = false; + Entity = EntityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); //spawning an entity in nullspace means it never actually gets sent to any clients until it's placed on the map, or it gets a PVS override + SpriteComponent = EntityManager.AddComponent(Entity); } + + AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } public override void Initialize(DreamProcArguments args) { base.Initialize(args); DreamValue icon = args.GetArgument(0); - if (icon.IsNull || !AtomManager.TryCreateAppearanceFrom(icon, out Appearance)) { + if (icon.IsNull || !AtomManager.TryCreateAppearanceFrom(icon, out var mutableAppearance)) { // Use a default appearance, but log a warning about it if icon wasn't null - Appearance = new(AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); + mutableAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); //object def appearance is created in the constructor if (!icon.IsNull) Logger.GetSawmill("opendream.image") .Warning($"Attempted to create an /image from {icon}. This is invalid and a default image was created instead."); @@ -61,14 +68,17 @@ public override void Initialize(DreamProcArguments args) { if (arg.IsNull) continue; - AtomManager.SetAppearanceVar(Appearance, argName, arg); + AtomManager.SetAppearanceVar(mutableAppearance, argName, arg); if (argName == "dir" && arg.TryGetValueAsInteger(out var argDir) && argDir > 0) { // If a dir is explicitly given in the constructor then overlays using this won't use their owner's dir // Setting dir after construction does not affect this // This is undocumented and I hate it - Appearance.InheritsDirection = false; + mutableAppearance.InheritsDirection = false; } } + + AtomManager.SetAtomAppearance(this, mutableAppearance); + mutableAppearance.Dispose(); } protected override bool TryGetVar(string varName, out DreamValue value) { @@ -89,7 +99,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { return true; default: { if (AtomManager.IsValidAppearanceVar(varName)) { - value = AtomManager.GetAppearanceVar(Appearance!, varName); + value = IsMutableAppearance ? AtomManager.GetAppearanceVar(MutableAppearance!, varName) : AtomManager.GetAppearanceVar(AtomManager.MustGetAppearance(this), varName); return true; } else { return base.TryGetVar(varName, out value); @@ -105,14 +115,10 @@ protected override void SetVar(string varName, DreamValue value) { return; // Ignore attempts to set an invalid appearance // The dir does not get changed - newAppearance.Direction = Appearance!.Direction; - - Appearance = newAppearance; - if(_entity != EntityUid.Invalid) { - DMISpriteComponent sprite = EntityManager.GetComponent(_entity); - sprite.SetAppearance(Appearance!); - } - + var originalAppearance = AtomManager.MustGetAppearance(this); + newAppearance.Direction = originalAppearance.Direction; + AtomManager.SetAtomAppearance(this, newAppearance); + newAppearance.Dispose(); break; case "loc": value.TryGetValueAsDreamObject(out _loc); @@ -128,12 +134,13 @@ protected override void SetVar(string varName, DreamValue value) { if (valueList != null) { _overlays = valueList.CreateCopy(); } else { - var overlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, Appearance?.Icon); + var overlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, AtomManager.MustGetAppearance(this).Icon); if (overlay == null) return; _overlays.Cut(); _overlays.AddValue(new(overlay)); + overlay.Dispose(); } return; @@ -160,12 +167,13 @@ protected override void SetVar(string varName, DreamValue value) { if (valueList != null) { _underlays = valueList.CreateCopy(); } else { - var underlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, Appearance?.Icon); + var underlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, AtomManager.MustGetAppearance(this).Icon); if (underlay == null) return; _underlays.Cut(); _underlays.AddValue(new(underlay)); + underlay.Dispose(); } return; @@ -202,17 +210,16 @@ protected override void SetVar(string varName, DreamValue value) { break; } case "override": { - Appearance!.Override = value.IsTruthy(); + using var mutableAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); + mutableAppearance.Override = value.IsTruthy(); + AtomManager.SetAtomAppearance(this, mutableAppearance); break; } default: if (AtomManager.IsValidAppearanceVar(varName)) { - AtomManager.SetAppearanceVar(Appearance!, varName, value); - if(_entity != EntityUid.Invalid) { - DMISpriteComponent sprite = EntityManager.GetComponent(_entity); - sprite.SetAppearance(Appearance!); - } - + using var mutableAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); + AtomManager.SetAppearanceVar(mutableAppearance, varName, value); + AtomManager.SetAtomAppearance(this, mutableAppearance); break; } @@ -225,20 +232,6 @@ protected override void SetVar(string varName, DreamValue value) { return this._loc; } - /// - /// Get or create the entity associated with this image. Used for putting this image in the world ie, with vis_contents - /// The associated entity is deleted when the image is. - /// - public EntityUid GetEntity() { - if(_entity == EntityUid.Invalid) { - _entity = EntityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); - DMISpriteComponent sprite = EntityManager.AddComponent(_entity); - sprite.SetAppearance(Appearance!); - } - - return _entity; - } - protected override void HandleDeletion(bool possiblyThreaded) { // SAFETY: Deleting entities is not threadsafe. if (possiblyThreaded) { @@ -246,10 +239,11 @@ protected override void HandleDeletion(bool possiblyThreaded) { return; } - if(_entity != EntityUid.Invalid) { - EntityManager.DeleteEntity(_entity); + if(Entity != EntityUid.Invalid) { + EntityManager.DeleteEntity(Entity); } + MutableAppearance?.Dispose(); base.HandleDeletion(possiblyThreaded); } } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs index ed1a311ddb..a4b3e3d7c4 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs @@ -259,7 +259,8 @@ public override DreamValue OperatorRemove(DreamValue b) { #endregion Operators #region Helpers - /// Used to create a float array understandable by to be a transform. + + /// Used to create a float array understandable by to be a transform. /// The matrix's values in an array, in [a,d,b,e,c,f] order. /// This will not verify that this is a /matrix public static float[] MatrixToTransformFloatArray(DreamObjectMatrix matrix) { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs index 9b38720942..685aeced7f 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs @@ -1,4 +1,4 @@ -using OpenDreamRuntime.Procs; +using OpenDreamRuntime.Procs; using OpenDreamRuntime.Rendering; using OpenDreamShared.Dream; using Robust.Shared.Map; @@ -18,27 +18,21 @@ public class DreamObjectMovable : DreamObjectAtom { public int Z => (int)_transformComponent.MapID; private readonly TransformComponent _transformComponent; - + private readonly MovableContentsList _contents; + private string? _screenLoc; private string? ScreenLoc { get => _screenLoc; - set { - _screenLoc = value; - if (!EntityManager.TryGetComponent(Entity, out var sprite)) - return; - - sprite.ScreenLocation = !string.IsNullOrEmpty(value) ? - new ScreenLocation(value) : - new ScreenLocation(0, 0, 0, 0); - } + set => SetScreenLoc(value); } - private string? _screenLoc; - public DreamObjectMovable(DreamObjectDefinition objectDefinition) : base(objectDefinition) { Entity = AtomManager.CreateMovableEntity(this); SpriteComponent = EntityManager.GetComponent(Entity); + AtomManager.SetSpriteAppearance((Entity, SpriteComponent), AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); + _transformComponent = EntityManager.GetComponent(Entity); + _contents = new MovableContentsList(ObjectTree.List.ObjectDefinition, this, _transformComponent); } public override void Initialize(DreamProcArguments args) { @@ -49,7 +43,7 @@ public override void Initialize(DreamProcArguments args) { if (EntityManager.TryGetComponent(Entity, out MetaDataComponent? metaData)) { MetaDataSystem?.SetEntityName(Entity, GetDisplayName(), metaData); - MetaDataSystem?.SetEntityDescription(Entity, Desc ?? string.Empty, metaData); + MetaDataSystem?.SetEntityDescription(Entity, GetRTEntityDesc(), metaData); } args.GetArgument(0).TryGetValueAsDreamObject(out var loc); @@ -83,22 +77,15 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "loc": value = new(Loc); return true; + case "bound_width": + case "bound_height": + value = new(32); // TODO: Custom bounds support + return true; case "screen_loc": value = (ScreenLoc != null) ? new(ScreenLoc) : DreamValue.Null; return true; case "contents": - DreamList contents = ObjectTree.CreateList(); - - using (var childEnumerator = _transformComponent.ChildEnumerator) { - while (childEnumerator.MoveNext(out EntityUid child)) { - if (!AtomManager.TryGetMovableFromEntity(child, out var childAtom)) - continue; - - contents.AddValue(new DreamValue(childAtom)); - } - } - - value = new(contents); + value = new(_contents); return true; case "locs": // Unimplemented; just return a list containing src.loc @@ -157,7 +144,7 @@ protected override void SetVar(string varName, DreamValue value) { } } - private void SetLoc(DreamObjectAtom? loc) { + public void SetLoc(DreamObjectAtom? loc) { Loc = loc; if (TransformSystem == null) return; @@ -204,4 +191,9 @@ private void SetLoc(DreamObjectAtom? loc) { throw new ArgumentException($"Invalid loc {loc}"); } } + + private void SetScreenLoc(string? screenLoc) { + _screenLoc = screenLoc; + AtomManager.SetMovableScreenLoc(this, !string.IsNullOrEmpty(screenLoc) ? new ScreenLocation(screenLoc) : new ScreenLocation(0, 0, 0, 0)); + } } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs b/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs index 81feba9d27..102fe44855 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs @@ -230,7 +230,8 @@ public static void FlushAllUpdates() { _sawmill ??= Logger.GetSawmill("opendream.res"); foreach (DreamObjectSavefile savefile in SavefilesToFlush) { try { - savefile.Flush(); + // We need to avoid Flush() recreating a nonexistent file + if (File.Exists(savefile.Resource!.ResourcePath)) savefile.Flush(); } catch (Exception e) { _sawmill.Error($"Error flushing savefile {savefile.Resource!.ResourcePath}: {e}"); } @@ -239,10 +240,12 @@ public static void FlushAllUpdates() { } public void Close() { - Flush(); + // We need to avoid Flush() recreating a nonexistent file + if (File.Exists(Resource!.ResourcePath)) Flush(); if (_isTemporary && Resource?.ResourcePath != null) { File.Delete(Resource.ResourcePath); } + //check to see if the file is still in use by another savefile datum if(Resource?.ResourcePath != null) { var fineToDelete = true; @@ -255,7 +258,9 @@ public void Close() { if (fineToDelete) SavefileDirectories.Remove(Resource.ResourcePath); } + Savefiles.Remove(this); + SavefilesToFlush.Remove(this); } public void Flush() { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs index 7248f524f6..b5ccd7404e 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs @@ -1,17 +1,24 @@ -namespace OpenDreamRuntime.Objects.Types; +using OpenDreamRuntime.Map; +using OpenDreamShared.Dream; + +namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectTurf : DreamObjectAtom { public readonly int X, Y, Z; public readonly TurfContentsList Contents; + public ImmutableAppearance Appearance; public IDreamMapManager.Cell Cell; - public int AppearanceId; + + public bool IsDense => GetVariable("density").IsTruthy(); public DreamObjectTurf(DreamObjectDefinition objectDefinition, int x, int y, int z) : base(objectDefinition) { X = x; Y = y; Z = z; + Cell = default!; // NEEDS to be set by DreamMapManager after creation Contents = new TurfContentsList(ObjectTree.List.ObjectDefinition, this); + Appearance = AppearanceSystem!.AddAppearance(AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } public void SetTurfType(DreamObjectDefinition objectDefinition) { @@ -24,6 +31,16 @@ public void SetTurfType(DreamObjectDefinition objectDefinition) { Initialize(new()); } + public void OnAreaChange(DreamObjectArea oldArea) { + if (Cell == null!) + return; + + using var newAppearance = Appearance.ToMutable(); + + newAppearance.Overlays.Remove(oldArea.Appearance); + DreamMapManager.SetTurfAppearance(this, newAppearance); + } + protected override bool TryGetVar(string varName, out DreamValue value) { switch (varName) { case "x": diff --git a/OpenDreamRuntime/OpenDreamRuntime.csproj b/OpenDreamRuntime/OpenDreamRuntime.csproj index e6858df7e9..bd5a6e2c75 100644 --- a/OpenDreamRuntime/OpenDreamRuntime.csproj +++ b/OpenDreamRuntime/OpenDreamRuntime.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 enable true Debug;Release;Tools diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 3eca92a72c..e6dc9e133a 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -923,6 +923,9 @@ public static ProcStatus BitShiftLeft(DMProcState state) { case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: state.Push(new DreamValue(first.MustGetValueAsInteger() << second.MustGetValueAsInteger())); break; + case DreamValue.DreamValueType.Float when second.IsNull: + state.Push(new DreamValue(first.MustGetValueAsInteger())); + break; default: throw new Exception($"Invalid bit shift left operation on {first} and {second}"); } @@ -930,7 +933,6 @@ public static ProcStatus BitShiftLeft(DMProcState state) { return ProcStatus.Continue; } - public static ProcStatus BitShiftLeftReference(DMProcState state) { DreamReference reference = state.ReadReference(); DreamValue second = state.Pop(); @@ -943,9 +945,13 @@ public static ProcStatus BitShiftLeftReference(DMProcState state) { case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: result = new DreamValue(first.MustGetValueAsInteger() << second.MustGetValueAsInteger()); break; + case DreamValue.DreamValueType.Float when second.IsNull: + result = new DreamValue(first.MustGetValueAsInteger()); + break; default: throw new Exception($"Invalid bit shift left operation on {first} and {second}"); } + state.AssignReference(reference, result); state.Push(result); return ProcStatus.Continue; @@ -955,12 +961,18 @@ public static ProcStatus BitShiftRight(DMProcState state) { DreamValue second = state.Pop(); DreamValue first = state.Pop(); - if (first.IsNull) { - state.Push(new DreamValue(0)); - } else if (first.Type == DreamValue.DreamValueType.Float && second.Type == DreamValue.DreamValueType.Float) { - state.Push(new DreamValue(first.MustGetValueAsInteger() >> second.MustGetValueAsInteger())); - } else { - throw new Exception($"Invalid bit shift right operation on {first} and {second}"); + switch (first.Type) { + case DreamValue.DreamValueType.DreamObject when first.IsNull: + state.Push(new DreamValue(0)); + break; + case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: + state.Push(new DreamValue(first.MustGetValueAsInteger() >> second.MustGetValueAsInteger())); + break; + case DreamValue.DreamValueType.Float when second.IsNull: + state.Push(new DreamValue(first.MustGetValueAsInteger())); + break; + default: + throw new Exception($"Invalid bit shift right operation on {first} and {second}"); } return ProcStatus.Continue; @@ -978,9 +990,13 @@ public static ProcStatus BitShiftRightReference(DMProcState state) { case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: result = new DreamValue(first.MustGetValueAsInteger() >> second.MustGetValueAsInteger()); break; + case DreamValue.DreamValueType.Float when second.IsNull: + result = new DreamValue(first.MustGetValueAsInteger()); + break; default: throw new Exception($"Invalid bit shift right operation on {first} and {second}"); } + state.AssignReference(reference, result); state.Push(result); return ProcStatus.Continue; @@ -1072,20 +1088,30 @@ public static ProcStatus Combine(DMProcState state) { public static ProcStatus Divide(DMProcState state) { DreamValue second = state.Pop(); DreamValue first = state.Pop(); - if (first.IsNull) { - state.Push(new(0)); - } else if (first.TryGetValueAsFloat(out var firstFloat) && second.TryGetValueAsFloat(out var secondFloat)) { - if (secondFloat == 0) { - throw new Exception("Division by zero"); - } - state.Push(new(firstFloat / secondFloat)); - } else if (first.TryGetValueAsDreamObject(out var firstDreamObject)) { - var result = firstDreamObject.OperatorDivide(second, state); - state.Push(result); - } else { - throw new Exception($"Invalid divide operation on {first} and {second}"); + switch (first.Type) { + case DreamValue.DreamValueType.DreamObject when first.IsNull: + state.Push(new DreamValue(0)); + break; + case DreamValue.DreamValueType.Float when second.IsNull: + state.Push(new DreamValue(first.MustGetValueAsFloat())); + break; + case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: + var secondFloat = second.MustGetValueAsFloat(); + if (secondFloat == 0) { + throw new Exception("Division by zero"); + } + + state.Push(new DreamValue(first.MustGetValueAsFloat() / secondFloat)); + break; + case DreamValue.DreamValueType.DreamObject: + var result = first.MustGetValueAsDreamObject()!.OperatorDivide(second, state); + state.Push(result); + break; + default: + throw new Exception($"Invalid divide operation on {first} and {second}"); } + return ProcStatus.Continue; } @@ -1128,6 +1154,9 @@ public static ProcStatus Mask(DMProcState state) { case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: result = new DreamValue(first.MustGetValueAsInteger() & second.MustGetValueAsInteger()); break; + case DreamValue.DreamValueType.Float when second.IsNull: + result = new DreamValue(0); + break; default: throw new Exception("Invalid mask operation on " + first + " and " + second); } @@ -2447,6 +2476,27 @@ public static ProcStatus Prompt(DMProcState state) { return ProcStatus.Called; } + public static ProcStatus Link(DMProcState state) { + DreamValue url = state.Pop(); + if (!state.Pop().TryGetValueAsDreamObject(out var receiver) || receiver == null) + return ProcStatus.Continue; + + DreamConnection? connection = receiver switch { + DreamObjectMob receiverMob => receiverMob.Connection, + DreamObjectClient receiverClient => receiverClient.Connection, + _ => throw new Exception("Invalid link() recipient") + }; + + if (!url.TryGetValueAsString(out var urlStr)) { + throw new Exception($"Invalid link() url: {url}"); + } else if (string.IsNullOrWhiteSpace(urlStr)) { + return ProcStatus.Continue; + } + + connection?.SendLink(urlStr); + return ProcStatus.Continue; + } + public static ProcStatus Ftp(DMProcState state) { DreamValue name = state.Pop(); DreamValue file = state.Pop(); @@ -2520,6 +2570,17 @@ public static ProcStatus DereferenceIndex(DMProcState state) { return ProcStatus.Continue; } + public static ProcStatus IndexRefWithString(DMProcState state) { + DreamReference reference = state.ReadReference(); + var refValue = state.GetReferenceValue(reference); + + var index = new DreamValue(state.ReadString()); + var indexResult = state.GetIndex(refValue, index, state); + + state.Push(indexResult); + return ProcStatus.Continue; + } + public static ProcStatus DereferenceCall(DMProcState state) { string name = state.ReadString(); var argumentInfo = state.ReadProcArguments(); @@ -2613,7 +2674,7 @@ private static bool IsEqual(DreamValue first, DreamValue second) { if (!second.TryGetValueAsAppearance(out var secondValue)) return false; - IconAppearance firstValue = first.MustGetValueAsAppearance(); + MutableAppearance firstValue = first.MustGetValueAsAppearance(); return firstValue.Equals(secondValue); } } @@ -2692,8 +2753,19 @@ private static DreamValue BitXorValues(DreamObjectTree objectTree, DreamValue fi } return new DreamValue(newList); - } else { - return new DreamValue(first.MustGetValueAsInteger() ^ second.MustGetValueAsInteger()); + } + + switch (first.Type) { + case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: + return new DreamValue(first.MustGetValueAsInteger() ^ second.MustGetValueAsInteger()); + case DreamValue.DreamValueType.DreamObject when first.IsNull && second.IsNull: + return DreamValue.Null; + case DreamValue.DreamValueType.DreamObject when first.IsNull && second.Type == DreamValue.DreamValueType.Float: + return new DreamValue(second.MustGetValueAsInteger()); + case DreamValue.DreamValueType.Float when second.IsNull: + return new DreamValue(first.MustGetValueAsInteger()); + default: + throw new Exception($"Invalid xor operation on {first} and {second}"); } } diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index 1ab4d5c0dd..9b8a536854 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -5,6 +5,7 @@ using DMCompiler.Bytecode; using DMCompiler.DM; using DMCompiler.Json; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs.DebugAdapter; @@ -247,6 +248,7 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.CreateFilteredListEnumerator, DMOpcodeHandlers.CreateFilteredListEnumerator}, {DreamProcOpcode.Power, DMOpcodeHandlers.Power}, {DreamProcOpcode.Prompt, DMOpcodeHandlers.Prompt}, + {DreamProcOpcode.Link, DMOpcodeHandlers.Link}, {DreamProcOpcode.Ftp, DMOpcodeHandlers.Ftp}, {DreamProcOpcode.Initial, DMOpcodeHandlers.Initial}, {DreamProcOpcode.IsType, DMOpcodeHandlers.IsType}, @@ -279,6 +281,7 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.JumpIfFalseReference, DMOpcodeHandlers.JumpIfFalseReference}, {DreamProcOpcode.DereferenceField, DMOpcodeHandlers.DereferenceField}, {DreamProcOpcode.DereferenceIndex, DMOpcodeHandlers.DereferenceIndex}, + {DreamProcOpcode.IndexRefWithString, DMOpcodeHandlers.IndexRefWithString}, {DreamProcOpcode.DereferenceCall, DMOpcodeHandlers.DereferenceCall}, {DreamProcOpcode.PopReference, DMOpcodeHandlers.PopReference}, {DreamProcOpcode.BitShiftLeftReference,DMOpcodeHandlers.BitShiftLeftReference}, diff --git a/OpenDreamRuntime/Procs/DllHelper.cs b/OpenDreamRuntime/Procs/DllHelper.cs index e52e262fdb..6f115db549 100644 --- a/OpenDreamRuntime/Procs/DllHelper.cs +++ b/OpenDreamRuntime/Procs/DllHelper.cs @@ -27,7 +27,7 @@ private static nint GetDll(DreamResourceManager resource, string dllName) return dll; if (!TryResolveDll(resource, dllName, out dll)) - throw new DllNotFoundException($"FFI: Unable to load {dllName}, unknown error."); //unknown because NativeLibrary doesn't give any error information. + throw new DllNotFoundException($"FFI: Unable to load {dllName}, unknown error. Did you remember to build a 64-bit DLL instead of 32-bit?"); //unknown because NativeLibrary doesn't give any error information. LoadedDlls.Add(dllName, dll); return dll; @@ -40,7 +40,7 @@ private static bool TryResolveDll(DreamResourceManager resource, string dllName, // Simple load didn't pass, try next to dmb. if(!File.Exists(dllName)) - throw new DllNotFoundException($"FFI: Unable to load DLL {dllName}."); + throw new DllNotFoundException($"FFI: Unable to load DLL {dllName}. File not found."); return NativeLibrary.TryLoad(dllName, out dll); } } diff --git a/OpenDreamRuntime/Procs/DreamEnumerators.cs b/OpenDreamRuntime/Procs/DreamEnumerators.cs index b954ea507b..6cd5583e31 100644 --- a/OpenDreamRuntime/Procs/DreamEnumerators.cs +++ b/OpenDreamRuntime/Procs/DreamEnumerators.cs @@ -47,8 +47,12 @@ public DreamObjectEnumerator(IEnumerable dreamObjects, TreeEntry? f public bool Enumerate(DMProcState state, DreamReference? reference) { bool success = _dreamObjectEnumerator.MoveNext(); + + while(success && _dreamObjectEnumerator.Current.Deleted) //skip over deleted + success = _dreamObjectEnumerator.MoveNext(); + if (_filterType != null) { - while (success && !_dreamObjectEnumerator.Current.IsSubtypeOf(_filterType)) { + while (success && (_dreamObjectEnumerator.Current.Deleted || !_dreamObjectEnumerator.Current.IsSubtypeOf(_filterType))) { success = _dreamObjectEnumerator.MoveNext(); } } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index c9da8e3c87..6ed7ebd2f3 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -32,6 +32,8 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_floor); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_fract); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_ftime); + objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_get_step_to); + objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_get_steps_to); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_hascall); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_hearers); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_html_decode); @@ -145,14 +147,14 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.Savefile, DreamProcNativeSavefile.NativeProc_ExportText); objectTree.SetNativeProc(objectTree.Savefile, DreamProcNativeSavefile.NativeProc_Flush); - + objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_Export); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_GetConfig); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_Profile); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_SetConfig); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_ODHotReloadInterface); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_ODHotReloadResource); - + objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_Close); objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_Error); objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_ErrorMsg); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs index fb85fda761..70bc2c820f 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using OpenDreamRuntime.Objects.Types; using System.Text; +using OpenDreamRuntime.Map; namespace OpenDreamRuntime.Procs.Native; @@ -177,7 +178,7 @@ public static (DreamObjectAtom?, ViewRange) ResolveViewArguments(DreamManager dr if (!mapManager.TryGetCellAt((eyePos.X + deltaX, eyePos.Y + deltaY), eyePos.Z, out var cell)) continue; - var appearance = atomManager.MustGetAppearance(cell.Turf!)!; + var appearance = atomManager.MustGetAppearance(cell.Turf); var tile = new ViewAlgorithm.Tile() { Opaque = appearance.Opacity, Luminosity = 0, @@ -186,7 +187,7 @@ public static (DreamObjectAtom?, ViewRange) ResolveViewArguments(DreamManager dr }; foreach (var movable in cell.Movables) { - appearance = atomManager.MustGetAppearance(movable)!; + appearance = atomManager.MustGetAppearance(movable); tile.Opaque |= appearance.Opacity; } @@ -541,21 +542,26 @@ public static AtomDirection GetDir(AtomManager atomManager, DreamObjectAtom loc1 var loc1Pos = atomManager.GetAtomPosition(loc1); var loc2Pos = atomManager.GetAtomPosition(loc2); - if (loc1Pos.Z != loc2Pos.Z) // They must be on the same z-level + return GetDir(loc1Pos, loc2Pos); + } + + /// + public static AtomDirection GetDir((int X, int Y, int Z) loc1, (int X, int Y, int Z) loc2) { + if (loc1.Z != loc2.Z) // They must be on the same z-level return 0; AtomDirection direction = AtomDirection.None; // East or West - if (loc2Pos.X < loc1Pos.X) + if (loc2.X < loc1.X) direction |= AtomDirection.West; - else if (loc2Pos.X > loc1Pos.X) + else if (loc2.X > loc1.X) direction |= AtomDirection.East; // North or South - if (loc2Pos.Y < loc1Pos.Y) + if (loc2.Y < loc1.Y) direction |= AtomDirection.South; - else if (loc2Pos.Y > loc1Pos.Y) + else if (loc2.Y > loc1.Y) direction |= AtomDirection.North; return direction; diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs index a06a823d57..09898b5d1d 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs @@ -117,7 +117,7 @@ DreamValue DoTextReplace(string replacement) { if(!regex.IsGlobal) { var match = regex.Regex.Match(haystackString, Math.Clamp(start - 1, 0, haystackSubstring.Length)); if (!match.Success) return new DreamValue(haystackString); - regexInstance.SetVariable("next", new DreamValue(match.Index + Math.Max(replacement.Length, 1))); + regexInstance.SetVariable("next", new DreamValue(match.Index + Math.Max(replacement.Length, 1) + 1)); } string replaced = regex.Regex.Replace(haystackSubstring, replacement, regex.IsGlobal ? -1 : 1, diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 8c24545377..f327495fb1 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; @@ -156,11 +156,12 @@ public static DreamValue NativeProc_animate(NativeProc.Bundle bundle, DreamObjec return DreamValue.Null; chainAnim = true; } - + bundle.LastAnimatedObject = new DreamValue(obj); if(obj.IsSubtypeOf(bundle.ObjectTree.Filter)) {//TODO animate filters return DreamValue.Null; } + // TODO: Is this the correct behavior for invalid time? if (!bundle.GetArgument(1, "time").TryGetValueAsFloat(out float time)) return DreamValue.Null; @@ -210,14 +211,16 @@ public static DreamValue NativeProc_animate(NativeProc.Bundle bundle, DreamObjec /* TODO these are not yet implemented if(!pixelZ.IsNull) pixelZ = new(pixelZ.UnsafeGetValueAsFloat() + obj.GetVariable("pixel_z").UnsafeGetValueAsFloat()); //TODO change to appearance when pixel_z is implemented + */ if(!maptextWidth.IsNull) - maptextWidth = new(maptextWidth.UnsafeGetValueAsFloat() + obj.GetVariable("maptext_width").UnsafeGetValueAsFloat()); //TODO change to appearance when maptext_width is implemented + maptextWidth = new(maptextWidth.UnsafeGetValueAsFloat() + appearance.MaptextSize.X); if(!maptextHeight.IsNull) - maptextHeight = new(maptextHeight.UnsafeGetValueAsFloat() + obj.GetVariable("maptext_height").UnsafeGetValueAsFloat()); //TODO change to appearance when maptext_height is implemented + maptextHeight = new(maptextHeight.UnsafeGetValueAsFloat() + appearance.MaptextSize.Y); if(!maptextX.IsNull) - maptextX = new(maptextX.UnsafeGetValueAsFloat() + obj.GetVariable("maptext_x").UnsafeGetValueAsFloat()); //TODO change to appearance when maptext_x is implemented + maptextX = new(maptextX.UnsafeGetValueAsFloat() + appearance.MaptextOffset.X); if(!maptextY.IsNull) - maptextY = new(maptextY.UnsafeGetValueAsFloat() + obj.GetVariable("maptext_y").UnsafeGetValueAsFloat()); //TODO change to appearance when maptext_y is implemented + maptextY = new(maptextY.UnsafeGetValueAsFloat() + appearance.MaptextOffset.Y); + /* if(!luminosity.IsNull) luminosity = new(luminosity.UnsafeGetValueAsFloat() + obj.GetVariable("luminosity").UnsafeGetValueAsFloat()); //TODO change to appearance when luminosity is implemented */ @@ -273,17 +276,30 @@ public static DreamValue NativeProc_animate(NativeProc.Bundle bundle, DreamObjec } */ - /* TODO maptext if (!maptextX.IsNull) { obj.SetVariableValue("maptext_x", maptextX); - maptextX.TryGetValueAsInteger(out appearance.MapTextOffset.X); + maptextX.TryGetValueAsInteger(out appearance.MaptextOffset.X); } if (!maptextY.IsNull) { obj.SetVariableValue("maptext_y", maptextY); - maptextY.TryGetValueAsInteger(out appearance.MapTextOffset.Y); + maptextY.TryGetValueAsInteger(out appearance.MaptextOffset.Y); + } + + if (!maptextWidth.IsNull) { + obj.SetVariableValue("maptext_width", maptextWidth); + maptextX.TryGetValueAsInteger(out appearance.MaptextSize.X); + } + + if (!maptextHeight.IsNull) { + obj.SetVariableValue("maptext_y", maptextHeight); + maptextY.TryGetValueAsInteger(out appearance.MaptextSize.Y); + } + + if(!maptext.IsNull){ + obj.SetVariableValue("maptext", maptext); + maptext.TryGetValueAsString(out appearance.Maptext); } - */ if (!dir.IsNull) { obj.SetVariableValue("dir", dir); @@ -700,7 +716,6 @@ public static DreamValue NativeProc_file2text(NativeProc.Bundle bundle, DreamObj DreamValue file = bundle.GetArgument(0, "File"); DreamResource? resource; - if (file.TryGetValueAsString(out var rscPath)) { resource = bundle.ResourceManager.LoadResource(rscPath); } else if (!file.TryGetValueAsDreamResource(out resource)) { @@ -993,11 +1008,71 @@ public static DreamValue NativeProc_ftime(NativeProc.Bundle bundle, DreamObject? if (isCreationTime.IsTruthy()) { return new DreamValue((fi.CreationTime - new DateTime(2000, 1, 1)).TotalMilliseconds / 100); } + return new DreamValue((fi.LastWriteTime - new DateTime(2000, 1, 1)).TotalMilliseconds / 100); } + throw new Exception("Invalid path argument"); } + [DreamProc("get_step_to")] + [DreamProcParameter("Ref", Type = DreamValueTypeFlag.DreamObject)] + [DreamProcParameter("Trg", Type = DreamValueTypeFlag.DreamObject)] + [DreamProcParameter("Min", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + public static DreamValue NativeProc_get_step_to(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var refArg = bundle.GetArgument(0, "Ref"); + var trgArg = bundle.GetArgument(1, "Trg"); + var minArg = (int)bundle.GetArgument(2, "Min").UnsafeGetValueAsFloat(); + + if (!refArg.TryGetValueAsDreamObject(out var refAtom)) + return DreamValue.Null; + if (!trgArg.TryGetValueAsDreamObject(out var trgAtom)) + return DreamValue.Null; + + var loc = bundle.AtomManager.GetAtomPosition(refAtom); + var dest = bundle.AtomManager.GetAtomPosition(trgAtom); + var steps = bundle.MapManager.CalculateSteps(loc, dest, minArg); + + // We perform a whole path-find then return only the first step + // Truly, DM's most optimized proc + var direction = steps.FirstOrDefault(); + var stepLoc = direction switch { + // The ref says get_step_to() returns 0 if there's no change, but it also says it returns null. + // I wasn't able to get it to return 0 so null it is. + 0 => null, + _ => DreamProcNativeHelpers.GetStep(bundle.AtomManager, bundle.MapManager, refAtom, direction) + }; + + return new(stepLoc); + } + + [DreamProc("get_steps_to")] + [DreamProcParameter("Ref", Type = DreamValueTypeFlag.DreamObject)] + [DreamProcParameter("Trg", Type = DreamValueTypeFlag.DreamObject)] + [DreamProcParameter("Min", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + public static DreamValue NativeProc_get_steps_to(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var refArg = bundle.GetArgument(0, "Ref"); + var trgArg = bundle.GetArgument(1, "Trg"); + var minArg = (int)bundle.GetArgument(2, "Min").UnsafeGetValueAsFloat(); + + if (!refArg.TryGetValueAsDreamObject(out var refAtom)) + return DreamValue.Null; + if (!trgArg.TryGetValueAsDreamObject(out var trgAtom)) + return DreamValue.Null; + + var loc = bundle.AtomManager.GetAtomPosition(refAtom); + var dest = bundle.AtomManager.GetAtomPosition(trgAtom); + var steps = bundle.MapManager.CalculateSteps(loc, dest, minArg); + var result = bundle.ObjectTree.CreateList(); + + foreach (var step in steps) { + result.AddValue(new((int)step)); + } + + // Null if there are no steps + return new(result.GetLength() > 0 ? result : null); + } + [DreamProc("hascall")] [DreamProcParameter("Object", Type = DreamValueTypeFlag.DreamObject)] [DreamProcParameter("ProcName", Type = DreamValueTypeFlag.String)] @@ -3024,6 +3099,14 @@ public static DreamValue NativeProc_view(NativeProc.Bundle bundle, DreamObject? if (center is null) return new(view); + if (center.TryGetVariable("contents", out var centerContents) && centerContents.TryGetValueAsDreamList(out var centerContentsList)) { + foreach (var item in centerContentsList.GetValues()) { + view.AddValue(item); + } + } + + // Center gets included during the walk through the tiles + var eyePos = bundle.AtomManager.GetAtomPosition(center); var viewData = DreamProcNativeHelpers.CollectViewData(bundle.AtomManager, bundle.MapManager, eyePos, range); diff --git a/OpenDreamRuntime/Procs/NativeProc.cs b/OpenDreamRuntime/Procs/NativeProc.cs index ae80f6895d..e6a46858b0 100644 --- a/OpenDreamRuntime/Procs/NativeProc.cs +++ b/OpenDreamRuntime/Procs/NativeProc.cs @@ -4,7 +4,7 @@ using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Resources; -using System.Threading; +using OpenDreamRuntime.Map; namespace OpenDreamRuntime.Procs; @@ -60,6 +60,7 @@ public readonly ref struct Bundle { public WalkManager WalkManager => Proc._walkManager; public DreamObjectTree ObjectTree => Proc._objectTree; private readonly DreamThread _thread; + public DreamValue? LastAnimatedObject { get => _thread.LastAnimatedObject; set => _thread.LastAnimatedObject = value; diff --git a/OpenDreamRuntime/Procs/ProcDecoder.cs b/OpenDreamRuntime/Procs/ProcDecoder.cs index 2b0ac676f6..2fd2d2f41f 100644 --- a/OpenDreamRuntime/Procs/ProcDecoder.cs +++ b/OpenDreamRuntime/Procs/ProcDecoder.cs @@ -119,6 +119,7 @@ public ITuple DecodeInstruction() { return (opcode, ReadReference(), ReadReference()); case DreamProcOpcode.PushRefAndDereferenceField: + case DreamProcOpcode.IndexRefWithString: return (opcode, ReadReference(), ReadString()); case DreamProcOpcode.CallStatement: diff --git a/OpenDreamRuntime/Rendering/DMISpriteComponent.cs b/OpenDreamRuntime/Rendering/DMISpriteComponent.cs index f89e0b9047..b1ed8c3ebe 100644 --- a/OpenDreamRuntime/Rendering/DMISpriteComponent.cs +++ b/OpenDreamRuntime/Rendering/DMISpriteComponent.cs @@ -1,27 +1,15 @@ using OpenDreamShared.Dream; using OpenDreamShared.Rendering; -namespace OpenDreamRuntime.Rendering { - [RegisterComponent] - public sealed partial class DMISpriteComponent : SharedDMISpriteComponent { - [ViewVariables] - public ScreenLocation? ScreenLocation { - get => _screenLocation; - set { - _screenLocation = value; - Dirty(); - } - } - private ScreenLocation? _screenLocation; +namespace OpenDreamRuntime.Rendering; - [ViewVariables] public IconAppearance? Appearance { get; private set; } +[RegisterComponent] +public sealed partial class DMISpriteComponent : SharedDMISpriteComponent { + [ViewVariables] + [Access(typeof(DMISpriteSystem))] + public ScreenLocation ScreenLocation; - public void SetAppearance(IconAppearance? appearance, bool dirty = true) { - Appearance = appearance; - - if (dirty) { - Dirty(); - } - } - } + [Access(typeof(DMISpriteSystem))] + [ViewVariables] public ImmutableAppearance? Appearance; } + diff --git a/OpenDreamRuntime/Rendering/DMISpriteSystem.cs b/OpenDreamRuntime/Rendering/DMISpriteSystem.cs index 7d7d7c865c..4b4a39de2e 100644 --- a/OpenDreamRuntime/Rendering/DMISpriteSystem.cs +++ b/OpenDreamRuntime/Rendering/DMISpriteSystem.cs @@ -1,4 +1,5 @@ -using OpenDreamShared.Rendering; +using OpenDreamShared.Dream; +using OpenDreamShared.Rendering; using Robust.Shared.GameStates; namespace OpenDreamRuntime.Rendering; @@ -11,11 +12,23 @@ public override void Initialize() { } private void GetComponentState(EntityUid uid, DMISpriteComponent component, ref ComponentGetState args) { - int? appearanceId = null; - if (component.Appearance != null) { - appearanceId = _appearance.AddAppearance(component.Appearance); - } + uint? appearanceId = (component.Appearance != null) + ? _appearance.AddAppearance(component.Appearance).MustGetId() + : null; args.State = new SharedDMISpriteComponent.DMISpriteComponentState(appearanceId, component.ScreenLocation); } + + public void SetSpriteAppearance(Entity ent, MutableAppearance appearance, bool dirty = true) { + DMISpriteComponent component = ent.Comp; + component.Appearance = new ImmutableAppearance(appearance, _appearance); + if(dirty) + Dirty(ent, component); + } + + public void SetSpriteScreenLocation(Entity ent, ScreenLocation screenLocation) { + DMISpriteComponent component = ent.Comp; + component.ScreenLocation = screenLocation; + Dirty(ent, component); + } } diff --git a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs index 8f05b3b686..c2afa5d9f1 100644 --- a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs @@ -1,81 +1,164 @@ using OpenDreamShared.Dream; using Robust.Server.Player; using Robust.Shared.Enums; -using SharedAppearanceSystem = OpenDreamShared.Rendering.SharedAppearanceSystem; using System.Diagnostics.CodeAnalysis; using OpenDreamShared.Network.Messages; using Robust.Shared.Player; +using System.Diagnostics; +using SharedAppearanceSystem = OpenDreamShared.Rendering.SharedAppearanceSystem; namespace OpenDreamRuntime.Rendering; public sealed class ServerAppearanceSystem : SharedAppearanceSystem { - private readonly Dictionary _appearanceToId = new(); - private readonly Dictionary _idToAppearance = new(); - private int _appearanceIdCounter; + public ImmutableAppearance DefaultAppearance = default!; + + /// + /// Each appearance gets a unique ID when marked as registered. Here we store these as a key -> weakref in a weaktable, which does not count + /// as a hard ref but allows quick lookup. Each object which holds an appearance MUST hold that ImmutableAppearance until it is no longer + /// needed or it will be GC'd. Overlays & underlays are stored as hard refs on the ImmutableAppearance so you only need to hold the main appearance. + /// + private readonly HashSet _appearanceLookup = new(); /// /// This system is used by the PVS thread, we need to be thread-safe /// private readonly object _lock = new(); + private readonly Dictionary _idToAppearance = new(); + private uint _counter; + [Dependency] private readonly IPlayerManager _playerManager = default!; public override void Initialize() { - //register empty appearance as ID 0 - _appearanceToId.Add(IconAppearance.Default, 0); - _idToAppearance.Add(0, IconAppearance.Default); - _appearanceIdCounter = 1; + DefaultAppearance = new ImmutableAppearance(MutableAppearance.Default, this); + DefaultAppearance.MarkRegistered(_counter++); //first appearance registered gets id 0, this is the blank default appearance + ProxyWeakRef proxyWeakRef = new(DefaultAppearance); + _appearanceLookup.Add(proxyWeakRef); + _idToAppearance.Add(DefaultAppearance.MustGetId(), proxyWeakRef); + //leaving this in as a sanity check for mutable and immutable appearance hashcodes covering all the same vars + //if this debug assert fails, you've probably changed appearance var and not updated its counterpart + Debug.Assert(DefaultAppearance.GetHashCode() == MutableAppearance.Default.GetHashCode()); + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } public override void Shutdown() { lock (_lock) { - _appearanceToId.Clear(); + _appearanceLookup.Clear(); _idToAppearance.Clear(); - _appearanceIdCounter = 0; } } private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { if (e.NewStatus == SessionStatus.InGame) { - e.Session.Channel.SendMessage(new MsgAllAppearances(_idToAppearance)); + //todo this is probably stupid slow + lock (_lock) { + Dictionary sendData = new(_appearanceLookup.Count); + + foreach(ProxyWeakRef proxyWeakRef in _appearanceLookup){ + if(proxyWeakRef.TryGetTarget(out var immutable)) + sendData.Add(immutable.MustGetId(), immutable); + } + + Logger.GetSawmill("appearance").Debug($"Sending {sendData.Count} appearances to new player {e.Session.Name}"); + e.Session.Channel.SendMessage(new MsgAllAppearances(sendData)); + } } } - public int AddAppearance(IconAppearance appearance) { + private void RegisterAppearance(ImmutableAppearance immutableAppearance) { + immutableAppearance.MarkRegistered(_counter++); //lets this appearance know it needs to do GC finaliser & get an ID + ProxyWeakRef proxyWeakRef = new(immutableAppearance); + _appearanceLookup.Add(proxyWeakRef); + _idToAppearance.Add(immutableAppearance.MustGetId(), proxyWeakRef); + + RaiseNetworkEvent(new NewAppearanceEvent(immutableAppearance)); + } + + public ImmutableAppearance AddAppearance(MutableAppearance appearance, bool registerAppearance = true) { + ImmutableAppearance immutableAppearance = new(appearance, this); + + return AddAppearance(immutableAppearance, registerAppearance); + } + + public ImmutableAppearance AddAppearance(ImmutableAppearance appearance, bool registerAppearance = true) { lock (_lock) { - if (!_appearanceToId.TryGetValue(appearance, out int appearanceId)) { - appearanceId = _appearanceIdCounter++; - _appearanceToId.Add(appearance, appearanceId); - _idToAppearance.Add(appearanceId, appearance); - RaiseNetworkEvent(new NewAppearanceEvent(appearanceId, appearance)); + if(_appearanceLookup.TryGetValue(new(appearance), out var weakReference) && weakReference.TryGetTarget(out var originalImmutable)) { + return originalImmutable; + } else if (registerAppearance) { + RegisterAppearance(appearance); + return appearance; + } else { + return appearance; } - - return appearanceId; } } - public IconAppearance MustGetAppearance(int appearanceId) { + //this should only be called by the ImmutableAppearance's finalizer + [Access(typeof(ImmutableAppearance))] + public override void RemoveAppearance(ImmutableAppearance appearance) { lock (_lock) { - return _idToAppearance[appearanceId]; + ProxyWeakRef proxyWeakRef = new(appearance); + if(_appearanceLookup.TryGetValue(proxyWeakRef, out var weakRef)) { + //it is possible that a new appearance was created with the same hash before the GC got around to cleaning up the old one + if(weakRef.TryGetTarget(out var target) && !ReferenceEquals(target,appearance)) + return; + _appearanceLookup.Remove(proxyWeakRef); + _idToAppearance.Remove(appearance.MustGetId()); + RaiseNetworkEvent(new RemoveAppearanceEvent(appearance.MustGetId())); + } } } - public bool TryGetAppearance(int appearanceId, [NotNullWhen(true)] out IconAppearance? appearance) { + public override ImmutableAppearance MustGetAppearanceById(uint appearanceId) { lock (_lock) { - return _idToAppearance.TryGetValue(appearanceId, out appearance); + if(!_idToAppearance[appearanceId].TryGetTarget(out var result)) + throw new Exception($"Attempted to access deleted appearance ID ${appearanceId} in MustGetAppearanceByID()"); + return result; } } - public bool TryGetAppearanceId(IconAppearance appearance, out int appearanceId) { + public bool TryGetAppearanceById(uint appearanceId, [NotNullWhen(true)] out ImmutableAppearance? appearance) { lock (_lock) { - return _appearanceToId.TryGetValue(appearance, out appearanceId); + appearance = null; + return _idToAppearance.TryGetValue(appearanceId, out var appearanceRef) && appearanceRef.TryGetTarget(out appearance); } } - public void Animate(NetEntity entity, IconAppearance targetAppearance, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim) { - int appearanceId = AddAppearance(targetAppearance); + public void Animate(NetEntity entity, MutableAppearance targetAppearance, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, uint? turfId) { + uint appearanceId = AddAppearance(targetAppearance).MustGetId(); + + RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration, easing, loop, flags, delay, chainAnim, turfId)); + } +} + +//this class lets us hold a weakref and also do quick lookups in hash tables +internal sealed class ProxyWeakRef: IEquatable{ + private readonly uint? _registeredId; + private readonly int _hashCode; + public bool TryGetTarget([NotNullWhen(true)] out ImmutableAppearance? target) => _weakRef.TryGetTarget(out target); + + private readonly WeakReference _weakRef; + + public ProxyWeakRef(ImmutableAppearance appearance) { + appearance.TryGetId(out _registeredId); + _weakRef = new(appearance); + _hashCode = appearance.GetHashCode(); + } + + public override int GetHashCode() { + return _hashCode; + } + + public override bool Equals(object? obj) => obj is ProxyWeakRef proxy && Equals(proxy); - RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration, easing, loop, flags, delay, chainAnim)); + public bool Equals(ProxyWeakRef? proxy) { + if(proxy is null) + return false; + if(_registeredId is not null && _registeredId == proxy._registeredId) + return true; + if(_weakRef.TryGetTarget(out ImmutableAppearance? thisRef) && proxy._weakRef.TryGetTarget(out ImmutableAppearance? thatRef)) + return thisRef.Equals(thatRef); + return false; } } diff --git a/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs b/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs index 5faef9180d..8e088b7bb2 100644 --- a/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs @@ -22,7 +22,7 @@ public void AddImageObject(DreamConnection connection, DreamObjectImage imageObj turfCoords = new Vector3(turf.X, turf.Y, turf.Z); NetEntity ent = GetNetEntity(locEntity); - EntityUid imageObjectEntity = imageObject.GetEntity(); + EntityUid imageObjectEntity = imageObject.Entity; NetEntity imageObjectNetEntity = GetNetEntity(imageObjectEntity); if (imageObjectEntity != EntityUid.Invalid) _pvsOverrideSystem.AddSessionOverride(imageObjectEntity, connection.Session!); @@ -44,10 +44,10 @@ public void RemoveImageObject(DreamConnection connection, DreamObjectImage image NetEntity ent = GetNetEntity(locEntity); - EntityUid imageObjectEntity = imageObject.GetEntity(); + EntityUid imageObjectEntity = imageObject.Entity; if (imageObjectEntity != EntityUid.Invalid) _pvsOverrideSystem.RemoveSessionOverride(imageObjectEntity, connection.Session!); - NetEntity imageObjectNetEntity = GetNetEntity(imageObject.GetEntity()); + NetEntity imageObjectNetEntity = GetNetEntity(imageObject.Entity); RaiseNetworkEvent(new RemoveClientImageEvent(ent, turfCoords, imageObjectNetEntity), connection.Session!.Channel); } } diff --git a/OpenDreamRuntime/ServerContentIoC.cs b/OpenDreamRuntime/ServerContentIoC.cs index 77ec1706bf..ced7a28efd 100644 --- a/OpenDreamRuntime/ServerContentIoC.cs +++ b/OpenDreamRuntime/ServerContentIoC.cs @@ -1,28 +1,29 @@ -using OpenDreamRuntime.Objects; +using OpenDreamRuntime.Map; +using OpenDreamRuntime.Objects; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.DebugAdapter; using OpenDreamRuntime.Resources; -namespace OpenDreamRuntime { - public static class ServerContentIoC { - public static void Register(bool unitTests = false) { - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); +namespace OpenDreamRuntime; - #if DEBUG - IoCManager.Register(); - #endif +public static class ServerContentIoC { + public static void Register(bool unitTests = false) { + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); - if (!unitTests) { - // Unit tests use their own version - IoCManager.Register(); - } +#if DEBUG + IoCManager.Register(); +#endif + + if (!unitTests) { + // Unit tests use their own version + IoCManager.Register(); } } } diff --git a/OpenDreamRuntime/ServerVerbSystem.cs b/OpenDreamRuntime/ServerVerbSystem.cs index a618d35ba1..6da5de48e6 100644 --- a/OpenDreamRuntime/ServerVerbSystem.cs +++ b/OpenDreamRuntime/ServerVerbSystem.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Linq; using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; @@ -180,8 +181,7 @@ private bool CanExecute(DreamConnection connection, DreamObject src, DreamProc v return true; } else if (src is DreamObjectAtom atom) { var appearance = _atomManager.MustGetAppearance(atom); - - if (appearance?.Verbs.Contains(verb.VerbId.Value) is not true) // Inside atom.verbs? + if (appearance.Verbs.Contains(verb.VerbId.Value) is not true) // Inside atom.verbs? return false; } diff --git a/OpenDreamRuntime/WalkManager.cs b/OpenDreamRuntime/WalkManager.cs index 3a9ea09579..560342d20a 100644 --- a/OpenDreamRuntime/WalkManager.cs +++ b/OpenDreamRuntime/WalkManager.cs @@ -1,4 +1,5 @@ using System.Threading; +using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.Native; diff --git a/OpenDreamShared/Dream/ClientObjectReference.cs b/OpenDreamShared/Dream/ClientObjectReference.cs index 966073e3a6..4c98cc1899 100644 --- a/OpenDreamShared/Dream/ClientObjectReference.cs +++ b/OpenDreamShared/Dream/ClientObjectReference.cs @@ -59,4 +59,17 @@ public bool Equals(ClientObjectReference? other) { return Equals(other.Value); } + + public override string ToString() { + switch (Type) { + case RefType.Client: + return "client"; + case RefType.Turf: + return $"turf{{{TurfX},{TurfY},{TurfZ}}}"; + case RefType.Entity: + return $"entity{{{Entity}}}"; + } + + return "unknown ClientObjectReference"; + } } diff --git a/OpenDreamShared/Dream/ColorMatrix.cs b/OpenDreamShared/Dream/ColorMatrix.cs index cc9d5c255f..333b394057 100644 --- a/OpenDreamShared/Dream/ColorMatrix.cs +++ b/OpenDreamShared/Dream/ColorMatrix.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; namespace OpenDreamShared.Dream; + /// /// Holds the 5x4 matrix data necessary to encapsulate a color matrix: https://www.byond.com/docs/ref/#/{notes}/color-matrix /// @@ -206,7 +207,6 @@ public IEnumerable GetValues() { yield return c52; yield return c53; yield return c54; - yield break; } public Matrix4 GetMatrix4(){ @@ -232,35 +232,47 @@ public Vector4 GetOffsetVector(){ /// This method avoids implementing since that would make the argument be copied -
/// the argument in that interface lacks an 'in' modifier and one cannot be provided! /// + [Pure] public bool Equals(in ColorMatrix other) { //there is currently no kosher, "safe" C# way //of doing a fast-path pointer compare here. //(ReferenceEquals actually boxes structs just like default Equals) //so this pretty much MUST be a long elementwise compare on all elements. - return c11 == other.c11 && - c12 == other.c12 && - c13 == other.c13 && - c14 == other.c14 && - - c21 == other.c21 && - c22 == other.c22 && - c23 == other.c23 && - c24 == other.c24 && - - c31 == other.c31 && - c32 == other.c32 && - c33 == other.c33 && - c34 == other.c34 && - - c41 == other.c41 && - c42 == other.c42 && - c43 == other.c43 && - c44 == other.c44 && - - c51 == other.c51 && - c52 == other.c52 && - c53 == other.c53 && - c54 == other.c54; + return c11.Equals(other.c11) && c12.Equals(other.c12) && c13.Equals(other.c13) && c14.Equals(other.c14) && + c21.Equals(other.c21) && c22.Equals(other.c22) && c23.Equals(other.c23) && c24.Equals(other.c24) && + c31.Equals(other.c31) && c32.Equals(other.c32) && c33.Equals(other.c33) && c34.Equals(other.c34) && + c41.Equals(other.c41) && c42.Equals(other.c42) && c43.Equals(other.c43) && c44.Equals(other.c44) && + c51.Equals(other.c51) && c52.Equals(other.c52) && c53.Equals(other.c53) && c54.Equals(other.c54); + } + + public override int GetHashCode() { + HashCode hashCode = new HashCode(); + hashCode.Add(c11); + hashCode.Add(c12); + hashCode.Add(c13); + hashCode.Add(c14); + + hashCode.Add(c21); + hashCode.Add(c22); + hashCode.Add(c23); + hashCode.Add(c24); + + hashCode.Add(c31); + hashCode.Add(c32); + hashCode.Add(c33); + hashCode.Add(c34); + + hashCode.Add(c41); + hashCode.Add(c42); + hashCode.Add(c43); + hashCode.Add(c44); + + hashCode.Add(c51); + hashCode.Add(c52); + hashCode.Add(c53); + hashCode.Add(c54); + + return hashCode.ToHashCode(); } /// @@ -270,7 +282,7 @@ public bool Equals(in ColorMatrix other) { /// The right operand of the multiplication. /// A new instance that is the result of the multiplication [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Multiply(ref ColorMatrix left, ref ColorMatrix right, out ColorMatrix result) { + public static void Multiply(ref readonly ColorMatrix left, ref readonly ColorMatrix right, out ColorMatrix result) { float lM11 = left.c11, lM12 = left.c12, lM13 = left.c13, @@ -331,7 +343,7 @@ public static void Multiply(ref ColorMatrix left, ref ColorMatrix right, out Col /// The right operand of the interpolation. /// The amount to interpolate between them. 0..1 is equivalent to left..right. /// A new instance that is the result of the interpolation - public static void Interpolate(ref ColorMatrix left, ref ColorMatrix right, float factor, out ColorMatrix result) { + public static void Interpolate(ref readonly ColorMatrix left, ref readonly ColorMatrix right, float factor, out ColorMatrix result) { result = new ColorMatrix( ((1-factor) * left.c11) + (factor * right.c11), ((1-factor) * left.c12) + (factor * right.c12), diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs new file mode 100644 index 0000000000..97bffc6d38 --- /dev/null +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -0,0 +1,697 @@ +using System.Diagnostics.Contracts; +using System.IO; +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using System.Linq; +using Robust.Shared.ViewVariables; +using Robust.Shared.Maths; +using System; +using OpenDreamShared.Rendering; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace OpenDreamShared.Dream; + +/* + * Woe, weary traveler, modifying this class is not for the faint of heart. + * If you modify MutableAppearance, be sure to update the following places: + * - All of the methods on ImmutableAppearance itself + * - MutableAppearance + * - MutableAppearance methods in AtomManager + * - There may be others + */ + +// TODO: Wow this is huge! Probably look into splitting this by most used/least used to reduce the size of these +[Serializable, NetSerializable] +public sealed class ImmutableAppearance : IEquatable { + private uint? _registeredId; + + [ViewVariables] public readonly string Name = MutableAppearance.Default.Name; + [ViewVariables] public readonly string? Desc = MutableAppearance.Default.Desc; + [ViewVariables] public readonly int? Icon = MutableAppearance.Default.Icon; + [ViewVariables] public readonly string? IconState = MutableAppearance.Default.IconState; + [ViewVariables] public readonly AtomDirection Direction = MutableAppearance.Default.Direction; + [ViewVariables] public readonly bool InheritsDirection = MutableAppearance.Default.InheritsDirection; // Inherits direction when used as an overlay + [ViewVariables] public readonly Vector2i PixelOffset = MutableAppearance.Default.PixelOffset; // pixel_x and pixel_y + [ViewVariables] public readonly Vector2i PixelOffset2 = MutableAppearance.Default.PixelOffset2; // pixel_w and pixel_z + [ViewVariables] public readonly Color Color = MutableAppearance.Default.Color; + [ViewVariables] public readonly byte Alpha = MutableAppearance.Default.Alpha; + [ViewVariables] public readonly float GlideSize = MutableAppearance.Default.GlideSize; + [ViewVariables] public readonly float Layer = MutableAppearance.Default.Layer; + [ViewVariables] public readonly int Plane = MutableAppearance.Default.Plane; + [ViewVariables] public readonly BlendMode BlendMode = MutableAppearance.Default.BlendMode; + [ViewVariables] public readonly AppearanceFlags AppearanceFlags = MutableAppearance.Default.AppearanceFlags; + [ViewVariables] public readonly sbyte Invisibility = MutableAppearance.Default.Invisibility; + [ViewVariables] public readonly bool Opacity = MutableAppearance.Default.Opacity; + [ViewVariables] public readonly bool Override = MutableAppearance.Default.Override; + [ViewVariables] public readonly string? RenderSource = MutableAppearance.Default.RenderSource; + [ViewVariables] public readonly string? RenderTarget = MutableAppearance.Default.RenderTarget; + [ViewVariables] public readonly MouseOpacity MouseOpacity = MutableAppearance.Default.MouseOpacity; + [ViewVariables] public readonly ImmutableAppearance[] Overlays; + [ViewVariables] public readonly ImmutableAppearance[] Underlays; + [ViewVariables] public readonly Robust.Shared.GameObjects.NetEntity[] VisContents; + [ViewVariables] public readonly DreamFilter[] Filters; + [ViewVariables] public readonly int[] Verbs; + [ViewVariables] public readonly ColorMatrix ColorMatrix = ColorMatrix.Identity; + [ViewVariables] public Vector2i MaptextSize = MutableAppearance.Default.MaptextSize; + [ViewVariables] public Vector2i MaptextOffset = MutableAppearance.Default.MaptextOffset; + [ViewVariables] public string? Maptext = MutableAppearance.Default.Maptext; + + /// The Transform property of this appearance, in [a,d,b,e,c,f] order + [ViewVariables] public readonly float[] Transform = [ + 1, 0, // a d + 0, 1, // b e + 0, 0 // c f + ]; + + // PixelOffset2 behaves the same as PixelOffset in top-down mode, so this is used + public Vector2i TotalPixelOffset => PixelOffset + PixelOffset2; + + [NonSerialized] private readonly SharedAppearanceSystem? _appearanceSystem; + [NonSerialized] private bool _needsFinalizer; + [NonSerialized] private int? _storedHashCode; + [NonSerialized] private List? _overlayIDs; + [NonSerialized] private List? _underlayIDs; + + public ImmutableAppearance(MutableAppearance appearance, SharedAppearanceSystem? serverAppearanceSystem) { + _appearanceSystem = serverAppearanceSystem; + + Name = appearance.Name; + Desc = appearance.Desc; + Icon = appearance.Icon; + IconState = appearance.IconState; + Direction = appearance.Direction; + InheritsDirection = appearance.InheritsDirection; + PixelOffset = appearance.PixelOffset; + PixelOffset2 = appearance.PixelOffset2; + Color = appearance.Color; + Alpha = appearance.Alpha; + GlideSize = appearance.GlideSize; + ColorMatrix = appearance.ColorMatrix; + Layer = appearance.Layer; + Plane = appearance.Plane; + RenderSource = appearance.RenderSource; + RenderTarget = appearance.RenderTarget; + BlendMode = appearance.BlendMode; + AppearanceFlags = appearance.AppearanceFlags; + Invisibility = appearance.Invisibility; + Opacity = appearance.Opacity; + MouseOpacity = appearance.MouseOpacity; + Maptext = appearance.Maptext; + MaptextSize = appearance.MaptextSize; + MaptextOffset = appearance.MaptextOffset; + + Overlays = appearance.Overlays.ToArray(); + Underlays = appearance.Underlays.ToArray(); + + VisContents = appearance.VisContents.ToArray(); + Filters = appearance.Filters.ToArray(); + Verbs = appearance.Verbs.ToArray(); + Override = appearance.Override; + + for (int i = 0; i < 6; i++) { + Transform[i] = appearance.Transform[i]; + } + } + + ~ImmutableAppearance() { + if(_needsFinalizer && _registeredId is not null) + _appearanceSystem!.RemoveAppearance(this); + } + + public void MarkRegistered(uint registeredId){ + _registeredId = registeredId; + _needsFinalizer = true; + } + + //this should only be called client-side, after network transfer + public void ResolveOverlays(SharedAppearanceSystem appearanceSystem) { + if(_overlayIDs is not null) + for (int i = 0; i < _overlayIDs.Count; i++) + Overlays[i] = appearanceSystem.MustGetAppearanceById(_overlayIDs[i]); + + if(_underlayIDs is not null) + for (int i = 0; i < _underlayIDs.Count; i++) + Underlays[i] = appearanceSystem.MustGetAppearanceById(_underlayIDs[i]); + + _overlayIDs = null; + _underlayIDs = null; + } + + public override bool Equals(object? obj) => obj is ImmutableAppearance immutable && Equals(immutable); + + public bool Equals(ImmutableAppearance? immutableAppearance) { + if (immutableAppearance == null) return false; + + if (immutableAppearance.Name != Name) return false; + if (immutableAppearance.Desc != Desc) return false; + if (immutableAppearance.Icon != Icon) return false; + if (immutableAppearance.IconState != IconState) return false; + if (immutableAppearance.Direction != Direction) return false; + if (immutableAppearance.InheritsDirection != InheritsDirection) return false; + if (immutableAppearance.PixelOffset != PixelOffset) return false; + if (immutableAppearance.PixelOffset2 != PixelOffset2) return false; + if (immutableAppearance.Color != Color) return false; + if (immutableAppearance.Alpha != Alpha) return false; + if (!immutableAppearance.GlideSize.Equals(GlideSize)) return false; + if (!immutableAppearance.ColorMatrix.Equals(ColorMatrix)) return false; + if (!immutableAppearance.Layer.Equals(Layer)) return false; + if (immutableAppearance.Plane != Plane) return false; + if (immutableAppearance.RenderSource != RenderSource) return false; + if (immutableAppearance.RenderTarget != RenderTarget) return false; + if (immutableAppearance.BlendMode != BlendMode) return false; + if (immutableAppearance.AppearanceFlags != AppearanceFlags) return false; + if (immutableAppearance.Invisibility != Invisibility) return false; + if (immutableAppearance.Opacity != Opacity) return false; + if (immutableAppearance.MouseOpacity != MouseOpacity) return false; + if (immutableAppearance.Overlays.Length != Overlays.Length) return false; + if (immutableAppearance.Underlays.Length != Underlays.Length) return false; + if (immutableAppearance.VisContents.Length != VisContents.Length) return false; + if (immutableAppearance.Filters.Length != Filters.Length) return false; + if (immutableAppearance.Verbs.Length != Verbs.Length) return false; + if (immutableAppearance.Override != Override) return false; + if (immutableAppearance.Maptext != Maptext) return false; + if (immutableAppearance.MaptextSize != MaptextSize) return false; + if (immutableAppearance.MaptextOffset != MaptextOffset) return false; + + for (int i = 0; i < Filters.Length; i++) { + if (immutableAppearance.Filters[i] != Filters[i]) return false; + } + + for (int i = 0; i < Overlays.Length; i++) { + if (!immutableAppearance.Overlays[i].Equals(Overlays[i])) return false; + } + + for (int i = 0; i < Underlays.Length; i++) { + if (!immutableAppearance.Underlays[i].Equals(Underlays[i])) return false; + } + + for (int i = 0; i < VisContents.Length; i++) { + if (immutableAppearance.VisContents[i] != VisContents[i]) return false; + } + + for (int i = 0; i < Verbs.Length; i++) { + if (immutableAppearance.Verbs[i] != Verbs[i]) return false; + } + + for (int i = 0; i < 6; i++) { + if (!immutableAppearance.Transform[i].Equals(Transform[i])) return false; + } + + return true; + } + + public uint MustGetId() { + if(_registeredId is null) + throw new InvalidDataException("GetID() was called on an appearance without an ID"); + return (uint)_registeredId; + } + + public bool TryGetId([NotNullWhen(true)] out uint? id) { + id = _registeredId; + return _registeredId is not null; + } + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() { + if(_storedHashCode is not null) //because everything is readonly, this only needs to be done once + return (int)_storedHashCode; + + HashCode hashCode = new HashCode(); + + hashCode.Add(Name); + hashCode.Add(Desc); + hashCode.Add(Icon); + hashCode.Add(IconState); + hashCode.Add(Direction); + hashCode.Add(InheritsDirection); + hashCode.Add(PixelOffset); + hashCode.Add(PixelOffset2); + hashCode.Add(Color); + hashCode.Add(ColorMatrix); + hashCode.Add(Layer); + hashCode.Add(Invisibility); + hashCode.Add(Opacity); + hashCode.Add(Override); + hashCode.Add(MouseOpacity); + hashCode.Add(Alpha); + hashCode.Add(GlideSize); + hashCode.Add(Plane); + hashCode.Add(RenderSource); + hashCode.Add(RenderTarget); + hashCode.Add(BlendMode); + hashCode.Add(AppearanceFlags); + hashCode.Add(Maptext); + hashCode.Add(MaptextOffset); + hashCode.Add(MaptextSize); + + foreach (ImmutableAppearance overlay in Overlays) { + hashCode.Add(overlay.GetHashCode()); + } + + foreach (ImmutableAppearance underlay in Underlays) { + hashCode.Add(underlay.GetHashCode()); + } + + foreach (int visContent in VisContents) { + hashCode.Add(visContent); + } + + foreach (DreamFilter filter in Filters) { + hashCode.Add(filter); + } + + foreach (int verb in Verbs) { + hashCode.Add(verb); + } + + for (int i = 0; i < 6; i++) { + hashCode.Add(Transform[i]); + } + + _storedHashCode = hashCode.ToHashCode(); + return (int)_storedHashCode; + } + + public ImmutableAppearance(NetIncomingMessage buffer, IRobustSerializer serializer) { + Overlays = []; + Underlays = []; + VisContents = []; + Filters = []; + Verbs =[]; + + var property = (IconAppearanceProperty)buffer.ReadByte(); + while (property != IconAppearanceProperty.End) { + switch (property) { + case IconAppearanceProperty.Name: + Name = buffer.ReadString(); + break; + case IconAppearanceProperty.Desc: + Desc = buffer.ReadString(); + break; + case IconAppearanceProperty.Id: + _registeredId = buffer.ReadVariableUInt32(); + break; + case IconAppearanceProperty.Icon: + Icon = buffer.ReadVariableInt32(); + break; + case IconAppearanceProperty.IconState: + IconState = buffer.ReadString(); + break; + case IconAppearanceProperty.Direction: + Direction = (AtomDirection)buffer.ReadByte(); + break; + case IconAppearanceProperty.DoesntInheritDirection: + InheritsDirection = false; + break; + case IconAppearanceProperty.PixelOffset: + PixelOffset = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); + break; + case IconAppearanceProperty.PixelOffset2: + PixelOffset2 = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); + break; + case IconAppearanceProperty.Color: + Color = buffer.ReadColor(); + break; + case IconAppearanceProperty.Alpha: + Alpha = buffer.ReadByte(); + break; + case IconAppearanceProperty.GlideSize: + GlideSize = buffer.ReadFloat(); + break; + case IconAppearanceProperty.Layer: + Layer = buffer.ReadFloat(); + break; + case IconAppearanceProperty.Plane: + Plane = buffer.ReadVariableInt32(); + break; + case IconAppearanceProperty.BlendMode: + BlendMode = (BlendMode)buffer.ReadByte(); + break; + case IconAppearanceProperty.AppearanceFlags: + AppearanceFlags = (AppearanceFlags)buffer.ReadInt32(); + break; + case IconAppearanceProperty.Invisibility: + Invisibility = buffer.ReadSByte(); + break; + case IconAppearanceProperty.Opacity: + Opacity = buffer.ReadBoolean(); + break; + case IconAppearanceProperty.Override: + Override = buffer.ReadBoolean(); + break; + case IconAppearanceProperty.RenderSource: + RenderSource = buffer.ReadString(); + break; + case IconAppearanceProperty.RenderTarget: + RenderTarget = buffer.ReadString(); + break; + case IconAppearanceProperty.MouseOpacity: + MouseOpacity = (MouseOpacity)buffer.ReadByte(); + break; + case IconAppearanceProperty.ColorMatrix: + ColorMatrix = new( + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle() + ); + + break; + case IconAppearanceProperty.Overlays: { + var overlaysCount = buffer.ReadVariableInt32(); + + Overlays = new ImmutableAppearance[overlaysCount]; + _overlayIDs = new(overlaysCount); + for (int overlaysI = 0; overlaysI < overlaysCount; overlaysI++) { + _overlayIDs.Add(buffer.ReadVariableUInt32()); + } + + break; + } + case IconAppearanceProperty.Underlays: { + var underlaysCount = buffer.ReadVariableInt32(); + + Underlays = new ImmutableAppearance[underlaysCount]; + _underlayIDs = new(underlaysCount); + for (int underlaysI = 0; underlaysI < underlaysCount; underlaysI++) { + _underlayIDs.Add(buffer.ReadVariableUInt32()); + } + + break; + } + case IconAppearanceProperty.VisContents: { + var visContentsCount = buffer.ReadVariableInt32(); + + VisContents = new Robust.Shared.GameObjects.NetEntity[visContentsCount]; + for (int visContentsI = 0; visContentsI < visContentsCount; visContentsI++) { + VisContents[visContentsI] = buffer.ReadNetEntity(); + } + + break; + } + case IconAppearanceProperty.Filters: { + var filtersCount = buffer.ReadInt32(); + + Filters = new DreamFilter[filtersCount]; + for (int filtersI = 0; filtersI < filtersCount; filtersI++) { + var filterLength = buffer.ReadVariableInt32(); + var filterData = buffer.ReadBytes(filterLength); + using var filterStream = new MemoryStream(filterData); + var filter = serializer.Deserialize(filterStream); + + Filters[filtersI] = filter; + } + + break; + } + case IconAppearanceProperty.Verbs: { + var verbsCount = buffer.ReadVariableInt32(); + + Verbs = new int[verbsCount]; + for (int verbsI = 0; verbsI < verbsCount; verbsI++) { + Verbs[verbsI] = buffer.ReadVariableInt32(); + } + + break; + } + case IconAppearanceProperty.Transform: { + Transform = [ + buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle() + ]; + + break; + } + case IconAppearanceProperty.Maptext: { + Maptext = buffer.ReadString(); + break; + } + case IconAppearanceProperty.MaptextSize: { + MaptextSize = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); + break; + } + case IconAppearanceProperty.MaptextOffset: { + MaptextOffset = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); + break; + } + default: + throw new Exception($"Invalid property {property}"); + } + + property = (IconAppearanceProperty)buffer.ReadByte(); + } + + if(_registeredId is null) + throw new Exception("No appearance ID found in buffer"); + } + + //Creates an editable *copy* of this appearance, which must be added to the ServerAppearanceSystem to be used. + [Pure] + public MutableAppearance ToMutable() { + MutableAppearance result = MutableAppearance.Get(); + + result.Name = Name; + result.Desc = Desc; + result.Icon = Icon; + result.IconState = IconState; + result.Direction = Direction; + result.InheritsDirection = InheritsDirection; + result.PixelOffset = PixelOffset; + result.PixelOffset2 = PixelOffset2; + result.Color = Color; + result.Alpha = Alpha; + result.GlideSize = GlideSize; + result.ColorMatrix = ColorMatrix; + result.Layer = Layer; + result.Plane = Plane; + result.RenderSource = RenderSource; + result.RenderTarget = RenderTarget; + result.BlendMode = BlendMode; + result.AppearanceFlags = AppearanceFlags; + result.Invisibility = Invisibility; + result.Opacity = Opacity; + result.MouseOpacity = MouseOpacity; + result.Override = Override; + result.Maptext = Maptext; + result.MaptextOffset = MaptextOffset; + result.MaptextSize = MaptextSize; + + result.Overlays.EnsureCapacity(Overlays.Length); + result.Underlays.EnsureCapacity(Underlays.Length); + result.VisContents.EnsureCapacity(VisContents.Length); + result.Filters.EnsureCapacity(Filters.Length); + result.Verbs.EnsureCapacity(Verbs.Length); + result.Overlays.AddRange(Overlays); + result.Underlays.AddRange(Underlays); + result.VisContents.AddRange(VisContents); + result.Filters.AddRange(Filters); + result.Verbs.AddRange(Verbs); + Array.Copy(Transform, result.Transform, 6); + + return result; + } + + public void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { + buffer.Write((byte)IconAppearanceProperty.Id); + buffer.WriteVariableUInt32(MustGetId()); + + if (Name != MutableAppearance.Default.Name) { + buffer.Write((byte)IconAppearanceProperty.Name); + buffer.Write(Name); + } + + if (Desc != MutableAppearance.Default.Desc) { + buffer.Write((byte)IconAppearanceProperty.Desc); + buffer.Write(Desc); + } + + if (Icon != null) { + buffer.Write((byte)IconAppearanceProperty.Icon); + buffer.WriteVariableInt32(Icon.Value); + } + + if (IconState != null) { + buffer.Write((byte)IconAppearanceProperty.IconState); + buffer.Write(IconState); + } + + if (Direction != MutableAppearance.Default.Direction) { + buffer.Write((byte)IconAppearanceProperty.Direction); + buffer.Write((byte)Direction); + } + + if (InheritsDirection != true) { + buffer.Write((byte)IconAppearanceProperty.DoesntInheritDirection); + } + + if (PixelOffset != MutableAppearance.Default.PixelOffset) { + buffer.Write((byte)IconAppearanceProperty.PixelOffset); + buffer.WriteVariableInt32(PixelOffset.X); + buffer.WriteVariableInt32(PixelOffset.Y); + } + + if (PixelOffset2 != MutableAppearance.Default.PixelOffset2) { + buffer.Write((byte)IconAppearanceProperty.PixelOffset2); + buffer.WriteVariableInt32(PixelOffset2.X); + buffer.WriteVariableInt32(PixelOffset2.Y); + } + + if (Color != MutableAppearance.Default.Color) { + buffer.Write((byte)IconAppearanceProperty.Color); + buffer.Write(Color); + } + + if (Alpha != MutableAppearance.Default.Alpha) { + buffer.Write((byte)IconAppearanceProperty.Alpha); + buffer.Write(Alpha); + } + + if (!GlideSize.Equals(MutableAppearance.Default.GlideSize)) { + buffer.Write((byte)IconAppearanceProperty.GlideSize); + buffer.Write(GlideSize); + } + + if (!ColorMatrix.Equals(MutableAppearance.Default.ColorMatrix)) { + buffer.Write((byte)IconAppearanceProperty.ColorMatrix); + + foreach (var value in ColorMatrix.GetValues()) + buffer.Write(value); + } + + if (!Layer.Equals(MutableAppearance.Default.Layer)) { + buffer.Write((byte)IconAppearanceProperty.Layer); + buffer.Write(Layer); + } + + if (Plane != MutableAppearance.Default.Plane) { + buffer.Write((byte)IconAppearanceProperty.Plane); + buffer.WriteVariableInt32(Plane); + } + + if (BlendMode != MutableAppearance.Default.BlendMode) { + buffer.Write((byte)IconAppearanceProperty.BlendMode); + buffer.Write((byte)BlendMode); + } + + if (AppearanceFlags != MutableAppearance.Default.AppearanceFlags) { + buffer.Write((byte)IconAppearanceProperty.AppearanceFlags); + buffer.Write((int)AppearanceFlags); + } + + if (Invisibility != MutableAppearance.Default.Invisibility) { + buffer.Write((byte)IconAppearanceProperty.Invisibility); + buffer.Write(Invisibility); + } + + if (Opacity != MutableAppearance.Default.Opacity) { + buffer.Write((byte)IconAppearanceProperty.Opacity); + buffer.Write(Opacity); + } + + if (Override != MutableAppearance.Default.Override) { + buffer.Write((byte)IconAppearanceProperty.Override); + buffer.Write(Override); + } + + if (!string.IsNullOrWhiteSpace(RenderSource)) { + buffer.Write((byte)IconAppearanceProperty.RenderSource); + buffer.Write(RenderSource); + } + + if (!string.IsNullOrWhiteSpace(RenderTarget)) { + buffer.Write((byte)IconAppearanceProperty.RenderTarget); + buffer.Write(RenderTarget); + } + + if (MouseOpacity != MutableAppearance.Default.MouseOpacity) { + buffer.Write((byte)IconAppearanceProperty.MouseOpacity); + buffer.Write((byte)MouseOpacity); + } + + if (Overlays.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Overlays); + + buffer.WriteVariableInt32(Overlays.Length); + foreach (var overlay in Overlays) { + buffer.WriteVariableUInt32(overlay.MustGetId()); + } + } + + if (Underlays.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Underlays); + + buffer.WriteVariableInt32(Underlays.Length); + foreach (var underlay in Underlays) { + buffer.WriteVariableUInt32(underlay.MustGetId()); + } + } + + if (VisContents.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.VisContents); + + buffer.WriteVariableInt32(VisContents.Length); + foreach (var item in VisContents) { + buffer.Write(item); + } + } + + if (Filters.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Filters); + + buffer.Write(Filters.Length); + foreach (var filter in Filters) { + using var filterStream = new MemoryStream(); + + serializer.Serialize(filterStream, filter); + buffer.WriteVariableInt32((int)filterStream.Length); + filterStream.TryGetBuffer(out var filterBuffer); + buffer.Write(filterBuffer); + } + } + + if (Verbs.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Verbs); + + buffer.WriteVariableInt32(Verbs.Length); + foreach (var verb in Verbs) { + buffer.WriteVariableInt32(verb); + } + } + + if (!Transform.SequenceEqual(MutableAppearance.Default.Transform)) { + buffer.Write((byte)IconAppearanceProperty.Transform); + + for (int i = 0; i < 6; i++) { + buffer.Write(Transform[i]); + } + } + + if(!string.IsNullOrEmpty(Maptext)){ + buffer.Write((byte) IconAppearanceProperty.Maptext); + buffer.Write(Maptext); + } + + if (MaptextOffset != MutableAppearance.Default.MaptextOffset) { + buffer.Write((byte)IconAppearanceProperty.MaptextOffset); + buffer.WriteVariableInt32(MaptextOffset.X); + buffer.WriteVariableInt32(MaptextOffset.Y); + } + + if (MaptextSize != MutableAppearance.Default.MaptextSize) { + buffer.Write((byte)IconAppearanceProperty.MaptextSize); + buffer.WriteVariableInt32(MaptextSize.X); + buffer.WriteVariableInt32(MaptextSize.Y); + } + + buffer.Write((byte)IconAppearanceProperty.End); + } + + public int ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + throw new NotImplementedException(); + } +} + diff --git a/OpenDreamShared/Dream/IconAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs similarity index 74% rename from OpenDreamShared/Dream/IconAppearance.cs rename to OpenDreamShared/Dream/MutableAppearance.cs index 78e38af359..76c853cb03 100644 --- a/OpenDreamShared/Dream/IconAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -1,29 +1,32 @@ using Robust.Shared.Maths; -using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Robust.Shared.GameObjects; namespace OpenDreamShared.Dream; /* * Woe, weary traveler, modifying this class is not for the faint of heart. - * If you modify IconAppearance, be sure to update the following places: - * - All of the methods on IconAppearance itself + * If you modify MutableAppearance, be sure to update the following places: + * - All of the methods on MutableAppearance itself + * - ImmutableAppearance * - IconAppearance methods in AtomManager * - MsgAllAppearances * - IconDebugWindow + * - IconAppearanceProperty enum * - There may be others */ // TODO: Wow this is huge! Probably look into splitting this by most used/least used to reduce the size of these -[Serializable, NetSerializable] -public sealed class IconAppearance : IEquatable { - public static readonly IconAppearance Default = new(); +[Serializable] +public sealed class MutableAppearance : IEquatable, IDisposable { + public static readonly MutableAppearance Default = new(); + + private static Stack _mutableAppearancePool = new(); [ViewVariables] public string Name = string.Empty; + [ViewVariables] public string? Desc = string.Empty; [ViewVariables] public int? Icon; [ViewVariables] public string? IconState; [ViewVariables] public AtomDirection Direction = AtomDirection.South; @@ -43,11 +46,14 @@ public sealed class IconAppearance : IEquatable { [ViewVariables] public string? RenderSource; [ViewVariables] public string? RenderTarget; [ViewVariables] public MouseOpacity MouseOpacity = MouseOpacity.PixelOpaque; - [ViewVariables] public List Overlays; - [ViewVariables] public List Underlays; - [ViewVariables] public List VisContents; + [ViewVariables] public List Overlays; + [ViewVariables] public List Underlays; + [ViewVariables] public List VisContents; [ViewVariables] public List Filters; [ViewVariables] public List Verbs; + [ViewVariables] public Vector2i MaptextSize = new(32,32); + [ViewVariables] public Vector2i MaptextOffset = new(0,0); + [ViewVariables] public string? Maptext; /// /// An appearance can gain a color matrix filter by two possible forces:
@@ -72,16 +78,36 @@ public sealed class IconAppearance : IEquatable { // PixelOffset2 behaves the same as PixelOffset in top-down mode, so this is used public Vector2i TotalPixelOffset => PixelOffset + PixelOffset2; - public IconAppearance() { - Overlays = new(); - Underlays = new(); - VisContents = new(); - Filters = new(); - Verbs = new(); + private MutableAppearance() { + Overlays = []; + Underlays = []; + VisContents = []; + Filters = []; + Verbs = []; + } + + public void Dispose() { + CopyFrom(Default); + _mutableAppearancePool.Push(this); + } + + public static MutableAppearance Get() { + if (_mutableAppearancePool.TryPop(out var popped)) + return popped; + + return new MutableAppearance(); + } + + public static MutableAppearance GetCopy(MutableAppearance appearance) { + MutableAppearance result = Get(); + + result.CopyFrom(appearance); + return result; } - public IconAppearance(IconAppearance appearance) { + public void CopyFrom(MutableAppearance appearance) { Name = appearance.Name; + Desc = appearance.Desc; Icon = appearance.Icon; IconState = appearance.IconState; Direction = appearance.Direction; @@ -101,24 +127,31 @@ public IconAppearance(IconAppearance appearance) { Invisibility = appearance.Invisibility; Opacity = appearance.Opacity; MouseOpacity = appearance.MouseOpacity; - Overlays = new(appearance.Overlays); - Underlays = new(appearance.Underlays); - VisContents = new(appearance.VisContents); - Filters = new(appearance.Filters); - Verbs = new(appearance.Verbs); Override = appearance.Override; - - for (int i = 0; i < 6; i++) { - Transform[i] = appearance.Transform[i]; - } + Maptext = appearance.Maptext; + MaptextSize = appearance.MaptextSize; + MaptextOffset = appearance.MaptextOffset; + + Overlays.Clear(); + Underlays.Clear(); + VisContents.Clear(); + Filters.Clear(); + Verbs.Clear(); + Overlays.AddRange(appearance.Overlays); + Underlays.AddRange(appearance.Underlays); + VisContents.AddRange(appearance.VisContents); + Filters.AddRange(appearance.Filters); + Verbs.AddRange(appearance.Verbs); + Array.Copy(appearance.Transform, Transform, 6); } - public override bool Equals(object? obj) => obj is IconAppearance appearance && Equals(appearance); + public override bool Equals(object? obj) => obj is MutableAppearance appearance && Equals(appearance); - public bool Equals(IconAppearance? appearance) { + public bool Equals(MutableAppearance? appearance) { if (appearance == null) return false; if (appearance.Name != Name) return false; + if (appearance.Desc != Desc) return false; if (appearance.Icon != Icon) return false; if (appearance.IconState != IconState) return false; if (appearance.Direction != Direction) return false; @@ -144,6 +177,9 @@ public bool Equals(IconAppearance? appearance) { if (appearance.Filters.Count != Filters.Count) return false; if (appearance.Verbs.Count != Verbs.Count) return false; if (appearance.Override != Override) return false; + if (appearance.Maptext != Maptext) return false; + if (appearance.MaptextSize != MaptextSize) return false; + if (appearance.MaptextOffset != MaptextOffset) return false; for (int i = 0; i < Filters.Count; i++) { if (appearance.Filters[i] != Filters[i]) return false; @@ -203,10 +239,13 @@ private static bool TryRepresentMatrixAsRgbaColor(in ColorMatrix matrix, [NotNul return maybeColor is not null; } + //it is *ESSENTIAL* that this matches the hashcode of the equivelant ImmutableAppearance. There's a debug assert and everything. + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] public override int GetHashCode() { HashCode hashCode = new HashCode(); hashCode.Add(Name); + hashCode.Add(Desc); hashCode.Add(Icon); hashCode.Add(IconState); hashCode.Add(Direction); @@ -218,6 +257,7 @@ public override int GetHashCode() { hashCode.Add(Layer); hashCode.Add(Invisibility); hashCode.Add(Opacity); + hashCode.Add(Override); hashCode.Add(MouseOpacity); hashCode.Add(Alpha); hashCode.Add(GlideSize); @@ -226,13 +266,16 @@ public override int GetHashCode() { hashCode.Add(RenderTarget); hashCode.Add(BlendMode); hashCode.Add(AppearanceFlags); + hashCode.Add(Maptext); + hashCode.Add(MaptextOffset); + hashCode.Add(MaptextSize); - foreach (int overlay in Overlays) { - hashCode.Add(overlay); + foreach (var overlay in Overlays) { + hashCode.Add(overlay.GetHashCode()); } - foreach (int underlay in Underlays) { - hashCode.Add(underlay); + foreach (var underlay in Underlays) { + hashCode.Add(underlay.GetHashCode()); } foreach (int visContent in VisContents) { @@ -333,3 +376,40 @@ public enum AnimationFlags { AnimationRelative = 256, AnimationContinue = 512 } + +//used for encoding for netmessages +public enum IconAppearanceProperty : byte { + Name, + Desc, + Icon, + IconState, + Direction, + DoesntInheritDirection, + PixelOffset, + PixelOffset2, + Color, + Alpha, + GlideSize, + ColorMatrix, + Layer, + Plane, + BlendMode, + AppearanceFlags, + Invisibility, + Opacity, + Override, + RenderSource, + RenderTarget, + MouseOpacity, + Overlays, + Underlays, + VisContents, + Filters, + Verbs, + Transform, + Maptext, + MaptextSize, + MaptextOffset, + Id, + End + } diff --git a/OpenDreamShared/Dream/ScreenLocation.cs b/OpenDreamShared/Dream/ScreenLocation.cs index 27cef2ec09..2b0b13d553 100644 --- a/OpenDreamShared/Dream/ScreenLocation.cs +++ b/OpenDreamShared/Dream/ScreenLocation.cs @@ -6,18 +6,23 @@ using System.Linq; using System.Text; using Robust.Shared.Log; +using Robust.Shared.Maths; namespace OpenDreamShared.Dream; public enum HorizontalAnchor { + West, Left, Center, + East, Right } public enum VerticalAnchor { + South, Bottom, Center, + North, Top } @@ -35,13 +40,13 @@ public sealed class ScreenLocation { private static ISawmill Sawmill => Logger.GetSawmill("opendream.screen_loc_parser"); - private static string[] _keywords = { + private static string[] _keywords = [ "CENTER", "WEST", "EAST", "LEFT", "RIGHT", "NORTH", "SOUTH", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT" - }; + ]; public ScreenLocation(int x, int y, int pixelOffsetX, int pixelOffsetY, ScreenLocation? range = null) { X = x - 1; @@ -66,20 +71,24 @@ public ScreenLocation(string screenLocation) { ParseScreenLoc(screenLocation); } - public Vector2 GetViewPosition(Vector2 viewOffset, ViewRange view, float iconSize) { - float x = (X + PixelOffsetX / iconSize); + public Vector2 GetViewPosition(Vector2 viewOffset, ViewRange view, float tileSize, Vector2i iconSize) { + // TODO: LEFT/RIGHT/TOP/BOTTOM need to stick to the edge of the visible map if the map's container is smaller than the map itself + + float x = (X + PixelOffsetX / tileSize); x += HorizontalAnchor switch { - HorizontalAnchor.Left => 0, + HorizontalAnchor.West or HorizontalAnchor.Left => 0, HorizontalAnchor.Center => view.CenterX, - HorizontalAnchor.Right => view.Width - 1, + HorizontalAnchor.East => view.Width - 1, + HorizontalAnchor.Right => view.Width - (iconSize.X / tileSize), _ => throw new Exception($"Invalid horizontal anchor {HorizontalAnchor}") }; - float y = (Y + PixelOffsetY / iconSize); + float y = (Y + PixelOffsetY / tileSize); y += VerticalAnchor switch { - VerticalAnchor.Bottom => 0, + VerticalAnchor.South or VerticalAnchor.Bottom => 0, VerticalAnchor.Center => view.CenterY, - VerticalAnchor.Top => view.Height - 1, + VerticalAnchor.North => view.Height - 1, + VerticalAnchor.Top => view.Height - (iconSize.Y / tileSize), _ => throw new Exception($"Invalid vertical anchor {VerticalAnchor}") }; @@ -109,7 +118,7 @@ private void ParseScreenLoc(string screenLoc) { if (mapControlSplitIndex > 0) { string mapControl = rangeSplit[0].Substring(0, mapControlSplitIndex); - if (char.IsAsciiLetter(mapControl[0]) && mapControl.IndexOfAny(new[] { '+', '-' }) == -1 && !_keywords.Contains(mapControl)) { + if (char.IsAsciiLetter(mapControl[0]) && mapControl.IndexOfAny(['+', '-']) == -1 && !_keywords.Contains(mapControl)) { MapControl = mapControl; coordinateSplit[0] = coordinateSplit[0].Substring(mapControlSplitIndex + 1); } @@ -121,10 +130,14 @@ private void ParseScreenLoc(string screenLoc) { (HorizontalAnchor, VerticalAnchor) = coordinateSplit[0].Trim() switch { "CENTER" => (HorizontalAnchor.Center, VerticalAnchor.Center), - "NORTHWEST" or "TOPLEFT" => (HorizontalAnchor.Left, VerticalAnchor.Top), - "NORTHEAST" or "TOPRIGHT" => (HorizontalAnchor.Right, VerticalAnchor.Top), - "SOUTHWEST" or "BOTTOMLEFT" => (HorizontalAnchor.Left, VerticalAnchor.Bottom), - "SOUTHEAST" or "BOTTOMRIGHT" => (HorizontalAnchor.Right, VerticalAnchor.Bottom), + "NORTHWEST" => (HorizontalAnchor.West, VerticalAnchor.North), + "TOPLEFT" => (HorizontalAnchor.Left, VerticalAnchor.Top), + "NORTHEAST" => (HorizontalAnchor.East, VerticalAnchor.North), + "TOPRIGHT" => (HorizontalAnchor.Right, VerticalAnchor.Top), + "SOUTHWEST" => (HorizontalAnchor.West, VerticalAnchor.South), + "BOTTOMLEFT" => (HorizontalAnchor.Left, VerticalAnchor.Bottom), + "SOUTHEAST" => (HorizontalAnchor.East, VerticalAnchor.South), + "BOTTOMRIGHT" => (HorizontalAnchor.Right, VerticalAnchor.Bottom), _ => throw new Exception($"Invalid screen_loc {screenLoc}") }; @@ -143,9 +156,7 @@ private bool ParseScreenLocCoordinate(string coordinate, bool isHorizontal) { List pieces = new(); StringBuilder currentPiece = new(); - for (int i = 0; i < coordinate.Length; i++) { - char c = coordinate[i]; - + foreach (var c in coordinate) { switch (c) { case ' ' or '\t': continue; @@ -193,23 +204,35 @@ private bool ParseScreenLocCoordinate(string coordinate, bool isHorizontal) { break; case "WEST": - case "LEFT": // Yes, this sets the horizontal anchor regardless of the isHorizontal arg. // Every macro sets their respective axis regardless of which coordinate it's in - HorizontalAnchor = HorizontalAnchor.Left; + HorizontalAnchor = HorizontalAnchor.West; settingHorizontal = true; break; case "EAST": + HorizontalAnchor = HorizontalAnchor.East; + settingHorizontal = true; + break; + case "NORTH": + VerticalAnchor = VerticalAnchor.North; + settingHorizontal = false; + break; + case "SOUTH": + VerticalAnchor = VerticalAnchor.South; + settingHorizontal = false; + break; + case "LEFT": + HorizontalAnchor = HorizontalAnchor.Left; + settingHorizontal = true; + break; case "RIGHT": HorizontalAnchor = HorizontalAnchor.Right; settingHorizontal = true; break; - case "NORTH": case "TOP": VerticalAnchor = VerticalAnchor.Top; settingHorizontal = false; break; - case "SOUTH": case "BOTTOM": VerticalAnchor = VerticalAnchor.Bottom; settingHorizontal = false; diff --git a/OpenDreamShared/Network/Messages/MsgAllAppearances.cs b/OpenDreamShared/Network/Messages/MsgAllAppearances.cs index 4377d00aa6..2d4b1afd91 100644 --- a/OpenDreamShared/Network/Messages/MsgAllAppearances.cs +++ b/OpenDreamShared/Network/Messages/MsgAllAppearances.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.Collections.Generic; using Lidgren.Network; using OpenDreamShared.Dream; using Robust.Shared.Network; @@ -9,389 +6,26 @@ namespace OpenDreamShared.Network.Messages; -public sealed class MsgAllAppearances(Dictionary allAppearances) : NetMessage { +public sealed class MsgAllAppearances(Dictionary allAppearances) : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - - private enum Property : byte { - Name, - Icon, - IconState, - Direction, - DoesntInheritDirection, - PixelOffset, - PixelOffset2, - Color, - Alpha, - GlideSize, - ColorMatrix, - Layer, - Plane, - BlendMode, - AppearanceFlags, - Invisibility, - Opacity, - Override, - RenderSource, - RenderTarget, - MouseOpacity, - Overlays, - Underlays, - VisContents, - Filters, - Verbs, - Transform, - - Id, - End - } - - public Dictionary AllAppearances = allAppearances; + public Dictionary AllAppearances = allAppearances; public MsgAllAppearances() : this(new()) { } public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { var count = buffer.ReadInt32(); - var appearanceId = -1; - AllAppearances = new(count); for (int i = 0; i < count; i++) { - var appearance = new IconAppearance(); - var property = (Property)buffer.ReadByte(); - - appearanceId++; - - while (property != Property.End) { - switch (property) { - case Property.Name: - appearance.Name = buffer.ReadString(); - break; - case Property.Id: - appearanceId = buffer.ReadVariableInt32(); - break; - case Property.Icon: - appearance.Icon = buffer.ReadVariableInt32(); - break; - case Property.IconState: - appearance.IconState = buffer.ReadString(); - break; - case Property.Direction: - appearance.Direction = (AtomDirection)buffer.ReadByte(); - break; - case Property.DoesntInheritDirection: - appearance.InheritsDirection = false; - break; - case Property.PixelOffset: - appearance.PixelOffset = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); - break; - case Property.PixelOffset2: - appearance.PixelOffset2 = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); - break; - case Property.Color: - appearance.Color = buffer.ReadColor(); - break; - case Property.Alpha: - appearance.Alpha = buffer.ReadByte(); - break; - case Property.GlideSize: - appearance.GlideSize = buffer.ReadFloat(); - break; - case Property.Layer: - appearance.Layer = buffer.ReadFloat(); - break; - case Property.Plane: - appearance.Plane = buffer.ReadVariableInt32(); - break; - case Property.BlendMode: - appearance.BlendMode = (BlendMode)buffer.ReadByte(); - break; - case Property.AppearanceFlags: - appearance.AppearanceFlags = (AppearanceFlags)buffer.ReadInt32(); - break; - case Property.Invisibility: - appearance.Invisibility = buffer.ReadSByte(); - break; - case Property.Opacity: - appearance.Opacity = buffer.ReadBoolean(); - break; - case Property.Override: - appearance.Override = buffer.ReadBoolean(); - break; - case Property.RenderSource: - appearance.RenderSource = buffer.ReadString(); - break; - case Property.RenderTarget: - appearance.RenderTarget = buffer.ReadString(); - break; - case Property.MouseOpacity: - appearance.MouseOpacity = (MouseOpacity)buffer.ReadByte(); - break; - case Property.ColorMatrix: - appearance.ColorMatrix = new( - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle() - ); - - break; - case Property.Overlays: { - var overlaysCount = buffer.ReadVariableInt32(); - - appearance.Overlays.EnsureCapacity(overlaysCount); - for (int overlaysI = 0; overlaysI < overlaysCount; overlaysI++) { - appearance.Overlays.Add(buffer.ReadVariableInt32()); - } - - break; - } - case Property.Underlays: { - var underlaysCount = buffer.ReadVariableInt32(); - - appearance.Underlays.EnsureCapacity(underlaysCount); - for (int underlaysI = 0; underlaysI < underlaysCount; underlaysI++) { - appearance.Underlays.Add(buffer.ReadVariableInt32()); - } - - break; - } - case Property.VisContents: { - var visContentsCount = buffer.ReadVariableInt32(); - - appearance.VisContents.EnsureCapacity(visContentsCount); - for (int visContentsI = 0; visContentsI < visContentsCount; visContentsI++) { - appearance.VisContents.Add(buffer.ReadNetEntity()); - } - - break; - } - case Property.Filters: { - var filtersCount = buffer.ReadInt32(); - - appearance.Filters.EnsureCapacity(filtersCount); - for (int filtersI = 0; filtersI < filtersCount; filtersI++) { - var filterLength = buffer.ReadVariableInt32(); - var filterData = buffer.ReadBytes(filterLength); - using var filterStream = new MemoryStream(filterData); - var filter = serializer.Deserialize(filterStream); - - appearance.Filters.Add(filter); - } - - break; - } - case Property.Verbs: { - var verbsCount = buffer.ReadVariableInt32(); - - appearance.Verbs.EnsureCapacity(verbsCount); - for (int verbsI = 0; verbsI < verbsCount; verbsI++) { - appearance.Verbs.Add(buffer.ReadVariableInt32()); - } - - break; - } - case Property.Transform: { - appearance.Transform = [ - buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle() - ]; - - break; - } - default: - throw new Exception($"Invalid property {property}"); - } - - property = (Property)buffer.ReadByte(); - } - - AllAppearances.Add(appearanceId, appearance); + var appearance = new ImmutableAppearance(buffer, serializer); + AllAppearances.Add(appearance.MustGetId(), appearance); } } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - int lastId = -1; - buffer.Write(AllAppearances.Count); foreach (var pair in AllAppearances) { - var appearance = pair.Value; - - if (pair.Key != lastId + 1) { - buffer.Write((byte)Property.Id); - buffer.WriteVariableInt32(pair.Key); - } - - lastId = pair.Key; - - if (appearance.Name != IconAppearance.Default.Name) { - buffer.Write((byte)Property.Name); - buffer.Write(appearance.Name); - } - - if (appearance.Icon != null) { - buffer.Write((byte)Property.Icon); - buffer.WriteVariableInt32(appearance.Icon.Value); - } - - if (appearance.IconState != null) { - buffer.Write((byte)Property.IconState); - buffer.Write(appearance.IconState); - } - - if (appearance.Direction != IconAppearance.Default.Direction) { - buffer.Write((byte)Property.Direction); - buffer.Write((byte)appearance.Direction); - } - - if (appearance.InheritsDirection != true) { - buffer.Write((byte)Property.DoesntInheritDirection); - } - - if (appearance.PixelOffset != IconAppearance.Default.PixelOffset) { - buffer.Write((byte)Property.PixelOffset); - buffer.WriteVariableInt32(appearance.PixelOffset.X); - buffer.WriteVariableInt32(appearance.PixelOffset.Y); - } - - if (appearance.PixelOffset2 != IconAppearance.Default.PixelOffset2) { - buffer.Write((byte)Property.PixelOffset2); - buffer.WriteVariableInt32(appearance.PixelOffset2.X); - buffer.WriteVariableInt32(appearance.PixelOffset2.Y); - } - - if (appearance.Color != IconAppearance.Default.Color) { - buffer.Write((byte)Property.Color); - buffer.Write(appearance.Color); - } - - if (appearance.Alpha != IconAppearance.Default.Alpha) { - buffer.Write((byte)Property.Alpha); - buffer.Write(appearance.Alpha); - } - - if (!appearance.GlideSize.Equals(IconAppearance.Default.GlideSize)) { - buffer.Write((byte)Property.GlideSize); - buffer.Write(appearance.GlideSize); - } - - if (!appearance.ColorMatrix.Equals(IconAppearance.Default.ColorMatrix)) { - buffer.Write((byte)Property.ColorMatrix); - - foreach (var value in appearance.ColorMatrix.GetValues()) - buffer.Write(value); - } - - if (!appearance.Layer.Equals(IconAppearance.Default.Layer)) { - buffer.Write((byte)Property.Layer); - buffer.Write(appearance.Layer); - } - - if (appearance.Plane != IconAppearance.Default.Plane) { - buffer.Write((byte)Property.Plane); - buffer.WriteVariableInt32(appearance.Plane); - } - - if (appearance.BlendMode != IconAppearance.Default.BlendMode) { - buffer.Write((byte)Property.BlendMode); - buffer.Write((byte)appearance.BlendMode); - } - - if (appearance.AppearanceFlags != IconAppearance.Default.AppearanceFlags) { - buffer.Write((byte)Property.AppearanceFlags); - buffer.Write((int)appearance.AppearanceFlags); - } - - if (appearance.Invisibility != IconAppearance.Default.Invisibility) { - buffer.Write((byte)Property.Invisibility); - buffer.Write(appearance.Invisibility); - } - - if (appearance.Opacity != IconAppearance.Default.Opacity) { - buffer.Write((byte)Property.Opacity); - buffer.Write(appearance.Opacity); - } - - if (appearance.Override != IconAppearance.Default.Override) { - buffer.Write((byte)Property.Override); - buffer.Write(appearance.Override); - } - - if (!string.IsNullOrWhiteSpace(appearance.RenderSource)) { - buffer.Write((byte)Property.RenderSource); - buffer.Write(appearance.RenderSource); - } - - if (!string.IsNullOrWhiteSpace(appearance.RenderTarget)) { - buffer.Write((byte)Property.RenderTarget); - buffer.Write(appearance.RenderTarget); - } - - if (appearance.MouseOpacity != IconAppearance.Default.MouseOpacity) { - buffer.Write((byte)Property.MouseOpacity); - buffer.Write((byte)appearance.MouseOpacity); - } - - if (appearance.Overlays.Count != 0) { - buffer.Write((byte)Property.Overlays); - - buffer.WriteVariableInt32(appearance.Overlays.Count); - foreach (var overlay in appearance.Overlays) { - buffer.WriteVariableInt32(overlay); - } - } - - if (appearance.Underlays.Count != 0) { - buffer.Write((byte)Property.Underlays); - - buffer.WriteVariableInt32(appearance.Underlays.Count); - foreach (var underlay in appearance.Underlays) { - buffer.WriteVariableInt32(underlay); - } - } - - if (appearance.VisContents.Count != 0) { - buffer.Write((byte)Property.VisContents); - - buffer.WriteVariableInt32(appearance.VisContents.Count); - foreach (var item in appearance.VisContents) { - buffer.Write(item); - } - } - - if (appearance.Filters.Count != 0) { - buffer.Write((byte)Property.Filters); - - buffer.Write(appearance.Filters.Count); - foreach (var filter in appearance.Filters) { - using var filterStream = new MemoryStream(); - - serializer.Serialize(filterStream, filter); - buffer.WriteVariableInt32((int)filterStream.Length); - filterStream.TryGetBuffer(out var filterBuffer); - buffer.Write(filterBuffer); - } - } - - if (appearance.Verbs.Count != 0) { - buffer.Write((byte)Property.Verbs); - - buffer.WriteVariableInt32(appearance.Verbs.Count); - foreach (var verb in appearance.Verbs) { - buffer.WriteVariableInt32(verb); - } - } - - if (!appearance.Transform.SequenceEqual(IconAppearance.Default.Transform)) { - buffer.Write((byte)Property.Transform); - - for (int i = 0; i < 6; i++) { - buffer.Write(appearance.Transform[i]); - } - } - - buffer.Write((byte)Property.End); + pair.Value.WriteToBuffer(buffer,serializer); } } } diff --git a/OpenDreamShared/Network/Messages/MsgFtp.cs b/OpenDreamShared/Network/Messages/MsgFtp.cs index 5756a26f9a..6aa36e0b6d 100644 --- a/OpenDreamShared/Network/Messages/MsgFtp.cs +++ b/OpenDreamShared/Network/Messages/MsgFtp.cs @@ -6,6 +6,7 @@ namespace OpenDreamShared.Network.Messages; public sealed class MsgFtp : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; + public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered; public int ResourceId; public string SuggestedName = string.Empty; diff --git a/OpenDreamShared/Network/Messages/MsgLink.cs b/OpenDreamShared/Network/Messages/MsgLink.cs new file mode 100644 index 0000000000..e058963acc --- /dev/null +++ b/OpenDreamShared/Network/Messages/MsgLink.cs @@ -0,0 +1,20 @@ +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace OpenDreamShared.Network.Messages; + +public sealed class MsgLink : NetMessage { + public override MsgGroups MsgGroup => MsgGroups.EntityEvent; + public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered; + + public string Url = string.Empty; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + Url = buffer.ReadString(); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { + buffer.Write(Url); + } +} diff --git a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs index d2d1ed5d5a..dfae4c5ebf 100644 --- a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs +++ b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs @@ -6,22 +6,30 @@ namespace OpenDreamShared.Rendering; public abstract class SharedAppearanceSystem : EntitySystem { + public abstract ImmutableAppearance MustGetAppearanceById(uint appearanceId); + public abstract void RemoveAppearance(ImmutableAppearance appearance); + + [Serializable, NetSerializable] + public sealed class NewAppearanceEvent(ImmutableAppearance appearance) : EntityEventArgs { + public ImmutableAppearance Appearance { get; } = appearance; + } + [Serializable, NetSerializable] - public sealed class NewAppearanceEvent(int appearanceId, IconAppearance appearance) : EntityEventArgs { - public int AppearanceId { get; } = appearanceId; - public IconAppearance Appearance { get; } = appearance; + public sealed class RemoveAppearanceEvent(uint appearanceId) : EntityEventArgs { + public uint AppearanceId { get; } = appearanceId; } [Serializable, NetSerializable] - public sealed class AnimationEvent(NetEntity entity, int targetAppearanceId, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim) + public sealed class AnimationEvent(NetEntity entity, uint targetAppearanceId, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, uint? turfId) : EntityEventArgs { public NetEntity Entity = entity; - public int TargetAppearanceId = targetAppearanceId; + public uint TargetAppearanceId = targetAppearanceId; public TimeSpan Duration = duration; public AnimationEasing Easing = easing; public int Loop = loop; public AnimationFlags Flags = flags; public int Delay = delay; public bool ChainAnim = chainAnim; + public uint? TurfId = turfId; } } diff --git a/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs b/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs index 004ffe2534..8434d22623 100644 --- a/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs +++ b/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs @@ -4,18 +4,18 @@ using Robust.Shared.GameStates; using OpenDreamShared.Dream; -namespace OpenDreamShared.Rendering { - [NetworkedComponent] - public abstract partial class SharedDMISpriteComponent : Component { - [Serializable, NetSerializable] - public sealed class DMISpriteComponentState : ComponentState { - public readonly int? AppearanceId; - public readonly ScreenLocation ScreenLocation; +namespace OpenDreamShared.Rendering; - public DMISpriteComponentState(int? appearanceId, ScreenLocation screenLocation) { - AppearanceId = appearanceId; - ScreenLocation = screenLocation; - } +[NetworkedComponent] +public abstract partial class SharedDMISpriteComponent : Component { + [Serializable, NetSerializable] + public sealed class DMISpriteComponentState : ComponentState { + public readonly uint? AppearanceId; + public readonly ScreenLocation ScreenLocation; + + public DMISpriteComponentState(uint? appearanceId, ScreenLocation screenLocation) { + AppearanceId = appearanceId; + ScreenLocation = screenLocation; } } } diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 0aa77a76c9..7d68c41e7e 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -177,6 +177,19 @@ binds: # These are all the current engine keybinds. - function: TextScrollToBottom type: State key: PageDown +- function: TextTabComplete + type: State + key: Tab +- function: TextCompleteNext + type: State + key: Down + priority: 1 + canRepeat: true +- function: TextCompletePrev + type: State + key: Up + priority: 1 + canRepeat: true - function: TextDelete type: State key: Delete diff --git a/RobustToolbox b/RobustToolbox index 32bca7cfd4..e4190f4f29 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 32bca7cfd417edcad9a60c2b1703eba8675f56af +Subproject commit e4190f4f2900634e332208a77cd6df9cef75c29a diff --git a/TestGame/code.dm b/TestGame/code.dm index c0b366a806..d1b94607d4 100644 --- a/TestGame/code.dm +++ b/TestGame/code.dm @@ -180,6 +180,20 @@ set category = "Test" usr << output("help sec griffing me", "honk.browser:foo") + verb/test_maptext() + set category = "Test" + if(length(src.maptext)) + src.maptext = null; + else + src.maptext = "Hello!" + animate(src, maptext_x=64, maptext_y=64, time=50) + animate(maptext_x=64, maptext_y=-64, time=50) + animate(maptext_x=-64, maptext_y=-64, time=50) + animate(maptext_x=-64, maptext_y=64, time=50) + animate(maptext_x=0, maptext_y=0, time=50) + animate(maptext="Hello :)", time=10) + + verb/demo_filters() set category = "Test" if(length(src.filters)) @@ -278,6 +292,54 @@ client.show_popup_menus = !client.show_popup_menus src << "Popups are now [client.show_popup_menus ? "enabled" : "disabled"]" + // input() test + verb/text_test() + set category = "Input Test" + src << input("Input test: text") as text|null + + // todo: implement + // verb/password_test() + // set category = "Input Test" + // src << input("Input test: password") as password|null + + verb/multiline_test() + set category = "Input Test" + src << input("Input test: message") as message|null + + verb/command_test() + set category = "Input Test" + src << input("Input test: command_text") as command_text|null + + verb/num_test() + set category = "Input Test" + src << input("Input test: num") as num|null + + verb/icon_test() + set category = "Input Test" + src << input("Input test: icon") as icon|null + + verb/sound_test() + set category = "Input Test" + src << input("Input test: sound") as sound|null + + verb/file_test() + set category = "Input Test" + src << input("Input test: file") as file|null + + // todo: implement + // verb/key_test() + // set category = "Input Test" + // src << input("Input test: key") as key|null + + verb/color_test() + set category = "Input Test" + src << input("Input test: color") as color|null + + verb/list_test() + set category = "Input Test" + src << input("Input test: list") as null|anything in list("option 1", "option 2", "option 3", "option 4", "option 5") + + /mob/Stat() if (statpanel("Status")) stat("tick_usage", world.tick_usage) diff --git a/TestGame/map_z1.dmm b/TestGame/map_z1.dmm index 92d34c551c..54a7a34400 100644 --- a/TestGame/map_z1.dmm +++ b/TestGame/map_z1.dmm @@ -37,8 +37,10 @@ "K" = (/obj/complex_overlay_test,/turf,/area) "L" = (/obj/float_layer_test,/turf,/area) "M" = (/mob,/turf,/area) +"N" = (/obj/plaque/animation_turf_test,/turf,/area) "O" = (/obj/plaque/animation_test,/turf,/area) "R" = (/turf/blue,/area/withicon) +"S" = (/obj/button/animation_turf_test,/turf,/area) "X" = (/turf,/area/withicon) "Z" = (/obj/button/animation_test,/turf,/area) @@ -46,8 +48,8 @@ bbbbbbbbbbbbbbbbbbbbbb bedciklwopsuaaaaaaaaab bfghjmnxqrtvaaaaaaaaab -byADFGZaaaaaaaaaaaaaab -bzBEHIOaaaaaaaaaaaaaab +byADFGZSaaaaaaaaaaaaab +bzBEHIONaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab diff --git a/TestGame/renderer_tests.dm b/TestGame/renderer_tests.dm index 45c414786a..4e806e75c0 100644 --- a/TestGame/renderer_tests.dm +++ b/TestGame/renderer_tests.dm @@ -350,9 +350,93 @@ i++; if(i>8) i = 0 + /obj/plaque/animation_test data = "

Animation Test

Click the button to apply a series of animations to your mob

" + +/obj/button/animation_turf_test + name = "Animation Turf Test" + desc = "Click me to animate the turfs around you!" + var/i = 0 + + push() + if(i==0) + //grow and fade + usr << "grow and fade" + for(var/turf/T in range(src, 2)) + animate(T, transform = matrix()*2, alpha = 0, time = 5) + animate(transform = matrix(), alpha = 255, time = 5) + sleep(5) + if(i==1) + //spin + usr << "spin" + for(var/turf/T in range(src, 2)) + animate(T, transform = turn(matrix(), 120), time = 2, loop = 5) + animate(transform = turn(matrix(), 240), time = 2) + animate(transform = null, time = 2) + sleep(14) + if(i==2) + //colour + usr << "colour" + for(var/turf/T in range(src, 2)) + animate(T, color="#ff0000", time=5) + animate(color="#00ff00", time=5) + animate(color="#0000ff", time=5) + animate(color="#ffffff", time=5) + sleep(20) + if(i==3) + //colour matrix + usr << "colour matrix" + for(var/turf/T in range(src, 2)) + animate(T, color=list(0,0,1,0, 1,0,0,0, 0,1,0,0, 0,0,0,1, 0,0,0,0), time=5) + animate(color=list(0,1,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,1, 0,0,0,0), time=5) + animate(color=list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0), time=5) + sleep(15) + if(i==4) + //parallel + usr << "parallel" + for(var/turf/T in range(src, 2)) + animate(T, color="#ff0000", time=4) + animate(T, transform = turn(matrix(), 120), time = 2, flags=ANIMATION_PARALLEL) + animate(transform = turn(matrix(), 240), time = 2) + animate(color="#ffffff", transform = null, time = 2) + sleep(6) + if(i==5) + //easings + usr << "easings" + for(var/turf/T in range(src, 2)) + animate(T, transform = matrix()*2, time = 5, easing=BACK_EASING) + animate(transform = matrix(), time = 5, easing=BOUNCE_EASING) + animate(transform = matrix()*2, time = 5, easing=ELASTIC_EASING) + animate(transform = matrix(), time = 5, easing=QUAD_EASING) + animate(transform = matrix()*2, time = 5, easing=CUBIC_EASING) + animate(transform = matrix(), time = 5, easing=SINE_EASING) + animate(transform = matrix()*2, time = 5, easing=CIRCULAR_EASING) + animate(transform = matrix(), time = 5, easing=JUMP_EASING) + if(i==6) + usr << "relative color" + for(var/turf/T in range(src, 2)) + animate(T, color="#ff0000", time=5, flags=ANIMATION_RELATIVE) + animate(color="#00ff00", time=5, flags=ANIMATION_RELATIVE) + animate(color="#0000ff", time=5, flags=ANIMATION_RELATIVE) + if(i==7) + usr << "relative transform" + for(var/turf/T in range(src, 2)) + animate(T, transform = matrix()*2, time = 5, flags=ANIMATION_RELATIVE) + animate(transform = matrix()*0.5, time = 5, flags=ANIMATION_RELATIVE) + if(i==8) + usr << "more relative tests" + for(var/turf/T in range(src, 2)) + animate(T, alpha=-125, pixel_x=16, time = 5, flags=ANIMATION_RELATIVE) + animate(alpha=125, pixel_x=-16, time = 5, flags=ANIMATION_RELATIVE) + i++; + if(i>8) + i = 0 + +/obj/plaque/animation_turf_test + data = "

Animation Turf Test

Click the button to apply a series of animations to the turfs your mob

" + //render order sanity checks /obj/order_test icon = 'icons/hanoi.dmi' diff --git a/global.json b/global.json index 3fea262b1b..66690f8397 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "9.0.0", "rollForward": "latestFeature" } }